From c98ee412c0188dadbd74dcd4f0df17037fa73e43 Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Wed, 31 Aug 2022 21:16:00 +0200 Subject: [PATCH 001/955] Catch unknown user exception in Overkiz integration (#76693) --- homeassistant/components/overkiz/config_flow.py | 3 +++ homeassistant/components/overkiz/strings.json | 3 ++- homeassistant/components/overkiz/translations/en.json | 3 ++- tests/components/overkiz/test_config_flow.py | 2 ++ 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/overkiz/config_flow.py b/homeassistant/components/overkiz/config_flow.py index 2808c309938..d3ab9722fca 100644 --- a/homeassistant/components/overkiz/config_flow.py +++ b/homeassistant/components/overkiz/config_flow.py @@ -12,6 +12,7 @@ from pyoverkiz.exceptions import ( MaintenanceException, TooManyAttemptsBannedException, TooManyRequestsException, + UnknownUserException, ) from pyoverkiz.models import obfuscate_id import voluptuous as vol @@ -83,6 +84,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "server_in_maintenance" except TooManyAttemptsBannedException: errors["base"] = "too_many_attempts" + except UnknownUserException: + errors["base"] = "unknown_user" except Exception as exception: # pylint: disable=broad-except errors["base"] = "unknown" LOGGER.exception(exception) diff --git a/homeassistant/components/overkiz/strings.json b/homeassistant/components/overkiz/strings.json index 9c64311a73e..ecc0329eb2a 100644 --- a/homeassistant/components/overkiz/strings.json +++ b/homeassistant/components/overkiz/strings.json @@ -18,7 +18,8 @@ "server_in_maintenance": "Server is down for maintenance", "too_many_attempts": "Too many attempts with an invalid token, temporarily banned", "too_many_requests": "Too many requests, try again later", - "unknown": "[%key:common::config_flow::error::unknown%]" + "unknown": "[%key:common::config_flow::error::unknown%]", + "unknown_user": "Unknown user. Somfy Protect accounts are not supported by this integration." }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", diff --git a/homeassistant/components/overkiz/translations/en.json b/homeassistant/components/overkiz/translations/en.json index 9e24a9d3cb3..9c8ad538695 100644 --- a/homeassistant/components/overkiz/translations/en.json +++ b/homeassistant/components/overkiz/translations/en.json @@ -11,7 +11,8 @@ "server_in_maintenance": "Server is down for maintenance", "too_many_attempts": "Too many attempts with an invalid token, temporarily banned", "too_many_requests": "Too many requests, try again later", - "unknown": "Unexpected error" + "unknown": "Unexpected error", + "unknown_user": "Unknown user. Somfy Protect accounts are not supported by this integration." }, "flow_title": "Gateway: {gateway_id}", "step": { diff --git a/tests/components/overkiz/test_config_flow.py b/tests/components/overkiz/test_config_flow.py index 0542f4dc9fc..940da7b39c2 100644 --- a/tests/components/overkiz/test_config_flow.py +++ b/tests/components/overkiz/test_config_flow.py @@ -9,6 +9,7 @@ from pyoverkiz.exceptions import ( MaintenanceException, TooManyAttemptsBannedException, TooManyRequestsException, + UnknownUserException, ) import pytest @@ -88,6 +89,7 @@ async def test_form(hass: HomeAssistant) -> None: (ClientError, "cannot_connect"), (MaintenanceException, "server_in_maintenance"), (TooManyAttemptsBannedException, "too_many_attempts"), + (UnknownUserException, "unknown_user"), (Exception, "unknown"), ], ) From c6b3b9fa9011dbc410326ef8ad2e5fb06d5d812d Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Wed, 31 Aug 2022 14:54:23 -0500 Subject: [PATCH 002/955] Convert life360 to aiohttp (#77508) Bump life360 package to 5.1.0. --- .../components/life360/config_flow.py | 21 ++++++++++--------- homeassistant/components/life360/const.py | 1 - .../components/life360/coordinator.py | 8 +++---- .../components/life360/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/life360/test_config_flow.py | 4 +++- 7 files changed, 20 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/life360/config_flow.py b/homeassistant/components/life360/config_flow.py index 331882aa991..5153e389d8b 100644 --- a/homeassistant/components/life360/config_flow.py +++ b/homeassistant/components/life360/config_flow.py @@ -12,10 +12,10 @@ from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from .const import ( - COMM_MAX_RETRIES, COMM_TIMEOUT, CONF_AUTHORIZATION, CONF_DRIVING_SPEED, @@ -53,13 +53,10 @@ class Life360ConfigFlow(ConfigFlow, domain=DOMAIN): """Life360 integration config flow.""" VERSION = 1 - - def __init__(self) -> None: - """Initialize.""" - self._api = Life360(timeout=COMM_TIMEOUT, max_retries=COMM_MAX_RETRIES) - self._username: str | vol.UNDEFINED = vol.UNDEFINED - self._password: str | vol.UNDEFINED = vol.UNDEFINED - self._reauth_entry: ConfigEntry | None = None + _api: Life360 | None = None + _username: str | vol.UNDEFINED = vol.UNDEFINED + _password: str | vol.UNDEFINED = vol.UNDEFINED + _reauth_entry: ConfigEntry | None = None @staticmethod @callback @@ -69,10 +66,14 @@ class Life360ConfigFlow(ConfigFlow, domain=DOMAIN): async def _async_verify(self, step_id: str) -> FlowResult: """Attempt to authorize the provided credentials.""" + if not self._api: + self._api = Life360( + session=async_get_clientsession(self.hass), timeout=COMM_TIMEOUT + ) errors: dict[str, str] = {} try: - authorization = await self.hass.async_add_executor_job( - self._api.get_authorization, self._username, self._password + authorization = await self._api.get_authorization( + self._username, self._password ) except LoginError as exc: LOGGER.debug("Login error: %s", exc) diff --git a/homeassistant/components/life360/const.py b/homeassistant/components/life360/const.py index ccaf69877d6..21bf9a89c5e 100644 --- a/homeassistant/components/life360/const.py +++ b/homeassistant/components/life360/const.py @@ -7,7 +7,6 @@ DOMAIN = "life360" LOGGER = logging.getLogger(__package__) ATTRIBUTION = "Data provided by life360.com" -COMM_MAX_RETRIES = 2 COMM_TIMEOUT = 3.05 SPEED_FACTOR_MPH = 2.25 SPEED_DIGITS = 1 diff --git a/homeassistant/components/life360/coordinator.py b/homeassistant/components/life360/coordinator.py index ed774bba8ca..a098f1f6735 100644 --- a/homeassistant/components/life360/coordinator.py +++ b/homeassistant/components/life360/coordinator.py @@ -19,12 +19,12 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util.distance import convert import homeassistant.util.dt as dt_util from .const import ( - COMM_MAX_RETRIES, COMM_TIMEOUT, CONF_AUTHORIZATION, DOMAIN, @@ -106,8 +106,8 @@ class Life360DataUpdateCoordinator(DataUpdateCoordinator[Life360Data]): ) self._hass = hass 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 @@ -115,9 +115,7 @@ class Life360DataUpdateCoordinator(DataUpdateCoordinator[Life360Data]): async def _retrieve_data(self, func: str, *args: Any) -> list[dict[str, Any]]: """Get data from Life360.""" try: - return await self._hass.async_add_executor_job( - getattr(self._api, func), *args - ) + return await getattr(self._api, func)(*args) except LoginError as exc: LOGGER.debug("Login error: %s", exc) raise ConfigEntryAuthFailed from exc diff --git a/homeassistant/components/life360/manifest.json b/homeassistant/components/life360/manifest.json index 23fdad892d2..c1a69b245ff 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==4.1.1"], + "requirements": ["life360==5.1.0"], "iot_class": "cloud_polling", "loggers": ["life360"] } diff --git a/requirements_all.txt b/requirements_all.txt index ffbff8840a0..175f5a88e82 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -980,7 +980,7 @@ librouteros==3.2.0 libsoundtouch==0.8 # homeassistant.components.life360 -life360==4.1.1 +life360==5.1.0 # homeassistant.components.osramlightify lightify==1.0.7.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bc79492c7a2..b8472bbafbb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -715,7 +715,7 @@ librouteros==3.2.0 libsoundtouch==0.8 # homeassistant.components.life360 -life360==4.1.1 +life360==5.1.0 # homeassistant.components.logi_circle logi_circle==0.2.3 diff --git a/tests/components/life360/test_config_flow.py b/tests/components/life360/test_config_flow.py index 02d5539e117..b614549fc04 100644 --- a/tests/components/life360/test_config_flow.py +++ b/tests/components/life360/test_config_flow.py @@ -76,7 +76,9 @@ def life360_fixture(): @pytest.fixture def life360_api(): """Mock Life360 api.""" - with patch("homeassistant.components.life360.config_flow.Life360") as mock: + with patch( + "homeassistant.components.life360.config_flow.Life360", autospec=True + ) as mock: yield mock.return_value From 471878b5faa22733223e7c88cb57d38b5a2f5480 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 31 Aug 2022 22:05:31 +0200 Subject: [PATCH 003/955] Adjust temperature_unit in hisense_aehw4a1 (#77585) --- .../components/hisense_aehw4a1/climate.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/hisense_aehw4a1/climate.py b/homeassistant/components/hisense_aehw4a1/climate.py index 9612fd74f0b..a5d8dc800af 100644 --- a/homeassistant/components/hisense_aehw4a1/climate.py +++ b/homeassistant/components/hisense_aehw4a1/climate.py @@ -160,7 +160,6 @@ class ClimateAehW4a1(ClimateEntity): self._preset_modes = PRESET_MODES self._available = None self._on = None - self._temperature_unit = None self._current_temperature = None self._target_temperature = None self._attr_hvac_mode = None @@ -185,9 +184,9 @@ class ClimateAehW4a1(ClimateEntity): self._on = status["run_status"] if status["temperature_Fahrenheit"] == "0": - self._temperature_unit = TEMP_CELSIUS + self._attr_temperature_unit = TEMP_CELSIUS else: - self._temperature_unit = TEMP_FAHRENHEIT + self._attr_temperature_unit = TEMP_FAHRENHEIT self._current_temperature = int(status["indoor_temperature_status"], 2) @@ -237,11 +236,6 @@ class ClimateAehW4a1(ClimateEntity): """Return the name of the climate device.""" return self._unique_id - @property - def temperature_unit(self): - """Return the unit of measurement.""" - return self._temperature_unit - @property def current_temperature(self): """Return the current temperature.""" @@ -285,14 +279,14 @@ class ClimateAehW4a1(ClimateEntity): @property def min_temp(self): """Return the minimum temperature.""" - if self._temperature_unit == TEMP_CELSIUS: + if self.temperature_unit == TEMP_CELSIUS: return MIN_TEMP_C return MIN_TEMP_F @property def max_temp(self): """Return the maximum temperature.""" - if self._temperature_unit == TEMP_CELSIUS: + if self.temperature_unit == TEMP_CELSIUS: return MAX_TEMP_C return MAX_TEMP_F @@ -312,7 +306,7 @@ class ClimateAehW4a1(ClimateEntity): _LOGGER.debug("Setting temp of %s to %s", self._unique_id, temp) if self._preset_mode != PRESET_NONE: await self.async_set_preset_mode(PRESET_NONE) - if self._temperature_unit == TEMP_CELSIUS: + if self.temperature_unit == TEMP_CELSIUS: await self._device.command(f"temp_{int(temp)}_C") else: await self._device.command(f"temp_{int(temp)}_F") From a40c1401d3ba6fa7b3ad8f30e41f409f8262ded4 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 31 Aug 2022 22:06:14 +0200 Subject: [PATCH 004/955] Adjust temperature_unit in heatmiser (#77584) --- homeassistant/components/heatmiser/climate.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/homeassistant/components/heatmiser/climate.py b/homeassistant/components/heatmiser/climate.py index 942bb673cf9..b3bcd451a1c 100644 --- a/homeassistant/components/heatmiser/climate.py +++ b/homeassistant/components/heatmiser/climate.py @@ -84,18 +84,12 @@ class HeatmiserV3Thermostat(ClimateEntity): self._id = device self.dcb = None self._attr_hvac_mode = HVACMode.HEAT - self._temperature_unit = None @property def name(self): """Return the name of the thermostat, if any.""" return self._name - @property - def temperature_unit(self): - """Return the unit of measurement which this thermostat uses.""" - return self._temperature_unit - @property def current_temperature(self): """Return the current temperature.""" @@ -119,7 +113,7 @@ class HeatmiserV3Thermostat(ClimateEntity): _LOGGER.error("Failed to update device %s", self._name) return self.dcb = self.therm.read_dcb() - self._temperature_unit = ( + self._attr_temperature_unit = ( TEMP_CELSIUS if (self.therm.get_temperature_format() == "C") else TEMP_FAHRENHEIT From 448f4ee755479896e8980530ca919973eac56beb Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 31 Aug 2022 22:10:18 +0200 Subject: [PATCH 005/955] Improve entity type hints [j-k] (#77594) --- homeassistant/components/juicenet/switch.py | 6 ++-- homeassistant/components/kaiterra/sensor.py | 4 +-- homeassistant/components/kankun/switch.py | 7 ++-- .../keenetic_ndms2/binary_sensor.py | 2 +- .../keenetic_ndms2/device_tracker.py | 4 +-- homeassistant/components/kef/media_player.py | 28 +++++++-------- homeassistant/components/kira/remote.py | 2 +- homeassistant/components/kmtronic/switch.py | 7 ++-- homeassistant/components/kodi/media_player.py | 36 +++++++++---------- .../components/konnected/binary_sensor.py | 2 +- homeassistant/components/konnected/sensor.py | 2 +- homeassistant/components/konnected/switch.py | 9 ++--- .../components/kostal_plenticore/switch.py | 5 +-- 13 files changed, 60 insertions(+), 54 deletions(-) diff --git a/homeassistant/components/juicenet/switch.py b/homeassistant/components/juicenet/switch.py index c7e2f499e04..576c66c0841 100644 --- a/homeassistant/components/juicenet/switch.py +++ b/homeassistant/components/juicenet/switch.py @@ -1,4 +1,6 @@ """Support for monitoring juicenet/juicepoint/juicebox based EVSE switches.""" +from typing import Any + from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -41,10 +43,10 @@ class JuiceNetChargeNowSwitch(JuiceNetDevice, SwitchEntity): """Return true if switch is on.""" return self.device.override_time != 0 - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Charge now.""" await self.device.set_override(True) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Don't charge now.""" await self.device.set_override(False) diff --git a/homeassistant/components/kaiterra/sensor.py b/homeassistant/components/kaiterra/sensor.py index 664c29110a1..29013052c1c 100644 --- a/homeassistant/components/kaiterra/sensor.py +++ b/homeassistant/components/kaiterra/sensor.py @@ -87,7 +87,7 @@ class KaiterraSensor(SensorEntity): ) @property - def available(self): + def available(self) -> bool: """Return the availability of the sensor.""" return self._api.data.get(self._device_id) is not None @@ -110,7 +110,7 @@ class KaiterraSensor(SensorEntity): return TEMP_CELSIUS return value - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callback.""" self.async_on_remove( async_dispatcher_connect( diff --git a/homeassistant/components/kankun/switch.py b/homeassistant/components/kankun/switch.py index 69a3dcee301..f64b11706a1 100644 --- a/homeassistant/components/kankun/switch.py +++ b/homeassistant/components/kankun/switch.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any import requests import voluptuous as vol @@ -114,16 +115,16 @@ class KankunSwitch(SwitchEntity): """Return true if device is on.""" return self._state - def update(self): + def update(self) -> None: """Update device state.""" self._state = self._query_state() - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" if self._switch("on"): self._state = True - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" if self._switch("off"): self._state = False diff --git a/homeassistant/components/keenetic_ndms2/binary_sensor.py b/homeassistant/components/keenetic_ndms2/binary_sensor.py index 5b8fa952e18..fa9b1fd48dd 100644 --- a/homeassistant/components/keenetic_ndms2/binary_sensor.py +++ b/homeassistant/components/keenetic_ndms2/binary_sensor.py @@ -53,7 +53,7 @@ class RouterOnlineBinarySensor(BinarySensorEntity): """Return a client description for device registry.""" return self._router.device_info - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Client entity created.""" self.async_on_remove( async_dispatcher_connect( diff --git a/homeassistant/components/keenetic_ndms2/device_tracker.py b/homeassistant/components/keenetic_ndms2/device_tracker.py index 116f82afe3a..ca51b9ba4aa 100644 --- a/homeassistant/components/keenetic_ndms2/device_tracker.py +++ b/homeassistant/components/keenetic_ndms2/device_tracker.py @@ -144,12 +144,12 @@ class KeeneticTracker(ScannerEntity): } return None - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Client entity created.""" _LOGGER.debug("New network device tracker %s (%s)", self.name, self.unique_id) @callback - def update_device(): + def update_device() -> None: _LOGGER.debug( "Updating Keenetic tracked device %s (%s)", self.entity_id, diff --git a/homeassistant/components/kef/media_player.py b/homeassistant/components/kef/media_player.py index 9ed669c3201..0ad56f92725 100644 --- a/homeassistant/components/kef/media_player.py +++ b/homeassistant/components/kef/media_player.py @@ -253,7 +253,7 @@ class KefMediaPlayer(MediaPlayerEntity): """Return the state of the device.""" return self._state - async def async_update(self): + async def async_update(self) -> None: """Update latest state.""" _LOGGER.debug("Running async_update") try: @@ -313,55 +313,55 @@ class KefMediaPlayer(MediaPlayerEntity): """Return the device's icon.""" return "mdi:speaker-wireless" - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Turn the media player off.""" await self._speaker.turn_off() - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Turn the media player on.""" if not self._supports_on: raise NotImplementedError() await self._speaker.turn_on() - async def async_volume_up(self): + async def async_volume_up(self) -> None: """Volume up the media player.""" await self._speaker.increase_volume() - async def async_volume_down(self): + async def async_volume_down(self) -> None: """Volume down the media player.""" await self._speaker.decrease_volume() - async def async_set_volume_level(self, volume): + async def async_set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" await self._speaker.set_volume(volume) - async def async_mute_volume(self, mute): + async def async_mute_volume(self, mute: bool) -> None: """Mute (True) or unmute (False) media player.""" if mute: await self._speaker.mute() else: await self._speaker.unmute() - async def async_select_source(self, source: str): + async def async_select_source(self, source: str) -> None: """Select input source.""" if source in self.source_list: await self._speaker.set_source(source) else: raise ValueError(f"Unknown input source: {source}.") - async def async_media_play(self): + async def async_media_play(self) -> None: """Send play command.""" await self._speaker.set_play_pause() - async def async_media_pause(self): + async def async_media_pause(self) -> None: """Send pause command.""" await self._speaker.set_play_pause() - async def async_media_previous_track(self): + async def async_media_previous_track(self) -> None: """Send previous track command.""" await self._speaker.prev_track() - async def async_media_next_track(self): + async def async_media_next_track(self) -> None: """Send next track command.""" await self._speaker.next_track() @@ -382,13 +382,13 @@ class KefMediaPlayer(MediaPlayerEntity): **mode._asdict(), ) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Subscribe to DSP updates.""" self._update_dsp_task_remover = async_track_time_interval( self.hass, self.update_dsp, DSP_SCAN_INTERVAL ) - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Unsubscribe to DSP updates.""" self._update_dsp_task_remover() self._update_dsp_task_remover = None diff --git a/homeassistant/components/kira/remote.py b/homeassistant/components/kira/remote.py index 37be3792aa9..f728ffa3d62 100644 --- a/homeassistant/components/kira/remote.py +++ b/homeassistant/components/kira/remote.py @@ -45,7 +45,7 @@ class KiraRemote(Entity): """Return the Kira device's name.""" return self._name - def update(self): + def update(self) -> None: """No-op.""" def send_command(self, command, **kwargs): diff --git a/homeassistant/components/kmtronic/switch.py b/homeassistant/components/kmtronic/switch.py index e941a2ffafa..860e5bf832e 100644 --- a/homeassistant/components/kmtronic/switch.py +++ b/homeassistant/components/kmtronic/switch.py @@ -1,4 +1,5 @@ """KMtronic Switch integration.""" +from typing import Any import urllib.parse from homeassistant.components.switch import SwitchEntity @@ -54,7 +55,7 @@ class KMtronicSwitch(CoordinatorEntity, SwitchEntity): return not self._relay.is_energised return self._relay.is_energised - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" if self._reverse: await self._relay.de_energise() @@ -62,7 +63,7 @@ class KMtronicSwitch(CoordinatorEntity, SwitchEntity): await self._relay.energise() self.async_write_ha_state() - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" if self._reverse: await self._relay.energise() @@ -70,7 +71,7 @@ class KMtronicSwitch(CoordinatorEntity, SwitchEntity): await self._relay.de_energise() self.async_write_ha_state() - async def async_toggle(self, **kwargs) -> None: + async def async_toggle(self, **kwargs: Any) -> None: """Toggle the switch.""" await self._relay.toggle() self.async_write_ha_state() diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index 2b509ed0e08..f4074678825 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -393,7 +393,7 @@ class KodiEntity(MediaPlayerEntity): return STATE_PLAYING - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Connect the websocket if needed.""" if not self._connection.can_subscribe: return @@ -481,7 +481,7 @@ class KodiEntity(MediaPlayerEntity): self._connection.server.System.OnSleep = self.async_on_quit @cmd - async def async_update(self): + async def async_update(self) -> None: """Retrieve latest state.""" if not self._connection.connected: self._reset_state() @@ -526,7 +526,7 @@ class KodiEntity(MediaPlayerEntity): self._reset_state([]) @property - def should_poll(self): + def should_poll(self) -> bool: """Return True if entity has to be polled for state.""" return not self._connection.can_subscribe @@ -636,68 +636,68 @@ class KodiEntity(MediaPlayerEntity): return None - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Turn the media player on.""" _LOGGER.debug("Firing event to turn on device") self.hass.bus.async_fire(EVENT_TURN_ON, {ATTR_ENTITY_ID: self.entity_id}) - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Turn the media player off.""" _LOGGER.debug("Firing event to turn off device") self.hass.bus.async_fire(EVENT_TURN_OFF, {ATTR_ENTITY_ID: self.entity_id}) @cmd - async def async_volume_up(self): + async def async_volume_up(self) -> None: """Volume up the media player.""" await self._kodi.volume_up() @cmd - async def async_volume_down(self): + async def async_volume_down(self) -> None: """Volume down the media player.""" await self._kodi.volume_down() @cmd - async def async_set_volume_level(self, volume): + async def async_set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" await self._kodi.set_volume_level(int(volume * 100)) @cmd - async def async_mute_volume(self, mute): + async def async_mute_volume(self, mute: bool) -> None: """Mute (true) or unmute (false) media player.""" await self._kodi.mute(mute) @cmd - async def async_media_play_pause(self): + async def async_media_play_pause(self) -> None: """Pause media on media player.""" await self._kodi.play_pause() @cmd - async def async_media_play(self): + async def async_media_play(self) -> None: """Play media.""" await self._kodi.play() @cmd - async def async_media_pause(self): + async def async_media_pause(self) -> None: """Pause the media player.""" await self._kodi.pause() @cmd - async def async_media_stop(self): + async def async_media_stop(self) -> None: """Stop the media player.""" await self._kodi.stop() @cmd - async def async_media_next_track(self): + async def async_media_next_track(self) -> None: """Send next track command.""" await self._kodi.next_track() @cmd - async def async_media_previous_track(self): + async def async_media_previous_track(self) -> None: """Send next track command.""" await self._kodi.previous_track() @cmd - async def async_media_seek(self, position): + async def async_media_seek(self, position: float) -> None: """Send seek command.""" await self._kodi.media_seek(position) @@ -746,7 +746,7 @@ class KodiEntity(MediaPlayerEntity): await self._kodi.play_file(media_id) @cmd - async def async_set_shuffle(self, shuffle): + async def async_set_shuffle(self, shuffle: bool) -> None: """Set shuffle mode, for the first player.""" if self._no_active_players: raise RuntimeError("Error: No active player.") @@ -790,7 +790,7 @@ class KodiEntity(MediaPlayerEntity): ) return result - async def async_clear_playlist(self): + async def async_clear_playlist(self) -> None: """Clear default playlist (i.e. playlistid=0).""" await self._kodi.clear_playlist() diff --git a/homeassistant/components/konnected/binary_sensor.py b/homeassistant/components/konnected/binary_sensor.py index e1823c1c7d9..a4ceed5c50d 100644 --- a/homeassistant/components/konnected/binary_sensor.py +++ b/homeassistant/components/konnected/binary_sensor.py @@ -76,7 +76,7 @@ class KonnectedBinarySensor(BinarySensorEntity): identifiers={(KONNECTED_DOMAIN, self._device_id)}, ) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Store entity_id and register state change callback.""" self._data[ATTR_ENTITY_ID] = self.entity_id self.async_on_remove( diff --git a/homeassistant/components/konnected/sensor.py b/homeassistant/components/konnected/sensor.py index 3bd3a05c609..7bfa1fad446 100644 --- a/homeassistant/components/konnected/sensor.py +++ b/homeassistant/components/konnected/sensor.py @@ -127,7 +127,7 @@ class KonnectedSensor(SensorEntity): """Return the state of the sensor.""" return self._state - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Store entity_id and register state change callback.""" entity_id_key = self._addr or self.entity_description.key self._data[entity_id_key] = self.entity_id diff --git a/homeassistant/components/konnected/switch.py b/homeassistant/components/konnected/switch.py index 123c5b94ab4..c5a0ca712e5 100644 --- a/homeassistant/components/konnected/switch.py +++ b/homeassistant/components/konnected/switch.py @@ -1,5 +1,6 @@ """Support for wired switches attached to a Konnected device.""" import logging +from typing import Any from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry @@ -89,11 +90,11 @@ class KonnectedSwitch(SwitchEntity): return DeviceInfo(identifiers={(KONNECTED_DOMAIN, self._device_id)}) @property - def available(self): + def available(self) -> bool: """Return whether the panel is available.""" return self.panel.available - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Send a command to turn on the switch.""" resp = await self.panel.update_switch( self._zone_num, @@ -110,7 +111,7 @@ class KonnectedSwitch(SwitchEntity): # Immediately set the state back off for momentary switches self._set_state(False) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Send a command to turn off the switch.""" resp = await self.panel.update_switch( self._zone_num, int(self._activation == STATE_LOW) @@ -142,7 +143,7 @@ class KonnectedSwitch(SwitchEntity): """Update the switch state.""" self._set_state(state) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Store entity_id and register state change callback.""" self._data["entity_id"] = self.entity_id self.async_on_remove( diff --git a/homeassistant/components/kostal_plenticore/switch.py b/homeassistant/components/kostal_plenticore/switch.py index 01ef16069ab..178b588e4c6 100644 --- a/homeassistant/components/kostal_plenticore/switch.py +++ b/homeassistant/components/kostal_plenticore/switch.py @@ -4,6 +4,7 @@ from __future__ import annotations from abc import ABC from datetime import timedelta import logging +from typing import Any from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry @@ -126,7 +127,7 @@ class PlenticoreDataSwitch(CoordinatorEntity, SwitchEntity, ABC): self.coordinator.stop_fetch_data(self.module_id, self.data_id) await super().async_will_remove_from_hass() - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Turn device on.""" if await self.coordinator.async_write_data( self.module_id, {self.data_id: self.on_value} @@ -134,7 +135,7 @@ class PlenticoreDataSwitch(CoordinatorEntity, SwitchEntity, ABC): self.coordinator.name = f"{self.platform_name} {self._name} {self.on_label}" await self.coordinator.async_request_refresh() - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn device off.""" if await self.coordinator.async_write_data( self.module_id, {self.data_id: self.off_value} From 152022aef3abe37021899344a873a9533fa58b04 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 31 Aug 2022 22:16:21 +0200 Subject: [PATCH 006/955] Improve type hints in home_connect (#77587) --- homeassistant/components/home_connect/binary_sensor.py | 2 +- homeassistant/components/home_connect/light.py | 8 +++++--- homeassistant/components/home_connect/sensor.py | 4 ++-- homeassistant/components/home_connect/switch.py | 5 ----- 4 files changed, 8 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/home_connect/binary_sensor.py b/homeassistant/components/home_connect/binary_sensor.py index c00e8303b66..93b90cbfbd3 100644 --- a/homeassistant/components/home_connect/binary_sensor.py +++ b/homeassistant/components/home_connect/binary_sensor.py @@ -68,7 +68,7 @@ class HomeConnectBinarySensor(HomeConnectEntity, BinarySensorEntity): return bool(self._state) @property - def available(self): + def available(self) -> bool: """Return true if the binary sensor is available.""" return self._state is not None diff --git a/homeassistant/components/home_connect/light.py b/homeassistant/components/home_connect/light.py index c7418060eaf..17dc842358f 100644 --- a/homeassistant/components/home_connect/light.py +++ b/homeassistant/components/home_connect/light.py @@ -93,7 +93,7 @@ class HomeConnectLight(HomeConnectEntity, LightEntity): """Return the color property.""" return self._hs_color - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Switch the light on, change brightness, change color.""" if self._ambient: _LOGGER.debug("Switching ambient light on for: %s", self.name) @@ -121,7 +121,9 @@ class HomeConnectLight(HomeConnectEntity, LightEntity): hs_color = kwargs.get(ATTR_HS_COLOR, self._hs_color) if hs_color is not None: - rgb = color_util.color_hsv_to_RGB(*hs_color, brightness) + rgb = color_util.color_hsv_to_RGB( + hs_color[0], hs_color[1], brightness + ) hex_val = color_util.color_rgb_to_hex(rgb[0], rgb[1], rgb[2]) try: await self.hass.async_add_executor_job( @@ -165,7 +167,7 @@ class HomeConnectLight(HomeConnectEntity, LightEntity): _LOGGER.error("Error while trying to turn off light: %s", err) self.async_entity_update() - async def async_update(self): + async def async_update(self) -> None: """Update the light's status.""" if self.device.appliance.status.get(self._key, {}).get(ATTR_VALUE) is True: self._state = True diff --git a/homeassistant/components/home_connect/sensor.py b/homeassistant/components/home_connect/sensor.py index de409484f1e..38a45ccf709 100644 --- a/homeassistant/components/home_connect/sensor.py +++ b/homeassistant/components/home_connect/sensor.py @@ -49,11 +49,11 @@ class HomeConnectSensor(HomeConnectEntity, SensorEntity): @property def native_value(self): - """Return true if the binary sensor is on.""" + """Return sensor value.""" return self._state @property - def available(self): + def available(self) -> bool: """Return true if the sensor is available.""" return self._state is not None diff --git a/homeassistant/components/home_connect/switch.py b/homeassistant/components/home_connect/switch.py index 2278c2b1f2d..89b1f23589f 100644 --- a/homeassistant/components/home_connect/switch.py +++ b/homeassistant/components/home_connect/switch.py @@ -60,11 +60,6 @@ class HomeConnectProgramSwitch(HomeConnectEntity, SwitchEntity): """Return true if the switch is on.""" return bool(self._state) - @property - def available(self): - """Return true if the entity is available.""" - return True - async def async_turn_on(self, **kwargs: Any) -> None: """Start the program.""" _LOGGER.debug("Tried to turn on program %s", self.program_name) From c15faa161b91884570967a8348dd9db5a8fca243 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 31 Aug 2022 22:18:39 +0200 Subject: [PATCH 007/955] Improve type hints in heatmiser (#77592) --- homeassistant/components/heatmiser/climate.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/heatmiser/climate.py b/homeassistant/components/heatmiser/climate.py index b3bcd451a1c..bae65107f55 100644 --- a/homeassistant/components/heatmiser/climate.py +++ b/homeassistant/components/heatmiser/climate.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from heatmiserV3 import connection, heatmiser import voluptuous as vol @@ -100,9 +101,10 @@ class HeatmiserV3Thermostat(ClimateEntity): """Return the temperature we try to reach.""" return self._target_temperature - def set_temperature(self, **kwargs): + def set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" - temperature = kwargs.get(ATTR_TEMPERATURE) + if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: + return self._target_temperature = int(temperature) self.therm.set_target_temp(self._target_temperature) From 86c7f0bbacc59d00219b7f046150bd1bbf8da760 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 31 Aug 2022 22:19:44 +0200 Subject: [PATCH 008/955] Improve type hints in hive (#77586) --- homeassistant/components/hive/climate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hive/climate.py b/homeassistant/components/hive/climate.py index b6f4f8270b4..8e33dda244a 100644 --- a/homeassistant/components/hive/climate.py +++ b/homeassistant/components/hive/climate.py @@ -128,12 +128,12 @@ class HiveClimateEntity(HiveEntity, ClimateEntity): await self.hive.heating.setTargetTemperature(self.device, new_temperature) @refresh_system - async def async_set_preset_mode(self, preset_mode): + async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" if preset_mode == PRESET_NONE and self.preset_mode == PRESET_BOOST: await self.hive.heating.setBoostOff(self.device) elif preset_mode == PRESET_BOOST: - curtemp = round(self.current_temperature * 2) / 2 + curtemp = round((self.current_temperature or 0) * 2) / 2 temperature = curtemp + 0.5 await self.hive.heating.setBoostOn(self.device, 30, temperature) From cf41dc639bd7cee65049aefcdb667bd57fbee3a1 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 31 Aug 2022 22:22:07 +0200 Subject: [PATCH 009/955] Adjust Available in hisense_aehw4a1 (#77590) --- homeassistant/components/hisense_aehw4a1/climate.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/hisense_aehw4a1/climate.py b/homeassistant/components/hisense_aehw4a1/climate.py index a5d8dc800af..6f6ce2f2366 100644 --- a/homeassistant/components/hisense_aehw4a1/climate.py +++ b/homeassistant/components/hisense_aehw4a1/climate.py @@ -158,7 +158,7 @@ class ClimateAehW4a1(ClimateEntity): self._fan_modes = FAN_MODES self._swing_modes = SWING_MODES self._preset_modes = PRESET_MODES - self._available = None + self._attr_available = False self._on = None self._current_temperature = None self._target_temperature = None @@ -176,10 +176,10 @@ class ClimateAehW4a1(ClimateEntity): _LOGGER.warning( "Unexpected error of %s: %s", self._unique_id, library_error ) - self._available = False + self._attr_available = False return - self._available = True + self._attr_available = True self._on = status["run_status"] @@ -226,11 +226,6 @@ class ClimateAehW4a1(ClimateEntity): self._target_temperature = None self._preset_mode = None - @property - def available(self): - """Return True if entity is available.""" - return self._available - @property def name(self): """Return the name of the climate device.""" From de9f22f308754b9b74370d02a5c7cd4ff6948d1d Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Wed, 31 Aug 2022 16:35:58 -0400 Subject: [PATCH 010/955] Bump version of pyunifiprotect to 4.2.0 (#77618) --- 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 a01706337e0..5958da8f00e 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Protect", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifiprotect", - "requirements": ["pyunifiprotect==4.1.9", "unifi-discovery==1.1.6"], + "requirements": ["pyunifiprotect==4.2.0", "unifi-discovery==1.1.6"], "dependencies": ["http"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index 175f5a88e82..b8010dd39cd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2037,7 +2037,7 @@ pytrafikverket==0.2.0.1 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.1.9 +pyunifiprotect==4.2.0 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b8472bbafbb..301058387cc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1400,7 +1400,7 @@ pytrafikverket==0.2.0.1 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.1.9 +pyunifiprotect==4.2.0 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 From a9fe9857bde9f878e91d39607a7223d90b4e3b61 Mon Sep 17 00:00:00 2001 From: y34hbuddy <47507530+y34hbuddy@users.noreply.github.com> Date: Wed, 31 Aug 2022 16:37:38 -0400 Subject: [PATCH 011/955] Implement reauth flow for volvooncall (#77328) * implement reauth flow * added more tests * implement feedback for __init__.py * implemented feedback * remove impossible return checks * raise UpdateFailed when update fails --- .../components/volvooncall/__init__.py | 18 +++--- .../components/volvooncall/config_flow.py | 62 ++++++++++++++----- .../components/volvooncall/strings.json | 3 +- .../volvooncall/translations/en.json | 7 ++- .../volvooncall/test_config_flow.py | 53 ++++++++++++++++ 5 files changed, 116 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/volvooncall/__init__.py b/homeassistant/components/volvooncall/__init__.py index 832a0460903..65e3b4b0c4e 100644 --- a/homeassistant/components/volvooncall/__init__.py +++ b/homeassistant/components/volvooncall/__init__.py @@ -127,10 +127,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass, volvo_data ) - try: - await coordinator.async_config_entry_first_refresh() - except ConfigEntryAuthFailed: - return False + await coordinator.async_config_entry_first_refresh() await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @@ -199,15 +196,17 @@ class VolvoData: async def update(self): """Update status from the online service.""" - if not await self.connection.update(journal=True): - return False + try: + await self.connection.update(journal=True) + except ClientResponseError as ex: + if ex.status == 401: + raise ConfigEntryAuthFailed(ex) from ex + raise UpdateFailed(ex) from ex for vehicle in self.connection.vehicles: if vehicle.vin not in self.vehicles: self.discover_vehicle(vehicle) - return True - async def auth_is_valid(self): """Check if provided username/password/region authenticate.""" try: @@ -235,8 +234,7 @@ class VolvoUpdateCoordinator(DataUpdateCoordinator): """Fetch data from API endpoint.""" async with async_timeout.timeout(10): - if not await self.volvo_data.update(): - raise UpdateFailed("Error communicating with API") + await self.volvo_data.update() class VolvoEntity(CoordinatorEntity): diff --git a/homeassistant/components/volvooncall/config_flow.py b/homeassistant/components/volvooncall/config_flow.py index 5a0756b5725..2ed3404ae94 100644 --- a/homeassistant/components/volvooncall/config_flow.py +++ b/homeassistant/components/volvooncall/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Volvo On Call integration.""" from __future__ import annotations +from collections.abc import Mapping import logging from typing import Any @@ -11,7 +12,6 @@ from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_USERNAME from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession -import homeassistant.helpers.config_validation as cv from . import VolvoData from .const import CONF_MUTABLE, CONF_SCANDINAVIAN_MILES, DOMAIN @@ -19,32 +19,31 @@ from .errors import InvalidAuth _LOGGER = logging.getLogger(__name__) -USER_SCHEMA = vol.Schema( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_REGION, default=None): vol.In( - {"na": "North America", "cn": "China", None: "Rest of world"} - ), - vol.Optional(CONF_MUTABLE, default=True): cv.boolean, - vol.Optional(CONF_SCANDINAVIAN_MILES, default=False): cv.boolean, - }, -) - class VolvoOnCallConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """VolvoOnCall config flow.""" VERSION = 1 + _reauth_entry: config_entries.ConfigEntry | None = None async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle user step.""" errors = {} + defaults = { + CONF_USERNAME: "", + CONF_PASSWORD: "", + CONF_REGION: None, + CONF_MUTABLE: True, + CONF_SCANDINAVIAN_MILES: False, + } + 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: + self._abort_if_unique_id_configured() try: await self.is_valid(user_input) @@ -54,18 +53,51 @@ class VolvoOnCallConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): _LOGGER.exception("Unhandled exception in user step") errors["base"] = "unknown" if not errors: + if self._reauth_entry: + self.hass.config_entries.async_update_entry( + self._reauth_entry, data=self._reauth_entry.data | user_input + ) + await self.hass.config_entries.async_reload( + self._reauth_entry.entry_id + ) + return self.async_abort(reason="reauth_successful") + return self.async_create_entry( title=user_input[CONF_USERNAME], data=user_input ) + elif self._reauth_entry: + for key in defaults: + defaults[key] = self._reauth_entry.data.get(key) + + user_schema = vol.Schema( + { + vol.Required(CONF_USERNAME, default=defaults[CONF_USERNAME]): str, + vol.Required(CONF_PASSWORD, default=defaults[CONF_PASSWORD]): str, + vol.Required(CONF_REGION, default=defaults[CONF_REGION]): vol.In( + {"na": "North America", "cn": "China", None: "Rest of world"} + ), + vol.Optional(CONF_MUTABLE, default=defaults[CONF_MUTABLE]): bool, + vol.Optional( + CONF_SCANDINAVIAN_MILES, default=defaults[CONF_SCANDINAVIAN_MILES] + ): bool, + }, + ) return self.async_show_form( - step_id="user", data_schema=USER_SCHEMA, errors=errors + step_id="user", data_schema=user_schema, errors=errors ) async def async_step_import(self, import_data) -> FlowResult: """Import volvooncall config from configuration.yaml.""" return await self.async_step_user(import_data) + async def async_step_reauth(self, user_input: Mapping[str, Any]) -> FlowResult: + """Perform reauth upon an API authentication error.""" + self._reauth_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + return await self.async_step_user() + async def is_valid(self, user_input): """Check for user input errors.""" diff --git a/homeassistant/components/volvooncall/strings.json b/homeassistant/components/volvooncall/strings.json index 1982b3c353c..a0504094b4d 100644 --- a/homeassistant/components/volvooncall/strings.json +++ b/homeassistant/components/volvooncall/strings.json @@ -16,7 +16,8 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { - "already_configured": "Account is already configured" + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } }, "issues": { diff --git a/homeassistant/components/volvooncall/translations/en.json b/homeassistant/components/volvooncall/translations/en.json index aeabdbc8d45..dbbd1ebf3c6 100644 --- a/homeassistant/components/volvooncall/translations/en.json +++ b/homeassistant/components/volvooncall/translations/en.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Account is already configured" + "reauth_successful": "You have successfully re-authenticated to Volvo On Call", + "cant_reauth": "Could not find original configuration that needs reauthentication" }, "error": { "invalid_auth": "Invalid authentication", @@ -16,6 +17,10 @@ "scandinavian_miles": "Use Scandinavian Miles", "username": "Username" } + }, + "reauth_confirm": { + "title": "Re-authentication Required", + "description": "The Volvo On Call integration needs to re-authenticate your account" } } }, diff --git a/tests/components/volvooncall/test_config_flow.py b/tests/components/volvooncall/test_config_flow.py index 3f64b052824..b558bb7843f 100644 --- a/tests/components/volvooncall/test_config_flow.py +++ b/tests/components/volvooncall/test_config_flow.py @@ -167,3 +167,56 @@ async def test_import(hass: HomeAssistant) -> None: "scandinavian_miles": False, } assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_reauth(hass: HomeAssistant) -> None: + """Test that we handle the reauth flow.""" + + first_entry = MockConfigEntry( + domain=DOMAIN, + unique_id="test-username", + data={ + "username": "test-username", + "password": "test-password", + "region": "na", + "mutable": True, + "scandinavian_miles": False, + }, + ) + first_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": first_entry.entry_id, + }, + ) + + # the first form is just the confirmation prompt + assert result["type"] == FlowResultType.FORM + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + await hass.async_block_till_done() + + # the second form is the user flow where reauth happens + assert result2["type"] == FlowResultType.FORM + + with patch("volvooncall.Connection.get"): + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + "username": "test-username", + "password": "test-new-password", + "region": "na", + "mutable": True, + "scandinavian_miles": False, + }, + ) + await hass.async_block_till_done() + + assert result3["type"] == FlowResultType.ABORT + assert result3["reason"] == "reauth_successful" From ae3dca11febf40f898628088b2cb667775a830f3 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Thu, 1 Sep 2022 01:18:34 +0200 Subject: [PATCH 012/955] Update xknx to 1.0.2 (#77627) --- homeassistant/components/knx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index bb5599939db..c0aa6c3941c 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -3,7 +3,7 @@ "name": "KNX", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/knx", - "requirements": ["xknx==1.0.1"], + "requirements": ["xknx==1.0.2"], "codeowners": ["@Julius2342", "@farmio", "@marvin-w"], "quality_scale": "platinum", "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index b8010dd39cd..9d0a2378aa1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2522,7 +2522,7 @@ xboxapi==2.0.1 xiaomi-ble==0.9.0 # homeassistant.components.knx -xknx==1.0.1 +xknx==1.0.2 # homeassistant.components.bluesound # homeassistant.components.fritz diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 301058387cc..0ea12512282 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1729,7 +1729,7 @@ xbox-webapi==2.0.11 xiaomi-ble==0.9.0 # homeassistant.components.knx -xknx==1.0.1 +xknx==1.0.2 # homeassistant.components.bluesound # homeassistant.components.fritz From f81cdf4bcaa3d036e8bfdccb2ebfda9c0f78c039 Mon Sep 17 00:00:00 2001 From: Justin Vanderhooft Date: Wed, 31 Aug 2022 20:23:45 -0400 Subject: [PATCH 013/955] Bump melnor-bluetooth to 0.0.15 (#77631) --- homeassistant/components/melnor/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/melnor/manifest.json b/homeassistant/components/melnor/manifest.json index 37ac40cb3aa..a59758f705b 100644 --- a/homeassistant/components/melnor/manifest.json +++ b/homeassistant/components/melnor/manifest.json @@ -12,5 +12,5 @@ "documentation": "https://www.home-assistant.io/integrations/melnor", "iot_class": "local_polling", "name": "Melnor Bluetooth", - "requirements": ["melnor-bluetooth==0.0.13"] + "requirements": ["melnor-bluetooth==0.0.15"] } diff --git a/requirements_all.txt b/requirements_all.txt index 9d0a2378aa1..be9bcad352e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1037,7 +1037,7 @@ mcstatus==6.0.0 meater-python==0.0.8 # homeassistant.components.melnor -melnor-bluetooth==0.0.13 +melnor-bluetooth==0.0.15 # homeassistant.components.message_bird messagebird==1.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0ea12512282..4b070207610 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -742,7 +742,7 @@ mcstatus==6.0.0 meater-python==0.0.8 # homeassistant.components.melnor -melnor-bluetooth==0.0.13 +melnor-bluetooth==0.0.15 # homeassistant.components.meteo_france meteofrance-api==1.0.2 From 95dd9def665d85234aef28c1cb131e1ebd04f28c Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Thu, 1 Sep 2022 03:40:13 +0200 Subject: [PATCH 014/955] Required config_flow values for here_travel_time (#75026) --- .../here_travel_time/config_flow.py | 46 ++++++++++++++----- .../components/here_travel_time/const.py | 2 + 2 files changed, 36 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/here_travel_time/config_flow.py b/homeassistant/components/here_travel_time/config_flow.py index e8a05796b66..b4756c82922 100644 --- a/homeassistant/components/here_travel_time/config_flow.py +++ b/homeassistant/components/here_travel_time/config_flow.py @@ -11,6 +11,8 @@ from homeassistant import config_entries from homeassistant.const import ( CONF_API_KEY, CONF_ENTITY_NAMESPACE, + CONF_LATITUDE, + CONF_LONGITUDE, CONF_MODE, CONF_NAME, CONF_UNIT_SYSTEM, @@ -30,6 +32,8 @@ from .const import ( CONF_ARRIVAL_TIME, CONF_DEPARTURE, CONF_DEPARTURE_TIME, + CONF_DESTINATION, + CONF_ORIGIN, CONF_ROUTE_MODE, CONF_TRAFFIC_MODE, DEFAULT_NAME, @@ -187,13 +191,25 @@ class HERETravelTimeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Configure origin by using gps coordinates.""" if user_input is not None: - self._config[CONF_ORIGIN_LATITUDE] = user_input["origin"]["latitude"] - self._config[CONF_ORIGIN_LONGITUDE] = user_input["origin"]["longitude"] + self._config[CONF_ORIGIN_LATITUDE] = user_input[CONF_ORIGIN][CONF_LATITUDE] + self._config[CONF_ORIGIN_LONGITUDE] = user_input[CONF_ORIGIN][ + CONF_LONGITUDE + ] return self.async_show_menu( step_id="destination_menu", menu_options=["destination_coordinates", "destination_entity"], ) - schema = vol.Schema({"origin": selector({LocationSelector.selector_type: {}})}) + schema = vol.Schema( + { + vol.Required( + CONF_ORIGIN, + default={ + CONF_LATITUDE: self.hass.config.latitude, + CONF_LONGITUDE: self.hass.config.longitude, + }, + ): LocationSelector() + } + ) return self.async_show_form(step_id="origin_coordinates", data_schema=schema) async def async_step_origin_entity( @@ -206,9 +222,7 @@ class HERETravelTimeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="destination_menu", menu_options=["destination_coordinates", "destination_entity"], ) - schema = vol.Schema( - {CONF_ORIGIN_ENTITY_ID: selector({EntitySelector.selector_type: {}})} - ) + schema = vol.Schema({vol.Required(CONF_ORIGIN_ENTITY_ID): EntitySelector()}) return self.async_show_form(step_id="origin_entity", data_schema=schema) async def async_step_destination_coordinates( @@ -217,11 +231,11 @@ class HERETravelTimeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Configure destination by using gps coordinates.""" if user_input is not None: - self._config[CONF_DESTINATION_LATITUDE] = user_input["destination"][ - "latitude" + self._config[CONF_DESTINATION_LATITUDE] = user_input[CONF_DESTINATION][ + CONF_LATITUDE ] - self._config[CONF_DESTINATION_LONGITUDE] = user_input["destination"][ - "longitude" + self._config[CONF_DESTINATION_LONGITUDE] = user_input[CONF_DESTINATION][ + CONF_LONGITUDE ] return self.async_create_entry( title=self._config[CONF_NAME], @@ -229,7 +243,15 @@ class HERETravelTimeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): options=default_options(self.hass), ) schema = vol.Schema( - {"destination": selector({LocationSelector.selector_type: {}})} + { + vol.Required( + CONF_DESTINATION, + default={ + CONF_LATITUDE: self.hass.config.latitude, + CONF_LONGITUDE: self.hass.config.longitude, + }, + ): LocationSelector() + } ) return self.async_show_form( step_id="destination_coordinates", data_schema=schema @@ -250,7 +272,7 @@ class HERETravelTimeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): options=default_options(self.hass), ) schema = vol.Schema( - {CONF_DESTINATION_ENTITY_ID: selector({EntitySelector.selector_type: {}})} + {vol.Required(CONF_DESTINATION_ENTITY_ID): EntitySelector()} ) return self.async_show_form(step_id="destination_entity", data_schema=schema) diff --git a/homeassistant/components/here_travel_time/const.py b/homeassistant/components/here_travel_time/const.py index b3768b2d69d..4e9b8beaf12 100644 --- a/homeassistant/components/here_travel_time/const.py +++ b/homeassistant/components/here_travel_time/const.py @@ -9,9 +9,11 @@ DOMAIN = "here_travel_time" DEFAULT_SCAN_INTERVAL = 300 +CONF_DESTINATION = "destination" CONF_DESTINATION_LATITUDE = "destination_latitude" CONF_DESTINATION_LONGITUDE = "destination_longitude" CONF_DESTINATION_ENTITY_ID = "destination_entity_id" +CONF_ORIGIN = "origin" CONF_ORIGIN_LATITUDE = "origin_latitude" CONF_ORIGIN_LONGITUDE = "origin_longitude" CONF_ORIGIN_ENTITY_ID = "origin_entity_id" From e19fb56dd34e01d590e2bfd03f96d194dcf1457b Mon Sep 17 00:00:00 2001 From: Artem Draft Date: Thu, 1 Sep 2022 05:42:23 +0300 Subject: [PATCH 015/955] Suppress 404 in Bravia TV (#77288) --- homeassistant/components/braviatv/coordinator.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/braviatv/coordinator.py b/homeassistant/components/braviatv/coordinator.py index 2744911007d..bdacddcdb2f 100644 --- a/homeassistant/components/braviatv/coordinator.py +++ b/homeassistant/components/braviatv/coordinator.py @@ -7,7 +7,7 @@ from functools import wraps import logging from typing import Any, Final, TypeVar -from pybravia import BraviaTV, BraviaTVError +from pybravia import BraviaTV, BraviaTVError, BraviaTVNotFound from typing_extensions import Concatenate, ParamSpec from homeassistant.components.media_player.const import ( @@ -79,6 +79,7 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): self.connected = False # Assume that the TV is in Play mode self.playing = True + self.skipped_updates = 0 super().__init__( hass, @@ -113,6 +114,7 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): power_status = await self.client.get_power_status() self.is_on = power_status == "active" + self.skipped_updates = 0 if self.is_on is False: return @@ -121,6 +123,13 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): await self.async_update_sources() await self.async_update_volume() await self.async_update_playing() + except BraviaTVNotFound as err: + if self.skipped_updates < 10: + self.connected = False + self.skipped_updates += 1 + _LOGGER.debug("Update skipped, Bravia API service is reloading") + return + raise UpdateFailed("Error communicating with device") from err except BraviaTVError as err: self.is_on = False self.connected = False From 34ed6d1ecb97386d43c7540f85236a1a9e263f1d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 1 Sep 2022 02:43:18 +0000 Subject: [PATCH 016/955] Bump bleak to 0.16.0 (#77629) Co-authored-by: Justin Vanderhooft --- 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 5cd83a2d51a..981b7854e36 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -5,7 +5,7 @@ "dependencies": ["usb"], "quality_scale": "internal", "requirements": [ - "bleak==0.15.1", + "bleak==0.16.0", "bluetooth-adapters==0.3.2", "bluetooth-auto-recovery==0.3.0" ], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3f40589bf64..a456e9ec965 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.8.0 bcrypt==3.1.7 -bleak==0.15.1 +bleak==0.16.0 bluetooth-adapters==0.3.2 bluetooth-auto-recovery==0.3.0 certifi>=2021.5.30 diff --git a/requirements_all.txt b/requirements_all.txt index be9bcad352e..3e778246ec2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -408,7 +408,7 @@ bimmer_connected==0.10.2 bizkaibus==0.1.1 # homeassistant.components.bluetooth -bleak==0.15.1 +bleak==0.16.0 # homeassistant.components.blebox blebox_uniapi==2.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4b070207610..5e1c1b44334 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -329,7 +329,7 @@ bellows==0.33.1 bimmer_connected==0.10.2 # homeassistant.components.bluetooth -bleak==0.15.1 +bleak==0.16.0 # homeassistant.components.blebox blebox_uniapi==2.0.2 From 8dda2389c89ea3a70fc14212a432fc2e8b985e29 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 1 Sep 2022 01:01:10 -0400 Subject: [PATCH 017/955] 2022.10.0.dev0 (#77635) --- 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 750b014e0da..7afb781b3d9 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -6,7 +6,7 @@ from typing import Final from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 -MINOR_VERSION: Final = 9 +MINOR_VERSION: Final = 10 PATCH_VERSION: Final = "0.dev0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" diff --git a/pyproject.toml b/pyproject.toml index d68cac82923..8611dab5d4f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.9.0.dev0" +version = "2022.10.0.dev0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 6f8b032a6ed65d43dc10297271341a3436555961 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 1 Sep 2022 10:11:09 +0200 Subject: [PATCH 018/955] Adjust notify type hints in mysensors (#77647) --- homeassistant/components/mysensors/notify.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/mysensors/notify.py b/homeassistant/components/mysensors/notify.py index 43f4779604d..9083b325e8d 100644 --- a/homeassistant/components/mysensors/notify.py +++ b/homeassistant/components/mysensors/notify.py @@ -1,11 +1,12 @@ """MySensors notification service.""" from __future__ import annotations -from typing import Any +from typing import Any, cast from homeassistant.components.notify import ATTR_TARGET, BaseNotificationService from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .. import mysensors from .const import DevId, DiscoveryInfo @@ -13,15 +14,18 @@ from .const import DevId, DiscoveryInfo async def async_get_service( hass: HomeAssistant, - config: dict[str, Any], - discovery_info: DiscoveryInfo | None = None, + config: ConfigType, + discovery_info: DiscoveryInfoType | None = None, ) -> BaseNotificationService | None: """Get the MySensors notification service.""" if not discovery_info: return None new_devices = mysensors.setup_mysensors_platform( - hass, Platform.NOTIFY, discovery_info, MySensorsNotificationDevice + hass, + Platform.NOTIFY, + cast(DiscoveryInfo, discovery_info), + MySensorsNotificationDevice, ) if not new_devices: return None From dec2661322e2fe47d077a0b28fa277c0e14a4e09 Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Thu, 1 Sep 2022 10:19:21 +0200 Subject: [PATCH 019/955] Required option_flow values for here_travel_time (#77651) --- .../here_travel_time/config_flow.py | 61 ++++++++++--------- 1 file changed, 31 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/here_travel_time/config_flow.py b/homeassistant/components/here_travel_time/config_flow.py index b4756c82922..d42e6d6bf3e 100644 --- a/homeassistant/components/here_travel_time/config_flow.py +++ b/homeassistant/components/here_travel_time/config_flow.py @@ -24,7 +24,6 @@ from homeassistant.helpers.selector import ( EntitySelector, LocationSelector, TimeSelector, - selector, ) from .const import ( @@ -361,28 +360,30 @@ class HERETravelTimeOptionsFlow(config_entries.OptionsFlow): menu_options=["departure_time", "no_time"], ) - options = { - vol.Optional( - CONF_TRAFFIC_MODE, - default=self.config_entry.options.get( - CONF_TRAFFIC_MODE, TRAFFIC_MODE_ENABLED - ), - ): vol.In(TRAFFIC_MODES), - vol.Optional( - CONF_ROUTE_MODE, - default=self.config_entry.options.get( - CONF_ROUTE_MODE, ROUTE_MODE_FASTEST - ), - ): vol.In(ROUTE_MODES), - vol.Optional( - CONF_UNIT_SYSTEM, - default=self.config_entry.options.get( - CONF_UNIT_SYSTEM, self.hass.config.units.name - ), - ): vol.In(UNITS), - } + schema = vol.Schema( + { + vol.Optional( + CONF_TRAFFIC_MODE, + default=self.config_entry.options.get( + CONF_TRAFFIC_MODE, TRAFFIC_MODE_ENABLED + ), + ): vol.In(TRAFFIC_MODES), + vol.Optional( + CONF_ROUTE_MODE, + default=self.config_entry.options.get( + CONF_ROUTE_MODE, ROUTE_MODE_FASTEST + ), + ): vol.In(ROUTE_MODES), + vol.Optional( + CONF_UNIT_SYSTEM, + default=self.config_entry.options.get( + CONF_UNIT_SYSTEM, self.hass.config.units.name + ), + ): vol.In(UNITS), + } + ) - return self.async_show_form(step_id="init", data_schema=vol.Schema(options)) + return self.async_show_form(step_id="init", data_schema=schema) async def async_step_no_time( self, user_input: dict[str, Any] | None = None @@ -398,12 +399,12 @@ class HERETravelTimeOptionsFlow(config_entries.OptionsFlow): self._config[CONF_ARRIVAL_TIME] = user_input[CONF_ARRIVAL_TIME] return self.async_create_entry(title="", data=self._config) - options = {"arrival_time": selector({TimeSelector.selector_type: {}})} - - return self.async_show_form( - step_id="arrival_time", data_schema=vol.Schema(options) + schema = vol.Schema( + {vol.Required(CONF_ARRIVAL_TIME, default="00:00:00"): TimeSelector()} ) + return self.async_show_form(step_id="arrival_time", data_schema=schema) + async def async_step_departure_time( self, user_input: dict[str, Any] | None = None ) -> FlowResult: @@ -412,8 +413,8 @@ class HERETravelTimeOptionsFlow(config_entries.OptionsFlow): self._config[CONF_DEPARTURE_TIME] = user_input[CONF_DEPARTURE_TIME] return self.async_create_entry(title="", data=self._config) - options = {"departure_time": selector({TimeSelector.selector_type: {}})} - - return self.async_show_form( - step_id="departure_time", data_schema=vol.Schema(options) + schema = vol.Schema( + {vol.Required(CONF_DEPARTURE_TIME, default="00:00:00"): TimeSelector()} ) + + return self.async_show_form(step_id="departure_time", data_schema=schema) From f9a3757d71a63e64989f894174f6c864ef613699 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 1 Sep 2022 10:24:51 +0200 Subject: [PATCH 020/955] Bump ci env HA_SHORT_VERSION (#77644) --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 75a200402d6..02e50ddcc65 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -22,7 +22,7 @@ on: env: CACHE_VERSION: 1 PIP_CACHE_VERSION: 1 - HA_SHORT_VERSION: 2022.9 + HA_SHORT_VERSION: 2022.10 DEFAULT_PYTHON: 3.9 PRE_COMMIT_CACHE: ~/.cache/pre-commit PIP_CACHE: /tmp/pip-cache From 3d64cf730404ced8ec80dfc46e21077d67cf2208 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Thu, 1 Sep 2022 16:46:43 +0800 Subject: [PATCH 021/955] Fix basic browse_media support in forked-daapd (#77595) --- .../components/forked_daapd/const.py | 11 ++++++++ .../components/forked_daapd/media_player.py | 25 ++++++++++++------- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/forked_daapd/const.py b/homeassistant/components/forked_daapd/const.py index d711ae1b35b..f0d915ce3e5 100644 --- a/homeassistant/components/forked_daapd/const.py +++ b/homeassistant/components/forked_daapd/const.py @@ -1,6 +1,17 @@ """Const for forked-daapd.""" from homeassistant.components.media_player import MediaPlayerEntityFeature +CAN_PLAY_TYPE = { + "audio/mp4", + "audio/aac", + "audio/mpeg", + "audio/flac", + "audio/ogg", + "audio/x-ms-wma", + "audio/aiff", + "audio/wav", +} + CALLBACK_TIMEOUT = 8 # max time between command and callback from forked-daapd server CONF_LIBRESPOT_JAVA_PORT = "librespot_java_port" CONF_MAX_PLAYLISTS = "max_playlists" diff --git a/homeassistant/components/forked_daapd/media_player.py b/homeassistant/components/forked_daapd/media_player.py index 6c1a772fb4d..953461c1019 100644 --- a/homeassistant/components/forked_daapd/media_player.py +++ b/homeassistant/components/forked_daapd/media_player.py @@ -1,4 +1,6 @@ """This library brings support for forked_daapd to Home Assistant.""" +from __future__ import annotations + import asyncio from collections import defaultdict import logging @@ -8,7 +10,7 @@ from pyforked_daapd import ForkedDaapdAPI from pylibrespot_java import LibrespotJavaAPI from homeassistant.components import media_source -from homeassistant.components.media_player import MediaPlayerEntity +from homeassistant.components.media_player import BrowseMedia, MediaPlayerEntity from homeassistant.components.media_player.browse_media import ( async_process_play_media_url, ) @@ -35,6 +37,7 @@ from homeassistant.util.dt import utcnow from .const import ( CALLBACK_TIMEOUT, + CAN_PLAY_TYPE, CONF_LIBRESPOT_JAVA_PORT, CONF_MAX_PLAYLISTS, CONF_TTS_PAUSE_TIME, @@ -769,6 +772,18 @@ class ForkedDaapdMaster(MediaPlayerEntity): )() _LOGGER.warning("No pipe control available for %s", pipe_name) + async def async_browse_media( + self, + media_content_type: str | None = None, + media_content_id: str | None = None, + ) -> BrowseMedia: + """Implement the websocket media browsing helper.""" + return await media_source.async_browse_media( + self.hass, + media_content_id, + content_filter=lambda bm: bm.media_content_type in CAN_PLAY_TYPE, + ) + class ForkedDaapdUpdater: """Manage updates for the forked-daapd device.""" @@ -885,11 +900,3 @@ class ForkedDaapdUpdater: self._api, outputs_to_add, ) - - async def async_browse_media(self, media_content_type=None, media_content_id=None): - """Implement the websocket media browsing helper.""" - return await media_source.async_browse_media( - self.hass, - media_content_id, - content_filter=lambda item: item.media_content_type.startswith("audio/"), - ) From 08ab10d470f227ca2491f1c09edfcd94cb6ebadc Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Thu, 1 Sep 2022 04:49:36 -0400 Subject: [PATCH 022/955] Fix timezone edge cases for Unifi Protect media source (#77636) * Fixes timezone edge cases for Unifi Protect media source * linting --- .../components/unifiprotect/media_source.py | 19 ++- .../unifiprotect/test_media_source.py | 133 ++++++++++++++++-- 2 files changed, 137 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/unifiprotect/media_source.py b/homeassistant/components/unifiprotect/media_source.py index 58b14ab9b3b..4910c18cf5f 100644 --- a/homeassistant/components/unifiprotect/media_source.py +++ b/homeassistant/components/unifiprotect/media_source.py @@ -101,12 +101,12 @@ async def async_get_media_source(hass: HomeAssistant) -> MediaSource: @callback -def _get_start_end(hass: HomeAssistant, start: datetime) -> tuple[datetime, datetime]: +def _get_month_start_end(start: datetime) -> tuple[datetime, datetime]: start = dt_util.as_local(start) end = dt_util.now() - start = start.replace(day=1, hour=1, minute=0, second=0, microsecond=0) - end = end.replace(day=1, hour=0, minute=0, second=0, microsecond=0) + start = start.replace(day=1, hour=0, minute=0, second=1, microsecond=0) + end = end.replace(day=1, hour=0, minute=0, second=2, microsecond=0) return start, end @@ -571,9 +571,16 @@ class ProtectMediaSource(MediaSource): if not build_children: return source - month = start.month + if data.api.bootstrap.recording_start is not None: + recording_start = data.api.bootstrap.recording_start.date() + start = max(recording_start, start) + + recording_end = dt_util.now().date() + end = start.replace(month=start.month + 1) - timedelta(days=1) + end = min(recording_end, end) + children = [self._build_days(data, camera_id, event_type, start, is_all=True)] - while start.month == month: + while start <= end: children.append( self._build_days(data, camera_id, event_type, start, is_all=False) ) @@ -702,7 +709,7 @@ class ProtectMediaSource(MediaSource): self._build_recent(data, camera_id, event_type, 30), ] - start, end = _get_start_end(self.hass, data.api.bootstrap.recording_start) + start, end = _get_month_start_end(data.api.bootstrap.recording_start) while end > start: children.append(self._build_month(data, camera_id, event_type, end.date())) end = (end - timedelta(days=1)).replace(day=1) diff --git a/tests/components/unifiprotect/test_media_source.py b/tests/components/unifiprotect/test_media_source.py index bb3bc8aa345..74a007e0ba0 100644 --- a/tests/components/unifiprotect/test_media_source.py +++ b/tests/components/unifiprotect/test_media_source.py @@ -4,7 +4,9 @@ from datetime import datetime, timedelta from ipaddress import IPv4Address from unittest.mock import AsyncMock, Mock, patch +from freezegun import freeze_time import pytest +import pytz from pyunifiprotect.data import ( Bootstrap, Camera, @@ -28,6 +30,7 @@ from homeassistant.components.unifiprotect.media_source import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +from homeassistant.util import dt as dt_util from .conftest import MockUFPFixture from .utils import init_entry @@ -430,13 +433,52 @@ async def test_browse_media_event_type( assert browse.children[3].identifier == "test_id:browse:all:smart" +ONE_MONTH_SIMPLE = ( + datetime( + year=2022, + month=9, + day=1, + hour=3, + minute=0, + second=0, + microsecond=0, + tzinfo=pytz.timezone("US/Pacific"), + ), + 1, +) +TWO_MONTH_SIMPLE = ( + datetime( + year=2022, + month=8, + day=31, + hour=3, + minute=0, + second=0, + microsecond=0, + tzinfo=pytz.timezone("US/Pacific"), + ), + 2, +) + + +@pytest.mark.parametrize( + "start,months", + [ONE_MONTH_SIMPLE, TWO_MONTH_SIMPLE], +) +@freeze_time("2022-09-15 03:00:00-07:00") async def test_browse_media_time( - hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera, fixed_now: datetime + hass: HomeAssistant, + ufp: MockUFPFixture, + doorbell: Camera, + start: datetime, + months: int, ): """Test browsing time selector level media.""" - last_month = fixed_now.replace(day=1) - timedelta(days=1) - ufp.api.bootstrap._recording_start = last_month + end = datetime.fromisoformat("2022-09-15 03:00:00-07:00") + end_local = dt_util.as_local(end) + + ufp.api.bootstrap._recording_start = dt_util.as_utc(start) ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap) await init_entry(hass, ufp, [doorbell], regenerate_ids=False) @@ -449,17 +491,89 @@ async def test_browse_media_time( assert browse.title == f"UnifiProtect > {doorbell.name} > All Events" assert browse.identifier == base_id - assert len(browse.children) == 4 + assert len(browse.children) == 3 + months assert browse.children[0].title == "Last 24 Hours" assert browse.children[0].identifier == f"{base_id}:recent:1" assert browse.children[1].title == "Last 7 Days" assert browse.children[1].identifier == f"{base_id}:recent:7" assert browse.children[2].title == "Last 30 Days" assert browse.children[2].identifier == f"{base_id}:recent:30" - assert browse.children[3].title == f"{fixed_now.strftime('%B %Y')}" + assert browse.children[3].title == f"{end_local.strftime('%B %Y')}" assert ( browse.children[3].identifier - == f"{base_id}:range:{fixed_now.year}:{fixed_now.month}" + == f"{base_id}:range:{end_local.year}:{end_local.month}" + ) + + +ONE_MONTH_TIMEZONE = ( + datetime( + year=2022, + month=8, + day=1, + hour=3, + minute=0, + second=0, + microsecond=0, + tzinfo=pytz.timezone("US/Pacific"), + ), + 1, +) +TWO_MONTH_TIMEZONE = ( + datetime( + year=2022, + month=7, + day=31, + hour=21, + minute=0, + second=0, + microsecond=0, + tzinfo=pytz.timezone("US/Pacific"), + ), + 2, +) + + +@pytest.mark.parametrize( + "start,months", + [ONE_MONTH_TIMEZONE, TWO_MONTH_TIMEZONE], +) +@freeze_time("2022-08-31 21:00:00-07:00") +async def test_browse_media_time_timezone( + hass: HomeAssistant, + ufp: MockUFPFixture, + doorbell: Camera, + start: datetime, + months: int, +): + """Test browsing time selector level media.""" + + end = datetime.fromisoformat("2022-08-31 21:00:00-07:00") + end_local = dt_util.as_local(end) + + ufp.api.bootstrap._recording_start = dt_util.as_utc(start) + + ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap) + await init_entry(hass, ufp, [doorbell], regenerate_ids=False) + + base_id = f"test_id:browse:{doorbell.id}:all" + source = await async_get_media_source(hass) + media_item = MediaSourceItem(hass, DOMAIN, base_id, None) + + browse = await source.async_browse_media(media_item) + + assert browse.title == f"UnifiProtect > {doorbell.name} > All Events" + assert browse.identifier == base_id + assert len(browse.children) == 3 + months + assert browse.children[0].title == "Last 24 Hours" + assert browse.children[0].identifier == f"{base_id}:recent:1" + assert browse.children[1].title == "Last 7 Days" + assert browse.children[1].identifier == f"{base_id}:recent:7" + assert browse.children[2].title == "Last 30 Days" + assert browse.children[2].identifier == f"{base_id}:recent:30" + assert browse.children[3].title == f"{end_local.strftime('%B %Y')}" + assert ( + browse.children[3].identifier + == f"{base_id}:range:{end_local.year}:{end_local.month}" ) @@ -599,13 +713,14 @@ async def test_browse_media_eventthumb( assert browse.media_class == MEDIA_CLASS_IMAGE +@freeze_time("2022-09-15 03:00:00-07:00") async def test_browse_media_day( hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera, fixed_now: datetime ): """Test browsing day selector level media.""" - last_month = fixed_now.replace(day=1) - timedelta(days=1) - ufp.api.bootstrap._recording_start = last_month + start = datetime.fromisoformat("2022-09-03 03:00:00-07:00") + ufp.api.bootstrap._recording_start = dt_util.as_utc(start) ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap) await init_entry(hass, ufp, [doorbell], regenerate_ids=False) @@ -623,7 +738,7 @@ async def test_browse_media_day( == f"UnifiProtect > {doorbell.name} > All Events > {fixed_now.strftime('%B %Y')}" ) assert browse.identifier == base_id - assert len(browse.children) in (29, 30, 31, 32) + assert len(browse.children) == 14 assert browse.children[0].title == "Whole Month" assert browse.children[0].identifier == f"{base_id}:all" From d1ecd74a1a153b85b829acf45b5c6a5ea79df5c1 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 1 Sep 2022 14:14:31 +0200 Subject: [PATCH 023/955] Improve entity type hints [l] (#77655) --- .../components/landisgyr_heat_meter/sensor.py | 2 +- .../components/lg_netcast/media_player.py | 29 ++++++++++--------- .../components/lg_soundbar/media_player.py | 14 ++++----- homeassistant/components/lightwave/climate.py | 6 ++-- homeassistant/components/lightwave/sensor.py | 2 +- homeassistant/components/lightwave/switch.py | 6 ++-- .../components/linode/binary_sensor.py | 2 +- homeassistant/components/linode/switch.py | 7 +++-- .../components/linux_battery/sensor.py | 2 +- homeassistant/components/litejet/switch.py | 9 +++--- .../components/locative/device_tracker.py | 4 +-- .../components/logi_circle/camera.py | 10 +++---- .../components/logi_circle/sensor.py | 2 +- homeassistant/components/london_air/sensor.py | 2 +- homeassistant/components/lupusec/switch.py | 5 ++-- homeassistant/components/lutron/__init__.py | 4 +-- homeassistant/components/lutron/scene.py | 2 +- homeassistant/components/lutron/switch.py | 16 +++++----- .../components/lutron_caseta/binary_sensor.py | 2 +- .../components/lutron_caseta/switch.py | 8 +++-- 20 files changed, 73 insertions(+), 61 deletions(-) diff --git a/homeassistant/components/landisgyr_heat_meter/sensor.py b/homeassistant/components/landisgyr_heat_meter/sensor.py index 1d38b1f5816..23a6e217458 100644 --- a/homeassistant/components/landisgyr_heat_meter/sensor.py +++ b/homeassistant/components/landisgyr_heat_meter/sensor.py @@ -73,7 +73,7 @@ class HeatMeterSensor(CoordinatorEntity, RestoreSensor): self._attr_device_info = device self._attr_should_poll = bool(self.key in ("heat_usage", "heat_previous_year")) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Call when entity about to be added to hass.""" await super().async_added_to_hass() state = await self.async_get_last_sensor_data() diff --git a/homeassistant/components/lg_netcast/media_player.py b/homeassistant/components/lg_netcast/media_player.py index a36ac83d37c..19046316803 100644 --- a/homeassistant/components/lg_netcast/media_player.py +++ b/homeassistant/components/lg_netcast/media_player.py @@ -2,6 +2,7 @@ from __future__ import annotations from datetime import datetime +from typing import Any from pylgnetcast import LgNetCastClient, LgNetCastError from requests import RequestException @@ -106,7 +107,7 @@ class LgTVDevice(MediaPlayerEntity): except (LgNetCastError, RequestException): self._state = STATE_OFF - def update(self): + def update(self) -> None: """Retrieve the latest data from the LG TV.""" try: @@ -219,63 +220,63 @@ class LgTVDevice(MediaPlayerEntity): f"{self._client.url}data?target=screen_image&_={datetime.now().timestamp()}" ) - def turn_off(self): + def turn_off(self) -> None: """Turn off media player.""" self.send_command(1) - def turn_on(self): + def turn_on(self) -> None: """Turn on the media player.""" if self._on_action_script: self._on_action_script.run(context=self._context) - def volume_up(self): + def volume_up(self) -> None: """Volume up the media player.""" self.send_command(24) - def volume_down(self): + def volume_down(self) -> None: """Volume down media player.""" self.send_command(25) - def set_volume_level(self, volume): + def set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" self._client.set_volume(float(volume * 100)) - def mute_volume(self, mute): + def mute_volume(self, mute: bool) -> None: """Send mute command.""" self.send_command(26) - def select_source(self, source): + def select_source(self, source: str) -> None: """Select input source.""" self._client.change_channel(self._sources[source]) - def media_play_pause(self): + def media_play_pause(self) -> None: """Simulate play pause media player.""" if self._playing: self.media_pause() else: self.media_play() - def media_play(self): + def media_play(self) -> None: """Send play command.""" self._playing = True self._state = STATE_PLAYING self.send_command(33) - def media_pause(self): + def media_pause(self) -> None: """Send media pause command to media player.""" self._playing = False self._state = STATE_PAUSED self.send_command(34) - def media_next_track(self): + def media_next_track(self) -> None: """Send next track command.""" self.send_command(36) - def media_previous_track(self): + def media_previous_track(self) -> None: """Send the previous track command.""" self.send_command(37) - def play_media(self, media_type, media_id, **kwargs): + def play_media(self, media_type: str, media_id: str, **kwargs: Any) -> None: """Tune to channel.""" if media_type != MEDIA_TYPE_CHANNEL: raise ValueError(f"Invalid media type: {media_type}") diff --git a/homeassistant/components/lg_soundbar/media_player.py b/homeassistant/components/lg_soundbar/media_player.py index 941042d5bce..5ff5f63a544 100644 --- a/homeassistant/components/lg_soundbar/media_player.py +++ b/homeassistant/components/lg_soundbar/media_player.py @@ -65,11 +65,11 @@ class LGDevice(MediaPlayerEntity): self._treble = 0 self._device = None - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register the callback after hass is ready for it.""" await self.hass.async_add_executor_job(self._connect) - def _connect(self): + def _connect(self) -> None: """Perform the actual devices setup.""" self._device = temescal.temescal( self._host, port=self._port, callback=self.handle_event @@ -126,7 +126,7 @@ class LGDevice(MediaPlayerEntity): self.schedule_update_ha_state() - def update(self): + def update(self) -> None: """Trigger updates from the device.""" self._device.get_eq() self._device.get_info() @@ -182,19 +182,19 @@ class LGDevice(MediaPlayerEntity): sources.append(temescal.functions[function]) return sorted(sources) - def set_volume_level(self, volume): + def set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" volume = volume * self._volume_max self._device.set_volume(int(volume)) - def mute_volume(self, mute): + def mute_volume(self, mute: bool) -> None: """Mute (true) or unmute (false) media player.""" self._device.set_mute(mute) - def select_source(self, source): + def select_source(self, source: str) -> None: """Select input source.""" self._device.set_func(temescal.functions.index(source)) - def select_sound_mode(self, sound_mode): + def select_sound_mode(self, sound_mode: str) -> None: """Set Sound Mode for Receiver..""" self._device.set_eq(temescal.equalisers.index(sound_mode)) diff --git a/homeassistant/components/lightwave/climate.py b/homeassistant/components/lightwave/climate.py index 8076cbc058b..968d67bbcd2 100644 --- a/homeassistant/components/lightwave/climate.py +++ b/homeassistant/components/lightwave/climate.py @@ -1,6 +1,8 @@ """Support for LightwaveRF TRVs.""" from __future__ import annotations +from typing import Any + from homeassistant.components.climate import ( DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, @@ -61,7 +63,7 @@ class LightwaveTrv(ClimateEntity): # inhibit is used to prevent race condition on update. If non zero, skip next update cycle. self._inhibit = 0 - def update(self): + def update(self) -> None: """Communicate with a Lightwave RTF Proxy to get state.""" (temp, targ, _, trv_output) = self._lwlink.read_trv_status(self._serial) if temp is not None: @@ -95,7 +97,7 @@ class LightwaveTrv(ClimateEntity): self._attr_target_temperature = self._inhibit return self._attr_target_temperature - def set_temperature(self, **kwargs): + def set_temperature(self, **kwargs: Any) -> None: """Set TRV target temperature.""" if ATTR_TEMPERATURE in kwargs: self._attr_target_temperature = kwargs[ATTR_TEMPERATURE] diff --git a/homeassistant/components/lightwave/sensor.py b/homeassistant/components/lightwave/sensor.py index 14ea3bb85a8..dac591aea34 100644 --- a/homeassistant/components/lightwave/sensor.py +++ b/homeassistant/components/lightwave/sensor.py @@ -50,7 +50,7 @@ class LightwaveBattery(SensorEntity): self._serial = serial self._attr_unique_id = f"{serial}-trv-battery" - def update(self): + def update(self) -> None: """Communicate with a Lightwave RTF Proxy to get state.""" (dummy_temp, dummy_targ, battery, dummy_output) = self._lwlink.read_trv_status( self._serial diff --git a/homeassistant/components/lightwave/switch.py b/homeassistant/components/lightwave/switch.py index 80cc80510a4..67b69d0e5c4 100644 --- a/homeassistant/components/lightwave/switch.py +++ b/homeassistant/components/lightwave/switch.py @@ -1,6 +1,8 @@ """Support for LightwaveRF switches.""" from __future__ import annotations +from typing import Any + from homeassistant.components.switch import SwitchEntity from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant @@ -41,13 +43,13 @@ class LWRFSwitch(SwitchEntity): self._device_id = device_id self._lwlink = lwlink - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the LightWave switch on.""" self._attr_is_on = True self._lwlink.turn_on_switch(self._device_id, self._attr_name) self.async_write_ha_state() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the LightWave switch off.""" self._attr_is_on = False self._lwlink.turn_off(self._device_id, self._attr_name) diff --git a/homeassistant/components/linode/binary_sensor.py b/homeassistant/components/linode/binary_sensor.py index 67ccde764e1..2c63bbc0bc8 100644 --- a/homeassistant/components/linode/binary_sensor.py +++ b/homeassistant/components/linode/binary_sensor.py @@ -68,7 +68,7 @@ class LinodeBinarySensor(BinarySensorEntity): self._attr_extra_state_attributes = {} self._attr_name = None - def update(self): + def update(self) -> None: """Update state of sensor.""" data = None self._linode.update() diff --git a/homeassistant/components/linode/switch.py b/homeassistant/components/linode/switch.py index 76cd95e5bca..183abbc068c 100644 --- a/homeassistant/components/linode/switch.py +++ b/homeassistant/components/linode/switch.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any import voluptuous as vol @@ -63,17 +64,17 @@ class LinodeSwitch(SwitchEntity): self.data = None self._attr_extra_state_attributes = {} - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Boot-up the Node.""" if self.data.status != "running": self.data.boot() - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Shutdown the nodes.""" if self.data.status == "running": self.data.shutdown() - def update(self): + def update(self) -> None: """Get the latest data from the device and update the data.""" self._linode.update() if self._linode.data is not None: diff --git a/homeassistant/components/linux_battery/sensor.py b/homeassistant/components/linux_battery/sensor.py index ec747778b7b..765e0d79537 100644 --- a/homeassistant/components/linux_battery/sensor.py +++ b/homeassistant/components/linux_battery/sensor.py @@ -124,7 +124,7 @@ class LinuxBatterySensor(SensorEntity): ATTR_VOLTAGE_NOW: self._battery_stat.voltage_now, } - def update(self): + def update(self) -> None: """Get the latest data and updates the states.""" self._battery.update() self._battery_stat = self._battery.stat[self._battery_id] diff --git a/homeassistant/components/litejet/switch.py b/homeassistant/components/litejet/switch.py index 66b68a345f2..375e3dd9f46 100644 --- a/homeassistant/components/litejet/switch.py +++ b/homeassistant/components/litejet/switch.py @@ -1,5 +1,6 @@ """Support for LiteJet switch.""" import logging +from typing import Any from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry @@ -45,12 +46,12 @@ class LiteJetSwitch(SwitchEntity): self._state = False self._name = name - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Run when this Entity has been added to HA.""" self._lj.on_switch_pressed(self._index, self._on_switch_pressed) self._lj.on_switch_released(self._index, self._on_switch_released) - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Entity being removed from hass.""" self._lj.unsubscribe(self._on_switch_pressed) self._lj.unsubscribe(self._on_switch_released) @@ -85,11 +86,11 @@ class LiteJetSwitch(SwitchEntity): """Return the device-specific state attributes.""" return {ATTR_NUMBER: self._index} - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Press the switch.""" self._lj.press_switch(self._index) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Release the switch.""" self._lj.release_switch(self._index) diff --git a/homeassistant/components/locative/device_tracker.py b/homeassistant/components/locative/device_tracker.py index cd927aace38..f8fa1671034 100644 --- a/homeassistant/components/locative/device_tracker.py +++ b/homeassistant/components/locative/device_tracker.py @@ -64,13 +64,13 @@ class LocativeEntity(TrackerEntity): """Return the source type, eg gps or router, of the device.""" return SourceType.GPS - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register state update callback.""" self._unsub_dispatcher = async_dispatcher_connect( self.hass, TRACKER_UPDATE, self._async_receive_data ) - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Clean up after entity before removal.""" self._unsub_dispatcher() diff --git a/homeassistant/components/logi_circle/camera.py b/homeassistant/components/logi_circle/camera.py index 231de83a135..733e49ca0bf 100644 --- a/homeassistant/components/logi_circle/camera.py +++ b/homeassistant/components/logi_circle/camera.py @@ -75,7 +75,7 @@ class LogiCam(Camera): self._ffmpeg = ffmpeg self._listeners = [] - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Connect camera methods to signals.""" def _dispatch_proxy(method): @@ -111,7 +111,7 @@ class LogiCam(Camera): ] ) - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Disconnect dispatcher listeners when removed.""" for detach in self._listeners: detach() @@ -161,11 +161,11 @@ class LogiCam(Camera): """Return a still image from the camera.""" return await self._camera.live_stream.download_jpeg() - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Disable streaming mode for this camera.""" await self._camera.set_config("streaming", False) - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Enable streaming mode for this camera.""" await self._camera.set_config("streaming", True) @@ -210,6 +210,6 @@ class LogiCam(Camera): filename=snapshot_file, refresh=True ) - async def async_update(self): + async def async_update(self) -> None: """Update camera entity and refresh attributes.""" await self._camera.update() diff --git a/homeassistant/components/logi_circle/sensor.py b/homeassistant/components/logi_circle/sensor.py index da115f789da..baf6d933916 100644 --- a/homeassistant/components/logi_circle/sensor.py +++ b/homeassistant/components/logi_circle/sensor.py @@ -112,7 +112,7 @@ class LogiSensor(SensorEntity): ) return self.entity_description.icon - async def async_update(self): + async def async_update(self) -> None: """Get the latest data and updates the state.""" _LOGGER.debug("Pulling data from %s sensor", self.name) await self._camera.update() diff --git a/homeassistant/components/london_air/sensor.py b/homeassistant/components/london_air/sensor.py index 23bf3bb2e64..33fe1d4d7fb 100644 --- a/homeassistant/components/london_air/sensor.py +++ b/homeassistant/components/london_air/sensor.py @@ -141,7 +141,7 @@ class AirSensor(SensorEntity): attrs["data"] = self._site_data return attrs - def update(self): + def update(self) -> None: """Update the sensor.""" sites_status = [] self._api_data.update() diff --git a/homeassistant/components/lupusec/switch.py b/homeassistant/components/lupusec/switch.py index 4b5f38a81b3..546f96fd0a6 100644 --- a/homeassistant/components/lupusec/switch.py +++ b/homeassistant/components/lupusec/switch.py @@ -2,6 +2,7 @@ from __future__ import annotations from datetime import timedelta +from typing import Any import lupupy.constants as CONST @@ -39,11 +40,11 @@ def setup_platform( class LupusecSwitch(LupusecDevice, SwitchEntity): """Representation of a Lupusec switch.""" - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn on the device.""" self._device.switch_on() - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn off the device.""" self._device.switch_off() diff --git a/homeassistant/components/lutron/__init__.py b/homeassistant/components/lutron/__init__.py index 75561dd275b..d8ccce8a6bc 100644 --- a/homeassistant/components/lutron/__init__.py +++ b/homeassistant/components/lutron/__init__.py @@ -124,7 +124,7 @@ class LutronDevice(Entity): self._controller = controller self._area_name = area_name - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" self._lutron_device.subscribe(self._update_callback, None) @@ -133,7 +133,7 @@ class LutronDevice(Entity): self.schedule_update_ha_state() @property - def name(self): + def name(self) -> str: """Return the name of the device.""" return f"{self._area_name} {self._lutron_device.name}" diff --git a/homeassistant/components/lutron/scene.py b/homeassistant/components/lutron/scene.py index 68d1a4805fc..f2d008a1187 100644 --- a/homeassistant/components/lutron/scene.py +++ b/homeassistant/components/lutron/scene.py @@ -43,6 +43,6 @@ class LutronScene(LutronDevice, Scene): self._lutron_device.press() @property - def name(self): + def name(self) -> str: """Return the name of the device.""" return f"{self._area_name} {self._keypad_name}: {self._lutron_device.name}" diff --git a/homeassistant/components/lutron/switch.py b/homeassistant/components/lutron/switch.py index 49d4181a8e0..8595f809035 100644 --- a/homeassistant/components/lutron/switch.py +++ b/homeassistant/components/lutron/switch.py @@ -1,6 +1,8 @@ """Support for Lutron switches.""" from __future__ import annotations +from typing import Any + from homeassistant.components.switch import SwitchEntity from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -43,11 +45,11 @@ class LutronSwitch(LutronDevice, SwitchEntity): self._prev_state = None super().__init__(area_name, lutron_device, controller) - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" self._lutron_device.level = 100 - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" self._lutron_device.level = 0 @@ -61,7 +63,7 @@ class LutronSwitch(LutronDevice, SwitchEntity): """Return true if device is on.""" return self._lutron_device.last_level() > 0 - def update(self): + def update(self) -> None: """Call when forcing a refresh of the device.""" if self._prev_state is None: self._prev_state = self._lutron_device.level > 0 @@ -76,11 +78,11 @@ class LutronLed(LutronDevice, SwitchEntity): self._scene_name = scene_device.name super().__init__(area_name, led_device, controller) - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the LED on.""" self._lutron_device.state = 1 - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the LED off.""" self._lutron_device.state = 0 @@ -99,11 +101,11 @@ class LutronLed(LutronDevice, SwitchEntity): return self._lutron_device.last_state @property - def name(self): + def name(self) -> str: """Return the name of the LED.""" return f"{self._area_name} {self._keypad_name}: {self._scene_name} LED" - def update(self): + def update(self) -> None: """Call when forcing a refresh of the device.""" if self._lutron_device.last_state is not None: return diff --git a/homeassistant/components/lutron_caseta/binary_sensor.py b/homeassistant/components/lutron_caseta/binary_sensor.py index 4b1c53d194b..20fc221cdef 100644 --- a/homeassistant/components/lutron_caseta/binary_sensor.py +++ b/homeassistant/components/lutron_caseta/binary_sensor.py @@ -61,7 +61,7 @@ class LutronOccupancySensor(LutronCasetaDevice, BinarySensorEntity): """Return the brightness of the light.""" return self._device["status"] == OCCUPANCY_GROUP_OCCUPIED - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" self._smartbridge.add_occupancy_subscriber( self.device_id, self.async_write_ha_state diff --git a/homeassistant/components/lutron_caseta/switch.py b/homeassistant/components/lutron_caseta/switch.py index 062c8891672..92ec6b35f98 100644 --- a/homeassistant/components/lutron_caseta/switch.py +++ b/homeassistant/components/lutron_caseta/switch.py @@ -1,5 +1,7 @@ """Support for Lutron Caseta switches.""" +from typing import Any + from homeassistant.components.switch import DOMAIN, SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -33,15 +35,15 @@ async def async_setup_entry( class LutronCasetaLight(LutronCasetaDeviceUpdatableEntity, SwitchEntity): """Representation of a Lutron Caseta switch.""" - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" await self._smartbridge.turn_on(self.device_id) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" await self._smartbridge.turn_off(self.device_id) @property - def is_on(self): + def is_on(self) -> bool: """Return true if device is on.""" return self._device["current_state"] > 0 From 8afcde4ea9630e2189bbf7fe202817b52c8bc259 Mon Sep 17 00:00:00 2001 From: luar123 <49960470+luar123@users.noreply.github.com> Date: Thu, 1 Sep 2022 14:52:06 +0200 Subject: [PATCH 024/955] Add and remove Snapcast client/group callbacks properly (#77624) Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- .../components/snapcast/media_player.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/snapcast/media_player.py b/homeassistant/components/snapcast/media_player.py index 703cb41a38f..0e6524c8504 100644 --- a/homeassistant/components/snapcast/media_player.py +++ b/homeassistant/components/snapcast/media_player.py @@ -125,10 +125,17 @@ class SnapcastGroupDevice(MediaPlayerEntity): def __init__(self, group, uid_part): """Initialize the Snapcast group device.""" - group.set_callback(self.schedule_update_ha_state) self._group = group self._uid = f"{GROUP_PREFIX}{uid_part}_{self._group.identifier}" + async def async_added_to_hass(self) -> None: + """Subscribe to group events.""" + self._group.set_callback(self.schedule_update_ha_state) + + async def async_will_remove_from_hass(self) -> None: + """Disconnect group object when removed.""" + self._group.set_callback(None) + @property def state(self): """Return the state of the player.""" @@ -213,10 +220,17 @@ class SnapcastClientDevice(MediaPlayerEntity): def __init__(self, client, uid_part): """Initialize the Snapcast client device.""" - client.set_callback(self.schedule_update_ha_state) self._client = client self._uid = f"{CLIENT_PREFIX}{uid_part}_{self._client.identifier}" + async def async_added_to_hass(self) -> None: + """Subscribe to client events.""" + self._client.set_callback(self.schedule_update_ha_state) + + async def async_will_remove_from_hass(self) -> None: + """Disconnect client object when removed.""" + self._client.set_callback(None) + @property def unique_id(self): """ From cd2045b66d1f30c4cdae151989eb6bd725f3081e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 1 Sep 2022 17:45:19 +0200 Subject: [PATCH 025/955] Clean up user overridden device class in entity registry (#77662) --- homeassistant/helpers/entity_registry.py | 14 ++++- tests/helpers/test_entity_registry.py | 72 ++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 23e9cc5f752..d495d196440 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -30,6 +30,7 @@ from homeassistant.const import ( MAX_LENGTH_STATE_ENTITY_ID, STATE_UNAVAILABLE, STATE_UNKNOWN, + Platform, ) from homeassistant.core import ( Event, @@ -62,7 +63,7 @@ SAVE_DELAY = 10 _LOGGER = logging.getLogger(__name__) STORAGE_VERSION_MAJOR = 1 -STORAGE_VERSION_MINOR = 7 +STORAGE_VERSION_MINOR = 8 STORAGE_KEY = "core.entity_registry" # Attributes relevant to describing entity @@ -970,10 +971,19 @@ async def _async_migrate( entity["hidden_by"] = None if old_major_version == 1 and old_minor_version < 7: - # Version 1.6 adds has_entity_name + # Version 1.7 adds has_entity_name for entity in data["entities"]: entity["has_entity_name"] = False + if old_major_version == 1 and old_minor_version < 8: + # Cleanup after frontend bug which incorrectly updated device_class + # Fixed by frontend PR #13551 + for entity in data["entities"]: + domain = split_entity_id(entity["entity_id"])[0] + if domain in [Platform.BINARY_SENSOR, Platform.COVER]: + continue + entity["device_class"] = None + if old_major_version > 1: raise NotImplementedError return data diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index 9c2592eace0..e4c371a0198 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -528,6 +528,78 @@ async def test_migration_1_1(hass, hass_storage): assert entry.original_device_class == "best_class" +@pytest.mark.parametrize("load_registries", [False]) +async def test_migration_1_7(hass, hass_storage): + """Test migration from version 1.7. + + This tests cleanup after frontend bug which incorrectly updated device_class + """ + entity_dict = { + "area_id": None, + "capabilities": {}, + "config_entry_id": None, + "device_id": None, + "disabled_by": None, + "entity_category": None, + "has_entity_name": False, + "hidden_by": None, + "icon": None, + "id": "12345", + "name": None, + "options": None, + "original_icon": None, + "original_name": None, + "platform": "super_platform", + "supported_features": 0, + "unique_id": "very_unique", + "unit_of_measurement": None, + } + + hass_storage[er.STORAGE_KEY] = { + "version": 1, + "minor_version": 7, + "data": { + "entities": [ + { + **entity_dict, + "device_class": "original_class_by_integration", + "entity_id": "test.entity", + "original_device_class": "new_class_by_integration", + }, + { + **entity_dict, + "device_class": "class_by_user", + "entity_id": "binary_sensor.entity", + "original_device_class": "class_by_integration", + }, + { + **entity_dict, + "device_class": "class_by_user", + "entity_id": "cover.entity", + "original_device_class": "class_by_integration", + }, + ] + }, + } + + await er.async_load(hass) + registry = er.async_get(hass) + + entry = registry.async_get_or_create("test", "super_platform", "very_unique") + assert entry.device_class is None + assert entry.original_device_class == "new_class_by_integration" + + entry = registry.async_get_or_create( + "binary_sensor", "super_platform", "very_unique" + ) + assert entry.device_class == "class_by_user" + assert entry.original_device_class == "class_by_integration" + + entry = registry.async_get_or_create("cover", "super_platform", "very_unique") + assert entry.device_class == "class_by_user" + assert entry.original_device_class == "class_by_integration" + + @pytest.mark.parametrize("load_registries", [False]) async def test_loading_invalid_entity_id(hass, hass_storage): """Test we skip entities with invalid entity IDs.""" From db4391adffeed1be11378ba65c6a87b603fd573e Mon Sep 17 00:00:00 2001 From: Ernst Klamer Date: Thu, 1 Sep 2022 17:47:47 +0200 Subject: [PATCH 026/955] Add device class moisture (#77666) --- homeassistant/components/sensor/__init__.py | 3 +++ homeassistant/components/sensor/device_condition.py | 3 +++ homeassistant/components/sensor/device_trigger.py | 3 +++ homeassistant/components/sensor/strings.json | 2 ++ tests/components/sensor/test_device_condition.py | 2 +- tests/components/sensor/test_device_trigger.py | 2 +- tests/testing_config/custom_components/test/sensor.py | 1 + 7 files changed, 14 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 562b50bbc61..d4f4aa3c37d 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -119,6 +119,9 @@ class SensorDeviceClass(StrEnum): # current light level (lx/lm) ILLUMINANCE = "illuminance" + # moisture (%) + MOISTURE = "moisture" + # Amount of money (currency) MONETARY = "monetary" diff --git a/homeassistant/components/sensor/device_condition.py b/homeassistant/components/sensor/device_condition.py index 808d6367cdf..01be2f37172 100644 --- a/homeassistant/components/sensor/device_condition.py +++ b/homeassistant/components/sensor/device_condition.py @@ -43,6 +43,7 @@ CONF_IS_FREQUENCY = "is_frequency" CONF_IS_HUMIDITY = "is_humidity" CONF_IS_GAS = "is_gas" CONF_IS_ILLUMINANCE = "is_illuminance" +CONF_IS_MOISTURE = "is_moisture" CONF_IS_NITROGEN_DIOXIDE = "is_nitrogen_dioxide" CONF_IS_NITROGEN_MONOXIDE = "is_nitrogen_monoxide" CONF_IS_NITROUS_OXIDE = "is_nitrous_oxide" @@ -72,6 +73,7 @@ ENTITY_CONDITIONS = { SensorDeviceClass.GAS: [{CONF_TYPE: CONF_IS_GAS}], SensorDeviceClass.HUMIDITY: [{CONF_TYPE: CONF_IS_HUMIDITY}], SensorDeviceClass.ILLUMINANCE: [{CONF_TYPE: CONF_IS_ILLUMINANCE}], + SensorDeviceClass.MOISTURE: [{CONF_TYPE: CONF_IS_MOISTURE}], SensorDeviceClass.NITROGEN_DIOXIDE: [{CONF_TYPE: CONF_IS_NITROGEN_DIOXIDE}], SensorDeviceClass.NITROGEN_MONOXIDE: [{CONF_TYPE: CONF_IS_NITROGEN_MONOXIDE}], SensorDeviceClass.NITROUS_OXIDE: [{CONF_TYPE: CONF_IS_NITROUS_OXIDE}], @@ -110,6 +112,7 @@ CONDITION_SCHEMA = vol.All( CONF_IS_HUMIDITY, CONF_IS_ILLUMINANCE, CONF_IS_OZONE, + CONF_IS_MOISTURE, CONF_IS_NITROGEN_DIOXIDE, CONF_IS_NITROGEN_MONOXIDE, CONF_IS_NITROUS_OXIDE, diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py index 5c815d3c546..90d0a4c3d0e 100644 --- a/homeassistant/components/sensor/device_trigger.py +++ b/homeassistant/components/sensor/device_trigger.py @@ -42,6 +42,7 @@ CONF_FREQUENCY = "frequency" CONF_GAS = "gas" CONF_HUMIDITY = "humidity" CONF_ILLUMINANCE = "illuminance" +CONF_MOISTURE = "moisture" CONF_NITROGEN_DIOXIDE = "nitrogen_dioxide" CONF_NITROGEN_MONOXIDE = "nitrogen_monoxide" CONF_NITROUS_OXIDE = "nitrous_oxide" @@ -71,6 +72,7 @@ ENTITY_TRIGGERS = { SensorDeviceClass.GAS: [{CONF_TYPE: CONF_GAS}], SensorDeviceClass.HUMIDITY: [{CONF_TYPE: CONF_HUMIDITY}], SensorDeviceClass.ILLUMINANCE: [{CONF_TYPE: CONF_ILLUMINANCE}], + SensorDeviceClass.MOISTURE: [{CONF_TYPE: CONF_MOISTURE}], SensorDeviceClass.NITROGEN_DIOXIDE: [{CONF_TYPE: CONF_NITROGEN_DIOXIDE}], SensorDeviceClass.NITROGEN_MONOXIDE: [{CONF_TYPE: CONF_NITROGEN_MONOXIDE}], SensorDeviceClass.NITROUS_OXIDE: [{CONF_TYPE: CONF_NITROUS_OXIDE}], @@ -109,6 +111,7 @@ TRIGGER_SCHEMA = vol.All( CONF_GAS, CONF_HUMIDITY, CONF_ILLUMINANCE, + CONF_MOISTURE, CONF_NITROGEN_DIOXIDE, CONF_NITROGEN_MONOXIDE, CONF_NITROUS_OXIDE, diff --git a/homeassistant/components/sensor/strings.json b/homeassistant/components/sensor/strings.json index f09f2c80db0..bac12bd8cb2 100644 --- a/homeassistant/components/sensor/strings.json +++ b/homeassistant/components/sensor/strings.json @@ -9,6 +9,7 @@ "is_gas": "Current {entity_name} gas", "is_humidity": "Current {entity_name} humidity", "is_illuminance": "Current {entity_name} illuminance", + "is_moisture": "Current {entity_name} moisture", "is_nitrogen_dioxide": "Current {entity_name} nitrogen dioxide concentration level", "is_nitrogen_monoxide": "Current {entity_name} nitrogen monoxide concentration level", "is_nitrous_oxide": "Current {entity_name} nitrous oxide concentration level", @@ -38,6 +39,7 @@ "gas": "{entity_name} gas changes", "humidity": "{entity_name} humidity changes", "illuminance": "{entity_name} illuminance changes", + "moisture": "{entity_name} moisture changes", "nitrogen_dioxide": "{entity_name} nitrogen dioxide concentration changes", "nitrogen_monoxide": "{entity_name} nitrogen monoxide concentration changes", "nitrous_oxide": "{entity_name} nitrous oxide concentration changes", diff --git a/tests/components/sensor/test_device_condition.py b/tests/components/sensor/test_device_condition.py index ab143e615d1..de0aa61b994 100644 --- a/tests/components/sensor/test_device_condition.py +++ b/tests/components/sensor/test_device_condition.py @@ -85,7 +85,7 @@ async def test_get_conditions(hass, device_reg, entity_reg, enable_custom_integr conditions = await async_get_device_automations( hass, DeviceAutomationType.CONDITION, device_entry.id ) - assert len(conditions) == 26 + assert len(conditions) == 27 assert_lists_same(conditions, expected_conditions) diff --git a/tests/components/sensor/test_device_trigger.py b/tests/components/sensor/test_device_trigger.py index 2592d51dcdf..c4b321fa59d 100644 --- a/tests/components/sensor/test_device_trigger.py +++ b/tests/components/sensor/test_device_trigger.py @@ -89,7 +89,7 @@ async def test_get_triggers(hass, device_reg, entity_reg, enable_custom_integrat triggers = await async_get_device_automations( hass, DeviceAutomationType.TRIGGER, device_entry.id ) - assert len(triggers) == 26 + assert len(triggers) == 27 assert_lists_same(triggers, expected_triggers) diff --git a/tests/testing_config/custom_components/test/sensor.py b/tests/testing_config/custom_components/test/sensor.py index 56587c80c34..6404a126807 100644 --- a/tests/testing_config/custom_components/test/sensor.py +++ b/tests/testing_config/custom_components/test/sensor.py @@ -32,6 +32,7 @@ UNITS_OF_MEASUREMENT = { SensorDeviceClass.CO2: CONCENTRATION_PARTS_PER_MILLION, # ppm of CO2 concentration SensorDeviceClass.HUMIDITY: PERCENTAGE, # % of humidity in the air SensorDeviceClass.ILLUMINANCE: "lm", # current light level (lx/lm) + SensorDeviceClass.MOISTURE: PERCENTAGE, # % of water in a substance SensorDeviceClass.NITROGEN_DIOXIDE: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, # µg/m³ of nitrogen dioxide SensorDeviceClass.NITROGEN_MONOXIDE: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, # µg/m³ of nitrogen monoxide SensorDeviceClass.NITROUS_OXIDE: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, # µg/m³ of nitrogen oxide From d65eaf11f4529d47fa9d46931dd399aed5c01b25 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 1 Sep 2022 17:51:27 +0200 Subject: [PATCH 027/955] Include entity registry id in entity registry WS API (#77668) --- homeassistant/components/config/entity_registry.py | 1 + tests/components/config/test_entity_registry.py | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/homeassistant/components/config/entity_registry.py b/homeassistant/components/config/entity_registry.py index 6d022aa2d14..cbfd092bc0c 100644 --- a/homeassistant/components/config/entity_registry.py +++ b/homeassistant/components/config/entity_registry.py @@ -237,6 +237,7 @@ def _entry_dict(entry: er.RegistryEntry) -> dict[str, Any]: "entity_id": entry.entity_id, "hidden_by": entry.hidden_by, "icon": entry.icon, + "id": entry.id, "name": entry.name, "original_name": entry.original_name, "platform": entry.platform, diff --git a/tests/components/config/test_entity_registry.py b/tests/components/config/test_entity_registry.py index 11a2adb5646..30153195eec 100644 --- a/tests/components/config/test_entity_registry.py +++ b/tests/components/config/test_entity_registry.py @@ -1,4 +1,6 @@ """Test entity_registry API.""" +from unittest.mock import ANY + import pytest from homeassistant.components.config import entity_registry @@ -67,6 +69,7 @@ async def test_list_entities(hass, client): "has_entity_name": False, "hidden_by": None, "icon": None, + "id": ANY, "name": "Hello World", "original_name": None, "platform": "test_platform", @@ -81,6 +84,7 @@ async def test_list_entities(hass, client): "has_entity_name": False, "hidden_by": None, "icon": None, + "id": ANY, "name": None, "original_name": None, "platform": "test_platform", @@ -117,6 +121,7 @@ async def test_list_entities(hass, client): "has_entity_name": False, "hidden_by": None, "icon": None, + "id": ANY, "name": "Hello World", "original_name": None, "platform": "test_platform", @@ -159,6 +164,7 @@ async def test_get_entity(hass, client): "entity_id": "test_domain.name", "hidden_by": None, "icon": None, + "id": ANY, "has_entity_name": False, "name": "Hello World", "options": {}, @@ -189,6 +195,7 @@ async def test_get_entity(hass, client): "entity_id": "test_domain.no_name", "hidden_by": None, "icon": None, + "id": ANY, "has_entity_name": False, "name": None, "options": {}, @@ -252,6 +259,7 @@ async def test_update_entity(hass, client): "entity_id": "test_domain.world", "hidden_by": "user", # We exchange strings over the WS API, not enums "icon": "icon:after update", + "id": ANY, "has_entity_name": False, "name": "after update", "options": {}, @@ -324,6 +332,7 @@ async def test_update_entity(hass, client): "entity_id": "test_domain.world", "hidden_by": "user", # We exchange strings over the WS API, not enums "icon": "icon:after update", + "id": ANY, "has_entity_name": False, "name": "after update", "options": {}, @@ -361,6 +370,7 @@ async def test_update_entity(hass, client): "entity_id": "test_domain.world", "hidden_by": "user", # We exchange strings over the WS API, not enums "icon": "icon:after update", + "id": ANY, "has_entity_name": False, "name": "after update", "options": {"sensor": {"unit_of_measurement": "beard_second"}}, @@ -409,6 +419,7 @@ async def test_update_entity_require_restart(hass, client): "entity_category": None, "entity_id": entity_id, "icon": None, + "id": ANY, "hidden_by": None, "has_entity_name": False, "name": None, @@ -515,6 +526,7 @@ async def test_update_entity_no_changes(hass, client): "entity_id": "test_domain.world", "hidden_by": None, "icon": None, + "id": ANY, "has_entity_name": False, "name": "name of entity", "options": {}, @@ -601,6 +613,7 @@ async def test_update_entity_id(hass, client): "entity_id": "test_domain.planet", "hidden_by": None, "icon": None, + "id": ANY, "has_entity_name": False, "name": None, "options": {}, From 93a8aef4ccb0c344f1eb232b313dc239e9566b26 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Thu, 1 Sep 2022 17:57:49 +0100 Subject: [PATCH 028/955] Fix async_all_discovered_devices(False) to return connectable and unconnectable devices (#77670) --- homeassistant/components/bluetooth/manager.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index d2b59469bd9..d274939c610 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -221,10 +221,14 @@ class BluetoothManager: @hass_callback def async_all_discovered_devices(self, connectable: bool) -> Iterable[BLEDevice]: """Return all of discovered devices from all the scanners including duplicates.""" - return itertools.chain.from_iterable( - scanner.discovered_devices - for scanner in self._get_scanners_by_type(connectable) + yield from itertools.chain.from_iterable( + scanner.discovered_devices for scanner in self._get_scanners_by_type(True) ) + if not connectable: + yield from itertools.chain.from_iterable( + scanner.discovered_devices + for scanner in self._get_scanners_by_type(False) + ) @hass_callback def async_discovered_devices(self, connectable: bool) -> list[BLEDevice]: From 1692808d5b4e91c3316ac04dcf93e1213dec9a1c Mon Sep 17 00:00:00 2001 From: On Freund Date: Thu, 1 Sep 2022 21:02:09 +0300 Subject: [PATCH 029/955] Increase sleep in Risco setup (#77619) --- homeassistant/components/risco/__init__.py | 4 ++++ homeassistant/components/risco/config_flow.py | 5 ----- homeassistant/components/risco/const.py | 2 -- homeassistant/components/risco/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/risco/test_alarm_control_panel.py | 3 +++ tests/components/risco/test_config_flow.py | 6 ------ 8 files changed, 10 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/risco/__init__.py b/homeassistant/components/risco/__init__.py index e95b3016139..179ddd5cad6 100644 --- a/homeassistant/components/risco/__init__.py +++ b/homeassistant/components/risco/__init__.py @@ -154,6 +154,10 @@ 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 is_local(entry): + local_data: LocalData = hass.data[DOMAIN][entry.entry_id] + await local_data.system.disconnect() + hass.data[DOMAIN].pop(entry.entry_id) return unload_ok diff --git a/homeassistant/components/risco/config_flow.py b/homeassistant/components/risco/config_flow.py index 1befe626347..5e1cdb75b5a 100644 --- a/homeassistant/components/risco/config_flow.py +++ b/homeassistant/components/risco/config_flow.py @@ -1,7 +1,6 @@ """Config flow for Risco integration.""" from __future__ import annotations -import asyncio from collections.abc import Mapping import logging @@ -32,7 +31,6 @@ from .const import ( DEFAULT_OPTIONS, DOMAIN, RISCO_STATES, - SLEEP_INTERVAL, TYPE_LOCAL, ) @@ -150,9 +148,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(info["title"]) self._abort_if_unique_id_configured() - # Risco can hang if we don't wait before creating a new connection - await asyncio.sleep(SLEEP_INTERVAL) - return self.async_create_entry( title=info["title"], data={**user_input, **{CONF_TYPE: TYPE_LOCAL}} ) diff --git a/homeassistant/components/risco/const.py b/homeassistant/components/risco/const.py index f4ac170d3c7..9f0e71701c6 100644 --- a/homeassistant/components/risco/const.py +++ b/homeassistant/components/risco/const.py @@ -46,5 +46,3 @@ DEFAULT_OPTIONS = { CONF_RISCO_STATES_TO_HA: DEFAULT_RISCO_STATES_TO_HA, CONF_HA_STATES_TO_RISCO: DEFAULT_HA_STATES_TO_RISCO, } - -SLEEP_INTERVAL = 1 diff --git a/homeassistant/components/risco/manifest.json b/homeassistant/components/risco/manifest.json index 38035e22c62..9703b5775bc 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.3"], + "requirements": ["pyrisco==0.5.4"], "codeowners": ["@OnFreund"], "quality_scale": "platinum", "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 3e778246ec2..3fa89f3898e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1814,7 +1814,7 @@ pyrecswitch==1.0.2 pyrepetierng==0.1.0 # homeassistant.components.risco -pyrisco==0.5.3 +pyrisco==0.5.4 # homeassistant.components.rituals_perfume_genie pyrituals==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5e1c1b44334..e9bbc312652 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1270,7 +1270,7 @@ pyps4-2ndscreen==1.3.1 pyqwikswitch==0.93 # homeassistant.components.risco -pyrisco==0.5.3 +pyrisco==0.5.4 # homeassistant.components.rituals_perfume_genie pyrituals==0.0.6 diff --git a/tests/components/risco/test_alarm_control_panel.py b/tests/components/risco/test_alarm_control_panel.py index 0014e712ab1..1625e78ece6 100644 --- a/tests/components/risco/test_alarm_control_panel.py +++ b/tests/components/risco/test_alarm_control_panel.py @@ -479,6 +479,9 @@ async def test_local_setup(hass, two_part_local_alarm, setup_risco_local): device = registry.async_get_device({(DOMAIN, TEST_SITE_UUID + "_1_local")}) assert device is not None assert device.manufacturer == "Risco" + with patch("homeassistant.components.risco.RiscoLocal.disconnect") as mock_close: + await hass.config_entries.async_unload(setup_risco_local.entry_id) + mock_close.assert_awaited_once() async def _check_local_state( diff --git a/tests/components/risco/test_config_flow.py b/tests/components/risco/test_config_flow.py index a39a724d7b9..396aad8015d 100644 --- a/tests/components/risco/test_config_flow.py +++ b/tests/components/risco/test_config_flow.py @@ -72,9 +72,6 @@ async def test_cloud_form(hass): ), patch( "homeassistant.components.risco.config_flow.RiscoCloud.close" ) as mock_close, patch( - "homeassistant.components.risco.config_flow.SLEEP_INTERVAL", - 0, - ), patch( "homeassistant.components.risco.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -168,9 +165,6 @@ async def test_local_form(hass): ), patch( "homeassistant.components.risco.config_flow.RiscoLocal.disconnect" ) as mock_close, patch( - "homeassistant.components.risco.config_flow.SLEEP_INTERVAL", - 0, - ), patch( "homeassistant.components.risco.async_setup_entry", return_value=True, ) as mock_setup_entry: From d0d1b303fd0a4dc5c5e1f9f16ae0327d9e3489ed Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Thu, 1 Sep 2022 12:02:46 -0600 Subject: [PATCH 030/955] Code quality improvements for litterrobot integration (#77605) --- .../components/litterrobot/button.py | 52 +++++++++---------- .../components/litterrobot/entity.py | 50 ++++++++++++------ .../components/litterrobot/select.py | 20 ++++--- .../components/litterrobot/sensor.py | 32 +++++------- .../components/litterrobot/switch.py | 25 ++++----- .../components/litterrobot/vacuum.py | 19 +++---- tests/components/litterrobot/test_vacuum.py | 19 +++++++ 7 files changed, 123 insertions(+), 94 deletions(-) diff --git a/homeassistant/components/litterrobot/button.py b/homeassistant/components/litterrobot/button.py index 74c659fd474..81d9c65927e 100644 --- a/homeassistant/components/litterrobot/button.py +++ b/homeassistant/components/litterrobot/button.py @@ -1,21 +1,25 @@ """Support for Litter-Robot button.""" from __future__ import annotations -from collections.abc import Callable, Coroutine, Iterable +from collections.abc import Callable, Coroutine from dataclasses import dataclass import itertools from typing import Any, Generic from pylitterbot import FeederRobot, LitterRobot3 -from homeassistant.components.button import ButtonEntity, ButtonEntityDescription +from homeassistant.components.button import ( + DOMAIN as PLATFORM, + ButtonEntity, + ButtonEntityDescription, +) 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 .const import DOMAIN -from .entity import LitterRobotEntity, _RobotT +from .entity import LitterRobotEntity, _RobotT, async_update_unique_id from .hub import LitterRobotHub @@ -26,21 +30,24 @@ async def async_setup_entry( ) -> None: """Set up Litter-Robot cleaner using config entry.""" hub: LitterRobotHub = hass.data[DOMAIN][entry.entry_id] - entities: Iterable[LitterRobotButtonEntity] = itertools.chain( - ( - LitterRobotButtonEntity( - robot=robot, hub=hub, description=LITTER_ROBOT_BUTTON - ) - for robot in hub.litter_robots() - if isinstance(robot, LitterRobot3) - ), - ( - LitterRobotButtonEntity( - robot=robot, hub=hub, description=FEEDER_ROBOT_BUTTON - ) - for robot in hub.feeder_robots() - ), + entities: list[LitterRobotButtonEntity] = list( + itertools.chain( + ( + LitterRobotButtonEntity( + robot=robot, hub=hub, description=LITTER_ROBOT_BUTTON + ) + for robot in hub.litter_robots() + if isinstance(robot, LitterRobot3) + ), + ( + LitterRobotButtonEntity( + robot=robot, hub=hub, description=FEEDER_ROBOT_BUTTON + ) + for robot in hub.feeder_robots() + ), + ) ) + async_update_unique_id(hass, PLATFORM, entities) async_add_entities(entities) @@ -76,17 +83,6 @@ class LitterRobotButtonEntity(LitterRobotEntity[_RobotT], ButtonEntity): entity_description: RobotButtonEntityDescription[_RobotT] - def __init__( - self, - robot: _RobotT, - hub: LitterRobotHub, - description: RobotButtonEntityDescription[_RobotT], - ) -> None: - """Initialize a Litter-Robot button entity.""" - assert description.name - super().__init__(robot, description.name, hub) - self.entity_description = description - async def async_press(self) -> None: """Press the button.""" await self.entity_description.press_fn(self.robot) diff --git a/homeassistant/components/litterrobot/entity.py b/homeassistant/components/litterrobot/entity.py index 8471e007ce9..9716793f70e 100644 --- a/homeassistant/components/litterrobot/entity.py +++ b/homeassistant/components/litterrobot/entity.py @@ -1,7 +1,7 @@ """Litter-Robot entities for common data and methods.""" from __future__ import annotations -from collections.abc import Callable, Coroutine +from collections.abc import Callable, Coroutine, Iterable from datetime import time import logging from typing import Any, Generic, TypeVar @@ -10,8 +10,9 @@ from pylitterbot import Robot from pylitterbot.exceptions import InvalidCommandException from typing_extensions import ParamSpec -from homeassistant.core import CALLBACK_TYPE, callback -from homeassistant.helpers.entity import DeviceInfo, EntityCategory +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.helpers.entity import DeviceInfo, EntityCategory, EntityDescription +import homeassistant.helpers.entity_registry as er from homeassistant.helpers.event import async_call_later from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -36,18 +37,18 @@ class LitterRobotEntity( _attr_has_entity_name = True - def __init__(self, robot: _RobotT, entity_type: str, hub: LitterRobotHub) -> None: + def __init__( + self, robot: _RobotT, hub: LitterRobotHub, description: EntityDescription + ) -> None: """Pass coordinator to CoordinatorEntity.""" super().__init__(hub.coordinator) self.robot = robot - self.entity_type = entity_type self.hub = hub - self._attr_name = entity_type.capitalize() - - @property - def unique_id(self) -> str: - """Return a unique ID.""" - return f"{self.robot.serial}-{self.entity_type}" + self.entity_description = description + self._attr_unique_id = f"{self.robot.serial}-{description.key}" + # The following can be removed in 2022.12 after adjusting names in entities appropriately + if description.name is not None: + self._attr_name = description.name.capitalize() @property def device_info(self) -> DeviceInfo: @@ -65,9 +66,11 @@ class LitterRobotEntity( class LitterRobotControlEntity(LitterRobotEntity[_RobotT]): """A Litter-Robot entity that can control the unit.""" - def __init__(self, robot: _RobotT, entity_type: str, hub: LitterRobotHub) -> None: + def __init__( + self, robot: _RobotT, hub: LitterRobotHub, description: EntityDescription + ) -> None: """Init a Litter-Robot control entity.""" - super().__init__(robot=robot, entity_type=entity_type, hub=hub) + super().__init__(robot=robot, hub=hub, description=description) self._refresh_callback: CALLBACK_TYPE | None = None async def perform_action_and_refresh( @@ -134,9 +137,11 @@ class LitterRobotConfigEntity(LitterRobotControlEntity[_RobotT]): _attr_entity_category = EntityCategory.CONFIG - def __init__(self, robot: _RobotT, entity_type: str, hub: LitterRobotHub) -> None: + def __init__( + self, robot: _RobotT, hub: LitterRobotHub, description: EntityDescription + ) -> None: """Init a Litter-Robot control entity.""" - super().__init__(robot=robot, entity_type=entity_type, hub=hub) + super().__init__(robot=robot, hub=hub, description=description) self._assumed_state: bool | None = None async def perform_action_and_assume_state( @@ -146,3 +151,18 @@ class LitterRobotConfigEntity(LitterRobotControlEntity[_RobotT]): if await self.perform_action_and_refresh(action, assumed_state): self._assumed_state = assumed_state self.async_write_ha_state() + + +def async_update_unique_id( + hass: HomeAssistant, domain: str, entities: Iterable[LitterRobotEntity[_RobotT]] +) -> None: + """Update unique ID to be based on entity description key instead of name. + + Introduced with release 2022.9. + """ + ent_reg = er.async_get(hass) + for entity in entities: + old_unique_id = f"{entity.robot.serial}-{entity.entity_description.name}" + if entity_id := ent_reg.async_get_entity_id(domain, DOMAIN, old_unique_id): + new_unique_id = f"{entity.robot.serial}-{entity.entity_description.key}" + ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id) diff --git a/homeassistant/components/litterrobot/select.py b/homeassistant/components/litterrobot/select.py index a18cd3b46b5..9ec784db8f2 100644 --- a/homeassistant/components/litterrobot/select.py +++ b/homeassistant/components/litterrobot/select.py @@ -8,13 +8,18 @@ from typing import Any, Generic, TypeVar from pylitterbot import FeederRobot, LitterRobot -from homeassistant.components.select import SelectEntity, SelectEntityDescription +from homeassistant.components.select import ( + DOMAIN as PLATFORM, + SelectEntity, + SelectEntityDescription, +) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import TIME_MINUTES from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .entity import LitterRobotConfigEntity, _RobotT +from .entity import LitterRobotConfigEntity, _RobotT, async_update_unique_id from .hub import LitterRobotHub _CastTypeT = TypeVar("_CastTypeT", int, float) @@ -40,9 +45,10 @@ class RobotSelectEntityDescription( LITTER_ROBOT_SELECT = RobotSelectEntityDescription[LitterRobot, int]( - key="clean_cycle_wait_time_minutes", + key="cycle_delay", name="Clean Cycle Wait Time Minutes", icon="mdi:timer-outline", + unit_of_measurement=TIME_MINUTES, current_fn=lambda robot: robot.clean_cycle_wait_time_minutes, options_fn=lambda robot: robot.VALID_WAIT_TIMES, select_fn=lambda robot, option: (robot.set_wait_time, int(option)), @@ -65,7 +71,7 @@ async def async_setup_entry( ) -> None: """Set up Litter-Robot selects using config entry.""" hub: LitterRobotHub = hass.data[DOMAIN][config_entry.entry_id] - async_add_entities( + entities: list[LitterRobotSelect] = list( itertools.chain( ( LitterRobotSelect(robot=robot, hub=hub, description=LITTER_ROBOT_SELECT) @@ -77,6 +83,8 @@ async def async_setup_entry( ), ) ) + async_update_unique_id(hass, PLATFORM, entities) + async_add_entities(entities) class LitterRobotSelect( @@ -93,9 +101,7 @@ class LitterRobotSelect( description: RobotSelectEntityDescription[_RobotT, _CastTypeT], ) -> None: """Initialize a Litter-Robot select entity.""" - assert description.name - super().__init__(robot, description.name, hub) - self.entity_description = description + super().__init__(robot, hub, description) options = self.entity_description.options_fn(self.robot) self._attr_options = list(map(str, options)) diff --git a/homeassistant/components/litterrobot/sensor.py b/homeassistant/components/litterrobot/sensor.py index 90bdfcbda73..c904335d23f 100644 --- a/homeassistant/components/litterrobot/sensor.py +++ b/homeassistant/components/litterrobot/sensor.py @@ -9,6 +9,7 @@ from typing import Any, Generic, Union, cast from pylitterbot import FeederRobot, LitterRobot, LitterRobot4, Robot from homeassistant.components.sensor import ( + DOMAIN as PLATFORM, SensorDeviceClass, SensorEntity, SensorEntityDescription, @@ -20,7 +21,7 @@ from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .entity import LitterRobotEntity, _RobotT +from .entity import LitterRobotEntity, _RobotT, async_update_unique_id from .hub import LitterRobotHub @@ -48,17 +49,6 @@ class LitterRobotSensorEntity(LitterRobotEntity[_RobotT], SensorEntity): entity_description: RobotSensorEntityDescription[_RobotT] - def __init__( - self, - robot: _RobotT, - hub: LitterRobotHub, - description: RobotSensorEntityDescription[_RobotT], - ) -> None: - """Initialize a Litter-Robot sensor entity.""" - assert description.name - super().__init__(robot, description.name, hub) - self.entity_description = description - @property def native_value(self) -> float | datetime | str | None: """Return the state.""" @@ -79,32 +69,32 @@ class LitterRobotSensorEntity(LitterRobotEntity[_RobotT], SensorEntity): ROBOT_SENSOR_MAP: dict[type[Robot], list[RobotSensorEntityDescription]] = { LitterRobot: [ RobotSensorEntityDescription[LitterRobot]( - name="Waste Drawer", key="waste_drawer_level", + name="Waste Drawer", native_unit_of_measurement=PERCENTAGE, icon_fn=lambda state: icon_for_gauge_level(state, 10), ), RobotSensorEntityDescription[LitterRobot]( - name="Sleep Mode Start Time", key="sleep_mode_start_time", + name="Sleep Mode Start Time", device_class=SensorDeviceClass.TIMESTAMP, should_report=lambda robot: robot.sleep_mode_enabled, ), RobotSensorEntityDescription[LitterRobot]( - name="Sleep Mode End Time", key="sleep_mode_end_time", + name="Sleep Mode End Time", device_class=SensorDeviceClass.TIMESTAMP, should_report=lambda robot: robot.sleep_mode_enabled, ), RobotSensorEntityDescription[LitterRobot]( - name="Last Seen", key="last_seen", + name="Last Seen", device_class=SensorDeviceClass.TIMESTAMP, entity_category=EntityCategory.DIAGNOSTIC, ), RobotSensorEntityDescription[LitterRobot]( - name="Status Code", key="status_code", + name="Status Code", device_class="litterrobot__status_code", entity_category=EntityCategory.DIAGNOSTIC, ), @@ -119,8 +109,8 @@ ROBOT_SENSOR_MAP: dict[type[Robot], list[RobotSensorEntityDescription]] = { ], FeederRobot: [ RobotSensorEntityDescription[FeederRobot]( - name="Food level", key="food_level", + name="Food level", native_unit_of_measurement=PERCENTAGE, icon_fn=lambda state: icon_for_gauge_level(state, 10), ) @@ -135,10 +125,12 @@ async def async_setup_entry( ) -> None: """Set up Litter-Robot sensors using config entry.""" hub: LitterRobotHub = hass.data[DOMAIN][entry.entry_id] - async_add_entities( + entities = [ LitterRobotSensorEntity(robot=robot, hub=hub, description=description) for robot in hub.account.robots for robot_type, entity_descriptions in ROBOT_SENSOR_MAP.items() if isinstance(robot, robot_type) for description in entity_descriptions - ) + ] + async_update_unique_id(hass, PLATFORM, entities) + async_add_entities(entities) diff --git a/homeassistant/components/litterrobot/switch.py b/homeassistant/components/litterrobot/switch.py index 2f54ede38b8..779ee699b41 100644 --- a/homeassistant/components/litterrobot/switch.py +++ b/homeassistant/components/litterrobot/switch.py @@ -7,13 +7,17 @@ from typing import Any, Generic, Union from pylitterbot import FeederRobot, LitterRobot -from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription +from homeassistant.components.switch import ( + DOMAIN as PLATFORM, + SwitchEntity, + SwitchEntityDescription, +) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .entity import LitterRobotConfigEntity, _RobotT +from .entity import LitterRobotConfigEntity, _RobotT, async_update_unique_id from .hub import LitterRobotHub @@ -51,17 +55,6 @@ class RobotSwitchEntity(LitterRobotConfigEntity[_RobotT], SwitchEntity): entity_description: RobotSwitchEntityDescription[_RobotT] - def __init__( - self, - robot: _RobotT, - hub: LitterRobotHub, - description: RobotSwitchEntityDescription[_RobotT], - ) -> None: - """Initialize a Litter-Robot switch entity.""" - assert description.name - super().__init__(robot, description.name, hub) - self.entity_description = description - @property def is_on(self) -> bool | None: """Return true if switch is on.""" @@ -93,9 +86,11 @@ async def async_setup_entry( ) -> None: """Set up Litter-Robot switches using config entry.""" hub: LitterRobotHub = hass.data[DOMAIN][entry.entry_id] - async_add_entities( + entities = [ RobotSwitchEntity(robot=robot, hub=hub, description=description) for description in ROBOT_SWITCHES for robot in hub.account.robots if isinstance(robot, (LitterRobot, FeederRobot)) - ) + ] + async_update_unique_id(hass, PLATFORM, entities) + async_add_entities(entities) diff --git a/homeassistant/components/litterrobot/vacuum.py b/homeassistant/components/litterrobot/vacuum.py index 9a4b825045f..27cd3e6758a 100644 --- a/homeassistant/components/litterrobot/vacuum.py +++ b/homeassistant/components/litterrobot/vacuum.py @@ -1,7 +1,6 @@ """Support for Litter-Robot "Vacuum".""" from __future__ import annotations -import logging from typing import Any from pylitterbot import LitterRobot @@ -9,11 +8,13 @@ from pylitterbot.enums import LitterBoxStatus import voluptuous as vol from homeassistant.components.vacuum import ( + DOMAIN as PLATFORM, STATE_CLEANING, STATE_DOCKED, STATE_ERROR, STATE_PAUSED, StateVacuumEntity, + StateVacuumEntityDescription, VacuumEntityFeature, ) from homeassistant.config_entries import ConfigEntry @@ -23,13 +24,9 @@ from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .entity import LitterRobotControlEntity +from .entity import LitterRobotControlEntity, async_update_unique_id from .hub import LitterRobotHub -_LOGGER = logging.getLogger(__name__) - -TYPE_LITTER_BOX = "Litter Box" - SERVICE_SET_SLEEP_MODE = "set_sleep_mode" LITTER_BOX_STATUS_STATE_MAP = { @@ -44,6 +41,8 @@ LITTER_BOX_STATUS_STATE_MAP = { LitterBoxStatus.OFF: STATE_OFF, } +LITTER_BOX_ENTITY = StateVacuumEntityDescription("litter_box", name="Litter Box") + async def async_setup_entry( hass: HomeAssistant, @@ -53,10 +52,12 @@ async def async_setup_entry( """Set up Litter-Robot cleaner using config entry.""" hub: LitterRobotHub = hass.data[DOMAIN][entry.entry_id] - async_add_entities( - LitterRobotCleaner(robot=robot, entity_type=TYPE_LITTER_BOX, hub=hub) + entities = [ + LitterRobotCleaner(robot=robot, hub=hub, description=LITTER_BOX_ENTITY) for robot in hub.litter_robots() - ) + ] + async_update_unique_id(hass, PLATFORM, entities) + async_add_entities(entities) platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( diff --git a/tests/components/litterrobot/test_vacuum.py b/tests/components/litterrobot/test_vacuum.py index 02667bb8310..eb9a4c8c60b 100644 --- a/tests/components/litterrobot/test_vacuum.py +++ b/tests/components/litterrobot/test_vacuum.py @@ -21,6 +21,7 @@ from homeassistant.components.vacuum import ( ) from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant +import homeassistant.helpers.entity_registry as er from homeassistant.util.dt import utcnow from .common import VACUUM_ENTITY_ID @@ -28,6 +29,9 @@ from .conftest import setup_integration from tests.common import async_fire_time_changed +VACUUM_UNIQUE_ID_OLD = "LR3C012345-Litter Box" +VACUUM_UNIQUE_ID_NEW = "LR3C012345-litter_box" + COMPONENT_SERVICE_DOMAIN = { SERVICE_SET_SLEEP_MODE: DOMAIN, } @@ -35,6 +39,18 @@ COMPONENT_SERVICE_DOMAIN = { async def test_vacuum(hass: HomeAssistant, mock_account: MagicMock) -> None: """Tests the vacuum entity was set up.""" + ent_reg = er.async_get(hass) + + # Create entity entry to migrate to new unique ID + ent_reg.async_get_or_create( + PLATFORM_DOMAIN, + DOMAIN, + VACUUM_UNIQUE_ID_OLD, + suggested_object_id=VACUUM_ENTITY_ID.replace(PLATFORM_DOMAIN, ""), + ) + ent_reg_entry = ent_reg.async_get(VACUUM_ENTITY_ID) + assert ent_reg_entry.unique_id == VACUUM_UNIQUE_ID_OLD + await setup_integration(hass, mock_account, PLATFORM_DOMAIN) assert hass.services.has_service(DOMAIN, SERVICE_SET_SLEEP_MODE) @@ -43,6 +59,9 @@ async def test_vacuum(hass: HomeAssistant, mock_account: MagicMock) -> None: assert vacuum.state == STATE_DOCKED assert vacuum.attributes["is_sleeping"] is False + ent_reg_entry = ent_reg.async_get(VACUUM_ENTITY_ID) + assert ent_reg_entry.unique_id == VACUUM_UNIQUE_ID_NEW + async def test_vacuum_status_when_sleeping( hass: HomeAssistant, mock_account_with_sleeping_robot: MagicMock From e326dd2847a18913ec10050e5a0ac01dbd4209fc Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 1 Sep 2022 20:03:37 +0200 Subject: [PATCH 031/955] Fix demo external energy statistics (#77665) --- homeassistant/components/demo/__init__.py | 27 ++++++++++++++--------- tests/components/demo/test_init.py | 6 ++++- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/demo/__init__.py b/homeassistant/components/demo/__init__.py index cc80edbd484..45ce061fb1d 100644 --- a/homeassistant/components/demo/__init__.py +++ b/homeassistant/components/demo/__init__.py @@ -16,8 +16,13 @@ from homeassistant.components.recorder.statistics import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, + ENERGY_KILO_WATT_HOUR, + ENERGY_MEGA_WATT_HOUR, EVENT_HOMEASSISTANT_START, SOUND_PRESSURE_DB, + TEMP_CELSIUS, + VOLUME_CUBIC_FEET, + VOLUME_CUBIC_METERS, Platform, ) import homeassistant.core as ha @@ -262,7 +267,7 @@ async def _insert_sum_statistics( statistic_id = metadata["statistic_id"] last_stats = await get_instance(hass).async_add_executor_job( - get_last_statistics, hass, 1, statistic_id, True + get_last_statistics, hass, 1, statistic_id, False ) if statistic_id in last_stats: sum_ = last_stats[statistic_id][0]["sum"] or 0 @@ -291,7 +296,7 @@ async def _insert_statistics(hass: HomeAssistant) -> None: "source": DOMAIN, "name": "Outdoor temperature", "statistic_id": f"{DOMAIN}:temperature_outdoor", - "unit_of_measurement": "°C", + "unit_of_measurement": TEMP_CELSIUS, "has_mean": True, "has_sum": False, } @@ -304,11 +309,11 @@ async def _insert_statistics(hass: HomeAssistant) -> None: "source": DOMAIN, "name": "Energy consumption 1", "statistic_id": f"{DOMAIN}:energy_consumption_kwh", - "unit_of_measurement": "kWh", + "unit_of_measurement": ENERGY_KILO_WATT_HOUR, "has_mean": False, "has_sum": True, } - await _insert_sum_statistics(hass, metadata, yesterday_midnight, today_midnight, 2) + await _insert_sum_statistics(hass, metadata, yesterday_midnight, today_midnight, 1) # Add external energy consumption in MWh, ~ 12 kWh / day # This should not be possible to pick for the energy dashboard @@ -316,12 +321,12 @@ async def _insert_statistics(hass: HomeAssistant) -> None: "source": DOMAIN, "name": "Energy consumption 2", "statistic_id": f"{DOMAIN}:energy_consumption_mwh", - "unit_of_measurement": "MWh", + "unit_of_measurement": ENERGY_MEGA_WATT_HOUR, "has_mean": False, "has_sum": True, } await _insert_sum_statistics( - hass, metadata, yesterday_midnight, today_midnight, 0.002 + hass, metadata, yesterday_midnight, today_midnight, 0.001 ) # Add external gas consumption in m³, ~6 m3/day @@ -330,11 +335,13 @@ async def _insert_statistics(hass: HomeAssistant) -> None: "source": DOMAIN, "name": "Gas consumption 1", "statistic_id": f"{DOMAIN}:gas_consumption_m3", - "unit_of_measurement": "m³", + "unit_of_measurement": VOLUME_CUBIC_METERS, "has_mean": False, "has_sum": True, } - await _insert_sum_statistics(hass, metadata, yesterday_midnight, today_midnight, 1) + await _insert_sum_statistics( + hass, metadata, yesterday_midnight, today_midnight, 0.5 + ) # Add external gas consumption in ft³, ~180 ft3/day # This should not be possible to pick for the energy dashboard @@ -342,11 +349,11 @@ async def _insert_statistics(hass: HomeAssistant) -> None: "source": DOMAIN, "name": "Gas consumption 2", "statistic_id": f"{DOMAIN}:gas_consumption_ft3", - "unit_of_measurement": "ft³", + "unit_of_measurement": VOLUME_CUBIC_FEET, "has_mean": False, "has_sum": True, } - await _insert_sum_statistics(hass, metadata, yesterday_midnight, today_midnight, 30) + await _insert_sum_statistics(hass, metadata, yesterday_midnight, today_midnight, 15) async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: diff --git a/tests/components/demo/test_init.py b/tests/components/demo/test_init.py index 1577b411c23..f7fec9629ac 100644 --- a/tests/components/demo/test_init.py +++ b/tests/components/demo/test_init.py @@ -17,6 +17,7 @@ from homeassistant.components.repairs import DOMAIN as REPAIRS_DOMAIN from homeassistant.helpers.json import JSONEncoder from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from homeassistant.util.unit_system import IMPERIAL_SYSTEM from tests.components.recorder.common import async_wait_recording_done @@ -83,6 +84,8 @@ async def test_demo_statistics(hass, recorder_mock): async def test_demo_statistics_growth(hass, recorder_mock): """Test that the demo sum statistics adds to the previous state.""" + hass.config.units = IMPERIAL_SYSTEM + now = dt_util.now() last_week = now - datetime.timedelta(days=7) last_week_midnight = last_week.replace(hour=0, minute=0, second=0, microsecond=0) @@ -92,7 +95,7 @@ async def test_demo_statistics_growth(hass, recorder_mock): "source": DOMAIN, "name": "Energy consumption 1", "statistic_id": statistic_id, - "unit_of_measurement": "kWh", + "unit_of_measurement": "m³", "has_mean": False, "has_sum": True, } @@ -114,6 +117,7 @@ async def test_demo_statistics_growth(hass, recorder_mock): get_last_statistics, hass, 1, statistic_id, False ) assert statistics[statistic_id][0]["sum"] > 2**20 + assert statistics[statistic_id][0]["sum"] <= (2**20 + 24) async def test_issues_created(hass, hass_client, hass_ws_client): From 9a5bdaf87eae493751423a7e422980390bed09cc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 1 Sep 2022 18:10:20 +0000 Subject: [PATCH 032/955] Ensure unique id is set for esphome when setup via user flow (#77677) --- homeassistant/components/esphome/__init__.py | 4 +++ .../components/esphome/config_flow.py | 2 ++ tests/components/esphome/test_config_flow.py | 30 +++++++++++++++++++ 3 files changed, 36 insertions(+) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 2a885ed90ec..07b6d3071f6 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -333,6 +333,10 @@ async def async_setup_entry( # noqa: C901 if entry_data.device_info is not None and entry_data.device_info.name: cli.expected_name = entry_data.device_info.name reconnect_logic.name = entry_data.device_info.name + if entry.unique_id is None: + hass.config_entries.async_update_entry( + entry, unique_id=entry_data.device_info.name + ) await reconnect_logic.start() entry_data.cleanup_callbacks.append(reconnect_logic.stop_callback) diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index 9fd12634e43..ea64fb7fb7f 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -331,6 +331,8 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): await cli.disconnect(force=True) self._name = self._device_info.name + await self.async_set_unique_id(self._name, raise_on_progress=False) + self._abort_if_unique_id_configured(updates={CONF_HOST: self._host}) return None diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index 43e2f916082..a4d1f416868 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -81,6 +81,7 @@ async def test_user_connection_works(hass, mock_client, mock_zeroconf): CONF_NOISE_PSK: "", } assert result["title"] == "test" + assert result["result"].unique_id == "test" assert len(mock_client.connect.mock_calls) == 1 assert len(mock_client.device_info.mock_calls) == 1 @@ -91,6 +92,35 @@ async def test_user_connection_works(hass, mock_client, mock_zeroconf): assert mock_client.noise_psk is None +async def test_user_connection_updates_host(hass, mock_client, mock_zeroconf): + """Test setup up the same name updates the host.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: "test.local", CONF_PORT: 6053, CONF_PASSWORD: ""}, + unique_id="test", + ) + entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + "esphome", + context={"source": config_entries.SOURCE_USER}, + data=None, + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + mock_client.device_info = AsyncMock(return_value=MockDeviceInfo(False, "test")) + + result = await hass.config_entries.flow.async_init( + "esphome", + context={"source": config_entries.SOURCE_USER}, + data={CONF_HOST: "127.0.0.1", CONF_PORT: 80}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + assert entry.data[CONF_HOST] == "127.0.0.1" + + async def test_user_resolve_error(hass, mock_client, mock_zeroconf): """Test user step with IP resolve error.""" From 7bac5fecee3b00533436a10b665069130f3000ef Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 1 Sep 2022 19:00:14 +0000 Subject: [PATCH 033/955] Bump pySwitchbot to 0.18.22 (#77673) --- 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 cda3f958f5c..9fb73a62dd6 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.18.21"], + "requirements": ["PySwitchbot==0.18.22"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 3fa89f3898e..98f0adc0d3d 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.18.21 +PySwitchbot==0.18.22 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e9bbc312652..242952979e1 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.18.21 +PySwitchbot==0.18.22 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From 57c766c03c71398081b61b64d279a27570092406 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 1 Sep 2022 15:00:50 -0400 Subject: [PATCH 034/955] Pin Pandas 1.4.3 (#77679) --- homeassistant/package_constraints.txt | 3 +++ script/gen_requirements_all.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a456e9ec965..84edd18206c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -126,3 +126,6 @@ pubnub!=6.4.0 # Package's __init__.pyi stub has invalid syntax and breaks mypy # https://github.com/dahlia/iso4217/issues/16 iso4217!=1.10.20220401 + +# Pandas 1.4.4 has issues with wheels om armhf + Py3.10 +pandas==1.4.3 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 3709d4cff08..d0eb830f088 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -139,6 +139,9 @@ pubnub!=6.4.0 # Package's __init__.pyi stub has invalid syntax and breaks mypy # https://github.com/dahlia/iso4217/issues/16 iso4217!=1.10.20220401 + +# Pandas 1.4.4 has issues with wheels om armhf + Py3.10 +pandas==1.4.3 """ IGNORE_PRE_COMMIT_HOOK_ID = ( From 73e26b71b1d4d2ee8707952e46eff04836bc404f Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 1 Sep 2022 15:32:32 -0400 Subject: [PATCH 035/955] Migrate ZHA lighting to use newer zigpy ZCL request syntax (#77676) * Migrate unit test to use more command definition constants * Use keyword argument syntax for sending ZCL requests * Ensure all ZHA unit tests pass --- homeassistant/components/zha/core/const.py | 6 - homeassistant/components/zha/core/device.py | 7 +- homeassistant/components/zha/light.py | 96 +++++++------ tests/components/zha/test_discover.py | 8 +- tests/components/zha/test_light.py | 152 ++++++++++---------- 5 files changed, 132 insertions(+), 137 deletions(-) diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index a1c5a55bd76..311d3d48b5b 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -396,12 +396,6 @@ ZHA_GW_MSG_LOG_OUTPUT = "log_output" ZHA_GW_MSG_RAW_INIT = "raw_device_initialized" ZHA_DEVICES_LOADED_EVENT = "zha_devices_loaded_event" -EFFECT_BLINK = 0x00 -EFFECT_BREATHE = 0x01 -EFFECT_OKAY = 0x02 - -EFFECT_DEFAULT_VARIANT = 0x00 - class Strobe(t.enum8): """Strobe enum.""" diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index 150241a091b..a0a4521e19d 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -17,7 +17,7 @@ import zigpy.exceptions from zigpy.profiles import PROFILES import zigpy.quirks from zigpy.types.named import EUI64, NWK -from zigpy.zcl.clusters.general import Groups +from zigpy.zcl.clusters.general import Groups, Identify import zigpy.zdo.types as zdo_types from homeassistant.const import ATTR_COMMAND, ATTR_NAME @@ -65,8 +65,6 @@ from .const import ( CONF_DEFAULT_CONSIDER_UNAVAILABLE_BATTERY, CONF_DEFAULT_CONSIDER_UNAVAILABLE_MAINS, CONF_ENABLE_IDENTIFY_ON_JOIN, - EFFECT_DEFAULT_VARIANT, - EFFECT_OKAY, POWER_BATTERY_OR_UNKNOWN, POWER_MAINS_POWERED, SIGNAL_AVAILABLE, @@ -488,7 +486,8 @@ class ZHADevice(LogMixin): and not self.skip_configuration ): await self._channels.identify_ch.trigger_effect( - EFFECT_OKAY, EFFECT_DEFAULT_VARIANT + effect_id=Identify.EffectIdentifier.Okay, + effect_variant=Identify.EffectVariant.Default, ) async def async_initialize(self, from_cache: bool = False) -> None: diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 5a8011e2386..dbb250a5d33 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -47,9 +47,6 @@ from .core.const import ( CONF_ENABLE_ENHANCED_LIGHT_TRANSITION, CONF_ENABLE_LIGHT_TRANSITIONING_FLAG, DATA_ZHA, - EFFECT_BLINK, - EFFECT_BREATHE, - EFFECT_DEFAULT_VARIANT, SIGNAL_ADD_ENTITIES, SIGNAL_ATTR_UPDATED, SIGNAL_SET_LEVEL, @@ -70,14 +67,11 @@ DEFAULT_EXTRA_TRANSITION_DELAY_LONG = 2.0 DEFAULT_LONG_TRANSITION_TIME = 10 DEFAULT_MIN_BRIGHTNESS = 2 -UPDATE_COLORLOOP_ACTION = 0x1 -UPDATE_COLORLOOP_DIRECTION = 0x2 -UPDATE_COLORLOOP_TIME = 0x4 -UPDATE_COLORLOOP_HUE = 0x8 +FLASH_EFFECTS = { + light.FLASH_SHORT: Identify.EffectIdentifier.Blink, + light.FLASH_LONG: Identify.EffectIdentifier.Breathe, +} -FLASH_EFFECTS = {light.FLASH_SHORT: EFFECT_BLINK, light.FLASH_LONG: EFFECT_BREATHE} - -UNSUPPORTED_ATTRIBUTE = 0x86 STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, Platform.LIGHT) GROUP_MATCH = functools.partial(ZHA_ENTITIES.group_match, Platform.LIGHT) PARALLEL_UPDATES = 0 @@ -157,7 +151,7 @@ class BaseLight(LogMixin, light.LightEntity): return self._attr_state @callback - def set_level(self, value): + def set_level(self, value: int) -> None: """Set the brightness of this light between 0..254. brightness level 255 is a special value instructing the device to come @@ -275,7 +269,8 @@ class BaseLight(LogMixin, light.LightEntity): # If the light is currently off, we first need to turn it on at a low brightness level with no transition. # After that, we set it to the desired color/temperature with no transition. result = await self._level_channel.move_to_level_with_on_off( - DEFAULT_MIN_BRIGHTNESS, self._DEFAULT_MIN_TRANSITION_TIME + level=DEFAULT_MIN_BRIGHTNESS, + transition_time=self._DEFAULT_MIN_TRANSITION_TIME, ) t_log["move_to_level_with_on_off"] = result if isinstance(result, Exception) or result[1] is not Status.SUCCESS: @@ -294,7 +289,8 @@ class BaseLight(LogMixin, light.LightEntity): and brightness_supported(self._attr_supported_color_modes) ): result = await self._level_channel.move_to_level_with_on_off( - level, duration + level=level, + transition_time=duration, ) t_log["move_to_level_with_on_off"] = result if isinstance(result, Exception) or result[1] is not Status.SUCCESS: @@ -340,7 +336,9 @@ class BaseLight(LogMixin, light.LightEntity): if new_color_provided_while_off: # The light is has the correct color, so we can now transition it to the correct brightness level. - result = await self._level_channel.move_to_level(level, duration) + result = await self._level_channel.move_to_level( + level=level, transition_time=duration + ) t_log["move_to_level_if_color"] = result if isinstance(result, Exception) or result[1] is not Status.SUCCESS: self.debug("turned on: %s", t_log) @@ -355,13 +353,15 @@ class BaseLight(LogMixin, light.LightEntity): if effect == light.EFFECT_COLORLOOP: result = await self._color_channel.color_loop_set( - UPDATE_COLORLOOP_ACTION - | UPDATE_COLORLOOP_DIRECTION - | UPDATE_COLORLOOP_TIME, - 0x2, # start from current hue - 0x1, # only support up - transition if transition else 7, # transition - 0, # no hue + update_flags=( + Color.ColorLoopUpdateFlags.Action + | Color.ColorLoopUpdateFlags.Direction + | Color.ColorLoopUpdateFlags.Time + ), + action=Color.ColorLoopAction.Activate_from_current_hue, + direction=Color.ColorLoopDirection.Increment, + time=transition if transition else 7, + start_hue=0, ) t_log["color_loop_set"] = result self._attr_effect = light.EFFECT_COLORLOOP @@ -370,18 +370,19 @@ class BaseLight(LogMixin, light.LightEntity): and effect != light.EFFECT_COLORLOOP ): result = await self._color_channel.color_loop_set( - UPDATE_COLORLOOP_ACTION, - 0x0, - 0x0, - 0x0, - 0x0, # update action only, action off, no dir, time, hue + update_flags=Color.ColorLoopUpdateFlags.Action, + action=Color.ColorLoopAction.Deactivate, + direction=Color.ColorLoopDirection.Decrement, + time=0, + start_hue=0, ) t_log["color_loop_set"] = result self._attr_effect = None if flash is not None: result = await self._identify_channel.trigger_effect( - FLASH_EFFECTS[flash], EFFECT_DEFAULT_VARIANT + effect_id=FLASH_EFFECTS[flash], + effect_variant=Identify.EffectVariant.Default, ) t_log["trigger_effect"] = result @@ -400,6 +401,7 @@ class BaseLight(LogMixin, light.LightEntity): if transition is not None else DEFAULT_ON_OFF_TRANSITION ) + DEFAULT_EXTRA_TRANSITION_DELAY_SHORT + # Start pausing attribute report parsing if self._zha_config_enable_light_transitioning_flag: self.async_transition_set_flag() @@ -407,7 +409,8 @@ class BaseLight(LogMixin, light.LightEntity): # is not none looks odd here but it will override built in bulb transition times if we pass 0 in here if transition is not None and supports_level: result = await self._level_channel.move_to_level_with_on_off( - 0, transition * 10 or self._DEFAULT_MIN_TRANSITION_TIME + level=0, + transition_time=(transition * 10 or self._DEFAULT_MIN_TRANSITION_TIME), ) else: result = await self._on_off_channel.off() @@ -437,12 +440,17 @@ class BaseLight(LogMixin, light.LightEntity): t_log, ): """Process ZCL color commands.""" + + transition_time = ( + self._DEFAULT_MIN_TRANSITION_TIME + if new_color_provided_while_off + else duration + ) + if temperature is not None: result = await self._color_channel.move_to_color_temp( - temperature, - self._DEFAULT_MIN_TRANSITION_TIME - if new_color_provided_while_off - else duration, + color_temp_mireds=temperature, + transition_time=transition_time, ) t_log["move_to_color_temp"] = result if isinstance(result, Exception) or result[1] is not Status.SUCCESS: @@ -458,20 +466,16 @@ class BaseLight(LogMixin, light.LightEntity): and self._color_channel.enhanced_hue_supported ): result = await self._color_channel.enhanced_move_to_hue_and_saturation( - int(hs_color[0] * 65535 / 360), - int(hs_color[1] * 2.54), - self._DEFAULT_MIN_TRANSITION_TIME - if new_color_provided_while_off - else duration, + enhanced_hue=int(hs_color[0] * 65535 / 360), + saturation=int(hs_color[1] * 2.54), + transition_time=transition_time, ) t_log["enhanced_move_to_hue_and_saturation"] = result else: result = await self._color_channel.move_to_hue_and_saturation( - int(hs_color[0] * 254 / 360), - int(hs_color[1] * 2.54), - self._DEFAULT_MIN_TRANSITION_TIME - if new_color_provided_while_off - else duration, + hue=int(hs_color[0] * 254 / 360), + saturation=int(hs_color[1] * 2.54), + transition_time=transition_time, ) t_log["move_to_hue_and_saturation"] = result if isinstance(result, Exception) or result[1] is not Status.SUCCESS: @@ -484,11 +488,9 @@ class BaseLight(LogMixin, light.LightEntity): if xy_color is not None: result = await self._color_channel.move_to_color( - int(xy_color[0] * 65535), - int(xy_color[1] * 65535), - self._DEFAULT_MIN_TRANSITION_TIME - if new_color_provided_while_off - else duration, + color_x=int(xy_color[0] * 65535), + color_y=int(xy_color[1] * 65535), + transition_time=transition_time, ) t_log["move_to_color"] = result if isinstance(result, Exception) or result[1] is not Status.SUCCESS: diff --git a/tests/components/zha/test_discover.py b/tests/components/zha/test_discover.py index 0f51141ec5d..e3cb53efcd4 100644 --- a/tests/components/zha/test_discover.py +++ b/tests/components/zha/test_discover.py @@ -137,10 +137,12 @@ async def test_devices( if called: assert cluster_identify.request.call_args == mock.call( False, - 64, + cluster_identify.commands_by_name["trigger_effect"].id, cluster_identify.commands_by_name["trigger_effect"].schema, - 2, - 0, + effect_id=zigpy.zcl.clusters.general.Identify.EffectIdentifier.Okay, + effect_variant=( + zigpy.zcl.clusters.general.Identify.EffectVariant.Default + ), expect_reply=True, manufacturer=None, tries=1, diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index 156f692aa14..5f5e7ab2e38 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -33,8 +33,6 @@ from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE from tests.common import async_fire_time_changed from tests.components.zha.common import async_wait_for_updates -ON = 1 -OFF = 0 IEEE_GROUPABLE_DEVICE = "01:2d:6f:00:0a:90:69:e8" IEEE_GROUPABLE_DEVICE2 = "02:2d:6f:00:0a:90:69:e9" IEEE_GROUPABLE_DEVICE3 = "03:2d:6f:00:0a:90:69:e7" @@ -463,10 +461,10 @@ async def test_transitions( assert dev1_cluster_level.request.await_count == 1 assert dev1_cluster_level.request.call_args == call( False, - 4, + dev1_cluster_level.commands_by_name["move_to_level_with_on_off"].id, dev1_cluster_level.commands_by_name["move_to_level_with_on_off"].schema, - 50, # brightness (level in ZCL) - 0, # transition time + level=50, + transition_time=0, expect_reply=True, manufacturer=None, tries=1, @@ -499,10 +497,10 @@ async def test_transitions( assert dev1_cluster_level.request.await_count == 1 assert dev1_cluster_level.request.call_args == call( False, - 4, + dev1_cluster_level.commands_by_name["move_to_level_with_on_off"].id, dev1_cluster_level.commands_by_name["move_to_level_with_on_off"].schema, - 18, # brightness (level in ZCL) - 30, # transition time (ZCL time in 10ths of a second) + level=18, + transition_time=30, expect_reply=True, manufacturer=None, tries=1, @@ -510,10 +508,10 @@ async def test_transitions( ) assert dev1_cluster_color.request.call_args == call( False, - 10, + dev1_cluster_color.commands_by_name["move_to_color_temp"].id, dev1_cluster_color.commands_by_name["move_to_color_temp"].schema, - 432, # color temp mireds - 30.0, # transition time (ZCL time in 10ths of a second) + color_temp_mireds=432, + transition_time=30.0, expect_reply=True, manufacturer=None, tries=1, @@ -547,10 +545,10 @@ async def test_transitions( assert dev1_cluster_level.request.await_count == 1 assert dev1_cluster_level.request.call_args == call( False, - 4, + dev1_cluster_level.commands_by_name["move_to_level_with_on_off"].id, dev1_cluster_level.commands_by_name["move_to_level_with_on_off"].schema, - 0, # brightness (level in ZCL) - 0, # transition time (ZCL time in 10ths of a second) + level=0, + transition_time=0, expect_reply=True, manufacturer=None, tries=1, @@ -584,10 +582,10 @@ async def test_transitions( # first it comes on with no transition at 2 brightness assert dev1_cluster_level.request.call_args_list[0] == call( False, - 4, + dev1_cluster_level.commands_by_name["move_to_level_with_on_off"].id, dev1_cluster_level.commands_by_name["move_to_level_with_on_off"].schema, - 2, # brightness (level in ZCL) - 0, # transition time (ZCL time in 10ths of a second) + level=2, + transition_time=0, expect_reply=True, manufacturer=None, tries=1, @@ -595,10 +593,10 @@ async def test_transitions( ) assert dev1_cluster_color.request.call_args == call( False, - 10, + dev1_cluster_color.commands_by_name["move_to_color_temp"].id, dev1_cluster_color.commands_by_name["move_to_color_temp"].schema, - 235, # color temp mireds - 0, # transition time (ZCL time in 10ths of a second) - no transition when new_color_provided_while_off + color_temp_mireds=235, + transition_time=0, # no transition when new_color_provided_while_off expect_reply=True, manufacturer=None, tries=1, @@ -606,10 +604,10 @@ async def test_transitions( ) assert dev1_cluster_level.request.call_args_list[1] == call( False, - 0, + dev1_cluster_level.commands_by_name["move_to_level"].id, dev1_cluster_level.commands_by_name["move_to_level"].schema, - 25, # brightness (level in ZCL) - 10.0, # transition time (ZCL time in 10ths of a second) + level=25, + transition_time=10, expect_reply=True, manufacturer=None, tries=1, @@ -668,10 +666,10 @@ async def test_transitions( # first it comes on with no transition at 2 brightness assert dev1_cluster_level.request.call_args_list[0] == call( False, - 4, + dev1_cluster_level.commands_by_name["move_to_level_with_on_off"].id, dev1_cluster_level.commands_by_name["move_to_level_with_on_off"].schema, - 2, # brightness (level in ZCL) - 0, # transition time (ZCL time in 10ths of a second) + level=2, + transition_time=0, expect_reply=True, manufacturer=None, tries=1, @@ -679,10 +677,10 @@ async def test_transitions( ) assert dev1_cluster_color.request.call_args == call( False, - 10, + dev1_cluster_color.commands_by_name["move_to_color_temp"].id, dev1_cluster_color.commands_by_name["move_to_color_temp"].schema, - 236, # color temp mireds - 0, # transition time (ZCL time in 10ths of a second) - no transition when new_color_provided_while_off + color_temp_mireds=236, + transition_time=0, # no transition when new_color_provided_while_off expect_reply=True, manufacturer=None, tries=1, @@ -690,10 +688,10 @@ async def test_transitions( ) assert dev1_cluster_level.request.call_args_list[1] == call( False, - 0, + dev1_cluster_level.commands_by_name["move_to_level"].id, dev1_cluster_level.commands_by_name["move_to_level"].schema, - 25, # brightness (level in ZCL) - 0, # transition time (ZCL time in 10ths of a second) + level=25, + transition_time=0, expect_reply=True, manufacturer=None, tries=1, @@ -750,7 +748,7 @@ async def test_transitions( assert dev1_cluster_on_off.request.call_args == call( False, - 1, + dev1_cluster_on_off.commands_by_name["on"].id, dev1_cluster_on_off.commands_by_name["on"].schema, expect_reply=True, manufacturer=None, @@ -760,10 +758,10 @@ async def test_transitions( assert dev1_cluster_color.request.call_args == call( False, - 10, + dev1_cluster_color.commands_by_name["move_to_color_temp"].id, dev1_cluster_color.commands_by_name["move_to_color_temp"].schema, - 236, # color temp mireds - 0, # transition time (ZCL time in 10ths of a second) - no transition when new_color_provided_while_off + color_temp_mireds=236, + transition_time=0, # no transition when new_color_provided_while_off expect_reply=True, manufacturer=None, tries=1, @@ -820,10 +818,10 @@ async def test_transitions( assert dev2_cluster_level.request.await_count == 1 assert dev2_cluster_level.request.call_args == call( False, - 4, + dev2_cluster_level.commands_by_name["move_to_level_with_on_off"].id, dev2_cluster_level.commands_by_name["move_to_level_with_on_off"].schema, - 100, # brightness (level in ZCL) - 1, # transition time - sengled light uses default minimum + level=100, + transition_time=1, # transition time - sengled light uses default minimum expect_reply=True, manufacturer=None, tries=1, @@ -878,10 +876,10 @@ async def test_transitions( # first it comes on with no transition at 2 brightness assert dev2_cluster_level.request.call_args_list[0] == call( False, - 4, + dev2_cluster_level.commands_by_name["move_to_level_with_on_off"].id, dev2_cluster_level.commands_by_name["move_to_level_with_on_off"].schema, - 2, # brightness (level in ZCL) - 1, # transition time (ZCL time in 10ths of a second) + level=2, + transition_time=1, expect_reply=True, manufacturer=None, tries=1, @@ -889,10 +887,10 @@ async def test_transitions( ) assert dev2_cluster_color.request.call_args == call( False, - 10, + dev2_cluster_color.commands_by_name["move_to_color_temp"].id, dev2_cluster_color.commands_by_name["move_to_color_temp"].schema, - 235, # color temp mireds - 1, # transition time (ZCL time in 10ths of a second) - sengled transition == 1 when new_color_provided_while_off + color_temp_mireds=235, + transition_time=1, # sengled transition == 1 when new_color_provided_while_off expect_reply=True, manufacturer=None, tries=1, @@ -900,10 +898,10 @@ async def test_transitions( ) assert dev2_cluster_level.request.call_args_list[1] == call( False, - 0, + dev2_cluster_level.commands_by_name["move_to_level"].id, dev2_cluster_level.commands_by_name["move_to_level"].schema, - 25, # brightness (level in ZCL) - 10.0, # transition time (ZCL time in 10ths of a second) + level=25, + transition_time=10, expect_reply=True, manufacturer=None, tries=1, @@ -965,10 +963,10 @@ async def test_transitions( # groups are omitted from the 3 call dance for new_color_provided_while_off assert group_color_channel.request.call_args == call( False, - 10, + dev2_cluster_color.commands_by_name["move_to_color_temp"].id, dev2_cluster_color.commands_by_name["move_to_color_temp"].schema, - 235, # color temp mireds - 10.0, # transition time (ZCL time in 10ths of a second) - sengled transition == 1 when new_color_provided_while_off + color_temp_mireds=235, + transition_time=10, # sengled transition == 1 when new_color_provided_while_off expect_reply=True, manufacturer=None, tries=1, @@ -976,10 +974,10 @@ async def test_transitions( ) assert group_level_channel.request.call_args == call( False, - 4, + dev2_cluster_level.commands_by_name["move_to_level_with_on_off"].id, dev2_cluster_level.commands_by_name["move_to_level_with_on_off"].schema, - 25, # brightness (level in ZCL) - 10.0, # transition time (ZCL time in 10ths of a second) + level=25, + transition_time=10, expect_reply=True, manufacturer=None, tries=1, @@ -1031,10 +1029,10 @@ async def test_transitions( assert dev2_cluster_level.request.await_count == 1 assert dev2_cluster_level.request.call_args == call( False, - 4, + dev2_cluster_level.commands_by_name["move_to_level_with_on_off"].id, dev2_cluster_level.commands_by_name["move_to_level_with_on_off"].schema, - 0, # brightness (level in ZCL) - 20, # transition time + level=0, + transition_time=20, # transition time expect_reply=True, manufacturer=None, tries=1, @@ -1061,10 +1059,10 @@ async def test_transitions( assert dev2_cluster_level.request.await_count == 1 assert dev2_cluster_level.request.call_args == call( False, - 4, + dev2_cluster_level.commands_by_name["move_to_level_with_on_off"].id, dev2_cluster_level.commands_by_name["move_to_level_with_on_off"].schema, - 25, # brightness (level in ZCL) - this is the last brightness we set a few tests above - 1, # transition time - sengled light uses default minimum + level=25, + transition_time=1, # transition time - sengled light uses default minimum expect_reply=True, manufacturer=None, tries=1, @@ -1096,7 +1094,7 @@ async def test_transitions( # first it comes on assert eWeLink_cluster_on_off.request.call_args_list[0] == call( False, - 1, + eWeLink_cluster_on_off.commands_by_name["on"].id, eWeLink_cluster_on_off.commands_by_name["on"].schema, expect_reply=True, manufacturer=None, @@ -1105,10 +1103,10 @@ async def test_transitions( ) assert dev1_cluster_color.request.call_args == call( False, - 10, + dev1_cluster_color.commands_by_name["move_to_color_temp"].id, dev1_cluster_color.commands_by_name["move_to_color_temp"].schema, - 235, # color temp mireds - 0, # transition time (ZCL time in 10ths of a second) + color_temp_mireds=235, + transition_time=0, expect_reply=True, manufacturer=None, tries=1, @@ -1153,7 +1151,7 @@ async def async_test_on_off_from_hass(hass, cluster, entity_id): assert cluster.request.await_count == 1 assert cluster.request.call_args == call( False, - ON, + cluster.commands_by_name["on"].id, cluster.commands_by_name["on"].schema, expect_reply=True, manufacturer=None, @@ -1176,7 +1174,7 @@ async def async_test_off_from_hass(hass, cluster, entity_id): assert cluster.request.await_count == 1 assert cluster.request.call_args == call( False, - OFF, + cluster.commands_by_name["off"].id, cluster.commands_by_name["off"].schema, expect_reply=True, manufacturer=None, @@ -1204,7 +1202,7 @@ async def async_test_level_on_off_from_hass( assert level_cluster.request.await_count == 0 assert on_off_cluster.request.call_args == call( False, - ON, + on_off_cluster.commands_by_name["on"].id, on_off_cluster.commands_by_name["on"].schema, expect_reply=True, manufacturer=None, @@ -1228,7 +1226,7 @@ async def async_test_level_on_off_from_hass( assert level_cluster.request.await_count == 1 assert on_off_cluster.request.call_args == call( False, - ON, + on_off_cluster.commands_by_name["on"].id, on_off_cluster.commands_by_name["on"].schema, expect_reply=True, manufacturer=None, @@ -1237,10 +1235,10 @@ async def async_test_level_on_off_from_hass( ) assert level_cluster.request.call_args == call( False, - 4, + level_cluster.commands_by_name["move_to_level_with_on_off"].id, level_cluster.commands_by_name["move_to_level_with_on_off"].schema, - 254, - 100.0, + level=254, + transition_time=100, expect_reply=True, manufacturer=None, tries=1, @@ -1262,10 +1260,10 @@ async def async_test_level_on_off_from_hass( assert level_cluster.request.await_count == 1 assert level_cluster.request.call_args == call( False, - 4, + level_cluster.commands_by_name["move_to_level_with_on_off"].id, level_cluster.commands_by_name["move_to_level_with_on_off"].schema, - 10, - expected_default_transition, + level=10, + transition_time=int(expected_default_transition), expect_reply=True, manufacturer=None, tries=1, @@ -1305,10 +1303,10 @@ async def async_test_flash_from_hass(hass, cluster, entity_id, flash): assert cluster.request.await_count == 1 assert cluster.request.call_args == call( False, - 64, + cluster.commands_by_name["trigger_effect"].id, cluster.commands_by_name["trigger_effect"].schema, - FLASH_EFFECTS[flash], - 0, + effect_id=FLASH_EFFECTS[flash], + effect_variant=general.Identify.EffectVariant.Default, expect_reply=True, manufacturer=None, tries=1, From 1fe5948afd4115fee30421cbf022ede73af56203 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Thu, 1 Sep 2022 22:15:42 +0100 Subject: [PATCH 036/955] Cleanup IPMA code (#77674) revert yaml import --- homeassistant/components/ipma/config_flow.py | 7 ---- tests/components/ipma/test_config_flow.py | 44 -------------------- 2 files changed, 51 deletions(-) diff --git a/homeassistant/components/ipma/config_flow.py b/homeassistant/components/ipma/config_flow.py index 17fde104125..81ab8f98014 100644 --- a/homeassistant/components/ipma/config_flow.py +++ b/homeassistant/components/ipma/config_flow.py @@ -18,13 +18,6 @@ class IpmaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Init IpmaFlowHandler.""" self._errors = {} - async def async_step_import(self, config): - """Import a configuration from config.yaml.""" - - self._async_abort_entries_match(config) - config[CONF_MODE] = "daily" - return await self.async_step_user(user_input=config) - async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" self._errors = {} diff --git a/tests/components/ipma/test_config_flow.py b/tests/components/ipma/test_config_flow.py index c8d53f95a4a..9c1b50fee87 100644 --- a/tests/components/ipma/test_config_flow.py +++ b/tests/components/ipma/test_config_flow.py @@ -2,10 +2,8 @@ from unittest.mock import Mock, patch -from homeassistant import config_entries from homeassistant.components.ipma import DOMAIN, config_flow from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_MODE, CONF_NAME -from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component @@ -181,45 +179,3 @@ async def test_config_entry_migration(hass): weather_home2 = ent_reg.async_get("weather.hometown_2") assert weather_home2.unique_id == "0, 0, hourly" - - -async def test_import_flow_success(hass): - """Test a successful import of yaml.""" - - with patch( - "homeassistant.components.ipma.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=ENTRY_CONFIG, - ) - await hass.async_block_till_done() - - assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "Home Town" - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_import_flow_already_exist(hass): - """Test import of yaml already exist.""" - - MockConfigEntry( - domain=DOMAIN, - data=ENTRY_CONFIG, - ).add_to_hass(hass) - - with patch( - "homeassistant.components.ipma.async_setup_entry", - return_value=True, - ): - result3 = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=ENTRY_CONFIG, - ) - await hass.async_block_till_done() - - assert result3["type"] == FlowResultType.ABORT - assert result3["reason"] == "already_configured" From 45f8b64a34d7a34db9608978064adcedfff0b1ed Mon Sep 17 00:00:00 2001 From: Avi Miller Date: Fri, 2 Sep 2022 08:07:21 +1000 Subject: [PATCH 037/955] Add binary sensor platform to LIFX integration (#77535) Co-authored-by: J. Nick Koston --- homeassistant/components/lifx/__init__.py | 2 +- .../components/lifx/binary_sensor.py | 70 ++++++++++++++++++ homeassistant/components/lifx/const.py | 11 ++- homeassistant/components/lifx/coordinator.py | 35 ++++++--- homeassistant/components/lifx/light.py | 8 +- tests/components/lifx/__init__.py | 26 ++++++- tests/components/lifx/test_binary_sensor.py | 74 +++++++++++++++++++ 7 files changed, 203 insertions(+), 23 deletions(-) create mode 100644 homeassistant/components/lifx/binary_sensor.py create mode 100644 tests/components/lifx/test_binary_sensor.py diff --git a/homeassistant/components/lifx/__init__.py b/homeassistant/components/lifx/__init__.py index 6af30b91d28..5c91efa1d02 100644 --- a/homeassistant/components/lifx/__init__.py +++ b/homeassistant/components/lifx/__init__.py @@ -57,7 +57,7 @@ CONFIG_SCHEMA = vol.All( ) -PLATFORMS = [Platform.BUTTON, Platform.LIGHT] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.LIGHT] DISCOVERY_INTERVAL = timedelta(minutes=15) MIGRATION_INTERVAL = timedelta(minutes=5) diff --git a/homeassistant/components/lifx/binary_sensor.py b/homeassistant/components/lifx/binary_sensor.py new file mode 100644 index 00000000000..4a368a2f97f --- /dev/null +++ b/homeassistant/components/lifx/binary_sensor.py @@ -0,0 +1,70 @@ +"""Binary sensor entities for LIFX integration.""" +from __future__ import annotations + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +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, HEV_CYCLE_STATE +from .coordinator import LIFXUpdateCoordinator +from .entity import LIFXEntity +from .util import lifx_features + +HEV_CYCLE_STATE_SENSOR = BinarySensorEntityDescription( + key=HEV_CYCLE_STATE, + name="Clean Cycle", + entity_category=EntityCategory.DIAGNOSTIC, + device_class=BinarySensorDeviceClass.RUNNING, +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up LIFX from a config entry.""" + coordinator: LIFXUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + if lifx_features(coordinator.device)["hev"]: + async_add_entities( + [ + LIFXBinarySensorEntity( + coordinator=coordinator, description=HEV_CYCLE_STATE_SENSOR + ) + ] + ) + + +class LIFXBinarySensorEntity(LIFXEntity, BinarySensorEntity): + """LIFX sensor entity base class.""" + + _attr_has_entity_name = True + + def __init__( + self, + coordinator: LIFXUpdateCoordinator, + description: BinarySensorEntityDescription, + ) -> None: + """Initialise the sensor.""" + super().__init__(coordinator) + + self.entity_description = description + self._attr_name = description.name + self._attr_unique_id = f"{coordinator.serial_number}_{description.key}" + self._async_update_attrs() + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._async_update_attrs() + super()._handle_coordinator_update() + + @callback + def _async_update_attrs(self) -> None: + """Handle coordinator updates.""" + self._attr_is_on = self.coordinator.async_get_hev_cycle_state() diff --git a/homeassistant/components/lifx/const.py b/homeassistant/components/lifx/const.py index f6ec653c994..74960d59bd1 100644 --- a/homeassistant/components/lifx/const.py +++ b/homeassistant/components/lifx/const.py @@ -29,6 +29,15 @@ IDENTIFY_WAVEFORM = { IDENTIFY = "identify" RESTART = "restart" +ATTR_DURATION = "duration" +ATTR_INDICATION = "indication" +ATTR_INFRARED = "infrared" +ATTR_POWER = "power" +ATTR_REMAINING = "remaining" +ATTR_ZONES = "zones" + +HEV_CYCLE_STATE = "hev_cycle_state" + DATA_LIFX_MANAGER = "lifx_manager" -_LOGGER = logging.getLogger(__name__) +_LOGGER = logging.getLogger(__package__) diff --git a/homeassistant/components/lifx/coordinator.py b/homeassistant/components/lifx/coordinator.py index 1f3f49368ca..d01fb266c6f 100644 --- a/homeassistant/components/lifx/coordinator.py +++ b/homeassistant/components/lifx/coordinator.py @@ -15,6 +15,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import ( _LOGGER, + ATTR_REMAINING, IDENTIFY_WAVEFORM, MESSAGE_RETRIES, MESSAGE_TIMEOUT, @@ -101,26 +102,25 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator): self.device.get_hostfirmware() if self.device.product is None: self.device.get_version() - try: - response = await async_execute_lifx(self.device.get_color) - except asyncio.TimeoutError as ex: - raise UpdateFailed( - f"Failed to fetch state from device: {self.device.ip_addr}" - ) from ex + response = await async_execute_lifx(self.device.get_color) + if self.device.product is None: raise UpdateFailed( f"Failed to fetch get version from device: {self.device.ip_addr}" ) + # device.mac_addr is not the mac_address, its the serial number if self.device.mac_addr == TARGET_ANY: self.device.mac_addr = response.target_addr + if lifx_features(self.device)["multizone"]: - try: - await self.async_update_color_zones() - except asyncio.TimeoutError as ex: - raise UpdateFailed( - f"Failed to fetch zones from device: {self.device.ip_addr}" - ) from ex + await self.async_update_color_zones() + + if lifx_features(self.device)["hev"]: + if self.device.hev_cycle_configuration is None: + self.device.get_hev_configuration() + + await self.async_get_hev_cycle() async def async_update_color_zones(self) -> None: """Get updated color information for each zone.""" @@ -138,6 +138,17 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator): if zone == top - 1: zone -= 1 + def async_get_hev_cycle_state(self) -> bool | None: + """Return the current HEV cycle state.""" + if self.device.hev_cycle is None: + return None + return bool(self.device.hev_cycle.get(ATTR_REMAINING, 0) > 0) + + async def async_get_hev_cycle(self) -> None: + """Update the HEV cycle status from a LIFX Clean bulb.""" + if lifx_features(self.device)["hev"]: + await async_execute_lifx(self.device.get_hev_cycle) + async def async_set_waveform_optional( self, value: dict[str, Any], rapid: bool = False ) -> None: diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index 67bb3e91748..fe17dd95788 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -28,7 +28,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_point_in_utc_time import homeassistant.util.color as color_util -from .const import DATA_LIFX_MANAGER, DOMAIN +from .const import ATTR_INFRARED, ATTR_POWER, ATTR_ZONES, DATA_LIFX_MANAGER, DOMAIN from .coordinator import LIFXUpdateCoordinator from .entity import LIFXEntity from .manager import ( @@ -39,14 +39,8 @@ from .manager import ( ) from .util import convert_8_to_16, convert_16_to_8, find_hsbk, lifx_features, merge_hsbk -SERVICE_LIFX_SET_STATE = "set_state" - COLOR_ZONE_POPULATE_DELAY = 0.3 -ATTR_INFRARED = "infrared" -ATTR_ZONES = "zones" -ATTR_POWER = "power" - SERVICE_LIFX_SET_STATE = "set_state" LIFX_SET_STATE_SCHEMA = cv.make_entity_service_schema( diff --git a/tests/components/lifx/__init__.py b/tests/components/lifx/__init__.py index 8259314e77c..9e137c8532a 100644 --- a/tests/components/lifx/__init__.py +++ b/tests/components/lifx/__init__.py @@ -22,10 +22,13 @@ DEFAULT_ENTRY_TITLE = LABEL class MockMessage: """Mock a lifx message.""" - def __init__(self): + def __init__(self, **kwargs): """Init message.""" self.target_addr = SERIAL self.count = 9 + for k, v in kwargs.items(): + if k != "callb": + setattr(self, k, v) class MockFailingLifxCommand: @@ -50,15 +53,20 @@ class MockFailingLifxCommand: class MockLifxCommand: """Mock a lifx command.""" + def __name__(self): + """Return name.""" + return "mock_lifx_command" + def __init__(self, bulb, **kwargs): """Init command.""" self.bulb = bulb self.calls = [] + self.msg_kwargs = kwargs def __call__(self, *args, **kwargs): """Call command.""" if callb := kwargs.get("callb"): - callb(self.bulb, MockMessage()) + callb(self.bulb, MockMessage(**self.msg_kwargs)) self.calls.append([args, kwargs]) def reset_mock(self): @@ -108,6 +116,20 @@ def _mocked_brightness_bulb() -> Light: return bulb +def _mocked_clean_bulb() -> Light: + bulb = _mocked_bulb() + bulb.get_hev_cycle = MockLifxCommand( + bulb, duration=7200, remaining=0, last_power=False + ) + bulb.hev_cycle = { + "duration": 7200, + "remaining": 30, + "last_power": False, + } + bulb.product = 90 + return bulb + + def _mocked_light_strip() -> Light: bulb = _mocked_bulb() bulb.product = 31 # LIFX Z diff --git a/tests/components/lifx/test_binary_sensor.py b/tests/components/lifx/test_binary_sensor.py new file mode 100644 index 00000000000..bb0b210704a --- /dev/null +++ b/tests/components/lifx/test_binary_sensor.py @@ -0,0 +1,74 @@ +"""Test the lifx binary sensor platwform.""" +from __future__ import annotations + +from datetime import timedelta + +from homeassistant.components import lifx +from homeassistant.components.binary_sensor import BinarySensorDeviceClass +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + CONF_HOST, + STATE_OFF, + STATE_ON, + STATE_UNKNOWN, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity import EntityCategory +from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util + +from . import ( + DEFAULT_ENTRY_TITLE, + IP_ADDRESS, + MAC_ADDRESS, + SERIAL, + _mocked_clean_bulb, + _patch_config_flow_try_connect, + _patch_device, + _patch_discovery, +) + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_hev_cycle_state(hass: HomeAssistant) -> None: + """Test HEV cycle state binary sensor.""" + config_entry = MockConfigEntry( + domain=lifx.DOMAIN, + title=DEFAULT_ENTRY_TITLE, + data={CONF_HOST: IP_ADDRESS}, + unique_id=MAC_ADDRESS, + ) + config_entry.add_to_hass(hass) + bulb = _mocked_clean_bulb() + 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 = "binary_sensor.my_bulb_clean_cycle" + entity_registry = er.async_get(hass) + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_ON + assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.RUNNING + + entry = entity_registry.async_get(entity_id) + assert state + assert entry.unique_id == f"{SERIAL}_hev_cycle_state" + assert entry.entity_category == EntityCategory.DIAGNOSTIC + + bulb.hev_cycle = {"duration": 7200, "remaining": 0, "last_power": False} + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=30)) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_OFF + + bulb.hev_cycle = None + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=30)) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_UNKNOWN From 8e392b85ee5a185cd7e4378e76f36995a2b1169f Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 2 Sep 2022 00:08:14 +0200 Subject: [PATCH 038/955] bump pynetgear to 0.10.8 (#77672) --- homeassistant/components/netgear/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/netgear/manifest.json b/homeassistant/components/netgear/manifest.json index 69a21e5aace..92b3065147c 100644 --- a/homeassistant/components/netgear/manifest.json +++ b/homeassistant/components/netgear/manifest.json @@ -2,7 +2,7 @@ "domain": "netgear", "name": "NETGEAR", "documentation": "https://www.home-assistant.io/integrations/netgear", - "requirements": ["pynetgear==0.10.7"], + "requirements": ["pynetgear==0.10.8"], "codeowners": ["@hacf-fr", "@Quentame", "@starkillerOG"], "iot_class": "local_polling", "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index 98f0adc0d3d..16f67f60115 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1710,7 +1710,7 @@ pymyq==3.1.4 pymysensors==0.24.0 # homeassistant.components.netgear -pynetgear==0.10.7 +pynetgear==0.10.8 # homeassistant.components.netio pynetio==0.1.9.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 242952979e1..7c725efce11 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1199,7 +1199,7 @@ pymyq==3.1.4 pymysensors==0.24.0 # homeassistant.components.netgear -pynetgear==0.10.7 +pynetgear==0.10.8 # homeassistant.components.nina pynina==0.1.8 From ccef03f1d4b0a3dbcb4bdd241de985cf4911998b Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 2 Sep 2022 00:25:46 +0000 Subject: [PATCH 039/955] [ci skip] Translation update --- .../components/abode/translations/es.json | 2 +- .../components/airvisual/translations/es.json | 2 +- .../aladdin_connect/translations/es.json | 2 +- .../components/ambee/translations/es.json | 4 +- .../components/apple_tv/translations/es.json | 2 +- .../components/august/translations/es.json | 2 +- .../aussie_broadband/translations/es.json | 2 +- .../automation/translations/ca.json | 1 + .../automation/translations/cs.json | 13 ++ .../components/awair/translations/es.json | 2 +- .../azure_devops/translations/es.json | 4 +- .../components/bluetooth/translations/ca.json | 7 +- .../components/bluetooth/translations/hu.json | 2 +- .../components/bosch_shc/translations/es.json | 2 +- .../components/brunt/translations/es.json | 2 +- .../components/bthome/translations/es.json | 2 +- .../components/bthome/translations/hu.json | 32 +++++ .../components/camera/translations/ca.json | 2 +- .../cloudflare/translations/es.json | 4 +- .../devolo_home_control/translations/es.json | 2 +- .../components/discord/translations/es.json | 2 +- .../components/ecowitt/translations/ca.json | 17 +++ .../components/ecowitt/translations/cs.json | 7 + .../components/ecowitt/translations/de.json | 20 +++ .../components/ecowitt/translations/el.json | 20 +++ .../components/ecowitt/translations/en.json | 8 ++ .../components/ecowitt/translations/es.json | 20 +++ .../components/ecowitt/translations/et.json | 20 +++ .../components/ecowitt/translations/fr.json | 15 ++ .../components/ecowitt/translations/hu.json | 20 +++ .../components/ecowitt/translations/id.json | 20 +++ .../components/ecowitt/translations/it.json | 20 +++ .../components/ecowitt/translations/ja.json | 16 +++ .../components/ecowitt/translations/no.json | 20 +++ .../ecowitt/translations/pt-BR.json | 20 +++ .../ecowitt/translations/zh-Hant.json | 20 +++ .../components/efergy/translations/es.json | 2 +- .../enphase_envoy/translations/es.json | 2 +- .../components/esphome/translations/es.json | 2 +- .../fireservicerota/translations/es.json | 2 +- .../components/flume/translations/es.json | 2 +- .../components/fritz/translations/es.json | 2 +- .../components/fritzbox/translations/es.json | 2 +- .../geocaching/translations/es.json | 2 +- .../components/google/translations/es.json | 2 +- .../components/hive/translations/es.json | 2 +- .../components/hyperion/translations/es.json | 2 +- .../components/icloud/translations/ca.json | 7 + .../components/icloud/translations/cs.json | 6 + .../components/icloud/translations/de.json | 7 + .../components/icloud/translations/el.json | 7 + .../components/icloud/translations/en.json | 7 + .../components/icloud/translations/es.json | 9 +- .../components/icloud/translations/et.json | 7 + .../components/icloud/translations/fr.json | 6 + .../components/icloud/translations/hu.json | 7 + .../components/icloud/translations/id.json | 7 + .../components/icloud/translations/it.json | 7 + .../components/icloud/translations/ja.json | 7 + .../components/icloud/translations/no.json | 7 + .../components/icloud/translations/pt-BR.json | 7 + .../icloud/translations/zh-Hant.json | 7 + .../intellifire/translations/es.json | 2 +- .../components/isy994/translations/es.json | 2 +- .../lacrosse_view/translations/es.json | 2 +- .../components/lametric/translations/ca.json | 8 ++ .../components/led_ble/translations/el.json | 23 ++++ .../components/led_ble/translations/hu.json | 23 ++++ .../components/led_ble/translations/id.json | 23 ++++ .../components/led_ble/translations/ja.json | 23 ++++ .../components/life360/translations/es.json | 2 +- .../litterrobot/translations/es.json | 2 +- .../litterrobot/translations/hu.json | 10 +- .../litterrobot/translations/id.json | 10 +- .../components/lyric/translations/es.json | 4 +- .../components/mazda/translations/es.json | 2 +- .../components/meater/translations/es.json | 2 +- .../components/melnor/translations/ca.json | 13 ++ .../components/melnor/translations/de.json | 13 ++ .../components/melnor/translations/el.json | 13 ++ .../components/melnor/translations/es.json | 13 ++ .../components/melnor/translations/et.json | 13 ++ .../components/melnor/translations/fr.json | 13 ++ .../components/melnor/translations/hu.json | 13 ++ .../components/melnor/translations/id.json | 13 ++ .../components/melnor/translations/it.json | 13 ++ .../components/melnor/translations/no.json | 13 ++ .../melnor/translations/zh-Hant.json | 13 ++ .../components/motioneye/translations/es.json | 2 +- .../components/mqtt/translations/ca.json | 6 + .../components/myq/translations/es.json | 2 +- .../components/nam/translations/es.json | 4 +- .../nam/translations/sensor.ca.json | 11 ++ .../nam/translations/sensor.hu.json | 11 ++ .../nam/translations/sensor.id.json | 11 ++ .../nam/translations/sensor.ja.json | 11 ++ .../components/nanoleaf/translations/es.json | 2 +- .../components/neato/translations/es.json | 2 +- .../components/nest/translations/es.json | 2 +- .../components/netatmo/translations/es.json | 2 +- .../components/notion/translations/es.json | 2 +- .../components/nuki/translations/es.json | 2 +- .../openexchangerates/translations/es.json | 2 +- .../components/overkiz/translations/de.json | 3 +- .../components/overkiz/translations/el.json | 3 +- .../components/overkiz/translations/es.json | 5 +- .../components/overkiz/translations/et.json | 3 +- .../components/overkiz/translations/hu.json | 3 +- .../components/overkiz/translations/id.json | 3 +- .../components/overkiz/translations/it.json | 3 +- .../components/overkiz/translations/no.json | 3 +- .../overkiz/translations/pt-BR.json | 3 +- .../overkiz/translations/zh-Hant.json | 3 +- .../ovo_energy/translations/es.json | 2 +- .../components/picnic/translations/es.json | 2 +- .../components/plex/translations/es.json | 2 +- .../components/powerwall/translations/es.json | 2 +- .../components/prosegur/translations/es.json | 2 +- .../components/prusalink/translations/cs.json | 17 +++ .../components/prusalink/translations/el.json | 18 +++ .../components/prusalink/translations/fr.json | 18 +++ .../components/prusalink/translations/hu.json | 18 +++ .../components/prusalink/translations/id.json | 18 +++ .../components/prusalink/translations/it.json | 18 +++ .../components/prusalink/translations/ja.json | 17 +++ .../prusalink/translations/sensor.cs.json | 9 ++ .../prusalink/translations/sensor.el.json | 11 ++ .../prusalink/translations/sensor.fr.json | 11 ++ .../prusalink/translations/sensor.hu.json | 11 ++ .../prusalink/translations/sensor.id.json | 11 ++ .../prusalink/translations/sensor.it.json | 11 ++ .../prusalink/translations/sensor.ja.json | 11 ++ .../components/pushover/translations/es.json | 2 +- .../components/pvoutput/translations/es.json | 2 +- .../components/renault/translations/es.json | 2 +- .../components/ridwell/translations/es.json | 2 +- .../components/samsungtv/translations/es.json | 2 +- .../components/sense/translations/es.json | 2 +- .../components/sensibo/translations/es.json | 2 +- .../components/sensor/translations/cs.json | 2 + .../components/sensor/translations/de.json | 2 + .../components/sensor/translations/el.json | 2 + .../components/sensor/translations/en.json | 2 + .../components/sensor/translations/es.json | 2 + .../components/sensor/translations/id.json | 2 + .../components/sensor/translations/it.json | 2 + .../components/sensor/translations/pt-BR.json | 2 + .../components/sensorpro/translations/ca.json | 22 +++ .../components/sensorpro/translations/de.json | 22 +++ .../components/sensorpro/translations/el.json | 22 +++ .../components/sensorpro/translations/es.json | 22 +++ .../components/sensorpro/translations/et.json | 22 +++ .../components/sensorpro/translations/fr.json | 22 +++ .../components/sensorpro/translations/hu.json | 22 +++ .../components/sensorpro/translations/id.json | 22 +++ .../components/sensorpro/translations/it.json | 22 +++ .../components/sensorpro/translations/ja.json | 22 +++ .../components/sensorpro/translations/no.json | 22 +++ .../sensorpro/translations/pt-BR.json | 22 +++ .../sensorpro/translations/zh-Hant.json | 22 +++ .../components/sharkiq/translations/es.json | 2 +- .../simplisafe/translations/es.json | 2 +- .../components/skybell/translations/es.json | 2 +- .../components/sleepiq/translations/es.json | 2 +- .../components/smarttub/translations/es.json | 4 +- .../components/sonarr/translations/es.json | 2 +- .../speedtestdotnet/translations/ca.json | 1 + .../speedtestdotnet/translations/hu.json | 13 ++ .../steam_online/translations/es.json | 2 +- .../synology_dsm/translations/es.json | 2 +- .../system_bridge/translations/es.json | 2 +- .../components/tailscale/translations/es.json | 2 +- .../tankerkoenig/translations/es.json | 2 +- .../components/tautulli/translations/es.json | 4 +- .../thermobeacon/translations/hu.json | 22 +++ .../components/tile/translations/es.json | 2 +- .../totalconnect/translations/es.json | 2 +- .../components/tractive/translations/es.json | 2 +- .../trafikverket_ferry/translations/es.json | 2 +- .../trafikverket_train/translations/es.json | 2 +- .../transmission/translations/es.json | 2 +- .../components/unifi/translations/es.json | 2 +- .../unifiprotect/translations/hu.json | 4 + .../unifiprotect/translations/id.json | 4 + .../uptimerobot/translations/es.json | 2 +- .../components/verisure/translations/es.json | 4 +- .../vlc_telnet/translations/es.json | 2 +- .../volvooncall/translations/de.json | 3 +- .../volvooncall/translations/el.json | 3 +- .../volvooncall/translations/en.json | 8 +- .../volvooncall/translations/es.json | 3 +- .../volvooncall/translations/et.json | 3 +- .../volvooncall/translations/hu.json | 3 +- .../volvooncall/translations/id.json | 3 +- .../volvooncall/translations/it.json | 3 +- .../volvooncall/translations/no.json | 3 +- .../volvooncall/translations/pt-BR.json | 3 +- .../volvooncall/translations/zh-Hant.json | 3 +- .../components/vulcan/translations/es.json | 4 +- .../components/wallbox/translations/es.json | 2 +- .../components/watttime/translations/es.json | 2 +- .../xiaomi_ble/translations/es.json | 2 +- .../xiaomi_miio/translations/es.json | 2 +- .../yale_smart_alarm/translations/es.json | 2 +- .../components/yolink/translations/es.json | 2 +- .../components/zha/translations/ca.json | 77 ++++++++++- .../components/zha/translations/cs.json | 3 + .../components/zha/translations/el.json | 127 ++++++++++++++++- .../components/zha/translations/es.json | 1 + .../components/zha/translations/et.json | 7 + .../components/zha/translations/fr.json | 121 +++++++++++++++- .../components/zha/translations/hu.json | 129 +++++++++++++++++- .../components/zha/translations/id.json | 129 +++++++++++++++++- .../components/zha/translations/it.json | 129 +++++++++++++++++- .../components/zha/translations/ja.json | 125 ++++++++++++++++- .../components/zha/translations/no.json | 129 +++++++++++++++++- .../components/zha/translations/pt-BR.json | 6 +- .../components/zha/translations/zh-Hant.json | 129 +++++++++++++++++- 218 files changed, 2510 insertions(+), 143 deletions(-) create mode 100644 homeassistant/components/bthome/translations/hu.json create mode 100644 homeassistant/components/ecowitt/translations/ca.json create mode 100644 homeassistant/components/ecowitt/translations/cs.json create mode 100644 homeassistant/components/ecowitt/translations/de.json create mode 100644 homeassistant/components/ecowitt/translations/el.json create mode 100644 homeassistant/components/ecowitt/translations/es.json create mode 100644 homeassistant/components/ecowitt/translations/et.json create mode 100644 homeassistant/components/ecowitt/translations/fr.json create mode 100644 homeassistant/components/ecowitt/translations/hu.json create mode 100644 homeassistant/components/ecowitt/translations/id.json create mode 100644 homeassistant/components/ecowitt/translations/it.json create mode 100644 homeassistant/components/ecowitt/translations/ja.json create mode 100644 homeassistant/components/ecowitt/translations/no.json create mode 100644 homeassistant/components/ecowitt/translations/pt-BR.json create mode 100644 homeassistant/components/ecowitt/translations/zh-Hant.json create mode 100644 homeassistant/components/led_ble/translations/el.json create mode 100644 homeassistant/components/led_ble/translations/hu.json create mode 100644 homeassistant/components/led_ble/translations/id.json create mode 100644 homeassistant/components/led_ble/translations/ja.json create mode 100644 homeassistant/components/melnor/translations/ca.json create mode 100644 homeassistant/components/melnor/translations/de.json create mode 100644 homeassistant/components/melnor/translations/el.json create mode 100644 homeassistant/components/melnor/translations/es.json create mode 100644 homeassistant/components/melnor/translations/et.json create mode 100644 homeassistant/components/melnor/translations/fr.json create mode 100644 homeassistant/components/melnor/translations/hu.json create mode 100644 homeassistant/components/melnor/translations/id.json create mode 100644 homeassistant/components/melnor/translations/it.json create mode 100644 homeassistant/components/melnor/translations/no.json create mode 100644 homeassistant/components/melnor/translations/zh-Hant.json create mode 100644 homeassistant/components/nam/translations/sensor.ca.json create mode 100644 homeassistant/components/nam/translations/sensor.hu.json create mode 100644 homeassistant/components/nam/translations/sensor.id.json create mode 100644 homeassistant/components/nam/translations/sensor.ja.json create mode 100644 homeassistant/components/prusalink/translations/cs.json create mode 100644 homeassistant/components/prusalink/translations/el.json create mode 100644 homeassistant/components/prusalink/translations/fr.json create mode 100644 homeassistant/components/prusalink/translations/hu.json create mode 100644 homeassistant/components/prusalink/translations/id.json create mode 100644 homeassistant/components/prusalink/translations/it.json create mode 100644 homeassistant/components/prusalink/translations/ja.json create mode 100644 homeassistant/components/prusalink/translations/sensor.cs.json create mode 100644 homeassistant/components/prusalink/translations/sensor.el.json create mode 100644 homeassistant/components/prusalink/translations/sensor.fr.json create mode 100644 homeassistant/components/prusalink/translations/sensor.hu.json create mode 100644 homeassistant/components/prusalink/translations/sensor.id.json create mode 100644 homeassistant/components/prusalink/translations/sensor.it.json create mode 100644 homeassistant/components/prusalink/translations/sensor.ja.json create mode 100644 homeassistant/components/sensorpro/translations/ca.json create mode 100644 homeassistant/components/sensorpro/translations/de.json create mode 100644 homeassistant/components/sensorpro/translations/el.json create mode 100644 homeassistant/components/sensorpro/translations/es.json create mode 100644 homeassistant/components/sensorpro/translations/et.json create mode 100644 homeassistant/components/sensorpro/translations/fr.json create mode 100644 homeassistant/components/sensorpro/translations/hu.json create mode 100644 homeassistant/components/sensorpro/translations/id.json create mode 100644 homeassistant/components/sensorpro/translations/it.json create mode 100644 homeassistant/components/sensorpro/translations/ja.json create mode 100644 homeassistant/components/sensorpro/translations/no.json create mode 100644 homeassistant/components/sensorpro/translations/pt-BR.json create mode 100644 homeassistant/components/sensorpro/translations/zh-Hant.json create mode 100644 homeassistant/components/thermobeacon/translations/hu.json diff --git a/homeassistant/components/abode/translations/es.json b/homeassistant/components/abode/translations/es.json index c7db5e8db6a..6a9e70c1363 100644 --- a/homeassistant/components/abode/translations/es.json +++ b/homeassistant/components/abode/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente", "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, "error": { diff --git a/homeassistant/components/airvisual/translations/es.json b/homeassistant/components/airvisual/translations/es.json index acffe47f3ca..33802db6415 100644 --- a/homeassistant/components/airvisual/translations/es.json +++ b/homeassistant/components/airvisual/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "La ubicaci\u00f3n ya est\u00e1 configurada o el Nodo/Pro ID ya est\u00e1 registrado.", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/aladdin_connect/translations/es.json b/homeassistant/components/aladdin_connect/translations/es.json index 5621a1c69e9..e412d52efef 100644 --- a/homeassistant/components/aladdin_connect/translations/es.json +++ b/homeassistant/components/aladdin_connect/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/ambee/translations/es.json b/homeassistant/components/ambee/translations/es.json index fde555ad801..205b9adaf3a 100644 --- a/homeassistant/components/ambee/translations/es.json +++ b/homeassistant/components/ambee/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "cannot_connect": "No se pudo conectar", @@ -11,7 +11,7 @@ "reauth_confirm": { "data": { "api_key": "Clave API", - "description": "Vuelve a autenticarse con tu cuenta de Ambee." + "description": "Vuelve a autenticarte con tu cuenta Ambee." } }, "user": { diff --git a/homeassistant/components/apple_tv/translations/es.json b/homeassistant/components/apple_tv/translations/es.json index 3881692d0be..1f6966605cc 100644 --- a/homeassistant/components/apple_tv/translations/es.json +++ b/homeassistant/components/apple_tv/translations/es.json @@ -9,7 +9,7 @@ "inconsistent_device": "No se encontraron los protocolos esperados durante el descubrimiento. Esto normalmente indica un problema con multicast DNS (Zeroconf). Por favor, intenta a\u00f1adir el dispositivo de nuevo.", "ipv6_not_supported": "IPv6 no es compatible.", "no_devices_found": "No se encontraron dispositivos en la red", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente", "setup_failed": "No se pudo configurar el dispositivo.", "unknown": "Error inesperado" }, diff --git a/homeassistant/components/august/translations/es.json b/homeassistant/components/august/translations/es.json index b9334d7b473..25f62e6c1db 100644 --- a/homeassistant/components/august/translations/es.json +++ b/homeassistant/components/august/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/aussie_broadband/translations/es.json b/homeassistant/components/aussie_broadband/translations/es.json index ce0f6022094..49165aaf40f 100644 --- a/homeassistant/components/aussie_broadband/translations/es.json +++ b/homeassistant/components/aussie_broadband/translations/es.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", "no_services_found": "No se encontraron servicios para esta cuenta", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/automation/translations/ca.json b/homeassistant/components/automation/translations/ca.json index 4a6cc33e04c..3a85ab5fe22 100644 --- a/homeassistant/components/automation/translations/ca.json +++ b/homeassistant/components/automation/translations/ca.json @@ -4,6 +4,7 @@ "fix_flow": { "step": { "confirm": { + "description": "L'automatitzaci\u00f3 \"{name}\" (`{entity_id}`) cont\u00e9 una acci\u00f3 que crida el servei desconegut: `{service}`.\n\nAix\u00f2 fa que l'automatitzaci\u00f3 no funcioni correctament. Potser aquest servei ja no est\u00e0 disponible, o potser un l'ha causat un error ortogr\u00e0fic o d'escriptura. \n\nPer corregir aquest error, [edita l'automatitzaci\u00f3]({edit}) i elimina l'acci\u00f3 que crida aquest servei.\n\nFes clic a ENVIA, a continuaci\u00f3, quan hagis solucionat l'error d'aquesta automatitzaci\u00f3.", "title": "{name} utilitza un servei desconegut" } } diff --git a/homeassistant/components/automation/translations/cs.json b/homeassistant/components/automation/translations/cs.json index b4b3c61be5a..a59030fd397 100644 --- a/homeassistant/components/automation/translations/cs.json +++ b/homeassistant/components/automation/translations/cs.json @@ -1,4 +1,17 @@ { + "issues": { + "service_not_found": { + "fix_flow": { + "step": { + "confirm": { + "description": "Automatizace \"{name}\" (`{entity_id}`) m\u00e1 akci, kter\u00e1 vol\u00e1 nezn\u00e1mou slu\u017ebu: `{service}`.\n\nTato chyba br\u00e1n\u00ed spr\u00e1vn\u00e9mu spu\u0161t\u011bn\u00ed automatizace. Mo\u017en\u00e1 tato slu\u017eba ji\u017e nen\u00ed k dispozici, nebo ji zp\u016fsobil p\u0159eklep.\n\nChcete-li tuto chybu opravit, [upravte automatizaci]({edit}) a odstra\u0148te akci, kter\u00e1 tuto slu\u017ebu vol\u00e1.\n\nKliknut\u00edm na tla\u010d\u00edtko ULO\u017dIT potvr\u010fte, \u017ee jste tuto automatizaci opravili.", + "title": "{name} pou\u017e\u00edv\u00e1 nezn\u00e1mou slu\u017ebu" + } + } + }, + "title": "{name} pou\u017e\u00edv\u00e1 nezn\u00e1mou slu\u017ebu" + } + }, "state": { "_": { "off": "Vypnuto", diff --git a/homeassistant/components/awair/translations/es.json b/homeassistant/components/awair/translations/es.json index 82568ce9983..1f2508ec6e3 100644 --- a/homeassistant/components/awair/translations/es.json +++ b/homeassistant/components/awair/translations/es.json @@ -5,7 +5,7 @@ "already_configured_account": "La cuenta ya est\u00e1 configurada", "already_configured_device": "El dispositivo ya est\u00e1 configurado", "no_devices_found": "No se encontraron dispositivos en la red", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente", "unreachable": "No se pudo conectar" }, "error": { diff --git a/homeassistant/components/azure_devops/translations/es.json b/homeassistant/components/azure_devops/translations/es.json index 8414e03a727..a2a776f540c 100644 --- a/homeassistant/components/azure_devops/translations/es.json +++ b/homeassistant/components/azure_devops/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "cannot_connect": "No se pudo conectar", @@ -16,7 +16,7 @@ "personal_access_token": "Token Personal de Acceso (PAT)" }, "description": "Error de autenticaci\u00f3n para {project_url}. Por favor, introduce tus credenciales actuales.", - "title": "Reautenticaci\u00f3n" + "title": "Volver a autenticar" }, "user": { "data": { diff --git a/homeassistant/components/bluetooth/translations/ca.json b/homeassistant/components/bluetooth/translations/ca.json index 6c1554dc0d9..8adff8747b5 100644 --- a/homeassistant/components/bluetooth/translations/ca.json +++ b/homeassistant/components/bluetooth/translations/ca.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El servei ja est\u00e0 configurat", - "no_adapters": "No s'ha trobat cap adaptador Bluetooth" + "no_adapters": "No s'han trobat adaptadors Bluetooth sense configurar" }, "flow_title": "{name}", "step": { @@ -34,8 +34,9 @@ "init": { "data": { "adapter": "Adaptador Bluetooth a utilitzar per escanejar", - "passive": "Escolta passiva" - } + "passive": "Escaneig passiu" + }, + "description": "L'escolta passiva necessita BlueZ 5.63 o posterior i les funcions experimentals activades." } } } diff --git a/homeassistant/components/bluetooth/translations/hu.json b/homeassistant/components/bluetooth/translations/hu.json index a79bac619d8..38eb6d5ada0 100644 --- a/homeassistant/components/bluetooth/translations/hu.json +++ b/homeassistant/components/bluetooth/translations/hu.json @@ -34,7 +34,7 @@ "init": { "data": { "adapter": "A szkennel\u00e9shez haszn\u00e1lhat\u00f3 Bluetooth-adapter", - "passive": "Passz\u00edv hallgat\u00e1s" + "passive": "Passz\u00edv figyel\u00e9s" }, "description": "A passz\u00edv hallgat\u00e1shoz BlueZ 5.63 vagy \u00fajabb verzi\u00f3ra van sz\u00fcks\u00e9g, a k\u00eds\u00e9rleti funkci\u00f3k enged\u00e9lyez\u00e9s\u00e9vel." } diff --git a/homeassistant/components/bosch_shc/translations/es.json b/homeassistant/components/bosch_shc/translations/es.json index b2934bca747..bf4b20914ec 100644 --- a/homeassistant/components/bosch_shc/translations/es.json +++ b/homeassistant/components/bosch_shc/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/brunt/translations/es.json b/homeassistant/components/brunt/translations/es.json index 7a912e267f9..e48edfeb3f0 100644 --- a/homeassistant/components/brunt/translations/es.json +++ b/homeassistant/components/brunt/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/bthome/translations/es.json b/homeassistant/components/bthome/translations/es.json index 4cf15c9c1d3..ed86afea60b 100644 --- a/homeassistant/components/bthome/translations/es.json +++ b/homeassistant/components/bthome/translations/es.json @@ -4,7 +4,7 @@ "already_configured": "El dispositivo ya est\u00e1 configurado", "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "no_devices_found": "No se encontraron dispositivos en la red", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "decryption_failed": "La clave de enlace proporcionada no funcion\u00f3, los datos del sensor no se pudieron descifrar. Por favor, compru\u00e9balo e int\u00e9ntalo de nuevo.", diff --git a/homeassistant/components/bthome/translations/hu.json b/homeassistant/components/bthome/translations/hu.json new file mode 100644 index 00000000000..1bf4fffab68 --- /dev/null +++ b/homeassistant/components/bthome/translations/hu.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." + }, + "error": { + "decryption_failed": "A megadott kulcs nem m\u0171k\u00f6d\u00f6tt, az \u00e9rz\u00e9kel\u0151adatokat nem lehetett kiolvasni. K\u00e9rj\u00fck, ellen\u0151rizze \u00e9s pr\u00f3b\u00e1lja meg \u00fajra.", + "expected_32_characters": "32 karakterb\u0151l \u00e1ll\u00f3 hexadecim\u00e1lis kulcsra van sz\u00fcks\u00e9g." + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name}?" + }, + "get_encryption_key": { + "data": { + "bindkey": "Kulcs (bindkey)" + }, + "description": "Az \u00e9rz\u00e9kel\u0151 adatai titkos\u00edtva vannak. A visszafejt\u00e9shez egy 32 karakterb\u0151l \u00e1ll\u00f3 hexadecim\u00e1lis kulcsra van sz\u00fcks\u00e9g." + }, + "user": { + "data": { + "address": "Eszk\u00f6z" + }, + "description": "V\u00e1lassza ki a be\u00e1ll\u00edtani k\u00edv\u00e1nt eszk\u00f6zt" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/camera/translations/ca.json b/homeassistant/components/camera/translations/ca.json index 0a7b029aced..bf3b1fe0a6c 100644 --- a/homeassistant/components/camera/translations/ca.json +++ b/homeassistant/components/camera/translations/ca.json @@ -3,7 +3,7 @@ "_": { "idle": "Inactiu", "recording": "Enregistrant", - "streaming": "Transmetent v\u00eddeo" + "streaming": "En directe" } }, "title": "C\u00e0mera" diff --git a/homeassistant/components/cloudflare/translations/es.json b/homeassistant/components/cloudflare/translations/es.json index d47711bf0a5..a711ccfd819 100644 --- a/homeassistant/components/cloudflare/translations/es.json +++ b/homeassistant/components/cloudflare/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente", "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n.", "unknown": "Error inesperado" }, @@ -15,7 +15,7 @@ "reauth_confirm": { "data": { "api_token": "Token API", - "description": "Vuelve a autenticarte con tu cuenta de Cloudflare." + "description": "Vuelve a autenticarte con tu cuenta Cloudflare." } }, "records": { diff --git a/homeassistant/components/devolo_home_control/translations/es.json b/homeassistant/components/devolo_home_control/translations/es.json index 91a06530273..b8d998915fb 100644 --- a/homeassistant/components/devolo_home_control/translations/es.json +++ b/homeassistant/components/devolo_home_control/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", diff --git a/homeassistant/components/discord/translations/es.json b/homeassistant/components/discord/translations/es.json index df4bc5a7fa3..0ce9ee06583 100644 --- a/homeassistant/components/discord/translations/es.json +++ b/homeassistant/components/discord/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El servicio ya est\u00e1 configurado", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/ecowitt/translations/ca.json b/homeassistant/components/ecowitt/translations/ca.json new file mode 100644 index 00000000000..c2c0bfcfeab --- /dev/null +++ b/homeassistant/components/ecowitt/translations/ca.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "invalid_port": "Aquest port ja est\u00e0 en \u00fas.", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "path": "Cam\u00ed amb testimoni de seguretat", + "port": "Escoltant el port" + }, + "description": "S'han de completar els seg\u00fcents passos per configurar aquesta integraci\u00f3.\n\nUtilitza l'aplicaci\u00f3 Ecowitt (al teu tel\u00e8fon) o accedeix a la Web Ecowitt des d'un navegador a l'adre\u00e7a IP de l'estaci\u00f3.\nEscull la teva estaci\u00f3 -> Menu Others -> DIY Upload Servers.\nFes clic a Seg\u00fcent i selecciona \"Personalitzat\"\n\nEscull el protocol Ecowitt i estableix la IP o l'adre\u00e7a del teu servidor Home Assistant.\nLa ruta ha de conincidir, la pots copiar amb el testimoni de seguretat /.\nDesa la configuraci\u00f3. L'Ecowitt hauria de comen\u00e7ar a enviar dades al teu servidor." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecowitt/translations/cs.json b/homeassistant/components/ecowitt/translations/cs.json new file mode 100644 index 00000000000..e1bf8e7f45f --- /dev/null +++ b/homeassistant/components/ecowitt/translations/cs.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecowitt/translations/de.json b/homeassistant/components/ecowitt/translations/de.json new file mode 100644 index 00000000000..752363c97e7 --- /dev/null +++ b/homeassistant/components/ecowitt/translations/de.json @@ -0,0 +1,20 @@ +{ + "config": { + "create_entry": { + "default": "Um die Integration abzuschlie\u00dfen, verwende die Ecowitt App (auf deinem Telefon) oder rufe die Ecowitt WebUI in einem Browser unter der IP-Adresse der Station auf.\n\nW\u00e4hle deine Station -> Men\u00fc Andere -> DIY Upload Servers. Klicke auf \"Weiter\" und w\u00e4hle \"Angepasst\".\n\n- Server IP: `{server}`\n- Pfad: `{path}`\n- Anschluss: `{port}`\n\nKlicke auf \"Speichern\"." + }, + "error": { + "invalid_port": "Port wird bereits verwendet.", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "path": "Pfad mit Sicherheits-Token", + "port": "Listening-Port" + }, + "description": "M\u00f6chtest du Ecowitt wirklich einrichten?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecowitt/translations/el.json b/homeassistant/components/ecowitt/translations/el.json new file mode 100644 index 00000000000..e8022f8808a --- /dev/null +++ b/homeassistant/components/ecowitt/translations/el.json @@ -0,0 +1,20 @@ +{ + "config": { + "create_entry": { + "default": "\u0393\u03b9\u03b1 \u03bd\u03b1 \u03bf\u03bb\u03bf\u03ba\u03bb\u03b7\u03c1\u03ce\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2, \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae Ecowitt App (\u03c3\u03c4\u03bf \u03c4\u03b7\u03bb\u03ad\u03c6\u03c9\u03bd\u03cc \u03c3\u03b1\u03c2) \u03ae \u03b1\u03c0\u03bf\u03ba\u03c4\u03ae\u03c3\u03c4\u03b5 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7 \u03c3\u03c4\u03bf Ecowitt WebUI \u03c3\u03b5 \u03ad\u03bd\u03b1 \u03c0\u03c1\u03cc\u03b3\u03c1\u03b1\u03bc\u03bc\u03b1 \u03c0\u03b5\u03c1\u03b9\u03ae\u03b3\u03b7\u03c3\u03b7\u03c2 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03c4\u03bf\u03c5 \u03c3\u03c4\u03b1\u03b8\u03bc\u03bf\u03cd.\n\n\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03bf \u03c3\u03c4\u03b1\u03b8\u03bc\u03cc \u03c3\u03b1\u03c2 -> \u039c\u03b5\u03bd\u03bf\u03cd \u0386\u03bb\u03bb\u03bf\u03b9 -> \u0395\u03be\u03c5\u03c0\u03b7\u03c1\u03b5\u03c4\u03b7\u03c4\u03ad\u03c2 \u03c6\u03cc\u03c1\u03c4\u03c9\u03c3\u03b7\u03c2 DIY. \u03a0\u03b1\u03c4\u03ae\u03c3\u03c4\u03b5 next \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 'Customized' (\u03a0\u03c1\u03bf\u03c3\u03b1\u03c1\u03bc\u03bf\u03c3\u03bc\u03ad\u03bd\u03bf)\n\n- IP \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae: `{server}`\n- \u0394\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae: `{path}`\n- \u0398\u03cd\u03c1\u03b1: `{port}`\n\n\u039a\u03ac\u03bd\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03b7\u03bd '\u0391\u03c0\u03bf\u03b8\u03ae\u03ba\u03b5\u03c5\u03c3\u03b7'." + }, + "error": { + "invalid_port": "\u0397 \u03b8\u03cd\u03c1\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7.", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "user": { + "data": { + "path": "\u0394\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03bc\u03b5 \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03b1\u03c3\u03c6\u03b1\u03bb\u03b5\u03af\u03b1\u03c2", + "port": "\u0398\u03cd\u03c1\u03b1 \u03b1\u03ba\u03c1\u03cc\u03b1\u03c3\u03b7\u03c2" + }, + "description": "\u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03ba\u03c4\u03b5\u03bb\u03b5\u03c3\u03c4\u03bf\u03cd\u03bd \u03c4\u03b1 \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03b1 \u03b2\u03ae\u03bc\u03b1\u03c4\u03b1 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03ae\u03bd \u03c4\u03b7\u03bd \u03b5\u03bd\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7. \n\n \u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae Ecowitt (\u03c3\u03c4\u03bf \u03c4\u03b7\u03bb\u03ad\u03c6\u03c9\u03bd\u03cc \u03c3\u03b1\u03c2) \u03ae \u03b1\u03c0\u03bf\u03ba\u03c4\u03ae\u03c3\u03c4\u03b5 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7 \u03c3\u03c4\u03bf Ecowitt WebUI \u03c3\u03b5 \u03ad\u03bd\u03b1 \u03c0\u03c1\u03cc\u03b3\u03c1\u03b1\u03bc\u03bc\u03b1 \u03c0\u03b5\u03c1\u03b9\u03ae\u03b3\u03b7\u03c3\u03b7\u03c2 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03c4\u03bf\u03c5 \u03c3\u03c4\u03b1\u03b8\u03bc\u03bf\u03cd.\n \u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03bf \u03c3\u03c4\u03b1\u03b8\u03bc\u03cc \u03c3\u03b1\u03c2 - > \u039c\u03b5\u03bd\u03bf\u03cd \u0386\u03bb\u03bb\u03b1 - > \u0394\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ad\u03c2 \u03bc\u03b5\u03c4\u03b1\u03c6\u03cc\u03c1\u03c4\u03c9\u03c3\u03b7\u03c2 DIY.\n \u03a0\u03b1\u03c4\u03ae\u03c3\u03c4\u03b5 \u03b5\u03c0\u03cc\u03bc\u03b5\u03bd\u03bf \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \"\u03a0\u03c1\u03bf\u03c3\u03b1\u03c1\u03bc\u03bf\u03c3\u03bc\u03ad\u03bd\u03bf\" \n\n \u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03bf \u03c0\u03c1\u03c9\u03c4\u03cc\u03ba\u03bf\u03bb\u03bb\u03bf Ecowitt \u03ba\u03b1\u03b9 \u03b2\u03ac\u03bb\u03c4\u03b5 \u03c4\u03bf ip/hostname \u03c4\u03bf\u03c5 hass server \u03c3\u03b1\u03c2.\n \u0397 \u03b4\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c4\u03b1\u03b9\u03c1\u03b9\u03ac\u03b6\u03b5\u03b9, \u03bc\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03b1\u03bd\u03c4\u03b9\u03b3\u03c1\u03ac\u03c8\u03b5\u03c4\u03b5 \u03bc\u03b5 \u03b1\u03c3\u03c6\u03b1\u03bb\u03ad\u03c2 \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc /.\n \u0391\u03c0\u03bf\u03b8\u03ae\u03ba\u03b5\u03c5\u03c3\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2. \u03a4\u03bf Ecowitt \u03b8\u03b1 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03c3\u03c4\u03b7 \u03c3\u03c5\u03bd\u03ad\u03c7\u03b5\u03b9\u03b1 \u03bd\u03b1 \u03b1\u03c1\u03c7\u03af\u03c3\u03b5\u03b9 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03c0\u03b1\u03b8\u03b5\u03af \u03bd\u03b1 \u03c3\u03c4\u03b5\u03af\u03bb\u03b5\u03b9 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03b1 \u03c3\u03c4\u03bf\u03bd \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae \u03c3\u03b1\u03c2." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecowitt/translations/en.json b/homeassistant/components/ecowitt/translations/en.json index b8ce69c10b8..77b50cd6462 100644 --- a/homeassistant/components/ecowitt/translations/en.json +++ b/homeassistant/components/ecowitt/translations/en.json @@ -3,8 +3,16 @@ "create_entry": { "default": "To finish setting up the integration, use the Ecowitt App (on your phone) or access the Ecowitt WebUI in a browser at the station IP address.\n\nPick your station -> Menu Others -> DIY Upload Servers. Hit next and select 'Customized'\n\n- Server IP: `{server}`\n- Path: `{path}`\n- Port: `{port}`\n\nClick on 'Save'." }, + "error": { + "invalid_port": "Port is already used.", + "unknown": "Unexpected error" + }, "step": { "user": { + "data": { + "path": "Path with Security token", + "port": "Listening port" + }, "description": "Are you sure you want to set up Ecowitt?" } } diff --git a/homeassistant/components/ecowitt/translations/es.json b/homeassistant/components/ecowitt/translations/es.json new file mode 100644 index 00000000000..94e21c4782c --- /dev/null +++ b/homeassistant/components/ecowitt/translations/es.json @@ -0,0 +1,20 @@ +{ + "config": { + "create_entry": { + "default": "Para terminar de configurar la integraci\u00f3n, usa la aplicaci\u00f3n Ecowitt (en tu tel\u00e9fono) o accede a Ecowitt WebUI en un navegador en la direcci\u00f3n IP de la estaci\u00f3n. \n\nElige tu estaci\u00f3n - > Men\u00fa Otros - > Servidores de carga de bricolaje. Presiona siguiente y selecciona 'Personalizado' \n\n- IP del servidor: `{server}`\n- Ruta: `{path}`\n- Puerto: `{port}` \n\nHaz clic en 'Guardar'." + }, + "error": { + "invalid_port": "El puerto ya est\u00e1 en uso.", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "path": "Ruta con token de seguridad", + "port": "Puerto de escucha" + }, + "description": "\u00bfEst\u00e1s seguro de que quieres configurar Ecowitt?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecowitt/translations/et.json b/homeassistant/components/ecowitt/translations/et.json new file mode 100644 index 00000000000..e132191ca37 --- /dev/null +++ b/homeassistant/components/ecowitt/translations/et.json @@ -0,0 +1,20 @@ +{ + "config": { + "create_entry": { + "default": "Sidumise seadistamise l\u00f5petamiseks kasuta Ecowitti rakendust (telefonis) v\u00f5i sisene Ecowitt WebUI-sse brauseris jaama IP-aadressil.\n\nVali oma jaam -> men\u00fc\u00fc Muud -> DIY Upload Servers. Vajuta nuppu next ja vali 'Customized' (kohandatud)\n\n- Serveri IP: `{server}`\n- Path: `{path}`\n- Port: `{port}`\n\nVajuta nupule 'Save'." + }, + "error": { + "invalid_port": "Port on juba kasutusel.", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "path": "Turvam\u00e4rgiga asukoht", + "port": "Kuulamisport" + }, + "description": "Kas oled kindel, et soovid Ecowitti seadistada?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecowitt/translations/fr.json b/homeassistant/components/ecowitt/translations/fr.json new file mode 100644 index 00000000000..1cb6ad07d2d --- /dev/null +++ b/homeassistant/components/ecowitt/translations/fr.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "invalid_port": "Le port est d\u00e9j\u00e0 utilis\u00e9.", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "port": "Port d'\u00e9coute" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecowitt/translations/hu.json b/homeassistant/components/ecowitt/translations/hu.json new file mode 100644 index 00000000000..920602311bf --- /dev/null +++ b/homeassistant/components/ecowitt/translations/hu.json @@ -0,0 +1,20 @@ +{ + "config": { + "create_entry": { + "default": "Az integr\u00e1ci\u00f3 be\u00e1ll\u00edt\u00e1s\u00e1nak befejez\u00e9s\u00e9hez haszn\u00e1lja az Ecowitt alkalmaz\u00e1st (a telefonj\u00e1n), vagy l\u00e9pjen be az Ecowitt WebUI-ba egy b\u00f6ng\u00e9sz\u0151ben az \u00e1llom\u00e1s IP-c\u00edm\u00e9n.\n\nV\u00e1lassza ki az \u00e1llom\u00e1s\u00e1t -> 'Others' men\u00fc -> 'DIY Upload Servers'. Nyomja meg a 'Next' gombot, \u00e9s v\u00e1lassza a 'Customized' lehet\u0151s\u00e9get.\n\n- Szerver IP: `{server}`\n- \u00datvonal: `{path}`\n- Port: `{port}`\n\nKattintson a 'Save' gombra." + }, + "error": { + "invalid_port": "A port m\u00e1r haszn\u00e1latban van.", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "user": { + "data": { + "path": "Biztons\u00e1gi tokennel ell\u00e1tott el\u00e9r\u00e9si \u00fatvonal", + "port": "Figyel\u0151port" + }, + "description": "Biztos benne, hogy be szeretn\u00e9 \u00e1ll\u00edtani: Ecowitt?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecowitt/translations/id.json b/homeassistant/components/ecowitt/translations/id.json new file mode 100644 index 00000000000..36479f19729 --- /dev/null +++ b/homeassistant/components/ecowitt/translations/id.json @@ -0,0 +1,20 @@ +{ + "config": { + "create_entry": { + "default": "Untuk menyelesaikan pengaturan integrasi, gunakan Ecowitt App (pada ponsel Anda) atau akses Ecowitt WebUI di browser pada alamat IP stasiun.\n\nPilih stasiun Anda -> Menu Others -> DIY Upload Servers. Tekan 'Next' dan pilih 'Customized'\n\n- Server IP: `{server}`\n- Path: `{path}`\n- Port: `{port}`\n\nKlik 'Simpan'." + }, + "error": { + "invalid_port": "Port sudah digunakan.", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "path": "Jalur dengan token Keamanan", + "port": "Port mendengarkan" + }, + "description": "Yakin ingin menyiapkan Ecowitt?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecowitt/translations/it.json b/homeassistant/components/ecowitt/translations/it.json new file mode 100644 index 00000000000..91cfcd52fe2 --- /dev/null +++ b/homeassistant/components/ecowitt/translations/it.json @@ -0,0 +1,20 @@ +{ + "config": { + "create_entry": { + "default": "Per completare la configurazione dell'integrazione, utilizzare l'App Ecowitt (sul telefono) o accedere all'Ecowitt WebUI in un browser all'indirizzo IP della stazione. \n\nScegli la tua stazione - > Menu Altri - > Server di caricamento fai-da-te. Premi Avanti e seleziona \"Personalizzata\" \n\n - IP del server: `{server}`\n - Percorso: `{path}`\n - Porta: `{port}` \n\n Fai clic su \"Salva\"." + }, + "error": { + "invalid_port": "La porta \u00e8 gi\u00e0 utilizzata.", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "path": "Percorso con token di sicurezza", + "port": "Porta di ascolto" + }, + "description": "Sei sicuro di voler configurare Ecowitt?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecowitt/translations/ja.json b/homeassistant/components/ecowitt/translations/ja.json new file mode 100644 index 00000000000..0ac12f0af77 --- /dev/null +++ b/homeassistant/components/ecowitt/translations/ja.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "invalid_port": "\u30dd\u30fc\u30c8\u306f\u3059\u3067\u306b\u4f7f\u7528\u3055\u308c\u3066\u3044\u307e\u3059\u3002", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "step": { + "user": { + "data": { + "path": "\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u30c8\u30fc\u30af\u30f3\u3092\u542b\u3080\u30d1\u30b9", + "port": "\u30ea\u30b9\u30cb\u30f3\u30b0\u30dd\u30fc\u30c8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecowitt/translations/no.json b/homeassistant/components/ecowitt/translations/no.json new file mode 100644 index 00000000000..61372b6f49f --- /dev/null +++ b/homeassistant/components/ecowitt/translations/no.json @@ -0,0 +1,20 @@ +{ + "config": { + "create_entry": { + "default": "For \u00e5 fullf\u00f8re konfigureringen av integrasjonen, bruk Ecowitt-appen (p\u00e5 telefonen) eller g\u00e5 til Ecowitt WebUI i en nettleser p\u00e5 stasjonens IP-adresse. \n\n Velg stasjonen din - > Meny Andre - > DIY-opplastingsservere. Trykk neste og velg \"Tilpasset\" \n\n - Server IP: ` {server} `\n - Bane: ` {path} `\n - Port: ` {port} ` \n\n Klikk p\u00e5 'Lagre'." + }, + "error": { + "invalid_port": "Porten er allerede i bruk.", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "path": "Bane med sikkerhetstoken", + "port": "Lytteport" + }, + "description": "Er du sikker p\u00e5 at du vil sette opp Ecowitt?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecowitt/translations/pt-BR.json b/homeassistant/components/ecowitt/translations/pt-BR.json new file mode 100644 index 00000000000..b0c23d7a35d --- /dev/null +++ b/homeassistant/components/ecowitt/translations/pt-BR.json @@ -0,0 +1,20 @@ +{ + "config": { + "create_entry": { + "default": "Para finalizar a configura\u00e7\u00e3o da integra\u00e7\u00e3o, use o app Ecowitt (no seu smartphone) ou acesse o Ecowitt WebUI em um navegador no endere\u00e7o IP da esta\u00e7\u00e3o. \n\n Escolha sua esta\u00e7\u00e3o - > Menu Outros - > Servidores de Upload DIY. Clique em pr\u00f3ximo e selecione 'Personalizado' \n\n - IP do servidor: `{server}`\n - Caminho: `{path}`\n - Porta: `{port}` \n\n Clique em 'Salvar'." + }, + "error": { + "invalid_port": "A porta j\u00e1 \u00e9 usada.", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "path": "Caminho com token de seguran\u00e7a", + "port": "Porta de escuta" + }, + "description": "Tem certeza de que deseja configurar o Ecowitt?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecowitt/translations/zh-Hant.json b/homeassistant/components/ecowitt/translations/zh-Hant.json new file mode 100644 index 00000000000..3ad87a5733b --- /dev/null +++ b/homeassistant/components/ecowitt/translations/zh-Hant.json @@ -0,0 +1,20 @@ +{ + "config": { + "create_entry": { + "default": "\u5fc5\u9808\u57f7\u884c\u4ee5\u4e0b\u6b65\u9a5f\u4ee5\u8a2d\u5b9a\u6b64\u6574\u5408\u3001\u65bc\u624b\u6a5f\u4e0a\u4f7f\u7528 Ecowitt App \u6216\u4f7f\u7528\u700f\u89bd\u5668\u8f38\u5165\u7ad9\u9ede IP \u4f4d\u5740\u9032\u5165 Ecowitt WebUI\u3002\n\n\u9078\u64c7\u7ad9\u9ede -> \u9078\u55ae\u4e2d\u5176\u4ed6 -> DIY \u4e0a\u50b3\u4f3a\u670d\u5668\u3001\u9ede\u9078\u4e0b\u4e00\u6b65\u4e26\u9078\u64c7 '\u81ea\u8a02'\n\n- \u4f3a\u670d\u5668 IP\uff1a`{server}`\n- \u8def\u5f91\uff1a`{path}`\n- \u901a\u8a0a\u57e0\uff1a`{port}`\n\n\u9ede\u9078 '\u5132\u5b58'\u3002" + }, + "error": { + "invalid_port": "\u901a\u8a0a\u57e0\u5df2\u88ab\u4f7f\u7528\u3002", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "path": "\u52a0\u5bc6\u6b0a\u6756\u8def\u5f91", + "port": "\u76e3\u807d\u901a\u8a0a\u57e0" + }, + "description": "\u662f\u5426\u8981\u8a2d\u5b9a Ecowitt\uff1f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/efergy/translations/es.json b/homeassistant/components/efergy/translations/es.json index a93f2f42235..0318f1b5837 100644 --- a/homeassistant/components/efergy/translations/es.json +++ b/homeassistant/components/efergy/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/enphase_envoy/translations/es.json b/homeassistant/components/enphase_envoy/translations/es.json index ab385d0a282..c30865d6ffb 100644 --- a/homeassistant/components/enphase_envoy/translations/es.json +++ b/homeassistant/components/enphase_envoy/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/esphome/translations/es.json b/homeassistant/components/esphome/translations/es.json index 87c8dc6ddad..82066953472 100644 --- a/homeassistant/components/esphome/translations/es.json +++ b/homeassistant/components/esphome/translations/es.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "connection_error": "No se puede conectar a ESP. Por favor, aseg\u00farate de que tu archivo YAML contiene una l\u00ednea 'api:'.", diff --git a/homeassistant/components/fireservicerota/translations/es.json b/homeassistant/components/fireservicerota/translations/es.json index 19ba8da21dd..ddd231ce700 100644 --- a/homeassistant/components/fireservicerota/translations/es.json +++ b/homeassistant/components/fireservicerota/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "create_entry": { "default": "Autenticado correctamente" diff --git a/homeassistant/components/flume/translations/es.json b/homeassistant/components/flume/translations/es.json index 5de43c8dcd1..40f22bccd94 100644 --- a/homeassistant/components/flume/translations/es.json +++ b/homeassistant/components/flume/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/fritz/translations/es.json b/homeassistant/components/fritz/translations/es.json index a5263d4e056..5da1ae4197b 100644 --- a/homeassistant/components/fritz/translations/es.json +++ b/homeassistant/components/fritz/translations/es.json @@ -4,7 +4,7 @@ "already_configured": "El dispositivo ya est\u00e1 configurado", "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "ignore_ip6_link_local": "La direcci\u00f3n de enlace local IPv6 no es compatible.", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "already_configured": "El dispositivo ya est\u00e1 configurado", diff --git a/homeassistant/components/fritzbox/translations/es.json b/homeassistant/components/fritzbox/translations/es.json index ed8184d958a..9edd3ec72d6 100644 --- a/homeassistant/components/fritzbox/translations/es.json +++ b/homeassistant/components/fritzbox/translations/es.json @@ -6,7 +6,7 @@ "ignore_ip6_link_local": "La direcci\u00f3n de enlace local IPv6 no es compatible.", "no_devices_found": "No se encontraron dispositivos en la red", "not_supported": "Conectado a AVM FRITZ!Box pero no es capaz de controlar dispositivos Smart Home.", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" diff --git a/homeassistant/components/geocaching/translations/es.json b/homeassistant/components/geocaching/translations/es.json index 357eeba9000..1060c57258d 100644 --- a/homeassistant/components/geocaching/translations/es.json +++ b/homeassistant/components/geocaching/translations/es.json @@ -7,7 +7,7 @@ "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [revisa la secci\u00f3n de ayuda]({docs_url})", "oauth_error": "Se han recibido datos de token no v\u00e1lidos.", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "create_entry": { "default": "Autenticado correctamente" diff --git a/homeassistant/components/google/translations/es.json b/homeassistant/components/google/translations/es.json index 401a1f37b94..107a320eb30 100644 --- a/homeassistant/components/google/translations/es.json +++ b/homeassistant/components/google/translations/es.json @@ -11,7 +11,7 @@ "invalid_access_token": "Token de acceso no v\u00e1lido", "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", "oauth_error": "Se han recibido datos de token no v\u00e1lidos.", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente", "timeout_connect": "Tiempo de espera agotado para establecer la conexi\u00f3n" }, "create_entry": { diff --git a/homeassistant/components/hive/translations/es.json b/homeassistant/components/hive/translations/es.json index 0bece2f0e54..fb419244bf6 100644 --- a/homeassistant/components/hive/translations/es.json +++ b/homeassistant/components/hive/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente", "unknown_entry": "No se puede encontrar una entrada existente." }, "error": { diff --git a/homeassistant/components/hyperion/translations/es.json b/homeassistant/components/hyperion/translations/es.json index 3220866ab16..fa2bddc95fa 100644 --- a/homeassistant/components/hyperion/translations/es.json +++ b/homeassistant/components/hyperion/translations/es.json @@ -8,7 +8,7 @@ "auth_required_error": "No se pudo determinar si se requiere autorizaci\u00f3n", "cannot_connect": "No se pudo conectar", "no_id": "La instancia de Hyperion Ambilight no inform\u00f3 su id", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/icloud/translations/ca.json b/homeassistant/components/icloud/translations/ca.json index 0ffdf5bc0c1..187617b23e7 100644 --- a/homeassistant/components/icloud/translations/ca.json +++ b/homeassistant/components/icloud/translations/ca.json @@ -18,6 +18,13 @@ "description": "La contrasenya introdu\u00efda anteriorment per a {username} ja no funciona. Actualitza la contrasenya per continuar utilitzant aquesta integraci\u00f3.", "title": "Reautenticaci\u00f3 de la integraci\u00f3" }, + "reauth_confirm": { + "data": { + "password": "Contrasenya" + }, + "description": "La contrasenya introdu\u00efda anteriorment per a {username} ja no funciona. Actualitza la contrasenya per continuar utilitzant aquesta integraci\u00f3.", + "title": "Reautenticaci\u00f3 de la integraci\u00f3" + }, "trusted_device": { "data": { "trusted_device": "Dispositiu de confian\u00e7a" diff --git a/homeassistant/components/icloud/translations/cs.json b/homeassistant/components/icloud/translations/cs.json index f06cae4019d..72dc892d15f 100644 --- a/homeassistant/components/icloud/translations/cs.json +++ b/homeassistant/components/icloud/translations/cs.json @@ -18,6 +18,12 @@ "description": "Va\u0161e zadan\u00e9 heslo pro {username} ji\u017e nefunguje. Chcete-li tuto d\u00e1le integraci pou\u017e\u00edvat, aktualizujte sv\u00e9 heslo.", "title": "Znovu ov\u011b\u0159it integraci" }, + "reauth_confirm": { + "data": { + "password": "Heslo" + }, + "title": "Znovu ov\u011b\u0159it integraci" + }, "trusted_device": { "data": { "trusted_device": "D\u016fv\u011bryhodn\u00e9 za\u0159\u00edzen\u00ed" diff --git a/homeassistant/components/icloud/translations/de.json b/homeassistant/components/icloud/translations/de.json index 207735018f0..4d9c0d63d0c 100644 --- a/homeassistant/components/icloud/translations/de.json +++ b/homeassistant/components/icloud/translations/de.json @@ -18,6 +18,13 @@ "description": "Dein zuvor eingegebenes Passwort f\u00fcr {username} funktioniert nicht mehr. Aktualisiere dein Passwort, um diese Integration weiterhin zu verwenden.", "title": "Integration erneut authentifizieren" }, + "reauth_confirm": { + "data": { + "password": "Passwort" + }, + "description": "Dein zuvor eingegebenes Passwort f\u00fcr {username} funktioniert nicht mehr. Aktualisiere dein Passwort, um diese Integration weiterhin zu verwenden.", + "title": "Integration erneut authentifizieren" + }, "trusted_device": { "data": { "trusted_device": "Vertrauensw\u00fcrdiges Ger\u00e4t" diff --git a/homeassistant/components/icloud/translations/el.json b/homeassistant/components/icloud/translations/el.json index cc484bd5660..d47a4349648 100644 --- a/homeassistant/components/icloud/translations/el.json +++ b/homeassistant/components/icloud/translations/el.json @@ -18,6 +18,13 @@ "description": "\u039f \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c0\u03bf\u03c5 \u03b5\u03af\u03c7\u03b1\u03c4\u03b5 \u03b5\u03b9\u03c3\u03ac\u03b3\u03b5\u03b9 \u03c0\u03c1\u03bf\u03b7\u03b3\u03bf\u03c5\u03bc\u03ad\u03bd\u03c9\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03bf {username} \u03b4\u03b5\u03bd \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03b5\u03af \u03c0\u03bb\u03ad\u03bf\u03bd. \u0395\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03ae\u03c2 \u03c3\u03b1\u03c2 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b5\u03c7\u03af\u03c3\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b5 \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7.", "title": "\u0395\u03c0\u03b1\u03bd\u03b1\u03bb\u03b7\u03c0\u03c4\u03b9\u03ba\u03cc\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2" }, + "reauth_confirm": { + "data": { + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" + }, + "description": "\u039f \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c0\u03bf\u03c5 \u03b5\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b1\u03c4\u03b5 \u03c0\u03c1\u03bf\u03b7\u03b3\u03bf\u03c5\u03bc\u03ad\u03bd\u03c9\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03bf {username} \u03b4\u03b5\u03bd \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03b5\u03af \u03c0\u03bb\u03ad\u03bf\u03bd. \u0395\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03ae\u03c2 \u03c3\u03b1\u03c2 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b5\u03c7\u03af\u03c3\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b5 \u03b1\u03c5\u03c4\u03ae\u03bd \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7.", + "title": "\u0395\u03c0\u03b1\u03bd\u03b1\u03bb\u03b7\u03c0\u03c4\u03b9\u03ba\u03cc\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2" + }, "trusted_device": { "data": { "trusted_device": "\u0391\u03be\u03b9\u03cc\u03c0\u03b9\u03c3\u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" diff --git a/homeassistant/components/icloud/translations/en.json b/homeassistant/components/icloud/translations/en.json index 65a8892c480..0052a858ede 100644 --- a/homeassistant/components/icloud/translations/en.json +++ b/homeassistant/components/icloud/translations/en.json @@ -11,6 +11,13 @@ "validate_verification_code": "Failed to verify your verification code, try again" }, "step": { + "reauth": { + "data": { + "password": "Password" + }, + "description": "Your previously entered password for {username} is no longer working. Update your password to keep using this integration.", + "title": "Reauthenticate Integration" + }, "reauth_confirm": { "data": { "password": "Password" diff --git a/homeassistant/components/icloud/translations/es.json b/homeassistant/components/icloud/translations/es.json index 5f0f4ac6495..ef1e6804469 100644 --- a/homeassistant/components/icloud/translations/es.json +++ b/homeassistant/components/icloud/translations/es.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", "no_device": "Ninguno de tus dispositivos tiene activado \"Buscar mi iPhone\"", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", @@ -18,6 +18,13 @@ "description": "La contrase\u00f1a introducida anteriormente para {username} ya no funciona. Actualiza tu contrase\u00f1a para seguir usando esta integraci\u00f3n.", "title": "Volver a autenticar la integraci\u00f3n" }, + "reauth_confirm": { + "data": { + "password": "Contrase\u00f1a" + }, + "description": "La contrase\u00f1a introducida anteriormente para {username} ya no funciona. Actualiza tu contrase\u00f1a para seguir usando esta integraci\u00f3n.", + "title": "Volver a autenticar la integraci\u00f3n" + }, "trusted_device": { "data": { "trusted_device": "Dispositivo de confianza" diff --git a/homeassistant/components/icloud/translations/et.json b/homeassistant/components/icloud/translations/et.json index af3457bb0db..686205f3572 100644 --- a/homeassistant/components/icloud/translations/et.json +++ b/homeassistant/components/icloud/translations/et.json @@ -18,6 +18,13 @@ "description": "Varem sisestatud salas\u00f5na kasutajale {username} ei t\u00f6\u00f6ta enam. Selle sidumise kasutamise j\u00e4tkamiseks v\u00e4rskenda oma salas\u00f5na.", "title": "iCloudi tuvastusandmed" }, + "reauth_confirm": { + "data": { + "password": "Salas\u00f5na" + }, + "description": "Varem sisestatud salas\u00f5na kasutajale {username} ei t\u00f6\u00f6ta enam. Selle sidumise kasutamise j\u00e4tkamiseks v\u00e4rskenda oma salas\u00f5na.", + "title": "Taastuvasta sidumine" + }, "trusted_device": { "data": { "trusted_device": "Usaldusv\u00e4\u00e4rne seade" diff --git a/homeassistant/components/icloud/translations/fr.json b/homeassistant/components/icloud/translations/fr.json index 8e5ec918cb0..dec1bbdb34a 100644 --- a/homeassistant/components/icloud/translations/fr.json +++ b/homeassistant/components/icloud/translations/fr.json @@ -18,6 +18,12 @@ "description": "Votre mot de passe pr\u00e9c\u00e9demment saisi pour {username} ne fonctionne plus. Mettez \u00e0 jour votre mot de passe pour continuer \u00e0 utiliser cette int\u00e9gration.", "title": "R\u00e9-authentifier l'int\u00e9gration" }, + "reauth_confirm": { + "data": { + "password": "Mot de passe" + }, + "title": "R\u00e9-authentifier l'int\u00e9gration" + }, "trusted_device": { "data": { "trusted_device": "Appareil de confiance" diff --git a/homeassistant/components/icloud/translations/hu.json b/homeassistant/components/icloud/translations/hu.json index 1cbbbfb6974..539b3740e24 100644 --- a/homeassistant/components/icloud/translations/hu.json +++ b/homeassistant/components/icloud/translations/hu.json @@ -18,6 +18,13 @@ "description": "{username} kor\u00e1bban megadott jelszava m\u00e1r nem m\u0171k\u00f6dik. Az integr\u00e1ci\u00f3 haszn\u00e1lat\u00e1hoz friss\u00edtse jelszav\u00e1t.", "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" }, + "reauth_confirm": { + "data": { + "password": "Jelsz\u00f3" + }, + "description": "{username} kor\u00e1bban megadott jelszava m\u00e1r nem m\u0171k\u00f6dik. Az integr\u00e1ci\u00f3 haszn\u00e1lat\u00e1hoz friss\u00edtse jelszav\u00e1t.", + "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" + }, "trusted_device": { "data": { "trusted_device": "Megb\u00edzhat\u00f3 eszk\u00f6z" diff --git a/homeassistant/components/icloud/translations/id.json b/homeassistant/components/icloud/translations/id.json index cd7abc1945d..1f6ed7c84c9 100644 --- a/homeassistant/components/icloud/translations/id.json +++ b/homeassistant/components/icloud/translations/id.json @@ -18,6 +18,13 @@ "description": "Kata sandi yang Anda masukkan sebelumnya untuk {username} tidak lagi berfungsi. Perbarui kata sandi Anda untuk tetap menggunakan integrasi ini.", "title": "Autentikasi Ulang Integrasi" }, + "reauth_confirm": { + "data": { + "password": "Kata Sandi" + }, + "description": "Kata sandi yang Anda masukkan sebelumnya untuk {username} tidak lagi berfungsi. Perbarui kata sandi Anda untuk tetap menggunakan integrasi ini.", + "title": "Autentikasi Ulang Integrasi" + }, "trusted_device": { "data": { "trusted_device": "Perangkat tepercaya" diff --git a/homeassistant/components/icloud/translations/it.json b/homeassistant/components/icloud/translations/it.json index 777498d6340..856ed30d767 100644 --- a/homeassistant/components/icloud/translations/it.json +++ b/homeassistant/components/icloud/translations/it.json @@ -18,6 +18,13 @@ "description": "La password inserita in precedenza per {username} non funziona pi\u00f9. Aggiorna la tua password per continuare a utilizzare questa integrazione.", "title": "Autentica nuovamente l'integrazione" }, + "reauth_confirm": { + "data": { + "password": "Password" + }, + "description": "La password precedentemente inserita per {username} non funziona pi\u00f9. Aggiorna la tua password per continuare a utilizzare questa integrazione.", + "title": "Autentica nuovamente l'integrazione" + }, "trusted_device": { "data": { "trusted_device": "Dispositivo attendibile" diff --git a/homeassistant/components/icloud/translations/ja.json b/homeassistant/components/icloud/translations/ja.json index 2295e72f0b2..4d9230ec150 100644 --- a/homeassistant/components/icloud/translations/ja.json +++ b/homeassistant/components/icloud/translations/ja.json @@ -18,6 +18,13 @@ "description": "\u4ee5\u524d\u306b\u5165\u529b\u3057\u305f {username} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u306f\u4f7f\u3048\u306a\u304f\u306a\u308a\u307e\u3057\u305f\u3002\u3053\u306e\u7d71\u5408\u3092\u5f15\u304d\u7d9a\u304d\u4f7f\u7528\u3059\u308b\u306b\u306f\u3001\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u66f4\u65b0\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, + "reauth_confirm": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + }, + "description": "\u4ee5\u524d\u306b\u5165\u529b\u3057\u305f {username} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u306f\u4f7f\u3048\u306a\u304f\u306a\u308a\u307e\u3057\u305f\u3002\u3053\u306e\u7d71\u5408\u3092\u5f15\u304d\u7d9a\u304d\u4f7f\u7528\u3059\u308b\u306b\u306f\u3001\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u66f4\u65b0\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" + }, "trusted_device": { "data": { "trusted_device": "\u4fe1\u983c\u3067\u304d\u308b\u30c7\u30d0\u30a4\u30b9" diff --git a/homeassistant/components/icloud/translations/no.json b/homeassistant/components/icloud/translations/no.json index 3e20aef032e..662600eac36 100644 --- a/homeassistant/components/icloud/translations/no.json +++ b/homeassistant/components/icloud/translations/no.json @@ -18,6 +18,13 @@ "description": "Ditt tidligere angitte passord for {username} fungerer ikke lenger. Oppdater passordet ditt for \u00e5 fortsette \u00e5 bruke denne integrasjonen.", "title": "Godkjenne integrering p\u00e5 nytt" }, + "reauth_confirm": { + "data": { + "password": "Passord" + }, + "description": "Det tidligere oppgitte passordet for {username} fungerer ikke lenger. Oppdater passordet ditt for \u00e5 fortsette \u00e5 bruke denne integrasjonen.", + "title": "Godkjenne integrering p\u00e5 nytt" + }, "trusted_device": { "data": { "trusted_device": "P\u00e5litelig enhet" diff --git a/homeassistant/components/icloud/translations/pt-BR.json b/homeassistant/components/icloud/translations/pt-BR.json index 0c3363a3799..99f779e9ead 100644 --- a/homeassistant/components/icloud/translations/pt-BR.json +++ b/homeassistant/components/icloud/translations/pt-BR.json @@ -18,6 +18,13 @@ "description": "Sua senha inserida anteriormente para {username} n\u00e3o est\u00e1 mais funcionando. Atualize sua senha para continuar usando esta integra\u00e7\u00e3o.", "title": "Reautenticar Integra\u00e7\u00e3o" }, + "reauth_confirm": { + "data": { + "password": "Senha" + }, + "description": "Sua senha inserida anteriormente para {username} n\u00e3o est\u00e1 mais funcionando. Atualize sua senha para continuar usando esta integra\u00e7\u00e3o.", + "title": "Reautenticar Integra\u00e7\u00e3o" + }, "trusted_device": { "data": { "trusted_device": "Dispositivo confi\u00e1vel" diff --git a/homeassistant/components/icloud/translations/zh-Hant.json b/homeassistant/components/icloud/translations/zh-Hant.json index fe421275e2a..91f14636dd2 100644 --- a/homeassistant/components/icloud/translations/zh-Hant.json +++ b/homeassistant/components/icloud/translations/zh-Hant.json @@ -18,6 +18,13 @@ "description": "\u5148\u524d\u91dd\u5c0d\u5e33\u865f {username} \u6240\u8f38\u5165\u7684\u5bc6\u78bc\u5df2\u5931\u6548\u3002\u8acb\u66f4\u65b0\u5bc6\u78bc\u4ee5\u4f7f\u7528\u6b64\u6574\u5408\u3002", "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" }, + "reauth_confirm": { + "data": { + "password": "\u5bc6\u78bc" + }, + "description": "\u5148\u524d\u91dd\u5c0d\u5e33\u865f {username} \u6240\u8f38\u5165\u7684\u5bc6\u78bc\u5df2\u5931\u6548\u3002\u8acb\u66f4\u65b0\u5bc6\u78bc\u4ee5\u4f7f\u7528\u6b64\u6574\u5408\u3002", + "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" + }, "trusted_device": { "data": { "trusted_device": "\u4fe1\u4efb\u88dd\u7f6e" diff --git a/homeassistant/components/intellifire/translations/es.json b/homeassistant/components/intellifire/translations/es.json index 3cf2cfc6938..dcd4d7ed300 100644 --- a/homeassistant/components/intellifire/translations/es.json +++ b/homeassistant/components/intellifire/translations/es.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", "not_intellifire_device": "No es un dispositivo IntelliFire.", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "api_error": "Error de inicio de sesi\u00f3n", diff --git a/homeassistant/components/isy994/translations/es.json b/homeassistant/components/isy994/translations/es.json index 0890cf8e251..b320167b3f1 100644 --- a/homeassistant/components/isy994/translations/es.json +++ b/homeassistant/components/isy994/translations/es.json @@ -7,7 +7,7 @@ "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "invalid_host": "La entrada del host no estaba en formato URL completo, por ejemplo, http://192.168.10.100:80", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente", "unknown": "Error inesperado" }, "flow_title": "{name} ({host})", diff --git a/homeassistant/components/lacrosse_view/translations/es.json b/homeassistant/components/lacrosse_view/translations/es.json index 9b02b2bbd4f..63cf0081be9 100644 --- a/homeassistant/components/lacrosse_view/translations/es.json +++ b/homeassistant/components/lacrosse_view/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", diff --git a/homeassistant/components/lametric/translations/ca.json b/homeassistant/components/lametric/translations/ca.json index 4c8f002ce68..14066cdeab5 100644 --- a/homeassistant/components/lametric/translations/ca.json +++ b/homeassistant/components/lametric/translations/ca.json @@ -4,6 +4,7 @@ "already_configured": "El dispositiu ja est\u00e0 configurat", "authorize_url_timeout": "Temps d'espera esgotat durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3.", "invalid_discovery_info": "S'ha rebut informaci\u00f3 de descobriment no v\u00e0lida", + "link_local_address": "L'enlla\u00e7 amb adreces locals no est\u00e0 perm\u00e8s", "missing_configuration": "La integraci\u00f3 LaMetric no est\u00e0 configurada. Consulta la documentaci\u00f3.", "no_devices": "L'usuari autoritzat no t\u00e9 cap dispositiu LaMetric", "no_url_available": "No hi ha cap URL disponible. Per a m\u00e9s informaci\u00f3 sobre aquest error, [consulta la secci\u00f3 d'ajuda]({docs_url})" @@ -14,6 +15,7 @@ }, "step": { "choice_enter_manual_or_fetch_cloud": { + "description": "Els dispositius LaMetric es poden configurar a Home Assistant de dues maneres diferents. \n\nPots introduir tota la informaci\u00f3 del dispositiu i els 'tokens' API, o b\u00e9, Home Assistant els pot importar des del teu compte de LaMetric.com.", "menu_options": { "manual_entry": "Introdueix manualment", "pick_implementation": "Importa des de LaMetric.com (recomanat)" @@ -38,5 +40,11 @@ } } } + }, + "issues": { + "manual_migration": { + "description": "La integraci\u00f3 de LaMetric s'ha modernitzat: ara es configura a trav\u00e9s de la interf\u00edcie d'usuari (IU) i les comunicacions s\u00f3n locals. \n\nMalauradament, no hi ha cap possibilitat de migraci\u00f3 autom\u00e0tica i, per tant, cal que tornis a configurar el teu dispositiu LaMetric amb Home Assistant. Consulta la documentaci\u00f3 de la integraci\u00f3 LaMetric de Home Assistant per fer-ho. \n\nElimina l'antiga configuraci\u00f3 YAML de LaMetric del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", + "title": "\u00c9s necess\u00e0ria una migraci\u00f3 manual per a LaMetric" + } } } \ No newline at end of file diff --git a/homeassistant/components/led_ble/translations/el.json b/homeassistant/components/led_ble/translations/el.json new file mode 100644 index 00000000000..ad4b994de80 --- /dev/null +++ b/homeassistant/components/led_ble/translations/el.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "already_in_progress": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", + "no_devices_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf", + "no_unconfigured_devices": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03bc\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03bc\u03ad\u03bd\u03b5\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2.", + "not_supported": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9" + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 Bluetooth" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/led_ble/translations/hu.json b/homeassistant/components/led_ble/translations/hu.json new file mode 100644 index 00000000000..6b6e576abcc --- /dev/null +++ b/homeassistant/components/led_ble/translations/hu.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", + "no_unconfigured_devices": "Nem tal\u00e1lhat\u00f3 konfigur\u00e1latlan eszk\u00f6z.", + "not_supported": "Eszk\u00f6z nem t\u00e1mogatott" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "Bluetooth-c\u00edm" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/led_ble/translations/id.json b/homeassistant/components/led_ble/translations/id.json new file mode 100644 index 00000000000..80afbbcf132 --- /dev/null +++ b/homeassistant/components/led_ble/translations/id.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan", + "no_unconfigured_devices": "Tidak ditemukan perangkat yang tidak dikonfigurasi.", + "not_supported": "Perangkat tidak didukung" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "Alamat Bluetooth" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/led_ble/translations/ja.json b/homeassistant/components/led_ble/translations/ja.json new file mode 100644 index 00000000000..4fee9ec6904 --- /dev/null +++ b/homeassistant/components/led_ble/translations/ja.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", + "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", + "no_unconfigured_devices": "\u672a\u69cb\u6210\u306e\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3002", + "not_supported": "\u30c7\u30d0\u30a4\u30b9\u306f\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "Bluetooth\u30a2\u30c9\u30ec\u30b9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/translations/es.json b/homeassistant/components/life360/translations/es.json index 72e880463fe..a9b53fffd86 100644 --- a/homeassistant/components/life360/translations/es.json +++ b/homeassistant/components/life360/translations/es.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente", "unknown": "Error inesperado" }, "create_entry": { diff --git a/homeassistant/components/litterrobot/translations/es.json b/homeassistant/components/litterrobot/translations/es.json index 003a715f8a7..64e3408d47f 100644 --- a/homeassistant/components/litterrobot/translations/es.json +++ b/homeassistant/components/litterrobot/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/litterrobot/translations/hu.json b/homeassistant/components/litterrobot/translations/hu.json index cc0c820facf..e960eef8867 100644 --- a/homeassistant/components/litterrobot/translations/hu.json +++ b/homeassistant/components/litterrobot/translations/hu.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van" + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", @@ -9,6 +10,13 @@ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { + "reauth_confirm": { + "data": { + "password": "Jelsz\u00f3" + }, + "description": "K\u00e9rem, friss\u00edtse {username} jelszav\u00e1t", + "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" + }, "user": { "data": { "password": "Jelsz\u00f3", diff --git a/homeassistant/components/litterrobot/translations/id.json b/homeassistant/components/litterrobot/translations/id.json index 4a84db42a14..73e19f1d439 100644 --- a/homeassistant/components/litterrobot/translations/id.json +++ b/homeassistant/components/litterrobot/translations/id.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Perangkat sudah dikonfigurasi" + "already_configured": "Perangkat sudah dikonfigurasi", + "reauth_successful": "Autentikasi ulang berhasil" }, "error": { "cannot_connect": "Gagal terhubung", @@ -9,6 +10,13 @@ "unknown": "Kesalahan yang tidak diharapkan" }, "step": { + "reauth_confirm": { + "data": { + "password": "Kata Sandi" + }, + "description": "Perbarui kata sandi Anda untuk {username}", + "title": "Autentikasi Ulang Integrasi" + }, "user": { "data": { "password": "Kata Sandi", diff --git a/homeassistant/components/lyric/translations/es.json b/homeassistant/components/lyric/translations/es.json index e4bd36ad952..2506018a178 100644 --- a/homeassistant/components/lyric/translations/es.json +++ b/homeassistant/components/lyric/translations/es.json @@ -3,7 +3,7 @@ "abort": { "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.", "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "create_entry": { "default": "Autenticado correctamente" @@ -13,7 +13,7 @@ "title": "Selecciona el m\u00e9todo de autenticaci\u00f3n" }, "reauth_confirm": { - "description": "La integraci\u00f3n de Lyric necesita volver a autenticar tu cuenta.", + "description": "La integraci\u00f3n Lyric necesita volver a autenticar tu cuenta.", "title": "Volver a autenticar la integraci\u00f3n" } } diff --git a/homeassistant/components/mazda/translations/es.json b/homeassistant/components/mazda/translations/es.json index 53dec475cad..3702a1b8302 100644 --- a/homeassistant/components/mazda/translations/es.json +++ b/homeassistant/components/mazda/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "account_locked": "Cuenta bloqueada. Por favor, vuelve a intentarlo m\u00e1s tarde.", diff --git a/homeassistant/components/meater/translations/es.json b/homeassistant/components/meater/translations/es.json index 1fc556b8ee2..c2524a3e47c 100644 --- a/homeassistant/components/meater/translations/es.json +++ b/homeassistant/components/meater/translations/es.json @@ -10,7 +10,7 @@ "data": { "password": "Contrase\u00f1a" }, - "description": "Confirma la contrase\u00f1a de la cuenta de Meater Cloud {username}." + "description": "Confirma la contrase\u00f1a de la cuenta de {username} en Meater Cloud." }, "user": { "data": { diff --git a/homeassistant/components/melnor/translations/ca.json b/homeassistant/components/melnor/translations/ca.json new file mode 100644 index 00000000000..3aa06dfddc6 --- /dev/null +++ b/homeassistant/components/melnor/translations/ca.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "No hi ha cap dispositiu Melnor Bluetooth a prop." + }, + "step": { + "bluetooth_confirm": { + "description": "Vols afegir la v\u00e0lvula Melnor Bluetooth `{name}` a Home Assistant?", + "title": "S'ha descobert una v\u00e0lvula Melnor Bluetooth" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/melnor/translations/de.json b/homeassistant/components/melnor/translations/de.json new file mode 100644 index 00000000000..5792062dd87 --- /dev/null +++ b/homeassistant/components/melnor/translations/de.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "In der N\u00e4he gibt es keine Melnor Bluetooth-Ger\u00e4te." + }, + "step": { + "bluetooth_confirm": { + "description": "M\u00f6chtest du das Melnor Bluetooth-Ventil `{name}` zu Home Assistant hinzuf\u00fcgen?", + "title": "Melnor Bluetooth-Ventil entdeckt" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/melnor/translations/el.json b/homeassistant/components/melnor/translations/el.json new file mode 100644 index 00000000000..7380a05cb4d --- /dev/null +++ b/homeassistant/components/melnor/translations/el.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u0394\u03b5\u03bd \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 Melnor Bluetooth \u03ba\u03bf\u03bd\u03c4\u03ac \u03c3\u03b1\u03c2." + }, + "step": { + "bluetooth_confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03b2\u03b1\u03bb\u03b2\u03af\u03b4\u03b1 Bluetooth Melnor `{name}` \u03c3\u03c4\u03bf Home Assistant;", + "title": "\u0391\u03bd\u03b1\u03ba\u03b1\u03bb\u03cd\u03c6\u03b8\u03b7\u03ba\u03b5 \u03b7 \u03b2\u03b1\u03bb\u03b2\u03af\u03b4\u03b1 Bluetooth Melnor" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/melnor/translations/es.json b/homeassistant/components/melnor/translations/es.json new file mode 100644 index 00000000000..32ed43930db --- /dev/null +++ b/homeassistant/components/melnor/translations/es.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "No hay ning\u00fan dispositivo Bluetooth Melnor cerca." + }, + "step": { + "bluetooth_confirm": { + "description": "\u00bfQuieres a\u00f1adir la v\u00e1lvula Bluetooth Melnor `{name}` a Home Assistant?", + "title": "Descubierta v\u00e1lvula Bluetooth Melnor" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/melnor/translations/et.json b/homeassistant/components/melnor/translations/et.json new file mode 100644 index 00000000000..12a75835d26 --- /dev/null +++ b/homeassistant/components/melnor/translations/et.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "L\u00e4heduses pole \u00fchtegi Melnor Bluetooth-seadet." + }, + "step": { + "bluetooth_confirm": { + "description": "Kas lisada Melnor Bluetooth-klapp '{name}' Home Assistantisse?", + "title": "Leiti Melnor Bluetooth klapp" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/melnor/translations/fr.json b/homeassistant/components/melnor/translations/fr.json new file mode 100644 index 00000000000..9c5bdc781a4 --- /dev/null +++ b/homeassistant/components/melnor/translations/fr.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Il n'y a aucun appareil Bluetooth Melnor \u00e0 proximit\u00e9." + }, + "step": { + "bluetooth_confirm": { + "description": "Voulez-vous ajouter la valve Bluetooth Melnor `{name}` \u00e0 Home Assistant\u00a0?", + "title": "Valve Bluetooth Melnor d\u00e9couverte" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/melnor/translations/hu.json b/homeassistant/components/melnor/translations/hu.json new file mode 100644 index 00000000000..9f96c1bbaae --- /dev/null +++ b/homeassistant/components/melnor/translations/hu.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nincsenek Melnor Bluetooth-eszk\u00f6z\u00f6k a k\u00f6zelben." + }, + "step": { + "bluetooth_confirm": { + "description": "Szeretn\u00e9 hozz\u00e1adni a Melnor Bluetooth eszk\u00f6zt \"{name}\" Home Assistant-hoz?", + "title": "Felfedezett Melnor Bluetooth szelep" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/melnor/translations/id.json b/homeassistant/components/melnor/translations/id.json new file mode 100644 index 00000000000..551a50ca9e5 --- /dev/null +++ b/homeassistant/components/melnor/translations/id.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Tidak ada perangkat Bluetooth Melnor di sekitar." + }, + "step": { + "bluetooth_confirm": { + "description": "Ingin menambahkan katup Bluetooth Melnor `{nama}` ke Home Assistant?", + "title": "Katup Bluetooth Melnor yang ditemukan" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/melnor/translations/it.json b/homeassistant/components/melnor/translations/it.json new file mode 100644 index 00000000000..9124fdfce13 --- /dev/null +++ b/homeassistant/components/melnor/translations/it.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Non ci sono dispositivi Melnor Bluetooth nelle vicinanze." + }, + "step": { + "bluetooth_confirm": { + "description": "Vuoi aggiungere la valvola Bluetooth Melnor `{name}` a Home Assistant?", + "title": "Scoperta la valvola Bluetooth Melnor" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/melnor/translations/no.json b/homeassistant/components/melnor/translations/no.json new file mode 100644 index 00000000000..98c873ad9fe --- /dev/null +++ b/homeassistant/components/melnor/translations/no.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Det er ingen Melnor Bluetooth-enheter i n\u00e6rheten." + }, + "step": { + "bluetooth_confirm": { + "description": "Vil du legge til Melnor Bluetooth-ventilen ` {name} ` til Home Assistant?", + "title": "Oppdaget Melnor Bluetooth-ventil" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/melnor/translations/zh-Hant.json b/homeassistant/components/melnor/translations/zh-Hant.json new file mode 100644 index 00000000000..71bc8f33716 --- /dev/null +++ b/homeassistant/components/melnor/translations/zh-Hant.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u9644\u8fd1\u6c92\u6709\u4efb\u4f55 Melnor \u85cd\u7259\u88dd\u7f6e\u3002" + }, + "step": { + "bluetooth_confirm": { + "description": "\u662f\u5426\u8981\u5c07 Melnor Bluetooth valve `{name}`\u65b0\u589e\u81f3 Home Assistant\uff1f", + "title": "\u5df2\u767c\u73fe\u7684 Melnor Bluetooth valve" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motioneye/translations/es.json b/homeassistant/components/motioneye/translations/es.json index d7dc7f1a083..5db1a3f8869 100644 --- a/homeassistant/components/motioneye/translations/es.json +++ b/homeassistant/components/motioneye/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El servicio ya est\u00e1 configurado", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/mqtt/translations/ca.json b/homeassistant/components/mqtt/translations/ca.json index 569adc2e423..43ab994f4d8 100644 --- a/homeassistant/components/mqtt/translations/ca.json +++ b/homeassistant/components/mqtt/translations/ca.json @@ -49,6 +49,12 @@ "button_triple_press": "\"{subtype}\" clicat tres vegades" } }, + "issues": { + "deprecated_yaml": { + "description": "S'ha trobat el MQTT {platform}(s) sota el codi de la integraci\u00f3 `{platform}`.\n\nSi us plau, moveu la configuraci\u00f3 a la integraci\u00f3 `mqtt`i reinicieu el Home Assistant per solucionar aquesta incid\u00e8ncia. Vegeu la [documentaci\u00f3]({more_info_url}) per a m\u00e9s informaci\u00f3.", + "title": "El MQTT {platform}(s) configurat manualment necessita la vostra atenci\u00f3" + } + }, "options": { "error": { "bad_birth": "Topic del missatge de naixement inv\u00e0lid.", diff --git a/homeassistant/components/myq/translations/es.json b/homeassistant/components/myq/translations/es.json index 7cb7dcb9354..ccc709ce191 100644 --- a/homeassistant/components/myq/translations/es.json +++ b/homeassistant/components/myq/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El servicio ya est\u00e1 configurado", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/nam/translations/es.json b/homeassistant/components/nam/translations/es.json index c0ac0958d24..7b9558537a6 100644 --- a/homeassistant/components/nam/translations/es.json +++ b/homeassistant/components/nam/translations/es.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", "device_unsupported": "El dispositivo no es compatible.", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente", "reauth_unsuccessful": "No se pudo volver a autenticar, por favor, elimina la integraci\u00f3n y vuelve a configurarla." }, "error": { @@ -28,7 +28,7 @@ "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "description": "Por favor, introduce el nombre de usuario y la contrase\u00f1a correctos para el host: {host}" + "description": "Por favor, introduce el nombre de usuario y contrase\u00f1a correctos para el host: {host}" }, "user": { "data": { diff --git a/homeassistant/components/nam/translations/sensor.ca.json b/homeassistant/components/nam/translations/sensor.ca.json new file mode 100644 index 00000000000..ec1a642cbd2 --- /dev/null +++ b/homeassistant/components/nam/translations/sensor.ca.json @@ -0,0 +1,11 @@ +{ + "state": { + "nam__caqi_level": { + "high": "Alt", + "low": "Baix", + "medium": "Mitj\u00e0", + "very high": "Molt alt", + "very low": "Molt baix" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nam/translations/sensor.hu.json b/homeassistant/components/nam/translations/sensor.hu.json new file mode 100644 index 00000000000..ee30c2e4c44 --- /dev/null +++ b/homeassistant/components/nam/translations/sensor.hu.json @@ -0,0 +1,11 @@ +{ + "state": { + "nam__caqi_level": { + "high": "Magas", + "low": "Alacsony", + "medium": "K\u00f6zepes", + "very high": "Nagyon magas", + "very low": "Nagyon alacsony" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nam/translations/sensor.id.json b/homeassistant/components/nam/translations/sensor.id.json new file mode 100644 index 00000000000..6b208a54362 --- /dev/null +++ b/homeassistant/components/nam/translations/sensor.id.json @@ -0,0 +1,11 @@ +{ + "state": { + "nam__caqi_level": { + "high": "Tinggi", + "low": "Rendah", + "medium": "Sedang", + "very high": "Sangat tinggi", + "very low": "Sangat rendah" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nam/translations/sensor.ja.json b/homeassistant/components/nam/translations/sensor.ja.json new file mode 100644 index 00000000000..86356081b8a --- /dev/null +++ b/homeassistant/components/nam/translations/sensor.ja.json @@ -0,0 +1,11 @@ +{ + "state": { + "nam__caqi_level": { + "high": "\u9ad8", + "low": "\u4f4e", + "medium": "\u4e2d", + "very high": "\u975e\u5e38\u306b\u9ad8\u3044", + "very low": "\u3068\u3066\u3082\u4f4e\u3044" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nanoleaf/translations/es.json b/homeassistant/components/nanoleaf/translations/es.json index 9de6598da5c..f8a43b4fece 100644 --- a/homeassistant/components/nanoleaf/translations/es.json +++ b/homeassistant/components/nanoleaf/translations/es.json @@ -4,7 +4,7 @@ "already_configured": "El dispositivo ya est\u00e1 configurado", "cannot_connect": "No se pudo conectar", "invalid_token": "Token de acceso no v\u00e1lido", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente", "unknown": "Error inesperado" }, "error": { diff --git a/homeassistant/components/neato/translations/es.json b/homeassistant/components/neato/translations/es.json index 4dcab62da43..b9e818eda5e 100644 --- a/homeassistant/components/neato/translations/es.json +++ b/homeassistant/components/neato/translations/es.json @@ -5,7 +5,7 @@ "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.", "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [revisa la secci\u00f3n de ayuda]({docs_url})", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "create_entry": { "default": "Autenticado correctamente" diff --git a/homeassistant/components/nest/translations/es.json b/homeassistant/components/nest/translations/es.json index 7c7c8444479..3cd92fd644d 100644 --- a/homeassistant/components/nest/translations/es.json +++ b/homeassistant/components/nest/translations/es.json @@ -9,7 +9,7 @@ "invalid_access_token": "Token de acceso no v\u00e1lido", "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [revisa la secci\u00f3n de ayuda]({docs_url})", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente", "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n.", "unknown_authorize_url_generation": "Error desconocido al generar una URL de autorizaci\u00f3n." }, diff --git a/homeassistant/components/netatmo/translations/es.json b/homeassistant/components/netatmo/translations/es.json index 68f4160beb9..975995663da 100644 --- a/homeassistant/components/netatmo/translations/es.json +++ b/homeassistant/components/netatmo/translations/es.json @@ -4,7 +4,7 @@ "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.", "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [revisa la secci\u00f3n de ayuda]({docs_url})", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente", "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, "create_entry": { diff --git a/homeassistant/components/notion/translations/es.json b/homeassistant/components/notion/translations/es.json index f7a7a01f4d8..9c649324dc6 100644 --- a/homeassistant/components/notion/translations/es.json +++ b/homeassistant/components/notion/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", diff --git a/homeassistant/components/nuki/translations/es.json b/homeassistant/components/nuki/translations/es.json index d21ef9dfdb6..53ef4b360df 100644 --- a/homeassistant/components/nuki/translations/es.json +++ b/homeassistant/components/nuki/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/openexchangerates/translations/es.json b/homeassistant/components/openexchangerates/translations/es.json index fb5897846db..c5cf1b266e2 100644 --- a/homeassistant/components/openexchangerates/translations/es.json +++ b/homeassistant/components/openexchangerates/translations/es.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "El servicio ya est\u00e1 configurado", "cannot_connect": "No se pudo conectar", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente", "timeout_connect": "Tiempo de espera agotado para establecer la conexi\u00f3n" }, "error": { diff --git a/homeassistant/components/overkiz/translations/de.json b/homeassistant/components/overkiz/translations/de.json index 09cce8ea63f..1e4cd0cb254 100644 --- a/homeassistant/components/overkiz/translations/de.json +++ b/homeassistant/components/overkiz/translations/de.json @@ -11,7 +11,8 @@ "server_in_maintenance": "Server ist wegen Wartungsarbeiten au\u00dfer Betrieb", "too_many_attempts": "Zu viele Versuche mit einem ung\u00fcltigen Token, vor\u00fcbergehend gesperrt", "too_many_requests": "Zu viele Anfragen, versuche es sp\u00e4ter erneut.", - "unknown": "Unerwarteter Fehler" + "unknown": "Unerwarteter Fehler", + "unknown_user": "Unbekannter Benutzer. Somfy Protect-Konten werden von dieser Integration nicht unterst\u00fctzt." }, "flow_title": "Gateway: {gateway_id}", "step": { diff --git a/homeassistant/components/overkiz/translations/el.json b/homeassistant/components/overkiz/translations/el.json index 924ecc3219d..e9862479c27 100644 --- a/homeassistant/components/overkiz/translations/el.json +++ b/homeassistant/components/overkiz/translations/el.json @@ -11,7 +11,8 @@ "server_in_maintenance": "\u039f \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03ba\u03c4\u03cc\u03c2 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \u03b3\u03b9\u03b1 \u03c3\u03c5\u03bd\u03c4\u03ae\u03c1\u03b7\u03c3\u03b7", "too_many_attempts": "\u03a0\u03ac\u03c1\u03b1 \u03c0\u03bf\u03bb\u03bb\u03ad\u03c2 \u03c0\u03c1\u03bf\u03c3\u03c0\u03ac\u03b8\u03b5\u03b9\u03b5\u03c2 \u03bc\u03b5 \u03bc\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc, \u03c0\u03c1\u03bf\u03c3\u03c9\u03c1\u03b9\u03bd\u03ac \u03b1\u03c0\u03bf\u03ba\u03bb\u03b5\u03b9\u03c3\u03bc\u03ad\u03bd\u03b5\u03c2", "too_many_requests": "\u03a0\u03ac\u03c1\u03b1 \u03c0\u03bf\u03bb\u03bb\u03ac \u03b1\u03b9\u03c4\u03ae\u03bc\u03b1\u03c4\u03b1, \u03c0\u03c1\u03bf\u03c3\u03c0\u03b1\u03b8\u03ae\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac \u03b1\u03c1\u03b3\u03cc\u03c4\u03b5\u03c1\u03b1", - "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1", + "unknown_user": "\u0386\u03b3\u03bd\u03c9\u03c3\u03c4\u03bf\u03c2 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7\u03c2. \u039f\u03b9 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03af Somfy Protect \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03bf\u03bd\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7." }, "flow_title": "\u03a0\u03cd\u03bb\u03b7: {gateway_id}", "step": { diff --git a/homeassistant/components/overkiz/translations/es.json b/homeassistant/components/overkiz/translations/es.json index a2b04f05cf9..a5a3ad16e0d 100644 --- a/homeassistant/components/overkiz/translations/es.json +++ b/homeassistant/components/overkiz/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente", "reauth_wrong_account": "Solo puedes volver a autenticar esta entrada con la misma cuenta y concentrador de Overkiz" }, "error": { @@ -11,7 +11,8 @@ "server_in_maintenance": "El servidor est\u00e1 ca\u00eddo por mantenimiento", "too_many_attempts": "Demasiados intentos con un token no v\u00e1lido, prohibido temporalmente", "too_many_requests": "Demasiadas solicitudes, vuelve a intentarlo m\u00e1s tarde", - "unknown": "Error inesperado" + "unknown": "Error inesperado", + "unknown_user": "Usuario desconocido. Las cuentas de Somfy Protect no son compatibles con esta integraci\u00f3n." }, "flow_title": "Puerta de enlace: {gateway_id}", "step": { diff --git a/homeassistant/components/overkiz/translations/et.json b/homeassistant/components/overkiz/translations/et.json index 3cbfcb6af80..34639ea1739 100644 --- a/homeassistant/components/overkiz/translations/et.json +++ b/homeassistant/components/overkiz/translations/et.json @@ -11,7 +11,8 @@ "server_in_maintenance": "Server on hoolduse t\u00f5ttu maas", "too_many_attempts": "Liiga palju katseid kehtetu v\u00f5tmega, ajutiselt keelatud", "too_many_requests": "Liiga palju p\u00e4ringuid, proovi hiljem uuesti", - "unknown": "Ootamatu t\u00f5rge" + "unknown": "Ootamatu t\u00f5rge", + "unknown_user": "Tundmatu kasutaja. See sidumine ei toeta Somfy Protecti kontosid." }, "flow_title": "L\u00fc\u00fcs: {gateway_id}", "step": { diff --git a/homeassistant/components/overkiz/translations/hu.json b/homeassistant/components/overkiz/translations/hu.json index b6810749bd3..4fa4c0a9ddc 100644 --- a/homeassistant/components/overkiz/translations/hu.json +++ b/homeassistant/components/overkiz/translations/hu.json @@ -11,7 +11,8 @@ "server_in_maintenance": "A szerver karbantart\u00e1s miatt nem el\u00e9rhet\u0151", "too_many_attempts": "T\u00fal sok pr\u00f3b\u00e1lkoz\u00e1s \u00e9rv\u00e9nytelen tokennel, ideiglenesen kitiltva", "too_many_requests": "T\u00fal sok a k\u00e9r\u00e9s, pr\u00f3b\u00e1lja meg k\u00e9s\u0151bb \u00fajra.", - "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt", + "unknown_user": "Ismeretlen felhaszn\u00e1l\u00f3. Ez az integr\u00e1ci\u00f3 nem t\u00e1mogatja a Somfy Protect fi\u00f3kokat." }, "flow_title": "\u00c1tj\u00e1r\u00f3: {gateway_id}", "step": { diff --git a/homeassistant/components/overkiz/translations/id.json b/homeassistant/components/overkiz/translations/id.json index c58b66b10a7..709cef9819c 100644 --- a/homeassistant/components/overkiz/translations/id.json +++ b/homeassistant/components/overkiz/translations/id.json @@ -11,7 +11,8 @@ "server_in_maintenance": "Server sedang dalam masa pemeliharaan", "too_many_attempts": "Terlalu banyak percobaan dengan token yang tidak valid, untuk sementara diblokir", "too_many_requests": "Terlalu banyak permintaan, coba lagi nanti.", - "unknown": "Kesalahan yang tidak diharapkan" + "unknown": "Kesalahan yang tidak diharapkan", + "unknown_user": "Pengguna tidak dikenal. Akun Somfy Protect tidak didukung oleh integrasi ini." }, "flow_title": "Gateway: {gateway_id}", "step": { diff --git a/homeassistant/components/overkiz/translations/it.json b/homeassistant/components/overkiz/translations/it.json index 3dcc4e94e10..21602a4cb89 100644 --- a/homeassistant/components/overkiz/translations/it.json +++ b/homeassistant/components/overkiz/translations/it.json @@ -11,7 +11,8 @@ "server_in_maintenance": "Il server \u00e8 inattivo per manutenzione", "too_many_attempts": "Troppi tentativi con un token non valido, temporaneamente bandito", "too_many_requests": "Troppe richieste, riprova pi\u00f9 tardi.", - "unknown": "Errore imprevisto" + "unknown": "Errore imprevisto", + "unknown_user": "Utente sconosciuto. Gli account Somfy Protect non sono supportati da questa integrazione." }, "flow_title": "Gateway: {gateway_id}", "step": { diff --git a/homeassistant/components/overkiz/translations/no.json b/homeassistant/components/overkiz/translations/no.json index e0d52e0fc0e..2da02db164d 100644 --- a/homeassistant/components/overkiz/translations/no.json +++ b/homeassistant/components/overkiz/translations/no.json @@ -11,7 +11,8 @@ "server_in_maintenance": "serveren er nede for vedlikehold", "too_many_attempts": "For mange fors\u00f8k med et ugyldig token, midlertidig utestengt", "too_many_requests": "For mange foresp\u00f8rsler. Pr\u00f8v igjen senere", - "unknown": "Uventet feil" + "unknown": "Uventet feil", + "unknown_user": "Ukjent bruker. Somfy Protect-kontoer st\u00f8ttes ikke av denne integrasjonen." }, "flow_title": "Gateway: {gateway_id}", "step": { diff --git a/homeassistant/components/overkiz/translations/pt-BR.json b/homeassistant/components/overkiz/translations/pt-BR.json index 123b2a83d42..3e2ff048560 100644 --- a/homeassistant/components/overkiz/translations/pt-BR.json +++ b/homeassistant/components/overkiz/translations/pt-BR.json @@ -11,7 +11,8 @@ "server_in_maintenance": "O servidor est\u00e1 fora de servi\u00e7o para manuten\u00e7\u00e3o", "too_many_attempts": "Muitas tentativas com um token inv\u00e1lido, banido temporariamente", "too_many_requests": "Muitas solicita\u00e7\u00f5es, tente novamente mais tarde", - "unknown": "Erro inesperado" + "unknown": "Erro inesperado", + "unknown_user": "Usu\u00e1rio desconhecido. As contas Somfy Protect n\u00e3o s\u00e3o suportadas por esta integra\u00e7\u00e3o." }, "flow_title": "Gateway: {gateway_id}", "step": { diff --git a/homeassistant/components/overkiz/translations/zh-Hant.json b/homeassistant/components/overkiz/translations/zh-Hant.json index ab265301761..c9e20812ecc 100644 --- a/homeassistant/components/overkiz/translations/zh-Hant.json +++ b/homeassistant/components/overkiz/translations/zh-Hant.json @@ -11,7 +11,8 @@ "server_in_maintenance": "\u4f3a\u670d\u5668\u7dad\u8b77\u4e2d", "too_many_attempts": "\u4f7f\u7528\u7121\u6548\u6b0a\u6756\u5617\u8a66\u6b21\u6578\u904e\u591a\uff0c\u66ab\u6642\u906d\u5230\u5c01\u9396", "too_many_requests": "\u8acb\u6c42\u6b21\u6578\u904e\u591a\uff0c\u8acb\u7a0d\u5f8c\u91cd\u8a66\u3002", - "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + "unknown": "\u672a\u9810\u671f\u932f\u8aa4", + "unknown_user": "\u672a\u77e5\u4f7f\u7528\u8005\u3001\u6b64\u6574\u5408\u4e0d\u652f\u63f4 Somfy Protect \u5e33\u865f\u3002" }, "flow_title": "\u9598\u9053\u5668\uff1a{gateway_id}", "step": { diff --git a/homeassistant/components/ovo_energy/translations/es.json b/homeassistant/components/ovo_energy/translations/es.json index a28e441d9d8..d3ffba9b2e5 100644 --- a/homeassistant/components/ovo_energy/translations/es.json +++ b/homeassistant/components/ovo_energy/translations/es.json @@ -12,7 +12,7 @@ "password": "Contrase\u00f1a" }, "description": "La autenticaci\u00f3n fall\u00f3 para OVO Energy. Por favor, introduce tus credenciales actuales.", - "title": "Reautenticaci\u00f3n" + "title": "Volver a autenticar" }, "user": { "data": { diff --git a/homeassistant/components/picnic/translations/es.json b/homeassistant/components/picnic/translations/es.json index 93024c5611c..54899c8e8fd 100644 --- a/homeassistant/components/picnic/translations/es.json +++ b/homeassistant/components/picnic/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/plex/translations/es.json b/homeassistant/components/plex/translations/es.json index 14fe1b840a5..8074612c8b5 100644 --- a/homeassistant/components/plex/translations/es.json +++ b/homeassistant/components/plex/translations/es.json @@ -4,7 +4,7 @@ "all_configured": "Todos los servidores vinculados ya configurados", "already_configured": "Este servidor Plex ya est\u00e1 configurado", "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente", "token_request_timeout": "Tiempo de espera agotado para la obtenci\u00f3n del token", "unknown": "Error inesperado" }, diff --git a/homeassistant/components/powerwall/translations/es.json b/homeassistant/components/powerwall/translations/es.json index 0e34589a260..8d0cf5f8970 100644 --- a/homeassistant/components/powerwall/translations/es.json +++ b/homeassistant/components/powerwall/translations/es.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", "cannot_connect": "No se pudo conectar", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/prosegur/translations/es.json b/homeassistant/components/prosegur/translations/es.json index 4447bbbc2e4..80e3b80d97e 100644 --- a/homeassistant/components/prosegur/translations/es.json +++ b/homeassistant/components/prosegur/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/prusalink/translations/cs.json b/homeassistant/components/prusalink/translations/cs.json new file mode 100644 index 00000000000..323cf668090 --- /dev/null +++ b/homeassistant/components/prusalink/translations/cs.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "api_key": "Kl\u00ed\u010d API", + "host": "Hostitel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/el.json b/homeassistant/components/prusalink/translations/el.json new file mode 100644 index 00000000000..8116be1ad89 --- /dev/null +++ b/homeassistant/components/prusalink/translations/el.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", + "not_supported": "\u03a5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03bc\u03cc\u03bd\u03bf \u03c4\u03bf PrusaLink API v2", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "user": { + "data": { + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/fr.json b/homeassistant/components/prusalink/translations/fr.json new file mode 100644 index 00000000000..372360dbde8 --- /dev/null +++ b/homeassistant/components/prusalink/translations/fr.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification non valide", + "not_supported": "Seule l'API PrusaLink v2 est prise en charge", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "api_key": "Cl\u00e9 d'API", + "host": "H\u00f4te" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/hu.json b/homeassistant/components/prusalink/translations/hu.json new file mode 100644 index 00000000000..7fe5692714a --- /dev/null +++ b/homeassistant/components/prusalink/translations/hu.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "not_supported": "Csak a PrusaLink API v2 t\u00e1mogatott", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "user": { + "data": { + "api_key": "API kulcs", + "host": "C\u00edm" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/id.json b/homeassistant/components/prusalink/translations/id.json new file mode 100644 index 00000000000..3582565c801 --- /dev/null +++ b/homeassistant/components/prusalink/translations/id.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "not_supported": "Hanya API PrusaLink v2 yang didukung", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "api_key": "Kunci API", + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/it.json b/homeassistant/components/prusalink/translations/it.json new file mode 100644 index 00000000000..02b2b8a53aa --- /dev/null +++ b/homeassistant/components/prusalink/translations/it.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "not_supported": "\u00c8 supportata solo l'API PrusaLink v2", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "api_key": "Chiave API", + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/ja.json b/homeassistant/components/prusalink/translations/ja.json new file mode 100644 index 00000000000..e507bbbda7e --- /dev/null +++ b/homeassistant/components/prusalink/translations/ja.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "step": { + "user": { + "data": { + "api_key": "API\u30ad\u30fc", + "host": "\u30db\u30b9\u30c8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/sensor.cs.json b/homeassistant/components/prusalink/translations/sensor.cs.json new file mode 100644 index 00000000000..53663d41858 --- /dev/null +++ b/homeassistant/components/prusalink/translations/sensor.cs.json @@ -0,0 +1,9 @@ +{ + "state": { + "prusalink__printer_state": { + "cancelling": "Prob\u00edh\u00e1 zru\u0161en\u00ed", + "idle": "Ne\u010dinn\u00fd", + "paused": "Pozastaveno" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/sensor.el.json b/homeassistant/components/prusalink/translations/sensor.el.json new file mode 100644 index 00000000000..f391a4d238e --- /dev/null +++ b/homeassistant/components/prusalink/translations/sensor.el.json @@ -0,0 +1,11 @@ +{ + "state": { + "prusalink__printer_state": { + "cancelling": "\u0391\u03ba\u03cd\u03c1\u03c9\u03c3\u03b7", + "idle": "\u0391\u03b4\u03c1\u03b1\u03bd\u03ae\u03c2", + "paused": "\u03a3\u03b5 \u03c0\u03b1\u03cd\u03c3\u03b7", + "pausing": "\u03a0\u03b1\u03cd\u03c3\u03b7", + "printing": "\u0395\u03ba\u03c4\u03cd\u03c0\u03c9\u03c3\u03b7" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/sensor.fr.json b/homeassistant/components/prusalink/translations/sensor.fr.json new file mode 100644 index 00000000000..e1134b4f55c --- /dev/null +++ b/homeassistant/components/prusalink/translations/sensor.fr.json @@ -0,0 +1,11 @@ +{ + "state": { + "prusalink__printer_state": { + "cancelling": "Annulation", + "idle": "Inactif", + "paused": "En pause", + "pausing": "Mise en pause", + "printing": "Impression" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/sensor.hu.json b/homeassistant/components/prusalink/translations/sensor.hu.json new file mode 100644 index 00000000000..ab836fe34f8 --- /dev/null +++ b/homeassistant/components/prusalink/translations/sensor.hu.json @@ -0,0 +1,11 @@ +{ + "state": { + "prusalink__printer_state": { + "cancelling": "\u00c9rv\u00e9nytelen\u00edt\u00e9sben", + "idle": "T\u00e9tlen", + "paused": "Sz\u00fcneteltetve", + "pausing": "Sz\u00fcneteltet\u00e9sben", + "printing": "Nyomtat\u00e1sban" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/sensor.id.json b/homeassistant/components/prusalink/translations/sensor.id.json new file mode 100644 index 00000000000..d09930825fe --- /dev/null +++ b/homeassistant/components/prusalink/translations/sensor.id.json @@ -0,0 +1,11 @@ +{ + "state": { + "prusalink__printer_state": { + "cancelling": "Membatalkan", + "idle": "Siaga", + "paused": "Dijeda", + "pausing": "Jeda", + "printing": "Mencetak" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/sensor.it.json b/homeassistant/components/prusalink/translations/sensor.it.json new file mode 100644 index 00000000000..7336cd6c2bf --- /dev/null +++ b/homeassistant/components/prusalink/translations/sensor.it.json @@ -0,0 +1,11 @@ +{ + "state": { + "prusalink__printer_state": { + "cancelling": "In annullamento", + "idle": "Inattiva", + "paused": "Fermata", + "pausing": "In pausa", + "printing": "In stampa" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/sensor.ja.json b/homeassistant/components/prusalink/translations/sensor.ja.json new file mode 100644 index 00000000000..43a54856edb --- /dev/null +++ b/homeassistant/components/prusalink/translations/sensor.ja.json @@ -0,0 +1,11 @@ +{ + "state": { + "prusalink__printer_state": { + "cancelling": "\u30ad\u30e3\u30f3\u30bb\u30eb\u4e2d", + "idle": "\u30a2\u30a4\u30c9\u30eb", + "paused": "\u4e00\u6642\u505c\u6b62", + "pausing": "\u4e00\u6642\u505c\u6b62\u4e2d", + "printing": "\u5370\u5237" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pushover/translations/es.json b/homeassistant/components/pushover/translations/es.json index c36644e575e..e28f7045aa0 100644 --- a/homeassistant/components/pushover/translations/es.json +++ b/homeassistant/components/pushover/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El servicio ya est\u00e1 configurado", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/pvoutput/translations/es.json b/homeassistant/components/pvoutput/translations/es.json index 188a7e8d293..67a63360727 100644 --- a/homeassistant/components/pvoutput/translations/es.json +++ b/homeassistant/components/pvoutput/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/renault/translations/es.json b/homeassistant/components/renault/translations/es.json index 75a0ca557c5..fc110deb15b 100644 --- a/homeassistant/components/renault/translations/es.json +++ b/homeassistant/components/renault/translations/es.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", "kamereon_no_account": "No se puede encontrar la cuenta de Kamereon", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "invalid_credentials": "Autenticaci\u00f3n no v\u00e1lida" diff --git a/homeassistant/components/ridwell/translations/es.json b/homeassistant/components/ridwell/translations/es.json index 07037e942cf..61ba7c53361 100644 --- a/homeassistant/components/ridwell/translations/es.json +++ b/homeassistant/components/ridwell/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", diff --git a/homeassistant/components/samsungtv/translations/es.json b/homeassistant/components/samsungtv/translations/es.json index b102c85b2f3..e6ff4576e54 100644 --- a/homeassistant/components/samsungtv/translations/es.json +++ b/homeassistant/components/samsungtv/translations/es.json @@ -7,7 +7,7 @@ "cannot_connect": "No se pudo conectar", "id_missing": "Este dispositivo Samsung no tiene un n\u00famero de serie.", "not_supported": "Este dispositivo Samsung no es compatible por el momento.", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente", "unknown": "Error inesperado" }, "error": { diff --git a/homeassistant/components/sense/translations/es.json b/homeassistant/components/sense/translations/es.json index 6790c0841e3..87a6e256028 100644 --- a/homeassistant/components/sense/translations/es.json +++ b/homeassistant/components/sense/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/sensibo/translations/es.json b/homeassistant/components/sensibo/translations/es.json index beb6ed575a2..37c2f022039 100644 --- a/homeassistant/components/sensibo/translations/es.json +++ b/homeassistant/components/sensibo/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/sensor/translations/cs.json b/homeassistant/components/sensor/translations/cs.json index f6f2e97fdd4..23ba416dd34 100644 --- a/homeassistant/components/sensor/translations/cs.json +++ b/homeassistant/components/sensor/translations/cs.json @@ -7,6 +7,7 @@ "is_gas": "Aktu\u00e1ln\u00ed mno\u017estv\u00ed plynu {entity_name}", "is_humidity": "Aktu\u00e1ln\u00ed vlhkost {entity_name}", "is_illuminance": "Aktu\u00e1ln\u00ed osv\u011btlen\u00ed {entity_name}", + "is_moisture": "Aktu\u00e1ln\u00ed vlhkost {entity_name}", "is_power": "Aktu\u00e1ln\u00ed v\u00fdkon {entity_name}", "is_power_factor": "Aktu\u00e1ln\u00ed \u00fa\u010din\u00edk {entity_name}", "is_pressure": "Aktu\u00e1ln\u00ed tlak {entity_name}", @@ -23,6 +24,7 @@ "gas": "P\u0159i zm\u011bn\u011b mno\u017estv\u00ed plynu {entity_name}", "humidity": "P\u0159i zm\u011bn\u011b vlhkosti {entity_name}", "illuminance": "P\u0159i zm\u011bn\u011b osv\u011btlen\u00ed {entity_name}", + "moisture": "P\u0159i zm\u011bn\u011b vlhkosti {entity_name}", "nitrogen_monoxide": "Zm\u011bna koncentrace oxidu dusnat\u00e9ho {entity_name}", "power": "P\u0159i zm\u011bn\u011b el. v\u00fdkonu {entity_name}", "power_factor": "P\u0159i zm\u011bn\u011b \u00fa\u010din\u00edku {entity_name}", diff --git a/homeassistant/components/sensor/translations/de.json b/homeassistant/components/sensor/translations/de.json index cb21759b2dd..b0cdbd198aa 100644 --- a/homeassistant/components/sensor/translations/de.json +++ b/homeassistant/components/sensor/translations/de.json @@ -11,6 +11,7 @@ "is_gas": "Aktuelles {entity_name} Gas", "is_humidity": "{entity_name} Feuchtigkeit", "is_illuminance": "Aktuelle {entity_name} Helligkeit", + "is_moisture": "Aktuelle Feuchtigkeit von {entity_name}", "is_nitrogen_dioxide": "Aktuelle Stickstoffdioxid-Konzentration von {entity_name}", "is_nitrogen_monoxide": "Aktuelle Stickstoffmonoxidkonzentration von {entity_name}", "is_nitrous_oxide": "Aktuelle Lachgaskonzentration von {entity_name}", @@ -40,6 +41,7 @@ "gas": "{entity_name} Gas\u00e4nderungen", "humidity": "{entity_name} Feuchtigkeits\u00e4nderungen", "illuminance": "{entity_name} Helligkeits\u00e4nderungen", + "moisture": "{entity_name} Feuchtigkeits\u00e4nderungen", "nitrogen_dioxide": "\u00c4nderung der Stickstoffdioxidkonzentration bei {entity_name}", "nitrogen_monoxide": "\u00c4nderung der Stickstoffmonoxid-Konzentration bei {entity_name}", "nitrous_oxide": "\u00c4nderung der Lachgaskonzentration bei {entity_name}", diff --git a/homeassistant/components/sensor/translations/el.json b/homeassistant/components/sensor/translations/el.json index dc15aec106a..543ef1c24ad 100644 --- a/homeassistant/components/sensor/translations/el.json +++ b/homeassistant/components/sensor/translations/el.json @@ -11,6 +11,7 @@ "is_gas": "\u03a4\u03c1\u03ad\u03c7\u03bf\u03bd \u03b1\u03ad\u03c1\u03b9\u03bf {entity_name}", "is_humidity": "\u03a4\u03c1\u03ad\u03c7\u03bf\u03c5\u03c3\u03b1 \u03c5\u03b3\u03c1\u03b1\u03c3\u03af\u03b1 {entity_name}", "is_illuminance": "\u03a4\u03c1\u03ad\u03c7\u03bf\u03c5\u03c3\u03b1 \u03c6\u03c9\u03c4\u03b5\u03b9\u03bd\u03cc\u03c4\u03b7\u03c4\u03b1 {entity_name}", + "is_moisture": "\u03a4\u03c1\u03ad\u03c7\u03bf\u03c5\u03c3\u03b1 \u03c5\u03b3\u03c1\u03b1\u03c3\u03af\u03b1 {entity_name}", "is_nitrogen_dioxide": "\u03a4\u03c1\u03ad\u03c7\u03bf\u03bd \u03b5\u03c0\u03af\u03c0\u03b5\u03b4\u03bf \u03c3\u03c5\u03b3\u03ba\u03ad\u03bd\u03c4\u03c1\u03c9\u03c3\u03b7\u03c2 \u03b4\u03b9\u03bf\u03be\u03b5\u03b9\u03b4\u03af\u03bf\u03c5 \u03c4\u03bf\u03c5 \u03b1\u03b6\u03ce\u03c4\u03bf\u03c5 {entity_name}", "is_nitrogen_monoxide": "\u03a4\u03c1\u03ad\u03c7\u03bf\u03bd \u03b5\u03c0\u03af\u03c0\u03b5\u03b4\u03bf \u03c3\u03c5\u03b3\u03ba\u03ad\u03bd\u03c4\u03c1\u03c9\u03c3\u03b7\u03c2 \u03bc\u03bf\u03bd\u03bf\u03be\u03b5\u03b9\u03b4\u03af\u03bf\u03c5 \u03c4\u03bf\u03c5 \u03b1\u03b6\u03ce\u03c4\u03bf\u03c5 {entity_name}", "is_nitrous_oxide": "\u03a4\u03c1\u03ad\u03c7\u03bf\u03bd \u03b5\u03c0\u03af\u03c0\u03b5\u03b4\u03bf \u03c3\u03c5\u03b3\u03ba\u03ad\u03bd\u03c4\u03c1\u03c9\u03c3\u03b7\u03c2 \u03bf\u03be\u03b5\u03b9\u03b4\u03af\u03bf\u03c5 \u03c4\u03bf\u03c5 \u03b1\u03b6\u03ce\u03c4\u03bf\u03c5 {entity_name}", @@ -40,6 +41,7 @@ "gas": "{entity_name} \u03bc\u03b5\u03c4\u03b1\u03b2\u03bf\u03bb\u03ad\u03c2 \u03b1\u03b5\u03c1\u03af\u03bf\u03c5", "humidity": "\u0397 \u03c5\u03b3\u03c1\u03b1\u03c3\u03af\u03b1 \u03c4\u03bf\u03c5 {entity_name} \u03b1\u03bb\u03bb\u03ac\u03b6\u03b5\u03b9", "illuminance": "\u039f \u03c6\u03c9\u03c4\u03b9\u03c3\u03bc\u03cc\u03c2 \u03c4\u03bf\u03c5 {entity_name} \u03b1\u03bb\u03bb\u03ac\u03b6\u03b5\u03b9", + "moisture": "{entity_name} \u03b1\u03bb\u03bb\u03ac\u03b6\u03b5\u03b9 \u03c5\u03b3\u03c1\u03b1\u03c3\u03af\u03b1", "nitrogen_dioxide": "{entity_name} \u03bc\u03b5\u03c4\u03b1\u03b2\u03bf\u03bb\u03ad\u03c2 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03b3\u03ba\u03ad\u03bd\u03c4\u03c1\u03c9\u03c3\u03b7\u03c2 \u03b4\u03b9\u03bf\u03be\u03b5\u03b9\u03b4\u03af\u03bf\u03c5 \u03c4\u03bf\u03c5 \u03b1\u03b6\u03ce\u03c4\u03bf\u03c5", "nitrogen_monoxide": "{entity_name} \u03bc\u03b5\u03c4\u03b1\u03b2\u03bf\u03bb\u03ad\u03c2 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03b3\u03ba\u03ad\u03bd\u03c4\u03c1\u03c9\u03c3\u03b7\u03c2 \u03bc\u03bf\u03bd\u03bf\u03be\u03b5\u03b9\u03b4\u03af\u03bf\u03c5 \u03c4\u03bf\u03c5 \u03b1\u03b6\u03ce\u03c4\u03bf\u03c5", "nitrous_oxide": "{entity_name} \u03bc\u03b5\u03c4\u03b1\u03b2\u03bf\u03bb\u03ad\u03c2 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03b3\u03ba\u03ad\u03bd\u03c4\u03c1\u03c9\u03c3\u03b7\u03c2 \u03bf\u03be\u03b5\u03b9\u03b4\u03af\u03bf\u03c5 \u03c4\u03bf\u03c5 \u03b1\u03b6\u03ce\u03c4\u03bf\u03c5", diff --git a/homeassistant/components/sensor/translations/en.json b/homeassistant/components/sensor/translations/en.json index 23b20ac9974..5c38db03687 100644 --- a/homeassistant/components/sensor/translations/en.json +++ b/homeassistant/components/sensor/translations/en.json @@ -11,6 +11,7 @@ "is_gas": "Current {entity_name} gas", "is_humidity": "Current {entity_name} humidity", "is_illuminance": "Current {entity_name} illuminance", + "is_moisture": "Current {entity_name} moisture", "is_nitrogen_dioxide": "Current {entity_name} nitrogen dioxide concentration level", "is_nitrogen_monoxide": "Current {entity_name} nitrogen monoxide concentration level", "is_nitrous_oxide": "Current {entity_name} nitrous oxide concentration level", @@ -40,6 +41,7 @@ "gas": "{entity_name} gas changes", "humidity": "{entity_name} humidity changes", "illuminance": "{entity_name} illuminance changes", + "moisture": "{entity_name} moisture changes", "nitrogen_dioxide": "{entity_name} nitrogen dioxide concentration changes", "nitrogen_monoxide": "{entity_name} nitrogen monoxide concentration changes", "nitrous_oxide": "{entity_name} nitrous oxide concentration changes", diff --git a/homeassistant/components/sensor/translations/es.json b/homeassistant/components/sensor/translations/es.json index c61b0b7927a..d005742fee4 100644 --- a/homeassistant/components/sensor/translations/es.json +++ b/homeassistant/components/sensor/translations/es.json @@ -11,6 +11,7 @@ "is_gas": "El gas actual de {entity_name}", "is_humidity": "La humedad actual de {entity_name}", "is_illuminance": "La luminosidad actual de {entity_name}", + "is_moisture": "La humedad actual de {entity_name}", "is_nitrogen_dioxide": "El nivel de la concentraci\u00f3n de di\u00f3xido de nitr\u00f3geno actual de {entity_name}", "is_nitrogen_monoxide": "El nivel de la concentraci\u00f3n de mon\u00f3xido de nitr\u00f3geno actual de {entity_name}", "is_nitrous_oxide": "El nivel de la concentraci\u00f3n de \u00f3xido nitroso actual de {entity_name}", @@ -40,6 +41,7 @@ "gas": "El gas de {entity_name} cambia", "humidity": "La humedad de {entity_name} cambia", "illuminance": "La luminosidad de {entity_name} cambia", + "moisture": "La humedad de {entity_name} cambia", "nitrogen_dioxide": "La concentraci\u00f3n de di\u00f3xido de nitr\u00f3geno de {entity_name} cambia", "nitrogen_monoxide": "La concentraci\u00f3n de mon\u00f3xido de nitr\u00f3geno de {entity_name} cambia", "nitrous_oxide": "La concentraci\u00f3n de \u00f3xido nitroso de {entity_name} cambia", diff --git a/homeassistant/components/sensor/translations/id.json b/homeassistant/components/sensor/translations/id.json index 27c4a76a325..81f1126591d 100644 --- a/homeassistant/components/sensor/translations/id.json +++ b/homeassistant/components/sensor/translations/id.json @@ -11,6 +11,7 @@ "is_gas": "Gas {entity_name} saat ini", "is_humidity": "Kelembaban {entity_name} saat ini", "is_illuminance": "Pencahayaan {entity_name} saat ini", + "is_moisture": "Pengembunan {entity_name} saat ini", "is_nitrogen_dioxide": "Tingkat konsentrasi nitrogen dioksida {entity_name} saat ini", "is_nitrogen_monoxide": "Tingkat konsentrasi nitrogen monoksida {entity_name} saat ini", "is_nitrous_oxide": "Tingkat konsentrasi nitrit oksida {entity_name} saat ini", @@ -40,6 +41,7 @@ "gas": "Perubahan gas {entity_name}", "humidity": "Perubahan kelembaban {entity_name}", "illuminance": "Perubahan pencahayaan {entity_name}", + "moisture": "Perubahan pengembunan {entity_name}", "nitrogen_dioxide": "Perubahan konsentrasi nitrogen dioksida {entity_name}", "nitrogen_monoxide": "Perubahan konsentrasi nitrogen monoksida {entity_name}", "nitrous_oxide": "Perubahan konsentrasi nitro oksida {entity_name}", diff --git a/homeassistant/components/sensor/translations/it.json b/homeassistant/components/sensor/translations/it.json index 321e3e3108b..caaddb8c858 100644 --- a/homeassistant/components/sensor/translations/it.json +++ b/homeassistant/components/sensor/translations/it.json @@ -11,6 +11,7 @@ "is_gas": "Attuale gas di {entity_name}", "is_humidity": "Umidit\u00e0 attuale di {entity_name}", "is_illuminance": "Illuminazione attuale di {entity_name}", + "is_moisture": "Umidit\u00e0 attuale di {entity_name}", "is_nitrogen_dioxide": "Attuale livello di concentrazione di biossido di azoto di {entity_name}", "is_nitrogen_monoxide": "Attuale livello di concentrazione di monossido di azoto di {entity_name}", "is_nitrous_oxide": "Attuale livello di concentrazione di ossidi di azoto di {entity_name}", @@ -40,6 +41,7 @@ "gas": "Variazioni di gas di {entity_name}", "humidity": "Variazioni di umidit\u00e0 di {entity_name} ", "illuminance": "Variazioni dell'illuminazione di {entity_name}", + "moisture": "{entity_name} cambiamenti di umidit\u00e0", "nitrogen_dioxide": "Variazioni della concentrazione di biossido di azoto di {entity_name}", "nitrogen_monoxide": "Variazioni della concentrazione di monossido di azoto di {entity_name}", "nitrous_oxide": "Variazioni della concentrazione di ossidi di azoto di {entity_name}", diff --git a/homeassistant/components/sensor/translations/pt-BR.json b/homeassistant/components/sensor/translations/pt-BR.json index 72e1c7ba023..436a43056f1 100644 --- a/homeassistant/components/sensor/translations/pt-BR.json +++ b/homeassistant/components/sensor/translations/pt-BR.json @@ -11,6 +11,7 @@ "is_gas": "G\u00e1s atual de {entity_name}", "is_humidity": "Humidade atual do(a) {entity_name}", "is_illuminance": "Luminosidade atual {entity_name}", + "is_moisture": "Umidade atual {entity_name}", "is_nitrogen_dioxide": "N\u00edvel atual de concentra\u00e7\u00e3o de di\u00f3xido de nitrog\u00eanio de {entity_name}", "is_nitrogen_monoxide": "N\u00edvel atual de concentra\u00e7\u00e3o de mon\u00f3xido de nitrog\u00eanio de {entity_name}", "is_nitrous_oxide": "N\u00edvel atual de concentra\u00e7\u00e3o de \u00f3xido nitroso de {entity_name}", @@ -40,6 +41,7 @@ "gas": "Mudan\u00e7as de g\u00e1s de {entity_name}", "humidity": "{entity_name} mudan\u00e7as de umidade", "illuminance": "{entity_name} mudan\u00e7as de luminosidade", + "moisture": "Mudan\u00e7as de umidade {entity_name}", "nitrogen_dioxide": "Mudan\u00e7as na concentra\u00e7\u00e3o de di\u00f3xido de nitrog\u00eanio de {entity_name}", "nitrogen_monoxide": "Mudan\u00e7as na concentra\u00e7\u00e3o de mon\u00f3xido de nitrog\u00eanio de {entity_name}", "nitrous_oxide": "Altera\u00e7\u00f5es na concentra\u00e7\u00e3o de \u00f3xido nitroso de {entity_name}", diff --git a/homeassistant/components/sensorpro/translations/ca.json b/homeassistant/components/sensorpro/translations/ca.json new file mode 100644 index 00000000000..c121ff7408c --- /dev/null +++ b/homeassistant/components/sensorpro/translations/ca.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", + "no_devices_found": "No s'han trobat dispositius a la xarxa", + "not_supported": "Dispositiu no compatible" + }, + "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/sensorpro/translations/de.json b/homeassistant/components/sensorpro/translations/de.json new file mode 100644 index 00000000000..4c5720ec6fb --- /dev/null +++ b/homeassistant/components/sensorpro/translations/de.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden", + "not_supported": "Ger\u00e4t nicht unterst\u00fctzt" + }, + "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/sensorpro/translations/el.json b/homeassistant/components/sensorpro/translations/el.json new file mode 100644 index 00000000000..cdb57c8ac1b --- /dev/null +++ b/homeassistant/components/sensorpro/translations/el.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "already_in_progress": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", + "no_devices_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf", + "not_supported": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name};" + }, + "user": { + "data": { + "address": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b3\u03b9\u03b1 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensorpro/translations/es.json b/homeassistant/components/sensorpro/translations/es.json new file mode 100644 index 00000000000..ae0ab01acdf --- /dev/null +++ b/homeassistant/components/sensorpro/translations/es.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", + "no_devices_found": "No se encontraron dispositivos en la red", + "not_supported": "Dispositivo no compatible" + }, + "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/sensorpro/translations/et.json b/homeassistant/components/sensorpro/translations/et.json new file mode 100644 index 00000000000..170815ec87e --- /dev/null +++ b/homeassistant/components/sensorpro/translations/et.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "already_in_progress": "Seadistamine on juba k\u00e4imas", + "no_devices_found": "V\u00f5rgust seadmeid ei leitud", + "not_supported": "Seadet ei toetata" + }, + "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/sensorpro/translations/fr.json b/homeassistant/components/sensorpro/translations/fr.json new file mode 100644 index 00000000000..8ddb4af4dbc --- /dev/null +++ b/homeassistant/components/sensorpro/translations/fr.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", + "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau", + "not_supported": "Appareil non pris en charge" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Voulez-vous configurer {name}\u00a0?" + }, + "user": { + "data": { + "address": "Appareil" + }, + "description": "S\u00e9lectionnez l'appareil \u00e0 configurer" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensorpro/translations/hu.json b/homeassistant/components/sensorpro/translations/hu.json new file mode 100644 index 00000000000..97fbb5b9408 --- /dev/null +++ b/homeassistant/components/sensorpro/translations/hu.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", + "not_supported": "Eszk\u00f6z nem t\u00e1mogatott" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name}?" + }, + "user": { + "data": { + "address": "Eszk\u00f6z" + }, + "description": "V\u00e1lassza ki a be\u00e1ll\u00edtani k\u00edv\u00e1nt eszk\u00f6zt" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensorpro/translations/id.json b/homeassistant/components/sensorpro/translations/id.json new file mode 100644 index 00000000000..573eb39ed15 --- /dev/null +++ b/homeassistant/components/sensorpro/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/sensorpro/translations/it.json b/homeassistant/components/sensorpro/translations/it.json new file mode 100644 index 00000000000..7784ed3a240 --- /dev/null +++ b/homeassistant/components/sensorpro/translations/it.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "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": "{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/sensorpro/translations/ja.json b/homeassistant/components/sensorpro/translations/ja.json new file mode 100644 index 00000000000..fe1c5746cda --- /dev/null +++ b/homeassistant/components/sensorpro/translations/ja.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", + "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", + "not_supported": "\u30c7\u30d0\u30a4\u30b9\u306f\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "{name} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" + }, + "user": { + "data": { + "address": "\u30c7\u30d0\u30a4\u30b9" + }, + "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3059\u308b\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensorpro/translations/no.json b/homeassistant/components/sensorpro/translations/no.json new file mode 100644 index 00000000000..0bf8b1695ec --- /dev/null +++ b/homeassistant/components/sensorpro/translations/no.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", + "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket", + "not_supported": "Enheten st\u00f8ttes ikke" + }, + "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/sensorpro/translations/pt-BR.json b/homeassistant/components/sensorpro/translations/pt-BR.json new file mode 100644 index 00000000000..5b654163201 --- /dev/null +++ b/homeassistant/components/sensorpro/translations/pt-BR.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "no_devices_found": "Nenhum dispositivo encontrado na rede", + "not_supported": "Dispositivo n\u00e3o suportado" + }, + "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/sensorpro/translations/zh-Hant.json b/homeassistant/components/sensorpro/translations/zh-Hant.json new file mode 100644 index 00000000000..64ae1f19094 --- /dev/null +++ b/homeassistant/components/sensorpro/translations/zh-Hant.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", + "not_supported": "\u88dd\u7f6e\u4e0d\u652f\u63f4" + }, + "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/sharkiq/translations/es.json b/homeassistant/components/sharkiq/translations/es.json index 976840e0a9f..f58156d208e 100644 --- a/homeassistant/components/sharkiq/translations/es.json +++ b/homeassistant/components/sharkiq/translations/es.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", "cannot_connect": "No se pudo conectar", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente", "unknown": "Error inesperado" }, "error": { diff --git a/homeassistant/components/simplisafe/translations/es.json b/homeassistant/components/simplisafe/translations/es.json index 408b595d329..89bdaab3eaf 100644 --- a/homeassistant/components/simplisafe/translations/es.json +++ b/homeassistant/components/simplisafe/translations/es.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Esta cuenta SimpliSafe ya est\u00e1 en uso.", "email_2fa_timed_out": "Se agot\u00f3 el tiempo de espera para la autenticaci\u00f3n de dos factores basada en correo electr\u00f3nico.", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente", "wrong_account": "Las credenciales de usuario proporcionadas no coinciden con esta cuenta de SimpliSafe." }, "error": { diff --git a/homeassistant/components/skybell/translations/es.json b/homeassistant/components/skybell/translations/es.json index a86c086c788..09ff7eb8af1 100644 --- a/homeassistant/components/skybell/translations/es.json +++ b/homeassistant/components/skybell/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/sleepiq/translations/es.json b/homeassistant/components/sleepiq/translations/es.json index cdea85e7360..54a0b8e6d66 100644 --- a/homeassistant/components/sleepiq/translations/es.json +++ b/homeassistant/components/sleepiq/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/smarttub/translations/es.json b/homeassistant/components/smarttub/translations/es.json index fa2b9d91e9b..89ff2276c70 100644 --- a/homeassistant/components/smarttub/translations/es.json +++ b/homeassistant/components/smarttub/translations/es.json @@ -2,14 +2,14 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" }, "step": { "reauth_confirm": { - "description": "La integraci\u00f3n de SmartTub necesita volver a autenticar tu cuenta", + "description": "La integraci\u00f3n SmartTub necesita volver a autenticar tu cuenta", "title": "Volver a autenticar la integraci\u00f3n" }, "user": { diff --git a/homeassistant/components/sonarr/translations/es.json b/homeassistant/components/sonarr/translations/es.json index eee1e025e36..19668a5a469 100644 --- a/homeassistant/components/sonarr/translations/es.json +++ b/homeassistant/components/sonarr/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El servicio ya est\u00e1 configurado", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente", "unknown": "Error inesperado" }, "error": { diff --git a/homeassistant/components/speedtestdotnet/translations/ca.json b/homeassistant/components/speedtestdotnet/translations/ca.json index 1f4d059555c..fa29c792992 100644 --- a/homeassistant/components/speedtestdotnet/translations/ca.json +++ b/homeassistant/components/speedtestdotnet/translations/ca.json @@ -14,6 +14,7 @@ "fix_flow": { "step": { "confirm": { + "description": "Actualitza totes les automatitzacions o 'scripts' que utilitzin aquest servei perqu\u00e8 passin a utilitzar el servei `homeassistant.update_entity` amb un 'entity_id' objectiu o 'target' de Speedtest. Despr\u00e9s, fes clic a ENVIA per marcar aquest problema com a resolt.", "title": "El servei speedtest est\u00e0 sent eliminat" } } diff --git a/homeassistant/components/speedtestdotnet/translations/hu.json b/homeassistant/components/speedtestdotnet/translations/hu.json index c223e8b9376..f99db19fe4a 100644 --- a/homeassistant/components/speedtestdotnet/translations/hu.json +++ b/homeassistant/components/speedtestdotnet/translations/hu.json @@ -9,6 +9,19 @@ } } }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Friss\u00edtsen minden olyan automatiz\u00e1l\u00e1st vagy szkriptet, amely ezt a szolg\u00e1ltat\u00e1st haszn\u00e1lja, hogy helyette a `homeassistant.update_entity` szolg\u00e1ltat\u00e1st haszn\u00e1lja a Speedtest entity_id azonos\u00edt\u00f3val. Ezut\u00e1n kattintson az al\u00e1bbi MEHET gombra a probl\u00e9ma megoldottk\u00e9nt val\u00f3 megjel\u00f6l\u00e9s\u00e9hez.", + "title": "A speedtest szolg\u00e1ltat\u00e1s elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + } + } + }, + "title": "A speedtest szolg\u00e1ltat\u00e1s elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/steam_online/translations/es.json b/homeassistant/components/steam_online/translations/es.json index dfb1cdbd50e..2a4a0bf157f 100644 --- a/homeassistant/components/steam_online/translations/es.json +++ b/homeassistant/components/steam_online/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El servicio ya est\u00e1 configurado", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/synology_dsm/translations/es.json b/homeassistant/components/synology_dsm/translations/es.json index 817001cdc20..e6c45511109 100644 --- a/homeassistant/components/synology_dsm/translations/es.json +++ b/homeassistant/components/synology_dsm/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente", "reconfigure_successful": "La reconfiguraci\u00f3n se realiz\u00f3 correctamente" }, "error": { diff --git a/homeassistant/components/system_bridge/translations/es.json b/homeassistant/components/system_bridge/translations/es.json index b6df8d6f57c..94036b7b856 100644 --- a/homeassistant/components/system_bridge/translations/es.json +++ b/homeassistant/components/system_bridge/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente", "unknown": "Error inesperado" }, "error": { diff --git a/homeassistant/components/tailscale/translations/es.json b/homeassistant/components/tailscale/translations/es.json index 62f803025e8..c1598f7e1c5 100644 --- a/homeassistant/components/tailscale/translations/es.json +++ b/homeassistant/components/tailscale/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/tankerkoenig/translations/es.json b/homeassistant/components/tankerkoenig/translations/es.json index 27ee462e17b..c83d6d68c74 100644 --- a/homeassistant/components/tankerkoenig/translations/es.json +++ b/homeassistant/components/tankerkoenig/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "La ubicaci\u00f3n ya est\u00e1 configurada", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", diff --git a/homeassistant/components/tautulli/translations/es.json b/homeassistant/components/tautulli/translations/es.json index 2d9a09d3432..e1c7c71b2f4 100644 --- a/homeassistant/components/tautulli/translations/es.json +++ b/homeassistant/components/tautulli/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente", "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, "error": { @@ -15,7 +15,7 @@ "api_key": "Clave API" }, "description": "Para encontrar tu clave API, abre la p\u00e1gina web de Tautulli y navega a Configuraci\u00f3n y luego a Interfaz web. La clave API estar\u00e1 en la parte inferior de esa p\u00e1gina.", - "title": "Re-autenticaci\u00f3n de Tautulli" + "title": "Volver a autenticar Tautulli" }, "user": { "data": { diff --git a/homeassistant/components/thermobeacon/translations/hu.json b/homeassistant/components/thermobeacon/translations/hu.json new file mode 100644 index 00000000000..97fbb5b9408 --- /dev/null +++ b/homeassistant/components/thermobeacon/translations/hu.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", + "not_supported": "Eszk\u00f6z nem t\u00e1mogatott" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name}?" + }, + "user": { + "data": { + "address": "Eszk\u00f6z" + }, + "description": "V\u00e1lassza ki a be\u00e1ll\u00edtani k\u00edv\u00e1nt eszk\u00f6zt" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tile/translations/es.json b/homeassistant/components/tile/translations/es.json index 0b0979991bb..5457d7e7cdb 100644 --- a/homeassistant/components/tile/translations/es.json +++ b/homeassistant/components/tile/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" diff --git a/homeassistant/components/totalconnect/translations/es.json b/homeassistant/components/totalconnect/translations/es.json index 079e0be5af0..3dda0b88eaa 100644 --- a/homeassistant/components/totalconnect/translations/es.json +++ b/homeassistant/components/totalconnect/translations/es.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", "no_locations": "No hay ubicaciones disponibles para este usuario, comprueba la configuraci\u00f3n de TotalConnect", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", diff --git a/homeassistant/components/tractive/translations/es.json b/homeassistant/components/tractive/translations/es.json index 41f5827ac1a..1e61714ff9c 100644 --- a/homeassistant/components/tractive/translations/es.json +++ b/homeassistant/components/tractive/translations/es.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", "reauth_failed_existing": "No se pudo actualizar la entrada de configuraci\u00f3n, por favor, elimina la integraci\u00f3n y vuelve a configurarla.", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", diff --git a/homeassistant/components/trafikverket_ferry/translations/es.json b/homeassistant/components/trafikverket_ferry/translations/es.json index 93996b1923c..31463cc7799 100644 --- a/homeassistant/components/trafikverket_ferry/translations/es.json +++ b/homeassistant/components/trafikverket_ferry/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/trafikverket_train/translations/es.json b/homeassistant/components/trafikverket_train/translations/es.json index 9f546bd1676..8a5fd0baddb 100644 --- a/homeassistant/components/trafikverket_train/translations/es.json +++ b/homeassistant/components/trafikverket_train/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/transmission/translations/es.json b/homeassistant/components/transmission/translations/es.json index e9d084d9758..30180811cb4 100644 --- a/homeassistant/components/transmission/translations/es.json +++ b/homeassistant/components/transmission/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/unifi/translations/es.json b/homeassistant/components/unifi/translations/es.json index 1c03e7b3617..5cf1ec9ea22 100644 --- a/homeassistant/components/unifi/translations/es.json +++ b/homeassistant/components/unifi/translations/es.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "El sitio UniFi Network ya est\u00e1 configurado", "configuration_updated": "Configuraci\u00f3n actualizada", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "faulty_credentials": "Autenticaci\u00f3n no v\u00e1lida", diff --git a/homeassistant/components/unifiprotect/translations/hu.json b/homeassistant/components/unifiprotect/translations/hu.json index d9162f74a91..dac543f97c2 100644 --- a/homeassistant/components/unifiprotect/translations/hu.json +++ b/homeassistant/components/unifiprotect/translations/hu.json @@ -42,11 +42,15 @@ } }, "options": { + "error": { + "invalid_mac_list": "Egy list\u00e1ra van sz\u00fcks\u00e9g, melyben a MAC-c\u00edmek vessz\u0151vel vannak elv\u00e1lasztva" + }, "step": { "init": { "data": { "all_updates": "Val\u00f3s idej\u0171 m\u00e9r\u0151sz\u00e1mok (FIGYELEM: nagym\u00e9rt\u00e9kben n\u00f6veli a CPU terhel\u00e9st)", "disable_rtsp": "Az RTSP adatfolyam letilt\u00e1sa", + "ignored_devices": "A figyelmen k\u00edv\u00fcl hagyand\u00f3 eszk\u00f6z\u00f6k MAC-c\u00edm\u00e9nek vessz\u0151vel elv\u00e1lasztott list\u00e1ja", "max_media": "A m\u00e9diab\u00f6ng\u00e9sz\u0151be bet\u00f6ltend\u0151 esem\u00e9nyek maxim\u00e1lis sz\u00e1ma (n\u00f6veli a RAM-haszn\u00e1latot)", "override_connection_host": "Kapcsolat c\u00edm\u00e9nek fel\u00fclb\u00edr\u00e1l\u00e1sa" }, diff --git a/homeassistant/components/unifiprotect/translations/id.json b/homeassistant/components/unifiprotect/translations/id.json index 38f5a87cb05..772c339f3df 100644 --- a/homeassistant/components/unifiprotect/translations/id.json +++ b/homeassistant/components/unifiprotect/translations/id.json @@ -42,11 +42,15 @@ } }, "options": { + "error": { + "invalid_mac_list": "Harus berupa daftar alamat MAC yang dipisahkan dengan koma" + }, "step": { "init": { "data": { "all_updates": "Metrik waktu nyata (PERINGATAN: Meningkatkan penggunaan CPU)", "disable_rtsp": "Nonaktifkan aliran RTSP", + "ignored_devices": "Daftar alamat MAC perangkat yang dipisahkan koma untuk diabaikan", "max_media": "Jumlah maksimum peristiwa yang akan dimuat untuk Browser Media (meningkatkan penggunaan RAM)", "override_connection_host": "Timpa Host Koneksi" }, diff --git a/homeassistant/components/uptimerobot/translations/es.json b/homeassistant/components/uptimerobot/translations/es.json index 67e99d60ef4..6bc4fe5adf6 100644 --- a/homeassistant/components/uptimerobot/translations/es.json +++ b/homeassistant/components/uptimerobot/translations/es.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", "reauth_failed_existing": "No se pudo actualizar la entrada de configuraci\u00f3n, por favor, elimina la integraci\u00f3n y vuelve a configurarla.", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente", "unknown": "Error inesperado" }, "error": { diff --git a/homeassistant/components/verisure/translations/es.json b/homeassistant/components/verisure/translations/es.json index ed0d1328e79..0bd56a81c2e 100644 --- a/homeassistant/components/verisure/translations/es.json +++ b/homeassistant/components/verisure/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", @@ -24,7 +24,7 @@ }, "reauth_confirm": { "data": { - "description": "Vuelva a autenticarse con tu cuenta Verisure My Pages.", + "description": "Vuelve a autenticarte con tu cuenta Verisure My Pages.", "email": "Correo electr\u00f3nico", "password": "Contrase\u00f1a" } diff --git a/homeassistant/components/vlc_telnet/translations/es.json b/homeassistant/components/vlc_telnet/translations/es.json index 4cdc7cb2154..2c7d5ec1ea3 100644 --- a/homeassistant/components/vlc_telnet/translations/es.json +++ b/homeassistant/components/vlc_telnet/translations/es.json @@ -4,7 +4,7 @@ "already_configured": "El servicio ya est\u00e1 configurado", "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente", "unknown": "Error inesperado" }, "error": { diff --git a/homeassistant/components/volvooncall/translations/de.json b/homeassistant/components/volvooncall/translations/de.json index 9f7687ebc80..561a19e635d 100644 --- a/homeassistant/components/volvooncall/translations/de.json +++ b/homeassistant/components/volvooncall/translations/de.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Konto ist bereits konfiguriert" + "already_configured": "Konto wurde bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { "invalid_auth": "Ung\u00fcltige Authentifizierung", diff --git a/homeassistant/components/volvooncall/translations/el.json b/homeassistant/components/volvooncall/translations/el.json index 1911ff66fe3..7739ac02417 100644 --- a/homeassistant/components/volvooncall/translations/el.json +++ b/homeassistant/components/volvooncall/translations/el.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u039f \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" + "already_configured": "\u039f \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2" }, "error": { "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", diff --git a/homeassistant/components/volvooncall/translations/en.json b/homeassistant/components/volvooncall/translations/en.json index dbbd1ebf3c6..2b311052741 100644 --- a/homeassistant/components/volvooncall/translations/en.json +++ b/homeassistant/components/volvooncall/translations/en.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "reauth_successful": "You have successfully re-authenticated to Volvo On Call", - "cant_reauth": "Could not find original configuration that needs reauthentication" + "already_configured": "Account is already configured", + "reauth_successful": "Re-authentication was successful" }, "error": { "invalid_auth": "Invalid authentication", @@ -17,10 +17,6 @@ "scandinavian_miles": "Use Scandinavian Miles", "username": "Username" } - }, - "reauth_confirm": { - "title": "Re-authentication Required", - "description": "The Volvo On Call integration needs to re-authenticate your account" } } }, diff --git a/homeassistant/components/volvooncall/translations/es.json b/homeassistant/components/volvooncall/translations/es.json index 39174aa1253..57a21a3d905 100644 --- a/homeassistant/components/volvooncall/translations/es.json +++ b/homeassistant/components/volvooncall/translations/es.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "La cuenta ya est\u00e1 configurada" + "already_configured": "La cuenta ya est\u00e1 configurada", + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", diff --git a/homeassistant/components/volvooncall/translations/et.json b/homeassistant/components/volvooncall/translations/et.json index 0469a09a381..740e0bbc68c 100644 --- a/homeassistant/components/volvooncall/translations/et.json +++ b/homeassistant/components/volvooncall/translations/et.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Kasutaja on juba seadistatud" + "already_configured": "Kasutaja on juba seadistatud", + "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "error": { "invalid_auth": "Tuvastamine nurjus", diff --git a/homeassistant/components/volvooncall/translations/hu.json b/homeassistant/components/volvooncall/translations/hu.json index c8b3ff45d8c..ae91d2d4b9b 100644 --- a/homeassistant/components/volvooncall/translations/hu.json +++ b/homeassistant/components/volvooncall/translations/hu.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van" + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "error": { "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", diff --git a/homeassistant/components/volvooncall/translations/id.json b/homeassistant/components/volvooncall/translations/id.json index df0b1efc0d6..c8815d0000e 100644 --- a/homeassistant/components/volvooncall/translations/id.json +++ b/homeassistant/components/volvooncall/translations/id.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Akun sudah dikonfigurasi" + "already_configured": "Akun sudah dikonfigurasi", + "reauth_successful": "Autentikasi ulang berhasil" }, "error": { "invalid_auth": "Autentikasi tidak valid", diff --git a/homeassistant/components/volvooncall/translations/it.json b/homeassistant/components/volvooncall/translations/it.json index 84075dc8756..dedf72573b6 100644 --- a/homeassistant/components/volvooncall/translations/it.json +++ b/homeassistant/components/volvooncall/translations/it.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "L'account \u00e8 gi\u00e0 configurato" + "already_configured": "L'account \u00e8 gi\u00e0 configurato", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" }, "error": { "invalid_auth": "Autenticazione non valida", diff --git a/homeassistant/components/volvooncall/translations/no.json b/homeassistant/components/volvooncall/translations/no.json index 51fd5531c57..c1c56e7fe23 100644 --- a/homeassistant/components/volvooncall/translations/no.json +++ b/homeassistant/components/volvooncall/translations/no.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Kontoen er allerede konfigurert" + "already_configured": "Kontoen er allerede konfigurert", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" }, "error": { "invalid_auth": "Ugyldig godkjenning", diff --git a/homeassistant/components/volvooncall/translations/pt-BR.json b/homeassistant/components/volvooncall/translations/pt-BR.json index f1e66ecd29e..515ff7d3f31 100644 --- a/homeassistant/components/volvooncall/translations/pt-BR.json +++ b/homeassistant/components/volvooncall/translations/pt-BR.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "A conta j\u00e1 est\u00e1 configurada" + "already_configured": "A conta j\u00e1 est\u00e1 configurada", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" }, "error": { "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", diff --git a/homeassistant/components/volvooncall/translations/zh-Hant.json b/homeassistant/components/volvooncall/translations/zh-Hant.json index 8167ca9b9b8..6eedaa512cf 100644 --- a/homeassistant/components/volvooncall/translations/zh-Hant.json +++ b/homeassistant/components/volvooncall/translations/zh-Hant.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", diff --git a/homeassistant/components/vulcan/translations/es.json b/homeassistant/components/vulcan/translations/es.json index f98fa85f818..aa92ffbf6c0 100644 --- a/homeassistant/components/vulcan/translations/es.json +++ b/homeassistant/components/vulcan/translations/es.json @@ -4,7 +4,7 @@ "all_student_already_configured": "Todos los estudiantes ya han sido a\u00f1adidos.", "already_configured": "Ese estudiante ya ha sido a\u00f1adido.", "no_matching_entries": "No se encontraron entradas que coincidan, por favor, usa una cuenta diferente o elimina la integraci\u00f3n con el estudiante obsoleto.", - "reauth_successful": "Re-autenticaci\u00f3n exitosa" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "cannot_connect": "Error de conexi\u00f3n - por favor, comprueba tu conexi\u00f3n a Internet", @@ -36,7 +36,7 @@ "region": "S\u00edmbolo", "token": "Token" }, - "description": "Inicie sesi\u00f3n en tu cuenta de Vulcan utilizando la p\u00e1gina de registro de la aplicaci\u00f3n m\u00f3vil." + "description": "Inicia sesi\u00f3n en tu cuenta Vulcan utilizando la p\u00e1gina de registro de la aplicaci\u00f3n m\u00f3vil." }, "select_saved_credentials": { "data": { diff --git a/homeassistant/components/wallbox/translations/es.json b/homeassistant/components/wallbox/translations/es.json index e93a47ce3b3..6bcf24b4a08 100644 --- a/homeassistant/components/wallbox/translations/es.json +++ b/homeassistant/components/wallbox/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/watttime/translations/es.json b/homeassistant/components/watttime/translations/es.json index 9267314d960..52361f8c28a 100644 --- a/homeassistant/components/watttime/translations/es.json +++ b/homeassistant/components/watttime/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", diff --git a/homeassistant/components/xiaomi_ble/translations/es.json b/homeassistant/components/xiaomi_ble/translations/es.json index 357bb14cdf4..cf441adbefa 100644 --- a/homeassistant/components/xiaomi_ble/translations/es.json +++ b/homeassistant/components/xiaomi_ble/translations/es.json @@ -7,7 +7,7 @@ "expected_24_characters": "Se esperaba una clave de enlace hexadecimal de 24 caracteres.", "expected_32_characters": "Se esperaba una clave de enlace hexadecimal de 32 caracteres.", "no_devices_found": "No se encontraron dispositivos en la red", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "decryption_failed": "La clave de enlace proporcionada no funcion\u00f3, los datos del sensor no se pudieron descifrar. Por favor, compru\u00e9balo e int\u00e9ntalo de nuevo.", diff --git a/homeassistant/components/xiaomi_miio/translations/es.json b/homeassistant/components/xiaomi_miio/translations/es.json index a93ca090955..68bdda15a22 100644 --- a/homeassistant/components/xiaomi_miio/translations/es.json +++ b/homeassistant/components/xiaomi_miio/translations/es.json @@ -5,7 +5,7 @@ "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "incomplete_info": "Informaci\u00f3n incompleta para configurar el dispositivo, no se proporcion\u00f3 host ni token.", "not_xiaomi_miio": "El dispositivo no es (todav\u00eda) compatible con Xiaomi Miio.", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/yale_smart_alarm/translations/es.json b/homeassistant/components/yale_smart_alarm/translations/es.json index 94f54854126..2e85a8d0d86 100644 --- a/homeassistant/components/yale_smart_alarm/translations/es.json +++ b/homeassistant/components/yale_smart_alarm/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/yolink/translations/es.json b/homeassistant/components/yolink/translations/es.json index 391bf1b1164..69b5fbc04d3 100644 --- a/homeassistant/components/yolink/translations/es.json +++ b/homeassistant/components/yolink/translations/es.json @@ -7,7 +7,7 @@ "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [revisa la secci\u00f3n de ayuda]({docs_url})", "oauth_error": "Se han recibido datos de token no v\u00e1lidos.", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "create_entry": { "default": "Autenticado correctamente" diff --git a/homeassistant/components/zha/translations/ca.json b/homeassistant/components/zha/translations/ca.json index 792ea63c57d..1ee6d49cfc3 100644 --- a/homeassistant/components/zha/translations/ca.json +++ b/homeassistant/components/zha/translations/ca.json @@ -10,7 +10,28 @@ }, "flow_title": "{name}", "step": { + "choose_automatic_backup": { + "data": { + "choose_automatic_backup": "Trieu una c\u00f2pia de seguretat autom\u00e0tica" + }, + "description": "Restaura la configuraci\u00f3 de la xarxa des d'una c\u00f2pia de seguretat autom\u00e0tica", + "title": "Restauraci\u00f3 de la c\u00f2pia de seguretat autom\u00e0tica" + }, + "choose_formation_strategy": { + "description": "Trieu la configuraci\u00f3 de xarxa per a la vostra r\u00e0dio.", + "menu_options": { + "choose_automatic_backup": "Restaura una c\u00f2pia de seguretat autom\u00e0tica", + "form_new_network": "Esborra la configuraci\u00f3 de xarxa i crea una xarxa nova", + "reuse_settings": "Mant\u00e9 la configuraci\u00f3 de xarxa r\u00e0dio", + "upload_manual_backup": "Puja c\u00f2pia de seguretat manual" + }, + "title": "Formaci\u00f3 de xarxa" + }, "choose_serial_port": { + "data": { + "path": "Ruta del port s\u00e8rie al dispositiu" + }, + "description": "Selecciona el port s\u00e8rie de la r\u00e0dio Zigbee", "title": "Selecciona un port s\u00e8rie" }, "confirm": { @@ -23,14 +44,25 @@ "data": { "radio_type": "Tipus de r\u00e0dio" }, + "description": "Tria el tipus de r\u00e0dio Zigbee", "title": "Tipus de r\u00e0dio" }, "manual_port_config": { "data": { - "baudrate": "velocitat del port" + "baudrate": "velocitat del port", + "flow_control": "control de flux de dades", + "path": "Ruta del port s\u00e8rie al dispositiu" }, + "description": "Introdueix la configuraci\u00f3 del port s\u00e8rie", "title": "Configuraci\u00f3 del port s\u00e8rie" }, + "maybe_confirm_ezsp_restore": { + "data": { + "overwrite_coordinator_ieee": "Sobreescriu permanentment l'adre\u00e7a IEEE r\u00e0dio" + }, + "description": "La teva c\u00f2pia de seguretat t\u00e9 una adre\u00e7a IEEE diferent de la teva r\u00e0dio. Perqu\u00e8 la xarxa funcioni correctament, tamb\u00e9 s'ha de canviar l'adre\u00e7a IEEE de la teva r\u00e0dio. \n\nAquesta \u00e9s una operaci\u00f3 permanent.", + "title": "Sobreescriu l'adre\u00e7a IEEE r\u00e0dio" + }, "pick_radio": { "data": { "radio_type": "Tipus de r\u00e0dio" @@ -50,7 +82,9 @@ "upload_manual_backup": { "data": { "uploaded_backup_file": "Puja un fitxer" - } + }, + "description": "Restaura la configuraci\u00f3 de xarxa des d'un fitxer JSON de c\u00f2pia de seguretat penjat. Pots baixar-ne un des d'una instal\u00b7laci\u00f3 ZHA diferent anant a **Configuraci\u00f3 de xarxa** o utilitzar un fitxer `coordinator_backup.json` de Zigbee2MQTT.", + "title": "Pujada de c\u00f2pia de seguretat manual" }, "user": { "data": { @@ -146,28 +180,63 @@ }, "flow_title": "{name}", "step": { + "choose_automatic_backup": { + "data": { + "choose_automatic_backup": "Trieu una c\u00f2pia de seguretat autom\u00e0tica" + }, + "description": "Restaura la configuraci\u00f3 de la xarxa des d'una c\u00f2pia de seguretat autom\u00e0tica", + "title": "Restauraci\u00f3 de la c\u00f2pia de seguretat autom\u00e0tica" + }, + "choose_formation_strategy": { + "description": "Trieu la configuraci\u00f3 de xarxa per a la vostra r\u00e0dio.", + "menu_options": { + "choose_automatic_backup": "Restaura una c\u00f2pia de seguretat autom\u00e0tica", + "form_new_network": "Esborra la configuraci\u00f3 de xarxa i crea una xarxa nova", + "reuse_settings": "Mant\u00e9 la configuraci\u00f3 de xarxa r\u00e0dio", + "upload_manual_backup": "Puja c\u00f2pia de seguretat manual" + }, + "title": "Formaci\u00f3 de xarxa" + }, "choose_serial_port": { + "data": { + "path": "Ruta del port s\u00e8rie al dispositiu" + }, + "description": "Selecciona el port s\u00e8rie de la r\u00e0dio Zigbee", "title": "Selecciona un port s\u00e8rie" }, "init": { + "description": "ZHA s'aturar\u00e0. Vols continuar?", "title": "Reconfiguraci\u00f3 de ZHA" }, "manual_pick_radio_type": { "data": { "radio_type": "Tipus de r\u00e0dio" }, + "description": "Tria el tipus de r\u00e0dio Zigbee", "title": "Tipus de r\u00e0dio" }, "manual_port_config": { "data": { - "baudrate": "velocitat del port" + "baudrate": "velocitat del port", + "flow_control": "control de flux de dades", + "path": "Ruta del port s\u00e8rie al dispositiu" }, + "description": "Introdueix la configuraci\u00f3 del port s\u00e8rie", "title": "Configuraci\u00f3 del port s\u00e8rie" }, + "maybe_confirm_ezsp_restore": { + "data": { + "overwrite_coordinator_ieee": "Sobreescriu permanentment l'adre\u00e7a IEEE r\u00e0dio" + }, + "description": "La teva c\u00f2pia de seguretat t\u00e9 una adre\u00e7a IEEE diferent de la teva r\u00e0dio. Perqu\u00e8 la xarxa funcioni correctament, tamb\u00e9 s'ha de canviar l'adre\u00e7a IEEE de la teva r\u00e0dio. \n\nAquesta \u00e9s una operaci\u00f3 permanent.", + "title": "Sobreescriu l'adre\u00e7a IEEE r\u00e0dio" + }, "upload_manual_backup": { "data": { "uploaded_backup_file": "Puja un fitxer" - } + }, + "description": "Restaura la configuraci\u00f3 de xarxa des d'un fitxer JSON de c\u00f2pia de seguretat penjat. Pots baixar-ne un des d'una instal\u00b7laci\u00f3 ZHA diferent anant a **Configuraci\u00f3 de xarxa** o utilitzar un fitxer `coordinator_backup.json` de Zigbee2MQTT.", + "title": "Pujada de c\u00f2pia de seguretat manual" } } } diff --git a/homeassistant/components/zha/translations/cs.json b/homeassistant/components/zha/translations/cs.json index de16e3bd387..5ce5ad2d090 100644 --- a/homeassistant/components/zha/translations/cs.json +++ b/homeassistant/components/zha/translations/cs.json @@ -69,5 +69,8 @@ "remote_button_short_release": "Uvoln\u011bno tla\u010d\u00edtko \"{subtype}\"", "remote_button_triple_press": "Tla\u010d\u00edtko \"{subtype}\" stisknuto t\u0159ikr\u00e1t" } + }, + "options": { + "flow_title": "ZHA: {name}" } } \ No newline at end of file diff --git a/homeassistant/components/zha/translations/el.json b/homeassistant/components/zha/translations/el.json index 68ba1b9f0c4..6a0cd501197 100644 --- a/homeassistant/components/zha/translations/el.json +++ b/homeassistant/components/zha/translations/el.json @@ -6,16 +6,64 @@ "usb_probe_failed": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03b1\u03bd\u03af\u03c7\u03bd\u03b5\u03c5\u03c3\u03b7\u03c2 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 usb" }, "error": { - "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "invalid_backup_json": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03b1\u03bd\u03c4\u03af\u03b3\u03c1\u03b1\u03c6\u03bf \u03b1\u03c3\u03c6\u03b1\u03bb\u03b5\u03af\u03b1\u03c2 JSON" }, "flow_title": "{name}", "step": { + "choose_automatic_backup": { + "data": { + "choose_automatic_backup": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03bf \u03b1\u03bd\u03c4\u03af\u03b3\u03c1\u03b1\u03c6\u03bf \u03b1\u03c3\u03c6\u03b1\u03bb\u03b5\u03af\u03b1\u03c2" + }, + "description": "\u0395\u03c0\u03b1\u03bd\u03b1\u03c6\u03bf\u03c1\u03ac \u03c4\u03c9\u03bd \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c9\u03bd \u03c4\u03bf\u03c5 \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5 \u03c3\u03b1\u03c2 \u03b1\u03c0\u03cc \u03ad\u03bd\u03b1 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03bf \u03b1\u03bd\u03c4\u03af\u03b3\u03c1\u03b1\u03c6\u03bf \u03b1\u03c3\u03c6\u03b1\u03bb\u03b5\u03af\u03b1\u03c2", + "title": "\u0395\u03c0\u03b1\u03bd\u03b1\u03c6\u03bf\u03c1\u03ac \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03bf\u03c5 \u03b1\u03bd\u03c4\u03b9\u03b3\u03c1\u03ac\u03c6\u03bf\u03c5 \u03b1\u03c3\u03c6\u03b1\u03bb\u03b5\u03af\u03b1\u03c2" + }, + "choose_formation_strategy": { + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5 \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03ba\u03b5\u03c1\u03b1\u03af\u03b1 \u03c3\u03b1\u03c2.", + "menu_options": { + "choose_automatic_backup": "\u0395\u03c0\u03b1\u03bd\u03b1\u03c6\u03bf\u03c1\u03ac \u03b5\u03bd\u03cc\u03c2 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03bf\u03c5 \u03b1\u03bd\u03c4\u03b9\u03b3\u03c1\u03ac\u03c6\u03bf\u03c5 \u03b1\u03c3\u03c6\u03b1\u03bb\u03b5\u03af\u03b1\u03c2", + "form_new_network": "\u0394\u03b9\u03b1\u03b3\u03c1\u03b1\u03c6\u03ae \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c9\u03bd \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5 \u03ba\u03b1\u03b9 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03bd\u03ad\u03bf\u03c5 \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5", + "reuse_settings": "\u0394\u03b9\u03b1\u03c4\u03ae\u03c1\u03b7\u03c3\u03b7 \u03c4\u03c9\u03bd \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c9\u03bd \u03c1\u03b1\u03b4\u03b9\u03bf\u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5", + "upload_manual_backup": "\u0391\u03bd\u03b5\u03b2\u03ac\u03c3\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03c7\u03b5\u03b9\u03c1\u03bf\u03ba\u03af\u03bd\u03b7\u03c4\u03bf \u03b1\u03bd\u03c4\u03af\u03b3\u03c1\u03b1\u03c6\u03bf \u03b1\u03c3\u03c6\u03b1\u03bb\u03b5\u03af\u03b1\u03c2" + }, + "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5" + }, + "choose_serial_port": { + "data": { + "path": "\u0394\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03ae\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03ae \u03b8\u03cd\u03c1\u03b1 \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03ba\u03b5\u03c1\u03b1\u03af\u03b1 Zigbee", + "title": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03ae \u03b8\u03cd\u03c1\u03b1" + }, "confirm": { "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name} ;" }, "confirm_hardware": { "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name};" }, + "manual_pick_radio_type": { + "data": { + "radio_type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03ba\u03b5\u03c1\u03b1\u03af\u03b1\u03c2" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c4\u03cd\u03c0\u03bf \u03c4\u03b7\u03c2 \u03ba\u03b5\u03c1\u03b1\u03af\u03b1\u03c2 Zigbee", + "title": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03ba\u03b5\u03c1\u03b1\u03af\u03b1\u03c2" + }, + "manual_port_config": { + "data": { + "baudrate": "\u03c4\u03b1\u03c7\u03cd\u03c4\u03b7\u03c4\u03b1 \u03b8\u03cd\u03c1\u03b1\u03c2", + "flow_control": "\u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c1\u03bf\u03ae\u03c2 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03c9\u03bd", + "path": "\u0394\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03ae\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2" + }, + "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03c4\u03b7\u03c2 \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03ae\u03c2 \u03b8\u03cd\u03c1\u03b1\u03c2", + "title": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03ae\u03c2 \u03b8\u03cd\u03c1\u03b1\u03c2" + }, + "maybe_confirm_ezsp_restore": { + "data": { + "overwrite_coordinator_ieee": "\u039c\u03cc\u03bd\u03b9\u03bc\u03b7 \u03b1\u03bd\u03c4\u03b9\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03c1\u03b1\u03b4\u03b9\u03bf\u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7\u03c2 IEEE" + }, + "description": "\u03a4\u03bf \u03b5\u03c6\u03b5\u03b4\u03c1\u03b9\u03ba\u03cc \u03c3\u03b1\u03c2 \u03b1\u03bd\u03c4\u03af\u03b3\u03c1\u03b1\u03c6\u03bf \u03ad\u03c7\u03b5\u03b9 \u03b4\u03b9\u03b1\u03c6\u03bf\u03c1\u03b5\u03c4\u03b9\u03ba\u03ae \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IEEE \u03b1\u03c0\u03cc \u03c4\u03bf\u03bd \u03b1\u03c3\u03cd\u03c1\u03bc\u03b1\u03c4\u03cc \u03c3\u03b1\u03c2. \u0393\u03b9\u03b1 \u03bd\u03b1 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03b5\u03b9 \u03c3\u03c9\u03c3\u03c4\u03ac \u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03cc \u03c3\u03b1\u03c2, \u03b8\u03b1 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b1\u03bb\u03bb\u03ac\u03be\u03b5\u03b9 \u03ba\u03b1\u03b9 \u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IEEE \u03c4\u03bf\u03c5 \u03c1\u03b1\u03b4\u03b9\u03bf\u03c6\u03ce\u03bd\u03bf\u03c5 \u03c3\u03b1\u03c2.\n\n\u0391\u03c5\u03c4\u03ae \u03b5\u03af\u03bd\u03b1\u03b9 \u03bc\u03b9\u03b1 \u03bc\u03cc\u03bd\u03b9\u03bc\u03b7 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1.", + "title": "\u0391\u03bd\u03c4\u03b9\u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IEEE Radio" + }, "pick_radio": { "data": { "radio_type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03ba\u03b5\u03c1\u03b1\u03af\u03b1\u03c2" @@ -32,6 +80,13 @@ "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03c9\u03b3\u03ae \u03c3\u03c5\u03b3\u03ba\u03b5\u03ba\u03c1\u03b9\u03bc\u03ad\u03bd\u03c9\u03bd \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c9\u03bd \u03b8\u03cd\u03c1\u03b1\u03c2", "title": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2" }, + "upload_manual_backup": { + "data": { + "uploaded_backup_file": "\u0391\u03bd\u03b5\u03b2\u03ac\u03c3\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf" + }, + "description": "\u0395\u03c0\u03b1\u03bd\u03b1\u03c6\u03ad\u03c1\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5 \u03c3\u03b1\u03c2 \u03b1\u03c0\u03cc \u03ad\u03bd\u03b1 \u03bc\u03b5\u03c4\u03b1\u03c6\u03bf\u03c1\u03c4\u03c9\u03bc\u03ad\u03bd\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf \u03b1\u03bd\u03c4\u03b9\u03b3\u03c1\u03ac\u03c6\u03bf\u03c5 \u03b1\u03c3\u03c6\u03b1\u03bb\u03b5\u03af\u03b1\u03c2 JSON. \u039c\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03ba\u03ac\u03bd\u03b5\u03c4\u03b5 \u03bb\u03ae\u03c8\u03b7 \u03b5\u03bd\u03cc\u03c2 \u03b1\u03c0\u03cc \u03b4\u03b9\u03b1\u03c6\u03bf\u03c1\u03b5\u03c4\u03b9\u03ba\u03ae \u03b5\u03b3\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 ZHA \u03b1\u03c0\u03cc \u03c4\u03b9\u03c2 **\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5** \u03ae \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf \u00abcoordinator_backup.json\u00bb Zigbee2MQTT.", + "title": "\u0391\u03bd\u03b5\u03b2\u03ac\u03c3\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03c7\u03b5\u03b9\u03c1\u03bf\u03ba\u03af\u03bd\u03b7\u03c4\u03bf \u03b1\u03bd\u03c4\u03af\u03b3\u03c1\u03b1\u03c6\u03bf \u03b1\u03c3\u03c6\u03b1\u03bb\u03b5\u03af\u03b1\u03c2" + }, "user": { "data": { "path": "\u0394\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03ae\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2" @@ -114,5 +169,75 @@ "remote_button_short_release": "\u0391\u03c6\u03ad\u03b8\u03b7\u03ba\u03b5 \u03c4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af \u03c4\u03bf\u03c5 \"{subtype}\"", "remote_button_triple_press": "\u03a4\u03c1\u03b9\u03c0\u03bb\u03cc \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af \"{subtype}\"" } + }, + "options": { + "abort": { + "not_zha_device": "\u0391\u03c5\u03c4\u03ae \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae zha", + "usb_probe_failed": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03b1\u03bd\u03af\u03c7\u03bd\u03b5\u03c5\u03c3\u03b7\u03c2 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 usb" + }, + "error": { + "invalid_backup_json": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03b1\u03bd\u03c4\u03af\u03b3\u03c1\u03b1\u03c6\u03bf \u03b1\u03c3\u03c6\u03b1\u03bb\u03b5\u03af\u03b1\u03c2 JSON" + }, + "flow_title": "{name}", + "step": { + "choose_automatic_backup": { + "data": { + "choose_automatic_backup": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03bf \u03b1\u03bd\u03c4\u03af\u03b3\u03c1\u03b1\u03c6\u03bf \u03b1\u03c3\u03c6\u03b1\u03bb\u03b5\u03af\u03b1\u03c2" + }, + "description": "\u0395\u03c0\u03b1\u03bd\u03b1\u03c6\u03bf\u03c1\u03ac \u03c4\u03c9\u03bd \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c9\u03bd \u03c4\u03bf\u03c5 \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5 \u03c3\u03b1\u03c2 \u03b1\u03c0\u03cc \u03ad\u03bd\u03b1 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03bf \u03b1\u03bd\u03c4\u03af\u03b3\u03c1\u03b1\u03c6\u03bf \u03b1\u03c3\u03c6\u03b1\u03bb\u03b5\u03af\u03b1\u03c2", + "title": "\u0395\u03c0\u03b1\u03bd\u03b1\u03c6\u03bf\u03c1\u03ac \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03bf\u03c5 \u03b1\u03bd\u03c4\u03b9\u03b3\u03c1\u03ac\u03c6\u03bf\u03c5 \u03b1\u03c3\u03c6\u03b1\u03bb\u03b5\u03af\u03b1\u03c2" + }, + "choose_formation_strategy": { + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5 \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03ba\u03b5\u03c1\u03b1\u03af\u03b1 \u03c3\u03b1\u03c2.", + "menu_options": { + "choose_automatic_backup": "\u0395\u03c0\u03b1\u03bd\u03b1\u03c6\u03bf\u03c1\u03ac \u03b5\u03bd\u03cc\u03c2 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03bf\u03c5 \u03b1\u03bd\u03c4\u03b9\u03b3\u03c1\u03ac\u03c6\u03bf\u03c5 \u03b1\u03c3\u03c6\u03b1\u03bb\u03b5\u03af\u03b1\u03c2", + "form_new_network": "\u0394\u03b9\u03b1\u03b3\u03c1\u03b1\u03c6\u03ae \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c9\u03bd \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5 \u03ba\u03b1\u03b9 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03bd\u03ad\u03bf\u03c5 \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5", + "reuse_settings": "\u0394\u03b9\u03b1\u03c4\u03ae\u03c1\u03b7\u03c3\u03b7 \u03c4\u03c9\u03bd \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c9\u03bd \u03c1\u03b1\u03b4\u03b9\u03bf\u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5", + "upload_manual_backup": "\u0391\u03bd\u03b5\u03b2\u03ac\u03c3\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03c7\u03b5\u03b9\u03c1\u03bf\u03ba\u03af\u03bd\u03b7\u03c4\u03bf \u03b1\u03bd\u03c4\u03af\u03b3\u03c1\u03b1\u03c6\u03bf \u03b1\u03c3\u03c6\u03b1\u03bb\u03b5\u03af\u03b1\u03c2" + }, + "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5" + }, + "choose_serial_port": { + "data": { + "path": "\u0394\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03ae\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03ae \u03b8\u03cd\u03c1\u03b1 \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03ba\u03b5\u03c1\u03b1\u03af\u03b1 Zigbee", + "title": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03ae \u03b8\u03cd\u03c1\u03b1" + }, + "init": { + "description": "\u03a4\u03bf ZHA \u03b8\u03b1 \u03c3\u03c4\u03b1\u03bc\u03b1\u03c4\u03ae\u03c3\u03b5\u03b9. \u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b5\u03c7\u03af\u03c3\u03b5\u03c4\u03b5;", + "title": "\u0395\u03c0\u03b1\u03bd\u03b1\u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 ZHA" + }, + "manual_pick_radio_type": { + "data": { + "radio_type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03ba\u03b5\u03c1\u03b1\u03af\u03b1\u03c2" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c4\u03cd\u03c0\u03bf \u03c4\u03b7\u03c2 \u03ba\u03b5\u03c1\u03b1\u03af\u03b1\u03c2 Zigbee", + "title": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03ba\u03b5\u03c1\u03b1\u03af\u03b1\u03c2" + }, + "manual_port_config": { + "data": { + "baudrate": "\u03c4\u03b1\u03c7\u03cd\u03c4\u03b7\u03c4\u03b1 \u03b8\u03cd\u03c1\u03b1\u03c2", + "flow_control": "\u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c1\u03bf\u03ae\u03c2 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03c9\u03bd", + "path": "\u0394\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03ae\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2" + }, + "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03c4\u03b7\u03c2 \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03ae\u03c2 \u03b8\u03cd\u03c1\u03b1\u03c2", + "title": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03ae\u03c2 \u03b8\u03cd\u03c1\u03b1\u03c2" + }, + "maybe_confirm_ezsp_restore": { + "data": { + "overwrite_coordinator_ieee": "\u039c\u03cc\u03bd\u03b9\u03bc\u03b7 \u03b1\u03bd\u03c4\u03b9\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03c1\u03b1\u03b4\u03b9\u03bf\u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7\u03c2 IEEE" + }, + "description": "\u03a4\u03bf \u03b5\u03c6\u03b5\u03b4\u03c1\u03b9\u03ba\u03cc \u03c3\u03b1\u03c2 \u03b1\u03bd\u03c4\u03af\u03b3\u03c1\u03b1\u03c6\u03bf \u03ad\u03c7\u03b5\u03b9 \u03b4\u03b9\u03b1\u03c6\u03bf\u03c1\u03b5\u03c4\u03b9\u03ba\u03ae \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IEEE \u03b1\u03c0\u03cc \u03c4\u03bf\u03bd \u03b1\u03c3\u03cd\u03c1\u03bc\u03b1\u03c4\u03cc \u03c3\u03b1\u03c2. \u0393\u03b9\u03b1 \u03bd\u03b1 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03b5\u03b9 \u03c3\u03c9\u03c3\u03c4\u03ac \u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03cc \u03c3\u03b1\u03c2, \u03b8\u03b1 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b1\u03bb\u03bb\u03ac\u03be\u03b5\u03b9 \u03ba\u03b1\u03b9 \u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IEEE \u03c4\u03bf\u03c5 \u03c1\u03b1\u03b4\u03b9\u03bf\u03c6\u03ce\u03bd\u03bf\u03c5 \u03c3\u03b1\u03c2.\n\n\u0391\u03c5\u03c4\u03ae \u03b5\u03af\u03bd\u03b1\u03b9 \u03bc\u03b9\u03b1 \u03bc\u03cc\u03bd\u03b9\u03bc\u03b7 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1.", + "title": "\u0391\u03bd\u03c4\u03b9\u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IEEE Radio" + }, + "upload_manual_backup": { + "data": { + "uploaded_backup_file": "\u0391\u03bd\u03b5\u03b2\u03ac\u03c3\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf" + }, + "description": "\u0395\u03c0\u03b1\u03bd\u03b1\u03c6\u03ad\u03c1\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5 \u03c3\u03b1\u03c2 \u03b1\u03c0\u03cc \u03ad\u03bd\u03b1 \u03bc\u03b5\u03c4\u03b1\u03c6\u03bf\u03c1\u03c4\u03c9\u03bc\u03ad\u03bd\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf \u03b1\u03bd\u03c4\u03b9\u03b3\u03c1\u03ac\u03c6\u03bf\u03c5 \u03b1\u03c3\u03c6\u03b1\u03bb\u03b5\u03af\u03b1\u03c2 JSON. \u039c\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03ba\u03ac\u03bd\u03b5\u03c4\u03b5 \u03bb\u03ae\u03c8\u03b7 \u03b5\u03bd\u03cc\u03c2 \u03b1\u03c0\u03cc \u03b4\u03b9\u03b1\u03c6\u03bf\u03c1\u03b5\u03c4\u03b9\u03ba\u03ae \u03b5\u03b3\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 ZHA \u03b1\u03c0\u03cc \u03c4\u03b9\u03c2 **\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5** \u03ae \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf \u00abcoordinator_backup.json\u00bb Zigbee2MQTT.", + "title": "\u0391\u03bd\u03b5\u03b2\u03ac\u03c3\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03c7\u03b5\u03b9\u03c1\u03bf\u03ba\u03af\u03bd\u03b7\u03c4\u03bf \u03b1\u03bd\u03c4\u03af\u03b3\u03c1\u03b1\u03c6\u03bf \u03b1\u03c3\u03c6\u03b1\u03bb\u03b5\u03af\u03b1\u03c2" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/translations/es.json b/homeassistant/components/zha/translations/es.json index feb8b9efaed..7aa42172d05 100644 --- a/homeassistant/components/zha/translations/es.json +++ b/homeassistant/components/zha/translations/es.json @@ -173,6 +173,7 @@ "options": { "abort": { "not_zha_device": "Este dispositivo no es un dispositivo zha", + "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n.", "usb_probe_failed": "Error al sondear el dispositivo USB" }, "error": { diff --git a/homeassistant/components/zha/translations/et.json b/homeassistant/components/zha/translations/et.json index aa3c26144fa..2953c94f4cf 100644 --- a/homeassistant/components/zha/translations/et.json +++ b/homeassistant/components/zha/translations/et.json @@ -114,5 +114,12 @@ "remote_button_short_release": "\"{subtype}\" nupp vabastati", "remote_button_triple_press": "Nuppu \"{subtype}\" kl\u00f5psati kolm korda" } + }, + "options": { + "step": { + "upload_manual_backup": { + "title": "Lae k\u00e4sitsi loodud varukoopia \u00fcles" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/translations/fr.json b/homeassistant/components/zha/translations/fr.json index 87027c7ca62..505c6780d6a 100644 --- a/homeassistant/components/zha/translations/fr.json +++ b/homeassistant/components/zha/translations/fr.json @@ -6,16 +6,61 @@ "usb_probe_failed": "\u00c9chec de l'analyse du p\u00e9riph\u00e9rique USB" }, "error": { - "cannot_connect": "\u00c9chec de connexion" + "cannot_connect": "\u00c9chec de connexion", + "invalid_backup_json": "JSON de sauvegarde non valide" }, "flow_title": "{name}", "step": { + "choose_automatic_backup": { + "data": { + "choose_automatic_backup": "S\u00e9lectionnez une sauvegarde automatique" + }, + "description": "Restaurer vos param\u00e8tres r\u00e9seau \u00e0 partir d'une sauvegarde automatique", + "title": "Restaurer une sauvegarde automatique" + }, + "choose_formation_strategy": { + "description": "S\u00e9lectionnez les param\u00e8tres r\u00e9seau de votre radio.", + "menu_options": { + "choose_automatic_backup": "Restaurer une sauvegarde automatique", + "form_new_network": "Effacer les param\u00e8tres r\u00e9seau pour cr\u00e9er un nouveau r\u00e9seau", + "reuse_settings": "Conserver les param\u00e8tres r\u00e9seau de la radio", + "upload_manual_backup": "T\u00e9l\u00e9charger une sauvegarde manuelle" + } + }, + "choose_serial_port": { + "data": { + "path": "Chemin d\u2019acc\u00e8s du p\u00e9riph\u00e9rique s\u00e9rie" + }, + "description": "S\u00e9lectionnez le port s\u00e9rie de votre radio Zigbee", + "title": "S\u00e9lectionnez un port s\u00e9rie" + }, "confirm": { "description": "Voulez-vous configurer {name}\u00a0?" }, "confirm_hardware": { "description": "Voulez-vous configurer {name}\u00a0?" }, + "manual_pick_radio_type": { + "data": { + "radio_type": "Type de radio" + }, + "description": "S\u00e9lectionnez le type de votre radio Zigbee", + "title": "Type de radio" + }, + "manual_port_config": { + "data": { + "baudrate": "vitesse du port", + "path": "Chemin d\u2019acc\u00e8s du p\u00e9riph\u00e9rique s\u00e9rie" + }, + "description": "Saisissez les param\u00e8tres du port s\u00e9rie", + "title": "Param\u00e8tres du port s\u00e9rie" + }, + "maybe_confirm_ezsp_restore": { + "data": { + "overwrite_coordinator_ieee": "Remplacer d\u00e9finitivement l'adresse IEEE de la radio" + }, + "title": "\u00c9craser l'adresse IEEE de la radio" + }, "pick_radio": { "data": { "radio_type": "Type de radio" @@ -32,6 +77,12 @@ "description": "Saisir les param\u00e8tres sp\u00e9cifiques au port", "title": "R\u00e9glages" }, + "upload_manual_backup": { + "data": { + "uploaded_backup_file": "T\u00e9l\u00e9verser un fichier" + }, + "title": "T\u00e9l\u00e9verser une sauvegarde manuelle" + }, "user": { "data": { "path": "Chemin du p\u00e9riph\u00e9rique s\u00e9rie" @@ -111,5 +162,73 @@ "remote_button_short_release": "Bouton \" {subtype} \" est rel\u00e2ch\u00e9", "remote_button_triple_press": "Bouton \"{subtype}\" \u00e0 trois clics" } + }, + "options": { + "abort": { + "not_zha_device": "Cet appareil n'est pas un appareil zha", + "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible.", + "usb_probe_failed": "\u00c9chec de l'analyse du p\u00e9riph\u00e9rique USB" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_backup_json": "JSON de sauvegarde non valide" + }, + "flow_title": "{name}", + "step": { + "choose_automatic_backup": { + "data": { + "choose_automatic_backup": "S\u00e9lectionnez une sauvegarde automatique" + }, + "description": "Restaurer vos param\u00e8tres r\u00e9seau \u00e0 partir d'une sauvegarde automatique", + "title": "Restaurer une sauvegarde automatique" + }, + "choose_formation_strategy": { + "description": "S\u00e9lectionnez les param\u00e8tres r\u00e9seau de votre radio.", + "menu_options": { + "choose_automatic_backup": "Restaurer une sauvegarde automatique", + "form_new_network": "Effacer les param\u00e8tres r\u00e9seau pour cr\u00e9er un nouveau r\u00e9seau", + "reuse_settings": "Conserver les param\u00e8tres r\u00e9seau de la radio", + "upload_manual_backup": "T\u00e9l\u00e9charger une sauvegarde manuelle" + } + }, + "choose_serial_port": { + "data": { + "path": "Chemin d\u2019acc\u00e8s du p\u00e9riph\u00e9rique s\u00e9rie" + }, + "description": "S\u00e9lectionnez le port s\u00e9rie de votre radio Zigbee", + "title": "S\u00e9lectionnez un port s\u00e9rie" + }, + "init": { + "description": "ZHA sera arr\u00eat\u00e9. Souhaitez-vous continuer\u00a0?", + "title": "Reconfigurer ZHA" + }, + "manual_pick_radio_type": { + "data": { + "radio_type": "Type de radio" + }, + "description": "S\u00e9lectionnez le type de votre radio Zigbee", + "title": "Type de radio" + }, + "manual_port_config": { + "data": { + "baudrate": "vitesse du port", + "path": "Chemin d\u2019acc\u00e8s du p\u00e9riph\u00e9rique s\u00e9rie" + }, + "description": "Saisissez les param\u00e8tres du port s\u00e9rie", + "title": "Param\u00e8tres du port s\u00e9rie" + }, + "maybe_confirm_ezsp_restore": { + "data": { + "overwrite_coordinator_ieee": "Remplacer d\u00e9finitivement l'adresse IEEE de la radio" + }, + "title": "\u00c9craser l'adresse IEEE de la radio" + }, + "upload_manual_backup": { + "data": { + "uploaded_backup_file": "T\u00e9l\u00e9verser un fichier" + }, + "title": "T\u00e9l\u00e9verser une sauvegarde manuelle" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/translations/hu.json b/homeassistant/components/zha/translations/hu.json index 65a93dcf79e..9061246043a 100644 --- a/homeassistant/components/zha/translations/hu.json +++ b/homeassistant/components/zha/translations/hu.json @@ -6,16 +6,64 @@ "usb_probe_failed": "Nem siker\u00fclt megvizsg\u00e1lni az USB eszk\u00f6zt" }, "error": { - "cannot_connect": "Sikertelen csatlakoz\u00e1s" + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_backup_json": "\u00c9rv\u00e9nytelen JSON biztons\u00e1gi m\u00e1solat" }, "flow_title": "{name}", "step": { + "choose_automatic_backup": { + "data": { + "choose_automatic_backup": "Automatikus biztons\u00e1gi ment\u00e9s kiv\u00e1laszt\u00e1sa" + }, + "description": "H\u00e1l\u00f3zati be\u00e1ll\u00edt\u00e1sok vissza\u00e1ll\u00edt\u00e1sa automatikus biztons\u00e1gi ment\u00e9sb\u0151l", + "title": "Automatikus biztons\u00e1gi ment\u00e9s vissza\u00e1ll\u00edt\u00e1sa" + }, + "choose_formation_strategy": { + "description": "V\u00e1lassza ki a r\u00e1di\u00f3 h\u00e1l\u00f3zati be\u00e1ll\u00edt\u00e1sait.", + "menu_options": { + "choose_automatic_backup": "Automatikus biztons\u00e1gi ment\u00e9s vissza\u00e1ll\u00edt\u00e1sa", + "form_new_network": "H\u00e1l\u00f3zati be\u00e1ll\u00edt\u00e1sok t\u00f6rl\u00e9se \u00e9s \u00faj h\u00e1l\u00f3zat l\u00e9trehoz\u00e1sa", + "reuse_settings": "R\u00e1di\u00f3h\u00e1l\u00f3zati be\u00e1ll\u00edt\u00e1sok megtart\u00e1sa", + "upload_manual_backup": "Manu\u00e1lis biztons\u00e1gi ment\u00e9s felt\u00f6lt\u00e9se" + }, + "title": "H\u00e1l\u00f3zat kialak\u00edt\u00e1sa" + }, + "choose_serial_port": { + "data": { + "path": "Soros eszk\u00f6z el\u00e9r\u00e9si \u00fatja" + }, + "description": "V\u00e1lassza ki a soros portot a Zigbee r\u00e1di\u00f3j\u00e1hoz.", + "title": "Soros port kiv\u00e1laszt\u00e1sa" + }, "confirm": { "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name}?" }, "confirm_hardware": { "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name}?" }, + "manual_pick_radio_type": { + "data": { + "radio_type": "R\u00e1di\u00f3 t\u00edpusa" + }, + "description": "V\u00e1lassza ki a Zigbee r\u00e1di\u00f3 t\u00edpus\u00e1t", + "title": "R\u00e1di\u00f3 t\u00edpusa" + }, + "manual_port_config": { + "data": { + "baudrate": "port sebess\u00e9g", + "flow_control": "adat\u00e1raml\u00e1s szab\u00e1lyoz\u00e1sa", + "path": "Soros eszk\u00f6z el\u00e9r\u00e9si \u00fatja" + }, + "description": "Adja meg a soros port be\u00e1ll\u00edt\u00e1sait", + "title": "Soros port be\u00e1ll\u00edt\u00e1sai" + }, + "maybe_confirm_ezsp_restore": { + "data": { + "overwrite_coordinator_ieee": "A r\u00e1di\u00f3 IEEE-c\u00edm\u00e9nek v\u00e9gleges cser\u00e9je" + }, + "description": "A biztons\u00e1gi m\u00e1solat IEEE-c\u00edme elt\u00e9r a r\u00e1di\u00f3\u00e9t\u00f3l. A h\u00e1l\u00f3zat megfelel\u0151 m\u0171k\u00f6d\u00e9s\u00e9hez a r\u00e1di\u00f3 IEEE-c\u00edm\u00e9t is meg kell v\u00e1ltoztatni. \n\n Ez egy v\u00e9gleles m\u0171velet.", + "title": "A r\u00e1di\u00f3 IEEE-c\u00edm\u00e9nek fel\u00fcl\u00edr\u00e1sa" + }, "pick_radio": { "data": { "radio_type": "R\u00e1di\u00f3 t\u00edpusa" @@ -32,6 +80,13 @@ "description": "Adja meg a port specifikus be\u00e1ll\u00edt\u00e1sokat", "title": "Be\u00e1ll\u00edt\u00e1sok" }, + "upload_manual_backup": { + "data": { + "uploaded_backup_file": "F\u00e1jl felt\u00f6lt\u00e9se" + }, + "description": "H\u00e1l\u00f3zati be\u00e1ll\u00edt\u00e1sok vissza\u00e1ll\u00edt\u00e1sa egy felt\u00f6lt\u00f6tt JSON biztons\u00e1gi m\u00e1solatb\u00f3l. Let\u00f6lthet egyet egy m\u00e1sik ZHA telep\u00edt\u00e9sb\u0151l a **H\u00e1l\u00f3zati be\u00e1ll\u00edt\u00e1sok** men\u00fcpontb\u00f3l, vagy haszn\u00e1lhat egy Zigbee2MQTT `coordinator_backup.json` f\u00e1jlt.", + "title": "K\u00e9zi biztons\u00e1gi m\u00e1solat felt\u00f6lt\u00e9se" + }, "user": { "data": { "path": "Soros eszk\u00f6z el\u00e9r\u00e9si \u00fatja" @@ -114,5 +169,77 @@ "remote_button_short_release": "\"{subtype}\" gomb elengedve", "remote_button_triple_press": "\"{subtype}\" gombra h\u00e1romszor kattintottak" } + }, + "options": { + "abort": { + "not_zha_device": "Ez az eszk\u00f6z nem zha eszk\u00f6z", + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.", + "usb_probe_failed": "Nem siker\u00fclt megvizsg\u00e1lni az USB eszk\u00f6zt" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_backup_json": "\u00c9rv\u00e9nytelen JSON biztons\u00e1gi m\u00e1solat" + }, + "flow_title": "{name}", + "step": { + "choose_automatic_backup": { + "data": { + "choose_automatic_backup": "Automatikus biztons\u00e1gi ment\u00e9s kiv\u00e1laszt\u00e1sa" + }, + "description": "H\u00e1l\u00f3zati be\u00e1ll\u00edt\u00e1sok vissza\u00e1ll\u00edt\u00e1sa automatikus biztons\u00e1gi ment\u00e9sb\u0151l", + "title": "Automatikus biztons\u00e1gi ment\u00e9s vissza\u00e1ll\u00edt\u00e1sa" + }, + "choose_formation_strategy": { + "description": "V\u00e1lassza ki a r\u00e1di\u00f3 h\u00e1l\u00f3zati be\u00e1ll\u00edt\u00e1sait.", + "menu_options": { + "choose_automatic_backup": "Automatikus biztons\u00e1gi ment\u00e9s vissza\u00e1ll\u00edt\u00e1sa", + "form_new_network": "H\u00e1l\u00f3zati be\u00e1ll\u00edt\u00e1sok t\u00f6rl\u00e9se \u00e9s \u00faj h\u00e1l\u00f3zat l\u00e9trehoz\u00e1sa", + "reuse_settings": "R\u00e1di\u00f3h\u00e1l\u00f3zati be\u00e1ll\u00edt\u00e1sok megtart\u00e1sa", + "upload_manual_backup": "Manu\u00e1lis biztons\u00e1gi ment\u00e9s felt\u00f6lt\u00e9se" + }, + "title": "H\u00e1l\u00f3zat kialak\u00edt\u00e1sa" + }, + "choose_serial_port": { + "data": { + "path": "Soros eszk\u00f6z el\u00e9r\u00e9si \u00fatja" + }, + "description": "V\u00e1lassza ki a soros portot a Zigbee r\u00e1di\u00f3j\u00e1hoz.", + "title": "Soros port kiv\u00e1laszt\u00e1sa" + }, + "init": { + "description": "A ZHA le\u00e1ll. Biztos benne, hogy folytatja?", + "title": "A ZHA \u00fajrakonfigur\u00e1l\u00e1sa" + }, + "manual_pick_radio_type": { + "data": { + "radio_type": "R\u00e1di\u00f3 t\u00edpusa" + }, + "description": "V\u00e1lassza ki a Zigbee r\u00e1di\u00f3 t\u00edpus\u00e1t", + "title": "R\u00e1di\u00f3 t\u00edpusa" + }, + "manual_port_config": { + "data": { + "baudrate": "port sebess\u00e9g", + "flow_control": "adat\u00e1raml\u00e1s szab\u00e1lyoz\u00e1sa", + "path": "Soros eszk\u00f6z el\u00e9r\u00e9si \u00fatja" + }, + "description": "Adja meg a soros port be\u00e1ll\u00edt\u00e1sait", + "title": "Soros port be\u00e1ll\u00edt\u00e1sai" + }, + "maybe_confirm_ezsp_restore": { + "data": { + "overwrite_coordinator_ieee": "A r\u00e1di\u00f3 IEEE-c\u00edm\u00e9nek v\u00e9gleges cser\u00e9je" + }, + "description": "A biztons\u00e1gi m\u00e1solat IEEE-c\u00edme elt\u00e9r a r\u00e1di\u00f3\u00e9t\u00f3l. A h\u00e1l\u00f3zat megfelel\u0151 m\u0171k\u00f6d\u00e9s\u00e9hez a r\u00e1di\u00f3 IEEE-c\u00edm\u00e9t is meg kell v\u00e1ltoztatni. \n\n Ez egy v\u00e9gleles m\u0171velet.", + "title": "A r\u00e1di\u00f3 IEEE-c\u00edm\u00e9nek fel\u00fcl\u00edr\u00e1sa" + }, + "upload_manual_backup": { + "data": { + "uploaded_backup_file": "F\u00e1jl felt\u00f6lt\u00e9se" + }, + "description": "H\u00e1l\u00f3zati be\u00e1ll\u00edt\u00e1sok vissza\u00e1ll\u00edt\u00e1sa egy felt\u00f6lt\u00f6tt JSON biztons\u00e1gi m\u00e1solatb\u00f3l. Let\u00f6lthet egyet egy m\u00e1sik ZHA telep\u00edt\u00e9sb\u0151l a **H\u00e1l\u00f3zati be\u00e1ll\u00edt\u00e1sok** men\u00fcpontb\u00f3l, vagy haszn\u00e1lhat egy Zigbee2MQTT `coordinator_backup.json` f\u00e1jlt.", + "title": "K\u00e9zi biztons\u00e1gi m\u00e1solat felt\u00f6lt\u00e9se" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/translations/id.json b/homeassistant/components/zha/translations/id.json index f9947809a6d..65f7588cb60 100644 --- a/homeassistant/components/zha/translations/id.json +++ b/homeassistant/components/zha/translations/id.json @@ -6,16 +6,64 @@ "usb_probe_failed": "Gagal mendeteksi perangkat usb" }, "error": { - "cannot_connect": "Gagal terhubung" + "cannot_connect": "Gagal terhubung", + "invalid_backup_json": "JSON cadangan tidak valid" }, "flow_title": "{name}", "step": { + "choose_automatic_backup": { + "data": { + "choose_automatic_backup": "Pilih cadangan otomatis" + }, + "description": "Pulihkan pengaturan jaringan Anda dari cadangan otomatis", + "title": "Pulihkan Cadangan Otomatis" + }, + "choose_formation_strategy": { + "description": "Pilih pengaturan jaringan untuk radio Anda.", + "menu_options": { + "choose_automatic_backup": "Pulihkan cadangan otomatis", + "form_new_network": "Hapus pengaturan jaringan dan bangun jaringan baru", + "reuse_settings": "Pertahankan pengaturan jaringan radio", + "upload_manual_backup": "Unggah cadangan manual" + }, + "title": "Formasi Jaringan" + }, + "choose_serial_port": { + "data": { + "path": "Jalur Perangkat Serial" + }, + "description": "Pilih port serial untuk radio Zigbee Anda", + "title": "Pilih Port Serial" + }, "confirm": { "description": "Ingin menyiapkan {name}?" }, "confirm_hardware": { "description": "Ingin menyiapkan {name}?" }, + "manual_pick_radio_type": { + "data": { + "radio_type": "Jenis Radio" + }, + "description": "Pilih jenis radio Zigbee Anda", + "title": "Jenis Radio" + }, + "manual_port_config": { + "data": { + "baudrate": "kecepatan port", + "flow_control": "kontrol data flow", + "path": "Jalur perangkat serial" + }, + "description": "Masukkan pengaturan port serial", + "title": "Pengaturan Port Serial" + }, + "maybe_confirm_ezsp_restore": { + "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.", + "title": "Timpa Alamat IEEE Radio" + }, "pick_radio": { "data": { "radio_type": "Jenis Radio" @@ -32,6 +80,13 @@ "description": "Masukkan pengaturan khusus port", "title": "Setelan" }, + "upload_manual_backup": { + "data": { + "uploaded_backup_file": "Unggah file" + }, + "description": "Pulihkan pengaturan jaringan Anda dari file JSON cadangan yang diunggah. Anda dapat mengunduhnya dari instalasi ZHA yang berbeda dari **Pengaturan Jaringan**, atau menggunakan file Zigbee2MQTT 'coordinator_backup.json'.", + "title": "Unggah Cadangan Manual" + }, "user": { "data": { "path": "Jalur Perangkat Serial" @@ -114,5 +169,77 @@ "remote_button_short_release": "Tombol \"{subtype}\" dilepaskan", "remote_button_triple_press": "Tombol \"{subtype}\" diklik tiga kali" } + }, + "options": { + "abort": { + "not_zha_device": "Perangkat ini bukan perangkat zha", + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan.", + "usb_probe_failed": "Gagal mendeteksi perangkat usb" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_backup_json": "JSON cadangan tidak valid" + }, + "flow_title": "{name}", + "step": { + "choose_automatic_backup": { + "data": { + "choose_automatic_backup": "Pilih cadangan otomatis" + }, + "description": "Pulihkan pengaturan jaringan Anda dari cadangan otomatis", + "title": "Pulihkan Cadangan Otomatis" + }, + "choose_formation_strategy": { + "description": "Pilih pengaturan jaringan untuk radio Anda.", + "menu_options": { + "choose_automatic_backup": "Pulihkan cadangan otomatis", + "form_new_network": "Hapus pengaturan jaringan dan bangun jaringan baru", + "reuse_settings": "Pertahankan pengaturan jaringan radio", + "upload_manual_backup": "Unggah cadangan manual" + }, + "title": "Formasi Jaringan" + }, + "choose_serial_port": { + "data": { + "path": "Jalur Perangkat Serial" + }, + "description": "Pilih port serial untuk radio Zigbee Anda", + "title": "Pilih Port Serial" + }, + "init": { + "description": "ZHA akan dihentikan. Ingin melanjutkan?", + "title": "Konfigurasi Ulang ZHA" + }, + "manual_pick_radio_type": { + "data": { + "radio_type": "Jenis Radio" + }, + "description": "Pilih jenis radio Zigbee Anda", + "title": "Jenis Radio" + }, + "manual_port_config": { + "data": { + "baudrate": "kecepatan port", + "flow_control": "kontrol data flow", + "path": "Jalur perangkat serial" + }, + "description": "Masukkan pengaturan port serial", + "title": "Pengaturan Port Serial" + }, + "maybe_confirm_ezsp_restore": { + "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.", + "title": "Timpa Alamat IEEE Radio" + }, + "upload_manual_backup": { + "data": { + "uploaded_backup_file": "Unggah file" + }, + "description": "Pulihkan pengaturan jaringan Anda dari file JSON cadangan yang diunggah. Anda dapat mengunduhnya dari instalasi ZHA yang berbeda dari **Pengaturan Jaringan**, atau menggunakan file Zigbee2MQTT 'coordinator_backup.json'.", + "title": "Unggah Cadangan Manual" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/translations/it.json b/homeassistant/components/zha/translations/it.json index 9ceb94cbd00..7ff1fc354ba 100644 --- a/homeassistant/components/zha/translations/it.json +++ b/homeassistant/components/zha/translations/it.json @@ -6,16 +6,64 @@ "usb_probe_failed": "Impossibile interrogare il dispositivo USB" }, "error": { - "cannot_connect": "Impossibile connettersi" + "cannot_connect": "Impossibile connettersi", + "invalid_backup_json": "Backup JSON non valido" }, "flow_title": "{name}", "step": { + "choose_automatic_backup": { + "data": { + "choose_automatic_backup": "Scegli un backup automatico" + }, + "description": "Ripristina le impostazioni di rete da un backup automatico", + "title": "Ripristina backup automatico" + }, + "choose_formation_strategy": { + "description": "Scegli le impostazioni di rete per la tua radio.", + "menu_options": { + "choose_automatic_backup": "Ripristina un backup automatico", + "form_new_network": "Cancella le impostazioni di rete e crea una nuova rete", + "reuse_settings": "Mantieni le impostazioni della rete radio", + "upload_manual_backup": "Carica un backup manuale" + }, + "title": "Formazione di rete" + }, + "choose_serial_port": { + "data": { + "path": "Percorso del dispositivo seriale" + }, + "description": "Seleziona la porta seriale per la tua radio Zigbee", + "title": "Seleziona una porta seriale" + }, "confirm": { "description": "Vuoi configurare {name}?" }, "confirm_hardware": { "description": "Vuoi configurare {name}?" }, + "manual_pick_radio_type": { + "data": { + "radio_type": "Tipo di radio" + }, + "description": "Scegli il tuo tipo di radio Zigbee", + "title": "Tipo di radio" + }, + "manual_port_config": { + "data": { + "baudrate": "velocit\u00e0 della porta", + "flow_control": "controllo del flusso di dati", + "path": "Percorso del dispositivo seriale" + }, + "description": "Inserire le impostazioni della porta seriale", + "title": "Impostazioni della porta seriale" + }, + "maybe_confirm_ezsp_restore": { + "data": { + "overwrite_coordinator_ieee": "Sostituire definitivamente l'indirizzo IEEE radio" + }, + "description": "Il tuo backup ha un indirizzo IEEE diverso dalla tua radio. Affinch\u00e9 la rete funzioni correttamente, \u00e8 necessario modificare anche l'indirizzo IEEE della radio. \n\nQuesta \u00e8 un'operazione permanente.", + "title": "Sovrascrivi indirizzo IEEE radio" + }, "pick_radio": { "data": { "radio_type": "Tipo di radio" @@ -32,6 +80,13 @@ "description": "Inserire le impostazioni specifiche della porta", "title": "Impostazioni" }, + "upload_manual_backup": { + "data": { + "uploaded_backup_file": "Carica un file" + }, + "description": "Ripristina le impostazioni di rete da un file JSON di backup caricato. Puoi scaricarne uno da un'installazione ZHA diversa da **Impostazioni di rete** o utilizzare un file Zigbee2MQTT `coordinator_backup.json`.", + "title": "Carica un backup manuale" + }, "user": { "data": { "path": "Percorso del dispositivo seriale" @@ -114,5 +169,77 @@ "remote_button_short_release": "Pulsante \"{subtype}\" rilasciato", "remote_button_triple_press": "Pulsante \"{subtype}\" cliccato tre volte" } + }, + "options": { + "abort": { + "not_zha_device": "Questo dispositivo non \u00e8 un dispositivo zha", + "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione.", + "usb_probe_failed": "Impossibile interrogare il dispositivo USB" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_backup_json": "Backup JSON non valido" + }, + "flow_title": "{name}", + "step": { + "choose_automatic_backup": { + "data": { + "choose_automatic_backup": "Scegli un backup automatico" + }, + "description": "Ripristina le impostazioni di rete da un backup automatico", + "title": "Ripristina backup automatico" + }, + "choose_formation_strategy": { + "description": "Scegli le impostazioni di rete per la tua radio.", + "menu_options": { + "choose_automatic_backup": "Ripristina un backup automatico", + "form_new_network": "Cancella le impostazioni di rete e crea una nuova rete", + "reuse_settings": "Mantieni le impostazioni della rete radio", + "upload_manual_backup": "Carica un backup manuale" + }, + "title": "Formazione di rete" + }, + "choose_serial_port": { + "data": { + "path": "Percorso del dispositivo seriale" + }, + "description": "Seleziona la porta seriale per la tua radio Zigbee", + "title": "Seleziona una porta seriale" + }, + "init": { + "description": "ZHA verr\u00e0 interrotto. Vuoi continuare?", + "title": "Riconfigura ZHA" + }, + "manual_pick_radio_type": { + "data": { + "radio_type": "Tipo di radio" + }, + "description": "Scegli il tuo tipo di radio Zigbee", + "title": "Tipo di radio" + }, + "manual_port_config": { + "data": { + "baudrate": "velocit\u00e0 della porta", + "flow_control": "controllo del flusso di dati", + "path": "Percorso del dispositivo seriale" + }, + "description": "Inserire le impostazioni della porta seriale", + "title": "Impostazioni della porta seriale" + }, + "maybe_confirm_ezsp_restore": { + "data": { + "overwrite_coordinator_ieee": "Sostituire definitivamente l'indirizzo IEEE radio" + }, + "description": "Il tuo backup ha un indirizzo IEEE diverso dalla tua radio. Affinch\u00e9 la rete funzioni correttamente, \u00e8 necessario modificare anche l'indirizzo IEEE della radio. \n\nQuesta \u00e8 un'operazione permanente.", + "title": "Sovrascrivi indirizzo IEEE radio" + }, + "upload_manual_backup": { + "data": { + "uploaded_backup_file": "Carica un file" + }, + "description": "Ripristina le impostazioni di rete da un file JSON di backup caricato. Puoi scaricarne uno da un'installazione ZHA diversa da **Impostazioni di rete** o utilizzare un file Zigbee2MQTT `coordinator_backup.json`.", + "title": "Carica un backup manuale" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/translations/ja.json b/homeassistant/components/zha/translations/ja.json index 6cfd70056b6..9cfb8421c68 100644 --- a/homeassistant/components/zha/translations/ja.json +++ b/homeassistant/components/zha/translations/ja.json @@ -6,16 +6,63 @@ "usb_probe_failed": "USB\u30c7\u30d0\u30a4\u30b9\u3092\u63a2\u3057\u51fa\u3059\u3053\u3068\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "invalid_backup_json": "\u7121\u52b9\u306a\u30d0\u30c3\u30af\u30a2\u30c3\u30d7JSON" }, "flow_title": "{name}", "step": { + "choose_automatic_backup": { + "data": { + "choose_automatic_backup": "\u81ea\u52d5\u30d0\u30c3\u30af\u30a2\u30c3\u30d7\u3092\u9078\u629e\u3059\u308b" + }, + "description": "\u81ea\u52d5\u30d0\u30c3\u30af\u30a2\u30c3\u30d7\u304b\u3089\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u8a2d\u5b9a\u3092\u5fa9\u5143\u3059\u308b", + "title": "\u81ea\u52d5\u30d0\u30c3\u30af\u30a2\u30c3\u30d7\u306e\u5fa9\u5143" + }, + "choose_formation_strategy": { + "description": "\u7121\u7dda\u306e\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u8a2d\u5b9a\u3092\u9078\u629e\u3057\u307e\u3059\u3002", + "menu_options": { + "choose_automatic_backup": "\u81ea\u52d5\u30d0\u30c3\u30af\u30a2\u30c3\u30d7\u306e\u5fa9\u5143", + "form_new_network": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u8a2d\u5b9a\u3092\u6d88\u53bb\u3057\u3001\u65b0\u3057\u3044\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u3092\u5f62\u6210\u3059\u308b", + "reuse_settings": "\u7121\u7dda\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u8a2d\u5b9a\u3092\u4fdd\u6301", + "upload_manual_backup": "\u624b\u52d5\u30d0\u30c3\u30af\u30a2\u30c3\u30d7\u3092\u30a2\u30c3\u30d7\u30ed\u30fc\u30c9\u3059\u308b" + }, + "title": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u5f62\u6210" + }, + "choose_serial_port": { + "data": { + "path": "\u30b7\u30ea\u30a2\u30eb \u30c7\u30d0\u30a4\u30b9\u306e\u30d1\u30b9" + }, + "description": "Zigbee\u30e9\u30b8\u30aa\u306e\u30b7\u30ea\u30a2\u30eb\u30dd\u30fc\u30c8\u3092\u9078\u629e\u3057\u307e\u3059", + "title": "\u30b7\u30ea\u30a2\u30eb\u30dd\u30fc\u30c8\u306e\u9078\u629e" + }, "confirm": { "description": "{name} \u3092\u8a2d\u5b9a\u3057\u307e\u3059\u304b\uff1f" }, "confirm_hardware": { "description": "{name} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" }, + "manual_pick_radio_type": { + "data": { + "radio_type": "\u7121\u7dda\u30bf\u30a4\u30d7" + }, + "description": "Zigbee\u7121\u7dda\u6a5f\u306e\u30bf\u30a4\u30d7\u3092\u9078\u629e", + "title": "\u7121\u7dda\u30bf\u30a4\u30d7" + }, + "manual_port_config": { + "data": { + "baudrate": "\u30dd\u30fc\u30c8\u901f\u5ea6", + "flow_control": "\u30c7\u30fc\u30bf\u30d5\u30ed\u30fc\u30b3\u30f3\u30c8\u30ed\u30fc\u30eb", + "path": "\u30b7\u30ea\u30a2\u30eb \u30c7\u30d0\u30a4\u30b9\u306e\u30d1\u30b9" + }, + "description": "\u30b7\u30ea\u30a2\u30eb\u30dd\u30fc\u30c8\u8a2d\u5b9a\u306e\u5165\u529b", + "title": "\u30b7\u30ea\u30a2\u30eb\u30dd\u30fc\u30c8\u306e\u8a2d\u5b9a" + }, + "maybe_confirm_ezsp_restore": { + "data": { + "overwrite_coordinator_ieee": "\u7121\u7ddaIEEE \u30c9\u30ec\u30b9\u3092\u5b8c\u5168\u306b\u7f6e\u304d\u63db\u3048\u308b" + }, + "title": "\u7121\u7ddaIEEE\u30a2\u30c9\u30ec\u30b9\u306e\u4e0a\u66f8\u304d" + }, "pick_radio": { "data": { "radio_type": "\u7121\u7dda\u30bf\u30a4\u30d7" @@ -32,6 +79,12 @@ "description": "\u30dd\u30fc\u30c8\u56fa\u6709\u306e\u8a2d\u5b9a\u3092\u5165\u529b", "title": "\u8a2d\u5b9a" }, + "upload_manual_backup": { + "data": { + "uploaded_backup_file": "\u30d5\u30a1\u30a4\u30eb\u3092\u30a2\u30c3\u30d7\u30ed\u30fc\u30c9\u3059\u308b" + }, + "title": "\u624b\u52d5\u30d0\u30c3\u30af\u30a2\u30c3\u30d7\u3092\u30a2\u30c3\u30d7\u30ed\u30fc\u30c9\u3059\u308b" + }, "user": { "data": { "path": "\u30b7\u30ea\u30a2\u30eb \u30c7\u30d0\u30a4\u30b9\u306e\u30d1\u30b9" @@ -114,5 +167,75 @@ "remote_button_short_release": "\"{subtype}\" \u30dc\u30bf\u30f3\u304c\u30ea\u30ea\u30fc\u30b9\u3055\u308c\u307e\u3057\u305f", "remote_button_triple_press": "\"{subtype}\" \u30dc\u30bf\u30f3\u30923\u56de\u30af\u30ea\u30c3\u30af" } + }, + "options": { + "abort": { + "not_zha_device": "\u3053\u306e\u30c7\u30d0\u30a4\u30b9\u306fzha\u30c7\u30d0\u30a4\u30b9\u3067\u306f\u3042\u308a\u307e\u305b\u3093", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002", + "usb_probe_failed": "USB\u30c7\u30d0\u30a4\u30b9\u3092\u63a2\u3057\u51fa\u3059\u3053\u3068\u306b\u5931\u6557\u3057\u307e\u3057\u305f" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "invalid_backup_json": "\u7121\u52b9\u306a\u30d0\u30c3\u30af\u30a2\u30c3\u30d7JSON" + }, + "flow_title": "{name}", + "step": { + "choose_automatic_backup": { + "data": { + "choose_automatic_backup": "\u81ea\u52d5\u30d0\u30c3\u30af\u30a2\u30c3\u30d7\u3092\u9078\u629e\u3059\u308b" + }, + "description": "\u81ea\u52d5\u30d0\u30c3\u30af\u30a2\u30c3\u30d7\u304b\u3089\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u8a2d\u5b9a\u3092\u5fa9\u5143\u3059\u308b", + "title": "\u81ea\u52d5\u30d0\u30c3\u30af\u30a2\u30c3\u30d7\u306e\u5fa9\u5143" + }, + "choose_formation_strategy": { + "description": "\u7121\u7dda\u306e\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u8a2d\u5b9a\u3092\u9078\u629e\u3057\u307e\u3059\u3002", + "menu_options": { + "choose_automatic_backup": "\u81ea\u52d5\u30d0\u30c3\u30af\u30a2\u30c3\u30d7\u306e\u5fa9\u5143", + "form_new_network": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u8a2d\u5b9a\u3092\u6d88\u53bb\u3057\u3001\u65b0\u3057\u3044\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u3092\u5f62\u6210\u3059\u308b", + "reuse_settings": "\u7121\u7dda\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u8a2d\u5b9a\u3092\u4fdd\u6301", + "upload_manual_backup": "\u624b\u52d5\u30d0\u30c3\u30af\u30a2\u30c3\u30d7\u3092\u30a2\u30c3\u30d7\u30ed\u30fc\u30c9\u3059\u308b" + }, + "title": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u5f62\u6210" + }, + "choose_serial_port": { + "data": { + "path": "\u30b7\u30ea\u30a2\u30eb \u30c7\u30d0\u30a4\u30b9\u306e\u30d1\u30b9" + }, + "description": "Zigbee\u30e9\u30b8\u30aa\u306e\u30b7\u30ea\u30a2\u30eb\u30dd\u30fc\u30c8\u3092\u9078\u629e\u3057\u307e\u3059", + "title": "\u30b7\u30ea\u30a2\u30eb\u30dd\u30fc\u30c8\u306e\u9078\u629e" + }, + "init": { + "description": "ZHA\u3092\u505c\u6b62\u3057\u307e\u3059\u3002\u7d9a\u3051\u307e\u3059\u304b\uff1f", + "title": "ZHA\u306e\u518d\u8a2d\u5b9a" + }, + "manual_pick_radio_type": { + "data": { + "radio_type": "\u7121\u7dda\u30bf\u30a4\u30d7" + }, + "description": "Zigbee\u7121\u7dda\u6a5f\u306e\u30bf\u30a4\u30d7\u3092\u9078\u629e", + "title": "\u7121\u7dda\u30bf\u30a4\u30d7" + }, + "manual_port_config": { + "data": { + "baudrate": "\u30dd\u30fc\u30c8\u901f\u5ea6", + "flow_control": "\u30c7\u30fc\u30bf\u30d5\u30ed\u30fc\u30b3\u30f3\u30c8\u30ed\u30fc\u30eb", + "path": "\u30b7\u30ea\u30a2\u30eb \u30c7\u30d0\u30a4\u30b9\u306e\u30d1\u30b9" + }, + "description": "\u30b7\u30ea\u30a2\u30eb\u30dd\u30fc\u30c8\u8a2d\u5b9a\u306e\u5165\u529b", + "title": "\u30b7\u30ea\u30a2\u30eb\u30dd\u30fc\u30c8\u306e\u8a2d\u5b9a" + }, + "maybe_confirm_ezsp_restore": { + "data": { + "overwrite_coordinator_ieee": "\u7121\u7ddaIEEE \u30c9\u30ec\u30b9\u3092\u5b8c\u5168\u306b\u7f6e\u304d\u63db\u3048\u308b" + }, + "title": "\u7121\u7ddaIEEE\u30a2\u30c9\u30ec\u30b9\u306e\u4e0a\u66f8\u304d" + }, + "upload_manual_backup": { + "data": { + "uploaded_backup_file": "\u30d5\u30a1\u30a4\u30eb\u3092\u30a2\u30c3\u30d7\u30ed\u30fc\u30c9\u3059\u308b" + }, + "title": "\u624b\u52d5\u30d0\u30c3\u30af\u30a2\u30c3\u30d7\u3092\u30a2\u30c3\u30d7\u30ed\u30fc\u30c9\u3059\u308b" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/translations/no.json b/homeassistant/components/zha/translations/no.json index 72609918b23..c469eb54bd7 100644 --- a/homeassistant/components/zha/translations/no.json +++ b/homeassistant/components/zha/translations/no.json @@ -6,16 +6,64 @@ "usb_probe_failed": "Kunne ikke unders\u00f8ke usb -enheten" }, "error": { - "cannot_connect": "Tilkobling mislyktes" + "cannot_connect": "Tilkobling mislyktes", + "invalid_backup_json": "Ugyldig sikkerhetskopiering JSON" }, "flow_title": "{name}", "step": { + "choose_automatic_backup": { + "data": { + "choose_automatic_backup": "Velg en automatisk sikkerhetskopiering" + }, + "description": "Gjenopprette nettverksinnstillingene fra en automatisk sikkerhetskopiering", + "title": "Gjenopprett automatisk sikkerhetskopiering" + }, + "choose_formation_strategy": { + "description": "Velg nettverksinnstillingene for radioen.", + "menu_options": { + "choose_automatic_backup": "Gjenopprette en automatisk sikkerhetskopiering", + "form_new_network": "Slett nettverksinnstillinger og opprett et nytt nettverk", + "reuse_settings": "Behold innstillingene for radionettverk", + "upload_manual_backup": "Last opp en manuell sikkerhetskopiering" + }, + "title": "Nettverksdannelse" + }, + "choose_serial_port": { + "data": { + "path": "Bane til seriell enhet" + }, + "description": "Velg seriell port for din Zigbee-radio", + "title": "Velg en seriell port" + }, "confirm": { "description": "Vil du konfigurere {name}?" }, "confirm_hardware": { "description": "Vil du konfigurere {name} ?" }, + "manual_pick_radio_type": { + "data": { + "radio_type": "Radio type" + }, + "description": "Velg din Zigbee-radiotype", + "title": "Radio type" + }, + "manual_port_config": { + "data": { + "baudrate": "porthastighet", + "flow_control": "kontroll av dataflyt", + "path": "Bane til seriell enhet" + }, + "description": "Angi innstillingene for seriell port", + "title": "Innstillinger for seriell port" + }, + "maybe_confirm_ezsp_restore": { + "data": { + "overwrite_coordinator_ieee": "Erstatt radio-IEEE-adressen permanent" + }, + "description": "Sikkerhetskopien din har en annen IEEE-adresse enn radioen din. For at nettverket skal fungere ordentlig, b\u00f8r IEEE-adressen til radioen ogs\u00e5 endres. \n\n Dette er en permanent operasjon.", + "title": "Overskriv radio IEEE-adresse" + }, "pick_radio": { "data": { "radio_type": "Radio type" @@ -32,6 +80,13 @@ "description": "Angi portspesifikke innstillinger", "title": "Innstillinger" }, + "upload_manual_backup": { + "data": { + "uploaded_backup_file": "Last opp en fil" + }, + "description": "Gjenopprett nettverksinnstillingene fra en opplastet backup JSON-fil. Du kan laste ned en fra en annen ZHA-installasjon fra **Nettverksinnstillinger**, eller bruke en Zigbee2MQTT `coordinator_backup.json`-fil.", + "title": "Last opp en manuell sikkerhetskopiering" + }, "user": { "data": { "path": "Seriell enhetsbane" @@ -114,5 +169,77 @@ "remote_button_short_release": "\"{subtype}\"-knapp sluppet", "remote_button_triple_press": "\"{subtype}\"-knapp trykket p\u00e5 tre ganger" } + }, + "options": { + "abort": { + "not_zha_device": "Denne enheten er ikke en zha -enhet", + "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig.", + "usb_probe_failed": "Kunne ikke unders\u00f8ke usb -enheten" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_backup_json": "Ugyldig sikkerhetskopiering JSON" + }, + "flow_title": "{name}", + "step": { + "choose_automatic_backup": { + "data": { + "choose_automatic_backup": "Velg en automatisk sikkerhetskopiering" + }, + "description": "Gjenopprette nettverksinnstillingene fra en automatisk sikkerhetskopiering", + "title": "Gjenopprett automatisk sikkerhetskopiering" + }, + "choose_formation_strategy": { + "description": "Velg nettverksinnstillingene for radioen.", + "menu_options": { + "choose_automatic_backup": "Gjenopprette en automatisk sikkerhetskopiering", + "form_new_network": "Slett nettverksinnstillinger og opprett et nytt nettverk", + "reuse_settings": "Behold innstillingene for radionettverk", + "upload_manual_backup": "Last opp en manuell sikkerhetskopiering" + }, + "title": "Nettverksdannelse" + }, + "choose_serial_port": { + "data": { + "path": "Bane til seriell enhet" + }, + "description": "Velg seriell port for din Zigbee-radio", + "title": "Velg en seriell port" + }, + "init": { + "description": "ZHA vil bli stoppet. \u00d8nsker du \u00e5 fortsette?", + "title": "Konfigurer ZHA p\u00e5 nytt" + }, + "manual_pick_radio_type": { + "data": { + "radio_type": "Radio type" + }, + "description": "Velg din Zigbee-radiotype", + "title": "Radio type" + }, + "manual_port_config": { + "data": { + "baudrate": "porthastighet", + "flow_control": "kontroll av dataflyt", + "path": "Bane til seriell enhet" + }, + "description": "Angi innstillingene for seriell port", + "title": "Innstillinger for seriell port" + }, + "maybe_confirm_ezsp_restore": { + "data": { + "overwrite_coordinator_ieee": "Erstatt radio-IEEE-adressen permanent" + }, + "description": "Sikkerhetskopien din har en annen IEEE-adresse enn radioen din. For at nettverket skal fungere ordentlig, b\u00f8r IEEE-adressen til radioen ogs\u00e5 endres. \n\n Dette er en permanent operasjon.", + "title": "Overskriv radio IEEE-adresse" + }, + "upload_manual_backup": { + "data": { + "uploaded_backup_file": "Last opp en fil" + }, + "description": "Gjenopprett nettverksinnstillingene fra en opplastet backup JSON-fil. Du kan laste ned en fra en annen ZHA-installasjon fra **Nettverksinnstillinger**, eller bruke en Zigbee2MQTT `coordinator_backup.json`-fil.", + "title": "Last opp en manuell sikkerhetskopiering" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/translations/pt-BR.json b/homeassistant/components/zha/translations/pt-BR.json index 0d78cea3201..2ec2b97438a 100644 --- a/homeassistant/components/zha/translations/pt-BR.json +++ b/homeassistant/components/zha/translations/pt-BR.json @@ -172,10 +172,12 @@ }, "options": { "abort": { - "not_zha_device": "Este dispositivo n\u00e3o \u00e9 um dispositivo zha", + "not_zha_device": "Este dispositivo n\u00e3o \u00e9 um dispositivo ZHA", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel.", "usb_probe_failed": "Falha ao sondar o dispositivo usb" }, "error": { + "cannot_connect": "Falha ao conectar", "invalid_backup_json": "JSON de backup inv\u00e1lido" }, "flow_title": "{name}", @@ -228,7 +230,7 @@ "data": { "overwrite_coordinator_ieee": "Substituir permanentemente o endere\u00e7o IEEE do r\u00e1dio" }, - "description": "Seu backup tem um endere\u00e7o IEEE diferente do seu r\u00e1dio. Para que sua rede funcione corretamente, o endere\u00e7o IEEE do seu r\u00e1dio tamb\u00e9m deve ser alterado.\n\nEsta \u00e9 uma opera\u00e7\u00e3o permanente.", + "description": "Seu backup tem um endere\u00e7o IEEE diferente do seu r\u00e1dio. Para que sua rede funcione corretamente, o endere\u00e7o IEEE do seu r\u00e1dio tamb\u00e9m deve ser alterado. \n\n Esta \u00e9 uma opera\u00e7\u00e3o permanente.", "title": "Sobrescrever o endere\u00e7o IEEE do r\u00e1dio" }, "upload_manual_backup": { diff --git a/homeassistant/components/zha/translations/zh-Hant.json b/homeassistant/components/zha/translations/zh-Hant.json index a6c69311181..0fbd233bc60 100644 --- a/homeassistant/components/zha/translations/zh-Hant.json +++ b/homeassistant/components/zha/translations/zh-Hant.json @@ -6,16 +6,64 @@ "usb_probe_failed": "\u5075\u6e2c USB \u88dd\u7f6e\u5931\u6557" }, "error": { - "cannot_connect": "\u9023\u7dda\u5931\u6557" + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_backup_json": "\u7121\u6548\u5099\u4efd JSON" }, "flow_title": "{name}", "step": { + "choose_automatic_backup": { + "data": { + "choose_automatic_backup": "\u9078\u5247\u81ea\u52d5\u5099\u4efd" + }, + "description": "\u7531\u81ea\u52d5\u5099\u4efd\u4e2d\u56de\u5fa9\u7db2\u8def\u8a2d\u5b9a", + "title": "\u56de\u5fa9\u81ea\u52d5\u5099\u4efd" + }, + "choose_formation_strategy": { + "description": "\u9078\u64c7\u7121\u7dda\u96fb\u7684\u7db2\u8def\u8a2d\u5b9a", + "menu_options": { + "choose_automatic_backup": "\u56de\u5fa9\u81ea\u52d5\u5099\u4efd", + "form_new_network": "\u522a\u9664\u7db2\u8def\u8a2d\u5b9a\u4e26\u5efa\u7acb\u65b0\u7db2\u8def\u8a2d\u5b9a", + "reuse_settings": "\u4fdd\u7559\u7121\u7dda\u96fb\u7db2\u8def\u8a2d\u5b9a", + "upload_manual_backup": "\u4e0a\u50b3\u624b\u52d5\u5099\u4efd" + }, + "title": "\u7db2\u8def\u683c\u5f0f" + }, + "choose_serial_port": { + "data": { + "path": "\u5e8f\u5217\u88dd\u7f6e\u8def\u5f91" + }, + "description": "\u9078\u64c7 Zigbee \u7121\u7dda\u96fb\u5e8f\u5217\u57e0", + "title": "\u9078\u64c7\u5e8f\u5217\u57e0" + }, "confirm": { "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f" }, "confirm_hardware": { "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f" }, + "manual_pick_radio_type": { + "data": { + "radio_type": "\u7121\u7dda\u96fb\u985e\u5225" + }, + "description": "\u9078\u64c7 Zigbee \u7121\u7dda\u96fb\u985e\u5225", + "title": "\u7121\u7dda\u96fb\u985e\u5225" + }, + "manual_port_config": { + "data": { + "baudrate": "\u901a\u8a0a\u57e0\u901f\u5ea6", + "flow_control": "\u8cc7\u6599\u6d41\u91cf\u63a7\u5236", + "path": "\u5e8f\u5217\u88dd\u7f6e\u8def\u5f91" + }, + "description": "\u8f38\u5165\u5e8f\u5217\u57e0\u8a2d\u5b9a", + "title": "\u5e8f\u5217\u57e0\u8a2d\u5b9a" + }, + "maybe_confirm_ezsp_restore": { + "data": { + "overwrite_coordinator_ieee": "\u6c38\u4e45\u53d6\u4ee3\u7121\u7dda\u96fb IEEE \u4f4d\u5740" + }, + "description": "\u5099\u4efd\u4e2d\u7684 IEEE \u4f4d\u5740\u8207\u73fe\u6709\u7121\u7dda\u96fb\u4e0d\u540c\u3002\u70ba\u4e86\u78ba\u8a8d\u7db2\u8def\u6b63\u5e38\u5de5\u4f5c\uff0c\u7121\u7dda\u96fb\u7684 IEEE \u4f4d\u5740\u5fc5\u9808\u9032\u884c\u8b8a\u66f4\u3002\n\n\u6b64\u70ba\u6c38\u4e45\u6027\u64cd\u4f5c\u3002.", + "title": "\u8986\u5beb\u7121\u7dda\u96fb IEEE \u4f4d\u5740" + }, "pick_radio": { "data": { "radio_type": "\u7121\u7dda\u96fb\u985e\u5225" @@ -32,6 +80,13 @@ "description": "\u8f38\u5165\u901a\u8a0a\u57e0\u7279\u5b9a\u8a2d\u5b9a", "title": "\u8a2d\u5b9a" }, + "upload_manual_backup": { + "data": { + "uploaded_backup_file": "\u4e0a\u50b3\u6a94\u6848" + }, + "description": "\u7531\u4e0a\u50b3\u7684\u5099\u4efd JSON \u6a94\u6848\u4e2d\u56de\u5fa9\u7db2\u8def\u8a2d\u5b9a\u3002\u53ef\u4ee5\u7531\u4e0d\u540c\u7684 ZHA \u5b89\u88dd\u4e2d\u7684 **\u7db2\u8def\u8a2d\u5b9a** \u9032\u884c\u4e0b\u8f09\u3001\u6216\u4f7f\u7528 Zigbee2MQTT \u4e2d\u7684 `coordinator_backup.json` \u6a94\u6848\u3002", + "title": "\u4e0a\u50b3\u624b\u52d5\u5099\u4efd" + }, "user": { "data": { "path": "\u5e8f\u5217\u88dd\u7f6e\u8def\u5f91" @@ -114,5 +169,77 @@ "remote_button_short_release": "\"{subtype}\" \u6309\u9215\u5df2\u91cb\u653e", "remote_button_triple_press": "\"{subtype}\" \u6309\u9215\u4e09\u9023\u64ca" } + }, + "options": { + "abort": { + "not_zha_device": "\u6240\u767c\u73fe\u7684\u88dd\u7f6e\u4e26\u975e ZHA \u88dd\u7f6e", + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", + "usb_probe_failed": "\u5075\u6e2c USB \u88dd\u7f6e\u5931\u6557" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_backup_json": "\u7121\u6548\u5099\u4efd JSON" + }, + "flow_title": "{name}", + "step": { + "choose_automatic_backup": { + "data": { + "choose_automatic_backup": "\u9078\u5247\u81ea\u52d5\u5099\u4efd" + }, + "description": "\u7531\u81ea\u52d5\u5099\u4efd\u4e2d\u56de\u5fa9\u7db2\u8def\u8a2d\u5b9a", + "title": "\u56de\u5fa9\u81ea\u52d5\u5099\u4efd" + }, + "choose_formation_strategy": { + "description": "\u9078\u64c7\u7121\u7dda\u96fb\u7684\u7db2\u8def\u8a2d\u5b9a", + "menu_options": { + "choose_automatic_backup": "\u56de\u5fa9\u81ea\u52d5\u5099\u4efd", + "form_new_network": "\u522a\u9664\u7db2\u8def\u8a2d\u5b9a\u4e26\u5efa\u7acb\u65b0\u7db2\u8def\u8a2d\u5b9a", + "reuse_settings": "\u4fdd\u7559\u7121\u7dda\u96fb\u7db2\u8def\u8a2d\u5b9a", + "upload_manual_backup": "\u4e0a\u50b3\u624b\u52d5\u5099\u4efd" + }, + "title": "\u7db2\u8def\u683c\u5f0f" + }, + "choose_serial_port": { + "data": { + "path": "\u5e8f\u5217\u88dd\u7f6e\u8def\u5f91" + }, + "description": "\u9078\u64c7 Zigbee \u7121\u7dda\u96fb\u5e8f\u5217\u57e0", + "title": "\u9078\u64c7\u5e8f\u5217\u57e0" + }, + "init": { + "description": "ZHA \u5c07\u505c\u6b62\u3001\u662f\u5426\u8981\u7e7c\u7e8c\uff1f", + "title": "\u91cd\u65b0\u8a2d\u5b9a ZHA" + }, + "manual_pick_radio_type": { + "data": { + "radio_type": "\u7121\u7dda\u96fb\u985e\u5225" + }, + "description": "\u9078\u64c7 Zigbee \u7121\u7dda\u96fb\u985e\u5225", + "title": "\u7121\u7dda\u96fb\u985e\u5225" + }, + "manual_port_config": { + "data": { + "baudrate": "\u901a\u8a0a\u57e0\u901f\u5ea6", + "flow_control": "\u8cc7\u6599\u6d41\u91cf\u63a7\u5236", + "path": "\u5e8f\u5217\u88dd\u7f6e\u8def\u5f91" + }, + "description": "\u8f38\u5165\u5e8f\u5217\u57e0\u8a2d\u5b9a", + "title": "\u5e8f\u5217\u57e0\u8a2d\u5b9a" + }, + "maybe_confirm_ezsp_restore": { + "data": { + "overwrite_coordinator_ieee": "\u6c38\u4e45\u53d6\u4ee3\u7121\u7dda\u96fb IEEE \u4f4d\u5740" + }, + "description": "\u5099\u4efd\u4e2d\u7684 IEEE \u4f4d\u5740\u8207\u73fe\u6709\u7121\u7dda\u96fb\u4e0d\u540c\u3002\u70ba\u4e86\u78ba\u8a8d\u7db2\u8def\u6b63\u5e38\u5de5\u4f5c\uff0c\u7121\u7dda\u96fb\u7684 IEEE \u4f4d\u5740\u5fc5\u9808\u9032\u884c\u8b8a\u66f4\u3002\n\n\u6b64\u70ba\u6c38\u4e45\u6027\u64cd\u4f5c\u3002.", + "title": "\u8986\u5beb\u7121\u7dda\u96fb IEEE \u4f4d\u5740" + }, + "upload_manual_backup": { + "data": { + "uploaded_backup_file": "\u4e0a\u50b3\u6a94\u6848" + }, + "description": "\u7531\u4e0a\u50b3\u7684\u5099\u4efd JSON \u6a94\u6848\u4e2d\u56de\u5fa9\u7db2\u8def\u8a2d\u5b9a\u3002\u53ef\u4ee5\u7531\u4e0d\u540c\u7684 ZHA \u5b89\u88dd\u4e2d\u7684 **\u7db2\u8def\u8a2d\u5b9a** \u9032\u884c\u4e0b\u8f09\u3001\u6216\u4f7f\u7528 Zigbee2MQTT \u4e2d\u7684 `coordinator_backup.json` \u6a94\u6848\u3002", + "title": "\u4e0a\u50b3\u624b\u52d5\u5099\u4efd" + } + } } } \ No newline at end of file From e230a1f253f21fd868613177611d8e8c44d7ecf6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 2 Sep 2022 01:22:12 +0000 Subject: [PATCH 040/955] Bump bluetooth-adapters to 0.3.3 (#77683) --- 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 981b7854e36..9bc4c50a1e4 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -6,7 +6,7 @@ "quality_scale": "internal", "requirements": [ "bleak==0.16.0", - "bluetooth-adapters==0.3.2", + "bluetooth-adapters==0.3.3", "bluetooth-auto-recovery==0.3.0" ], "codeowners": ["@bdraco"], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 84edd18206c..56a6e5efd05 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ attrs==21.2.0 awesomeversion==22.8.0 bcrypt==3.1.7 bleak==0.16.0 -bluetooth-adapters==0.3.2 +bluetooth-adapters==0.3.3 bluetooth-auto-recovery==0.3.0 certifi>=2021.5.30 ciso8601==2.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 16f67f60115..f36995134b4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -427,7 +427,7 @@ blockchain==1.4.4 # bluepy==1.3.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.3.2 +bluetooth-adapters==0.3.3 # homeassistant.components.bluetooth bluetooth-auto-recovery==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7c725efce11..5fef48b1fe4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -338,7 +338,7 @@ blebox_uniapi==2.0.2 blinkpy==0.19.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.3.2 +bluetooth-adapters==0.3.3 # homeassistant.components.bluetooth bluetooth-auto-recovery==0.3.0 From 8eaadc0aaf9fe1d09957a5ea27ff6701727c3beb Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 1 Sep 2022 21:24:30 -0400 Subject: [PATCH 041/955] Bump frontend to 20220901.0 (#77689) --- 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 1bf8962d615..ebaa83f8d46 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==20220831.0"], + "requirements": ["home-assistant-frontend==20220901.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 56a6e5efd05..1fe755a9321 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -19,7 +19,7 @@ cryptography==37.0.4 fnvhash==0.1.0 hass-nabucasa==0.55.0 home-assistant-bluetooth==1.3.0 -home-assistant-frontend==20220831.0 +home-assistant-frontend==20220901.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index f36995134b4..ff853b243b0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -848,7 +848,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220831.0 +home-assistant-frontend==20220901.0 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5fef48b1fe4..3e10d0676da 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -625,7 +625,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220831.0 +home-assistant-frontend==20220901.0 # homeassistant.components.home_connect homeconnect==0.7.2 From 6b361b70c936f5c5bca536bd36fe37e7ac486a4f Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 2 Sep 2022 03:33:55 +0200 Subject: [PATCH 042/955] Move up setup of service to make it more robust when running multiple instances of deCONZ (#77621) --- homeassistant/components/deconz/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index cb4715f3c28..4750c40fab2 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -43,6 +43,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b except AuthenticationRequired as err: raise ConfigEntryAuthFailed from err + if not hass.data[DOMAIN]: + async_setup_services(hass) + gateway = hass.data[DOMAIN][config_entry.entry_id] = DeconzGateway( hass, config_entry, api ) @@ -53,9 +56,6 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b await async_setup_events(gateway) await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) - if len(hass.data[DOMAIN]) == 1: - async_setup_services(hass) - api.start() config_entry.async_on_unload( From 32e4a2515e2b42b2a5038e4a0199a389a44c282e Mon Sep 17 00:00:00 2001 From: amitfin Date: Fri, 2 Sep 2022 09:14:06 +0300 Subject: [PATCH 043/955] Time range should be treated as open ended (#77660) * Time range should be treated as open end * Refactored the logic of calculating the state * Improve tests * Improve tests Co-authored-by: Erik --- homeassistant/components/schedule/__init__.py | 27 ++- tests/components/schedule/test_init.py | 187 ++++++++++++++---- 2 files changed, 166 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/schedule/__init__.py b/homeassistant/components/schedule/__init__.py index 6ad3bcff58e..f5519e93c3f 100644 --- a/homeassistant/components/schedule/__init__.py +++ b/homeassistant/components/schedule/__init__.py @@ -291,14 +291,17 @@ class Schedule(Entity): todays_schedule = self._config.get(WEEKDAY_TO_CONF[now.weekday()], []) # Determine current schedule state - self._attr_state = next( - ( - STATE_ON - for time_range in todays_schedule - if time_range[CONF_FROM] <= now.time() <= time_range[CONF_TO] - ), - STATE_OFF, - ) + for time_range in todays_schedule: + # The current time should be greater or equal to CONF_FROM. + if now.time() < time_range[CONF_FROM]: + continue + # The current time should be smaller (and not equal) to CONF_TO. + # Note that any time in the day is treated as smaller than time.max. + if now.time() < time_range[CONF_TO] or time_range[CONF_TO] == time.max: + self._attr_state = STATE_ON + break + else: + self._attr_state = STATE_OFF # Find next event in the schedule, loop over each day (starting with # the current day) until the next event has been found. @@ -319,11 +322,15 @@ class Schedule(Entity): if next_event := next( ( possible_next_event - for time in times + for timestamp in times if ( possible_next_event := ( - datetime.combine(now.date(), time, tzinfo=now.tzinfo) + datetime.combine(now.date(), timestamp, tzinfo=now.tzinfo) + timedelta(days=day) + if not timestamp == time.max + # Special case for midnight of the following day. + else datetime.combine(now.date(), time(), tzinfo=now.tzinfo) + + timedelta(days=day + 1) ) ) > now diff --git a/tests/components/schedule/test_init.py b/tests/components/schedule/test_init.py index 5d5de581349..0eda21f5ba0 100644 --- a/tests/components/schedule/test_init.py +++ b/tests/components/schedule/test_init.py @@ -221,25 +221,14 @@ async def test_events_one_day( state = hass.states.get(f"{DOMAIN}.from_yaml") assert state - assert state.state == STATE_ON + assert state.state == STATE_OFF assert state.attributes[ATTR_NEXT_EVENT].isoformat() == "2022-09-11T07:00:00-07:00" -@pytest.mark.parametrize( - "sun_schedule, mon_schedule", - ( - ( - {CONF_FROM: "23:00:00", CONF_TO: "24:00:00"}, - {CONF_FROM: "00:00:00", CONF_TO: "01:00:00"}, - ), - ), -) -async def test_adjacent( +async def test_adjacent_cross_midnight( hass: HomeAssistant, schedule_setup: Callable[..., Coroutine[Any, Any, bool]], caplog: pytest.LogCaptureFixture, - sun_schedule: dict[str, str], - mon_schedule: dict[str, str], freezer, ) -> None: """Test adjacent events don't toggle on->off->on.""" @@ -251,8 +240,8 @@ async def test_adjacent( "from_yaml": { CONF_NAME: "from yaml", CONF_ICON: "mdi:party-popper", - CONF_SUNDAY: sun_schedule, - CONF_MONDAY: mon_schedule, + CONF_SUNDAY: {CONF_FROM: "23:00:00", CONF_TO: "24:00:00"}, + CONF_MONDAY: {CONF_FROM: "00:00:00", CONF_TO: "01:00:00"}, } } }, @@ -269,17 +258,6 @@ async def test_adjacent( freezer.move_to(state.attributes[ATTR_NEXT_EVENT]) async_fire_time_changed(hass) - state = hass.states.get(f"{DOMAIN}.from_yaml") - assert state - assert state.state == STATE_ON - assert ( - state.attributes[ATTR_NEXT_EVENT].isoformat() - == "2022-09-04T23:59:59.999999-07:00" - ) - - freezer.move_to(state.attributes[ATTR_NEXT_EVENT]) - async_fire_time_changed(hass) - state = hass.states.get(f"{DOMAIN}.from_yaml") assert state assert state.state == STATE_ON @@ -298,13 +276,149 @@ async def test_adjacent( state = hass.states.get(f"{DOMAIN}.from_yaml") assert state - assert state.state == STATE_ON + assert state.state == STATE_OFF assert state.attributes[ATTR_NEXT_EVENT].isoformat() == "2022-09-11T23:00:00-07:00" await hass.async_block_till_done() - assert len(state_changes) == 4 - for event in state_changes: + assert len(state_changes) == 3 + for event in state_changes[:-1]: assert event.data["new_state"].state == STATE_ON + assert state_changes[2].data["new_state"].state == STATE_OFF + + +async def test_adjacent_within_day( + hass: HomeAssistant, + schedule_setup: Callable[..., Coroutine[Any, Any, bool]], + caplog: pytest.LogCaptureFixture, + freezer, +) -> None: + """Test adjacent events don't toggle on->off->on.""" + freezer.move_to("2022-08-30 13:20:00-07:00") + + assert await schedule_setup( + config={ + DOMAIN: { + "from_yaml": { + CONF_NAME: "from yaml", + CONF_ICON: "mdi:party-popper", + CONF_SUNDAY: [ + {CONF_FROM: "22:00:00", CONF_TO: "22:30:00"}, + {CONF_FROM: "22:30:00", CONF_TO: "23:00:00"}, + ], + } + } + }, + items=[], + ) + + state = hass.states.get(f"{DOMAIN}.from_yaml") + assert state + assert state.state == STATE_OFF + assert state.attributes[ATTR_NEXT_EVENT].isoformat() == "2022-09-04T22:00:00-07:00" + + state_changes = async_capture_events(hass, EVENT_STATE_CHANGED) + + freezer.move_to(state.attributes[ATTR_NEXT_EVENT]) + async_fire_time_changed(hass) + + state = hass.states.get(f"{DOMAIN}.from_yaml") + assert state + assert state.state == STATE_ON + assert state.attributes[ATTR_NEXT_EVENT].isoformat() == "2022-09-04T22:30:00-07:00" + + freezer.move_to(state.attributes[ATTR_NEXT_EVENT]) + async_fire_time_changed(hass) + + state = hass.states.get(f"{DOMAIN}.from_yaml") + assert state + assert state.state == STATE_ON + assert state.attributes[ATTR_NEXT_EVENT].isoformat() == "2022-09-04T23:00:00-07:00" + + freezer.move_to(state.attributes[ATTR_NEXT_EVENT]) + async_fire_time_changed(hass) + + state = hass.states.get(f"{DOMAIN}.from_yaml") + assert state + assert state.state == STATE_OFF + assert state.attributes[ATTR_NEXT_EVENT].isoformat() == "2022-09-11T22:00:00-07:00" + + await hass.async_block_till_done() + assert len(state_changes) == 3 + for event in state_changes[:-1]: + assert event.data["new_state"].state == STATE_ON + assert state_changes[2].data["new_state"].state == STATE_OFF + + +async def test_non_adjacent_within_day( + hass: HomeAssistant, + schedule_setup: Callable[..., Coroutine[Any, Any, bool]], + caplog: pytest.LogCaptureFixture, + freezer, +) -> None: + """Test adjacent events don't toggle on->off->on.""" + freezer.move_to("2022-08-30 13:20:00-07:00") + + assert await schedule_setup( + config={ + DOMAIN: { + "from_yaml": { + CONF_NAME: "from yaml", + CONF_ICON: "mdi:party-popper", + CONF_SUNDAY: [ + {CONF_FROM: "22:00:00", CONF_TO: "22:15:00"}, + {CONF_FROM: "22:30:00", CONF_TO: "23:00:00"}, + ], + } + } + }, + items=[], + ) + + state = hass.states.get(f"{DOMAIN}.from_yaml") + assert state + assert state.state == STATE_OFF + assert state.attributes[ATTR_NEXT_EVENT].isoformat() == "2022-09-04T22:00:00-07:00" + + state_changes = async_capture_events(hass, EVENT_STATE_CHANGED) + + freezer.move_to(state.attributes[ATTR_NEXT_EVENT]) + async_fire_time_changed(hass) + + state = hass.states.get(f"{DOMAIN}.from_yaml") + assert state + assert state.state == STATE_ON + assert state.attributes[ATTR_NEXT_EVENT].isoformat() == "2022-09-04T22:15:00-07:00" + + freezer.move_to(state.attributes[ATTR_NEXT_EVENT]) + async_fire_time_changed(hass) + + state = hass.states.get(f"{DOMAIN}.from_yaml") + assert state + assert state.state == STATE_OFF + assert state.attributes[ATTR_NEXT_EVENT].isoformat() == "2022-09-04T22:30:00-07:00" + + freezer.move_to(state.attributes[ATTR_NEXT_EVENT]) + async_fire_time_changed(hass) + + state = hass.states.get(f"{DOMAIN}.from_yaml") + assert state + assert state.state == STATE_ON + assert state.attributes[ATTR_NEXT_EVENT].isoformat() == "2022-09-04T23:00:00-07:00" + + freezer.move_to(state.attributes[ATTR_NEXT_EVENT]) + async_fire_time_changed(hass) + + state = hass.states.get(f"{DOMAIN}.from_yaml") + assert state + assert state.state == STATE_OFF + assert state.attributes[ATTR_NEXT_EVENT].isoformat() == "2022-09-11T22:00:00-07:00" + + await hass.async_block_till_done() + assert len(state_changes) == 4 + assert state_changes[0].data["new_state"].state == STATE_ON + assert state_changes[1].data["new_state"].state == STATE_OFF + assert state_changes[2].data["new_state"].state == STATE_ON + assert state_changes[3].data["new_state"].state == STATE_OFF @pytest.mark.parametrize( @@ -348,17 +462,14 @@ async def test_to_midnight( state = hass.states.get(f"{DOMAIN}.from_yaml") assert state assert state.state == STATE_ON - assert ( - state.attributes[ATTR_NEXT_EVENT].isoformat() - == "2022-09-04T23:59:59.999999-07:00" - ) + assert state.attributes[ATTR_NEXT_EVENT].isoformat() == "2022-09-05T00:00:00-07:00" freezer.move_to(state.attributes[ATTR_NEXT_EVENT]) async_fire_time_changed(hass) state = hass.states.get(f"{DOMAIN}.from_yaml") assert state - assert state.state == STATE_ON + assert state.state == STATE_OFF assert state.attributes[ATTR_NEXT_EVENT].isoformat() == "2022-09-11T00:00:00-07:00" @@ -490,8 +601,8 @@ async def test_ws_delete( "to, next_event, saved_to", ( ("23:59:59", "2022-08-10T23:59:59-07:00", "23:59:59"), - ("24:00", "2022-08-10T23:59:59.999999-07:00", "24:00:00"), - ("24:00:00", "2022-08-10T23:59:59.999999-07:00", "24:00:00"), + ("24:00", "2022-08-11T00:00:00-07:00", "24:00:00"), + ("24:00:00", "2022-08-11T00:00:00-07:00", "24:00:00"), ), ) async def test_update( @@ -560,8 +671,8 @@ async def test_update( "to, next_event, saved_to", ( ("14:00:00", "2022-08-15T14:00:00-07:00", "14:00:00"), - ("24:00", "2022-08-15T23:59:59.999999-07:00", "24:00:00"), - ("24:00:00", "2022-08-15T23:59:59.999999-07:00", "24:00:00"), + ("24:00", "2022-08-16T00:00:00-07:00", "24:00:00"), + ("24:00:00", "2022-08-16T00:00:00-07:00", "24:00:00"), ), ) async def test_ws_create( From 8bab2a9bea7af06ea812268ddac60cfd7c093610 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 2 Sep 2022 08:39:27 +0200 Subject: [PATCH 044/955] Tweak schedule test (#77696) --- tests/components/schedule/test_init.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/tests/components/schedule/test_init.py b/tests/components/schedule/test_init.py index 0eda21f5ba0..92a82b974df 100644 --- a/tests/components/schedule/test_init.py +++ b/tests/components/schedule/test_init.py @@ -176,15 +176,10 @@ async def test_invalid_schedules( assert error in caplog.text -@pytest.mark.parametrize( - "schedule", - ({CONF_FROM: "07:00:00", CONF_TO: "11:00:00"},), -) async def test_events_one_day( hass: HomeAssistant, schedule_setup: Callable[..., Coroutine[Any, Any, bool]], caplog: pytest.LogCaptureFixture, - schedule: list[dict[str, str]], freezer, ) -> None: """Test events only during one day of the week.""" @@ -196,7 +191,7 @@ async def test_events_one_day( "from_yaml": { CONF_NAME: "from yaml", CONF_ICON: "mdi:party-popper", - CONF_SUNDAY: schedule, + CONF_SUNDAY: {CONF_FROM: "07:00:00", CONF_TO: "11:00:00"}, } } }, From 8924725d69493f1b5fc2636d9c1d261c8d37a327 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 2 Sep 2022 08:54:02 +0200 Subject: [PATCH 045/955] Improve some device registry tests (#77659) --- .../components/config/device_registry.py | 2 +- homeassistant/helpers/device_registry.py | 30 ++-- .../components/config/test_device_registry.py | 26 ++-- tests/helpers/test_device_registry.py | 145 ++++++++---------- 4 files changed, 94 insertions(+), 109 deletions(-) diff --git a/homeassistant/components/config/device_registry.py b/homeassistant/components/config/device_registry.py index 587710a8c2a..8edd9f1f4d3 100644 --- a/homeassistant/components/config/device_registry.py +++ b/homeassistant/components/config/device_registry.py @@ -160,6 +160,7 @@ def _entry_dict(entry): "connections": list(entry.connections), "disabled_by": entry.disabled_by, "entry_type": entry.entry_type, + "hw_version": entry.hw_version, "id": entry.id, "identifiers": list(entry.identifiers), "manufacturer": entry.manufacturer, @@ -167,6 +168,5 @@ def _entry_dict(entry): "name_by_user": entry.name_by_user, "name": entry.name, "sw_version": entry.sw_version, - "hw_version": entry.hw_version, "via_device_id": entry.via_device_id, } diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index f133eba8f92..c7825918752 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -83,6 +83,7 @@ class DeviceEntry: connections: set[tuple[str, str]] = attr.ib(converter=set, factory=set) disabled_by: DeviceEntryDisabler | None = attr.ib(default=None) entry_type: DeviceEntryType | None = attr.ib(default=None) + hw_version: str | None = attr.ib(default=None) id: str = attr.ib(factory=uuid_util.random_uuid_hex) identifiers: set[tuple[str, str]] = attr.ib(converter=set, factory=set) manufacturer: str | None = attr.ib(default=None) @@ -91,7 +92,6 @@ class DeviceEntry: name: str | None = attr.ib(default=None) suggested_area: str | None = attr.ib(default=None) sw_version: str | None = attr.ib(default=None) - hw_version: str | None = attr.ib(default=None) via_device_id: str | None = attr.ib(default=None) # This value is not stored, just used to keep track of events to fire. is_new: bool = attr.ib(default=False) @@ -318,13 +318,13 @@ class DeviceRegistry: # To disable a device if it gets created disabled_by: DeviceEntryDisabler | None | UndefinedType = UNDEFINED, entry_type: DeviceEntryType | None | UndefinedType = UNDEFINED, + hw_version: str | None | UndefinedType = UNDEFINED, identifiers: set[tuple[str, str]] | None = None, manufacturer: str | None | UndefinedType = UNDEFINED, model: str | None | UndefinedType = UNDEFINED, name: str | None | UndefinedType = UNDEFINED, suggested_area: str | None | UndefinedType = UNDEFINED, sw_version: str | None | UndefinedType = UNDEFINED, - hw_version: str | None | UndefinedType = UNDEFINED, via_device: tuple[str, str] | None = None, ) -> DeviceEntry: """Get device. Create if it doesn't exist.""" @@ -382,6 +382,7 @@ class DeviceRegistry: configuration_url=configuration_url, disabled_by=disabled_by, entry_type=entry_type, + hw_version=hw_version, manufacturer=manufacturer, merge_connections=connections or UNDEFINED, merge_identifiers=identifiers or UNDEFINED, @@ -389,7 +390,6 @@ class DeviceRegistry: name=name, suggested_area=suggested_area, sw_version=sw_version, - hw_version=hw_version, via_device_id=via_device_id, ) @@ -408,6 +408,7 @@ class DeviceRegistry: configuration_url: str | None | UndefinedType = UNDEFINED, disabled_by: DeviceEntryDisabler | None | UndefinedType = UNDEFINED, entry_type: DeviceEntryType | None | UndefinedType = UNDEFINED, + hw_version: str | None | UndefinedType = UNDEFINED, manufacturer: str | None | UndefinedType = UNDEFINED, merge_connections: set[tuple[str, str]] | UndefinedType = UNDEFINED, merge_identifiers: set[tuple[str, str]] | UndefinedType = UNDEFINED, @@ -418,7 +419,6 @@ class DeviceRegistry: remove_config_entry_id: str | UndefinedType = UNDEFINED, suggested_area: str | None | UndefinedType = UNDEFINED, sw_version: str | None | UndefinedType = UNDEFINED, - hw_version: str | None | UndefinedType = UNDEFINED, via_device_id: str | None | UndefinedType = UNDEFINED, ) -> DeviceEntry | None: """Update device attributes.""" @@ -492,17 +492,17 @@ class DeviceRegistry: old_values["identifiers"] = old.identifiers for attr_name, value in ( + ("area_id", area_id), ("configuration_url", configuration_url), ("disabled_by", disabled_by), ("entry_type", entry_type), + ("hw_version", hw_version), ("manufacturer", manufacturer), ("model", model), ("name", name), ("name_by_user", name_by_user), - ("area_id", area_id), ("suggested_area", suggested_area), ("sw_version", sw_version), - ("hw_version", hw_version), ("via_device_id", via_device_id), ): if value is not UNDEFINED and value != getattr(old, attr_name): @@ -584,6 +584,7 @@ class DeviceRegistry: entry_type=DeviceEntryType(device["entry_type"]) if device["entry_type"] else None, + hw_version=device["hw_version"], id=device["id"], identifiers={tuple(iden) for iden in device["identifiers"]}, # type: ignore[misc] manufacturer=device["manufacturer"], @@ -591,7 +592,6 @@ class DeviceRegistry: name_by_user=device["name_by_user"], name=device["name"], sw_version=device["sw_version"], - hw_version=device["hw_version"], via_device_id=device["via_device_id"], ) # Introduced in 0.111 @@ -617,25 +617,25 @@ class DeviceRegistry: @callback def _data_to_save(self) -> dict[str, list[dict[str, Any]]]: """Return data of device registry to store in a file.""" - data = {} + data: dict[str, list[dict[str, Any]]] = {} data["devices"] = [ { + "area_id": entry.area_id, "config_entries": list(entry.config_entries), + "configuration_url": entry.configuration_url, "connections": list(entry.connections), + "disabled_by": entry.disabled_by, + "entry_type": entry.entry_type, + "hw_version": entry.hw_version, + "id": entry.id, "identifiers": list(entry.identifiers), "manufacturer": entry.manufacturer, "model": entry.model, + "name_by_user": entry.name_by_user, "name": entry.name, "sw_version": entry.sw_version, - "hw_version": entry.hw_version, - "entry_type": entry.entry_type, - "id": entry.id, "via_device_id": entry.via_device_id, - "area_id": entry.area_id, - "name_by_user": entry.name_by_user, - "disabled_by": entry.disabled_by, - "configuration_url": entry.configuration_url, } for entry in self.devices.values() ] diff --git a/tests/components/config/test_device_registry.py b/tests/components/config/test_device_registry.py index f9289b6e3b3..4f47e463751 100644 --- a/tests/components/config/test_device_registry.py +++ b/tests/components/config/test_device_registry.py @@ -52,36 +52,36 @@ async def test_list_devices(hass, client, registry): assert msg["result"] == [ { + "area_id": None, "config_entries": ["1234"], + "configuration_url": None, "connections": [["ethernet", "12:34:56:78:90:AB:CD:EF"]], + "disabled_by": None, + "entry_type": None, + "hw_version": None, "identifiers": [["bridgeid", "0123"]], "manufacturer": "manufacturer", "model": "model", + "name_by_user": None, "name": None, "sw_version": None, - "hw_version": None, - "entry_type": None, "via_device_id": None, - "area_id": None, - "name_by_user": None, - "disabled_by": None, - "configuration_url": None, }, { + "area_id": None, "config_entries": ["1234"], + "configuration_url": None, "connections": [], + "disabled_by": None, + "entry_type": helpers_dr.DeviceEntryType.SERVICE, + "hw_version": None, "identifiers": [["bridgeid", "1234"]], "manufacturer": "manufacturer", "model": "model", + "name_by_user": None, "name": None, "sw_version": None, - "hw_version": None, - "entry_type": helpers_dr.DeviceEntryType.SERVICE, "via_device_id": dev1, - "area_id": None, - "name_by_user": None, - "disabled_by": None, - "configuration_url": None, }, ] @@ -104,8 +104,8 @@ async def test_list_devices(hass, client, registry): "identifiers": [["bridgeid", "0123"]], "manufacturer": "manufacturer", "model": "model", - "name": None, "name_by_user": None, + "name": None, "sw_version": None, "via_device_id": None, } diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index 38632ba6545..5f1827c9e6d 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -178,10 +178,11 @@ async def test_loading_from_storage(hass, hass_storage): { "area_id": "12345A", "config_entries": ["1234"], - "configuration_url": None, + "configuration_url": "configuration_url", "connections": [["Zigbee", "01.23.45.67.89"]], "disabled_by": device_registry.DeviceEntryDisabler.USER, "entry_type": device_registry.DeviceEntryType.SERVICE, + "hw_version": "hw_version", "id": "abcdefghijklm", "identifiers": [["serial", "12:34:56:AB:CD:EF"]], "manufacturer": "manufacturer", @@ -189,7 +190,6 @@ async def test_loading_from_storage(hass, hass_storage): "name_by_user": "Test Friendly Name", "name": "name", "sw_version": "version", - "hw_version": "hw_version", "via_device_id": None, } ], @@ -217,16 +217,28 @@ async def test_loading_from_storage(hass, hass_storage): manufacturer="manufacturer", model="model", ) - assert entry.id == "abcdefghijklm" - assert entry.area_id == "12345A" - assert entry.name_by_user == "Test Friendly Name" - assert entry.hw_version == "hw_version" - assert entry.entry_type is device_registry.DeviceEntryType.SERVICE - assert entry.disabled_by is device_registry.DeviceEntryDisabler.USER + assert entry == device_registry.DeviceEntry( + area_id="12345A", + config_entries={"1234"}, + configuration_url="configuration_url", + connections={("Zigbee", "01.23.45.67.89")}, + disabled_by=device_registry.DeviceEntryDisabler.USER, + entry_type=device_registry.DeviceEntryType.SERVICE, + hw_version="hw_version", + id="abcdefghijklm", + identifiers={("serial", "12:34:56:AB:CD:EF")}, + manufacturer="manufacturer", + model="model", + name_by_user="Test Friendly Name", + name="name", + suggested_area=None, # Not stored + sw_version="version", + ) assert isinstance(entry.config_entries, set) assert isinstance(entry.connections, set) assert isinstance(entry.identifiers, set) + # Restore a device, id should be reused from the deleted device entry entry = registry.async_get_or_create( config_entry_id="1234", connections={("Zigbee", "23.45.67.89.01")}, @@ -234,6 +246,14 @@ async def test_loading_from_storage(hass, hass_storage): manufacturer="manufacturer", model="model", ) + assert entry == device_registry.DeviceEntry( + config_entries={"1234"}, + connections={("Zigbee", "23.45.67.89.01")}, + id="bcdefghijklmn", + identifiers={("serial", "34:56:AB:CD:EF:12")}, + manufacturer="manufacturer", + model="model", + ) assert entry.id == "bcdefghijklmn" assert isinstance(entry.config_entries, set) assert isinstance(entry.connections, set) @@ -323,6 +343,7 @@ async def test_migration_1_1_to_1_3(hass, hass_storage): "connections": [["Zigbee", "01.23.45.67.89"]], "disabled_by": None, "entry_type": "service", + "hw_version": None, "id": "abcdefghijklm", "identifiers": [["serial", "12:34:56:AB:CD:EF"]], "manufacturer": "manufacturer", @@ -330,7 +351,6 @@ async def test_migration_1_1_to_1_3(hass, hass_storage): "name": "name", "name_by_user": None, "sw_version": "new_version", - "hw_version": None, "via_device_id": None, }, { @@ -340,6 +360,7 @@ async def test_migration_1_1_to_1_3(hass, hass_storage): "connections": [], "disabled_by": None, "entry_type": None, + "hw_version": None, "id": "invalid-entry-type", "identifiers": [["serial", "mock-id-invalid-entry"]], "manufacturer": None, @@ -347,7 +368,6 @@ async def test_migration_1_1_to_1_3(hass, hass_storage): "name_by_user": None, "name": None, "sw_version": None, - "hw_version": None, "via_device_id": None, }, ], @@ -386,8 +406,7 @@ async def test_migration_1_2_to_1_3(hass, hass_storage): "model": "model", "name": "name", "name_by_user": None, - "sw_version": "new_version", - "hw_version": None, + "sw_version": "version", "via_device_id": None, }, { @@ -404,7 +423,6 @@ async def test_migration_1_2_to_1_3(hass, hass_storage): "name_by_user": None, "name": None, "sw_version": None, - "hw_version": None, "via_device_id": None, }, ], @@ -428,7 +446,7 @@ async def test_migration_1_2_to_1_3(hass, hass_storage): config_entry_id="1234", connections={("Zigbee", "01.23.45.67.89")}, identifiers={("serial", "12:34:56:AB:CD:EF")}, - hw_version="new_version", + sw_version="new_version", ) assert entry.id == "abcdefghijklm" @@ -448,6 +466,7 @@ async def test_migration_1_2_to_1_3(hass, hass_storage): "connections": [["Zigbee", "01.23.45.67.89"]], "disabled_by": None, "entry_type": "service", + "hw_version": None, "id": "abcdefghijklm", "identifiers": [["serial", "12:34:56:AB:CD:EF"]], "manufacturer": "manufacturer", @@ -455,7 +474,6 @@ async def test_migration_1_2_to_1_3(hass, hass_storage): "name": "name", "name_by_user": None, "sw_version": "new_version", - "hw_version": "new_version", "via_device_id": None, }, { @@ -465,6 +483,7 @@ async def test_migration_1_2_to_1_3(hass, hass_storage): "connections": [], "disabled_by": None, "entry_type": None, + "hw_version": None, "id": "invalid-entry-type", "identifiers": [["serial", "mock-id-invalid-entry"]], "manufacturer": None, @@ -472,7 +491,6 @@ async def test_migration_1_2_to_1_3(hass, hass_storage): "name_by_user": None, "name": None, "sw_version": None, - "hw_version": None, "via_device_id": None, }, ], @@ -921,23 +939,40 @@ async def test_update(hass, registry, update_events): updated_entry = registry.async_update_device( entry.id, area_id="12345A", + configuration_url="configuration_url", + disabled_by=device_registry.DeviceEntryDisabler.USER, + entry_type=device_registry.DeviceEntryType.SERVICE, + hw_version="hw_version", manufacturer="Test Producer", model="Test Model", name_by_user="Test Friendly Name", + name="name", new_identifiers=new_identifiers, + suggested_area="suggested_area", + sw_version="version", via_device_id="98765B", - disabled_by=device_registry.DeviceEntryDisabler.USER, ) assert mock_save.call_count == 1 assert updated_entry != entry - assert updated_entry.area_id == "12345A" - assert updated_entry.manufacturer == "Test Producer" - assert updated_entry.model == "Test Model" - assert updated_entry.name_by_user == "Test Friendly Name" - assert updated_entry.identifiers == new_identifiers - assert updated_entry.via_device_id == "98765B" - assert updated_entry.disabled_by is device_registry.DeviceEntryDisabler.USER + assert updated_entry == device_registry.DeviceEntry( + area_id="12345A", + config_entries={"1234"}, + configuration_url="configuration_url", + connections={("mac", "12:34:56:ab:cd:ef")}, + disabled_by=device_registry.DeviceEntryDisabler.USER, + entry_type=device_registry.DeviceEntryType.SERVICE, + hw_version="hw_version", + id=entry.id, + identifiers={("bla", "321"), ("hue", "654")}, + manufacturer="Test Producer", + model="Test Model", + name_by_user="Test Friendly Name", + name="name", + suggested_area="suggested_area", + sw_version="version", + via_device_id="98765B", + ) assert registry.async_get_device({("hue", "456")}) is None assert registry.async_get_device({("bla", "123")}) is None @@ -964,11 +999,17 @@ async def test_update(hass, registry, update_events): assert update_events[1]["device_id"] == entry.id assert update_events[1]["changes"] == { "area_id": None, + "configuration_url": None, "disabled_by": None, + "entry_type": None, + "hw_version": None, "identifiers": {("bla", "123"), ("hue", "456")}, "manufacturer": None, "model": None, + "name": None, "name_by_user": None, + "suggested_area": None, + "sw_version": None, "via_device_id": None, } @@ -1036,62 +1077,6 @@ async def test_update_remove_config_entries(hass, registry, update_events): assert "changes" not in update_events[4] -async def test_update_sw_version(hass, registry, update_events): - """Verify that we can update software version of a device.""" - entry = registry.async_get_or_create( - config_entry_id="1234", - connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, - identifiers={("bla", "123")}, - ) - assert not entry.sw_version - sw_version = "0x20020263" - - with patch.object(registry, "async_schedule_save") as mock_save: - updated_entry = registry.async_update_device(entry.id, sw_version=sw_version) - - assert mock_save.call_count == 1 - assert updated_entry != entry - assert updated_entry.sw_version == sw_version - - await hass.async_block_till_done() - - assert len(update_events) == 2 - assert update_events[0]["action"] == "create" - assert update_events[0]["device_id"] == entry.id - assert "changes" not in update_events[0] - assert update_events[1]["action"] == "update" - assert update_events[1]["device_id"] == entry.id - assert update_events[1]["changes"] == {"sw_version": None} - - -async def test_update_hw_version(hass, registry, update_events): - """Verify that we can update hardware version of a device.""" - entry = registry.async_get_or_create( - config_entry_id="1234", - connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, - identifiers={("bla", "123")}, - ) - assert not entry.hw_version - hw_version = "0x20020263" - - with patch.object(registry, "async_schedule_save") as mock_save: - updated_entry = registry.async_update_device(entry.id, hw_version=hw_version) - - assert mock_save.call_count == 1 - assert updated_entry != entry - assert updated_entry.hw_version == hw_version - - await hass.async_block_till_done() - - assert len(update_events) == 2 - assert update_events[0]["action"] == "create" - assert update_events[0]["device_id"] == entry.id - assert "changes" not in update_events[0] - assert update_events[1]["action"] == "update" - assert update_events[1]["device_id"] == entry.id - assert update_events[1]["changes"] == {"hw_version": None} - - async def test_update_suggested_area(hass, registry, area_registry, update_events): """Verify that we can update the suggested area version of a device.""" entry = registry.async_get_or_create( From 1bc8770b51658f0dc1bd076b392d70be5a7433bc Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 2 Sep 2022 13:31:05 +0200 Subject: [PATCH 046/955] Remove area_id from entity_registry.async_get_or_create (#77700) * Remove area_id from entity_registry.async_get_or_create * Adjust tests * Fix lying comment in test --- homeassistant/helpers/entity_registry.py | 3 --- tests/components/cloud/test_google_config.py | 5 ++--- tests/components/google_assistant/test_smart_home.py | 4 +++- tests/helpers/test_entity_registry.py | 12 ++++-------- tests/helpers/test_template.py | 4 ++-- 5 files changed, 11 insertions(+), 17 deletions(-) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index d495d196440..cd1be43690d 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -327,7 +327,6 @@ class EntityRegistry: disabled_by: RegistryEntryDisabler | None = None, hidden_by: RegistryEntryHider | None = None, # Data that we want entry to have - area_id: str | None | UndefinedType = UNDEFINED, capabilities: Mapping[str, Any] | None | UndefinedType = UNDEFINED, config_entry: ConfigEntry | None | UndefinedType = UNDEFINED, device_id: str | None | UndefinedType = UNDEFINED, @@ -353,7 +352,6 @@ class EntityRegistry: if entity_id: return self.async_update_entity( entity_id, - area_id=area_id, capabilities=capabilities, config_entry_id=config_entry_id, device_id=device_id, @@ -404,7 +402,6 @@ class EntityRegistry: return None if value is UNDEFINED else value entry = RegistryEntry( - area_id=none_if_undefined(area_id), capabilities=none_if_undefined(capabilities), config_entry_id=none_if_undefined(config_entry_id), device_id=none_if_undefined(device_id), diff --git a/tests/components/cloud/test_google_config.py b/tests/components/cloud/test_google_config.py index a86f1f3cf8a..20bca347590 100644 --- a/tests/components/cloud/test_google_config.py +++ b/tests/components/cloud/test_google_config.py @@ -197,9 +197,8 @@ async def test_google_device_registry_sync(hass, mock_cloud_login, cloud_prefs): hass, GACTIONS_SCHEMA({}), "mock-user-id", cloud_prefs, hass.data["cloud"] ) ent_reg = er.async_get(hass) - entity_entry = ent_reg.async_get_or_create( - "light", "hue", "1234", device_id="1234", area_id="ABCD" - ) + entity_entry = ent_reg.async_get_or_create("light", "hue", "1234", device_id="1234") + entity_entry = ent_reg.async_update_entity(entity_entry.entity_id, area_id="ABCD") with patch.object(config, "async_sync_entities_all"): await config.async_initialize() diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 684a6db2640..eefa163bdd8 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -232,7 +232,9 @@ async def test_sync_in_area(area_on_device, hass, registries): "1235", suggested_object_id="demo_light", device_id=device.id, - area_id=area.id if not area_on_device else None, + ) + entity = registries.entity.async_update_entity( + entity.entity_id, area_id=area.id if not area_on_device else None ) light = DemoLight( diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index e4c371a0198..8ccba2c7ecc 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -73,7 +73,6 @@ def test_get_or_create_updates_data(registry): "light", "hue", "5678", - area_id="mock-area-id", capabilities={"max": 100}, config_entry=orig_config_entry, device_id="mock-dev-id", @@ -92,7 +91,6 @@ def test_get_or_create_updates_data(registry): "light.hue_5678", "5678", "hue", - area_id="mock-area-id", capabilities={"max": 100}, config_entry_id=orig_config_entry.entry_id, device_class=None, @@ -117,8 +115,7 @@ def test_get_or_create_updates_data(registry): "light", "hue", "5678", - area_id="new-mock-area-id", - capabilities={"new-max": 100}, + capabilities={"new-max": 150}, config_entry=new_config_entry, device_id="new-mock-dev-id", disabled_by=er.RegistryEntryDisabler.USER, @@ -136,8 +133,8 @@ def test_get_or_create_updates_data(registry): "light.hue_5678", "5678", "hue", - area_id="new-mock-area-id", - capabilities={"new-max": 100}, + area_id=None, + capabilities={"new-max": 150}, config_entry_id=new_config_entry.entry_id, device_class=None, device_id="new-mock-dev-id", @@ -159,7 +156,6 @@ def test_get_or_create_updates_data(registry): "light", "hue", "5678", - area_id=None, capabilities=None, config_entry=None, device_id=None, @@ -235,7 +231,6 @@ async def test_loading_saving_data(hass, registry): "light", "hue", "5678", - area_id="mock-area-id", capabilities={"max": 100}, config_entry=mock_config, device_id="mock-dev-id", @@ -251,6 +246,7 @@ async def test_loading_saving_data(hass, registry): ) registry.async_update_entity( orig_entry2.entity_id, + area_id="mock-area-id", device_class="user-class", name="User Name", icon="hass:user-icon", diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index fb57eff7685..3186c10b20e 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -2768,13 +2768,13 @@ async def test_area_entities(hass): assert info.rate_limit is None area_entry = area_registry.async_get_or_create("sensor.fake") - entity_registry.async_get_or_create( + entity_entry = entity_registry.async_get_or_create( "light", "hue", "5678", config_entry=config_entry, - area_id=area_entry.id, ) + entity_registry.async_update_entity(entity_entry.entity_id, area_id=area_entry.id) info = render_to_info(hass, f"{{{{ area_entities('{area_entry.id}') }}}}") assert_result_info(info, ["light.hue_5678"]) From 1acb9a981a5c7b9a012d6b18bad581fb84ac54c7 Mon Sep 17 00:00:00 2001 From: Avi Miller Date: Fri, 2 Sep 2022 22:13:03 +1000 Subject: [PATCH 047/955] Rename the binary sensor to better reflect its purpose (#77711) --- homeassistant/components/lifx/binary_sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lifx/binary_sensor.py b/homeassistant/components/lifx/binary_sensor.py index 4a368a2f97f..273ef035757 100644 --- a/homeassistant/components/lifx/binary_sensor.py +++ b/homeassistant/components/lifx/binary_sensor.py @@ -33,15 +33,15 @@ async def async_setup_entry( if lifx_features(coordinator.device)["hev"]: async_add_entities( [ - LIFXBinarySensorEntity( + LIFXHevCycleBinarySensorEntity( coordinator=coordinator, description=HEV_CYCLE_STATE_SENSOR ) ] ) -class LIFXBinarySensorEntity(LIFXEntity, BinarySensorEntity): - """LIFX sensor entity base class.""" +class LIFXHevCycleBinarySensorEntity(LIFXEntity, BinarySensorEntity): + """LIFX HEV cycle state binary sensor.""" _attr_has_entity_name = True From 2e4d5aca093e935a5826cc1fc8c2594af548adf8 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Fri, 2 Sep 2022 10:50:05 -0400 Subject: [PATCH 048/955] Change zwave_js firmware update service API key (#77719) * Change zwave_js firmware update service API key * Update const.py --- homeassistant/components/zwave_js/const.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index cd10109bb3d..ddd4917e596 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -123,4 +123,6 @@ ENTITY_DESC_KEY_TOTAL_INCREASING = "total_increasing" # This API key is only for use with Home Assistant. Reach out to Z-Wave JS to apply for # your own (https://github.com/zwave-js/firmware-updates/). -API_KEY_FIRMWARE_UPDATE_SERVICE = "b48e74337db217f44e1e003abb1e9144007d260a17e2b2422e0a45d0eaf6f4ad86f2a9943f17fee6dde343941f238a64" +API_KEY_FIRMWARE_UPDATE_SERVICE = ( + "55eea74f055bef2ad893348112df6a38980600aaf82d2b02011297fc7ba495f830ca2b70" +) From 2e34814d7a29c4f4eedb31e2f6adf6d557c7db76 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 2 Sep 2022 18:54:20 +0200 Subject: [PATCH 049/955] Fix reload of MQTT config entries (#76089) * Wait for unsubscribes * Spelling comment * Remove notify_all() during _register_mid() * Update homeassistant/components/mqtt/client.py Co-authored-by: Erik Montnemery * Correct handling reload manual set up MQTT items * Save and restore device trigger subscriptions * Clarify we are storing all remaining subscriptions Co-authored-by: Erik Montnemery --- homeassistant/components/mqtt/__init__.py | 26 +++++++++++++++++--- homeassistant/components/mqtt/client.py | 10 +++++--- homeassistant/components/mqtt/const.py | 1 + homeassistant/components/mqtt/mixins.py | 7 ++++++ tests/components/mqtt/test_device_trigger.py | 20 ++++++++++++++- 5 files changed, 56 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 1121377a30e..3ec9a7e9d4e 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -71,6 +71,7 @@ from .const import ( # noqa: F401 DATA_MQTT_RELOAD_DISPATCHERS, DATA_MQTT_RELOAD_ENTRY, DATA_MQTT_RELOAD_NEEDED, + DATA_MQTT_SUBSCRIPTIONS_TO_RESTORE, DATA_MQTT_UPDATED_CONFIG, DEFAULT_ENCODING, DEFAULT_QOS, @@ -315,6 +316,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return False hass.data[DATA_MQTT] = MQTT(hass, entry, conf) + # Restore saved subscriptions + if DATA_MQTT_SUBSCRIPTIONS_TO_RESTORE in hass.data: + hass.data[DATA_MQTT].subscriptions = hass.data.pop( + DATA_MQTT_SUBSCRIPTIONS_TO_RESTORE + ) entry.add_update_listener(_async_config_entry_updated) await hass.data[DATA_MQTT].async_connect() @@ -438,6 +444,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_forward_entry_setup_and_setup_discovery(config_entry): """Forward the config entry setup to the platforms and set up discovery.""" + reload_manual_setup: bool = False # Local import to avoid circular dependencies # pylint: disable-next=import-outside-toplevel from . import device_automation, tag @@ -460,8 +467,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await _async_setup_discovery(hass, conf, entry) # Setup reload service after all platforms have loaded await async_setup_reload_service() + # When the entry is reloaded, also reload manual set up items to enable MQTT + if DATA_MQTT_RELOAD_ENTRY in hass.data: + hass.data.pop(DATA_MQTT_RELOAD_ENTRY) + reload_manual_setup = True + + # When the entry was disabled before, reload manual set up items to enable MQTT again if DATA_MQTT_RELOAD_NEEDED in hass.data: hass.data.pop(DATA_MQTT_RELOAD_NEEDED) + reload_manual_setup = True + + if reload_manual_setup: await async_reload_manual_mqtt_items(hass) await async_forward_entry_setup_and_setup_discovery(entry) @@ -613,8 +629,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: mqtt_client.cleanup() # Trigger reload manual MQTT items at entry setup - # Reload the legacy yaml platform - await async_reload_integration_platforms(hass, DOMAIN, RELOADABLE_PLATFORMS) if (mqtt_entry_status := mqtt_config_entry_enabled(hass)) is False: # The entry is disabled reload legacy manual items when the entry is enabled again hass.data[DATA_MQTT_RELOAD_NEEDED] = True @@ -622,7 +636,13 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # The entry is reloaded: # Trigger re-fetching the yaml config at entry setup hass.data[DATA_MQTT_RELOAD_ENTRY] = True - # Stop the loop + # Reload the legacy yaml platform to make entities unavailable + await async_reload_integration_platforms(hass, DOMAIN, RELOADABLE_PLATFORMS) + # Wait for all ACKs and stop the loop await mqtt_client.async_disconnect() + # Store remaining subscriptions to be able to restore or reload them + # when the entry is set up again + if mqtt_client.subscriptions: + hass.data[DATA_MQTT_SUBSCRIPTIONS_TO_RESTORE] = mqtt_client.subscriptions return True diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index 192de624f17..57f51593ed4 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -309,7 +309,7 @@ class MQTT: def __init__( self, - hass: HomeAssistant, + hass, config_entry, conf, ) -> None: @@ -435,12 +435,13 @@ class MQTT: """Return False if there are unprocessed ACKs.""" return not bool(self._pending_operations) - # wait for ACK-s to be processesed (unsubscribe only) + # wait for ACKs to be processed async with self._pending_operations_condition: await self._pending_operations_condition.wait_for(no_more_acks) # stop the MQTT loop - await self.hass.async_add_executor_job(stop) + async with self._paho_lock: + await self.hass.async_add_executor_job(stop) async def async_subscribe( self, @@ -501,7 +502,8 @@ class MQTT: async with self._paho_lock: mid = await self.hass.async_add_executor_job(_client_unsubscribe, topic) await self._register_mid(mid) - self.hass.async_create_task(self._wait_for_mid(mid)) + + self.hass.async_create_task(self._wait_for_mid(mid)) async def _async_perform_subscriptions( self, subscriptions: Iterable[tuple[str, int]] diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index 6a5cb912fce..0c711e097d5 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -32,6 +32,7 @@ CONF_TLS_VERSION = "tls_version" CONFIG_ENTRY_IS_SETUP = "mqtt_config_entry_is_setup" DATA_MQTT = "mqtt" +DATA_MQTT_SUBSCRIPTIONS_TO_RESTORE = "mqtt_client_subscriptions" DATA_MQTT_CONFIG = "mqtt_config" MQTT_DATA_DEVICE_TRACKER_LEGACY = "mqtt_device_tracker_legacy" DATA_MQTT_RELOAD_DISPATCHERS = "mqtt_reload_dispatchers" diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index faef154dd84..75532f75c13 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -65,6 +65,7 @@ from .const import ( DATA_MQTT, DATA_MQTT_CONFIG, DATA_MQTT_RELOAD_DISPATCHERS, + DATA_MQTT_RELOAD_ENTRY, DATA_MQTT_UPDATED_CONFIG, DEFAULT_ENCODING, DEFAULT_PAYLOAD_AVAILABLE, @@ -363,6 +364,12 @@ async def async_setup_platform_helper( async_setup_entities: SetupEntity, ) -> None: """Help to set up the platform for manual configured MQTT entities.""" + if DATA_MQTT_RELOAD_ENTRY in hass.data: + _LOGGER.debug( + "MQTT integration is %s, skipping setup of manually configured MQTT items while unloading the config entry", + platform_domain, + ) + return if not (entry_status := mqtt_config_entry_enabled(hass)): _LOGGER.warning( "MQTT integration is %s, skipping setup of manually configured MQTT %s", diff --git a/tests/components/mqtt/test_device_trigger.py b/tests/components/mqtt/test_device_trigger.py index 8363ca34fd7..661075c3cbe 100644 --- a/tests/components/mqtt/test_device_trigger.py +++ b/tests/components/mqtt/test_device_trigger.py @@ -4,6 +4,7 @@ from unittest.mock import patch import pytest +from homeassistant import config as hass_config import homeassistant.components.automation as automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.mqtt import _LOGGER, DOMAIN, debug_info @@ -1425,7 +1426,24 @@ async def test_unload_entry(hass, calls, device_reg, mqtt_mock, tmp_path) -> Non await help_test_unload_config_entry(hass, tmp_path, {}) - # Fake short press 2 + # Rediscover message and fake short press 2 (non impact) + async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", data1) + await hass.async_block_till_done() async_fire_mqtt_message(hass, "foobar/triggers/button1", "short_press") await hass.async_block_till_done() assert len(calls) == 1 + + mqtt_entry = hass.config_entries.async_entries("mqtt")[0] + + # Load the entry again + new_yaml_config_file = tmp_path / "configuration.yaml" + new_yaml_config_file.write_text("") + with patch.object(hass_config, "YAML_CONFIG_FILE", new_yaml_config_file): + await mqtt_entry.async_setup(hass) + + # Rediscover and fake short press 3 + async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", data1) + await hass.async_block_till_done() + async_fire_mqtt_message(hass, "foobar/triggers/button1", "short_press") + await hass.async_block_till_done() + assert len(calls) == 2 From 3a86209dec8ded9f4539bc661ac42ddf458c6e06 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 2 Sep 2022 19:02:14 +0200 Subject: [PATCH 050/955] Remove unnecessary use of dunder methods from entity registry (#77716) --- homeassistant/helpers/entity_registry.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index cd1be43690d..701b0816d1c 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -185,7 +185,7 @@ class EntityRegistryItems(UserDict[str, "RegistryEntry"]): Maintains two additional indexes: - id -> entry - - (domain, platform, unique_id) -> entry + - (domain, platform, unique_id) -> entity_id """ def __init__(self) -> None: @@ -201,14 +201,14 @@ class EntityRegistryItems(UserDict[str, "RegistryEntry"]): del self._entry_ids[old_entry.id] del self._index[(old_entry.domain, old_entry.platform, old_entry.unique_id)] super().__setitem__(key, entry) - self._entry_ids.__setitem__(entry.id, entry) + self._entry_ids[entry.id] = entry self._index[(entry.domain, entry.platform, entry.unique_id)] = entry.entity_id def __delitem__(self, key: str) -> None: """Remove an item.""" entry = self[key] - self._entry_ids.__delitem__(entry.id) - self._index.__delitem__((entry.domain, entry.platform, entry.unique_id)) + del self._entry_ids[entry.id] + del self._index[(entry.domain, entry.platform, entry.unique_id)] super().__delitem__(key) def get_entity_id(self, key: tuple[str, str, str]) -> str | None: From e4e29aa29de1bb36adc1464135e517d8ad80ca75 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 2 Sep 2022 20:17:35 +0000 Subject: [PATCH 051/955] Bump bluetooth-adapters to 3.3.4 (#77705) --- 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 9bc4c50a1e4..b98312040f0 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -6,7 +6,7 @@ "quality_scale": "internal", "requirements": [ "bleak==0.16.0", - "bluetooth-adapters==0.3.3", + "bluetooth-adapters==0.3.4", "bluetooth-auto-recovery==0.3.0" ], "codeowners": ["@bdraco"], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 1fe755a9321..8cfb71c5db0 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ attrs==21.2.0 awesomeversion==22.8.0 bcrypt==3.1.7 bleak==0.16.0 -bluetooth-adapters==0.3.3 +bluetooth-adapters==0.3.4 bluetooth-auto-recovery==0.3.0 certifi>=2021.5.30 ciso8601==2.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index ff853b243b0..9898b53f4a4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -427,7 +427,7 @@ blockchain==1.4.4 # bluepy==1.3.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.3.3 +bluetooth-adapters==0.3.4 # homeassistant.components.bluetooth bluetooth-auto-recovery==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3e10d0676da..e349afbabaf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -338,7 +338,7 @@ blebox_uniapi==2.0.2 blinkpy==0.19.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.3.3 +bluetooth-adapters==0.3.4 # homeassistant.components.bluetooth bluetooth-auto-recovery==0.3.0 From 916c44b5b4d69eebba83241829b55d1815ee0b57 Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Fri, 2 Sep 2022 14:18:10 -0600 Subject: [PATCH 052/955] Adjust litterrobot platform loading/unloading (#77682) Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- .../components/litterrobot/__init__.py | 57 +++++++------------ tests/components/litterrobot/conftest.py | 4 +- tests/components/litterrobot/test_vacuum.py | 9 ++- 3 files changed, 30 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/litterrobot/__init__.py b/homeassistant/components/litterrobot/__init__.py index d302989fc01..742e9dcb9c7 100644 --- a/homeassistant/components/litterrobot/__init__.py +++ b/homeassistant/components/litterrobot/__init__.py @@ -1,7 +1,7 @@ """The Litter-Robot integration.""" from __future__ import annotations -from pylitterbot import FeederRobot, LitterRobot, LitterRobot3, LitterRobot4 +from pylitterbot import FeederRobot, LitterRobot, LitterRobot3, Robot from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform @@ -10,65 +10,48 @@ from homeassistant.core import HomeAssistant from .const import DOMAIN from .hub import LitterRobotHub -PLATFORMS = [ - Platform.BUTTON, - Platform.SELECT, - Platform.SENSOR, - Platform.SWITCH, - Platform.VACUUM, -] - PLATFORMS_BY_TYPE = { - LitterRobot: ( - Platform.SELECT, - Platform.SENSOR, - Platform.SWITCH, - Platform.VACUUM, - ), - LitterRobot3: ( - Platform.BUTTON, - Platform.SELECT, - Platform.SENSOR, - Platform.SWITCH, - Platform.VACUUM, - ), - LitterRobot4: ( - Platform.SELECT, - Platform.SENSOR, - Platform.SWITCH, - Platform.VACUUM, - ), - FeederRobot: ( - Platform.BUTTON, + Robot: ( Platform.SELECT, Platform.SENSOR, Platform.SWITCH, ), + LitterRobot: (Platform.VACUUM,), + LitterRobot3: (Platform.BUTTON,), + FeederRobot: (Platform.BUTTON,), } +def get_platforms_for_robots(robots: list[Robot]) -> set[Platform]: + """Get platforms for robots.""" + return { + platform + for robot in robots + for robot_type, platforms in PLATFORMS_BY_TYPE.items() + if isinstance(robot, robot_type) + for platform in platforms + } + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Litter-Robot from a config entry.""" hass.data.setdefault(DOMAIN, {}) hub = hass.data[DOMAIN][entry.entry_id] = LitterRobotHub(hass, entry.data) await hub.login(load_robots=True) - platforms: set[str] = set() - for robot in hub.account.robots: - platforms.update(PLATFORMS_BY_TYPE[type(robot)]) - if platforms: + if platforms := get_platforms_for_robots(hub.account.robots): 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.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - hub: LitterRobotHub = hass.data[DOMAIN][entry.entry_id] await hub.account.disconnect() + platforms = get_platforms_for_robots(hub.account.robots) + unload_ok = await hass.config_entries.async_unload_platforms(entry, platforms) + if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/tests/components/litterrobot/conftest.py b/tests/components/litterrobot/conftest.py index d5d29e12988..e5d5e730b61 100644 --- a/tests/components/litterrobot/conftest.py +++ b/tests/components/litterrobot/conftest.py @@ -99,8 +99,8 @@ async def setup_integration( with patch( "homeassistant.components.litterrobot.hub.Account", return_value=mock_account ), patch( - "homeassistant.components.litterrobot.PLATFORMS", - [platform_domain] if platform_domain else [], + "homeassistant.components.litterrobot.PLATFORMS_BY_TYPE", + {Robot: (platform_domain,)} if platform_domain else {}, ): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/litterrobot/test_vacuum.py b/tests/components/litterrobot/test_vacuum.py index eb9a4c8c60b..08aa8b2399b 100644 --- a/tests/components/litterrobot/test_vacuum.py +++ b/tests/components/litterrobot/test_vacuum.py @@ -52,6 +52,7 @@ async def test_vacuum(hass: HomeAssistant, mock_account: MagicMock) -> None: assert ent_reg_entry.unique_id == VACUUM_UNIQUE_ID_OLD await setup_integration(hass, mock_account, PLATFORM_DOMAIN) + assert len(ent_reg.entities) == 1 assert hass.services.has_service(DOMAIN, SERVICE_SET_SLEEP_MODE) vacuum = hass.states.get(VACUUM_ENTITY_ID) @@ -78,10 +79,16 @@ async def test_no_robots( hass: HomeAssistant, mock_account_with_no_robots: MagicMock ) -> None: """Tests the vacuum entity was set up.""" - await setup_integration(hass, mock_account_with_no_robots, PLATFORM_DOMAIN) + entry = await setup_integration(hass, mock_account_with_no_robots, PLATFORM_DOMAIN) assert not hass.services.has_service(DOMAIN, SERVICE_SET_SLEEP_MODE) + ent_reg = er.async_get(hass) + assert len(ent_reg.entities) == 0 + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + async def test_vacuum_with_error( hass: HomeAssistant, mock_account_with_error: MagicMock From 51c5f1d16a09fa9859bfa0b93875914aa5be7fbb Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 2 Sep 2022 22:44:42 +0200 Subject: [PATCH 053/955] Remove useless device_registry test (#77714) --- tests/helpers/test_device_registry.py | 30 --------------------------- 1 file changed, 30 deletions(-) diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index 5f1827c9e6d..2c9a7956874 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -667,36 +667,6 @@ async def test_removing_area_id(registry): assert entry_w_area != entry_wo_area -async def test_deleted_device_removing_area_id(registry): - """Make sure we can clear area id of deleted device.""" - entry = registry.async_get_or_create( - config_entry_id="123", - connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, - identifiers={("bridgeid", "0123")}, - manufacturer="manufacturer", - model="model", - ) - - entry_w_area = registry.async_update_device(entry.id, area_id="12345A") - - registry.async_remove_device(entry.id) - registry.async_clear_area_id("12345A") - - entry2 = registry.async_get_or_create( - config_entry_id="123", - connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, - identifiers={("bridgeid", "0123")}, - manufacturer="manufacturer", - model="model", - ) - assert entry.id == entry2.id - - entry_wo_area = registry.async_get_device({("bridgeid", "0123")}) - - assert not entry_wo_area.area_id - assert entry_w_area != entry_wo_area - - async def test_specifying_via_device_create(registry): """Test specifying a via_device and removal of the hub device.""" via = registry.async_get_or_create( From b49d47a559cf25f194eda931b0140a8399752c69 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 2 Sep 2022 23:50:52 +0200 Subject: [PATCH 054/955] Register xiaomi_miio unload callbacks later in setup (#76714) --- homeassistant/components/xiaomi_miio/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/__init__.py b/homeassistant/components/xiaomi_miio/__init__.py index 9f0be1da528..8719319aec8 100644 --- a/homeassistant/components/xiaomi_miio/__init__.py +++ b/homeassistant/components/xiaomi_miio/__init__.py @@ -387,8 +387,6 @@ async def async_setup_gateway_entry(hass: HomeAssistant, entry: ConfigEntry) -> if gateway_id.endswith("-gateway"): hass.config_entries.async_update_entry(entry, unique_id=entry.data["mac"]) - entry.async_on_unload(entry.add_update_listener(update_listener)) - # Connect to gateway gateway = ConnectXiaomiGateway(hass, entry) try: @@ -444,6 +442,8 @@ async def async_setup_gateway_entry(hass: HomeAssistant, entry: ConfigEntry) -> await hass.config_entries.async_forward_entry_setups(entry, GATEWAY_PLATFORMS) + entry.async_on_unload(entry.add_update_listener(update_listener)) + async def async_setup_device_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up the Xiaomi Miio device component from a config entry.""" @@ -453,10 +453,10 @@ async def async_setup_device_entry(hass: HomeAssistant, entry: ConfigEntry) -> b if not platforms: return False - entry.async_on_unload(entry.add_update_listener(update_listener)) - await hass.config_entries.async_forward_entry_setups(entry, platforms) + entry.async_on_unload(entry.add_update_listener(update_listener)) + return True From 945299dfb6533dc8fdfd99babc78c9ea646f58a5 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 3 Sep 2022 00:24:50 +0000 Subject: [PATCH 055/955] [ci skip] Translation update --- .../components/bluetooth/translations/ru.json | 4 +- .../components/ecowitt/translations/fr.json | 3 +- .../components/ecowitt/translations/ru.json | 20 ++++ .../components/icloud/translations/ru.json | 7 ++ .../components/melnor/translations/ru.json | 13 +++ .../components/overkiz/translations/fr.json | 3 +- .../components/overkiz/translations/ru.json | 3 +- .../components/sensor/translations/fr.json | 1 + .../components/sensor/translations/hu.json | 2 + .../components/sensor/translations/no.json | 2 + .../components/sensor/translations/ru.json | 2 + .../sensor/translations/zh-Hant.json | 2 + .../components/sensorpro/translations/ru.json | 22 ++++ .../volvooncall/translations/fr.json | 3 +- .../volvooncall/translations/ru.json | 3 +- .../components/zha/translations/ru.json | 108 ++++++++++++++++-- 16 files changed, 181 insertions(+), 17 deletions(-) create mode 100644 homeassistant/components/ecowitt/translations/ru.json create mode 100644 homeassistant/components/melnor/translations/ru.json create mode 100644 homeassistant/components/sensorpro/translations/ru.json diff --git a/homeassistant/components/bluetooth/translations/ru.json b/homeassistant/components/bluetooth/translations/ru.json index 3270f8b840d..5b2029bb8b0 100644 --- a/homeassistant/components/bluetooth/translations/ru.json +++ b/homeassistant/components/bluetooth/translations/ru.json @@ -2,7 +2,7 @@ "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.", - "no_adapters": "\u0410\u0434\u0430\u043f\u0442\u0435\u0440\u044b Bluetooth \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b." + "no_adapters": "\u041d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e \u043d\u0435\u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u043d\u044b\u0445 \u0430\u0434\u0430\u043f\u0442\u0435\u0440\u043e\u0432 Bluetooth." }, "flow_title": "{name}", "step": { @@ -34,7 +34,7 @@ "init": { "data": { "adapter": "\u0410\u0434\u0430\u043f\u0442\u0435\u0440 Bluetooth, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0431\u0443\u0434\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0434\u043b\u044f \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f", - "passive": "\u041f\u0430\u0441\u0441\u0438\u0432\u043d\u043e\u0435 \u0441\u043b\u0443\u0448\u0430\u043d\u0438\u0435" + "passive": "\u041f\u0430\u0441\u0441\u0438\u0432\u043d\u043e\u0435 \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435" }, "description": "\u0414\u043b\u044f \u043f\u0430\u0441\u0441\u0438\u0432\u043d\u043e\u0433\u043e \u043f\u0440\u043e\u0441\u043b\u0443\u0448\u0438\u0432\u0430\u043d\u0438\u044f \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f BlueZ 5.63 \u0438\u043b\u0438 \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0437\u0434\u043d\u044f\u044f \u0432\u0435\u0440\u0441\u0438\u044f \u0441 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044b\u043c\u0438 \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u0430\u043b\u044c\u043d\u044b\u043c\u0438 \u0444\u0443\u043d\u043a\u0446\u0438\u044f\u043c\u0438." } diff --git a/homeassistant/components/ecowitt/translations/fr.json b/homeassistant/components/ecowitt/translations/fr.json index 1cb6ad07d2d..c4f3bfbb937 100644 --- a/homeassistant/components/ecowitt/translations/fr.json +++ b/homeassistant/components/ecowitt/translations/fr.json @@ -8,7 +8,8 @@ "user": { "data": { "port": "Port d'\u00e9coute" - } + }, + "description": "Voulez-vous vraiment configurer Ecowitt\u00a0?" } } } diff --git a/homeassistant/components/ecowitt/translations/ru.json b/homeassistant/components/ecowitt/translations/ru.json new file mode 100644 index 00000000000..97532b0726b --- /dev/null +++ b/homeassistant/components/ecowitt/translations/ru.json @@ -0,0 +1,20 @@ +{ + "config": { + "create_entry": { + "default": "\u0427\u0442\u043e\u0431\u044b \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 Ecowitt (\u043d\u0430 \u0442\u0435\u043b\u0435\u0444\u043e\u043d\u0435) \u0438\u043b\u0438 \u0432\u043e\u0439\u0434\u0438\u0442\u0435 \u0432 \u0432\u0435\u0431-\u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 Ecowitt \u0432 \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0435 \u043f\u043e IP-\u0430\u0434\u0440\u0435\u0441\u0443 \u0441\u0442\u0430\u043d\u0446\u0438\u0438. \n\n\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u0432\u043e\u044e \u0441\u0442\u0430\u043d\u0446\u0438\u044e - > \u041c\u0435\u043d\u044e 'Others' - > 'DIY Upload Servers'. \u041d\u0430\u0436\u043c\u0438\u0442\u0435 'Next' \u0438 \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 'Customized'. \n\n- IP-\u0430\u0434\u0440\u0435\u0441 \u0441\u0435\u0440\u0432\u0435\u0440\u0430: `{server}`\n- \u041f\u0443\u0442\u044c: `{path}`\n- \u041f\u043e\u0440\u0442: `{port}` \n\n\u041d\u0430\u0436\u043c\u0438\u0442\u0435 'Save'." + }, + "error": { + "invalid_port": "\u041f\u043e\u0440\u0442 \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\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." + }, + "step": { + "user": { + "data": { + "path": "\u041f\u0443\u0442\u044c \u0441 \u0442\u043e\u043a\u0435\u043d\u043e\u043c \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u0438", + "port": "\u041f\u043e\u0440\u0442 \u043f\u0440\u043e\u0441\u043b\u0443\u0448\u0438\u0432\u0430\u043d\u0438\u044f" + }, + "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Ecowitt?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/translations/ru.json b/homeassistant/components/icloud/translations/ru.json index f3f85630215..17acded5703 100644 --- a/homeassistant/components/icloud/translations/ru.json +++ b/homeassistant/components/icloud/translations/ru.json @@ -18,6 +18,13 @@ "description": "\u0420\u0430\u043d\u0435\u0435 \u0432\u0432\u0435\u0434\u0435\u043d\u043d\u044b\u0439 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f {username} \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442. \u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0443\u0439\u0442\u0435\u0441\u044c, \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0438\u0442\u044c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u044d\u0442\u0443 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044e.", "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" }, + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u0420\u0430\u043d\u0435\u0435 \u0432\u0432\u0435\u0434\u0435\u043d\u043d\u044b\u0439 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f {username} \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442. \u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0443\u0439\u0442\u0435\u0441\u044c, \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0438\u0442\u044c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u044d\u0442\u0443 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044e.", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" + }, "trusted_device": { "data": { "trusted_device": "\u0414\u043e\u0432\u0435\u0440\u0435\u043d\u043d\u043e\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" diff --git a/homeassistant/components/melnor/translations/ru.json b/homeassistant/components/melnor/translations/ru.json new file mode 100644 index 00000000000..b6e0488ce38 --- /dev/null +++ b/homeassistant/components/melnor/translations/ru.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u041f\u043e\u0431\u043b\u0438\u0437\u043e\u0441\u0442\u0438 \u043d\u0435\u0442 \u043d\u0438 \u043e\u0434\u043d\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Melnor Bluetooth." + }, + "step": { + "bluetooth_confirm": { + "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u043a\u043b\u0430\u043f\u0430\u043d Melnor Bluetooth `{name}`?", + "title": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d \u043a\u043b\u0430\u043f\u0430\u043d Melnor Bluetooth" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/fr.json b/homeassistant/components/overkiz/translations/fr.json index c919301d541..89d7af10f33 100644 --- a/homeassistant/components/overkiz/translations/fr.json +++ b/homeassistant/components/overkiz/translations/fr.json @@ -11,7 +11,8 @@ "server_in_maintenance": "Le serveur est ferm\u00e9 pour maintenance", "too_many_attempts": "Trop de tentatives avec un jeton non valide\u00a0: banni temporairement", "too_many_requests": "Trop de demandes, r\u00e9essayez plus tard.", - "unknown": "Erreur inattendue" + "unknown": "Erreur inattendue", + "unknown_user": "Utilisateur inconnu. Les comptes Somfy Protect ne sont pas pris en charge par cette int\u00e9gration." }, "flow_title": "Passerelle\u00a0: {gateway_id}", "step": { diff --git a/homeassistant/components/overkiz/translations/ru.json b/homeassistant/components/overkiz/translations/ru.json index 53f08e1dbd6..17ef0ecd27e 100644 --- a/homeassistant/components/overkiz/translations/ru.json +++ b/homeassistant/components/overkiz/translations/ru.json @@ -11,7 +11,8 @@ "server_in_maintenance": "\u0421\u0435\u0440\u0432\u0435\u0440 \u043d\u0435 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u0432 \u0441\u0432\u044f\u0437\u0438 \u0441 \u0442\u0435\u0445\u043d\u0438\u0447\u0435\u0441\u043a\u0438\u043c \u043e\u0431\u0441\u043b\u0443\u0436\u0438\u0432\u0430\u043d\u0438\u0435\u043c.", "too_many_attempts": "\u0421\u043b\u0438\u0448\u043a\u043e\u043c \u043c\u043d\u043e\u0433\u043e \u043f\u043e\u043f\u044b\u0442\u043e\u043a \u0441 \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u043c \u0442\u043e\u043a\u0435\u043d\u043e\u043c, \u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e \u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043e.", "too_many_requests": "\u0421\u043b\u0438\u0448\u043a\u043e\u043c \u043c\u043d\u043e\u0433\u043e \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432, \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435.", - "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430.", + "unknown_user": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u044b\u0439 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c. \u042d\u0442\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0437\u0430\u043f\u0438\u0441\u0438 Somfy Protect." }, "flow_title": "\u0428\u043b\u044e\u0437: {gateway_id}", "step": { diff --git a/homeassistant/components/sensor/translations/fr.json b/homeassistant/components/sensor/translations/fr.json index fd9cb2aeecb..fb08f5c75ba 100644 --- a/homeassistant/components/sensor/translations/fr.json +++ b/homeassistant/components/sensor/translations/fr.json @@ -11,6 +11,7 @@ "is_gas": "Gaz actuel de {entity_name}", "is_humidity": "Humidit\u00e9 actuelle de {entity_name}", "is_illuminance": "\u00c9clairement lumineux actuel de {entity_name}", + "is_moisture": "Humidit\u00e9 actuelle de {entity_name}", "is_nitrogen_dioxide": "Niveau actuel de concentration en dioxyde d'azote de {entity_name}", "is_nitrogen_monoxide": "Niveau actuel de concentration en monoxyde d'azote de {entity_name}", "is_nitrous_oxide": "Niveau actuel de concentration d'oxyde nitreux de {entity_name}", diff --git a/homeassistant/components/sensor/translations/hu.json b/homeassistant/components/sensor/translations/hu.json index 0f74b6cdd0f..76e5a6cd23d 100644 --- a/homeassistant/components/sensor/translations/hu.json +++ b/homeassistant/components/sensor/translations/hu.json @@ -11,6 +11,7 @@ "is_gas": "Jelenlegi {entity_name} g\u00e1z", "is_humidity": "{entity_name} aktu\u00e1lis p\u00e1ratartalma", "is_illuminance": "{entity_name} aktu\u00e1lis megvil\u00e1g\u00edt\u00e1sa", + "is_moisture": "{entity_name} aktu\u00e1lis p\u00e1ratartalom", "is_nitrogen_dioxide": "Jelenlegi {entity_name} nitrog\u00e9n-dioxid-koncentr\u00e1ci\u00f3 szint", "is_nitrogen_monoxide": "Jelenlegi {entity_name} nitrog\u00e9n-monoxid-koncentr\u00e1ci\u00f3 szint", "is_nitrous_oxide": "Jelenlegi {entity_name} dinitrog\u00e9n-oxid-koncentr\u00e1ci\u00f3 szint", @@ -40,6 +41,7 @@ "gas": "{entity_name} g\u00e1z v\u00e1ltoz\u00e1sok", "humidity": "{entity_name} p\u00e1ratartalma v\u00e1ltozik", "illuminance": "{entity_name} megvil\u00e1g\u00edt\u00e1sa v\u00e1ltozik", + "moisture": "{entity_name} p\u00e1ratartalom-v\u00e1ltoz\u00e1s", "nitrogen_dioxide": "{entity_name} nitrog\u00e9n-dioxid koncentr\u00e1ci\u00f3 v\u00e1ltozik", "nitrogen_monoxide": "{entity_name} nitrog\u00e9n-monoxid koncentr\u00e1ci\u00f3 v\u00e1ltozik", "nitrous_oxide": "{entity_name} dinitrog\u00e9n-oxid koncentr\u00e1ci\u00f3ja v\u00e1ltozik", diff --git a/homeassistant/components/sensor/translations/no.json b/homeassistant/components/sensor/translations/no.json index 174ce79e656..e5b9c70846f 100644 --- a/homeassistant/components/sensor/translations/no.json +++ b/homeassistant/components/sensor/translations/no.json @@ -11,6 +11,7 @@ "is_gas": "Gjeldende {entity_name} gass", "is_humidity": "Gjeldende {entity_name} fuktighet", "is_illuminance": "Gjeldende {entity_name} belysningsstyrke", + "is_moisture": "Gjeldende {entity_name}", "is_nitrogen_dioxide": "Gjeldende konsentrasjonsniv\u00e5 for {entity_name}", "is_nitrogen_monoxide": "Gjeldende {entity_name} nitrogenmonoksidkonsentrasjonsniv\u00e5", "is_nitrous_oxide": "Gjeldende {entity_name} lystgasskonsentrasjonsniv\u00e5", @@ -40,6 +41,7 @@ "gas": "{entity_name} gass endres", "humidity": "{entity_name} fuktighets endringer", "illuminance": "{entity_name} belysningsstyrke endringer", + "moisture": "{entity_name} fuktighetsendringer", "nitrogen_dioxide": "{entity_name} nitrogendioksidkonsentrasjonsendringer", "nitrogen_monoxide": "{entity_name} nitrogenmonoksidkonsentrasjonsendringer", "nitrous_oxide": "{entity_name} endringer i nitrogenoksidskonsentrasjonen", diff --git a/homeassistant/components/sensor/translations/ru.json b/homeassistant/components/sensor/translations/ru.json index 35a40104d19..1efde71a7c6 100644 --- a/homeassistant/components/sensor/translations/ru.json +++ b/homeassistant/components/sensor/translations/ru.json @@ -11,6 +11,7 @@ "is_gas": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "is_humidity": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "is_illuminance": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", + "is_moisture": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "is_nitrogen_dioxide": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0443\u0440\u043e\u0432\u043d\u044f \u043a\u043e\u043d\u0446\u0435\u043d\u0442\u0440\u0430\u0446\u0438\u0438 \u0434\u0438\u043e\u043a\u0441\u0438\u0434\u0430 \u0430\u0437\u043e\u0442\u0430", "is_nitrogen_monoxide": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0443\u0440\u043e\u0432\u043d\u044f \u043a\u043e\u043d\u0446\u0435\u043d\u0442\u0440\u0430\u0446\u0438\u0438 \u043c\u043e\u043d\u043e\u043e\u043a\u0441\u0438\u0434\u0430 \u0430\u0437\u043e\u0442\u0430", "is_nitrous_oxide": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0443\u0440\u043e\u0432\u043d\u044f \u043a\u043e\u043d\u0446\u0435\u043d\u0442\u0440\u0430\u0446\u0438\u0438 \u0437\u0430\u043a\u0438\u0441\u0438 \u0430\u0437\u043e\u0442\u0430", @@ -40,6 +41,7 @@ "gas": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0441\u043e\u0434\u0435\u0440\u0436\u0430\u043d\u0438\u0435 \u0433\u0430\u0437\u0430", "humidity": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "illuminance": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", + "moisture": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0441\u043e\u0434\u0435\u0440\u0436\u0430\u043d\u0438\u0435 \u0432\u043b\u0430\u0433\u0438", "nitrogen_dioxide": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043a\u043e\u043d\u0446\u0435\u043d\u0442\u0440\u0430\u0446\u0438\u0438 \u0434\u0438\u043e\u043a\u0441\u0438\u0434\u0430 \u0430\u0437\u043e\u0442\u0430", "nitrogen_monoxide": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043a\u043e\u043d\u0446\u0435\u043d\u0442\u0440\u0430\u0446\u0438\u0438 \u043c\u043e\u043d\u043e\u043e\u043a\u0441\u0438\u0434\u0430 \u0430\u0437\u043e\u0442\u0430", "nitrous_oxide": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043a\u043e\u043d\u0446\u0435\u043d\u0442\u0440\u0430\u0446\u0438\u0438 \u0437\u0430\u043a\u0438\u0441\u0438 \u0430\u0437\u043e\u0442\u0430", diff --git a/homeassistant/components/sensor/translations/zh-Hant.json b/homeassistant/components/sensor/translations/zh-Hant.json index 344f9d6119e..eb0bbfc50f9 100644 --- a/homeassistant/components/sensor/translations/zh-Hant.json +++ b/homeassistant/components/sensor/translations/zh-Hant.json @@ -11,6 +11,7 @@ "is_gas": "\u76ee\u524d{entity_name}\u6c23\u9ad4", "is_humidity": "\u76ee\u524d{entity_name}\u6fd5\u5ea6", "is_illuminance": "\u76ee\u524d{entity_name}\u7167\u5ea6", + "is_moisture": "\u76ee\u524d{entity_name}\u6fd5\u5ea6", "is_nitrogen_dioxide": "\u76ee\u524d {entity_name} \u4e8c\u6c27\u5316\u6c2e\u6fc3\u5ea6\u72c0\u614b", "is_nitrogen_monoxide": "\u76ee\u524d {entity_name} \u4e00\u6c27\u5316\u6c2e\u6fc3\u5ea6\u72c0\u614b", "is_nitrous_oxide": "\u76ee\u524d {entity_name} \u4e00\u6c27\u5316\u4e8c\u6c2e\u6fc3\u5ea6\u72c0\u614b", @@ -40,6 +41,7 @@ "gas": "{entity_name}\u6c23\u9ad4\u8b8a\u66f4", "humidity": "{entity_name}\u6fd5\u5ea6\u8b8a\u66f4", "illuminance": "{entity_name}\u7167\u5ea6\u8b8a\u66f4", + "moisture": "{entity_name}\u6fd5\u5ea6\u8b8a\u66f4", "nitrogen_dioxide": "{entity_name} \u4e8c\u6c27\u5316\u6c2e\u6fc3\u5ea6\u8b8a\u5316", "nitrogen_monoxide": "{entity_name} \u4e00\u6c27\u5316\u6c2e\u6fc3\u5ea6\u8b8a\u5316", "nitrous_oxide": "{entity_name} \u4e00\u6c27\u5316\u4e8c\u6c2e\u6fc3\u5ea6\u8b8a\u5316", diff --git a/homeassistant/components/sensorpro/translations/ru.json b/homeassistant/components/sensorpro/translations/ru.json new file mode 100644 index 00000000000..887499e5f2e --- /dev/null +++ b/homeassistant/components/sensorpro/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.", + "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.", + "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.", + "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." + }, + "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/volvooncall/translations/fr.json b/homeassistant/components/volvooncall/translations/fr.json index ac007625497..cc35aa10fbb 100644 --- a/homeassistant/components/volvooncall/translations/fr.json +++ b/homeassistant/components/volvooncall/translations/fr.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9" + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { "invalid_auth": "Authentification non valide", diff --git a/homeassistant/components/volvooncall/translations/ru.json b/homeassistant/components/volvooncall/translations/ru.json index 79d45c47f4d..8279196a31d 100644 --- a/homeassistant/components/volvooncall/translations/ru.json +++ b/homeassistant/components/volvooncall/translations/ru.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." + "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." }, "error": { "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", diff --git a/homeassistant/components/zha/translations/ru.json b/homeassistant/components/zha/translations/ru.json index 9204a618f2f..fee8080d8eb 100644 --- a/homeassistant/components/zha/translations/ru.json +++ b/homeassistant/components/zha/translations/ru.json @@ -19,11 +19,11 @@ "title": "\u0412\u043e\u0441\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u0438\u0437 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0441\u043e\u0437\u0434\u0430\u043d\u043d\u043e\u0439 \u0440\u0435\u0437\u0435\u0440\u0432\u043d\u043e\u0439 \u043a\u043e\u043f\u0438\u0438" }, "choose_formation_strategy": { - "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u0435\u0442\u0435\u0432\u044b\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0434\u043b\u044f \u0412\u0430\u0448\u0435\u0433\u043e \u0440\u0430\u0434\u0438\u043e.", + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0441\u0435\u0442\u0438 \u0434\u043b\u044f \u0412\u0430\u0448\u0435\u0433\u043e \u0440\u0430\u0434\u0438\u043e\u043c\u043e\u0434\u0443\u043b\u044f.", "menu_options": { "choose_automatic_backup": "\u0412\u043e\u0441\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u0438\u0437 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0441\u043e\u0437\u0434\u0430\u043d\u043d\u043e\u0439 \u0440\u0435\u0437\u0435\u0440\u0432\u043d\u043e\u0439 \u043a\u043e\u043f\u0438\u0438", "form_new_network": "\u0421\u0442\u0435\u0440\u0435\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0441\u0435\u0442\u0438 \u0438 \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u043d\u043e\u0432\u0443\u044e \u0441\u0435\u0442\u044c", - "reuse_settings": "\u0421\u043e\u0445\u0440\u0430\u043d\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0440\u0430\u0434\u0438\u043e\u0441\u0435\u0442\u0438", + "reuse_settings": "\u0421\u043e\u0445\u0440\u0430\u043d\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0441\u0435\u0442\u0438", "upload_manual_backup": "\u0417\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0440\u0435\u0437\u0435\u0440\u0432\u043d\u0443\u044e \u043a\u043e\u043f\u0438\u044e \u0432\u0440\u0443\u0447\u043d\u0443\u044e" }, "title": "\u0424\u043e\u0440\u043c\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0441\u0435\u0442\u0438" @@ -32,7 +32,7 @@ "data": { "path": "\u041f\u0443\u0442\u044c \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" }, - "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043f\u043e\u0440\u0442 \u0434\u043b\u044f \u0412\u0430\u0448\u0435\u0433\u043e \u0440\u0430\u0434\u0438\u043e\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Zigbee.", + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043f\u043e\u0440\u0442 \u0412\u0430\u0448\u0435\u0433\u043e \u0440\u0430\u0434\u0438\u043e\u043c\u043e\u0434\u0443\u043b\u044f Zigbee.", "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043f\u043e\u0440\u0442" }, "confirm": { @@ -43,24 +43,33 @@ }, "manual_pick_radio_type": { "data": { - "radio_type": "\u0422\u0438\u043f \u0440\u0430\u0434\u0438\u043e\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" + "radio_type": "\u0422\u0438\u043f \u0440\u0430\u0434\u0438\u043e\u043c\u043e\u0434\u0443\u043b\u044f" }, - "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u0438\u043f \u0440\u0430\u0434\u0438\u043e\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Zigbee", - "title": "\u0422\u0438\u043f \u0440\u0430\u0434\u0438\u043e\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u0438\u043f \u0440\u0430\u0434\u0438\u043e\u043c\u043e\u0434\u0443\u043b\u044f Zigbee", + "title": "\u0422\u0438\u043f \u0440\u0430\u0434\u0438\u043e\u043c\u043e\u0434\u0443\u043b\u044f" }, "manual_port_config": { "data": { "baudrate": "\u0441\u043a\u043e\u0440\u043e\u0441\u0442\u044c \u043f\u043e\u0440\u0442\u0430", "flow_control": "\u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u043f\u043e\u0442\u043e\u043a\u043e\u043c \u0434\u0430\u043d\u043d\u044b\u0445", "path": "\u041f\u0443\u0442\u044c \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" - } + }, + "description": "\u0423\u043a\u0430\u0436\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u043e\u0433\u043e \u043f\u043e\u0440\u0442\u0430", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u043e\u0433\u043e \u043f\u043e\u0440\u0442\u0430" + }, + "maybe_confirm_ezsp_restore": { + "data": { + "overwrite_coordinator_ieee": "\u0418\u0437\u043c\u0435\u043d\u0438\u0442\u044c IEEE-\u0430\u0434\u0440\u0435\u0441 \u0440\u0430\u0434\u0438\u043e\u043c\u043e\u0434\u0443\u043b\u044f" + }, + "description": "\u0412 \u0412\u0430\u0448\u0435\u0439 \u0440\u0435\u0437\u0435\u0440\u0432\u043d\u043e\u0439 \u043a\u043e\u043f\u0438\u0438 IEEE-\u0430\u0434\u0440\u0435\u0441 \u043e\u0442\u043b\u0438\u0447\u0430\u0435\u0442\u0441\u044f \u043e\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u043e\u0433\u043e \u0441\u0435\u0439\u0447\u0430\u0441. \u0427\u0442\u043e\u0431\u044b \u0412\u0430\u0448\u0430 \u0441\u0435\u0442\u044c \u0444\u0443\u043d\u043a\u0446\u0438\u043e\u043d\u0438\u0440\u043e\u0432\u0430\u043b\u0430 \u0434\u043e\u043b\u0436\u043d\u044b\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c, IEEE-\u0430\u0434\u0440\u0435\u0441 \u0412\u0430\u0448\u0435\u0433\u043e \u0440\u0430\u0434\u0438\u043e\u043c\u043e\u0434\u0443\u043b\u044f \u0442\u0430\u043a\u0436\u0435 \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0438\u0437\u043c\u0435\u043d\u0435\u043d. \n\n\u042d\u0442\u043e \u043d\u0435\u043e\u0431\u0440\u0430\u0442\u0438\u043c\u0430\u044f \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u044f.", + "title": "\u041f\u0435\u0440\u0435\u0437\u0430\u043f\u0438\u0441\u044c IEEE-\u0430\u0434\u0440\u0435\u0441\u0430 \u0440\u0430\u0434\u0438\u043e\u043c\u043e\u0434\u0443\u043b\u044f" }, "pick_radio": { "data": { - "radio_type": "\u0422\u0438\u043f \u0440\u0430\u0434\u0438\u043e\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" + "radio_type": "\u0422\u0438\u043f \u0440\u0430\u0434\u0438\u043e\u043c\u043e\u0434\u0443\u043b\u044f" }, - "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u0438\u043f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Zigbee", - "title": "\u0422\u0438\u043f \u0440\u0430\u0434\u0438\u043e\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u0438\u043f \u0440\u0430\u0434\u0438\u043e\u043c\u043e\u0434\u0443\u043b\u044f Zigbee", + "title": "\u0422\u0438\u043f \u0440\u0430\u0434\u0438\u043e\u043c\u043e\u0434\u0443\u043b\u044f" }, "port_config": { "data": { @@ -71,6 +80,13 @@ "description": "\u0423\u043a\u0430\u0436\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043f\u043e\u0440\u0442\u0430", "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" }, + "upload_manual_backup": { + "data": { + "uploaded_backup_file": "\u0417\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0444\u0430\u0439\u043b" + }, + "description": "\u0412\u043e\u0441\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a \u0441\u0435\u0442\u0438 \u0438\u0437 \u0437\u0430\u0433\u0440\u0443\u0436\u0435\u043d\u043d\u043e\u0433\u043e \u0444\u0430\u0439\u043b\u0430 JSON. \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0435\u0433\u043e \u0438\u0437 \u0434\u0440\u0443\u0433\u043e\u0433\u043e \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440\u0430 ZHA \u0438\u0437 \u0440\u0430\u0437\u0434\u0435\u043b\u0430 **\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0441\u0435\u0442\u0438** \u0438\u043b\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0444\u0430\u0439\u043b \u0438\u0437 Zigbee2MQTT `coordinator_backup.json`.", + "title": "\u0417\u0430\u0433\u0440\u0443\u0437\u043a\u0430 \u0440\u0435\u0437\u0435\u0440\u0432\u043d\u043e\u0439 \u043a\u043e\u043f\u0438\u0438 \u0432\u0440\u0443\u0447\u043d\u0443\u044e" + }, "user": { "data": { "path": "\u041f\u0443\u0442\u044c \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" @@ -153,5 +169,77 @@ "remote_button_short_release": "{subtype} \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u043a\u043e\u0440\u043e\u0442\u043a\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f", "remote_button_triple_press": "{subtype} \u043d\u0430\u0436\u0430\u0442\u0430 \u0442\u0440\u0438 \u0440\u0430\u0437\u0430" } + }, + "options": { + "abort": { + "not_zha_device": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 ZHA.", + "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e.", + "usb_probe_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c USB-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_backup_json": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 JSON \u0440\u0435\u0437\u0435\u0440\u0432\u043d\u043e\u0433\u043e \u043a\u043e\u043f\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f" + }, + "flow_title": "{name}", + "step": { + "choose_automatic_backup": { + "data": { + "choose_automatic_backup": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0435 \u0440\u0435\u0437\u0435\u0440\u0432\u043d\u043e\u0435 \u043a\u043e\u043f\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435" + }, + "description": "\u0412\u043e\u0441\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u0441\u0435\u0442\u0435\u0432\u044b\u0445 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a \u0438\u0437 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0439 \u0440\u0435\u0437\u0435\u0440\u0432\u043d\u043e\u0439 \u043a\u043e\u043f\u0438\u0438", + "title": "\u0412\u043e\u0441\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u0438\u0437 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0441\u043e\u0437\u0434\u0430\u043d\u043d\u043e\u0439 \u0440\u0435\u0437\u0435\u0440\u0432\u043d\u043e\u0439 \u043a\u043e\u043f\u0438\u0438" + }, + "choose_formation_strategy": { + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0441\u0435\u0442\u0438 \u0434\u043b\u044f \u0412\u0430\u0448\u0435\u0433\u043e \u0440\u0430\u0434\u0438\u043e\u043c\u043e\u0434\u0443\u043b\u044f.", + "menu_options": { + "choose_automatic_backup": "\u0412\u043e\u0441\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u0438\u0437 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0441\u043e\u0437\u0434\u0430\u043d\u043d\u043e\u0439 \u0440\u0435\u0437\u0435\u0440\u0432\u043d\u043e\u0439 \u043a\u043e\u043f\u0438\u0438", + "form_new_network": "\u0421\u0442\u0435\u0440\u0435\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0441\u0435\u0442\u0438 \u0438 \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u043d\u043e\u0432\u0443\u044e \u0441\u0435\u0442\u044c", + "reuse_settings": "\u0421\u043e\u0445\u0440\u0430\u043d\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0441\u0435\u0442\u0438", + "upload_manual_backup": "\u0417\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0440\u0435\u0437\u0435\u0440\u0432\u043d\u0443\u044e \u043a\u043e\u043f\u0438\u044e \u0432\u0440\u0443\u0447\u043d\u0443\u044e" + }, + "title": "\u0424\u043e\u0440\u043c\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0441\u0435\u0442\u0438" + }, + "choose_serial_port": { + "data": { + "path": "\u041f\u0443\u0442\u044c \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043f\u043e\u0440\u0442 \u0412\u0430\u0448\u0435\u0433\u043e \u0440\u0430\u0434\u0438\u043e\u043c\u043e\u0434\u0443\u043b\u044f Zigbee.", + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043f\u043e\u0440\u0442" + }, + "init": { + "description": "\u0420\u0430\u0431\u043e\u0442\u0430 ZHA \u0431\u0443\u0434\u0435\u0442 \u043e\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u0430. \u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0438\u0442\u044c?", + "title": "\u041f\u0435\u0440\u0435\u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 ZHA" + }, + "manual_pick_radio_type": { + "data": { + "radio_type": "\u0422\u0438\u043f \u0440\u0430\u0434\u0438\u043e\u043c\u043e\u0434\u0443\u043b\u044f" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u0438\u043f \u0440\u0430\u0434\u0438\u043e\u043c\u043e\u0434\u0443\u043b\u044f Zigbee", + "title": "\u0422\u0438\u043f \u0440\u0430\u0434\u0438\u043e\u043c\u043e\u0434\u0443\u043b\u044f" + }, + "manual_port_config": { + "data": { + "baudrate": "\u0441\u043a\u043e\u0440\u043e\u0441\u0442\u044c \u043f\u043e\u0440\u0442\u0430", + "flow_control": "\u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u043f\u043e\u0442\u043e\u043a\u043e\u043c \u0434\u0430\u043d\u043d\u044b\u0445", + "path": "\u041f\u0443\u0442\u044c \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" + }, + "description": "\u0423\u043a\u0430\u0436\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u043e\u0433\u043e \u043f\u043e\u0440\u0442\u0430", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u043e\u0433\u043e \u043f\u043e\u0440\u0442\u0430" + }, + "maybe_confirm_ezsp_restore": { + "data": { + "overwrite_coordinator_ieee": "\u0418\u0437\u043c\u0435\u043d\u0438\u0442\u044c IEEE-\u0430\u0434\u0440\u0435\u0441 \u0440\u0430\u0434\u0438\u043e\u043c\u043e\u0434\u0443\u043b\u044f" + }, + "description": "\u0412 \u0412\u0430\u0448\u0435\u0439 \u0440\u0435\u0437\u0435\u0440\u0432\u043d\u043e\u0439 \u043a\u043e\u043f\u0438\u0438 IEEE-\u0430\u0434\u0440\u0435\u0441 \u043e\u0442\u043b\u0438\u0447\u0430\u0435\u0442\u0441\u044f \u043e\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u043e\u0433\u043e \u0441\u0435\u0439\u0447\u0430\u0441. \u0427\u0442\u043e\u0431\u044b \u0412\u0430\u0448\u0430 \u0441\u0435\u0442\u044c \u0444\u0443\u043d\u043a\u0446\u0438\u043e\u043d\u0438\u0440\u043e\u0432\u0430\u043b\u0430 \u0434\u043e\u043b\u0436\u043d\u044b\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c, IEEE-\u0430\u0434\u0440\u0435\u0441 \u0412\u0430\u0448\u0435\u0433\u043e \u0440\u0430\u0434\u0438\u043e\u043c\u043e\u0434\u0443\u043b\u044f \u0442\u0430\u043a\u0436\u0435 \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0438\u0437\u043c\u0435\u043d\u0435\u043d. \n\n\u042d\u0442\u043e \u043d\u0435\u043e\u0431\u0440\u0430\u0442\u0438\u043c\u0430\u044f \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u044f.", + "title": "\u041f\u0435\u0440\u0435\u0437\u0430\u043f\u0438\u0441\u044c IEEE-\u0430\u0434\u0440\u0435\u0441\u0430 \u0440\u0430\u0434\u0438\u043e\u043c\u043e\u0434\u0443\u043b\u044f" + }, + "upload_manual_backup": { + "data": { + "uploaded_backup_file": "\u0417\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0444\u0430\u0439\u043b" + }, + "description": "\u0412\u043e\u0441\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a \u0441\u0435\u0442\u0438 \u0438\u0437 \u0437\u0430\u0433\u0440\u0443\u0436\u0435\u043d\u043d\u043e\u0433\u043e \u0444\u0430\u0439\u043b\u0430 JSON. \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0435\u0433\u043e \u0438\u0437 \u0434\u0440\u0443\u0433\u043e\u0433\u043e \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440\u0430 ZHA \u0438\u0437 \u0440\u0430\u0437\u0434\u0435\u043b\u0430 **\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0441\u0435\u0442\u0438** \u0438\u043b\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0444\u0430\u0439\u043b \u0438\u0437 Zigbee2MQTT `coordinator_backup.json`.", + "title": "\u0417\u0430\u0433\u0440\u0443\u0437\u043a\u0430 \u0440\u0435\u0437\u0435\u0440\u0432\u043d\u043e\u0439 \u043a\u043e\u043f\u0438\u0438 \u0432\u0440\u0443\u0447\u043d\u0443\u044e" + } + } } } \ No newline at end of file From cf988354dbbd35704b542fec3c154d127f626d4c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 2 Sep 2022 20:53:33 -0400 Subject: [PATCH 056/955] Bump frontend to 20220902.0 (#77734) --- 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 ebaa83f8d46..8459d08eab7 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==20220901.0"], + "requirements": ["home-assistant-frontend==20220902.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 8cfb71c5db0..2e41ae13458 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -19,7 +19,7 @@ cryptography==37.0.4 fnvhash==0.1.0 hass-nabucasa==0.55.0 home-assistant-bluetooth==1.3.0 -home-assistant-frontend==20220901.0 +home-assistant-frontend==20220902.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index 9898b53f4a4..3574dba00ad 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -848,7 +848,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220901.0 +home-assistant-frontend==20220902.0 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e349afbabaf..9f2436c8ae8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -625,7 +625,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220901.0 +home-assistant-frontend==20220902.0 # homeassistant.components.home_connect homeconnect==0.7.2 From f52c00a1c1f43e23062f70bd91a2939da200a8d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Matheson=20Wergeland?= Date: Sat, 3 Sep 2022 10:11:40 +0200 Subject: [PATCH 057/955] =?UTF-8?q?Add=20Nob=C3=B8=20Ecohub=20integration?= =?UTF-8?q?=20(#50913)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Initial version of Nobø Ecohub. * Options update listener for Nobø Ecohub * Unit test for nobo_hub config flow * Cleanup * Moved comment re backwards compatibility * Improved tests * Improved tests * Options flow test Pylint * Fix backwards compatibility mode * Don't require Python 3.9 * Import form configuration.yaml * Check if device is already configured. Correct tests for only discovering serial prefix Fix importing when only serial suffix is configured * Use constants * Pylint and variable name clenaup. * Review Co-authored-by: Franck Nijhof * Fix tests * Correct disabling off_command and on_commands ("Default" is a hard coded week profile in the hub). * Improve options dialog * Configure override type in options dialog * Formatting * pyupgrade * Incorporated review comments * Incorporated review comments. * Incorporated second round of review comments. * Add polling to discover preset change in HVAC_MODE_AUTO. * Added tests/components/nobo_hub to CODEOWNERS. * Update homeassistant/components/nobo_hub/config_flow.py Review Co-authored-by: Allen Porter * Update homeassistant/components/nobo_hub/climate.py Review Co-authored-by: Allen Porter * Simplify if tests. * Update homeassistant/components/nobo_hub/__init__.py Co-authored-by: Franck Nijhof * Update homeassistant/components/nobo_hub/__init__.py Co-authored-by: Franck Nijhof * Update homeassistant/components/nobo_hub/__init__.py Co-authored-by: Franck Nijhof * Separate config step for manual configuration. * Fixed indentation * Made async_set_temperature more robust * Thermometer supports tenths even though thermostat is in ones. * Preserve serial suffix in config dialog on error. * Initial version of Nobø Ecohub. * Improved tests * Review Co-authored-by: Franck Nijhof * Configure override type in options dialog * Separate config step for manual configuration. * Update homeassistant/components/nobo_hub/__init__.py Co-authored-by: Franck Nijhof * Formatting (prettier) * Fix HA stop listener. * Review * Apply suggestions from code review Co-authored-by: Martin Hjelmare * Review - Removed workaround to support "OFF" setting. - Simplified config flow to add a new device. * Apply suggestions from code review Co-authored-by: Martin Hjelmare * Apply suggestions from code review Co-authored-by: Martin Hjelmare * Fixed review comments * Update en.json with correction in review. * Implemented review comments: - Register devices - Simplifed async_set_temperature * Register hub as device in init module * Implemented review comments. Upgraded pynobo to 1.4.0. * Apply suggestions from code review Co-authored-by: Martin Hjelmare * Avoid tacking on the device name in the entity name * Inherit entity name from device name Co-authored-by: Franck Nijhof Co-authored-by: Allen Porter Co-authored-by: Martin Hjelmare --- .coveragerc | 2 + CODEOWNERS | 2 + homeassistant/components/nobo_hub/__init__.py | 86 ++++++ homeassistant/components/nobo_hub/climate.py | 209 +++++++++++++ .../components/nobo_hub/config_flow.py | 210 +++++++++++++ homeassistant/components/nobo_hub/const.py | 19 ++ .../components/nobo_hub/manifest.json | 9 + .../components/nobo_hub/strings.json | 44 +++ .../components/nobo_hub/translations/en.json | 44 +++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/nobo_hub/__init__.py | 1 + tests/components/nobo_hub/test_config_flow.py | 289 ++++++++++++++++++ 14 files changed, 922 insertions(+) create mode 100644 homeassistant/components/nobo_hub/__init__.py create mode 100644 homeassistant/components/nobo_hub/climate.py create mode 100644 homeassistant/components/nobo_hub/config_flow.py create mode 100644 homeassistant/components/nobo_hub/const.py create mode 100644 homeassistant/components/nobo_hub/manifest.json create mode 100644 homeassistant/components/nobo_hub/strings.json create mode 100644 homeassistant/components/nobo_hub/translations/en.json create mode 100644 tests/components/nobo_hub/__init__.py create mode 100644 tests/components/nobo_hub/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 3ff0d49965c..5b543434bad 100644 --- a/.coveragerc +++ b/.coveragerc @@ -838,6 +838,8 @@ omit = homeassistant/components/nmap_tracker/__init__.py homeassistant/components/nmap_tracker/device_tracker.py homeassistant/components/nmbs/sensor.py + homeassistant/components/nobo_hub/__init__.py + homeassistant/components/nobo_hub/climate.py homeassistant/components/notion/__init__.py homeassistant/components/notion/binary_sensor.py homeassistant/components/notion/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index b135a418566..97d2b9f9d9b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -748,6 +748,8 @@ build.json @home-assistant/supervisor /homeassistant/components/nissan_leaf/ @filcole /homeassistant/components/nmbs/ @thibmaek /homeassistant/components/noaa_tides/ @jdelaney72 +/homeassistant/components/nobo_hub/ @echoromeo @oyvindwe +/tests/components/nobo_hub/ @echoromeo @oyvindwe /homeassistant/components/notify/ @home-assistant/core /tests/components/notify/ @home-assistant/core /homeassistant/components/notify_events/ @matrozov @papajojo diff --git a/homeassistant/components/nobo_hub/__init__.py b/homeassistant/components/nobo_hub/__init__.py new file mode 100644 index 00000000000..7db9eb96f7e --- /dev/null +++ b/homeassistant/components/nobo_hub/__init__.py @@ -0,0 +1,86 @@ +"""The Nobø Ecohub integration.""" +from __future__ import annotations + +import logging + +from pynobo import nobo + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + ATTR_NAME, + CONF_IP_ADDRESS, + EVENT_HOMEASSISTANT_STOP, + Platform, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry + +from .const import ( + ATTR_HARDWARE_VERSION, + ATTR_SERIAL, + ATTR_SOFTWARE_VERSION, + CONF_AUTO_DISCOVERED, + CONF_SERIAL, + DOMAIN, + NOBO_MANUFACTURER, +) + +PLATFORMS = [Platform.CLIMATE] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Nobø Ecohub from a config entry.""" + + serial = entry.data[CONF_SERIAL] + discover = entry.data[CONF_AUTO_DISCOVERED] + ip_address = None if discover else entry.data[CONF_IP_ADDRESS] + hub = nobo(serial=serial, ip=ip_address, discover=discover, synchronous=False) + await hub.start() + + hass.data.setdefault(DOMAIN, {}) + + # Register hub as device + dev_reg = device_registry.async_get(hass) + dev_reg.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, hub.hub_info[ATTR_SERIAL])}, + manufacturer=NOBO_MANUFACTURER, + name=hub.hub_info[ATTR_NAME], + model=f"Nobø Ecohub ({hub.hub_info[ATTR_HARDWARE_VERSION]})", + sw_version=hub.hub_info[ATTR_SOFTWARE_VERSION], + ) + + async def _async_close(event): + """Close the Nobø Ecohub socket connection when HA stops.""" + await hub.stop() + + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_close) + ) + hass.data[DOMAIN][entry.entry_id] = hub + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + entry.async_on_unload(entry.add_update_listener(options_update_listener)) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + + hub: nobo = hass.data[DOMAIN][entry.entry_id] + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + await hub.stop() + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok + + +async def options_update_listener( + hass: HomeAssistant, config_entry: ConfigEntry +) -> None: + """Handle options update.""" + await hass.config_entries.async_reload(config_entry.entry_id) diff --git a/homeassistant/components/nobo_hub/climate.py b/homeassistant/components/nobo_hub/climate.py new file mode 100644 index 00000000000..3b7dc2debd9 --- /dev/null +++ b/homeassistant/components/nobo_hub/climate.py @@ -0,0 +1,209 @@ +"""Python Control of Nobø Hub - Nobø Energy Control.""" +from __future__ import annotations + +import logging +from typing import Any + +from pynobo import nobo + +from homeassistant.components.climate import ClimateEntity +from homeassistant.components.climate.const import ( + ATTR_TARGET_TEMP_HIGH, + ATTR_TARGET_TEMP_LOW, + PRESET_AWAY, + PRESET_COMFORT, + PRESET_ECO, + PRESET_NONE, + ClimateEntityFeature, + HVACMode, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + ATTR_IDENTIFIERS, + ATTR_MODE, + ATTR_NAME, + ATTR_SUGGESTED_AREA, + ATTR_VIA_DEVICE, + PRECISION_WHOLE, + TEMP_CELSIUS, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import ( + ATTR_OVERRIDE_ALLOWED, + ATTR_SERIAL, + ATTR_TARGET_ID, + ATTR_TARGET_TYPE, + ATTR_TEMP_COMFORT_C, + ATTR_TEMP_ECO_C, + CONF_OVERRIDE_TYPE, + DOMAIN, + OVERRIDE_TYPE_NOW, +) + +SUPPORT_FLAGS = ( + ClimateEntityFeature.PRESET_MODE | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE +) + +PRESET_MODES = [PRESET_NONE, PRESET_COMFORT, PRESET_ECO, PRESET_AWAY] + +MIN_TEMPERATURE = 7 +MAX_TEMPERATURE = 40 + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Nobø Ecohub platform from UI configuration.""" + + # Setup connection with hub + hub: nobo = hass.data[DOMAIN][config_entry.entry_id] + + override_type = ( + nobo.API.OVERRIDE_TYPE_NOW + if config_entry.options.get(CONF_OVERRIDE_TYPE) == OVERRIDE_TYPE_NOW + else nobo.API.OVERRIDE_TYPE_CONSTANT + ) + + # Add zones as entities + async_add_entities( + [NoboZone(zone_id, hub, override_type) for zone_id in hub.zones], + True, + ) + + +class NoboZone(ClimateEntity): + """Representation of a Nobø zone. + + A Nobø zone consists of a group of physical devices that are + controlled as a unity. + """ + + _attr_max_temp = MAX_TEMPERATURE + _attr_min_temp = MIN_TEMPERATURE + _attr_precision = PRECISION_WHOLE + _attr_preset_modes = PRESET_MODES + # Need to poll to get preset change when in HVACMode.AUTO. + _attr_supported_features = SUPPORT_FLAGS + _attr_temperature_unit = TEMP_CELSIUS + + def __init__(self, zone_id, hub: nobo, override_type): + """Initialize the climate device.""" + self._id = zone_id + self._nobo = hub + self._attr_unique_id = f"{hub.hub_serial}:{zone_id}" + self._attr_name = None + self._attr_has_entity_name = True + self._attr_hvac_mode = HVACMode.AUTO + self._attr_hvac_modes = [HVACMode.HEAT, HVACMode.AUTO] + self._override_type = override_type + self._attr_device_info: DeviceInfo = { + ATTR_IDENTIFIERS: {(DOMAIN, f"{hub.hub_serial}:{zone_id}")}, + ATTR_NAME: hub.zones[zone_id][ATTR_NAME], + ATTR_VIA_DEVICE: (DOMAIN, hub.hub_info[ATTR_SERIAL]), + ATTR_SUGGESTED_AREA: hub.zones[zone_id][ATTR_NAME], + } + + async def async_added_to_hass(self) -> None: + """Register callback from hub.""" + self._nobo.register_callback(self._after_update) + + async def async_will_remove_from_hass(self) -> None: + """Deregister callback from hub.""" + self._nobo.deregister_callback(self._after_update) + + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: + """Set new target HVAC mode, if it's supported.""" + if hvac_mode not in self.hvac_modes: + raise ValueError( + f"Zone {self._id} '{self._attr_name}' called with unsupported HVAC mode '{hvac_mode}'" + ) + if hvac_mode == HVACMode.AUTO: + await self.async_set_preset_mode(PRESET_NONE) + elif hvac_mode == HVACMode.HEAT: + await self.async_set_preset_mode(PRESET_COMFORT) + self._attr_hvac_mode = hvac_mode + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set new zone override.""" + if self._nobo.zones[self._id][ATTR_OVERRIDE_ALLOWED] != "1": + return + if preset_mode == PRESET_ECO: + mode = nobo.API.OVERRIDE_MODE_ECO + elif preset_mode == PRESET_AWAY: + mode = nobo.API.OVERRIDE_MODE_AWAY + elif preset_mode == PRESET_COMFORT: + mode = nobo.API.OVERRIDE_MODE_COMFORT + else: # PRESET_NONE + mode = nobo.API.OVERRIDE_MODE_NORMAL + await self._nobo.async_create_override( + mode, + self._override_type, + nobo.API.OVERRIDE_TARGET_ZONE, + self._id, + ) + + async def async_set_temperature(self, **kwargs: Any) -> None: + """Set new target temperature.""" + if ATTR_TARGET_TEMP_LOW in kwargs: + low = round(kwargs[ATTR_TARGET_TEMP_LOW]) + high = round(kwargs[ATTR_TARGET_TEMP_HIGH]) + low = min(low, high) + high = max(low, high) + await self._nobo.async_update_zone( + self._id, temp_comfort_c=high, temp_eco_c=low + ) + + async def async_update(self) -> None: + """Fetch new state data for this zone.""" + self._read_state() + + @callback + def _read_state(self) -> None: + """Read the current state from the hub. These are only local calls.""" + state = self._nobo.get_current_zone_mode(self._id) + self._attr_hvac_mode = HVACMode.AUTO + self._attr_preset_mode = PRESET_NONE + + if state == nobo.API.NAME_OFF: + self._attr_hvac_mode = HVACMode.OFF + elif state == nobo.API.NAME_AWAY: + self._attr_preset_mode = PRESET_AWAY + elif state == nobo.API.NAME_ECO: + self._attr_preset_mode = PRESET_ECO + elif state == nobo.API.NAME_COMFORT: + self._attr_preset_mode = PRESET_COMFORT + + if self._nobo.zones[self._id][ATTR_OVERRIDE_ALLOWED] == "1": + for override in self._nobo.overrides: + if self._nobo.overrides[override][ATTR_MODE] == "0": + continue # "normal" overrides + if ( + self._nobo.overrides[override][ATTR_TARGET_TYPE] + == nobo.API.OVERRIDE_TARGET_ZONE + and self._nobo.overrides[override][ATTR_TARGET_ID] == self._id + ): + self._attr_hvac_mode = HVACMode.HEAT + break + + current_temperature = self._nobo.get_current_zone_temperature(self._id) + self._attr_current_temperature = ( + None if current_temperature is None else float(current_temperature) + ) + self._attr_target_temperature_high = int( + self._nobo.zones[self._id][ATTR_TEMP_COMFORT_C] + ) + self._attr_target_temperature_low = int( + self._nobo.zones[self._id][ATTR_TEMP_ECO_C] + ) + + @callback + def _after_update(self, hub): + self._read_state() + self.async_write_ha_state() diff --git a/homeassistant/components/nobo_hub/config_flow.py b/homeassistant/components/nobo_hub/config_flow.py new file mode 100644 index 00000000000..f1e2dd7d9d2 --- /dev/null +++ b/homeassistant/components/nobo_hub/config_flow.py @@ -0,0 +1,210 @@ +"""Config flow for Nobø Ecohub integration.""" +from __future__ import annotations + +import socket +from typing import Any + +from pynobo import nobo +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_IP_ADDRESS +from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult +from homeassistant.exceptions import HomeAssistantError + +from .const import ( + CONF_AUTO_DISCOVERED, + CONF_OVERRIDE_TYPE, + CONF_SERIAL, + DOMAIN, + OVERRIDE_TYPE_CONSTANT, + OVERRIDE_TYPE_NOW, +) + +DATA_NOBO_HUB_IMPL = "nobo_hub_flow_implementation" +DEVICE_INPUT = "device_input" + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Nobø Ecohub.""" + + VERSION = 1 + + def __init__(self): + """Initialize the config flow.""" + self._discovered_hubs = None + self._hub = None + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + if self._discovered_hubs is None: + self._discovered_hubs = dict(await nobo.async_discover_hubs()) + + if not self._discovered_hubs: + # No hubs auto discovered + return await self.async_step_manual() + + if user_input is not None: + if user_input["device"] == "manual": + return await self.async_step_manual() + self._hub = user_input["device"] + return await self.async_step_selected() + + hubs = self._hubs() + hubs["manual"] = "Manual" + data_schema = vol.Schema( + { + vol.Required("device"): vol.In(hubs), + } + ) + return self.async_show_form( + step_id="user", + data_schema=data_schema, + ) + + async def async_step_selected( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle configuration of a selected discovered device.""" + errors = {} + if user_input is not None: + serial_prefix = self._discovered_hubs[self._hub] + serial_suffix = user_input["serial_suffix"] + serial = f"{serial_prefix}{serial_suffix}" + try: + return await self._create_configuration(serial, self._hub, True) + except NoboHubConnectError as error: + errors["base"] = error.msg + + user_input = user_input or {} + return self.async_show_form( + step_id="selected", + data_schema=vol.Schema( + { + vol.Required( + "serial_suffix", default=user_input.get("serial_suffix") + ): str, + } + ), + errors=errors, + description_placeholders={ + "hub": self._format_hub(self._hub, self._discovered_hubs[self._hub]) + }, + ) + + async def async_step_manual( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle configuration of an undiscovered device.""" + errors = {} + if user_input is not None: + serial = user_input[CONF_SERIAL] + ip_address = user_input[CONF_IP_ADDRESS] + try: + return await self._create_configuration(serial, ip_address, False) + except NoboHubConnectError as error: + errors["base"] = error.msg + + user_input = user_input or {} + return self.async_show_form( + step_id="manual", + data_schema=vol.Schema( + { + vol.Required(CONF_SERIAL, default=user_input.get(CONF_SERIAL)): str, + vol.Required( + CONF_IP_ADDRESS, default=user_input.get(CONF_IP_ADDRESS) + ): str, + } + ), + errors=errors, + ) + + async def _create_configuration( + self, serial: str, ip_address: str, auto_discovered: bool + ) -> FlowResult: + await self.async_set_unique_id(serial) + self._abort_if_unique_id_configured() + name = await self._test_connection(serial, ip_address) + return self.async_create_entry( + title=name, + data={ + CONF_SERIAL: serial, + CONF_IP_ADDRESS: ip_address, + CONF_AUTO_DISCOVERED: auto_discovered, + }, + ) + + async def _test_connection(self, serial: str, ip_address: str) -> str: + if not len(serial) == 12 or not serial.isdigit(): + raise NoboHubConnectError("invalid_serial") + try: + socket.inet_aton(ip_address) + except OSError as err: + raise NoboHubConnectError("invalid_ip") from err + hub = nobo(serial=serial, ip=ip_address, discover=False, synchronous=False) + if not await hub.async_connect_hub(ip_address, serial): + raise NoboHubConnectError("cannot_connect") + name = hub.hub_info["name"] + await hub.close() + return name + + @staticmethod + def _format_hub(ip, serial_prefix): + return f"{serial_prefix}XXX ({ip})" + + def _hubs(self): + return { + ip: self._format_hub(ip, serial_prefix) + for ip, serial_prefix in self._discovered_hubs.items() + } + + @staticmethod + @callback + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> config_entries.OptionsFlow: + """Get the options flow for this handler.""" + return OptionsFlowHandler(config_entry) + + +class NoboHubConnectError(HomeAssistantError): + """Error with connecting to Nobø Ecohub.""" + + def __init__(self, msg) -> None: + """Instantiate error.""" + super().__init__() + self.msg = msg + + +class OptionsFlowHandler(config_entries.OptionsFlow): + """Handles options flow for the component.""" + + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + """Initialize the options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None) -> FlowResult: + """Manage the options.""" + + if user_input is not None: + data = { + CONF_OVERRIDE_TYPE: user_input.get(CONF_OVERRIDE_TYPE), + } + return self.async_create_entry(title="", data=data) + + override_type = self.config_entry.options.get( + CONF_OVERRIDE_TYPE, OVERRIDE_TYPE_CONSTANT + ) + + schema = vol.Schema( + { + vol.Required(CONF_OVERRIDE_TYPE, default=override_type): vol.In( + [OVERRIDE_TYPE_CONSTANT, OVERRIDE_TYPE_NOW] + ), + } + ) + + return self.async_show_form(step_id="init", data_schema=schema) diff --git a/homeassistant/components/nobo_hub/const.py b/homeassistant/components/nobo_hub/const.py new file mode 100644 index 00000000000..320c2f43c07 --- /dev/null +++ b/homeassistant/components/nobo_hub/const.py @@ -0,0 +1,19 @@ +"""Constants for the Nobø Ecohub integration.""" + +DOMAIN = "nobo_hub" + +CONF_AUTO_DISCOVERED = "auto_discovered" +CONF_SERIAL = "serial" +CONF_OVERRIDE_TYPE = "override_type" +OVERRIDE_TYPE_CONSTANT = "Constant" +OVERRIDE_TYPE_NOW = "Now" + +NOBO_MANUFACTURER = "Glen Dimplex Nordic AS" +ATTR_HARDWARE_VERSION = "hardware_version" +ATTR_SOFTWARE_VERSION = "software_version" +ATTR_SERIAL = "serial" +ATTR_TEMP_COMFORT_C = "temp_comfort_c" +ATTR_TEMP_ECO_C = "temp_eco_c" +ATTR_OVERRIDE_ALLOWED = "override_allowed" +ATTR_TARGET_TYPE = "target_type" +ATTR_TARGET_ID = "target_id" diff --git a/homeassistant/components/nobo_hub/manifest.json b/homeassistant/components/nobo_hub/manifest.json new file mode 100644 index 00000000000..14e10a1ffaf --- /dev/null +++ b/homeassistant/components/nobo_hub/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "nobo_hub", + "name": "Nob\u00f8 Ecohub", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/nobo_hub", + "requirements": ["pynobo==1.4.0"], + "codeowners": ["@echoromeo", "@oyvindwe"], + "iot_class": "local_push" +} diff --git a/homeassistant/components/nobo_hub/strings.json b/homeassistant/components/nobo_hub/strings.json new file mode 100644 index 00000000000..cfa339c98df --- /dev/null +++ b/homeassistant/components/nobo_hub/strings.json @@ -0,0 +1,44 @@ +{ + "config": { + "step": { + "user": { + "description": "Select Nobø Ecohub to configure.", + "data": { + "device": "Discovered hubs" + } + }, + "selected": { + "description": "Configuring {hub}.\r\rTo connect to the hub, you need to enter the last 3 digits of the hub's serial number.", + "data": { + "serial_suffix": "Serial number suffix (3 digits)" + } + }, + "manual": { + "description": "Configure a Nobø Ecohub not discovered on your local network. If your hub is on another network, you can still connect to it by entering the complete serial number (12 digits) and its IP address.", + "data": { + "serial": "Serial number (12 digits)", + "ip_address": "[%key:common::config_flow::data::ip%]" + } + } + }, + "error": { + "cannot_connect": "Failed to connect - check serial number", + "invalid_serial": "Invalid serial number", + "invalid_ip": "Invalid IP address", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + }, + "options": { + "step": { + "init": { + "data": { + "override_type": "Override type" + }, + "description": "Select override type \"Now\" to end override on next week profile change." + } + } + } +} diff --git a/homeassistant/components/nobo_hub/translations/en.json b/homeassistant/components/nobo_hub/translations/en.json new file mode 100644 index 00000000000..b35a32101c3 --- /dev/null +++ b/homeassistant/components/nobo_hub/translations/en.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect - check serial number", + "invalid_ip": "Invalid IP address", + "invalid_serial": "Invalid serial number", + "unknown": "Unexpected error" + }, + "step": { + "manual": { + "data": { + "ip_address": "IP Address", + "serial": "Serial number (12 digits)" + }, + "description": "Configure a Nob\u00f8 Ecohub not discovered on your local network. If your hub is on another network, you can still connect to it by entering the complete serial number (12 digits) and its IP address." + }, + "selected": { + "data": { + "serial_suffix": "Serial number suffix (3 digits)" + }, + "description": "Configuring {hub}.\r\rTo connect to the hub, you need to enter the last 3 digits of the hub's serial number." + }, + "user": { + "data": { + "device": "Discovered hubs" + }, + "description": "Select Nob\u00f8 Ecohub to configure." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "override_type": "Override type" + }, + "description": "Select override type \"Now\" to end override on next week profile change." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index c5437e14562..5aa0ed69336 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -255,6 +255,7 @@ FLOWS = { "nightscout", "nina", "nmap_tracker", + "nobo_hub", "notion", "nuheat", "nuki", diff --git a/requirements_all.txt b/requirements_all.txt index 3574dba00ad..a77007b00f8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1718,6 +1718,9 @@ pynetio==0.1.9.1 # homeassistant.components.nina pynina==0.1.8 +# homeassistant.components.nobo_hub +pynobo==1.4.0 + # homeassistant.components.nuki pynuki==1.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9f2436c8ae8..518af11783d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1204,6 +1204,9 @@ pynetgear==0.10.8 # homeassistant.components.nina pynina==0.1.8 +# homeassistant.components.nobo_hub +pynobo==1.4.0 + # homeassistant.components.nuki pynuki==1.5.2 diff --git a/tests/components/nobo_hub/__init__.py b/tests/components/nobo_hub/__init__.py new file mode 100644 index 00000000000..023f53bf6ee --- /dev/null +++ b/tests/components/nobo_hub/__init__.py @@ -0,0 +1 @@ +"""Tests for the Nobø Ecohub integration.""" diff --git a/tests/components/nobo_hub/test_config_flow.py b/tests/components/nobo_hub/test_config_flow.py new file mode 100644 index 00000000000..5cfcee9cdbf --- /dev/null +++ b/tests/components/nobo_hub/test_config_flow.py @@ -0,0 +1,289 @@ +"""Test the Nobø Ecohub config flow.""" +from unittest.mock import PropertyMock, patch + +from homeassistant import config_entries +from homeassistant.components.nobo_hub.const import CONF_OVERRIDE_TYPE, DOMAIN +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def test_configure_with_discover(hass: HomeAssistant) -> None: + """Test configure with discover.""" + with patch( + "pynobo.nobo.async_discover_hubs", + return_value=[("1.1.1.1", "123456789")], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["step_id"] == "user" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "device": "1.1.1.1", + }, + ) + assert result2["type"] == "form" + assert result2["errors"] == {} + assert result2["step_id"] == "selected" + + with patch( + "pynobo.nobo.async_connect_hub", return_value=True + ) as mock_connect, patch( + "pynobo.nobo.hub_info", + new_callable=PropertyMock, + create=True, + return_value={"name": "My Nobø Ecohub"}, + ), patch( + "homeassistant.components.nobo_hub.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + "serial_suffix": "012", + }, + ) + await hass.async_block_till_done() + + assert result3["type"] == "create_entry" + assert result3["title"] == "My Nobø Ecohub" + assert result3["data"] == { + "ip_address": "1.1.1.1", + "serial": "123456789012", + "auto_discovered": True, + } + mock_connect.assert_awaited_once_with("1.1.1.1", "123456789012") + mock_setup_entry.assert_awaited_once() + + +async def test_configure_manual(hass: HomeAssistant) -> None: + """Test manual configuration when no hubs are discovered.""" + with patch( + "pynobo.nobo.async_discover_hubs", + return_value=[], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + assert result["step_id"] == "manual" + + with patch( + "pynobo.nobo.async_connect_hub", return_value=True + ) as mock_connect, patch( + "pynobo.nobo.hub_info", + new_callable=PropertyMock, + create=True, + return_value={"name": "My Nobø Ecohub"}, + ), patch( + "homeassistant.components.nobo_hub.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "serial": "123456789012", + "ip_address": "1.1.1.1", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "My Nobø Ecohub" + assert result2["data"] == { + "serial": "123456789012", + "ip_address": "1.1.1.1", + "auto_discovered": False, + } + mock_connect.assert_awaited_once_with("1.1.1.1", "123456789012") + mock_setup_entry.assert_awaited_once() + + +async def test_configure_user_selected_manual(hass: HomeAssistant) -> None: + """Test configuration when user selects manual.""" + with patch( + "pynobo.nobo.async_discover_hubs", + return_value=[("1.1.1.1", "123456789")], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "device": "manual", + }, + ) + assert result2["type"] == "form" + assert result2["errors"] == {} + assert result2["step_id"] == "manual" + + with patch( + "pynobo.nobo.async_connect_hub", return_value=True + ) as mock_connect, patch( + "pynobo.nobo.hub_info", + new_callable=PropertyMock, + create=True, + return_value={"name": "My Nobø Ecohub"}, + ), patch( + "homeassistant.components.nobo_hub.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "serial": "123456789012", + "ip_address": "1.1.1.1", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "My Nobø Ecohub" + assert result2["data"] == { + "serial": "123456789012", + "ip_address": "1.1.1.1", + "auto_discovered": False, + } + mock_connect.assert_awaited_once_with("1.1.1.1", "123456789012") + mock_setup_entry.assert_awaited_once() + + +async def test_configure_invalid_serial_suffix(hass: HomeAssistant) -> None: + """Test we handle invalid serial suffix error.""" + with patch( + "pynobo.nobo.async_discover_hubs", + return_value=[("1.1.1.1", "123456789")], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "device": "1.1.1.1", + }, + ) + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + {"serial_suffix": "ABC"}, + ) + + assert result3["type"] == "form" + assert result3["errors"] == {"base": "invalid_serial"} + + +async def test_configure_invalid_serial_undiscovered(hass: HomeAssistant) -> None: + """Test we handle invalid serial error.""" + with patch( + "pynobo.nobo.async_discover_hubs", + return_value=[], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "manual"} + ) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"ip_address": "1.1.1.1", "serial": "123456789"}, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "invalid_serial"} + + +async def test_configure_invalid_ip_address(hass: HomeAssistant) -> None: + """Test we handle invalid ip address error.""" + with patch( + "pynobo.nobo.async_discover_hubs", + return_value=[("1.1.1.1", "123456789")], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "manual"} + ) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"serial": "123456789012", "ip_address": "ABCD"}, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "invalid_ip"} + + +async def test_configure_cannot_connect(hass: HomeAssistant) -> None: + """Test we handle cannot connect error.""" + with patch( + "pynobo.nobo.async_discover_hubs", + return_value=[("1.1.1.1", "123456789")], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "device": "1.1.1.1", + }, + ) + + with patch( + "pynobo.nobo.async_connect_hub", + return_value=False, + ) as mock_connect: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + {"serial_suffix": "012"}, + ) + assert result3["type"] == "form" + assert result3["errors"] == {"base": "cannot_connect"} + mock_connect.assert_awaited_once_with("1.1.1.1", "123456789012") + + +async def test_options_flow(hass: HomeAssistant) -> None: + """Test the options flow.""" + config_entry = MockConfigEntry( + domain="nobo_hub", + unique_id="123456789012", + data={"serial": "123456789012", "ip_address": "1.1.1.1", "auto_discover": True}, + ) + config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.nobo_hub.async_setup_entry", return_value=True + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + + assert result["type"] == "form" + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_OVERRIDE_TYPE: "Constant", + }, + ) + + assert result["type"] == "create_entry" + assert config_entry.options == {CONF_OVERRIDE_TYPE: "Constant"} + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_OVERRIDE_TYPE: "Now", + }, + ) + + assert result["type"] == "create_entry" + assert config_entry.options == {CONF_OVERRIDE_TYPE: "Now"} From 7e100b64eabb36bd38bd87c065d087e2504b9cad Mon Sep 17 00:00:00 2001 From: Simon Hansen <67142049+DurgNomis-drol@users.noreply.github.com> Date: Sat, 3 Sep 2022 10:32:03 +0200 Subject: [PATCH 058/955] Convert platform in iss integration (#77218) * Hopefully fix everthing and be happy * ... * update coverage file * Fix tests --- .coveragerc | 4 +-- homeassistant/components/iss/__init__.py | 14 +++------ homeassistant/components/iss/config_flow.py | 7 ++--- .../iss/{binary_sensor.py => sensor.py} | 31 ++++++------------- tests/components/iss/test_config_flow.py | 17 ---------- 5 files changed, 17 insertions(+), 56 deletions(-) rename homeassistant/components/iss/{binary_sensor.py => sensor.py} (67%) diff --git a/.coveragerc b/.coveragerc index 5b543434bad..2e5b3c96b41 100644 --- a/.coveragerc +++ b/.coveragerc @@ -587,7 +587,7 @@ omit = homeassistant/components/iqvia/sensor.py homeassistant/components/irish_rail_transport/sensor.py homeassistant/components/iss/__init__.py - homeassistant/components/iss/binary_sensor.py + homeassistant/components/iss/sensor.py homeassistant/components/isy994/__init__.py homeassistant/components/isy994/binary_sensor.py homeassistant/components/isy994/climate.py @@ -1218,7 +1218,7 @@ omit = homeassistant/components/switchbot/const.py homeassistant/components/switchbot/entity.py homeassistant/components/switchbot/cover.py - homeassistant/components/switchbot/light.py + homeassistant/components/switchbot/light.py homeassistant/components/switchbot/sensor.py homeassistant/components/switchbot/coordinator.py homeassistant/components/switchmate/switch.py diff --git a/homeassistant/components/iss/__init__.py b/homeassistant/components/iss/__init__.py index d6065fd4f78..640e9d5d1da 100644 --- a/homeassistant/components/iss/__init__.py +++ b/homeassistant/components/iss/__init__.py @@ -2,7 +2,7 @@ from __future__ import annotations from dataclasses import dataclass -from datetime import datetime, timedelta +from datetime import timedelta import logging import pyiss @@ -18,7 +18,7 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -PLATFORMS = [Platform.BINARY_SENSOR] +PLATFORMS = [Platform.SENSOR] @dataclass @@ -27,31 +27,25 @@ class IssData: number_of_people_in_space: int current_location: dict[str, str] - is_above: bool - next_rise: datetime -def update(iss: pyiss.ISS, latitude: float, longitude: float) -> IssData: +def update(iss: pyiss.ISS) -> IssData: """Retrieve data from the pyiss API.""" return IssData( number_of_people_in_space=iss.number_of_people_in_space(), current_location=iss.current_location(), - is_above=iss.is_ISS_above(latitude, longitude), - next_rise=iss.next_rise(latitude, longitude), ) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up this integration using UI.""" hass.data.setdefault(DOMAIN, {}) - latitude = hass.config.latitude - longitude = hass.config.longitude iss = pyiss.ISS() async def async_update() -> IssData: try: - return await hass.async_add_executor_job(update, iss, latitude, longitude) + return await hass.async_add_executor_job(update, iss) except (HTTPError, requests.exceptions.ConnectionError) as ex: raise UpdateFailed("Unable to retrieve data") from ex diff --git a/homeassistant/components/iss/config_flow.py b/homeassistant/components/iss/config_flow.py index b43949daadc..ebfd445f62c 100644 --- a/homeassistant/components/iss/config_flow.py +++ b/homeassistant/components/iss/config_flow.py @@ -7,9 +7,10 @@ from homeassistant.const import CONF_NAME, CONF_SHOW_ON_MAP from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult -from .binary_sensor import DEFAULT_NAME from .const import DOMAIN +DEFAULT_NAME = "ISS" + class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Config flow for iss component.""" @@ -30,10 +31,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") - # Check if location have been defined. - if not self.hass.config.latitude and not self.hass.config.longitude: - return self.async_abort(reason="latitude_longitude_not_defined") - if user_input is not None: return self.async_create_entry( title=user_input.get(CONF_NAME, DEFAULT_NAME), diff --git a/homeassistant/components/iss/binary_sensor.py b/homeassistant/components/iss/sensor.py similarity index 67% rename from homeassistant/components/iss/binary_sensor.py rename to homeassistant/components/iss/sensor.py index 77cb86fc45a..fac23dfd9fa 100644 --- a/homeassistant/components/iss/binary_sensor.py +++ b/homeassistant/components/iss/sensor.py @@ -1,10 +1,10 @@ -"""Support for iss binary sensor.""" +"""Support for iss sensor.""" from __future__ import annotations import logging from typing import Any -from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE, CONF_SHOW_ON_MAP from homeassistant.core import HomeAssistant @@ -19,12 +19,6 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -ATTR_ISS_NEXT_RISE = "next_rise" -ATTR_ISS_NUMBER_PEOPLE_SPACE = "number_of_people_in_space" - -DEFAULT_NAME = "ISS" -DEFAULT_DEVICE_CLASS = "visible" - async def async_setup_entry( hass: HomeAssistant, @@ -37,15 +31,11 @@ async def async_setup_entry( name = entry.title show_on_map = entry.options.get(CONF_SHOW_ON_MAP, False) - async_add_entities([IssBinarySensor(coordinator, name, show_on_map)]) + async_add_entities([IssSensor(coordinator, name, show_on_map)]) -class IssBinarySensor( - CoordinatorEntity[DataUpdateCoordinator[IssData]], BinarySensorEntity -): - """Implementation of the ISS binary sensor.""" - - _attr_device_class = DEFAULT_DEVICE_CLASS +class IssSensor(CoordinatorEntity[DataUpdateCoordinator[IssData]], SensorEntity): + """Implementation of the ISS sensor.""" def __init__( self, coordinator: DataUpdateCoordinator[IssData], name: str, show: bool @@ -57,17 +47,14 @@ class IssBinarySensor( self._show_on_map = show @property - def is_on(self) -> bool: - """Return true if the binary sensor is on.""" - return self.coordinator.data.is_above is True + def native_value(self) -> int: + """Return number of people in space.""" + return self.coordinator.data.number_of_people_in_space @property def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" - attrs = { - ATTR_ISS_NUMBER_PEOPLE_SPACE: self.coordinator.data.number_of_people_in_space, - ATTR_ISS_NEXT_RISE: self.coordinator.data.next_rise, - } + attrs = {} if self._show_on_map: attrs[ATTR_LONGITUDE] = self.coordinator.data.current_location.get( "longitude" diff --git a/tests/components/iss/test_config_flow.py b/tests/components/iss/test_config_flow.py index eabca610ddf..a806bea3056 100644 --- a/tests/components/iss/test_config_flow.py +++ b/tests/components/iss/test_config_flow.py @@ -3,7 +3,6 @@ from unittest.mock import patch from homeassistant import data_entry_flow from homeassistant.components.iss.const import DOMAIN -from homeassistant.config import async_process_ha_core_config from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_SHOW_ON_MAP from homeassistant.core import HomeAssistant @@ -48,22 +47,6 @@ async def test_integration_already_exists(hass: HomeAssistant): assert result.get("reason") == "single_instance_allowed" -async def test_abort_no_home(hass: HomeAssistant): - """Test we don't create an entry if no coordinates are set.""" - - await async_process_ha_core_config( - hass, - {"latitude": 0.0, "longitude": 0.0}, - ) - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data={} - ) - - assert result.get("type") == data_entry_flow.FlowResultType.ABORT - assert result.get("reason") == "latitude_longitude_not_defined" - - async def test_options(hass: HomeAssistant): """Test options flow.""" From 56278a4421209a475558600d220906e35df80fe0 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 3 Sep 2022 12:50:55 +0200 Subject: [PATCH 059/955] Simplify device registry (#77715) * Simplify device registry * Fix test fixture * Update homeassistant/helpers/device_registry.py Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> * Update device_registry.py * Remove dead code Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- homeassistant/helpers/device_registry.py | 202 +++++++++-------------- tests/common.py | 11 +- 2 files changed, 84 insertions(+), 129 deletions(-) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index c7825918752..57497df2d7e 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -1,11 +1,11 @@ """Provide a way to connect entities belonging to one device.""" from __future__ import annotations -from collections import OrderedDict +from collections import UserDict from collections.abc import Coroutine import logging import time -from typing import TYPE_CHECKING, Any, NamedTuple, cast +from typing import TYPE_CHECKING, Any, TypeVar, cast import attr @@ -48,11 +48,6 @@ ORPHANED_DEVICE_KEEP_SECONDS = 86400 * 30 RUNTIME_ONLY_ATTRS = {"suggested_area"} -class _DeviceIndex(NamedTuple): - identifiers: dict[tuple[str, str], str] - connections: dict[tuple[str, str], str] - - class DeviceEntryDisabler(StrEnum): """What disabled a device entry.""" @@ -149,23 +144,6 @@ def format_mac(mac: str) -> str: return mac -def _async_get_device_id_from_index( - devices_index: _DeviceIndex, - identifiers: set[tuple[str, str]], - connections: set[tuple[str, str]] | None, -) -> str | None: - """Check if device has previously been registered.""" - for identifier in identifiers: - if identifier in devices_index.identifiers: - return devices_index.identifiers[identifier] - if not connections: - return None - for connection in _normalize_connections(connections): - if connection in devices_index.connections: - return devices_index.connections[connection] - return None - - class DeviceRegistryStore(storage.Store[dict[str, list[dict[str, Any]]]]): """Store entity registry data.""" @@ -210,13 +188,69 @@ class DeviceRegistryStore(storage.Store[dict[str, list[dict[str, Any]]]]): return old_data +_EntryTypeT = TypeVar("_EntryTypeT", DeviceEntry, DeletedDeviceEntry) + + +class DeviceRegistryItems(UserDict[str, _EntryTypeT]): + """Container for device registry items, maps device id -> entry. + + Maintains two additional indexes: + - (connection_type, connection identifier) -> entry + - (DOMAIN, identifier) -> entry + """ + + def __init__(self) -> None: + """Initialize the container.""" + super().__init__() + self._connections: dict[tuple[str, str], _EntryTypeT] = {} + self._identifiers: dict[tuple[str, str], _EntryTypeT] = {} + + def __setitem__(self, key: str, entry: _EntryTypeT) -> None: + """Add an item.""" + if key in self: + old_entry = self[key] + for connection in old_entry.connections: + del self._connections[connection] + for identifier in old_entry.identifiers: + del self._identifiers[identifier] + # type ignore linked to mypy issue: https://github.com/python/mypy/issues/13596 + super().__setitem__(key, entry) # type: ignore[assignment] + for connection in entry.connections: + self._connections[connection] = entry + for identifier in entry.identifiers: + self._identifiers[identifier] = entry + + def __delitem__(self, key: str) -> None: + """Remove an item.""" + entry = self[key] + for connection in entry.connections: + del self._connections[connection] + for identifier in entry.identifiers: + del self._identifiers[identifier] + super().__delitem__(key) + + def get_entry( + self, + identifiers: set[tuple[str, str]], + connections: set[tuple[str, str]] | None, + ) -> _EntryTypeT | None: + """Get entry from identifiers or connections.""" + for identifier in identifiers: + if identifier in self._identifiers: + return self._identifiers[identifier] + if not connections: + return None + for connection in _normalize_connections(connections): + if connection in self._connections: + return self._connections[connection] + return None + + class DeviceRegistry: """Class to hold a registry of devices.""" - devices: dict[str, DeviceEntry] - deleted_devices: dict[str, DeletedDeviceEntry] - _registered_index: _DeviceIndex - _deleted_index: _DeviceIndex + devices: DeviceRegistryItems[DeviceEntry] + deleted_devices: DeviceRegistryItems[DeletedDeviceEntry] def __init__(self, hass: HomeAssistant) -> None: """Initialize the device registry.""" @@ -228,7 +262,6 @@ class DeviceRegistry: atomic_writes=True, minor_version=STORAGE_VERSION_MINOR, ) - self._clear_index() @callback def async_get(self, device_id: str) -> DeviceEntry | None: @@ -242,12 +275,7 @@ class DeviceRegistry: connections: set[tuple[str, str]] | None = None, ) -> DeviceEntry | None: """Check if device is registered.""" - device_id = _async_get_device_id_from_index( - self._registered_index, identifiers, connections - ) - if device_id is None: - return None - return self.devices[device_id] + return self.devices.get_entry(identifiers, connections) def _async_get_deleted_device( self, @@ -255,55 +283,7 @@ class DeviceRegistry: connections: set[tuple[str, str]] | None, ) -> DeletedDeviceEntry | None: """Check if device is deleted.""" - device_id = _async_get_device_id_from_index( - self._deleted_index, identifiers, connections - ) - if device_id is None: - return None - return self.deleted_devices[device_id] - - def _add_device(self, device: DeviceEntry | DeletedDeviceEntry) -> None: - """Add a device and index it.""" - if isinstance(device, DeletedDeviceEntry): - devices_index = self._deleted_index - self.deleted_devices[device.id] = device - else: - devices_index = self._registered_index - self.devices[device.id] = device - - _add_device_to_index(devices_index, device) - - def _remove_device(self, device: DeviceEntry | DeletedDeviceEntry) -> None: - """Remove a device and remove it from the index.""" - if isinstance(device, DeletedDeviceEntry): - devices_index = self._deleted_index - self.deleted_devices.pop(device.id) - else: - devices_index = self._registered_index - self.devices.pop(device.id) - - _remove_device_from_index(devices_index, device) - - def _update_device(self, old_device: DeviceEntry, new_device: DeviceEntry) -> None: - """Update a device and the index.""" - self.devices[new_device.id] = new_device - - devices_index = self._registered_index - _remove_device_from_index(devices_index, old_device) - _add_device_to_index(devices_index, new_device) - - def _clear_index(self) -> None: - """Clear the index.""" - self._registered_index = _DeviceIndex(identifiers={}, connections={}) - self._deleted_index = _DeviceIndex(identifiers={}, connections={}) - - def _rebuild_index(self) -> None: - """Create the index after loading devices.""" - self._clear_index() - for device in self.devices.values(): - _add_device_to_index(self._registered_index, device) - for deleted_device in self.deleted_devices.values(): - _add_device_to_index(self._deleted_index, deleted_device) + return self.deleted_devices.get_entry(identifiers, connections) @callback def async_get_or_create( @@ -346,11 +326,11 @@ class DeviceRegistry: if deleted_device is None: device = DeviceEntry(is_new=True) else: - self._remove_device(deleted_device) + self.deleted_devices.pop(deleted_device.id) device = deleted_device.to_device_entry( config_entry_id, connections, identifiers ) - self._add_device(device) + self.devices[device.id] = device if default_manufacturer is not UNDEFINED and device.manufacturer is None: manufacturer = default_manufacturer @@ -516,7 +496,7 @@ class DeviceRegistry: return old new = attr.evolve(old, **new_values) - self._update_device(old, new) + self.devices[device_id] = new # If its only run time attributes (suggested_area) # that do not get saved we do not want to write @@ -542,16 +522,13 @@ class DeviceRegistry: @callback def async_remove_device(self, device_id: str) -> None: """Remove a device from the device registry.""" - device = self.devices[device_id] - self._remove_device(device) - self._add_device( - DeletedDeviceEntry( - config_entries=device.config_entries, - connections=device.connections, - identifiers=device.identifiers, - id=device.id, - orphaned_timestamp=None, - ) + device = self.devices.pop(device_id) + self.deleted_devices[device_id] = DeletedDeviceEntry( + config_entries=device.config_entries, + connections=device.connections, + identifiers=device.identifiers, + id=device.id, + orphaned_timestamp=None, ) for other_device in list(self.devices.values()): if other_device.via_device_id == device_id: @@ -567,8 +544,8 @@ class DeviceRegistry: data = await self._store.async_load() - devices = OrderedDict() - deleted_devices = OrderedDict() + devices: DeviceRegistryItems[DeviceEntry] = DeviceRegistryItems() + deleted_devices: DeviceRegistryItems[DeletedDeviceEntry] = DeviceRegistryItems() if data is not None: for device in data["devices"]: @@ -607,7 +584,6 @@ class DeviceRegistry: self.devices = devices self.deleted_devices = deleted_devices - self._rebuild_index() @callback def async_schedule_save(self) -> None: @@ -692,7 +668,7 @@ class DeviceRegistry: deleted_device.orphaned_timestamp + ORPHANED_DEVICE_KEEP_SECONDS < now_time ): - self._remove_device(deleted_device) + del self.deleted_devices[deleted_device.id] @callback def async_clear_area_id(self, area_id: str) -> None: @@ -879,27 +855,3 @@ def _normalize_connections(connections: set[tuple[str, str]]) -> set[tuple[str, (key, format_mac(value)) if key == CONNECTION_NETWORK_MAC else (key, value) for key, value in connections } - - -def _add_device_to_index( - devices_index: _DeviceIndex, - device: DeviceEntry | DeletedDeviceEntry, -) -> None: - """Add a device to the index.""" - for identifier in device.identifiers: - devices_index.identifiers[identifier] = device.id - for connection in device.connections: - devices_index.connections[connection] = device.id - - -def _remove_device_from_index( - devices_index: _DeviceIndex, - device: DeviceEntry | DeletedDeviceEntry, -) -> None: - """Remove a device from the index.""" - for identifier in device.identifiers: - if identifier in devices_index.identifiers: - del devices_index.identifiers[identifier] - for connection in device.connections: - if connection in devices_index.connections: - del devices_index.connections[connection] diff --git a/tests/common.py b/tests/common.py index 89d1a1d9116..232701bd746 100644 --- a/tests/common.py +++ b/tests/common.py @@ -469,12 +469,15 @@ def mock_area_registry(hass, mock_entries=None): return registry -def mock_device_registry(hass, mock_entries=None, mock_deleted_entries=None): +def mock_device_registry(hass, mock_entries=None): """Mock the Device Registry.""" registry = device_registry.DeviceRegistry(hass) - registry.devices = mock_entries or OrderedDict() - registry.deleted_devices = mock_deleted_entries or OrderedDict() - registry._rebuild_index() + registry.devices = device_registry.DeviceRegistryItems() + if mock_entries is None: + mock_entries = {} + for key, entry in mock_entries.items(): + registry.devices[key] = entry + registry.deleted_devices = device_registry.DeviceRegistryItems() hass.data[device_registry.DATA_REGISTRY] = registry return registry From b0d033ef29abaf5ef8265d596cdaa3da8c124695 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 3 Sep 2022 12:56:49 +0200 Subject: [PATCH 060/955] Add mixin class CollectionEntity for the collection helper (#77703) * Add mixin class CollectionEntity for the collection helper * Improve typing * Address review comments * Fix tests --- homeassistant/components/counter/__init__.py | 21 +++++--- .../components/input_boolean/__init__.py | 15 ++++-- .../components/input_button/__init__.py | 17 ++++-- .../components/input_datetime/__init__.py | 21 +++++--- .../components/input_number/__init__.py | 21 +++++--- .../components/input_select/__init__.py | 15 ++++-- .../components/input_text/__init__.py | 21 +++++--- homeassistant/components/person/__init__.py | 19 ++++--- homeassistant/components/schedule/__init__.py | 18 ++++--- homeassistant/components/timer/__init__.py | 22 +++++--- homeassistant/components/zone/__init__.py | 24 ++++++--- homeassistant/helpers/collection.py | 52 ++++++++++++++++--- tests/components/timer/test_init.py | 8 +-- tests/helpers/test_collection.py | 33 ++++++++++-- 14 files changed, 221 insertions(+), 86 deletions(-) diff --git a/homeassistant/components/counter/__init__.py b/homeassistant/components/counter/__init__.py index a16d891af54..61ec384ae50 100644 --- a/homeassistant/components/counter/__init__.py +++ b/homeassistant/components/counter/__init__.py @@ -110,7 +110,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: logging.getLogger(f"{__name__}.yaml_collection"), id_manager ) collection.sync_entity_lifecycle( - hass, DOMAIN, DOMAIN, component, yaml_collection, Counter.from_yaml + hass, DOMAIN, DOMAIN, component, yaml_collection, Counter ) storage_collection = CounterStorageCollection( @@ -170,19 +170,26 @@ class CounterStorageCollection(collection.StorageCollection): return {**data, **update_data} -class Counter(RestoreEntity): +class Counter(collection.CollectionEntity, RestoreEntity): """Representation of a counter.""" _attr_should_poll: bool = False + editable: bool - def __init__(self, config: dict) -> None: + def __init__(self, config: ConfigType) -> None: """Initialize a counter.""" - self._config: dict = config + self._config: ConfigType = config self._state: int | None = config[CONF_INITIAL] - self.editable: bool = True @classmethod - def from_yaml(cls, config: dict) -> Counter: + def from_storage(cls, config: ConfigType) -> Counter: + """Create counter instance from storage.""" + counter = cls(config) + counter.editable = True + return counter + + @classmethod + def from_yaml(cls, config: ConfigType) -> Counter: """Create counter instance from yaml config.""" counter = cls(config) counter.editable = False @@ -273,7 +280,7 @@ class Counter(RestoreEntity): self._state = self.compute_next_state(new_state) self.async_write_ha_state() - async def async_update_config(self, config: dict) -> None: + async def async_update_config(self, config: ConfigType) -> None: """Change the counter's settings WS CRUD.""" self._config = config self._state = self.compute_next_state(self._state) diff --git a/homeassistant/components/input_boolean/__init__.py b/homeassistant/components/input_boolean/__init__.py index 7dee3614ad5..e6e99037afa 100644 --- a/homeassistant/components/input_boolean/__init__.py +++ b/homeassistant/components/input_boolean/__init__.py @@ -100,7 +100,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: logging.getLogger(f"{__name__}.yaml_collection"), id_manager ) collection.sync_entity_lifecycle( - hass, DOMAIN, DOMAIN, component, yaml_collection, InputBoolean.from_yaml + hass, DOMAIN, DOMAIN, component, yaml_collection, InputBoolean ) storage_collection = InputBooleanStorageCollection( @@ -150,21 +150,28 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -class InputBoolean(ToggleEntity, RestoreEntity): +class InputBoolean(collection.CollectionEntity, ToggleEntity, RestoreEntity): """Representation of a boolean input.""" _attr_should_poll = False + editable: bool def __init__(self, config: ConfigType) -> None: """Initialize a boolean input.""" self._config = config - self.editable = True self._attr_is_on = config.get(CONF_INITIAL, False) self._attr_unique_id = config[CONF_ID] + @classmethod + def from_storage(cls, config: ConfigType) -> InputBoolean: + """Return entity instance initialized from storage.""" + input_bool = cls(config) + input_bool.editable = True + return input_bool + @classmethod def from_yaml(cls, config: ConfigType) -> InputBoolean: - """Return entity instance initialized from yaml storage.""" + """Return entity instance initialized from yaml.""" input_bool = cls(config) input_bool.entity_id = f"{DOMAIN}.{config[CONF_ID]}" input_bool.editable = False diff --git a/homeassistant/components/input_button/__init__.py b/homeassistant/components/input_button/__init__.py index 3182e36d5fc..d59142fb915 100644 --- a/homeassistant/components/input_button/__init__.py +++ b/homeassistant/components/input_button/__init__.py @@ -85,7 +85,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: logging.getLogger(f"{__name__}.yaml_collection"), id_manager ) collection.sync_entity_lifecycle( - hass, DOMAIN, DOMAIN, component, yaml_collection, InputButton.from_yaml + hass, DOMAIN, DOMAIN, component, yaml_collection, InputButton ) storage_collection = InputButtonStorageCollection( @@ -131,20 +131,27 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -class InputButton(ButtonEntity, RestoreEntity): +class InputButton(collection.CollectionEntity, ButtonEntity, RestoreEntity): """Representation of a button.""" _attr_should_poll = False + editable: bool def __init__(self, config: ConfigType) -> None: """Initialize a button.""" self._config = config - self.editable = True self._attr_unique_id = config[CONF_ID] @classmethod - def from_yaml(cls, config: ConfigType) -> ButtonEntity: - """Return entity instance initialized from yaml storage.""" + def from_storage(cls, config: ConfigType) -> InputButton: + """Return entity instance initialized from storage.""" + button = cls(config) + button.editable = True + return button + + @classmethod + def from_yaml(cls, config: ConfigType) -> InputButton: + """Return entity instance initialized from yaml.""" button = cls(config) button.entity_id = f"{DOMAIN}.{config[CONF_ID]}" button.editable = False diff --git a/homeassistant/components/input_datetime/__init__.py b/homeassistant/components/input_datetime/__init__.py index bda5572081c..8b7e81f2c77 100644 --- a/homeassistant/components/input_datetime/__init__.py +++ b/homeassistant/components/input_datetime/__init__.py @@ -149,7 +149,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: logging.getLogger(f"{__name__}.yaml_collection"), id_manager ) collection.sync_entity_lifecycle( - hass, DOMAIN, DOMAIN, component, yaml_collection, InputDatetime.from_yaml + hass, DOMAIN, DOMAIN, component, yaml_collection, InputDatetime ) storage_collection = DateTimeStorageCollection( @@ -231,15 +231,15 @@ class DateTimeStorageCollection(collection.StorageCollection): return has_date_or_time({**data, **update_data}) -class InputDatetime(RestoreEntity): +class InputDatetime(collection.CollectionEntity, RestoreEntity): """Representation of a datetime input.""" _attr_should_poll = False + editable: bool - def __init__(self, config: dict) -> None: + def __init__(self, config: ConfigType) -> None: """Initialize a select input.""" self._config = config - self.editable = True self._current_datetime = None if not config.get(CONF_INITIAL): @@ -258,8 +258,15 @@ class InputDatetime(RestoreEntity): ) @classmethod - def from_yaml(cls, config: dict) -> InputDatetime: - """Return entity instance initialized from yaml storage.""" + def from_storage(cls, config: ConfigType) -> InputDatetime: + """Return entity instance initialized from storage.""" + input_dt = cls(config) + input_dt.editable = True + return input_dt + + @classmethod + def from_yaml(cls, config: ConfigType) -> InputDatetime: + """Return entity instance initialized from yaml.""" input_dt = cls(config) input_dt.entity_id = f"{DOMAIN}.{config[CONF_ID]}" input_dt.editable = False @@ -420,7 +427,7 @@ class InputDatetime(RestoreEntity): ) self.async_write_ha_state() - async def async_update_config(self, config: dict) -> None: + async def async_update_config(self, config: ConfigType) -> None: """Handle when the config is updated.""" self._config = config self.async_write_ha_state() diff --git a/homeassistant/components/input_number/__init__.py b/homeassistant/components/input_number/__init__.py index ff01bd124b6..d5fffeba3f9 100644 --- a/homeassistant/components/input_number/__init__.py +++ b/homeassistant/components/input_number/__init__.py @@ -130,7 +130,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: logging.getLogger(f"{__name__}.yaml_collection"), id_manager ) collection.sync_entity_lifecycle( - hass, DOMAIN, DOMAIN, component, yaml_collection, InputNumber.from_yaml + hass, DOMAIN, DOMAIN, component, yaml_collection, InputNumber ) storage_collection = NumberStorageCollection( @@ -202,20 +202,27 @@ class NumberStorageCollection(collection.StorageCollection): return _cv_input_number({**data, **update_data}) -class InputNumber(RestoreEntity): +class InputNumber(collection.CollectionEntity, RestoreEntity): """Representation of a slider.""" _attr_should_poll = False + editable: bool - def __init__(self, config: dict) -> None: + def __init__(self, config: ConfigType) -> None: """Initialize an input number.""" self._config = config - self.editable = True self._current_value: float | None = config.get(CONF_INITIAL) @classmethod - def from_yaml(cls, config: dict) -> InputNumber: - """Return entity instance initialized from yaml storage.""" + def from_storage(cls, config: ConfigType) -> InputNumber: + """Return entity instance initialized from storage.""" + input_num = cls(config) + input_num.editable = True + return input_num + + @classmethod + def from_yaml(cls, config: ConfigType) -> InputNumber: + """Return entity instance initialized from yaml.""" input_num = cls(config) input_num.entity_id = f"{DOMAIN}.{config[CONF_ID]}" input_num.editable = False @@ -310,7 +317,7 @@ class InputNumber(RestoreEntity): """Decrement value.""" await self.async_set_value(max(self._current_value - self._step, self._minimum)) - async def async_update_config(self, config: dict) -> None: + async def async_update_config(self, config: ConfigType) -> None: """Handle when the config is updated.""" self._config = config # just in case min/max values changed diff --git a/homeassistant/components/input_select/__init__.py b/homeassistant/components/input_select/__init__.py index 83d6684a366..fa582f22cd5 100644 --- a/homeassistant/components/input_select/__init__.py +++ b/homeassistant/components/input_select/__init__.py @@ -152,7 +152,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: logging.getLogger(f"{__name__}.yaml_collection"), id_manager ) collection.sync_entity_lifecycle( - hass, DOMAIN, DOMAIN, component, yaml_collection, InputSelect.from_yaml + hass, DOMAIN, DOMAIN, component, yaml_collection, InputSelect ) storage_collection = InputSelectStorageCollection( @@ -258,11 +258,11 @@ class InputSelectStorageCollection(collection.StorageCollection): return _cv_input_select({**data, **update_data}) -class InputSelect(SelectEntity, RestoreEntity): +class InputSelect(collection.CollectionEntity, SelectEntity, RestoreEntity): """Representation of a select input.""" _attr_should_poll = False - editable = True + editable: bool def __init__(self, config: ConfigType) -> None: """Initialize a select input.""" @@ -272,9 +272,16 @@ class InputSelect(SelectEntity, RestoreEntity): self._attr_options = config[CONF_OPTIONS] self._attr_unique_id = config[CONF_ID] + @classmethod + def from_storage(cls, config: ConfigType) -> InputSelect: + """Return entity instance initialized from storage.""" + input_select = cls(config) + input_select.editable = True + return input_select + @classmethod def from_yaml(cls, config: ConfigType) -> InputSelect: - """Return entity instance initialized from yaml storage.""" + """Return entity instance initialized from yaml.""" input_select = cls(config) input_select.entity_id = f"{DOMAIN}.{config[CONF_ID]}" input_select.editable = False diff --git a/homeassistant/components/input_text/__init__.py b/homeassistant/components/input_text/__init__.py index 38d74f57931..ac6557dad91 100644 --- a/homeassistant/components/input_text/__init__.py +++ b/homeassistant/components/input_text/__init__.py @@ -129,7 +129,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: logging.getLogger(f"{__name__}.yaml_collection"), id_manager ) collection.sync_entity_lifecycle( - hass, DOMAIN, DOMAIN, component, yaml_collection, InputText.from_yaml + hass, DOMAIN, DOMAIN, component, yaml_collection, InputText ) storage_collection = InputTextStorageCollection( @@ -195,20 +195,27 @@ class InputTextStorageCollection(collection.StorageCollection): return _cv_input_text({**data, **update_data}) -class InputText(RestoreEntity): +class InputText(collection.CollectionEntity, RestoreEntity): """Represent a text box.""" _attr_should_poll = False + editable: bool - def __init__(self, config: dict) -> None: + def __init__(self, config: ConfigType) -> None: """Initialize a text input.""" self._config = config - self.editable = True self._current_value = config.get(CONF_INITIAL) @classmethod - def from_yaml(cls, config: dict) -> InputText: - """Return entity instance initialized from yaml storage.""" + def from_storage(cls, config: ConfigType) -> InputText: + """Return entity instance initialized from storage.""" + input_text = cls(config) + input_text.editable = True + return input_text + + @classmethod + def from_yaml(cls, config: ConfigType) -> InputText: + """Return entity instance initialized from yaml.""" input_text = cls(config) input_text.entity_id = f"{DOMAIN}.{config[CONF_ID]}" input_text.editable = False @@ -286,7 +293,7 @@ class InputText(RestoreEntity): self._current_value = value self.async_write_ha_state() - async def async_update_config(self, config: dict) -> None: + async def async_update_config(self, config: ConfigType) -> None: """Handle when the config is updated.""" self._config = config self.async_write_ha_state() diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py index 09851d70384..c41be68d6ea 100644 --- a/homeassistant/components/person/__init__.py +++ b/homeassistant/components/person/__init__.py @@ -342,7 +342,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass, DOMAIN, DOMAIN, entity_component, yaml_collection, Person ) collection.sync_entity_lifecycle( - hass, DOMAIN, DOMAIN, entity_component, storage_collection, Person.from_yaml + hass, DOMAIN, DOMAIN, entity_component, storage_collection, Person ) await yaml_collection.async_load( @@ -385,15 +385,15 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -class Person(RestoreEntity): +class Person(collection.CollectionEntity, RestoreEntity): """Represent a tracked person.""" _attr_should_poll = False + editable: bool def __init__(self, config): """Set up person.""" self._config = config - self.editable = True self._latitude = None self._longitude = None self._gps_accuracy = None @@ -402,8 +402,15 @@ class Person(RestoreEntity): self._unsub_track_device = None @classmethod - def from_yaml(cls, config): - """Return entity instance initialized from yaml storage.""" + def from_storage(cls, config: ConfigType): + """Return entity instance initialized from storage.""" + person = cls(config) + person.editable = True + return person + + @classmethod + def from_yaml(cls, config: ConfigType): + """Return entity instance initialized from yaml.""" person = cls(config) person.editable = False return person @@ -468,7 +475,7 @@ class Person(RestoreEntity): EVENT_HOMEASSISTANT_START, person_start_hass ) - async def async_update_config(self, config): + async def async_update_config(self, config: ConfigType): """Handle when the config is updated.""" self._config = config diff --git a/homeassistant/components/schedule/__init__.py b/homeassistant/components/schedule/__init__.py index f5519e93c3f..394e2ae3c36 100644 --- a/homeassistant/components/schedule/__init__.py +++ b/homeassistant/components/schedule/__init__.py @@ -20,6 +20,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.helpers.collection import ( + CollectionEntity, IDManager, StorageCollection, StorageCollectionWebsocket, @@ -27,7 +28,6 @@ from homeassistant.helpers.collection import ( sync_entity_lifecycle, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.integration_platform import ( @@ -163,9 +163,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: id_manager = IDManager() yaml_collection = YamlCollection(LOGGER, id_manager) - sync_entity_lifecycle( - hass, DOMAIN, DOMAIN, component, yaml_collection, Schedule.from_yaml - ) + sync_entity_lifecycle(hass, DOMAIN, DOMAIN, component, yaml_collection, Schedule) storage_collection = ScheduleStorageCollection( Store( @@ -239,7 +237,7 @@ class ScheduleStorageCollection(StorageCollection): return data -class Schedule(Entity): +class Schedule(CollectionEntity): """Schedule entity.""" _attr_has_entity_name = True @@ -249,7 +247,7 @@ class Schedule(Entity): _next: datetime _unsub_update: Callable[[], None] | None = None - def __init__(self, config: ConfigType, editable: bool = True) -> None: + def __init__(self, config: ConfigType, editable: bool) -> None: """Initialize a schedule.""" self._config = ENTITY_SCHEMA(config) self._attr_capability_attributes = {ATTR_EDITABLE: editable} @@ -257,9 +255,15 @@ class Schedule(Entity): self._attr_name = self._config[CONF_NAME] self._attr_unique_id = self._config[CONF_ID] + @classmethod + def from_storage(cls, config: ConfigType) -> Schedule: + """Return entity instance initialized from storage.""" + schedule = cls(config, editable=True) + return schedule + @classmethod def from_yaml(cls, config: ConfigType) -> Schedule: - """Return entity instance initialized from yaml storage.""" + """Return entity instance initialized from yaml.""" schedule = cls(config, editable=False) schedule.entity_id = f"{DOMAIN}.{config[CONF_ID]}" return schedule diff --git a/homeassistant/components/timer/__init__.py b/homeassistant/components/timer/__init__.py index e5564736c74..53912a4dec8 100644 --- a/homeassistant/components/timer/__init__.py +++ b/homeassistant/components/timer/__init__.py @@ -119,7 +119,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: logging.getLogger(f"{__name__}.yaml_collection"), id_manager ) collection.sync_entity_lifecycle( - hass, DOMAIN, DOMAIN, component, yaml_collection, Timer.from_yaml + hass, DOMAIN, DOMAIN, component, yaml_collection, Timer ) storage_collection = TimerStorageCollection( @@ -195,13 +195,14 @@ class TimerStorageCollection(collection.StorageCollection): return data -class Timer(RestoreEntity): +class Timer(collection.CollectionEntity, RestoreEntity): """Representation of a timer.""" - def __init__(self, config: dict) -> None: + editable: bool + + def __init__(self, config: ConfigType) -> None: """Initialize a timer.""" self._config: dict = config - self.editable: bool = True self._state: str = STATUS_IDLE self._duration = cv.time_period_str(config[CONF_DURATION]) self._remaining: timedelta | None = None @@ -213,8 +214,15 @@ class Timer(RestoreEntity): self._attr_force_update = True @classmethod - def from_yaml(cls, config: dict) -> Timer: - """Return entity instance initialized from yaml storage.""" + def from_storage(cls, config: ConfigType) -> Timer: + """Return entity instance initialized from storage.""" + timer = cls(config) + timer.editable = True + return timer + + @classmethod + def from_yaml(cls, config: ConfigType) -> Timer: + """Return entity instance initialized from yaml.""" timer = cls(config) timer.entity_id = ENTITY_ID_FORMAT.format(config[CONF_ID]) timer.editable = False @@ -384,7 +392,7 @@ class Timer(RestoreEntity): ) self.async_write_ha_state() - async def async_update_config(self, config: dict) -> None: + async def async_update_config(self, config: ConfigType) -> None: """Handle when the config is updated.""" self._config = config self._duration = cv.time_period_str(config[CONF_DURATION]) diff --git a/homeassistant/components/zone/__init__.py b/homeassistant/components/zone/__init__.py index 9dac47eaafb..aa910a7789e 100644 --- a/homeassistant/components/zone/__init__.py +++ b/homeassistant/components/zone/__init__.py @@ -31,7 +31,6 @@ from homeassistant.core import Event, HomeAssistant, ServiceCall, State, callbac from homeassistant.helpers import ( collection, config_validation as cv, - entity, entity_component, event, service, @@ -193,7 +192,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: logging.getLogger(f"{__name__}.yaml_collection"), id_manager ) collection.sync_entity_lifecycle( - hass, DOMAIN, DOMAIN, component, yaml_collection, Zone.from_yaml + hass, DOMAIN, DOMAIN, component, yaml_collection, Zone ) storage_collection = ZoneStorageCollection( @@ -284,21 +283,30 @@ async def async_unload_entry( return True -class Zone(entity.Entity): +class Zone(collection.CollectionEntity): """Representation of a Zone.""" - def __init__(self, config: dict) -> None: + editable: bool + + def __init__(self, config: ConfigType) -> None: """Initialize the zone.""" self._config = config self.editable = True self._attrs: dict | None = None self._remove_listener: Callable[[], None] | None = None self._persons_in_zone: set[str] = set() - self._generate_attrs() @classmethod - def from_yaml(cls, config: dict) -> Zone: - """Return entity instance initialized from yaml storage.""" + def from_storage(cls, config: ConfigType) -> Zone: + """Return entity instance initialized from storage.""" + zone = cls(config) + zone.editable = True + zone._generate_attrs() + return zone + + @classmethod + def from_yaml(cls, config: ConfigType) -> Zone: + """Return entity instance initialized from yaml.""" zone = cls(config) zone.editable = False zone._generate_attrs() @@ -329,7 +337,7 @@ class Zone(entity.Entity): """Zone does not poll.""" return False - async def async_update_config(self, config: dict) -> None: + async def async_update_config(self, config: ConfigType) -> None: """Handle when the config is updated.""" if self._config == config: return diff --git a/homeassistant/helpers/collection.py b/homeassistant/helpers/collection.py index 7a73f90539c..e3c31ca0e33 100644 --- a/homeassistant/helpers/collection.py +++ b/homeassistant/helpers/collection.py @@ -22,6 +22,7 @@ from . import entity_registry from .entity import Entity from .entity_component import EntityComponent from .storage import Store +from .typing import ConfigType STORAGE_VERSION = 1 SAVE_DELAY = 10 @@ -101,6 +102,24 @@ class IDManager: return proposal +class CollectionEntity(Entity): + """Mixin class for entities managed by an ObservableCollection.""" + + @classmethod + @abstractmethod + def from_storage(cls, config: ConfigType) -> CollectionEntity: + """Create instance from storage.""" + + @classmethod + @abstractmethod + def from_yaml(cls, config: ConfigType) -> CollectionEntity: + """Create instance from yaml config.""" + + @abstractmethod + async def async_update_config(self, config: ConfigType) -> None: + """Handle updated configuration.""" + + class ObservableCollection(ABC): """Base collection type that can be observed.""" @@ -155,6 +174,13 @@ class ObservableCollection(ABC): class YamlCollection(ObservableCollection): """Offer a collection based on static data.""" + @staticmethod + def create_entity( + entity_class: type[CollectionEntity], config: ConfigType + ) -> CollectionEntity: + """Create a CollectionEntity instance.""" + return entity_class.from_yaml(config) + async def async_load(self, data: list[dict]) -> None: """Load the YAML collection. Overrides existing data.""" old_ids = set(self.data) @@ -198,6 +224,13 @@ class StorageCollection(ObservableCollection): super().__init__(logger, id_manager) self.store = store + @staticmethod + def create_entity( + entity_class: type[CollectionEntity], config: ConfigType + ) -> CollectionEntity: + """Create a CollectionEntity instance.""" + return entity_class.from_storage(config) + @property def hass(self) -> HomeAssistant: """Home Assistant object.""" @@ -290,7 +323,7 @@ class StorageCollection(ObservableCollection): return {"items": list(self.data.values())} -class IDLessCollection(ObservableCollection): +class IDLessCollection(YamlCollection): """A collection without IDs.""" counter = 0 @@ -326,20 +359,22 @@ def sync_entity_lifecycle( domain: str, platform: str, entity_component: EntityComponent, - collection: ObservableCollection, - create_entity: Callable[[dict], Entity], + collection: StorageCollection | YamlCollection, + entity_class: type[CollectionEntity], ) -> None: """Map a collection to an entity component.""" - entities: dict[str, Entity] = {} + entities: dict[str, CollectionEntity] = {} ent_reg = entity_registry.async_get(hass) - async def _add_entity(change_set: CollectionChangeSet) -> Entity: + async def _add_entity(change_set: CollectionChangeSet) -> CollectionEntity: def entity_removed() -> None: """Remove entity from entities if it's removed or not added.""" if change_set.item_id in entities: entities.pop(change_set.item_id) - entities[change_set.item_id] = create_entity(change_set.item) + entities[change_set.item_id] = collection.create_entity( + entity_class, change_set.item + ) entities[change_set.item_id].async_on_remove(entity_removed) return entities[change_set.item_id] @@ -359,10 +394,11 @@ def sync_entity_lifecycle( async def _update_entity(change_set: CollectionChangeSet) -> None: if change_set.item_id not in entities: return - await entities[change_set.item_id].async_update_config(change_set.item) # type: ignore[attr-defined] + await entities[change_set.item_id].async_update_config(change_set.item) _func_map: dict[ - str, Callable[[CollectionChangeSet], Coroutine[Any, Any, Entity | None]] + str, + Callable[[CollectionChangeSet], Coroutine[Any, Any, CollectionEntity | None]], ] = { CHANGE_ADDED: _add_entity, CHANGE_REMOVED: _remove_entity, diff --git a/tests/components/timer/test_init.py b/tests/components/timer/test_init.py index 8f605d2de9f..b51200c6ad0 100644 --- a/tests/components/timer/test_init.py +++ b/tests/components/timer/test_init.py @@ -672,7 +672,7 @@ async def test_restore_idle(hass): # Emulate a fresh load hass.data.pop(DATA_RESTORE_STATE_TASK) - entity = Timer( + entity = Timer.from_storage( { CONF_ID: "test", CONF_NAME: "test", @@ -712,7 +712,7 @@ async def test_restore_paused(hass): # Emulate a fresh load hass.data.pop(DATA_RESTORE_STATE_TASK) - entity = Timer( + entity = Timer.from_storage( { CONF_ID: "test", CONF_NAME: "test", @@ -756,7 +756,7 @@ async def test_restore_active_resume(hass): # Emulate a fresh load hass.data.pop(DATA_RESTORE_STATE_TASK) - entity = Timer( + entity = Timer.from_storage( { CONF_ID: "test", CONF_NAME: "test", @@ -807,7 +807,7 @@ async def test_restore_active_finished_outside_grace(hass): # Emulate a fresh load hass.data.pop(DATA_RESTORE_STATE_TASK) - entity = Timer( + entity = Timer.from_storage( { CONF_ID: "test", CONF_NAME: "test", diff --git a/tests/helpers/test_collection.py b/tests/helpers/test_collection.py index cd4bbba1a3e..0a141182065 100644 --- a/tests/helpers/test_collection.py +++ b/tests/helpers/test_collection.py @@ -1,4 +1,6 @@ """Tests for the collection helper.""" +from __future__ import annotations + import logging import pytest @@ -6,11 +8,11 @@ import voluptuous as vol from homeassistant.helpers import ( collection, - entity, entity_component, entity_registry as er, storage, ) +from homeassistant.helpers.typing import ConfigType from tests.common import flush_store @@ -29,13 +31,23 @@ def track_changes(coll: collection.ObservableCollection): return changes -class MockEntity(entity.Entity): +class MockEntity(collection.CollectionEntity): """Entity that is config based.""" def __init__(self, config): """Initialize entity.""" self._config = config + @classmethod + def from_storage(cls, config: ConfigType) -> MockEntity: + """Create instance from storage.""" + return cls(config) + + @classmethod + def from_yaml(cls, config: ConfigType) -> MockEntity: + """Create instance from storage.""" + raise NotImplementedError + @property def unique_id(self): """Return unique ID of entity.""" @@ -57,6 +69,17 @@ class MockEntity(entity.Entity): self.async_write_ha_state() +class MockObservableCollection(collection.ObservableCollection): + """Mock observable collection which can create entities.""" + + @staticmethod + def create_entity( + entity_class: type[collection.CollectionEntity], config: ConfigType + ) -> collection.CollectionEntity: + """Create a CollectionEntity instance.""" + return entity_class.from_storage(config) + + class MockStorageCollection(collection.StorageCollection): """Mock storage collection.""" @@ -231,7 +254,7 @@ async def test_storage_collection(hass): async def test_attach_entity_component_collection(hass): """Test attaching collection to entity component.""" ent_comp = entity_component.EntityComponent(_LOGGER, "test", hass) - coll = collection.ObservableCollection(_LOGGER) + coll = MockObservableCollection(_LOGGER) collection.sync_entity_lifecycle(hass, "test", "test", ent_comp, coll, MockEntity) await coll.notify_changes( @@ -270,7 +293,7 @@ async def test_attach_entity_component_collection(hass): async def test_entity_component_collection_abort(hass): """Test aborted entity adding is handled.""" ent_comp = entity_component.EntityComponent(_LOGGER, "test", hass) - coll = collection.ObservableCollection(_LOGGER) + coll = MockObservableCollection(_LOGGER) async_update_config_calls = [] async_remove_calls = [] @@ -336,7 +359,7 @@ async def test_entity_component_collection_abort(hass): async def test_entity_component_collection_entity_removed(hass): """Test entity removal is handled.""" ent_comp = entity_component.EntityComponent(_LOGGER, "test", hass) - coll = collection.ObservableCollection(_LOGGER) + coll = MockObservableCollection(_LOGGER) async_update_config_calls = [] async_remove_calls = [] From a3e6abd39614903c6d4d008044f4c12d9c752ea8 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sat, 3 Sep 2022 15:44:50 +0200 Subject: [PATCH 061/955] Use hass.config_entries.async_setup in mqtt test (#77750) * Use hass.config_entries.async_setup * The setup is awaited hence waiting is not needed --- tests/components/mqtt/test_device_trigger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/mqtt/test_device_trigger.py b/tests/components/mqtt/test_device_trigger.py index 661075c3cbe..6bec9a9094d 100644 --- a/tests/components/mqtt/test_device_trigger.py +++ b/tests/components/mqtt/test_device_trigger.py @@ -1439,7 +1439,7 @@ async def test_unload_entry(hass, calls, device_reg, mqtt_mock, tmp_path) -> Non new_yaml_config_file = tmp_path / "configuration.yaml" new_yaml_config_file.write_text("") with patch.object(hass_config, "YAML_CONFIG_FILE", new_yaml_config_file): - await mqtt_entry.async_setup(hass) + await hass.config_entries.async_setup(mqtt_entry.entry_id) # Rediscover and fake short press 3 async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", data1) From ab215b653abd98d513fd2b63b8eac4aa3f244acc Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sat, 3 Sep 2022 19:19:52 +0200 Subject: [PATCH 062/955] Fix upgrade api disabling during setup of Synology DSM (#77753) --- homeassistant/components/synology_dsm/common.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/synology_dsm/common.py b/homeassistant/components/synology_dsm/common.py index 12bad2954dd..82f2c214804 100644 --- a/homeassistant/components/synology_dsm/common.py +++ b/homeassistant/components/synology_dsm/common.py @@ -102,6 +102,7 @@ class SynoApi: self.dsm.upgrade.update() except SynologyDSMAPIErrorException as ex: self._with_upgrade = False + self.dsm.reset(SynoCoreUpgrade.API_KEY) LOGGER.debug("Disabled fetching upgrade data during setup: %s", ex) self._fetch_device_configuration() From cd24223c1f53ce0f1a80274e695cb596a53a610b Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Sat, 3 Sep 2022 22:18:56 +0200 Subject: [PATCH 063/955] Enhance operating time sensor in Overkiz integration (#76688) --- homeassistant/components/overkiz/sensor.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/overkiz/sensor.py b/homeassistant/components/overkiz/sensor.py index ac32c76c459..55b865c1aeb 100644 --- a/homeassistant/components/overkiz/sensor.py +++ b/homeassistant/components/overkiz/sensor.py @@ -354,10 +354,16 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ OverkizSensorDescription( key=OverkizState.IO_HEAT_PUMP_OPERATING_TIME, name="Heat Pump Operating Time", + icon="mdi:home-clock", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=TIME_SECONDS, ), OverkizSensorDescription( key=OverkizState.IO_ELECTRIC_BOOSTER_OPERATING_TIME, name="Electric Booster Operating Time", + icon="mdi:home-clock", + native_unit_of_measurement=TIME_SECONDS, + entity_category=EntityCategory.DIAGNOSTIC, ), # Cover OverkizSensorDescription( From fa987564a72219eda53a7938fcc1c623903756f9 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sat, 3 Sep 2022 16:53:21 -0400 Subject: [PATCH 064/955] Handle dead nodes in zwave_js update entity (#77763) --- homeassistant/components/zwave_js/update.py | 17 ++- tests/components/zwave_js/test_update.py | 140 +++++++++----------- 2 files changed, 77 insertions(+), 80 deletions(-) diff --git a/homeassistant/components/zwave_js/update.py b/homeassistant/components/zwave_js/update.py index 134c6cc6661..1f04c3acc47 100644 --- a/homeassistant/components/zwave_js/update.py +++ b/homeassistant/components/zwave_js/update.py @@ -86,17 +86,24 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): self._attr_installed_version = self._attr_latest_version = node.firmware_version - def _update_on_wake_up(self, _: dict[str, Any]) -> None: + def _update_on_status_change(self, _: dict[str, Any]) -> None: """Update the entity when node is awake.""" self._status_unsub = None self.hass.async_create_task(self.async_update(True)) async def async_update(self, write_state: bool = False) -> None: """Update the entity.""" - if self.node.status == NodeStatus.ASLEEP: - if not self._status_unsub: - self._status_unsub = self.node.once("wake up", self._update_on_wake_up) - return + for status, event_name in ( + (NodeStatus.ASLEEP, "wake up"), + (NodeStatus.DEAD, "alive"), + ): + if self.node.status == status: + if not self._status_unsub: + self._status_unsub = self.node.once( + event_name, self._update_on_status_change + ) + return + if available_firmware_updates := ( await self.driver.controller.async_get_available_firmware_updates( self.node, API_KEY_FIRMWARE_UPDATE_SERVICE diff --git a/tests/components/zwave_js/test_update.py b/tests/components/zwave_js/test_update.py index 852dcba5954..c9ec8fa68c6 100644 --- a/tests/components/zwave_js/test_update.py +++ b/tests/components/zwave_js/test_update.py @@ -24,6 +24,31 @@ from homeassistant.util import datetime as dt_util from tests.common import async_fire_time_changed UPDATE_ENTITY = "update.z_wave_thermostat_firmware" +FIRMWARE_UPDATES = { + "updates": [ + { + "version": "10.11.1", + "changelog": "blah 1", + "files": [ + {"target": 0, "url": "https://example1.com", "integrity": "sha1"} + ], + }, + { + "version": "11.2.4", + "changelog": "blah 2", + "files": [ + {"target": 0, "url": "https://example2.com", "integrity": "sha2"} + ], + }, + { + "version": "11.1.5", + "changelog": "blah 3", + "files": [ + {"target": 0, "url": "https://example3.com", "integrity": "sha3"} + ], + }, + ] +} async def test_update_entity_success( @@ -60,31 +85,7 @@ async def test_update_entity_success( result = await ws_client.receive_json() assert result["result"] is None - client.async_send_command.return_value = { - "updates": [ - { - "version": "10.11.1", - "changelog": "blah 1", - "files": [ - {"target": 0, "url": "https://example1.com", "integrity": "sha1"} - ], - }, - { - "version": "11.2.4", - "changelog": "blah 2", - "files": [ - {"target": 0, "url": "https://example2.com", "integrity": "sha2"} - ], - }, - { - "version": "11.1.5", - "changelog": "blah 3", - "files": [ - {"target": 0, "url": "https://example3.com", "integrity": "sha3"} - ], - }, - ] - } + client.async_send_command.return_value = FIRMWARE_UPDATES async_fire_time_changed(hass, dt_util.utcnow() + timedelta(days=2)) await hass.async_block_till_done() @@ -171,31 +172,7 @@ async def test_update_entity_failure( hass_ws_client, ): """Test update entity failed install.""" - client.async_send_command.return_value = { - "updates": [ - { - "version": "10.11.1", - "changelog": "blah 1", - "files": [ - {"target": 0, "url": "https://example1.com", "integrity": "sha1"} - ], - }, - { - "version": "11.2.4", - "changelog": "blah 2", - "files": [ - {"target": 0, "url": "https://example2.com", "integrity": "sha2"} - ], - }, - { - "version": "11.1.5", - "changelog": "blah 3", - "files": [ - {"target": 0, "url": "https://example3.com", "integrity": "sha3"} - ], - }, - ] - } + client.async_send_command.return_value = FIRMWARE_UPDATES async_fire_time_changed(hass, dt_util.utcnow() + timedelta(days=1)) await hass.async_block_till_done() @@ -228,31 +205,7 @@ async def test_update_entity_sleep( multisensor_6.receive_event(event) client.async_send_command.reset_mock() - client.async_send_command.return_value = { - "updates": [ - { - "version": "10.11.1", - "changelog": "blah 1", - "files": [ - {"target": 0, "url": "https://example1.com", "integrity": "sha1"} - ], - }, - { - "version": "11.2.4", - "changelog": "blah 2", - "files": [ - {"target": 0, "url": "https://example2.com", "integrity": "sha2"} - ], - }, - { - "version": "11.1.5", - "changelog": "blah 3", - "files": [ - {"target": 0, "url": "https://example3.com", "integrity": "sha3"} - ], - }, - ] - } + client.async_send_command.return_value = FIRMWARE_UPDATES async_fire_time_changed(hass, dt_util.utcnow() + timedelta(days=1)) await hass.async_block_till_done() @@ -273,3 +226,40 @@ async def test_update_entity_sleep( args = client.async_send_command.call_args_list[0][0][0] assert args["command"] == "controller.get_available_firmware_updates" assert args["nodeId"] == multisensor_6.node_id + + +async def test_update_entity_dead( + hass, + client, + multisensor_6, + integration, +): + """Test update occurs when device is dead after it becomes alive.""" + event = Event( + "dead", + data={"source": "node", "event": "dead", "nodeId": multisensor_6.node_id}, + ) + multisensor_6.receive_event(event) + client.async_send_command.reset_mock() + + client.async_send_command.return_value = FIRMWARE_UPDATES + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(days=1)) + await hass.async_block_till_done() + + # Because node is asleep we shouldn't attempt to check for firmware updates + assert len(client.async_send_command.call_args_list) == 0 + + event = Event( + "alive", + data={"source": "node", "event": "alive", "nodeId": multisensor_6.node_id}, + ) + multisensor_6.receive_event(event) + await hass.async_block_till_done() + + # Now that the node is up we can check for updates + assert len(client.async_send_command.call_args_list) > 0 + + args = client.async_send_command.call_args_list[0][0][0] + assert args["command"] == "controller.get_available_firmware_updates" + assert args["nodeId"] == multisensor_6.node_id From 5d7e9a6695eaa67b8319703fe1d10070fe493d75 Mon Sep 17 00:00:00 2001 From: Pete Date: Sat, 3 Sep 2022 22:55:34 +0200 Subject: [PATCH 065/955] Fix setting and reading percentage for MIOT based fans (#77626) --- homeassistant/components/xiaomi_miio/const.py | 9 +++++ homeassistant/components/xiaomi_miio/fan.py | 36 ++++++++++++++----- 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py index c0711a02a36..11922956c25 100644 --- a/homeassistant/components/xiaomi_miio/const.py +++ b/homeassistant/components/xiaomi_miio/const.py @@ -112,6 +112,15 @@ MODELS_FAN_MIOT = [ MODEL_FAN_ZA5, ] +# number of speed levels each fan has +SPEEDS_FAN_MIOT = { + MODEL_FAN_1C: 3, + MODEL_FAN_P10: 4, + MODEL_FAN_P11: 4, + MODEL_FAN_P9: 4, + MODEL_FAN_ZA5: 4, +} + MODELS_PURIFIER_MIOT = [ MODEL_AIRPURIFIER_3, MODEL_AIRPURIFIER_3C, diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index 39988976564..901211d1d2d 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -85,6 +85,7 @@ from .const import ( MODELS_PURIFIER_MIOT, SERVICE_RESET_FILTER, SERVICE_SET_EXTRA_FEATURES, + SPEEDS_FAN_MIOT, ) from .device import XiaomiCoordinatedMiioEntity @@ -234,9 +235,13 @@ async def async_setup_entry( elif model in MODELS_FAN_MIIO: entity = XiaomiFan(device, config_entry, unique_id, coordinator) elif model == MODEL_FAN_ZA5: - entity = XiaomiFanZA5(device, config_entry, unique_id, coordinator) + speed_count = SPEEDS_FAN_MIOT[model] + entity = XiaomiFanZA5(device, config_entry, unique_id, coordinator, speed_count) elif model in MODELS_FAN_MIOT: - entity = XiaomiFanMiot(device, config_entry, unique_id, coordinator) + speed_count = SPEEDS_FAN_MIOT[model] + entity = XiaomiFanMiot( + device, config_entry, unique_id, coordinator, speed_count + ) else: return @@ -1044,6 +1049,11 @@ class XiaomiFanP5(XiaomiGenericFan): class XiaomiFanMiot(XiaomiGenericFan): """Representation of a Xiaomi Fan Miot.""" + def __init__(self, device, entry, unique_id, coordinator, speed_count): + """Initialize MIOT fan with speed count.""" + super().__init__(device, entry, unique_id, coordinator) + self._speed_count = speed_count + @property def operation_mode_class(self): """Hold operation mode class.""" @@ -1061,7 +1071,9 @@ class XiaomiFanMiot(XiaomiGenericFan): self._preset_mode = self.coordinator.data.mode.name self._oscillating = self.coordinator.data.oscillate if self.coordinator.data.is_on: - self._percentage = self.coordinator.data.speed + self._percentage = ranged_value_to_percentage( + (1, self._speed_count), self.coordinator.data.speed + ) else: self._percentage = 0 @@ -1087,16 +1099,22 @@ class XiaomiFanMiot(XiaomiGenericFan): await self.async_turn_off() return - await self._try_command( - "Setting fan speed percentage of the miio device failed.", - self._device.set_speed, - percentage, + speed = math.ceil( + percentage_to_ranged_value((1, self._speed_count), percentage) ) - self._percentage = percentage + # if the fan is not on, we have to turn it on first if not self.is_on: await self.async_turn_on() - else: + + result = await self._try_command( + "Setting fan speed percentage of the miio device failed.", + self._device.set_speed, + speed, + ) + + if result: + self._percentage = ranged_value_to_percentage((1, self._speed_count), speed) self.async_write_ha_state() From 7ca7a28db94c64e80fb70cc2e75c5610c6c18009 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sat, 3 Sep 2022 23:19:05 +0200 Subject: [PATCH 066/955] Refactor zwave_js event handling (#77732) * Refactor zwave_js event handling * Clean up --- homeassistant/components/zwave_js/__init__.py | 585 ++++++++++-------- homeassistant/components/zwave_js/const.py | 1 - 2 files changed, 338 insertions(+), 248 deletions(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 538fe911dd0..03a8ee5fce2 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -3,7 +3,7 @@ from __future__ import annotations import asyncio from collections import defaultdict -from collections.abc import Callable +from collections.abc import Coroutine from typing import Any from async_timeout import timeout @@ -79,7 +79,6 @@ from .const import ( CONF_USB_PATH, CONF_USE_ADDON, DATA_CLIENT, - DATA_PLATFORM_SETUP, DOMAIN, EVENT_DEVICE_ADDED_TO_REGISTRY, LOGGER, @@ -104,7 +103,8 @@ from .services import ZWaveServices CONNECT_TIMEOUT = 10 DATA_CLIENT_LISTEN_TASK = "client_listen_task" -DATA_START_PLATFORM_TASK = "start_platform_task" +DATA_DRIVER_EVENTS = "driver_events" +DATA_START_CLIENT_TASK = "start_client_task" async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @@ -118,51 +118,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -@callback -def register_node_in_dev_reg( - hass: HomeAssistant, - entry: ConfigEntry, - dev_reg: device_registry.DeviceRegistry, - driver: Driver, - node: ZwaveNode, - remove_device_func: Callable[[device_registry.DeviceEntry], None], -) -> device_registry.DeviceEntry: - """Register node in dev reg.""" - device_id = get_device_id(driver, node) - device_id_ext = get_device_id_ext(driver, node) - device = dev_reg.async_get_device({device_id}) - - # Replace the device if it can be determined that this node is not the - # same product as it was previously. - if ( - device_id_ext - and device - and len(device.identifiers) == 2 - and device_id_ext not in device.identifiers - ): - remove_device_func(device) - device = None - - if device_id_ext: - ids = {device_id, device_id_ext} - else: - ids = {device_id} - - device = dev_reg.async_get_or_create( - config_entry_id=entry.entry_id, - identifiers=ids, - sw_version=node.firmware_version, - name=node.name or node.device_config.description or f"Node {node.node_id}", - model=node.device_config.label, - manufacturer=node.device_config.manufacturer, - suggested_area=node.location if node.location else UNDEFINED, - ) - - async_dispatcher_send(hass, EVENT_DEVICE_ADDED_TO_REGISTRY, device) - - return device - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Z-Wave JS from a config entry.""" if use_addon := entry.data.get(CONF_USE_ADDON): @@ -191,37 +146,40 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Set up websocket API async_register_api(hass) - platform_task = hass.async_create_task(start_platforms(hass, entry, client)) + # Create a task to allow the config entry to be unloaded before the driver is ready. + # Unloading the config entry is needed if the client listen task errors. + start_client_task = hass.async_create_task(start_client(hass, entry, client)) hass.data[DOMAIN].setdefault(entry.entry_id, {})[ - DATA_START_PLATFORM_TASK - ] = platform_task + DATA_START_CLIENT_TASK + ] = start_client_task return True -async def start_platforms( +async def start_client( hass: HomeAssistant, entry: ConfigEntry, client: ZwaveClient ) -> None: - """Start platforms and perform discovery.""" + """Start listening with the client.""" entry_hass_data: dict = hass.data[DOMAIN].setdefault(entry.entry_id, {}) entry_hass_data[DATA_CLIENT] = client - entry_hass_data[DATA_PLATFORM_SETUP] = {} - driver_ready = asyncio.Event() + driver_events = entry_hass_data[DATA_DRIVER_EVENTS] = DriverEvents(hass, entry) async def handle_ha_shutdown(event: Event) -> None: """Handle HA shutdown.""" await disconnect_client(hass, entry) - listen_task = asyncio.create_task(client_listen(hass, entry, client, driver_ready)) + listen_task = asyncio.create_task( + client_listen(hass, entry, client, driver_events.ready) + ) entry_hass_data[DATA_CLIENT_LISTEN_TASK] = listen_task entry.async_on_unload( hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, handle_ha_shutdown) ) try: - await driver_ready.wait() + await driver_events.ready.wait() except asyncio.CancelledError: - LOGGER.debug("Cancelling start platforms") + LOGGER.debug("Cancelling start client") return LOGGER.info("Connection to Zwave JS Server initialized") @@ -229,37 +187,289 @@ async def start_platforms( if client.driver is None: raise RuntimeError("Driver not ready.") - await setup_driver(hass, entry, client, client.driver) + await driver_events.setup(client.driver) -async def setup_driver( # noqa: C901 - hass: HomeAssistant, entry: ConfigEntry, client: ZwaveClient, driver: Driver -) -> None: - """Set up devices using the ready driver.""" - dev_reg = device_registry.async_get(hass) - ent_reg = entity_registry.async_get(hass) - entry_hass_data: dict = hass.data[DOMAIN].setdefault(entry.entry_id, {}) - platform_setup_tasks = entry_hass_data[DATA_PLATFORM_SETUP] - registered_unique_ids: dict[str, dict[str, set[str]]] = defaultdict(dict) - discovered_value_ids: dict[str, set[str]] = defaultdict(set) +class DriverEvents: + """Represent driver events.""" - async def async_setup_platform(platform: Platform) -> None: - """Set up platform if needed.""" - if platform not in platform_setup_tasks: - platform_setup_tasks[platform] = hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) + driver: Driver + + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + """Set up the driver events instance.""" + self.config_entry = entry + self.dev_reg = device_registry.async_get(hass) + self.hass = hass + self.platform_setup_tasks: dict[str, asyncio.Task] = {} + self.ready = asyncio.Event() + # Make sure to not pass self to ControllerEvents until all attributes are set. + self.controller_events = ControllerEvents(hass, self) + + async def setup(self, driver: Driver) -> None: + """Set up devices using the ready driver.""" + self.driver = driver + + # If opt in preference hasn't been specified yet, we do nothing, otherwise + # we apply the preference + if opted_in := self.config_entry.data.get(CONF_DATA_COLLECTION_OPTED_IN): + await async_enable_statistics(driver) + elif opted_in is False: + await driver.async_disable_statistics() + + # Check for nodes that no longer exist and remove them + stored_devices = device_registry.async_entries_for_config_entry( + self.dev_reg, self.config_entry.entry_id + ) + known_devices = [ + self.dev_reg.async_get_device({get_device_id(driver, node)}) + for node in driver.controller.nodes.values() + ] + + # Devices that are in the device registry that are not known by the controller can be removed + for device in stored_devices: + if device not in known_devices: + self.dev_reg.async_remove_device(device.id) + + # run discovery on all ready nodes + await asyncio.gather( + *( + self.controller_events.async_on_node_added(node) + for node in driver.controller.nodes.values() ) - await platform_setup_tasks[platform] + ) + + # listen for new nodes being added to the mesh + self.config_entry.async_on_unload( + driver.controller.on( + "node added", + lambda event: self.hass.async_create_task( + self.controller_events.async_on_node_added(event["node"]) + ), + ) + ) + # listen for nodes being removed from the mesh + # NOTE: This will not remove nodes that were removed when HA was not running + self.config_entry.async_on_unload( + driver.controller.on( + "node removed", self.controller_events.async_on_node_removed + ) + ) + + async def async_setup_platform(self, platform: Platform) -> None: + """Set up platform if needed.""" + if platform not in self.platform_setup_tasks: + self.platform_setup_tasks[platform] = self.hass.async_create_task( + self.hass.config_entries.async_forward_entry_setup( + self.config_entry, platform + ) + ) + await self.platform_setup_tasks[platform] + + +class ControllerEvents: + """Represent controller events. + + Handle the following events: + - node added + - node removed + """ + + def __init__(self, hass: HomeAssistant, driver_events: DriverEvents) -> None: + """Set up the controller events instance.""" + self.hass = hass + self.config_entry = driver_events.config_entry + self.discovered_value_ids: dict[str, set[str]] = defaultdict(set) + self.driver_events = driver_events + self.dev_reg = driver_events.dev_reg + self.registered_unique_ids: dict[str, dict[str, set[str]]] = defaultdict(dict) + self.node_events = NodeEvents(hass, self) @callback - def remove_device(device: device_registry.DeviceEntry) -> None: + def remove_device(self, device: device_registry.DeviceEntry) -> None: """Remove device from registry.""" # note: removal of entity registry entry is handled by core - dev_reg.async_remove_device(device.id) - registered_unique_ids.pop(device.id, None) - discovered_value_ids.pop(device.id, None) + self.dev_reg.async_remove_device(device.id) + self.registered_unique_ids.pop(device.id, None) + self.discovered_value_ids.pop(device.id, None) + + async def async_on_node_added(self, node: ZwaveNode) -> None: + """Handle node added event.""" + # No need for a ping button or node status sensor for controller nodes + if not node.is_controller_node: + # Create a node status sensor for each device + await self.driver_events.async_setup_platform(Platform.SENSOR) + async_dispatcher_send( + self.hass, + f"{DOMAIN}_{self.config_entry.entry_id}_add_node_status_sensor", + node, + ) + + # Create a ping button for each device + await self.driver_events.async_setup_platform(Platform.BUTTON) + async_dispatcher_send( + self.hass, + f"{DOMAIN}_{self.config_entry.entry_id}_add_ping_button_entity", + node, + ) + + # Create a firmware update entity for each device + await self.driver_events.async_setup_platform(Platform.UPDATE) + async_dispatcher_send( + self.hass, + f"{DOMAIN}_{self.config_entry.entry_id}_add_firmware_update_entity", + node, + ) + + # we only want to run discovery when the node has reached ready state, + # otherwise we'll have all kinds of missing info issues. + if node.ready: + await self.node_events.async_on_node_ready(node) + return + # if node is not yet ready, register one-time callback for ready state + LOGGER.debug("Node added: %s - waiting for it to become ready", node.node_id) + node.once( + "ready", + lambda event: self.hass.async_create_task( + self.node_events.async_on_node_ready(event["node"]) + ), + ) + # we do submit the node to device registry so user has + # some visual feedback that something is (in the process of) being added + self.register_node_in_dev_reg(node) + + @callback + def async_on_node_removed(self, event: dict) -> None: + """Handle node removed event.""" + node: ZwaveNode = event["node"] + replaced: bool = event.get("replaced", False) + # grab device in device registry attached to this node + dev_id = get_device_id(self.driver_events.driver, node) + device = self.dev_reg.async_get_device({dev_id}) + # We assert because we know the device exists + assert device + if replaced: + self.discovered_value_ids.pop(device.id, None) + + async_dispatcher_send( + self.hass, + f"{DOMAIN}_{get_valueless_base_unique_id(self.driver_events.driver, node)}_remove_entity", + ) + else: + self.remove_device(device) + + @callback + def register_node_in_dev_reg(self, node: ZwaveNode) -> device_registry.DeviceEntry: + """Register node in dev reg.""" + driver = self.driver_events.driver + device_id = get_device_id(driver, node) + device_id_ext = get_device_id_ext(driver, node) + device = self.dev_reg.async_get_device({device_id}) + + # Replace the device if it can be determined that this node is not the + # same product as it was previously. + if ( + device_id_ext + and device + and len(device.identifiers) == 2 + and device_id_ext not in device.identifiers + ): + self.remove_device(device) + device = None + + if device_id_ext: + ids = {device_id, device_id_ext} + else: + ids = {device_id} + + device = self.dev_reg.async_get_or_create( + config_entry_id=self.config_entry.entry_id, + identifiers=ids, + sw_version=node.firmware_version, + name=node.name or node.device_config.description or f"Node {node.node_id}", + model=node.device_config.label, + manufacturer=node.device_config.manufacturer, + suggested_area=node.location if node.location else UNDEFINED, + ) + + async_dispatcher_send(self.hass, EVENT_DEVICE_ADDED_TO_REGISTRY, device) + + return device + + +class NodeEvents: + """Represent node events. + + Handle the following events: + - ready + - value added + - value updated + - metadata updated + - value notification + - notification + """ + + def __init__( + self, hass: HomeAssistant, controller_events: ControllerEvents + ) -> None: + """Set up the node events instance.""" + self.config_entry = controller_events.config_entry + self.controller_events = controller_events + self.dev_reg = controller_events.dev_reg + self.ent_reg = entity_registry.async_get(hass) + self.hass = hass + + async def async_on_node_ready(self, node: ZwaveNode) -> None: + """Handle node ready event.""" + LOGGER.debug("Processing node %s", node) + # register (or update) node in device registry + device = self.controller_events.register_node_in_dev_reg(node) + # We only want to create the defaultdict once, even on reinterviews + if device.id not in self.controller_events.registered_unique_ids: + self.controller_events.registered_unique_ids[device.id] = defaultdict(set) + + value_updates_disc_info: dict[str, ZwaveDiscoveryInfo] = {} + + # run discovery on all node values and create/update entities + await asyncio.gather( + *( + self.async_handle_discovery_info( + device, disc_info, value_updates_disc_info + ) + for disc_info in async_discover_node_values( + node, device, self.controller_events.discovered_value_ids + ) + ) + ) + + # add listeners to handle new values that get added later + for event in ("value added", "value updated", "metadata updated"): + self.config_entry.async_on_unload( + node.on( + event, + lambda event: self.hass.async_create_task( + self.async_on_value_added( + value_updates_disc_info, event["value"] + ) + ), + ) + ) + + # add listener for stateless node value notification events + self.config_entry.async_on_unload( + node.on( + "value notification", + lambda event: self.async_on_value_notification( + event["value_notification"] + ), + ) + ) + # add listener for stateless node notification events + self.config_entry.async_on_unload( + node.on("notification", self.async_on_notification) + ) async def async_handle_discovery_info( + self, device: device_registry.DeviceEntry, disc_info: ZwaveDiscoveryInfo, value_updates_disc_info: dict[str, ZwaveDiscoveryInfo], @@ -269,20 +479,22 @@ async def setup_driver( # noqa: C901 # the value_id format. Some time in the future, this call (as well as the # helper functions) can be removed. async_migrate_discovered_value( - hass, - ent_reg, - registered_unique_ids[device.id][disc_info.platform], + self.hass, + self.ent_reg, + self.controller_events.registered_unique_ids[device.id][disc_info.platform], device, - driver, + self.controller_events.driver_events.driver, disc_info, ) platform = disc_info.platform - await async_setup_platform(platform) + await self.controller_events.driver_events.async_setup_platform(platform) LOGGER.debug("Discovered entity: %s", disc_info) async_dispatcher_send( - hass, f"{DOMAIN}_{entry.entry_id}_add_{platform}", disc_info + self.hass, + f"{DOMAIN}_{self.config_entry.entry_id}_add_{platform}", + disc_info, ) # If we don't need to watch for updates return early @@ -294,151 +506,57 @@ async def setup_driver( # noqa: C901 if len(value_updates_disc_info) != 1: return # add listener for value updated events - entry.async_on_unload( + self.config_entry.async_on_unload( disc_info.node.on( "value updated", - lambda event: async_on_value_updated_fire_event( + lambda event: self.async_on_value_updated_fire_event( value_updates_disc_info, event["value"] ), ) ) - async def async_on_node_ready(node: ZwaveNode) -> None: - """Handle node ready event.""" - LOGGER.debug("Processing node %s", node) - # register (or update) node in device registry - device = register_node_in_dev_reg( - hass, entry, dev_reg, driver, node, remove_device - ) - # We only want to create the defaultdict once, even on reinterviews - if device.id not in registered_unique_ids: - registered_unique_ids[device.id] = defaultdict(set) - - value_updates_disc_info: dict[str, ZwaveDiscoveryInfo] = {} - - # run discovery on all node values and create/update entities - await asyncio.gather( - *( - async_handle_discovery_info(device, disc_info, value_updates_disc_info) - for disc_info in async_discover_node_values( - node, device, discovered_value_ids - ) - ) - ) - - # add listeners to handle new values that get added later - for event in ("value added", "value updated", "metadata updated"): - entry.async_on_unload( - node.on( - event, - lambda event: hass.async_create_task( - async_on_value_added(value_updates_disc_info, event["value"]) - ), - ) - ) - - # add listener for stateless node value notification events - entry.async_on_unload( - node.on( - "value notification", - lambda event: async_on_value_notification(event["value_notification"]), - ) - ) - # add listener for stateless node notification events - entry.async_on_unload(node.on("notification", async_on_notification)) - - async def async_on_node_added(node: ZwaveNode) -> None: - """Handle node added event.""" - # No need for a ping button or node status sensor for controller nodes - if not node.is_controller_node: - # Create a node status sensor for each device - await async_setup_platform(Platform.SENSOR) - async_dispatcher_send( - hass, f"{DOMAIN}_{entry.entry_id}_add_node_status_sensor", node - ) - - # Create a ping button for each device - await async_setup_platform(Platform.BUTTON) - async_dispatcher_send( - hass, f"{DOMAIN}_{entry.entry_id}_add_ping_button_entity", node - ) - - # Create a firmware update entity for each device - await async_setup_platform(Platform.UPDATE) - async_dispatcher_send( - hass, f"{DOMAIN}_{entry.entry_id}_add_firmware_update_entity", node - ) - - # we only want to run discovery when the node has reached ready state, - # otherwise we'll have all kinds of missing info issues. - if node.ready: - await async_on_node_ready(node) - return - # if node is not yet ready, register one-time callback for ready state - LOGGER.debug("Node added: %s - waiting for it to become ready", node.node_id) - node.once( - "ready", - lambda event: hass.async_create_task(async_on_node_ready(event["node"])), - ) - # we do submit the node to device registry so user has - # some visual feedback that something is (in the process of) being added - register_node_in_dev_reg(hass, entry, dev_reg, driver, node, remove_device) - async def async_on_value_added( - value_updates_disc_info: dict[str, ZwaveDiscoveryInfo], value: Value + self, value_updates_disc_info: dict[str, ZwaveDiscoveryInfo], value: Value ) -> None: """Fire value updated event.""" # If node isn't ready or a device for this node doesn't already exist, we can # let the node ready event handler perform discovery. If a value has already # been processed, we don't need to do it again - device_id = get_device_id(driver, value.node) + device_id = get_device_id( + self.controller_events.driver_events.driver, value.node + ) if ( not value.node.ready - or not (device := dev_reg.async_get_device({device_id})) - or value.value_id in discovered_value_ids[device.id] + or not (device := self.dev_reg.async_get_device({device_id})) + or value.value_id in self.controller_events.discovered_value_ids[device.id] ): return LOGGER.debug("Processing node %s added value %s", value.node, value) await asyncio.gather( *( - async_handle_discovery_info(device, disc_info, value_updates_disc_info) + self.async_handle_discovery_info( + device, disc_info, value_updates_disc_info + ) for disc_info in async_discover_single_value( - value, device, discovered_value_ids + value, device, self.controller_events.discovered_value_ids ) ) ) @callback - def async_on_node_removed(event: dict) -> None: - """Handle node removed event.""" - node: ZwaveNode = event["node"] - replaced: bool = event.get("replaced", False) - # grab device in device registry attached to this node - dev_id = get_device_id(driver, node) - device = dev_reg.async_get_device({dev_id}) - # We assert because we know the device exists - assert device - if replaced: - discovered_value_ids.pop(device.id, None) - - async_dispatcher_send( - hass, - f"{DOMAIN}_{get_valueless_base_unique_id(driver, node)}_remove_entity", - ) - else: - remove_device(device) - - @callback - def async_on_value_notification(notification: ValueNotification) -> None: + def async_on_value_notification(self, notification: ValueNotification) -> None: """Relay stateless value notification events from Z-Wave nodes to hass.""" - device = dev_reg.async_get_device({get_device_id(driver, notification.node)}) + driver = self.controller_events.driver_events.driver + device = self.dev_reg.async_get_device( + {get_device_id(driver, notification.node)} + ) # We assert because we know the device exists assert device raw_value = value = notification.value if notification.metadata.states: value = notification.metadata.states.get(str(value), value) - hass.bus.async_fire( + self.hass.bus.async_fire( ZWAVE_JS_VALUE_NOTIFICATION_EVENT, { ATTR_DOMAIN: DOMAIN, @@ -459,15 +577,19 @@ async def setup_driver( # noqa: C901 ) @callback - def async_on_notification(event: dict[str, Any]) -> None: + def async_on_notification(self, event: dict[str, Any]) -> None: """Relay stateless notification events from Z-Wave nodes to hass.""" if "notification" not in event: LOGGER.info("Unknown notification: %s", event) return + + driver = self.controller_events.driver_events.driver notification: EntryControlNotification | NotificationNotification | PowerLevelNotification | MultilevelSwitchNotification = event[ "notification" ] - device = dev_reg.async_get_device({get_device_id(driver, notification.node)}) + device = self.dev_reg.async_get_device( + {get_device_id(driver, notification.node)} + ) # We assert because we know the device exists assert device event_data = { @@ -521,31 +643,35 @@ async def setup_driver( # noqa: C901 else: raise TypeError(f"Unhandled notification type: {notification}") - hass.bus.async_fire(ZWAVE_JS_NOTIFICATION_EVENT, event_data) + self.hass.bus.async_fire(ZWAVE_JS_NOTIFICATION_EVENT, event_data) @callback def async_on_value_updated_fire_event( - value_updates_disc_info: dict[str, ZwaveDiscoveryInfo], value: Value + self, value_updates_disc_info: dict[str, ZwaveDiscoveryInfo], value: Value ) -> None: """Fire value updated event.""" # Get the discovery info for the value that was updated. If there is # no discovery info for this value, we don't need to fire an event if value.value_id not in value_updates_disc_info: return + + driver = self.controller_events.driver_events.driver disc_info = value_updates_disc_info[value.value_id] - device = dev_reg.async_get_device({get_device_id(driver, value.node)}) + device = self.dev_reg.async_get_device({get_device_id(driver, value.node)}) # We assert because we know the device exists assert device unique_id = get_unique_id(driver, disc_info.primary_value.value_id) - entity_id = ent_reg.async_get_entity_id(disc_info.platform, DOMAIN, unique_id) + entity_id = self.ent_reg.async_get_entity_id( + disc_info.platform, DOMAIN, unique_id + ) raw_value = value_ = value.value if value.metadata.states: value_ = value.metadata.states.get(str(value), value_) - hass.bus.async_fire( + self.hass.bus.async_fire( ZWAVE_JS_VALUE_UPDATED_EVENT, { ATTR_NODE_ID: value.node.node_id, @@ -564,43 +690,6 @@ async def setup_driver( # noqa: C901 }, ) - # If opt in preference hasn't been specified yet, we do nothing, otherwise - # we apply the preference - if opted_in := entry.data.get(CONF_DATA_COLLECTION_OPTED_IN): - await async_enable_statistics(driver) - elif opted_in is False: - await driver.async_disable_statistics() - - # Check for nodes that no longer exist and remove them - stored_devices = device_registry.async_entries_for_config_entry( - dev_reg, entry.entry_id - ) - known_devices = [ - dev_reg.async_get_device({get_device_id(driver, node)}) - for node in driver.controller.nodes.values() - ] - - # Devices that are in the device registry that are not known by the controller can be removed - for device in stored_devices: - if device not in known_devices: - dev_reg.async_remove_device(device.id) - - # run discovery on all ready nodes - await asyncio.gather( - *(async_on_node_added(node) for node in driver.controller.nodes.values()) - ) - - # listen for new nodes being added to the mesh - entry.async_on_unload( - driver.controller.on( - "node added", - lambda event: hass.async_create_task(async_on_node_added(event["node"])), - ) - ) - # listen for nodes being removed from the mesh - # NOTE: This will not remove nodes that were removed when HA was not running - entry.async_on_unload(driver.controller.on("node removed", async_on_node_removed)) - async def client_listen( hass: HomeAssistant, @@ -633,14 +722,15 @@ async def disconnect_client(hass: HomeAssistant, entry: ConfigEntry) -> None: data = hass.data[DOMAIN][entry.entry_id] client: ZwaveClient = data[DATA_CLIENT] listen_task: asyncio.Task = data[DATA_CLIENT_LISTEN_TASK] - platform_task: asyncio.Task = data[DATA_START_PLATFORM_TASK] + start_client_task: asyncio.Task = data[DATA_START_CLIENT_TASK] + driver_events: DriverEvents = data[DATA_DRIVER_EVENTS] listen_task.cancel() - platform_task.cancel() - platform_setup_tasks = data.get(DATA_PLATFORM_SETUP, {}).values() + start_client_task.cancel() + platform_setup_tasks = driver_events.platform_setup_tasks.values() for task in platform_setup_tasks: task.cancel() - await asyncio.gather(listen_task, platform_task, *platform_setup_tasks) + await asyncio.gather(listen_task, start_client_task, *platform_setup_tasks) if client.connected: await client.disconnect() @@ -650,9 +740,10 @@ async def disconnect_client(hass: HomeAssistant, entry: ConfigEntry) -> None: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" info = hass.data[DOMAIN][entry.entry_id] + driver_events: DriverEvents = info[DATA_DRIVER_EVENTS] - tasks = [] - for platform, task in info[DATA_PLATFORM_SETUP].items(): + tasks: list[asyncio.Task | Coroutine] = [] + for platform, task in driver_events.platform_setup_tasks.items(): if task.done(): tasks.append( hass.config_entries.async_forward_entry_unload(entry, platform) diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index ddd4917e596..db3da247e7d 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -21,7 +21,6 @@ CONF_DATA_COLLECTION_OPTED_IN = "data_collection_opted_in" DOMAIN = "zwave_js" DATA_CLIENT = "client" -DATA_PLATFORM_SETUP = "platform_setup" EVENT_DEVICE_ADDED_TO_REGISTRY = f"{DOMAIN}_device_added_to_registry" From 6921583410de3d5797a022ef5c8bcdc23ba12013 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 4 Sep 2022 00:28:06 +0000 Subject: [PATCH 067/955] [ci skip] Translation update --- .../components/bthome/translations/bg.json | 19 ++++++++ .../dlna_dmr/translations/zh-Hant.json | 2 +- .../dlna_dms/translations/zh-Hant.json | 2 +- .../components/ecowitt/translations/bg.json | 7 +++ .../components/ecowitt/translations/ja.json | 3 +- .../components/icloud/translations/bg.json | 5 +++ .../components/kodi/translations/zh-Hant.json | 2 +- .../components/led_ble/translations/bg.json | 19 ++++++++ .../litterrobot/translations/bg.json | 9 ++++ .../litterrobot/translations/ja.json | 1 + .../components/melnor/translations/ja.json | 13 ++++++ .../melnor/translations/zh-Hant.json | 2 +- .../components/mqtt/translations/ja.json | 5 +++ .../components/nest/translations/ja.json | 1 + .../components/nest/translations/nl.json | 5 +++ .../components/nobo_hub/translations/bg.json | 19 ++++++++ .../components/nobo_hub/translations/de.json | 44 +++++++++++++++++++ .../components/nobo_hub/translations/el.json | 44 +++++++++++++++++++ .../components/nobo_hub/translations/es.json | 44 +++++++++++++++++++ .../components/nobo_hub/translations/fr.json | 32 ++++++++++++++ .../components/nobo_hub/translations/hu.json | 44 +++++++++++++++++++ .../components/nobo_hub/translations/id.json | 19 ++++++++ .../components/nobo_hub/translations/it.json | 31 +++++++++++++ .../components/nobo_hub/translations/ja.json | 43 ++++++++++++++++++ .../components/nobo_hub/translations/no.json | 44 +++++++++++++++++++ .../nobo_hub/translations/pt-BR.json | 44 +++++++++++++++++++ .../nobo_hub/translations/zh-Hant.json | 44 +++++++++++++++++++ .../components/overkiz/translations/ja.json | 3 +- .../components/prusalink/translations/bg.json | 17 +++++++ .../components/prusalink/translations/ja.json | 1 + .../components/risco/translations/bg.json | 5 +++ .../components/sensor/translations/ja.json | 2 + .../components/sensorpro/translations/bg.json | 15 +++++++ .../components/skybell/translations/bg.json | 6 +++ .../thermobeacon/translations/bg.json | 18 ++++++++ .../components/thermopro/translations/bg.json | 18 ++++++++ .../unifiprotect/translations/ja.json | 4 ++ .../volumio/translations/zh-Hant.json | 2 +- .../volvooncall/translations/bg.json | 7 +++ .../volvooncall/translations/ja.json | 3 +- .../components/zha/translations/bg.json | 15 +++++++ 41 files changed, 655 insertions(+), 8 deletions(-) create mode 100644 homeassistant/components/bthome/translations/bg.json create mode 100644 homeassistant/components/ecowitt/translations/bg.json create mode 100644 homeassistant/components/led_ble/translations/bg.json create mode 100644 homeassistant/components/melnor/translations/ja.json create mode 100644 homeassistant/components/nobo_hub/translations/bg.json create mode 100644 homeassistant/components/nobo_hub/translations/de.json create mode 100644 homeassistant/components/nobo_hub/translations/el.json create mode 100644 homeassistant/components/nobo_hub/translations/es.json create mode 100644 homeassistant/components/nobo_hub/translations/fr.json create mode 100644 homeassistant/components/nobo_hub/translations/hu.json create mode 100644 homeassistant/components/nobo_hub/translations/id.json create mode 100644 homeassistant/components/nobo_hub/translations/it.json create mode 100644 homeassistant/components/nobo_hub/translations/ja.json create mode 100644 homeassistant/components/nobo_hub/translations/no.json create mode 100644 homeassistant/components/nobo_hub/translations/pt-BR.json create mode 100644 homeassistant/components/nobo_hub/translations/zh-Hant.json create mode 100644 homeassistant/components/prusalink/translations/bg.json create mode 100644 homeassistant/components/sensorpro/translations/bg.json create mode 100644 homeassistant/components/thermobeacon/translations/bg.json create mode 100644 homeassistant/components/thermopro/translations/bg.json create mode 100644 homeassistant/components/volvooncall/translations/bg.json diff --git a/homeassistant/components/bthome/translations/bg.json b/homeassistant/components/bthome/translations/bg.json new file mode 100644 index 00000000000..e8427c23986 --- /dev/null +++ b/homeassistant/components/bthome/translations/bg.json @@ -0,0 +1,19 @@ +{ + "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\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" + }, + "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" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dmr/translations/zh-Hant.json b/homeassistant/components/dlna_dmr/translations/zh-Hant.json index 07293607685..571f6dcfd09 100644 --- a/homeassistant/components/dlna_dmr/translations/zh-Hant.json +++ b/homeassistant/components/dlna_dmr/translations/zh-Hant.json @@ -33,7 +33,7 @@ "host": "\u4e3b\u6a5f\u7aef" }, "description": "\u9078\u64c7\u88dd\u7f6e\u9032\u884c\u8a2d\u5b9a\u6216\u4fdd\u7559\u7a7a\u767d\u4ee5\u8f38\u5165 URL", - "title": "\u5df2\u767c\u73fe\u7684 DLNA DMR \u88dd\u7f6e" + "title": "\u81ea\u52d5\u641c\u7d22\u5230\u7684 DLNA DMR \u88dd\u7f6e" } } }, diff --git a/homeassistant/components/dlna_dms/translations/zh-Hant.json b/homeassistant/components/dlna_dms/translations/zh-Hant.json index 404b9b29b9a..b2f5f3e147b 100644 --- a/homeassistant/components/dlna_dms/translations/zh-Hant.json +++ b/homeassistant/components/dlna_dms/translations/zh-Hant.json @@ -17,7 +17,7 @@ "host": "\u4e3b\u6a5f\u7aef" }, "description": "\u9078\u64c7\u6240\u8981\u8a2d\u5b9a\u7684\u88dd\u7f6e", - "title": "\u5df2\u767c\u73fe\u7684 DLNA DMA \u88dd\u7f6e" + "title": "\u81ea\u52d5\u641c\u7d22\u5230\u7684 DLNA DMA \u88dd\u7f6e" } } } diff --git a/homeassistant/components/ecowitt/translations/bg.json b/homeassistant/components/ecowitt/translations/bg.json new file mode 100644 index 00000000000..5d274ec2b73 --- /dev/null +++ b/homeassistant/components/ecowitt/translations/bg.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecowitt/translations/ja.json b/homeassistant/components/ecowitt/translations/ja.json index 0ac12f0af77..821eb535e7b 100644 --- a/homeassistant/components/ecowitt/translations/ja.json +++ b/homeassistant/components/ecowitt/translations/ja.json @@ -9,7 +9,8 @@ "data": { "path": "\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u30c8\u30fc\u30af\u30f3\u3092\u542b\u3080\u30d1\u30b9", "port": "\u30ea\u30b9\u30cb\u30f3\u30b0\u30dd\u30fc\u30c8" - } + }, + "description": "Ecowitt\u3092\u3001\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u3066\u3082\u3088\u308d\u3057\u3044\u3067\u3059\u304b\uff1f" } } } diff --git a/homeassistant/components/icloud/translations/bg.json b/homeassistant/components/icloud/translations/bg.json index bd81093beb0..c4ebf72f36a 100644 --- a/homeassistant/components/icloud/translations/bg.json +++ b/homeassistant/components/icloud/translations/bg.json @@ -14,6 +14,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/kodi/translations/zh-Hant.json b/homeassistant/components/kodi/translations/zh-Hant.json index 169ad92e96b..7e72914d672 100644 --- a/homeassistant/components/kodi/translations/zh-Hant.json +++ b/homeassistant/components/kodi/translations/zh-Hant.json @@ -23,7 +23,7 @@ }, "discovery_confirm": { "description": "\u662f\u5426\u8981\u65b0\u589e Kodi (`{name}`) \u81f3 Home Assistant\uff1f", - "title": "\u5df2\u767c\u73fe\u7684 Kodi" + "title": "\u81ea\u52d5\u641c\u7d22\u5230\u7684 Kodi" }, "user": { "data": { diff --git a/homeassistant/components/led_ble/translations/bg.json b/homeassistant/components/led_ble/translations/bg.json new file mode 100644 index 00000000000..049c2684415 --- /dev/null +++ b/homeassistant/components/led_ble/translations/bg.json @@ -0,0 +1,19 @@ +{ + "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", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "Bluetooth \u0430\u0434\u0440\u0435\u0441" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/bg.json b/homeassistant/components/litterrobot/translations/bg.json index bad1fba5a87..657ad4878be 100644 --- a/homeassistant/components/litterrobot/translations/bg.json +++ b/homeassistant/components/litterrobot/translations/bg.json @@ -1,9 +1,18 @@ { "config": { + "abort": { + "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\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + }, + "description": "\u041c\u043e\u043b\u044f, \u0430\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u0439\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u0430\u0442\u0430 \u0441\u0438 \u0437\u0430 {username}" + }, "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u0430", diff --git a/homeassistant/components/litterrobot/translations/ja.json b/homeassistant/components/litterrobot/translations/ja.json index 6972cf2318a..0106d4b63c6 100644 --- a/homeassistant/components/litterrobot/translations/ja.json +++ b/homeassistant/components/litterrobot/translations/ja.json @@ -14,6 +14,7 @@ "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, + "description": "{username} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u66f4\u65b0\u3057\u3066\u304f\u3060\u3055\u3044", "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "user": { diff --git a/homeassistant/components/melnor/translations/ja.json b/homeassistant/components/melnor/translations/ja.json new file mode 100644 index 00000000000..5fe71d5781c --- /dev/null +++ b/homeassistant/components/melnor/translations/ja.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Melnor Bluetooth\u30c7\u30d0\u30a4\u30b9\u304c\u8fd1\u304f\u306b\u3042\u308a\u307e\u305b\u3093\u3002" + }, + "step": { + "bluetooth_confirm": { + "description": "Melnor Bluetooth\u30d0\u30eb\u30d6 `{name}` \u3092\u3001Home Assistan\u306b\u8ffd\u52a0\u3057\u307e\u3059\u304b\uff1f", + "title": "Melnor Bluetooth\u30d0\u30eb\u30d6\u3092\u767a\u898b" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/melnor/translations/zh-Hant.json b/homeassistant/components/melnor/translations/zh-Hant.json index 71bc8f33716..0374906bb4b 100644 --- a/homeassistant/components/melnor/translations/zh-Hant.json +++ b/homeassistant/components/melnor/translations/zh-Hant.json @@ -6,7 +6,7 @@ "step": { "bluetooth_confirm": { "description": "\u662f\u5426\u8981\u5c07 Melnor Bluetooth valve `{name}`\u65b0\u589e\u81f3 Home Assistant\uff1f", - "title": "\u5df2\u767c\u73fe\u7684 Melnor Bluetooth valve" + "title": "\u81ea\u52d5\u641c\u7d22\u5230\u7684 Melnor Bluetooth valve" } } } diff --git a/homeassistant/components/mqtt/translations/ja.json b/homeassistant/components/mqtt/translations/ja.json index 07af6230aa6..dd344b59374 100644 --- a/homeassistant/components/mqtt/translations/ja.json +++ b/homeassistant/components/mqtt/translations/ja.json @@ -49,6 +49,11 @@ "button_triple_press": "\"{subtype}\" 3\u56de\u30af\u30ea\u30c3\u30af" } }, + "issues": { + "deprecated_yaml": { + "title": "\u624b\u52d5\u3067\u8a2d\u5b9a\u3057\u305f\u3001MQTT {platform} \u306b\u306f\u6ce8\u610f\u304c\u5fc5\u8981\u3067\u3059" + } + }, "options": { "error": { "bad_birth": "(\u7121\u52b9\u306a)Invalid birth topic.", diff --git a/homeassistant/components/nest/translations/ja.json b/homeassistant/components/nest/translations/ja.json index 06c5e54ef88..f7c3afd128a 100644 --- a/homeassistant/components/nest/translations/ja.json +++ b/homeassistant/components/nest/translations/ja.json @@ -31,6 +31,7 @@ "title": "Google\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u30ea\u30f3\u30af\u3059\u308b" }, "auth_upgrade": { + "description": "App Auth\u306f\u3001\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u3092\u5411\u4e0a\u3055\u305b\u308b\u305f\u3081\u306bGoogle\u306b\u3088\u3063\u3066\u5ec3\u6b62\u3055\u308c\u307e\u3057\u305f\u3002\u65b0\u3057\u3044\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u306e\u8cc7\u683c\u60c5\u5831\u3092\u4f5c\u6210\u3059\u308b\u3053\u3068\u304c\u5fc5\u8981\u3067\u3059\u3002 \n\n [\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8]({more_info_url}) \u3092\u958b\u304d\u3001\u6b21\u306e\u624b\u9806\u306b\u5f93\u3063\u3066\u3001Nest\u30c7\u30d0\u30a4\u30b9\u3078\u306e\u30a2\u30af\u30bb\u30b9\u3092\u5fa9\u5143\u3059\u308b\u305f\u3081\u306b\u5fc5\u8981\u306a\u624b\u9806\u3092\u8aac\u660e\u3057\u307e\u3059\u3002", "title": "\u30cd\u30b9\u30c8: \u30a2\u30d7\u30ea\u8a8d\u8a3c\u306e\u975e\u63a8\u5968" }, "cloud_project": { diff --git a/homeassistant/components/nest/translations/nl.json b/homeassistant/components/nest/translations/nl.json index 8f5e0db9900..95ddcd4184d 100644 --- a/homeassistant/components/nest/translations/nl.json +++ b/homeassistant/components/nest/translations/nl.json @@ -67,5 +67,10 @@ "camera_sound": "Geluid gedetecteerd", "doorbell_chime": "Deurbel is ingedrukt" } + }, + "issues": { + "removed_app_auth": { + "description": "Om de beveiliging te verbeteren en het risico op phishing te verminderen, heeft Google de authenticatiemethode die door Home Assistant wordt gebruikt, be\u00ebindigd. \n\n **Dit vereist actie van jou om dit op te lossen** ([meer info]( {more_info_url} )) \n\n 1. Bezoek de integratiepagina.\n 2. Klik op Opnieuw configureren in de Nest-integratie.\n 3. Home Assistant leidt je door de stappen om te upgraden naar webauthenticatie. \n\n Zie de Nest [integratie-instructies]( {documentation_url} ) voor informatie over het oplossen van problemen." + } } } \ No newline at end of file diff --git a/homeassistant/components/nobo_hub/translations/bg.json b/homeassistant/components/nobo_hub/translations/bg.json new file mode 100644 index 00000000000..421434da3fd --- /dev/null +++ b/homeassistant/components/nobo_hub/translations/bg.json @@ -0,0 +1,19 @@ +{ + "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": { + "invalid_ip": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d IP \u0430\u0434\u0440\u0435\u0441", + "invalid_serial": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d \u0441\u0435\u0440\u0438\u0435\u043d \u043d\u043e\u043c\u0435\u0440", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "manual": { + "data": { + "ip_address": "IP \u0430\u0434\u0440\u0435\u0441" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nobo_hub/translations/de.json b/homeassistant/components/nobo_hub/translations/de.json new file mode 100644 index 00000000000..f3c392c1f5b --- /dev/null +++ b/homeassistant/components/nobo_hub/translations/de.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen - Seriennummer pr\u00fcfen", + "invalid_ip": "Ung\u00fcltige IP-Adresse", + "invalid_serial": "Ung\u00fcltige Seriennummer", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "manual": { + "data": { + "ip_address": "IP-Adresse", + "serial": "Seriennummer (12 Ziffern)" + }, + "description": "Konfiguriere einen Nob\u00f8 Ecohub, der nicht in deinem lokalen Netzwerk gefunden wurde. Wenn sich dein Hub in einem anderen Netzwerk befindet, kannst du dich trotzdem mit ihm verbinden, indem du die vollst\u00e4ndige Seriennummer (12 Ziffern) und seine IP-Adresse eingibst." + }, + "selected": { + "data": { + "serial_suffix": "Seriennummern-Suffix (3 Ziffern)" + }, + "description": "Konfigurieren von {hub}. Um eine Verbindung zum Hub herzustellen, musst du die letzten 3 Ziffern der Seriennummer des Hubs eingeben." + }, + "user": { + "data": { + "device": "Entdeckte Hubs" + }, + "description": "W\u00e4hle den Nob\u00f8 Ecohub zum Konfigurieren." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "override_type": "Typ \u00fcberschreiben" + }, + "description": "W\u00e4hle die \u00dcberschreibungsart \"Jetzt\", um die \u00dcberschreibung bei der n\u00e4chsten Profil\u00e4nderung in der Woche zu beenden." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nobo_hub/translations/el.json b/homeassistant/components/nobo_hub/translations/el.json new file mode 100644 index 00000000000..946b841361e --- /dev/null +++ b/homeassistant/components/nobo_hub/translations/el.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5 \u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 - \u03b5\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03cc \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc", + "invalid_ip": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP", + "invalid_serial": "\u039c\u03b7 \u03ad\u03b3\u03b3\u03c5\u03c1\u03bf\u03c2 \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03cc\u03c2 \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "manual": { + "data": { + "ip_address": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP", + "serial": "\u03a3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03cc\u03c2 \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 (12 \u03c8\u03b7\u03c6\u03af\u03b1)" + }, + "description": "\u0394\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03ce\u03c3\u03c4\u03b5 \u03ad\u03bd\u03b1 Nob\u00f8 Ecohub \u03c0\u03bf\u03c5 \u03b4\u03b5\u03bd \u03b1\u03bd\u03b1\u03ba\u03b1\u03bb\u03cd\u03c6\u03b8\u03b7\u03ba\u03b5 \u03c3\u03c4\u03bf \u03c4\u03bf\u03c0\u03b9\u03ba\u03cc \u03c3\u03b1\u03c2 \u03b4\u03af\u03ba\u03c4\u03c5\u03bf. \u0395\u03ac\u03bd \u03bf \u03b4\u03b9\u03b1\u03bd\u03bf\u03bc\u03ad\u03b1\u03c2 \u03c3\u03b1\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03c3\u03b5 \u03ac\u03bb\u03bb\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf, \u03bc\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03b5 \u03b1\u03c5\u03c4\u03cc \u03b5\u03b9\u03c3\u03ac\u03b3\u03bf\u03bd\u03c4\u03b1\u03c2 \u03c4\u03bf\u03bd \u03c0\u03bb\u03ae\u03c1\u03b7 \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03cc \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc (12 \u03c8\u03b7\u03c6\u03af\u03b1) \u03ba\u03b1\u03b9 \u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03c4\u03bf\u03c5." + }, + "selected": { + "data": { + "serial_suffix": "\u0395\u03c0\u03af\u03b8\u03b7\u03bc\u03b1 \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03bf\u03cd \u03b1\u03c1\u03b9\u03b8\u03bc\u03bf\u03cd (3 \u03c8\u03b7\u03c6\u03af\u03b1)" + }, + "description": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 {hub}. \n\n\u0393\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf hub, \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b1 \u03c4\u03b5\u03bb\u03b5\u03c5\u03c4\u03b1\u03af\u03b1 3 \u03c8\u03b7\u03c6\u03af\u03b1 \u03c4\u03bf\u03c5 \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03bf\u03cd \u03b1\u03c1\u03b9\u03b8\u03bc\u03bf\u03cd \u03c4\u03bf\u03c5 hub." + }, + "user": { + "data": { + "device": "\u0391\u03bd\u03b1\u03ba\u03b1\u03bb\u03c5\u03c6\u03b8\u03ad\u03bd\u03c4\u03b5\u03c2 \u03ba\u03cc\u03bc\u03b2\u03bf\u03b9" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 Nob\u00f8 Ecohub \u03b3\u03b9\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03c9\u03bd." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "override_type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03c0\u03b1\u03c1\u03ac\u03ba\u03b1\u03bc\u03c8\u03b7\u03c2" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c0\u03b1\u03c1\u03ac\u03ba\u03b1\u03bc\u03c8\u03b7 \u03c4\u03cd\u03c0\u03bf\u03c5 \"\u03a4\u03ce\u03c1\u03b1\" \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c4\u03b5\u03c1\u03bc\u03b1\u03c4\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c0\u03b1\u03c1\u03ac\u03ba\u03b1\u03bc\u03c8\u03b7 \u03c3\u03c4\u03b7\u03bd \u03b1\u03bb\u03bb\u03b1\u03b3\u03ae \u03c0\u03c1\u03bf\u03c6\u03af\u03bb \u03c4\u03b7\u03bd \u03b5\u03c0\u03cc\u03bc\u03b5\u03bd\u03b7 \u03b5\u03b2\u03b4\u03bf\u03bc\u03ac\u03b4\u03b1." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nobo_hub/translations/es.json b/homeassistant/components/nobo_hub/translations/es.json new file mode 100644 index 00000000000..3985b698acd --- /dev/null +++ b/homeassistant/components/nobo_hub/translations/es.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar: comprueba el n\u00famero de serie", + "invalid_ip": "Direcci\u00f3n IP no v\u00e1lida", + "invalid_serial": "N\u00famero de serie no v\u00e1lido", + "unknown": "Error inesperado" + }, + "step": { + "manual": { + "data": { + "ip_address": "Direcci\u00f3n IP", + "serial": "N\u00famero de serie (12 d\u00edgitos)" + }, + "description": "Configura un Nob\u00f8 Ecohub no descubierto en tu red local. Si tu hub est\u00e1 en otra red, a\u00fan puedes conectarte introduciendo el n\u00famero de serie completo (12 d\u00edgitos) y su direcci\u00f3n IP." + }, + "selected": { + "data": { + "serial_suffix": "Sufijo del n\u00famero de serie (3 d\u00edgitos)" + }, + "description": "Configurando {hub}.\n\nPara conectarte al hub, debes introducir los 3 \u00faltimos d\u00edgitos del n\u00famero de serie del mismo." + }, + "user": { + "data": { + "device": "Hubs descubiertos" + }, + "description": "Selecciona un Nob\u00f8 Ecohub para configurar." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "override_type": "Tipo de anulaci\u00f3n" + }, + "description": "Selecciona el tipo de anulaci\u00f3n \"Ahora\" para finalizar la anulaci\u00f3n en el cambio de perfil de la pr\u00f3xima semana." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nobo_hub/translations/fr.json b/homeassistant/components/nobo_hub/translations/fr.json new file mode 100644 index 00000000000..76a7f9b25d9 --- /dev/null +++ b/homeassistant/components/nobo_hub/translations/fr.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de la connexion \u2013\u00a0v\u00e9rifiez le num\u00e9ro de s\u00e9rie", + "invalid_ip": "Adresse IP non valide", + "invalid_serial": "Num\u00e9ro de s\u00e9rie non valide", + "unknown": "Erreur inattendue" + }, + "step": { + "manual": { + "data": { + "ip_address": "Adresse IP", + "serial": "Num\u00e9ro de s\u00e9rie (12\u00a0chiffres)" + } + }, + "selected": { + "data": { + "serial_suffix": "Suffixe du num\u00e9ro de s\u00e9rie (3\u00a0chiffres)" + } + }, + "user": { + "data": { + "device": "Hubs d\u00e9couverts" + }, + "description": "S\u00e9lectionnez le Nob\u00f8 Ecohub \u00e0 configurer." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nobo_hub/translations/hu.json b/homeassistant/components/nobo_hub/translations/hu.json new file mode 100644 index 00000000000..9058c96d3d6 --- /dev/null +++ b/homeassistant/components/nobo_hub/translations/hu.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Nem siker\u00fclt csatlakozni - ellen\u0151rizze a sorozatsz\u00e1mot", + "invalid_ip": "\u00c9rv\u00e9nytelen IP-c\u00edm", + "invalid_serial": "\u00c9rv\u00e9nytelen sorozatsz\u00e1m", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "manual": { + "data": { + "ip_address": "IP c\u00edm", + "serial": "Sorsz\u00e1m (12 sz\u00e1mjegy)" + }, + "description": "Konfigur\u00e1ljon egy Nob\u00f8 Ecohubot, amely nem tal\u00e1lhat\u00f3 a helyi h\u00e1l\u00f3zaton. Ha a hub egy m\u00e1sik h\u00e1l\u00f3zaton van, tov\u00e1bbra is csatlakozhat hozz\u00e1 a teljes sorozatsz\u00e1m (12 sz\u00e1mjegy) \u00e9s IP-c\u00edm\u00e9nek megad\u00e1s\u00e1val." + }, + "selected": { + "data": { + "serial_suffix": "Sorozatsz\u00e1m ut\u00f3tag (3 sz\u00e1mjegy)" + }, + "description": "{hub} konfigur\u00e1l\u00e1sa. A hubhoz val\u00f3 csatlakoz\u00e1shoz meg kell adnia a hub sorozatsz\u00e1m\u00e1nak utols\u00f3 3 sz\u00e1mjegy\u00e9t." + }, + "user": { + "data": { + "device": "Felfedezett hub-ok" + }, + "description": "V\u00e1lassza ki a Nob\u00f8 Ecohubot a konfigur\u00e1l\u00e1shoz." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "override_type": "T\u00edpus fel\u00fclb\u00edr\u00e1l\u00e1sa" + }, + "description": "V\u00e1lassza a \"Most\" fel\u00fclb\u00edr\u00e1l\u00e1si t\u00edpust a fel\u00fclb\u00edr\u00e1l\u00e1s megsz\u00fcntet\u00e9s\u00e9hez a k\u00f6vetkez\u0151 heti profilv\u00e1lt\u00e1skor." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nobo_hub/translations/id.json b/homeassistant/components/nobo_hub/translations/id.json new file mode 100644 index 00000000000..dc48a44b0ae --- /dev/null +++ b/homeassistant/components/nobo_hub/translations/id.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "invalid_ip": "Alamat IP tidak valid", + "invalid_serial": "Nomor seri tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "manual": { + "data": { + "ip_address": "Alamat IP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nobo_hub/translations/it.json b/homeassistant/components/nobo_hub/translations/it.json new file mode 100644 index 00000000000..ed7ce1f7cd6 --- /dev/null +++ b/homeassistant/components/nobo_hub/translations/it.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi - controllare il numero di serie", + "invalid_ip": "Indirizzo IP non valido", + "invalid_serial": "Numero di serie non valido", + "unknown": "Errore imprevisto" + }, + "step": { + "manual": { + "data": { + "ip_address": "Indirizzo IP", + "serial": "Numero di serie (12 cifre)" + } + }, + "selected": { + "data": { + "serial_suffix": "Suffisso del numero di serie (3 cifre)" + } + }, + "user": { + "data": { + "device": "Hub scoperti" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nobo_hub/translations/ja.json b/homeassistant/components/nobo_hub/translations/ja.json new file mode 100644 index 00000000000..cea3f2fb8df --- /dev/null +++ b/homeassistant/components/nobo_hub/translations/ja.json @@ -0,0 +1,43 @@ +{ + "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 - \u30b7\u30ea\u30a2\u30eb\u756a\u53f7\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044", + "invalid_ip": "\u7121\u52b9\u306aIP\u30a2\u30c9\u30ec\u30b9", + "invalid_serial": "\u7121\u52b9\u306a\u30b7\u30ea\u30a2\u30eb\u756a\u53f7", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "step": { + "manual": { + "data": { + "ip_address": "IP\u30a2\u30c9\u30ec\u30b9", + "serial": "\u30b7\u30ea\u30a2\u30eb\u30ca\u30f3\u30d0\u30fc(12\u6841)" + } + }, + "selected": { + "data": { + "serial_suffix": "\u30b7\u30ea\u30a2\u30eb\u30ca\u30f3\u30d0\u30fc\u306e\u672b\u5c3e(3\u6841)" + }, + "description": "{hub} \u306e\u8a2d\u5b9a\u3002\n\n\u30cf\u30d6\u306b\u63a5\u7d9a\u3059\u308b\u306b\u306f\u3001\u30cf\u30d6\u306e\u30b7\u30ea\u30a2\u30eb\u756a\u53f7\u306e\u672b\u5c3e3\u6841\u3092\u5165\u529b\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" + }, + "user": { + "data": { + "device": "\u691c\u51fa\u3055\u308c\u305f\u30cf\u30d6" + }, + "description": "Nob\u00f8 Ecohub\u3092\u9078\u629e\u3057\u3066\u8a2d\u5b9a\u3057\u307e\u3059\u3002" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "override_type": "\u30aa\u30fc\u30d0\u30fc\u30e9\u30a4\u30c9\u306e\u7a2e\u985e" + }, + "description": "\u30aa\u30fc\u30d0\u30fc\u30e9\u30a4\u30c9\u306e\u7a2e\u985e\u3092\"\u4eca\u3059\u3050\"\u306b\u3059\u308b\u3068\u3001\u6765\u9031\u306e\u30d7\u30ed\u30d5\u30a1\u30a4\u30eb\u5909\u66f4\u6642\u306b\u30aa\u30fc\u30d0\u30fc\u30e9\u30a4\u30c9\u304c\u7d42\u4e86\u3057\u307e\u3059\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nobo_hub/translations/no.json b/homeassistant/components/nobo_hub/translations/no.json new file mode 100644 index 00000000000..ffd37f30dee --- /dev/null +++ b/homeassistant/components/nobo_hub/translations/no.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling feilet - sjekk serienummer", + "invalid_ip": "Ugyldig IP-adresse", + "invalid_serial": "Ugyldig serienummer", + "unknown": "Uventet feil" + }, + "step": { + "manual": { + "data": { + "ip_address": "IP-adresse", + "serial": "Serienummer (12 sifre)" + }, + "description": "Konfigurer en Nob\u00f8 Ecohub som ikke er oppdaget p\u00e5 ditt lokale nettverk. Hvis huben er p\u00e5 et annet nettverk, kan du fortsatt koble den til med \u00e5 skrive inn fullstendig serienummer (12 sifre) og IP-adressen." + }, + "selected": { + "data": { + "serial_suffix": "Serienummersuffiks (3 sifre)" + }, + "description": "Konfigurerer {hub}.\n\nFor \u00e5 koble til huben, m\u00e5 du skrive inn de 3 siste sifrene i hubens serienummer." + }, + "user": { + "data": { + "device": "Oppdagede huber" + }, + "description": "Velg Nob\u00f8 Ecohub du vil konfigurere" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "override_type": "Overstyringstype" + }, + "description": "Velg overstyringstype \"Now\" for \u00e5 avslutte overstyringer ved neste endring i ukesprofilen." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nobo_hub/translations/pt-BR.json b/homeassistant/components/nobo_hub/translations/pt-BR.json new file mode 100644 index 00000000000..9de87f8dad5 --- /dev/null +++ b/homeassistant/components/nobo_hub/translations/pt-BR.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha ao conectar - verifique o n\u00famero de s\u00e9rie", + "invalid_ip": "Endere\u00e7o IP inv\u00e1lido", + "invalid_serial": "N\u00famero de s\u00e9rie inv\u00e1lido", + "unknown": "Erro inesperado" + }, + "step": { + "manual": { + "data": { + "ip_address": "Endere\u00e7o IP", + "serial": "N\u00famero de s\u00e9rie (12 d\u00edgitos)" + }, + "description": "Configure um Nob\u00f8 Ecohub n\u00e3o descoberto em sua rede local. Se o seu hub estiver em outra rede, voc\u00ea ainda poder\u00e1 se conectar a ele digitando o n\u00famero de s\u00e9rie completo (12 d\u00edgitos) e seu endere\u00e7o IP." + }, + "selected": { + "data": { + "serial_suffix": "Sufixo do n\u00famero de s\u00e9rie (3 d\u00edgitos)" + }, + "description": "Configurando {hub}. Para se conectar ao hub, voc\u00ea precisa inserir os 3 \u00faltimos d\u00edgitos do n\u00famero de s\u00e9rie do hub." + }, + "user": { + "data": { + "device": "Hubs descobertos" + }, + "description": "Selecione Nob\u00f8 Ecohub para configurar." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "override_type": "Tipo de substitui\u00e7\u00e3o" + }, + "description": "Selecione o tipo de substitui\u00e7\u00e3o \"Agora\" para encerrar a substitui\u00e7\u00e3o na pr\u00f3xima semana de altera\u00e7\u00e3o de perfil." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nobo_hub/translations/zh-Hant.json b/homeassistant/components/nobo_hub/translations/zh-Hant.json new file mode 100644 index 00000000000..1d2f4ccf917 --- /dev/null +++ b/homeassistant/components/nobo_hub/translations/zh-Hant.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557 - \u8acb\u6aa2\u67e5\u5e8f\u865f", + "invalid_ip": "IP \u4f4d\u5740\u7121\u6548", + "invalid_serial": "\u5e8f\u865f\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "manual": { + "data": { + "ip_address": "IP \u4f4d\u5740", + "serial": "\u5e8f\u865f\uff0812 \u4f4d\uff09" + }, + "description": "\u8a2d\u5b9a\u672c\u5730\u7db2\u8def\u672a\u81ea\u52d5\u767c\u73fe\u7684 Nob\u00f8 Ecohub\u3002 \u5047\u5982 hub \u9023\u7dda\u81f3\u5176\u4ed6\u7db2\u8def\u3001\u53ef\u4ee5\u8a66\u8457\u8f38\u5165\u5b8c\u6574\u7684\u5e8f\u865f\uff0812 \u4f4d\u6578\uff09\u53ca\u5176 IP \u4f4d\u5740\u9032\u884c\u9023\u7dda\u3002" + }, + "selected": { + "data": { + "serial_suffix": "\u5e8f\u865f\u5f8c\u7db4\uff083 \u4f4d\u6578\uff09" + }, + "description": "\u8a2d\u5b9a {hub}\u3002\n\n\u6b32\u9023\u7dda\u81f3 Hub\u3001\u9700\u8981\u8f38\u5165\u81f3\u5c11 3 \u4f4d\u6578\u4e4b Hub \u5e8f\u865f\u3002" + }, + "user": { + "data": { + "device": "\u81ea\u52d5\u641c\u7d22\u5230\u7684 Hub" + }, + "description": "\u9078\u64c7 Nob\u00f8 Ecohub \u4ee5\u9032\u884c\u8a2d\u5b9a\u3002" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "override_type": "\u8986\u5beb\u985e\u5225" + }, + "description": "\u9078\u64c7\u8986\u5beb\u985e\u5225 \"Now\" \u4ee5\u65bc\u4e0b\u9031\u6a94\u6848\u8b8a\u66f4\u6642\u7d50\u675f\u8986\u5beb\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/ja.json b/homeassistant/components/overkiz/translations/ja.json index 357408847fd..df49ea40332 100644 --- a/homeassistant/components/overkiz/translations/ja.json +++ b/homeassistant/components/overkiz/translations/ja.json @@ -11,7 +11,8 @@ "server_in_maintenance": "\u30e1\u30f3\u30c6\u30ca\u30f3\u30b9\u306e\u305f\u3081\u30b5\u30fc\u30d0\u30fc\u304c\u30c0\u30a6\u30f3\u3057\u3066\u3044\u307e\u3059", "too_many_attempts": "\u7121\u52b9\u306a\u30c8\u30fc\u30af\u30f3\u306b\u3088\u308b\u8a66\u884c\u56de\u6570\u304c\u591a\u3059\u304e\u305f\u305f\u3081\u3001\u4e00\u6642\u7684\u306b\u7981\u6b62\u3055\u308c\u307e\u3057\u305f\u3002", "too_many_requests": "\u30ea\u30af\u30a8\u30b9\u30c8\u304c\u591a\u3059\u304e\u307e\u3059\u3002\u3057\u3070\u3089\u304f\u3057\u3066\u304b\u3089\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044\u3002", - "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc", + "unknown_user": "\u4e0d\u660e\u306a\u30e6\u30fc\u30b6\u30fc\u3067\u3059\u3002Somfy Protect\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3001\u3053\u306e\u7d71\u5408\u3067\u306f\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002" }, "flow_title": "\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4: {gateway_id}", "step": { diff --git a/homeassistant/components/prusalink/translations/bg.json b/homeassistant/components/prusalink/translations/bg.json new file mode 100644 index 00000000000..54d6eb68c8b --- /dev/null +++ b/homeassistant/components/prusalink/translations/bg.json @@ -0,0 +1,17 @@ +{ + "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" + }, + "step": { + "user": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447", + "host": "\u0425\u043e\u0441\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/ja.json b/homeassistant/components/prusalink/translations/ja.json index e507bbbda7e..95a57633cbd 100644 --- a/homeassistant/components/prusalink/translations/ja.json +++ b/homeassistant/components/prusalink/translations/ja.json @@ -3,6 +3,7 @@ "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "not_supported": "PrusaLink API v2\u306e\u307f\u5bfe\u5fdc", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { diff --git a/homeassistant/components/risco/translations/bg.json b/homeassistant/components/risco/translations/bg.json index 805d72102aa..14bd2a15ccb 100644 --- a/homeassistant/components/risco/translations/bg.json +++ b/homeassistant/components/risco/translations/bg.json @@ -9,6 +9,11 @@ "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "step": { + "local": { + "data": { + "port": "\u041f\u043e\u0440\u0442" + } + }, "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u0430", diff --git a/homeassistant/components/sensor/translations/ja.json b/homeassistant/components/sensor/translations/ja.json index dbd8bd76e5e..037b6d70dbe 100644 --- a/homeassistant/components/sensor/translations/ja.json +++ b/homeassistant/components/sensor/translations/ja.json @@ -11,6 +11,7 @@ "is_gas": "\u73fe\u5728\u306e {entity_name} \u30ac\u30b9", "is_humidity": "\u73fe\u5728\u306e {entity_name} \u6e7f\u5ea6", "is_illuminance": "\u73fe\u5728\u306e {entity_name} \u7167\u5ea6", + "is_moisture": "\u73fe\u5728\u306e\u3001{entity_name} \u306e\u6c34\u5206", "is_nitrogen_dioxide": "\u73fe\u5728\u306e {entity_name} \u4e8c\u9178\u5316\u7a92\u7d20\u6fc3\u5ea6\u30ec\u30d9\u30eb", "is_nitrogen_monoxide": "\u73fe\u5728\u306e {entity_name} \u4e00\u9178\u5316\u7a92\u7d20\u6fc3\u5ea6\u30ec\u30d9\u30eb", "is_nitrous_oxide": "\u73fe\u5728\u306e {entity_name} \u4e9c\u9178\u5316\u7a92\u7d20\u6fc3\u5ea6\u30ec\u30d9\u30eb", @@ -40,6 +41,7 @@ "gas": "{entity_name} \u30ac\u30b9\u306e\u5909\u5316", "humidity": "{entity_name} \u6e7f\u5ea6\u306e\u5909\u5316", "illuminance": "{entity_name} \u7167\u5ea6\u306e\u5909\u5316", + "moisture": "{entity_name} \u306e\u6c34\u5206\u5909\u5316", "nitrogen_dioxide": "{entity_name} \u4e8c\u9178\u5316\u7a92\u7d20\u6fc3\u5ea6\u306e\u5909\u5316", "nitrogen_monoxide": "{entity_name} \u4e00\u9178\u5316\u7a92\u7d20\u6fc3\u5ea6\u306e\u5909\u5316", "nitrous_oxide": "{entity_name} \u4e9c\u9178\u5316\u7a92\u7d20\u6fc3\u5ea6\u306e\u5909\u5316", diff --git a/homeassistant/components/sensorpro/translations/bg.json b/homeassistant/components/sensorpro/translations/bg.json new file mode 100644 index 00000000000..0d059b2bf51 --- /dev/null +++ b/homeassistant/components/sensorpro/translations/bg.json @@ -0,0 +1,15 @@ +{ + "config": { + "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" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/skybell/translations/bg.json b/homeassistant/components/skybell/translations/bg.json index f3f182bbc3a..bdec7688abf 100644 --- a/homeassistant/components/skybell/translations/bg.json +++ b/homeassistant/components/skybell/translations/bg.json @@ -10,6 +10,12 @@ "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + }, + "description": "\u041c\u043e\u043b\u044f, \u0430\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u0439\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u0430\u0442\u0430 \u0441\u0438 \u0437\u0430 {email}" + }, "user": { "data": { "email": "Email", diff --git a/homeassistant/components/thermobeacon/translations/bg.json b/homeassistant/components/thermobeacon/translations/bg.json new file mode 100644 index 00000000000..c79e057d5c0 --- /dev/null +++ b/homeassistant/components/thermobeacon/translations/bg.json @@ -0,0 +1,18 @@ +{ + "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" + }, + "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" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/thermopro/translations/bg.json b/homeassistant/components/thermopro/translations/bg.json new file mode 100644 index 00000000000..c79e057d5c0 --- /dev/null +++ b/homeassistant/components/thermopro/translations/bg.json @@ -0,0 +1,18 @@ +{ + "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" + }, + "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" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifiprotect/translations/ja.json b/homeassistant/components/unifiprotect/translations/ja.json index 608601e2175..51193ed3123 100644 --- a/homeassistant/components/unifiprotect/translations/ja.json +++ b/homeassistant/components/unifiprotect/translations/ja.json @@ -42,11 +42,15 @@ } }, "options": { + "error": { + "invalid_mac_list": "\u30ab\u30f3\u30de\u3067\u533a\u5207\u3089\u308c\u305fMAC\u30a2\u30c9\u30ec\u30b9\u306e\u30ea\u30b9\u30c8\u3067\u3042\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059" + }, "step": { "init": { "data": { "all_updates": "\u30ea\u30a2\u30eb\u30bf\u30a4\u30e0\u30e1\u30c8\u30ea\u30c3\u30af(Realtime metrics)(\u8b66\u544a: CPU\u4f7f\u7528\u7387\u304c\u5927\u5e45\u306b\u5897\u52a0\u3057\u307e\u3059)", "disable_rtsp": "RTSP\u30b9\u30c8\u30ea\u30fc\u30e0\u3092\u7121\u52b9\u306b\u3059\u308b", + "ignored_devices": "\u7121\u8996\u3059\u308b\u6a5f\u5668\u306eMAC\u30a2\u30c9\u30ec\u30b9\u306e\u30ab\u30f3\u30de\u533a\u5207\u308a\u30ea\u30b9\u30c8", "max_media": "\u30e1\u30c7\u30a3\u30a2\u30d6\u30e9\u30a6\u30b6\u306b\u30ed\u30fc\u30c9\u3059\u308b\u30a4\u30d9\u30f3\u30c8\u306e\u6700\u5927\u6570(RAM\u4f7f\u7528\u91cf\u304c\u5897\u52a0)", "override_connection_host": "\u63a5\u7d9a\u30db\u30b9\u30c8\u3092\u4e0a\u66f8\u304d" }, diff --git a/homeassistant/components/volumio/translations/zh-Hant.json b/homeassistant/components/volumio/translations/zh-Hant.json index 79edffd79aa..787572b7304 100644 --- a/homeassistant/components/volumio/translations/zh-Hant.json +++ b/homeassistant/components/volumio/translations/zh-Hant.json @@ -11,7 +11,7 @@ "step": { "discovery_confirm": { "description": "\u662f\u5426\u8981\u65b0\u589e Volumio (`{name}`) \u81f3 Home Assistant\uff1f", - "title": "\u5df2\u767c\u73fe\u7684 Volumio" + "title": "\u81ea\u52d5\u641c\u7d22\u5230\u7684 Volumio" }, "user": { "data": { diff --git a/homeassistant/components/volvooncall/translations/bg.json b/homeassistant/components/volvooncall/translations/bg.json new file mode 100644 index 00000000000..c0ccf23f5b5 --- /dev/null +++ b/homeassistant/components/volvooncall/translations/bg.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "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" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/volvooncall/translations/ja.json b/homeassistant/components/volvooncall/translations/ja.json index 3a529158503..0f56a700da0 100644 --- a/homeassistant/components/volvooncall/translations/ja.json +++ b/homeassistant/components/volvooncall/translations/ja.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, "error": { "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", diff --git a/homeassistant/components/zha/translations/bg.json b/homeassistant/components/zha/translations/bg.json index 6196428fca6..75741130cc8 100644 --- a/homeassistant/components/zha/translations/bg.json +++ b/homeassistant/components/zha/translations/bg.json @@ -23,6 +23,11 @@ }, "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" }, + "upload_manual_backup": { + "data": { + "uploaded_backup_file": "\u041a\u0430\u0447\u0432\u0430\u043d\u0435 \u043d\u0430 \u0444\u0430\u0439\u043b" + } + }, "user": { "description": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0441\u0435\u0440\u0438\u0435\u043d \u043f\u043e\u0440\u0442 \u0437\u0430 Zigbee \u0440\u0430\u0434\u0438\u043e", "title": "ZHA" @@ -75,5 +80,15 @@ "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" } + }, + "options": { + "flow_title": "{name}", + "step": { + "upload_manual_backup": { + "data": { + "uploaded_backup_file": "\u041a\u0430\u0447\u0432\u0430\u043d\u0435 \u043d\u0430 \u0444\u0430\u0439\u043b" + } + } + } } } \ No newline at end of file From a4261d588bedca44a586c677818640877f268f96 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 4 Sep 2022 05:13:34 -0400 Subject: [PATCH 068/955] Increase default august timeout (#77762) Fixes ``` 2022-08-28 20:32:46.223 ERROR (MainThread) [homeassistant] Error doing job: Task exception was never retrieved Traceback (most recent call last): File "/Users/bdraco/home-assistant/homeassistant/helpers/debounce.py", line 82, in async_call await task File "/Users/bdraco/home-assistant/homeassistant/components/august/activity.py", line 49, in _async_update_house_id await self._async_update_house_id(house_id) File "/Users/bdraco/home-assistant/homeassistant/components/august/activity.py", line 137, in _async_update_house_id activities = await self._api.async_get_house_activities( File "/Users/bdraco/home-assistant/venv/lib/python3.10/site-packages/yalexs/api_async.py", line 96, in async_get_house_activities response = await self._async_dict_to_api( File "/Users/bdraco/home-assistant/venv/lib/python3.10/site-packages/yalexs/api_async.py", line 294, in _async_dict_to_api response = await self._aiohttp_session.request(method, url, **api_dict) File "/Users/bdraco/home-assistant/venv/lib/python3.10/site-packages/aiohttp/client.py", line 466, in _request with timer: File "/Users/bdraco/home-assistant/venv/lib/python3.10/site-packages/aiohttp/helpers.py", line 721, in __exit__ raise asyncio.TimeoutError from None asyncio.exceptions.TimeoutError ``` --- homeassistant/components/august/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/august/const.py b/homeassistant/components/august/const.py index 9a724d4a87b..5b936e9f159 100644 --- a/homeassistant/components/august/const.py +++ b/homeassistant/components/august/const.py @@ -4,7 +4,7 @@ from datetime import timedelta from homeassistant.const import Platform -DEFAULT_TIMEOUT = 15 +DEFAULT_TIMEOUT = 25 CONF_ACCESS_TOKEN_CACHE_FILE = "access_token_cache_file" CONF_LOGIN_METHOD = "login_method" From 9a6b22a156192ec056ac2f73db4de079506705a1 Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Sun, 4 Sep 2022 12:01:17 +0200 Subject: [PATCH 069/955] Address late review in Overkiz (add duration device class) (#77778) Address follow-up review in Overkiz (duration device class) --- homeassistant/components/overkiz/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/overkiz/sensor.py b/homeassistant/components/overkiz/sensor.py index 55b865c1aeb..cbe4234a5ac 100644 --- a/homeassistant/components/overkiz/sensor.py +++ b/homeassistant/components/overkiz/sensor.py @@ -354,14 +354,14 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ OverkizSensorDescription( key=OverkizState.IO_HEAT_PUMP_OPERATING_TIME, name="Heat Pump Operating Time", - icon="mdi:home-clock", + device_class=SensorDeviceClass.DURATION, entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=TIME_SECONDS, ), OverkizSensorDescription( key=OverkizState.IO_ELECTRIC_BOOSTER_OPERATING_TIME, name="Electric Booster Operating Time", - icon="mdi:home-clock", + device_class=SensorDeviceClass.DURATION, native_unit_of_measurement=TIME_SECONDS, entity_category=EntityCategory.DIAGNOSTIC, ), From 807d197ca0c131c9ef5f4a683be04c7df8f715bb Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Sun, 4 Sep 2022 14:01:42 +0200 Subject: [PATCH 070/955] Add goToAlias button (my position) to Overkiz integration (#76694) --- homeassistant/components/overkiz/button.py | 45 +++++++++++++++++----- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/overkiz/button.py b/homeassistant/components/overkiz/button.py index 8118d0fd23e..546c24cb6d1 100644 --- a/homeassistant/components/overkiz/button.py +++ b/homeassistant/components/overkiz/button.py @@ -1,6 +1,10 @@ """Support for Overkiz (virtual) buttons.""" from __future__ import annotations +from dataclasses import dataclass + +from pyoverkiz.types import StateType as OverkizStateType + from homeassistant.components.button import ButtonEntity, ButtonEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -11,41 +15,56 @@ from . import HomeAssistantOverkizData from .const import DOMAIN, IGNORED_OVERKIZ_DEVICES from .entity import OverkizDescriptiveEntity -BUTTON_DESCRIPTIONS: list[ButtonEntityDescription] = [ + +@dataclass +class OverkizButtonDescription(ButtonEntityDescription): + """Class to describe an Overkiz button.""" + + press_args: OverkizStateType | None = None + + +BUTTON_DESCRIPTIONS: list[OverkizButtonDescription] = [ # My Position (cover, light) - ButtonEntityDescription( + OverkizButtonDescription( key="my", name="My Position", icon="mdi:star", ), # Identify - ButtonEntityDescription( + OverkizButtonDescription( key="identify", # startIdentify and identify are reversed... Swap this when fixed in API. name="Start Identify", icon="mdi:human-greeting-variant", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), - ButtonEntityDescription( + OverkizButtonDescription( key="stopIdentify", name="Stop Identify", icon="mdi:human-greeting-variant", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), - ButtonEntityDescription( + OverkizButtonDescription( key="startIdentify", # startIdentify and identify are reversed... Swap this when fixed in API. name="Identify", icon="mdi:human-greeting-variant", entity_category=EntityCategory.DIAGNOSTIC, ), # RTDIndoorSiren / RTDOutdoorSiren - ButtonEntityDescription(key="dingDong", name="Ding Dong", icon="mdi:bell-ring"), - ButtonEntityDescription(key="bip", name="Bip", icon="mdi:bell-ring"), - ButtonEntityDescription( + OverkizButtonDescription(key="dingDong", name="Ding Dong", icon="mdi:bell-ring"), + OverkizButtonDescription(key="bip", name="Bip", icon="mdi:bell-ring"), + OverkizButtonDescription( key="fastBipSequence", name="Fast Bip Sequence", icon="mdi:bell-ring" ), - ButtonEntityDescription(key="ring", name="Ring", icon="mdi:bell-ring"), + OverkizButtonDescription(key="ring", name="Ring", icon="mdi:bell-ring"), + # DynamicScreen (ogp:blind) uses goToAlias (id 1: favorite1) instead of 'my' + OverkizButtonDescription( + key="goToAlias", + press_args="1", + name="My position", + icon="mdi:star", + ), ] SUPPORTED_COMMANDS = { @@ -85,6 +104,14 @@ async def async_setup_entry( class OverkizButton(OverkizDescriptiveEntity, ButtonEntity): """Representation of an Overkiz Button.""" + entity_description: OverkizButtonDescription + async def async_press(self) -> None: """Handle the button press.""" + if self.entity_description.press_args: + await self.executor.async_execute_command( + self.entity_description.key, self.entity_description.press_args + ) + return + await self.executor.async_execute_command(self.entity_description.key) From 6dcce6156533cd0458eb9071dbe506f8526380b7 Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Sun, 4 Sep 2022 14:02:00 +0200 Subject: [PATCH 071/955] Add support for AtlanticHeatRecoveryVentilation to Overkiz integration (#74015) --- .../overkiz/climate_entities/__init__.py | 2 + .../atlantic_heat_recovery_ventilation.py | 176 ++++++++++++++++++ homeassistant/components/overkiz/const.py | 1 + 3 files changed, 179 insertions(+) create mode 100644 homeassistant/components/overkiz/climate_entities/atlantic_heat_recovery_ventilation.py diff --git a/homeassistant/components/overkiz/climate_entities/__init__.py b/homeassistant/components/overkiz/climate_entities/__init__.py index 737ea342c40..32fae234be1 100644 --- a/homeassistant/components/overkiz/climate_entities/__init__.py +++ b/homeassistant/components/overkiz/climate_entities/__init__.py @@ -3,12 +3,14 @@ from pyoverkiz.enums.ui import UIWidget from .atlantic_electrical_heater import AtlanticElectricalHeater from .atlantic_electrical_towel_dryer import AtlanticElectricalTowelDryer +from .atlantic_heat_recovery_ventilation import AtlanticHeatRecoveryVentilation from .atlantic_pass_apc_zone_control import AtlanticPassAPCZoneControl from .somfy_thermostat import SomfyThermostat WIDGET_TO_CLIMATE_ENTITY = { UIWidget.ATLANTIC_ELECTRICAL_HEATER: AtlanticElectricalHeater, UIWidget.ATLANTIC_ELECTRICAL_TOWEL_DRYER: AtlanticElectricalTowelDryer, + UIWidget.ATLANTIC_HEAT_RECOVERY_VENTILATION: AtlanticHeatRecoveryVentilation, UIWidget.ATLANTIC_PASS_APC_ZONE_CONTROL: AtlanticPassAPCZoneControl, UIWidget.SOMFY_THERMOSTAT: SomfyThermostat, } diff --git a/homeassistant/components/overkiz/climate_entities/atlantic_heat_recovery_ventilation.py b/homeassistant/components/overkiz/climate_entities/atlantic_heat_recovery_ventilation.py new file mode 100644 index 00000000000..f28db995350 --- /dev/null +++ b/homeassistant/components/overkiz/climate_entities/atlantic_heat_recovery_ventilation.py @@ -0,0 +1,176 @@ +"""Support for AtlanticHeatRecoveryVentilation.""" +from __future__ import annotations + +from typing import cast + +from pyoverkiz.enums import OverkizCommand, OverkizCommandParam, OverkizState + +from homeassistant.components.climate import ClimateEntity +from homeassistant.components.climate.const import ( + FAN_AUTO, + ClimateEntityFeature, + HVACMode, +) +from homeassistant.const import TEMP_CELSIUS + +from ..coordinator import OverkizDataUpdateCoordinator +from ..entity import OverkizEntity + +FAN_BOOST = "home_boost" +FAN_KITCHEN = "kitchen_boost" +FAN_AWAY = "away" +FAN_BYPASS = "bypass_boost" + +PRESET_AUTO = "auto" +PRESET_PROG = "prog" +PRESET_MANUAL = "manual" + +OVERKIZ_TO_FAN_MODES: dict[str, str] = { + OverkizCommandParam.AUTO: FAN_AUTO, + OverkizCommandParam.AWAY: FAN_AWAY, + OverkizCommandParam.BOOST: FAN_BOOST, + OverkizCommandParam.HIGH: FAN_KITCHEN, + "": FAN_BYPASS, +} + +FAN_MODES_TO_OVERKIZ = {v: k for k, v in OVERKIZ_TO_FAN_MODES.items()} + +TEMPERATURE_SENSOR_DEVICE_INDEX = 4 + + +class AtlanticHeatRecoveryVentilation(OverkizEntity, ClimateEntity): + """Representation of a AtlanticHeatRecoveryVentilation device.""" + + _attr_fan_modes = [*FAN_MODES_TO_OVERKIZ] + _attr_hvac_mode = HVACMode.FAN_ONLY + _attr_hvac_modes = [HVACMode.FAN_ONLY] + _attr_preset_modes = [PRESET_AUTO, PRESET_PROG, PRESET_MANUAL] + _attr_temperature_unit = TEMP_CELSIUS + _attr_supported_features = ( + ClimateEntityFeature.PRESET_MODE | ClimateEntityFeature.FAN_MODE + ) + + 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 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 + + async def async_set_hvac_mode(self, hvac_mode: str) -> None: + """Not implemented since there is only one hvac_mode.""" + + @property + def preset_mode(self) -> str | None: + """Return the current preset mode.""" + ventilation_configuration = self.executor.select_state( + OverkizState.IO_VENTILATION_CONFIGURATION_MODE + ) + + if ventilation_configuration == OverkizCommandParam.COMFORT: + return PRESET_AUTO + + if ventilation_configuration == OverkizCommandParam.STANDARD: + return PRESET_MANUAL + + ventilation_mode = cast( + dict, self.executor.select_state(OverkizState.IO_VENTILATION_MODE) + ) + prog = ventilation_mode.get(OverkizCommandParam.PROG) + + if prog == OverkizCommandParam.ON: + return PRESET_PROG + + return None + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set the preset mode of the fan.""" + if preset_mode == PRESET_AUTO: + await self.executor.async_execute_command( + OverkizCommand.SET_VENTILATION_CONFIGURATION_MODE, + OverkizCommandParam.COMFORT, + ) + await self._set_ventilation_mode(prog=OverkizCommandParam.OFF) + + if preset_mode == PRESET_PROG: + await self.executor.async_execute_command( + OverkizCommand.SET_VENTILATION_CONFIGURATION_MODE, + OverkizCommandParam.STANDARD, + ) + await self._set_ventilation_mode(prog=OverkizCommandParam.ON) + + if preset_mode == PRESET_MANUAL: + await self.executor.async_execute_command( + OverkizCommand.SET_VENTILATION_CONFIGURATION_MODE, + OverkizCommandParam.STANDARD, + ) + await self._set_ventilation_mode(prog=OverkizCommandParam.OFF) + + await self.executor.async_execute_command( + OverkizCommand.REFRESH_VENTILATION_STATE, + ) + await self.executor.async_execute_command( + OverkizCommand.REFRESH_VENTILATION_CONFIGURATION_MODE, + ) + + @property + def fan_mode(self) -> str | None: + """Return the fan setting.""" + ventilation_mode = cast( + dict, self.executor.select_state(OverkizState.IO_VENTILATION_MODE) + ) + cooling = ventilation_mode.get(OverkizCommandParam.COOLING) + + if cooling == OverkizCommandParam.ON: + return FAN_BYPASS + + return OVERKIZ_TO_FAN_MODES[ + cast(str, self.executor.select_state(OverkizState.IO_AIR_DEMAND_MODE)) + ] + + async def async_set_fan_mode(self, fan_mode: str) -> None: + """Set new target fan mode.""" + if fan_mode == FAN_BYPASS: + await self.executor.async_execute_command( + OverkizCommand.SET_AIR_DEMAND_MODE, OverkizCommandParam.AUTO + ) + await self._set_ventilation_mode(cooling=OverkizCommandParam.ON) + else: + await self._set_ventilation_mode(cooling=OverkizCommandParam.OFF) + await self.executor.async_execute_command( + OverkizCommand.SET_AIR_DEMAND_MODE, FAN_MODES_TO_OVERKIZ[fan_mode] + ) + + await self.executor.async_execute_command( + OverkizCommand.REFRESH_VENTILATION_STATE, + ) + + async def _set_ventilation_mode( + self, + cooling: str | None = None, + prog: str | None = None, + ) -> None: + """Execute ventilation mode command with all parameters.""" + ventilation_mode = cast( + dict, self.executor.select_state(OverkizState.IO_VENTILATION_MODE) + ) + + if cooling: + ventilation_mode[OverkizCommandParam.COOLING] = cooling + + if prog: + ventilation_mode[OverkizCommandParam.PROG] = prog + + await self.executor.async_execute_command( + OverkizCommand.SET_VENTILATION_MODE, ventilation_mode + ) diff --git a/homeassistant/components/overkiz/const.py b/homeassistant/components/overkiz/const.py index 9091cd35998..d98709ba2b6 100644 --- a/homeassistant/components/overkiz/const.py +++ b/homeassistant/components/overkiz/const.py @@ -63,6 +63,7 @@ OVERKIZ_DEVICE_TO_PLATFORM: dict[UIClass | UIWidget, Platform | None] = { 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_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_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) From ed365cb8e957b4453a91dbb4b80574e122950ef9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 4 Sep 2022 09:45:52 -0400 Subject: [PATCH 072/955] Add BlueMaestro integration (#77758) * Add BlueMaestro integration * tests * dc --- CODEOWNERS | 2 + .../components/bluemaestro/__init__.py | 49 +++++ .../components/bluemaestro/config_flow.py | 94 ++++++++ homeassistant/components/bluemaestro/const.py | 3 + .../components/bluemaestro/device.py | 31 +++ .../components/bluemaestro/manifest.json | 16 ++ .../components/bluemaestro/sensor.py | 149 +++++++++++++ .../components/bluemaestro/strings.json | 22 ++ .../bluemaestro/translations/en.json | 22 ++ homeassistant/generated/bluetooth.py | 5 + homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/bluemaestro/__init__.py | 26 +++ tests/components/bluemaestro/conftest.py | 8 + .../bluemaestro/test_config_flow.py | 200 ++++++++++++++++++ tests/components/bluemaestro/test_sensor.py | 50 +++++ 17 files changed, 684 insertions(+) create mode 100644 homeassistant/components/bluemaestro/__init__.py create mode 100644 homeassistant/components/bluemaestro/config_flow.py create mode 100644 homeassistant/components/bluemaestro/const.py create mode 100644 homeassistant/components/bluemaestro/device.py create mode 100644 homeassistant/components/bluemaestro/manifest.json create mode 100644 homeassistant/components/bluemaestro/sensor.py create mode 100644 homeassistant/components/bluemaestro/strings.json create mode 100644 homeassistant/components/bluemaestro/translations/en.json create mode 100644 tests/components/bluemaestro/__init__.py create mode 100644 tests/components/bluemaestro/conftest.py create mode 100644 tests/components/bluemaestro/test_config_flow.py create mode 100644 tests/components/bluemaestro/test_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 97d2b9f9d9b..6e2efe9101d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -137,6 +137,8 @@ build.json @home-assistant/supervisor /tests/components/blebox/ @bbx-a @riokuu /homeassistant/components/blink/ @fronzbot /tests/components/blink/ @fronzbot +/homeassistant/components/bluemaestro/ @bdraco +/tests/components/bluemaestro/ @bdraco /homeassistant/components/blueprint/ @home-assistant/core /tests/components/blueprint/ @home-assistant/core /homeassistant/components/bluesound/ @thrawnarn diff --git a/homeassistant/components/bluemaestro/__init__.py b/homeassistant/components/bluemaestro/__init__.py new file mode 100644 index 00000000000..45eebedcfb2 --- /dev/null +++ b/homeassistant/components/bluemaestro/__init__.py @@ -0,0 +1,49 @@ +"""The BlueMaestro integration.""" +from __future__ import annotations + +import logging + +from bluemaestro_ble import BlueMaestroBluetoothDeviceData + +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 BlueMaestro BLE device from a config entry.""" + address = entry.unique_id + assert address is not None + data = BlueMaestroBluetoothDeviceData() + coordinator = hass.data.setdefault(DOMAIN, {})[ + entry.entry_id + ] = PassiveBluetoothProcessorCoordinator( + hass, + _LOGGER, + address=address, + mode=BluetoothScanningMode.PASSIVE, + 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/bluemaestro/config_flow.py b/homeassistant/components/bluemaestro/config_flow.py new file mode 100644 index 00000000000..ccb548fa42b --- /dev/null +++ b/homeassistant/components/bluemaestro/config_flow.py @@ -0,0 +1,94 @@ +"""Config flow for bluemaestro ble integration.""" +from __future__ import annotations + +from typing import Any + +from bluemaestro_ble import BlueMaestroBluetoothDeviceData as DeviceData +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 BlueMaestroConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for bluemaestro.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize the config flow.""" + self._discovery_info: BluetoothServiceInfoBleak | None = None + self._discovered_device: DeviceData | 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 = DeviceData() + 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 = DeviceData() + 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/bluemaestro/const.py b/homeassistant/components/bluemaestro/const.py new file mode 100644 index 00000000000..757f1b7c810 --- /dev/null +++ b/homeassistant/components/bluemaestro/const.py @@ -0,0 +1,3 @@ +"""Constants for the BlueMaestro integration.""" + +DOMAIN = "bluemaestro" diff --git a/homeassistant/components/bluemaestro/device.py b/homeassistant/components/bluemaestro/device.py new file mode 100644 index 00000000000..3d6e4546882 --- /dev/null +++ b/homeassistant/components/bluemaestro/device.py @@ -0,0 +1,31 @@ +"""Support for BlueMaestro devices.""" +from __future__ import annotations + +from bluemaestro_ble import DeviceKey, SensorDeviceInfo + +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothEntityKey, +) +from homeassistant.const import ATTR_MANUFACTURER, ATTR_MODEL, ATTR_NAME +from homeassistant.helpers.entity import DeviceInfo + + +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 bluemaestro device info to a sensor device info.""" + hass_device_info = DeviceInfo({}) + if sensor_device_info.name is not None: + hass_device_info[ATTR_NAME] = sensor_device_info.name + if sensor_device_info.manufacturer is not None: + hass_device_info[ATTR_MANUFACTURER] = sensor_device_info.manufacturer + if sensor_device_info.model is not None: + hass_device_info[ATTR_MODEL] = sensor_device_info.model + return hass_device_info diff --git a/homeassistant/components/bluemaestro/manifest.json b/homeassistant/components/bluemaestro/manifest.json new file mode 100644 index 00000000000..0ff9cdd0794 --- /dev/null +++ b/homeassistant/components/bluemaestro/manifest.json @@ -0,0 +1,16 @@ +{ + "domain": "bluemaestro", + "name": "BlueMaestro", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/bluemaestro", + "bluetooth": [ + { + "manufacturer_id": 307, + "connectable": false + } + ], + "requirements": ["bluemaestro-ble==0.2.0"], + "dependencies": ["bluetooth"], + "codeowners": ["@bdraco"], + "iot_class": "local_push" +} diff --git a/homeassistant/components/bluemaestro/sensor.py b/homeassistant/components/bluemaestro/sensor.py new file mode 100644 index 00000000000..8afdef48d51 --- /dev/null +++ b/homeassistant/components/bluemaestro/sensor.py @@ -0,0 +1,149 @@ +"""Support for BlueMaestro sensors.""" +from __future__ import annotations + +from typing import Optional, Union + +from bluemaestro_ble import ( + SensorDeviceClass as BlueMaestroSensorDeviceClass, + SensorUpdate, + Units, +) + +from homeassistant import config_entries +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothDataProcessor, + PassiveBluetoothDataUpdate, + PassiveBluetoothProcessorCoordinator, + PassiveBluetoothProcessorEntity, +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.const import ( + PERCENTAGE, + PRESSURE_MBAR, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + TEMP_CELSIUS, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .device import device_key_to_bluetooth_entity_key, sensor_device_info_to_hass + +SENSOR_DESCRIPTIONS = { + (BlueMaestroSensorDeviceClass.BATTERY, Units.PERCENTAGE): SensorEntityDescription( + key=f"{BlueMaestroSensorDeviceClass.BATTERY}_{Units.PERCENTAGE}", + device_class=SensorDeviceClass.BATTERY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), + (BlueMaestroSensorDeviceClass.HUMIDITY, Units.PERCENTAGE): SensorEntityDescription( + key=f"{BlueMaestroSensorDeviceClass.HUMIDITY}_{Units.PERCENTAGE}", + device_class=SensorDeviceClass.HUMIDITY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + ( + BlueMaestroSensorDeviceClass.SIGNAL_STRENGTH, + Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + ): SensorEntityDescription( + key=f"{BlueMaestroSensorDeviceClass.SIGNAL_STRENGTH}_{Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT}", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + ( + BlueMaestroSensorDeviceClass.TEMPERATURE, + Units.TEMP_CELSIUS, + ): SensorEntityDescription( + key=f"{BlueMaestroSensorDeviceClass.TEMPERATURE}_{Units.TEMP_CELSIUS}", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=TEMP_CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + ), + ( + BlueMaestroSensorDeviceClass.DEW_POINT, + Units.TEMP_CELSIUS, + ): SensorEntityDescription( + key=f"{BlueMaestroSensorDeviceClass.DEW_POINT}_{Units.TEMP_CELSIUS}", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=TEMP_CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + ), + ( + BlueMaestroSensorDeviceClass.PRESSURE, + Units.PRESSURE_MBAR, + ): SensorEntityDescription( + key=f"{BlueMaestroSensorDeviceClass.PRESSURE}_{Units.PRESSURE_MBAR}", + device_class=SensorDeviceClass.PRESSURE, + native_unit_of_measurement=PRESSURE_MBAR, + state_class=SensorStateClass.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[ + (description.device_class, description.native_unit_of_measurement) + ] + for device_key, description in sensor_update.entity_descriptions.items() + if description.device_class and description.native_unit_of_measurement + }, + 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 BlueMaestro 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( + BlueMaestroBluetoothSensorEntity, async_add_entities + ) + ) + entry.async_on_unload(coordinator.async_register_processor(processor)) + + +class BlueMaestroBluetoothSensorEntity( + PassiveBluetoothProcessorEntity[ + PassiveBluetoothDataProcessor[Optional[Union[float, int]]] + ], + SensorEntity, +): + """Representation of a BlueMaestro 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/components/bluemaestro/strings.json b/homeassistant/components/bluemaestro/strings.json new file mode 100644 index 00000000000..a045d84771e --- /dev/null +++ b/homeassistant/components/bluemaestro/strings.json @@ -0,0 +1,22 @@ +{ + "config": { + "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "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%]" + } + }, + "abort": { + "not_supported": "Device not supported", + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/bluemaestro/translations/en.json b/homeassistant/components/bluemaestro/translations/en.json new file mode 100644 index 00000000000..ebd9760c161 --- /dev/null +++ b/homeassistant/components/bluemaestro/translations/en.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "already_in_progress": "Configuration flow is already in progress", + "no_devices_found": "No devices found on the network", + "not_supported": "Device not supported" + }, + "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 14156ada20c..c217cf790b8 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -7,6 +7,11 @@ from __future__ import annotations # fmt: off BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ + { + "domain": "bluemaestro", + "manufacturer_id": 307, + "connectable": False + }, { "domain": "bthome", "connectable": False, diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 5aa0ed69336..7e12845cf74 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -48,6 +48,7 @@ FLOWS = { "balboa", "blebox", "blink", + "bluemaestro", "bluetooth", "bmw_connected_drive", "bond", diff --git a/requirements_all.txt b/requirements_all.txt index a77007b00f8..a9c99079d64 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -422,6 +422,9 @@ blinkstick==1.2.0 # homeassistant.components.bitcoin blockchain==1.4.4 +# homeassistant.components.bluemaestro +bluemaestro-ble==0.2.0 + # homeassistant.components.decora # homeassistant.components.zengge # bluepy==1.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 518af11783d..60bc497e6d8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -337,6 +337,9 @@ blebox_uniapi==2.0.2 # homeassistant.components.blink blinkpy==0.19.0 +# homeassistant.components.bluemaestro +bluemaestro-ble==0.2.0 + # homeassistant.components.bluetooth bluetooth-adapters==0.3.4 diff --git a/tests/components/bluemaestro/__init__.py b/tests/components/bluemaestro/__init__.py new file mode 100644 index 00000000000..bd9b86e040f --- /dev/null +++ b/tests/components/bluemaestro/__init__.py @@ -0,0 +1,26 @@ +"""Tests for the BlueMaestro integration.""" + + +from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo + +NOT_BLUEMAESTRO_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", +) + +BLUEMAESTRO_SERVICE_INFO = BluetoothServiceInfo( + name="FA17B62C", + manufacturer_data={ + 307: b"\x17d\x0e\x10\x00\x02\x00\xf2\x01\xf2\x00\x83\x01\x00\x01\r\x02\xab\x00\xf2\x01\xf2\x01\r\x02\xab\x00\xf2\x01\xf2\x00\xff\x02N\x00\x00\x00\x00\x00" + }, + address="aa:bb:cc:dd:ee:ff", + rssi=-60, + service_data={}, + service_uuids=[], + source="local", +) diff --git a/tests/components/bluemaestro/conftest.py b/tests/components/bluemaestro/conftest.py new file mode 100644 index 00000000000..e40cf1e30f4 --- /dev/null +++ b/tests/components/bluemaestro/conftest.py @@ -0,0 +1,8 @@ +"""BlueMaestro session fixtures.""" + +import pytest + + +@pytest.fixture(autouse=True) +def mock_bluetooth(enable_bluetooth): + """Auto mock bluetooth.""" diff --git a/tests/components/bluemaestro/test_config_flow.py b/tests/components/bluemaestro/test_config_flow.py new file mode 100644 index 00000000000..116380a0df0 --- /dev/null +++ b/tests/components/bluemaestro/test_config_flow.py @@ -0,0 +1,200 @@ +"""Test the BlueMaestro config flow.""" + +from unittest.mock import patch + +from homeassistant import config_entries +from homeassistant.components.bluemaestro.const import DOMAIN +from homeassistant.data_entry_flow import FlowResultType + +from . import BLUEMAESTRO_SERVICE_INFO, NOT_BLUEMAESTRO_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=BLUEMAESTRO_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + with patch( + "homeassistant.components.bluemaestro.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"] == "Tempo Disc THD EEFF" + assert result2["data"] == {} + assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" + + +async def test_async_step_bluetooth_not_bluemaestro(hass): + """Test discovery via bluetooth not bluemaestro.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=NOT_BLUEMAESTRO_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.bluemaestro.config_flow.async_discovered_service_info", + return_value=[BLUEMAESTRO_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.bluemaestro.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"] == "Tempo Disc THD EEFF" + 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): + """Test the device gets added via another flow between steps.""" + with patch( + "homeassistant.components.bluemaestro.config_flow.async_discovered_service_info", + return_value=[BLUEMAESTRO_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.bluemaestro.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): + """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.bluemaestro.config_flow.async_discovered_service_info", + return_value=[BLUEMAESTRO_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="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=BLUEMAESTRO_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=BLUEMAESTRO_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=BLUEMAESTRO_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=BLUEMAESTRO_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + with patch( + "homeassistant.components.bluemaestro.config_flow.async_discovered_service_info", + return_value=[BLUEMAESTRO_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.bluemaestro.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"] == "Tempo Disc THD EEFF" + 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) diff --git a/tests/components/bluemaestro/test_sensor.py b/tests/components/bluemaestro/test_sensor.py new file mode 100644 index 00000000000..2f964e65481 --- /dev/null +++ b/tests/components/bluemaestro/test_sensor.py @@ -0,0 +1,50 @@ +"""Test the BlueMaestro sensors.""" + +from unittest.mock import patch + +from homeassistant.components.bluemaestro.const import DOMAIN +from homeassistant.components.bluetooth import BluetoothChange +from homeassistant.components.sensor import ATTR_STATE_CLASS +from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT + +from . import BLUEMAESTRO_SERVICE_INFO + +from tests.common import MockConfigEntry + + +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) + + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all("sensor")) == 0 + saved_callback(BLUEMAESTRO_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + await hass.async_block_till_done() + assert len(hass.states.async_all("sensor")) == 4 + + humid_sensor = hass.states.get("sensor.tempo_disc_thd_eeff_temperature") + humid_sensor_attrs = humid_sensor.attributes + assert humid_sensor.state == "24.2" + assert humid_sensor_attrs[ATTR_FRIENDLY_NAME] == "Tempo Disc THD EEFF Temperature" + assert humid_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == "°C" + assert humid_sensor_attrs[ATTR_STATE_CLASS] == "measurement" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() From 7e9f1a508a52167e42fb772de5e36f6050022592 Mon Sep 17 00:00:00 2001 From: Justin Vanderhooft Date: Sun, 4 Sep 2022 09:56:10 -0400 Subject: [PATCH 073/955] Tweak unique id formatting for Melnor Bluetooth switches (#77773) --- homeassistant/components/melnor/switch.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/homeassistant/components/melnor/switch.py b/homeassistant/components/melnor/switch.py index c2d32c428d3..7a615a8582d 100644 --- a/homeassistant/components/melnor/switch.py +++ b/homeassistant/components/melnor/switch.py @@ -48,10 +48,7 @@ class MelnorSwitch(MelnorBluetoothBaseEntity, SwitchEntity): super().__init__(coordinator) self._valve_index = valve_index - self._attr_unique_id = ( - f"switch-{self._attr_unique_id}-zone{self._valve().id}-manual" - ) - + self._attr_unique_id = f"{self._attr_unique_id}-zone{self._valve().id}-manual" self._attr_name = f"{self._device.name} Zone {self._valve().id+1}" @property From fe65c02c2b129b70492aca32239f965a239ede21 Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Sun, 4 Sep 2022 16:06:59 +0200 Subject: [PATCH 074/955] Add Boost/Away mode duration to Overkiz integration (#76690) --- homeassistant/components/overkiz/number.py | 69 +++++++++++++++++++++- 1 file changed, 68 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/overkiz/number.py b/homeassistant/components/overkiz/number.py index 8e7d2a93ee9..3fb5cb2bcff 100644 --- a/homeassistant/components/overkiz/number.py +++ b/homeassistant/components/overkiz/number.py @@ -1,10 +1,12 @@ """Support for Overkiz (virtual) numbers.""" from __future__ import annotations +import asyncio +from collections.abc import Awaitable, Callable from dataclasses import dataclass from typing import cast -from pyoverkiz.enums import OverkizCommand, OverkizState +from pyoverkiz.enums import OverkizCommand, OverkizCommandParam, OverkizState from homeassistant.components.number import ( NumberDeviceClass, @@ -21,6 +23,9 @@ from . import HomeAssistantOverkizData from .const import DOMAIN, IGNORED_OVERKIZ_DEVICES from .entity import OverkizDescriptiveEntity +BOOST_MODE_DURATION_DELAY = 1 +OPERATING_MODE_DELAY = 3 + @dataclass class OverkizNumberDescriptionMixin: @@ -34,6 +39,41 @@ class OverkizNumberDescription(NumberEntityDescription, OverkizNumberDescription """Class to describe an Overkiz number.""" inverted: bool = False + set_native_value: Callable[ + [float, Callable[..., Awaitable[None]]], Awaitable[None] + ] | None = None + + +async def _async_set_native_value_boost_mode_duration( + value: float, execute_command: Callable[..., Awaitable[None]] +) -> None: + """Update the boost duration value.""" + + if value > 0: + await execute_command(OverkizCommand.SET_BOOST_MODE_DURATION, value) + await asyncio.sleep( + BOOST_MODE_DURATION_DELAY + ) # wait one second to not overload the device + await execute_command( + OverkizCommand.SET_CURRENT_OPERATING_MODE, + { + OverkizCommandParam.RELAUNCH: OverkizCommandParam.ON, + OverkizCommandParam.ABSENCE: OverkizCommandParam.OFF, + }, + ) + else: + await execute_command( + OverkizCommand.SET_CURRENT_OPERATING_MODE, + { + OverkizCommandParam.RELAUNCH: OverkizCommandParam.OFF, + OverkizCommandParam.ABSENCE: OverkizCommandParam.OFF, + }, + ) + + await asyncio.sleep( + OPERATING_MODE_DELAY + ) # wait 3 seconds to have the new duration in + await execute_command(OverkizCommand.REFRESH_BOOST_MODE_DURATION) NUMBER_DESCRIPTIONS: list[OverkizNumberDescription] = [ @@ -101,6 +141,27 @@ NUMBER_DESCRIPTIONS: list[OverkizNumberDescription] = [ native_max_value=100, inverted=True, ), + # DomesticHotWaterProduction - boost mode duration in days (0 - 7) + OverkizNumberDescription( + key=OverkizState.CORE_BOOST_MODE_DURATION, + name="Boost mode duration", + icon="mdi:water-boiler", + command=OverkizCommand.SET_BOOST_MODE_DURATION, + native_min_value=0, + native_max_value=7, + set_native_value=_async_set_native_value_boost_mode_duration, + entity_category=EntityCategory.CONFIG, + ), + # DomesticHotWaterProduction - away mode in days (0 - 6) + OverkizNumberDescription( + key=OverkizState.IO_AWAY_MODE_DURATION, + name="Away mode duration", + icon="mdi:water-boiler-off", + command=OverkizCommand.SET_AWAY_MODE_DURATION, + native_min_value=0, + native_max_value=6, + entity_category=EntityCategory.CONFIG, + ), ] SUPPORTED_STATES = {description.key: description for description in NUMBER_DESCRIPTIONS} @@ -156,6 +217,12 @@ class OverkizNumber(OverkizDescriptiveEntity, NumberEntity): if self.entity_description.inverted: value = self.native_max_value - value + if self.entity_description.set_native_value: + await self.entity_description.set_native_value( + value, self.executor.async_execute_command + ) + return + await self.executor.async_execute_command( self.entity_description.command, value ) From a9c19e2ffb070b5947fecb7cc802d3b3a5164775 Mon Sep 17 00:00:00 2001 From: Matt Zimmerman Date: Sun, 4 Sep 2022 07:51:48 -0700 Subject: [PATCH 075/955] Update smarttub to 0.0.33 (#77766) --- homeassistant/components/smarttub/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/smarttub/manifest.json b/homeassistant/components/smarttub/manifest.json index 1d7500b9185..e2f72642a91 100644 --- a/homeassistant/components/smarttub/manifest.json +++ b/homeassistant/components/smarttub/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/smarttub", "dependencies": [], "codeowners": ["@mdz"], - "requirements": ["python-smarttub==0.0.32"], + "requirements": ["python-smarttub==0.0.33"], "quality_scale": "platinum", "iot_class": "cloud_polling", "loggers": ["smarttub"] diff --git a/requirements_all.txt b/requirements_all.txt index a9c99079d64..f70b578d225 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1996,7 +1996,7 @@ python-qbittorrent==0.4.2 python-ripple-api==0.0.3 # homeassistant.components.smarttub -python-smarttub==0.0.32 +python-smarttub==0.0.33 # homeassistant.components.songpal python-songpal==0.15 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 60bc497e6d8..af769ef2bcd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1371,7 +1371,7 @@ python-nest==4.2.0 python-picnic-api==1.1.0 # homeassistant.components.smarttub -python-smarttub==0.0.32 +python-smarttub==0.0.33 # homeassistant.components.songpal python-songpal==0.15 From 0e63a4c091909b359f339f975b80c8a5385d034b Mon Sep 17 00:00:00 2001 From: Avi Miller Date: Mon, 5 Sep 2022 01:51:57 +1000 Subject: [PATCH 076/955] Fix lifx service call interference (#77770) * Fix #77735 by restoring the wait to let state settle Signed-off-by: Avi Miller * Skip the asyncio.sleep during testing Signed-off-by: Avi Miller * Patch out asyncio.sleep for lifx tests Signed-off-by: Avi Miller * Patch out a constant instead of overriding asyncio.sleep directly Signed-off-by: Avi Miller Signed-off-by: Avi Miller --- homeassistant/components/lifx/coordinator.py | 3 ++- homeassistant/components/lifx/light.py | 7 +++++-- tests/components/lifx/conftest.py | 1 - tests/components/lifx/test_button.py | 11 +++++++++++ tests/components/lifx/test_light.py | 8 +++++++- 5 files changed, 25 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/lifx/coordinator.py b/homeassistant/components/lifx/coordinator.py index d01fb266c6f..37e753c27a3 100644 --- a/homeassistant/components/lifx/coordinator.py +++ b/homeassistant/components/lifx/coordinator.py @@ -25,6 +25,7 @@ from .const import ( from .util import async_execute_lifx, get_real_mac_addr, lifx_features REQUEST_REFRESH_DELAY = 0.35 +LIFX_IDENTIFY_DELAY = 3.0 class LIFXUpdateCoordinator(DataUpdateCoordinator): @@ -92,7 +93,7 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator): # Turn the bulb on first, flash for 3 seconds, then turn off await self.async_set_power(state=True, duration=1) await self.async_set_waveform_optional(value=IDENTIFY_WAVEFORM) - await asyncio.sleep(3) + await asyncio.sleep(LIFX_IDENTIFY_DELAY) await self.async_set_power(state=False, duration=1) async def _async_update_data(self) -> None: diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index fe17dd95788..36d3b480f74 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -39,7 +39,7 @@ from .manager import ( ) from .util import convert_8_to_16, convert_16_to_8, find_hsbk, lifx_features, merge_hsbk -COLOR_ZONE_POPULATE_DELAY = 0.3 +LIFX_STATE_SETTLE_DELAY = 0.3 SERVICE_LIFX_SET_STATE = "set_state" @@ -231,6 +231,9 @@ class LIFXLight(LIFXEntity, LightEntity): if power_off: await self.set_power(False, duration=fade) + # Avoid state ping-pong by holding off updates as the state settles + await asyncio.sleep(LIFX_STATE_SETTLE_DELAY) + # Update when the transition starts and ends await self.update_during_transition(fade) @@ -338,7 +341,7 @@ class LIFXStrip(LIFXColor): # Zone brightness is not reported when powered off if not self.is_on and hsbk[HSBK_BRIGHTNESS] is None: await self.set_power(True) - await asyncio.sleep(COLOR_ZONE_POPULATE_DELAY) + await asyncio.sleep(LIFX_STATE_SETTLE_DELAY) await self.update_color_zones() await self.set_power(False) diff --git a/tests/components/lifx/conftest.py b/tests/components/lifx/conftest.py index 326c4f75413..a243132dc65 100644 --- a/tests/components/lifx/conftest.py +++ b/tests/components/lifx/conftest.py @@ -1,5 +1,4 @@ """Tests for the lifx integration.""" - from unittest.mock import AsyncMock, MagicMock, patch import pytest diff --git a/tests/components/lifx/test_button.py b/tests/components/lifx/test_button.py index abc91128e25..b166aa05d66 100644 --- a/tests/components/lifx/test_button.py +++ b/tests/components/lifx/test_button.py @@ -1,4 +1,8 @@ """Tests for button platform.""" +from unittest.mock import patch + +import pytest + from homeassistant.components import lifx from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN from homeassistant.components.lifx.const import DOMAIN @@ -21,6 +25,13 @@ from . import ( from tests.common import MockConfigEntry +@pytest.fixture(autouse=True) +def mock_lifx_coordinator_sleep(): + """Mock out lifx coordinator sleeps.""" + with patch("homeassistant.components.lifx.coordinator.LIFX_IDENTIFY_DELAY", 0): + yield + + async def test_button_restart(hass: HomeAssistant) -> None: """Test that a bulb can be restarted.""" config_entry = MockConfigEntry( diff --git a/tests/components/lifx/test_light.py b/tests/components/lifx/test_light.py index 5b641e850f2..6229e130a40 100644 --- a/tests/components/lifx/test_light.py +++ b/tests/components/lifx/test_light.py @@ -50,6 +50,13 @@ from . import ( from tests.common import MockConfigEntry, async_fire_time_changed +@pytest.fixture(autouse=True) +def patch_lifx_state_settle_delay(): + """Set asyncio.sleep for state settles to zero.""" + with patch("homeassistant.components.lifx.light.LIFX_STATE_SETTLE_DELAY", 0): + yield + + async def test_light_unique_id(hass: HomeAssistant) -> None: """Test a light unique id.""" already_migrated_config_entry = MockConfigEntry( @@ -98,7 +105,6 @@ async def test_light_unique_id_new_firmware(hass: HomeAssistant) -> None: assert device.identifiers == {(DOMAIN, SERIAL)} -@patch("homeassistant.components.lifx.light.COLOR_ZONE_POPULATE_DELAY", 0) async def test_light_strip(hass: HomeAssistant) -> None: """Test a light strip.""" already_migrated_config_entry = MockConfigEntry( From 1b131055380866d75f14a0f9e8fbc719a5e009d0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 4 Sep 2022 12:00:19 -0400 Subject: [PATCH 077/955] Bump flux_led to 0.28.32 (#77787) --- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 4afd0cdb855..632ef04e456 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["network"], "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.28.31"], + "requirements": ["flux_led==0.28.32"], "quality_scale": "platinum", "codeowners": ["@icemanch", "@bdraco"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index f70b578d225..1fd703dd6c5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -682,7 +682,7 @@ fjaraskupan==2.0.0 flipr-api==1.4.2 # homeassistant.components.flux_led -flux_led==0.28.31 +flux_led==0.28.32 # homeassistant.components.homekit # homeassistant.components.recorder diff --git a/requirements_test_all.txt b/requirements_test_all.txt index af769ef2bcd..531a3f80dd3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -501,7 +501,7 @@ fjaraskupan==2.0.0 flipr-api==1.4.2 # homeassistant.components.flux_led -flux_led==0.28.31 +flux_led==0.28.32 # homeassistant.components.homekit # homeassistant.components.recorder From 98441e8620f36fd73e35bda220c59c36f05f5e37 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 4 Sep 2022 18:06:04 +0200 Subject: [PATCH 078/955] Bump pysensibo to 1.0.19 (#77790) --- homeassistant/components/sensibo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensibo/manifest.json b/homeassistant/components/sensibo/manifest.json index 5ef8ff6fa4e..a2a7cbe3bd0 100644 --- a/homeassistant/components/sensibo/manifest.json +++ b/homeassistant/components/sensibo/manifest.json @@ -2,7 +2,7 @@ "domain": "sensibo", "name": "Sensibo", "documentation": "https://www.home-assistant.io/integrations/sensibo", - "requirements": ["pysensibo==1.0.18"], + "requirements": ["pysensibo==1.0.19"], "config_flow": true, "codeowners": ["@andrey-git", "@gjohansson-ST"], "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 1fd703dd6c5..f4055652d3a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1838,7 +1838,7 @@ pysaj==0.0.16 pysdcp==1 # homeassistant.components.sensibo -pysensibo==1.0.18 +pysensibo==1.0.19 # homeassistant.components.serial # homeassistant.components.zha diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 531a3f80dd3..291afe44e22 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1288,7 +1288,7 @@ pyruckus==0.16 pysabnzbd==1.1.1 # homeassistant.components.sensibo -pysensibo==1.0.18 +pysensibo==1.0.19 # homeassistant.components.serial # homeassistant.components.zha From e1150ce190f5f61f900f64c2e78469fc00ec2f41 Mon Sep 17 00:00:00 2001 From: Justin Vanderhooft Date: Sun, 4 Sep 2022 12:47:13 -0400 Subject: [PATCH 079/955] Expose battery and rssi sensors in Melnor Bluetooth integration (#77576) --- homeassistant/components/melnor/__init__.py | 5 +- homeassistant/components/melnor/models.py | 3 +- homeassistant/components/melnor/sensor.py | 101 ++++++++++++++ homeassistant/components/melnor/switch.py | 8 +- tests/components/melnor/__init__.py | 63 --------- tests/components/melnor/conftest.py | 139 ++++++++++++++++++++ tests/components/melnor/test_config_flow.py | 2 +- tests/components/melnor/test_sensor.py | 69 ++++++++++ 8 files changed, 319 insertions(+), 71 deletions(-) create mode 100644 homeassistant/components/melnor/sensor.py create mode 100644 tests/components/melnor/conftest.py create mode 100644 tests/components/melnor/test_sensor.py diff --git a/homeassistant/components/melnor/__init__.py b/homeassistant/components/melnor/__init__.py index 5fd697b2088..433380a9ab9 100644 --- a/homeassistant/components/melnor/__init__.py +++ b/homeassistant/components/melnor/__init__.py @@ -14,7 +14,10 @@ from homeassistant.exceptions import ConfigEntryNotReady from .const import DOMAIN from .models import MelnorDataUpdateCoordinator -PLATFORMS: list[Platform] = [Platform.SWITCH] +PLATFORMS: list[Platform] = [ + Platform.SENSOR, + Platform.SWITCH, +] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/melnor/models.py b/homeassistant/components/melnor/models.py index 4796bf601ff..c050c7f680b 100644 --- a/homeassistant/components/melnor/models.py +++ b/homeassistant/components/melnor/models.py @@ -43,6 +43,7 @@ class MelnorBluetoothBaseEntity(CoordinatorEntity[MelnorDataUpdateCoordinator]): """Base class for melnor entities.""" _device: Device + _attr_has_entity_name = True def __init__( self, @@ -59,8 +60,6 @@ class MelnorBluetoothBaseEntity(CoordinatorEntity[MelnorDataUpdateCoordinator]): model=self._device.model, name=self._device.name, ) - self._attr_name = self._device.name - self._attr_unique_id = self._device.mac @callback def _handle_coordinator_update(self) -> None: diff --git a/homeassistant/components/melnor/sensor.py b/homeassistant/components/melnor/sensor.py new file mode 100644 index 00000000000..567f9dc6f2c --- /dev/null +++ b/homeassistant/components/melnor/sensor.py @@ -0,0 +1,101 @@ +"""Support for Melnor RainCloud sprinkler water timer.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any + +from melnor_bluetooth.device import Device + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType + +from .const import DOMAIN +from .models import MelnorBluetoothBaseEntity, MelnorDataUpdateCoordinator + + +@dataclass +class MelnorSensorEntityDescriptionMixin: + """Mixin for required keys.""" + + state_fn: Callable[[Device], Any] + + +@dataclass +class MelnorSensorEntityDescription( + SensorEntityDescription, MelnorSensorEntityDescriptionMixin +): + """Describes Melnor sensor entity.""" + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_devices: AddEntitiesCallback, +) -> None: + """Set up the sensor platform.""" + + coordinator: MelnorDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + + sensors: list[MelnorSensorEntityDescription] = [ + MelnorSensorEntityDescription( + device_class=SensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, + key="battery", + name="Battery", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + state_fn=lambda device: device.battery_level, + ), + MelnorSensorEntityDescription( + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + key="rssi", + name="RSSI", + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + state_class=SensorStateClass.MEASUREMENT, + state_fn=lambda device: device.rssi, + ), + ] + + async_add_devices( + MelnorSensorEntity( + coordinator, + description, + ) + for description in sensors + ) + + +class MelnorSensorEntity(MelnorBluetoothBaseEntity, SensorEntity): + """Representation of a Melnor sensor.""" + + entity_description: MelnorSensorEntityDescription + + def __init__( + self, + coordinator: MelnorDataUpdateCoordinator, + entity_description: MelnorSensorEntityDescription, + ) -> None: + """Initialize a sensor for a Melnor device.""" + super().__init__(coordinator) + + self._attr_unique_id = f"{self._device.mac}-{entity_description.key}" + + self.entity_description = entity_description + + @property + def native_value(self) -> StateType: + """Return the battery level.""" + return self.entity_description.state_fn(self._device) diff --git a/homeassistant/components/melnor/switch.py b/homeassistant/components/melnor/switch.py index 7a615a8582d..125d3ffde8c 100644 --- a/homeassistant/components/melnor/switch.py +++ b/homeassistant/components/melnor/switch.py @@ -30,13 +30,12 @@ async def async_setup_entry( if coordinator.data[f"zone{i}"] is not None: switches.append(MelnorSwitch(coordinator, i)) - async_add_devices(switches, True) + async_add_devices(switches) class MelnorSwitch(MelnorBluetoothBaseEntity, SwitchEntity): """A switch implementation for a melnor device.""" - _valve_index: int _attr_icon = "mdi:sprinkler" def __init__( @@ -48,8 +47,9 @@ class MelnorSwitch(MelnorBluetoothBaseEntity, SwitchEntity): super().__init__(coordinator) self._valve_index = valve_index - self._attr_unique_id = f"{self._attr_unique_id}-zone{self._valve().id}-manual" - self._attr_name = f"{self._device.name} Zone {self._valve().id+1}" + valve_id = self._valve().id + self._attr_name = f"Zone {valve_id+1}" + self._attr_unique_id = f"{self._device.mac}-zone{valve_id}-manual" @property def is_on(self) -> bool: diff --git a/tests/components/melnor/__init__.py b/tests/components/melnor/__init__.py index 7af59d55a11..7f460e7848d 100644 --- a/tests/components/melnor/__init__.py +++ b/tests/components/melnor/__init__.py @@ -1,64 +1 @@ """Tests for the melnor integration.""" - -from __future__ import annotations - -from unittest.mock import patch - -from bleak.backends.device import BLEDevice -from bleak.backends.scanner import AdvertisementData - -from homeassistant.components.bluetooth.models import BluetoothServiceInfoBleak - -FAKE_ADDRESS_1 = "FAKE-ADDRESS-1" -FAKE_ADDRESS_2 = "FAKE-ADDRESS-2" - - -FAKE_SERVICE_INFO_1 = BluetoothServiceInfoBleak( - name="YM_TIMER%", - address=FAKE_ADDRESS_1, - rssi=-63, - manufacturer_data={ - 13: b"Y\x08\x02\x8f\x00\x00\x00\x00\x00\x00\xf0\x00\x00\xf0\x00\x00\xf0\x00\x00\xf0*\x9b\xcf\xbc" - }, - service_uuids=[], - service_data={}, - source="local", - device=BLEDevice(FAKE_ADDRESS_1, None), - advertisement=AdvertisementData(local_name=""), - time=0, - connectable=True, -) - -FAKE_SERVICE_INFO_2 = BluetoothServiceInfoBleak( - name="YM_TIMER%", - address=FAKE_ADDRESS_2, - rssi=-63, - manufacturer_data={ - 13: b"Y\x08\x02\x8f\x00\x00\x00\x00\x00\x00\xf0\x00\x00\xf0\x00\x00\xf0\x00\x00\xf0*\x9b\xcf\xbc" - }, - service_uuids=[], - service_data={}, - source="local", - device=BLEDevice(FAKE_ADDRESS_2, None), - advertisement=AdvertisementData(local_name=""), - time=0, - connectable=True, -) - - -def patch_async_setup_entry(return_value=True): - """Patch async setup entry to return True.""" - return patch( - "homeassistant.components.melnor.async_setup_entry", - return_value=return_value, - ) - - -def patch_async_discovered_service_info( - return_value: list[BluetoothServiceInfoBleak] = [FAKE_SERVICE_INFO_1], -): - """Patch async_discovered_service_info a mocked device info.""" - return patch( - "homeassistant.components.melnor.config_flow.async_discovered_service_info", - return_value=return_value, - ) diff --git a/tests/components/melnor/conftest.py b/tests/components/melnor/conftest.py new file mode 100644 index 00000000000..403ae83bb67 --- /dev/null +++ b/tests/components/melnor/conftest.py @@ -0,0 +1,139 @@ +"""Tests for the melnor integration.""" + +from __future__ import annotations + +from unittest.mock import AsyncMock, Mock, patch + +from bleak.backends.device import BLEDevice +from bleak.backends.scanner import AdvertisementData +from melnor_bluetooth.device import Device, Valve + +from homeassistant.components.bluetooth.models import BluetoothServiceInfoBleak +from homeassistant.components.melnor.const import DOMAIN +from homeassistant.const import CONF_ADDRESS +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + +FAKE_ADDRESS_1 = "FAKE-ADDRESS-1" +FAKE_ADDRESS_2 = "FAKE-ADDRESS-2" + + +FAKE_SERVICE_INFO_1 = BluetoothServiceInfoBleak( + name="YM_TIMER%", + address=FAKE_ADDRESS_1, + rssi=-63, + manufacturer_data={ + 13: b"Y\x08\x02\x8f\x00\x00\x00\x00\x00\x00\xf0\x00\x00\xf0\x00\x00\xf0\x00\x00\xf0*\x9b\xcf\xbc" + }, + service_uuids=[], + service_data={}, + source="local", + device=BLEDevice(FAKE_ADDRESS_1, None), + advertisement=AdvertisementData(local_name=""), + time=0, + connectable=True, +) + +FAKE_SERVICE_INFO_2 = BluetoothServiceInfoBleak( + name="YM_TIMER%", + address=FAKE_ADDRESS_2, + rssi=-63, + manufacturer_data={ + 13: b"Y\x08\x02\x8f\x00\x00\x00\x00\x00\x00\xf0\x00\x00\xf0\x00\x00\xf0\x00\x00\xf0*\x9b\xcf\xbc" + }, + service_uuids=[], + service_data={}, + source="local", + device=BLEDevice(FAKE_ADDRESS_2, None), + advertisement=AdvertisementData(local_name=""), + time=0, + connectable=True, +) + + +def mock_config_entry(hass: HomeAssistant): + """Return a mock config entry.""" + + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=FAKE_ADDRESS_1, + data={CONF_ADDRESS: FAKE_ADDRESS_1}, + ) + entry.add_to_hass(hass) + + return entry + + +def mock_melnor_valve(identifier: int): + """Return a mocked Melnor valve.""" + valve = Mock(spec=Valve) + valve.id = identifier + + return valve + + +def mock_melnor_device(): + """Return a mocked Melnor device.""" + + with patch("melnor_bluetooth.device.Device") as mock: + + device = mock.return_value + + device.connect = AsyncMock(return_value=True) + device.disconnect = AsyncMock(return_value=True) + device.fetch_state = AsyncMock(return_value=device) + + device.battery_level = 80 + device.mac = FAKE_ADDRESS_1 + device.model = "test_model" + device.name = "test_melnor" + device.rssi = -50 + + device.zone1 = mock_melnor_valve(1) + device.zone2 = mock_melnor_valve(2) + device.zone3 = mock_melnor_valve(3) + device.zone4 = mock_melnor_valve(4) + + device.__getitem__.side_effect = lambda key: getattr(device, key) + + return device + + +def patch_async_setup_entry(return_value=True): + """Patch async setup entry to return True.""" + return patch( + "homeassistant.components.melnor.async_setup_entry", + return_value=return_value, + ) + + +# pylint: disable=dangerous-default-value +def patch_async_discovered_service_info( + return_value: list[BluetoothServiceInfoBleak] = [FAKE_SERVICE_INFO_1], +): + """Patch async_discovered_service_info a mocked device info.""" + return patch( + "homeassistant.components.melnor.config_flow.async_discovered_service_info", + return_value=return_value, + ) + + +def patch_async_ble_device_from_address( + return_value: BluetoothServiceInfoBleak | None = FAKE_SERVICE_INFO_1, +): + """Patch async_ble_device_from_address to return a mocked BluetoothServiceInfoBleak.""" + return patch( + "homeassistant.components.bluetooth.async_ble_device_from_address", + return_value=return_value, + ) + + +def patch_melnor_device(device: Device = mock_melnor_device()): + """Patch melnor_bluetooth.device to return a mocked Melnor device.""" + return patch("homeassistant.components.melnor.Device", return_value=device) + + +def patch_async_register_callback(): + """Patch async_register_callback to return True.""" + return patch("homeassistant.components.bluetooth.async_register_callback") diff --git a/tests/components/melnor/test_config_flow.py b/tests/components/melnor/test_config_flow.py index 3b550fba3f7..364531a314a 100644 --- a/tests/components/melnor/test_config_flow.py +++ b/tests/components/melnor/test_config_flow.py @@ -8,7 +8,7 @@ from homeassistant.components.melnor.const import DOMAIN from homeassistant.const import CONF_ADDRESS, CONF_MAC from homeassistant.data_entry_flow import FlowResultType -from . import ( +from .conftest import ( FAKE_ADDRESS_1, FAKE_SERVICE_INFO_1, FAKE_SERVICE_INFO_2, diff --git a/tests/components/melnor/test_sensor.py b/tests/components/melnor/test_sensor.py new file mode 100644 index 00000000000..bef2bba35e0 --- /dev/null +++ b/tests/components/melnor/test_sensor.py @@ -0,0 +1,69 @@ +"""Test the Melnor sensors.""" + +from __future__ import annotations + +from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass +from homeassistant.const import PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT +from homeassistant.helpers import entity_registry + +from .conftest import ( + mock_config_entry, + mock_melnor_device, + patch_async_ble_device_from_address, + patch_async_register_callback, + patch_melnor_device, +) + + +async def test_battery_sensor(hass): + """Test the battery sensor.""" + + entry = mock_config_entry(hass) + + with patch_async_ble_device_from_address(), patch_melnor_device(), patch_async_register_callback(): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + battery_sensor = hass.states.get("sensor.test_melnor_battery") + assert battery_sensor.state == "80" + assert battery_sensor.attributes["unit_of_measurement"] == PERCENTAGE + assert battery_sensor.attributes["device_class"] == SensorDeviceClass.BATTERY + assert battery_sensor.attributes["state_class"] == SensorStateClass.MEASUREMENT + + +async def test_rssi_sensor(hass): + """Test the rssi sensor.""" + + entry = mock_config_entry(hass) + + device = mock_melnor_device() + + with patch_async_ble_device_from_address(), patch_melnor_device( + device + ), patch_async_register_callback(): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + entity_id = f"sensor.{device.name}_rssi" + + # Ensure the entity is disabled by default by checking the registry + ent_registry = entity_registry.async_get(hass) + + rssi_registry_entry = ent_registry.async_get(entity_id) + + assert rssi_registry_entry is not None + assert rssi_registry_entry.disabled_by is not None + + # Enable the entity and assert everything else is working as expected + ent_registry.async_update_entity(entity_id, disabled_by=None) + + await hass.config_entries.async_reload(entry.entry_id) + await hass.async_block_till_done() + + rssi = hass.states.get(entity_id) + + assert ( + rssi.attributes["unit_of_measurement"] == SIGNAL_STRENGTH_DECIBELS_MILLIWATT + ) + assert rssi.attributes["device_class"] == SensorDeviceClass.SIGNAL_STRENGTH + assert rssi.attributes["state_class"] == SensorStateClass.MEASUREMENT From 2b63d7644af9dd34a32c7ec1e56669af8b9e16b4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 4 Sep 2022 12:54:40 -0400 Subject: [PATCH 080/955] Bump led-ble to 0.6.0 (#77788) * Bump ble-led to 0.6.0 Fixes reading the white channel on same devices Changelog: https://github.com/Bluetooth-Devices/led-ble/compare/v0.5.4...v0.6.0 * Bump flux_led to 0.28.32 Changelog: https://github.com/Danielhiversen/flux_led/compare/0.28.31...0.28.32 Fixes white channel support for some more older protocols * keep them in sync * Update homeassistant/components/led_ble/manifest.json --- homeassistant/components/led_ble/manifest.json | 6 ++++-- homeassistant/generated/bluetooth.py | 8 ++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/led_ble/manifest.json b/homeassistant/components/led_ble/manifest.json index 376fadcb3be..a0f5e3481d5 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/ble_ble", - "requirements": ["led-ble==0.5.4"], + "requirements": ["led-ble==0.6.0"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "bluetooth": [ @@ -11,7 +11,9 @@ { "local_name": "BLE-LED*" }, { "local_name": "LEDBLE*" }, { "local_name": "Triones*" }, - { "local_name": "LEDBlue*" } + { "local_name": "LEDBlue*" }, + { "local_name": "Dream~*" }, + { "local_name": "QHM-*" } ], "iot_class": "local_polling" } diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index c217cf790b8..83bfd3ab5eb 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -156,6 +156,14 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ "domain": "led_ble", "local_name": "LEDBlue*" }, + { + "domain": "led_ble", + "local_name": "Dream~*" + }, + { + "domain": "led_ble", + "local_name": "QHM-*" + }, { "domain": "melnor", "manufacturer_data_start": [ diff --git a/requirements_all.txt b/requirements_all.txt index f4055652d3a..0a10f451e5c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -968,7 +968,7 @@ lakeside==0.12 laundrify_aio==1.1.2 # homeassistant.components.led_ble -led-ble==0.5.4 +led-ble==0.6.0 # homeassistant.components.foscam libpyfoscam==1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 291afe44e22..0db5d027900 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -706,7 +706,7 @@ lacrosse-view==0.0.9 laundrify_aio==1.1.2 # homeassistant.components.led_ble -led-ble==0.5.4 +led-ble==0.6.0 # homeassistant.components.foscam libpyfoscam==1.0 From 03b3959b959841283262d844706830f27cef485d Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 4 Sep 2022 18:57:50 +0200 Subject: [PATCH 081/955] Replace archived sucks by py-sucks and bump to 0.9.8 for Ecovacs integration (#77768) --- CODEOWNERS | 2 +- homeassistant/components/ecovacs/manifest.json | 4 ++-- requirements_all.txt | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 6e2efe9101d..61620875f39 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -277,7 +277,7 @@ build.json @home-assistant/supervisor /tests/components/ecobee/ @marthoc /homeassistant/components/econet/ @vangorra @w1ll1am23 /tests/components/econet/ @vangorra @w1ll1am23 -/homeassistant/components/ecovacs/ @OverloadUT +/homeassistant/components/ecovacs/ @OverloadUT @mib1185 /homeassistant/components/ecowitt/ @pvizeli /tests/components/ecowitt/ @pvizeli /homeassistant/components/edl21/ @mtdcr diff --git a/homeassistant/components/ecovacs/manifest.json b/homeassistant/components/ecovacs/manifest.json index 1712cea1578..3ac277217b8 100644 --- a/homeassistant/components/ecovacs/manifest.json +++ b/homeassistant/components/ecovacs/manifest.json @@ -2,8 +2,8 @@ "domain": "ecovacs", "name": "Ecovacs", "documentation": "https://www.home-assistant.io/integrations/ecovacs", - "requirements": ["sucks==0.9.4"], - "codeowners": ["@OverloadUT"], + "requirements": ["py-sucks==0.9.8"], + "codeowners": ["@OverloadUT", "@mib1185"], "iot_class": "cloud_push", "loggers": ["sleekxmppfs", "sucks"] } diff --git a/requirements_all.txt b/requirements_all.txt index 0a10f451e5c..06169cef6d0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1351,6 +1351,9 @@ py-nightscout==1.2.2 # homeassistant.components.schluter py-schluter==0.1.7 +# homeassistant.components.ecovacs +py-sucks==0.9.8 + # homeassistant.components.synology_dsm py-synologydsm-api==1.0.8 @@ -2311,9 +2314,6 @@ stringcase==1.2.0 # homeassistant.components.subaru subarulink==0.5.0 -# homeassistant.components.ecovacs -sucks==0.9.4 - # homeassistant.components.solarlog sunwatcher==0.2.1 From b3596fdea1b330633c3096c9d252bfa6426e3b03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A5le=20Stor=C3=B8=20Hauknes?= Date: Sun, 4 Sep 2022 19:32:57 +0200 Subject: [PATCH 082/955] Mill 3. gen add support for PRECISION_HALVES (#73592) * Add support for PRECISION_HALVES for Mill 3. generation heaters * Add support for precision halves for Mill local API * Make sure to cast to float for local api * Support both float for gen 3 heaters and int for gen < 3 heaters when using cloud api * Mill attribute bugfix * Add support for PRECISION_HALVES for Mill 3. generation heaters * Add support for precision halves for Mill local API * Make sure to cast to float for local api * Support both float for gen 3 heaters and int for gen < 3 heaters when using cloud api * Mill attribute bugfix * Revert PRECISION_HALVES for Mill cloud integration * Remove unused code * Revert to casting to int for mill cloud integration * Remove unused code --- homeassistant/components/mill/climate.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mill/climate.py b/homeassistant/components/mill/climate.py index e7cd297bd59..44c7f980274 100644 --- a/homeassistant/components/mill/climate.py +++ b/homeassistant/components/mill/climate.py @@ -15,6 +15,7 @@ from homeassistant.const import ( ATTR_TEMPERATURE, CONF_IP_ADDRESS, CONF_USERNAME, + PRECISION_HALVES, PRECISION_WHOLE, TEMP_CELSIUS, ) @@ -200,7 +201,7 @@ class LocalMillHeater(CoordinatorEntity, ClimateEntity): _attr_max_temp = MAX_TEMP _attr_min_temp = MIN_TEMP _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE - _attr_target_temperature_step = PRECISION_WHOLE + _attr_target_temperature_step = PRECISION_HALVES _attr_temperature_unit = TEMP_CELSIUS def __init__(self, coordinator): @@ -225,7 +226,7 @@ class LocalMillHeater(CoordinatorEntity, ClimateEntity): if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: return await self.coordinator.mill_data_connection.set_target_temperature( - int(temperature) + float(temperature) ) await self.coordinator.async_request_refresh() From 03d804123a039678476531d099f49d369d4ba318 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 4 Sep 2022 21:42:08 +0200 Subject: [PATCH 083/955] Sensibo clean code (#74437) --- homeassistant/components/sensibo/__init__.py | 2 +- .../components/sensibo/binary_sensor.py | 18 +- homeassistant/components/sensibo/button.py | 56 ++++-- homeassistant/components/sensibo/climate.py | 172 +++++++++++++----- .../components/sensibo/config_flow.py | 4 +- .../components/sensibo/coordinator.py | 8 + .../components/sensibo/diagnostics.py | 2 +- homeassistant/components/sensibo/entity.py | 98 ++++------ homeassistant/components/sensibo/number.py | 33 +++- homeassistant/components/sensibo/select.py | 67 ++++--- homeassistant/components/sensibo/sensor.py | 21 +-- homeassistant/components/sensibo/strings.json | 6 + homeassistant/components/sensibo/switch.py | 140 +++++++------- .../components/sensibo/translations/en.json | 6 + homeassistant/components/sensibo/util.py | 2 +- tests/components/sensibo/test_button.py | 14 +- tests/components/sensibo/test_climate.py | 33 +++- tests/components/sensibo/test_entity.py | 35 +--- tests/components/sensibo/test_switch.py | 27 +-- 19 files changed, 425 insertions(+), 319 deletions(-) diff --git a/homeassistant/components/sensibo/__init__.py b/homeassistant/components/sensibo/__init__.py index ee3cba7d001..29730216899 100644 --- a/homeassistant/components/sensibo/__init__.py +++ b/homeassistant/components/sensibo/__init__.py @@ -1,4 +1,4 @@ -"""The sensibo component.""" +"""The Sensibo component.""" from __future__ import annotations from pysensibo.exceptions import AuthenticationError diff --git a/homeassistant/components/sensibo/binary_sensor.py b/homeassistant/components/sensibo/binary_sensor.py index 3a61570701d..88800a9b2a8 100644 --- a/homeassistant/components/sensibo/binary_sensor.py +++ b/homeassistant/components/sensibo/binary_sensor.py @@ -65,7 +65,6 @@ MOTION_SENSOR_TYPES: tuple[SensiboMotionBinarySensorEntityDescription, ...] = ( device_class=BinarySensorDeviceClass.CONNECTIVITY, entity_category=EntityCategory.DIAGNOSTIC, name="Alive", - icon="mdi:wifi", value_fn=lambda data: data.alive, ), SensiboMotionBinarySensorEntityDescription( @@ -104,7 +103,6 @@ PURE_SENSOR_TYPES: tuple[SensiboDeviceBinarySensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, device_class=BinarySensorDeviceClass.CONNECTIVITY, name="Pure Boost linked with AC", - icon="mdi:connection", value_fn=lambda data: data.pure_ac_integration, ), SensiboDeviceBinarySensorEntityDescription( @@ -112,7 +110,6 @@ PURE_SENSOR_TYPES: tuple[SensiboDeviceBinarySensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, device_class=BinarySensorDeviceClass.CONNECTIVITY, name="Pure Boost linked with presence", - icon="mdi:connection", value_fn=lambda data: data.pure_geo_integration, ), SensiboDeviceBinarySensorEntityDescription( @@ -120,7 +117,6 @@ PURE_SENSOR_TYPES: tuple[SensiboDeviceBinarySensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, device_class=BinarySensorDeviceClass.CONNECTIVITY, name="Pure Boost linked with indoor air quality", - icon="mdi:connection", value_fn=lambda data: data.pure_measure_integration, ), SensiboDeviceBinarySensorEntityDescription( @@ -128,12 +124,13 @@ PURE_SENSOR_TYPES: tuple[SensiboDeviceBinarySensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, device_class=BinarySensorDeviceClass.CONNECTIVITY, name="Pure Boost linked with outdoor air quality", - icon="mdi:connection", value_fn=lambda data: data.pure_prime_integration, ), FILTER_CLEAN_REQUIRED_DESCRIPTION, ) +DESCRIPTION_BY_MODELS = {"pure": PURE_SENSOR_TYPES} + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback @@ -161,15 +158,10 @@ async def async_setup_entry( ) entities.extend( SensiboDeviceSensor(coordinator, device_id, description) - for description in PURE_SENSOR_TYPES for device_id, device_data in coordinator.data.parsed.items() - if device_data.model == "pure" - ) - entities.extend( - SensiboDeviceSensor(coordinator, device_id, description) - for description in DEVICE_SENSOR_TYPES - for device_id, device_data in coordinator.data.parsed.items() - if device_data.model != "pure" + for description in DESCRIPTION_BY_MODELS.get( + device_data.model, DEVICE_SENSOR_TYPES + ) ) async_add_entities(entities) diff --git a/homeassistant/components/sensibo/button.py b/homeassistant/components/sensibo/button.py index ad8a525aebb..b91bddaf882 100644 --- a/homeassistant/components/sensibo/button.py +++ b/homeassistant/components/sensibo/button.py @@ -1,24 +1,44 @@ """Button platform for Sensibo integration.""" from __future__ import annotations +from dataclasses import dataclass +from typing import Any + +from pysensibo.model import SensiboDevice + from homeassistant.components.button import ButtonEntity, ButtonEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN from .coordinator import SensiboDataUpdateCoordinator -from .entity import SensiboDeviceBaseEntity +from .entity import SensiboDeviceBaseEntity, async_handle_api_call PARALLEL_UPDATES = 0 -DEVICE_BUTTON_TYPES: ButtonEntityDescription = ButtonEntityDescription( + +@dataclass +class SensiboEntityDescriptionMixin: + """Mixin values for Sensibo entities.""" + + data_key: str + + +@dataclass +class SensiboButtonEntityDescription( + ButtonEntityDescription, SensiboEntityDescriptionMixin +): + """Class describing Sensibo Button entities.""" + + +DEVICE_BUTTON_TYPES = SensiboButtonEntityDescription( key="reset_filter", name="Reset filter", icon="mdi:air-filter", entity_category=EntityCategory.CONFIG, + data_key="filter_clean", ) @@ -29,26 +49,22 @@ async def async_setup_entry( coordinator: SensiboDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - entities: list[SensiboDeviceButton] = [] - - entities.extend( + async_add_entities( SensiboDeviceButton(coordinator, device_id, DEVICE_BUTTON_TYPES) for device_id, device_data in coordinator.data.parsed.items() ) - async_add_entities(entities) - class SensiboDeviceButton(SensiboDeviceBaseEntity, ButtonEntity): """Representation of a Sensibo Device Binary Sensor.""" - entity_description: ButtonEntityDescription + entity_description: SensiboButtonEntityDescription def __init__( self, coordinator: SensiboDataUpdateCoordinator, device_id: str, - entity_description: ButtonEntityDescription, + entity_description: SensiboButtonEntityDescription, ) -> None: """Initiate Sensibo Device Button.""" super().__init__( @@ -60,8 +76,18 @@ class SensiboDeviceButton(SensiboDeviceBaseEntity, ButtonEntity): async def async_press(self) -> None: """Press the button.""" - result = await self.async_send_command("reset_filter") - if result["status"] == "success": - await self.coordinator.async_request_refresh() - return - raise HomeAssistantError(f"Could not set calibration for device {self.name}") + await self.async_send_api_call( + device_data=self.device_data, + key=self.entity_description.data_key, + value=False, + ) + + @async_handle_api_call + async def async_send_api_call( + self, device_data: SensiboDevice, key: Any, value: Any + ) -> bool: + """Make service call to api.""" + result = await self._client.async_reset_filter( + self._device_id, + ) + return bool(result.get("status") == "success") diff --git a/homeassistant/components/sensibo/climate.py b/homeassistant/components/sensibo/climate.py index 25ac73bfd4f..eda60458d4f 100644 --- a/homeassistant/components/sensibo/climate.py +++ b/homeassistant/components/sensibo/climate.py @@ -4,6 +4,7 @@ from __future__ import annotations from bisect import bisect_left from typing import TYPE_CHECKING, Any +from pysensibo.model import SensiboDevice import voluptuous as vol from homeassistant.components.climate import ClimateEntity @@ -24,7 +25,7 @@ from homeassistant.util.temperature import convert as convert_temperature from .const import DOMAIN from .coordinator import SensiboDataUpdateCoordinator -from .entity import SensiboDeviceBaseEntity +from .entity import SensiboDeviceBaseEntity, async_handle_api_call SERVICE_ASSUME_STATE = "assume_state" SERVICE_ENABLE_TIMER = "enable_timer" @@ -123,7 +124,7 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity): def __init__( self, coordinator: SensiboDataUpdateCoordinator, device_id: str ) -> None: - """Initiate SensiboClimate.""" + """Initiate Sensibo Climate.""" super().__init__(coordinator, device_id) self._attr_unique_id = device_id self._attr_temperature_unit = ( @@ -173,6 +174,11 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity): ) return None + @property + def temperature_unit(self) -> str: + """Return temperature unit.""" + return TEMP_CELSIUS if self.device_data.temp_unit == "C" else TEMP_FAHRENHEIT + @property def target_temperature(self) -> float | None: """Return the temperature we try to reach.""" @@ -242,69 +248,99 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity): return new_temp = _find_valid_target_temp(temperature, self.device_data.temp_list) - await self._async_set_ac_state_property("targetTemperature", new_temp) + await self.async_send_api_call( + device_data=self.device_data, + key=AC_STATE_TO_DATA["targetTemperature"], + value=new_temp, + name="targetTemperature", + assumed_state=False, + ) async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new target fan mode.""" if "fanLevel" not in self.device_data.active_features: raise HomeAssistantError("Current mode doesn't support setting Fanlevel") - await self._async_set_ac_state_property("fanLevel", fan_mode) + await self.async_send_api_call( + device_data=self.device_data, + key=AC_STATE_TO_DATA["fanLevel"], + value=fan_mode, + name="fanLevel", + assumed_state=False, + ) async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new target operation mode.""" if hvac_mode == HVACMode.OFF: - await self._async_set_ac_state_property("on", False) + await self.async_send_api_call( + device_data=self.device_data, + key=AC_STATE_TO_DATA["on"], + value=False, + name="on", + assumed_state=False, + ) return # Turn on if not currently on. if not self.device_data.device_on: - await self._async_set_ac_state_property("on", True) + await self.async_send_api_call( + device_data=self.device_data, + key=AC_STATE_TO_DATA["on"], + value=True, + name="on", + assumed_state=False, + ) - await self._async_set_ac_state_property("mode", HA_TO_SENSIBO[hvac_mode]) - await self.coordinator.async_request_refresh() + await self.async_send_api_call( + device_data=self.device_data, + key=AC_STATE_TO_DATA["mode"], + value=HA_TO_SENSIBO[hvac_mode], + name="mode", + assumed_state=False, + ) async def async_set_swing_mode(self, swing_mode: str) -> None: """Set new target swing operation.""" if "swing" not in self.device_data.active_features: raise HomeAssistantError("Current mode doesn't support setting Swing") - await self._async_set_ac_state_property("swing", swing_mode) + await self.async_send_api_call( + device_data=self.device_data, + key=AC_STATE_TO_DATA["swing"], + value=swing_mode, + name="swing", + assumed_state=False, + ) async def async_turn_on(self) -> None: """Turn Sensibo unit on.""" - await self._async_set_ac_state_property("on", True) + await self.async_send_api_call( + device_data=self.device_data, + key=AC_STATE_TO_DATA["on"], + value=True, + name="on", + assumed_state=False, + ) async def async_turn_off(self) -> None: """Turn Sensibo unit on.""" - await self._async_set_ac_state_property("on", False) - - async def _async_set_ac_state_property( - self, name: str, value: str | int | bool, assumed_state: bool = False - ) -> None: - """Set AC state.""" - params = { - "name": name, - "value": value, - "ac_states": self.device_data.ac_states, - "assumed_state": assumed_state, - } - result = await self.async_send_command("set_ac_state", params) - - if result["result"]["status"] == "Success": - setattr(self.device_data, AC_STATE_TO_DATA[name], value) - self.async_write_ha_state() - return - - failure = result["result"]["failureReason"] - raise HomeAssistantError( - f"Could not set state for device {self.name} due to reason {failure}" + await self.async_send_api_call( + device_data=self.device_data, + key=AC_STATE_TO_DATA["on"], + value=False, + name="on", + assumed_state=False, ) async def async_assume_state(self, state: str) -> None: """Sync state with api.""" - await self._async_set_ac_state_property("on", state != HVACMode.OFF, True) - await self.coordinator.async_refresh() + await self.async_send_api_call( + device_data=self.device_data, + key=AC_STATE_TO_DATA["on"], + value=state != HVACMode.OFF, + name="on", + assumed_state=True, + ) async def async_enable_timer(self, minutes: int) -> None: """Enable the timer.""" @@ -313,11 +349,13 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity): "minutesFromNow": minutes, "acState": {**self.device_data.ac_states, "on": new_state}, } - result = await self.async_send_command("set_timer", params) - - if result["status"] == "success": - return await self.coordinator.async_request_refresh() - raise HomeAssistantError(f"Could not enable timer for device {self.name}") + await self.api_call_custom_service_timer( + device_data=self.device_data, + key="timer_on", + value=True, + command="set_timer", + data=params, + ) async def async_enable_pure_boost( self, @@ -343,5 +381,57 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity): if outdoor_integration is not None: params["primeIntegration"] = outdoor_integration - await self.async_send_command("set_pure_boost", params) - await self.coordinator.async_refresh() + await self.api_call_custom_service_pure_boost( + device_data=self.device_data, + key="pure_boost_enabled", + value=True, + command="set_pure_boost", + data=params, + ) + + @async_handle_api_call + async def async_send_api_call( + self, + device_data: SensiboDevice, + key: Any, + value: Any, + name: str, + assumed_state: bool = False, + ) -> bool: + """Make service call to api.""" + result = await self._client.async_set_ac_state_property( + self._device_id, + name, + value, + self.device_data.ac_states, + assumed_state, + ) + return bool(result.get("result", {}).get("status") == "Success") + + @async_handle_api_call + async def api_call_custom_service_timer( + self, + device_data: SensiboDevice, + key: Any, + value: Any, + command: str, + data: dict, + ) -> bool: + """Make service call to api.""" + result = {} + result = await self._client.async_set_timer(self._device_id, data) + return bool(result.get("status") == "success") + + @async_handle_api_call + async def api_call_custom_service_pure_boost( + self, + device_data: SensiboDevice, + key: Any, + value: Any, + command: str, + data: dict, + ) -> bool: + """Make service call to api.""" + result = {} + result = await self._client.async_set_pureboost(self._device_id, data) + return bool(result.get("status") == "success") diff --git a/homeassistant/components/sensibo/config_flow.py b/homeassistant/components/sensibo/config_flow.py index c7aaa30b3db..f3b413071e8 100644 --- a/homeassistant/components/sensibo/config_flow.py +++ b/homeassistant/components/sensibo/config_flow.py @@ -10,14 +10,14 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_API_KEY from homeassistant.data_entry_flow import FlowResult -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.selector import TextSelector from .const import DEFAULT_NAME, DOMAIN from .util import NoDevicesError, NoUsernameError, async_validate_api DATA_SCHEMA = vol.Schema( { - vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_API_KEY): TextSelector(), } ) diff --git a/homeassistant/components/sensibo/coordinator.py b/homeassistant/components/sensibo/coordinator.py index a0321bf611e..6c5a993e34a 100644 --- a/homeassistant/components/sensibo/coordinator.py +++ b/homeassistant/components/sensibo/coordinator.py @@ -12,10 +12,13 @@ from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, LOGGER, TIMEOUT +REQUEST_REFRESH_DELAY = 0.35 + class SensiboDataUpdateCoordinator(DataUpdateCoordinator): """A Sensibo Data Update Coordinator.""" @@ -34,6 +37,11 @@ class SensiboDataUpdateCoordinator(DataUpdateCoordinator): LOGGER, name=DOMAIN, update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL), + # We don't want an immediate refresh since the device + # takes a moment to reflect the state change + request_refresh_debouncer=Debouncer( + hass, LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=False + ), ) async def _async_update_data(self) -> SensiboData: diff --git a/homeassistant/components/sensibo/diagnostics.py b/homeassistant/components/sensibo/diagnostics.py index e4a4672bf64..72029acc2f1 100644 --- a/homeassistant/components/sensibo/diagnostics.py +++ b/homeassistant/components/sensibo/diagnostics.py @@ -31,6 +31,6 @@ TO_REDACT = { async def async_get_config_entry_diagnostics( hass: HomeAssistant, entry: ConfigEntry ) -> dict[str, Any]: - """Return diagnostics for a config entry.""" + """Return diagnostics for Sensibo config entry.""" coordinator: SensiboDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] return async_redact_data(coordinator.data.raw, TO_REDACT) diff --git a/homeassistant/components/sensibo/entity.py b/homeassistant/components/sensibo/entity.py index 41d8b8b5070..56a7c820739 100644 --- a/homeassistant/components/sensibo/entity.py +++ b/homeassistant/components/sensibo/entity.py @@ -1,10 +1,12 @@ """Base entity for Sensibo integration.""" from __future__ import annotations -from typing import TYPE_CHECKING, Any +from collections.abc import Callable, Coroutine +from typing import TYPE_CHECKING, Any, TypeVar import async_timeout from pysensibo.model import MotionSensor, SensiboDevice +from typing_extensions import Concatenate, ParamSpec from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC @@ -14,9 +16,39 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN, LOGGER, SENSIBO_ERRORS, TIMEOUT from .coordinator import SensiboDataUpdateCoordinator +_T = TypeVar("_T", bound="SensiboDeviceBaseEntity") +_P = ParamSpec("_P") + + +def async_handle_api_call( + function: Callable[Concatenate[_T, _P], Coroutine[Any, Any, Any]] +) -> Callable[Concatenate[_T, _P], Coroutine[Any, Any, Any]]: + """Decorate api calls.""" + + async def wrap_api_call(*args: Any, **kwargs: Any) -> None: + """Wrap services for api calls.""" + res: bool = False + try: + async with async_timeout.timeout(TIMEOUT): + res = await function(*args, **kwargs) + except SENSIBO_ERRORS as err: + raise HomeAssistantError from err + + LOGGER.debug("Result %s for entity %s with arguments %s", res, args[0], kwargs) + entity: SensiboDeviceBaseEntity = args[0] + if res is not True: + raise HomeAssistantError(f"Could not execute service for {entity.name}") + if kwargs.get("key") is not None and kwargs.get("value") is not None: + setattr(entity.device_data, kwargs["key"], kwargs["value"]) + LOGGER.debug("Debug check key %s is now %s", kwargs["key"], kwargs["value"]) + entity.async_write_ha_state() + await entity.coordinator.async_request_refresh() + + return wrap_api_call + class SensiboBaseEntity(CoordinatorEntity[SensiboDataUpdateCoordinator]): - """Representation of a Sensibo entity.""" + """Representation of a Sensibo Base Entity.""" def __init__( self, @@ -35,7 +67,7 @@ class SensiboBaseEntity(CoordinatorEntity[SensiboDataUpdateCoordinator]): class SensiboDeviceBaseEntity(SensiboBaseEntity): - """Representation of a Sensibo device.""" + """Representation of a Sensibo Device.""" _attr_has_entity_name = True @@ -44,7 +76,7 @@ class SensiboDeviceBaseEntity(SensiboBaseEntity): coordinator: SensiboDataUpdateCoordinator, device_id: str, ) -> None: - """Initiate Sensibo Number.""" + """Initiate Sensibo Device.""" super().__init__(coordinator, device_id) self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, self.device_data.id)}, @@ -58,63 +90,9 @@ class SensiboDeviceBaseEntity(SensiboBaseEntity): suggested_area=self.device_data.name, ) - async def async_send_command( - self, command: str, params: dict[str, Any] | None = None - ) -> dict[str, Any]: - """Send command to Sensibo api.""" - try: - async with async_timeout.timeout(TIMEOUT): - result = await self.async_send_api_call(command, params) - except SENSIBO_ERRORS as err: - raise HomeAssistantError( - f"Failed to send command {command} for device {self.name} to Sensibo servers: {err}" - ) from err - - LOGGER.debug("Result: %s", result) - return result - - async def async_send_api_call( - self, command: str, params: dict[str, Any] | None = None - ) -> dict[str, Any]: - """Send api call.""" - result: dict[str, Any] = {"status": None} - if command == "set_calibration": - if TYPE_CHECKING: - assert params is not None - result = await self._client.async_set_calibration( - self._device_id, - params["data"], - ) - if command == "set_ac_state": - if TYPE_CHECKING: - assert params is not None - result = await self._client.async_set_ac_state_property( - self._device_id, - params["name"], - params["value"], - params["ac_states"], - params["assumed_state"], - ) - if command == "set_timer": - if TYPE_CHECKING: - assert params is not None - result = await self._client.async_set_timer(self._device_id, params) - if command == "del_timer": - result = await self._client.async_del_timer(self._device_id) - if command == "set_pure_boost": - if TYPE_CHECKING: - assert params is not None - result = await self._client.async_set_pureboost( - self._device_id, - params, - ) - if command == "reset_filter": - result = await self._client.async_reset_filter(self._device_id) - return result - class SensiboMotionBaseEntity(SensiboBaseEntity): - """Representation of a Sensibo motion entity.""" + """Representation of a Sensibo Motion Entity.""" _attr_has_entity_name = True @@ -141,7 +119,7 @@ class SensiboMotionBaseEntity(SensiboBaseEntity): @property def sensor_data(self) -> MotionSensor | None: - """Return data for device.""" + """Return data for Motion Sensor.""" if TYPE_CHECKING: assert self.device_data.motion_sensors return self.device_data.motion_sensors[self._sensor_id] diff --git a/homeassistant/components/sensibo/number.py b/homeassistant/components/sensibo/number.py index bcad658c700..6550d7382d3 100644 --- a/homeassistant/components/sensibo/number.py +++ b/homeassistant/components/sensibo/number.py @@ -1,18 +1,21 @@ """Number platform for Sensibo integration.""" from __future__ import annotations +from collections.abc import Callable from dataclasses import dataclass +from typing import Any + +from pysensibo.model import SensiboDevice from homeassistant.components.number import NumberEntity, NumberEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN from .coordinator import SensiboDataUpdateCoordinator -from .entity import SensiboDeviceBaseEntity +from .entity import SensiboDeviceBaseEntity, async_handle_api_call PARALLEL_UPDATES = 0 @@ -22,6 +25,7 @@ class SensiboEntityDescriptionMixin: """Mixin values for Sensibo entities.""" remote_key: str + value_fn: Callable[[SensiboDevice], float | None] @dataclass @@ -42,6 +46,7 @@ DEVICE_NUMBER_TYPES = ( native_min_value=-10, native_max_value=10, native_step=0.1, + value_fn=lambda data: data.calibration_temp, ), SensiboNumberEntityDescription( key="calibration_hum", @@ -53,6 +58,7 @@ DEVICE_NUMBER_TYPES = ( native_min_value=-10, native_max_value=10, native_step=0.1, + value_fn=lambda data: data.calibration_hum, ), ) @@ -90,15 +96,22 @@ class SensiboNumber(SensiboDeviceBaseEntity, NumberEntity): @property def native_value(self) -> float | None: """Return the value from coordinator data.""" - value: float | None = getattr(self.device_data, self.entity_description.key) - return value + return self.entity_description.value_fn(self.device_data) async def async_set_native_value(self, value: float) -> None: """Set value for calibration.""" + await self.async_send_api_call( + device_data=self.device_data, key=self.entity_description.key, value=value + ) + + @async_handle_api_call + async def async_send_api_call( + self, device_data: SensiboDevice, key: Any, value: Any + ) -> bool: + """Make service call to api.""" data = {self.entity_description.remote_key: value} - result = await self.async_send_command("set_calibration", {"data": data}) - if result["status"] == "success": - setattr(self.device_data, self.entity_description.key, value) - self.async_write_ha_state() - return - raise HomeAssistantError(f"Could not set calibration for device {self.name}") + result = await self._client.async_set_calibration( + self._device_id, + data, + ) + return bool(result.get("status") == "success") diff --git a/homeassistant/components/sensibo/select.py b/homeassistant/components/sensibo/select.py index a8cfc527704..ab95377d016 100644 --- a/homeassistant/components/sensibo/select.py +++ b/homeassistant/components/sensibo/select.py @@ -1,7 +1,11 @@ -"""Number platform for Sensibo integration.""" +"""Select platform for Sensibo integration.""" from __future__ import annotations +from collections.abc import Callable from dataclasses import dataclass +from typing import TYPE_CHECKING, Any + +from pysensibo.model import SensiboDevice from homeassistant.components.select import SelectEntity, SelectEntityDescription from homeassistant.config_entries import ConfigEntry @@ -11,7 +15,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN from .coordinator import SensiboDataUpdateCoordinator -from .entity import SensiboDeviceBaseEntity +from .entity import SensiboDeviceBaseEntity, async_handle_api_call PARALLEL_UPDATES = 0 @@ -20,31 +24,34 @@ PARALLEL_UPDATES = 0 class SensiboSelectDescriptionMixin: """Mixin values for Sensibo entities.""" - remote_key: str - remote_options: str + data_key: str + value_fn: Callable[[SensiboDevice], str | None] + options_fn: Callable[[SensiboDevice], list[str] | None] @dataclass class SensiboSelectEntityDescription( SelectEntityDescription, SensiboSelectDescriptionMixin ): - """Class describing Sensibo Number entities.""" + """Class describing Sensibo Select entities.""" DEVICE_SELECT_TYPES = ( SensiboSelectEntityDescription( key="horizontalSwing", - remote_key="horizontal_swing_mode", - remote_options="horizontal_swing_modes", + data_key="horizontal_swing_mode", name="Horizontal swing", icon="mdi:air-conditioner", + value_fn=lambda data: data.horizontal_swing_mode, + options_fn=lambda data: data.horizontal_swing_modes, ), SensiboSelectEntityDescription( key="light", - remote_key="light_mode", - remote_options="light_modes", + data_key="light_mode", name="Light", icon="mdi:flashlight", + value_fn=lambda data: data.light_mode, + options_fn=lambda data: data.light_modes, ), ) @@ -83,15 +90,15 @@ class SensiboSelect(SensiboDeviceBaseEntity, SelectEntity): @property def current_option(self) -> str | None: """Return the current selected option.""" - option: str | None = getattr( - self.device_data, self.entity_description.remote_key - ) - return option + return self.entity_description.value_fn(self.device_data) @property def options(self) -> list[str]: """Return possible options.""" - return getattr(self.device_data, self.entity_description.remote_options) or [] + options = self.entity_description.options_fn(self.device_data) + if TYPE_CHECKING: + assert options is not None + return options async def async_select_option(self, option: str) -> None: """Set state to the selected option.""" @@ -100,20 +107,28 @@ class SensiboSelect(SensiboDeviceBaseEntity, SelectEntity): f"Current mode {self.device_data.hvac_mode} doesn't support setting {self.entity_description.name}" ) - params = { + await self.async_send_api_call( + device_data=self.device_data, + key=self.entity_description.data_key, + value=option, + ) + + @async_handle_api_call + async def async_send_api_call( + self, device_data: SensiboDevice, key: Any, value: Any + ) -> bool: + """Make service call to api.""" + data = { "name": self.entity_description.key, - "value": option, + "value": value, "ac_states": self.device_data.ac_states, "assumed_state": False, } - result = await self.async_send_command("set_ac_state", params) - - if result["result"]["status"] == "Success": - setattr(self.device_data, self.entity_description.remote_key, option) - self.async_write_ha_state() - return - - failure = result["result"]["failureReason"] - raise HomeAssistantError( - f"Could not set state for device {self.name} due to reason {failure}" + result = await self._client.async_set_ac_state_property( + self._device_id, + data["name"], + data["value"], + data["ac_states"], + data["assumed_state"], ) + return bool(result.get("result", {}).get("status") == "Success") diff --git a/homeassistant/components/sensibo/sensor.py b/homeassistant/components/sensibo/sensor.py index f21366c7aa6..e9d9447ad81 100644 --- a/homeassistant/components/sensibo/sensor.py +++ b/homeassistant/components/sensibo/sensor.py @@ -63,7 +63,7 @@ class SensiboMotionSensorEntityDescription( class SensiboDeviceSensorEntityDescription( SensorEntityDescription, DeviceBaseEntityDescriptionMixin ): - """Describes Sensibo Motion sensor entity.""" + """Describes Sensibo Device sensor entity.""" FILTER_LAST_RESET_DESCRIPTION = SensiboDeviceSensorEntityDescription( @@ -178,6 +178,8 @@ AIRQ_SENSOR_TYPES: tuple[SensiboDeviceSensorEntityDescription, ...] = ( ), ) +DESCRIPTION_BY_MODELS = {"pure": PURE_SENSOR_TYPES, "airq": AIRQ_SENSOR_TYPES} + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback @@ -200,20 +202,9 @@ async def async_setup_entry( entities.extend( SensiboDeviceSensor(coordinator, device_id, description) for device_id, device_data in coordinator.data.parsed.items() - for description in PURE_SENSOR_TYPES - if device_data.model == "pure" - ) - entities.extend( - SensiboDeviceSensor(coordinator, device_id, description) - for device_id, device_data in coordinator.data.parsed.items() - for description in DEVICE_SENSOR_TYPES - if device_data.model != "pure" - ) - entities.extend( - SensiboDeviceSensor(coordinator, device_id, description) - for device_id, device_data in coordinator.data.parsed.items() - for description in AIRQ_SENSOR_TYPES - if device_data.model == "airq" + for description in DESCRIPTION_BY_MODELS.get( + device_data.model, DEVICE_SENSOR_TYPES + ) ) async_add_entities(entities) diff --git a/homeassistant/components/sensibo/strings.json b/homeassistant/components/sensibo/strings.json index dc27644b3e1..19c6a7e594a 100644 --- a/homeassistant/components/sensibo/strings.json +++ b/homeassistant/components/sensibo/strings.json @@ -15,11 +15,17 @@ "user": { "data": { "api_key": "[%key:common::config_flow::data::api_key%]" + }, + "data_description": { + "api_key": "Follow the documentation to get your api key." } }, "reauth_confirm": { "data": { "api_key": "[%key:common::config_flow::data::api_key%]" + }, + "data_description": { + "api_key": "Follow the documentation to get a new api key." } } } diff --git a/homeassistant/components/sensibo/switch.py b/homeassistant/components/sensibo/switch.py index 14cfaac73ae..c06bf4d1ac6 100644 --- a/homeassistant/components/sensibo/switch.py +++ b/homeassistant/components/sensibo/switch.py @@ -14,25 +14,24 @@ from homeassistant.components.switch import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN from .coordinator import SensiboDataUpdateCoordinator -from .entity import SensiboDeviceBaseEntity +from .entity import SensiboDeviceBaseEntity, async_handle_api_call PARALLEL_UPDATES = 0 @dataclass class DeviceBaseEntityDescriptionMixin: - """Mixin for required Sensibo base description keys.""" + """Mixin for required Sensibo Device description keys.""" value_fn: Callable[[SensiboDevice], bool | None] extra_fn: Callable[[SensiboDevice], dict[str, str | bool | None]] | None command_on: str command_off: str - remote_key: str + data_key: str @dataclass @@ -52,7 +51,7 @@ DEVICE_SWITCH_TYPES: tuple[SensiboDeviceSwitchEntityDescription, ...] = ( extra_fn=lambda data: {"id": data.timer_id, "turn_on": data.timer_state_on}, command_on="set_timer", command_off="del_timer", - remote_key="timer_on", + data_key="timer_on", ), ) @@ -65,56 +64,27 @@ PURE_SWITCH_TYPES: tuple[SensiboDeviceSwitchEntityDescription, ...] = ( extra_fn=None, command_on="set_pure_boost", command_off="set_pure_boost", - remote_key="pure_boost_enabled", + data_key="pure_boost_enabled", ), ) - -def build_params(command: str, device_data: SensiboDevice) -> dict[str, Any] | None: - """Build params for turning on switch.""" - if command == "set_timer": - new_state = bool(device_data.ac_states["on"] is False) - params = { - "minutesFromNow": 60, - "acState": {**device_data.ac_states, "on": new_state}, - } - return params - if command == "set_pure_boost": - new_state = bool(device_data.pure_boost_enabled is False) - params = {"enabled": new_state} - if device_data.pure_measure_integration is None: - params["sensitivity"] = "N" - params["measurementsIntegration"] = True - params["acIntegration"] = False - params["geoIntegration"] = False - params["primeIntegration"] = False - return params - return None +DESCRIPTION_BY_MODELS = {"pure": PURE_SWITCH_TYPES} async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: - """Set up Sensibo binary sensor platform.""" + """Set up Sensibo Switch platform.""" coordinator: SensiboDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - entities: list[SensiboDeviceSwitch] = [] - - entities.extend( + async_add_entities( SensiboDeviceSwitch(coordinator, device_id, description) - for description in DEVICE_SWITCH_TYPES for device_id, device_data in coordinator.data.parsed.items() - if device_data.model != "pure" + for description in DESCRIPTION_BY_MODELS.get( + device_data.model, DEVICE_SWITCH_TYPES + ) ) - entities.extend( - SensiboDeviceSwitch(coordinator, device_id, description) - for description in PURE_SWITCH_TYPES - for device_id, device_data in coordinator.data.parsed.items() - if device_data.model == "pure" - ) - - async_add_entities(entities) class SensiboDeviceSwitch(SensiboDeviceBaseEntity, SwitchEntity): @@ -143,33 +113,33 @@ class SensiboDeviceSwitch(SensiboDeviceBaseEntity, SwitchEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" - params = build_params(self.entity_description.command_on, self.device_data) - result = await self.async_send_command( - self.entity_description.command_on, params - ) - - if result["status"] == "success": - setattr(self.device_data, self.entity_description.remote_key, True) - self.async_write_ha_state() - return await self.coordinator.async_request_refresh() - raise HomeAssistantError( - f"Could not execute {self.entity_description.command_on} for device {self.name}" - ) + if self.entity_description.key == "timer_on_switch": + await self.async_turn_on_timer( + device_data=self.device_data, + key=self.entity_description.data_key, + value=True, + ) + if self.entity_description.key == "pure_boost_switch": + await self.async_turn_on_off_pure_boost( + device_data=self.device_data, + key=self.entity_description.data_key, + value=True, + ) async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" - params = build_params(self.entity_description.command_on, self.device_data) - result = await self.async_send_command( - self.entity_description.command_off, params - ) - - if result["status"] == "success": - setattr(self.device_data, self.entity_description.remote_key, False) - self.async_write_ha_state() - return await self.coordinator.async_request_refresh() - raise HomeAssistantError( - f"Could not execute {self.entity_description.command_off} for device {self.name}" - ) + if self.entity_description.key == "timer_on_switch": + await self.async_turn_off_timer( + device_data=self.device_data, + key=self.entity_description.data_key, + value=False, + ) + if self.entity_description.key == "pure_boost_switch": + await self.async_turn_on_off_pure_boost( + device_data=self.device_data, + key=self.entity_description.data_key, + value=False, + ) @property def extra_state_attributes(self) -> Mapping[str, Any] | None: @@ -177,3 +147,43 @@ class SensiboDeviceSwitch(SensiboDeviceBaseEntity, SwitchEntity): if self.entity_description.extra_fn: return self.entity_description.extra_fn(self.device_data) return None + + @async_handle_api_call + async def async_turn_on_timer( + self, device_data: SensiboDevice, key: Any, value: Any + ) -> bool: + """Make service call to api for setting timer.""" + result = {} + new_state = bool(device_data.ac_states["on"] is False) + data = { + "minutesFromNow": 60, + "acState": {**device_data.ac_states, "on": new_state}, + } + result = await self._client.async_set_timer(self._device_id, data) + return bool(result.get("status") == "success") + + @async_handle_api_call + async def async_turn_off_timer( + self, device_data: SensiboDevice, key: Any, value: Any + ) -> bool: + """Make service call to api for deleting timer.""" + result = {} + result = await self._client.async_del_timer(self._device_id) + return bool(result.get("status") == "success") + + @async_handle_api_call + async def async_turn_on_off_pure_boost( + self, device_data: SensiboDevice, key: Any, value: Any + ) -> bool: + """Make service call to api for setting Pure Boost.""" + result = {} + new_state = bool(device_data.pure_boost_enabled is False) + data: dict[str, Any] = {"enabled": new_state} + if device_data.pure_measure_integration is None: + data["sensitivity"] = "N" + data["measurementsIntegration"] = True + data["acIntegration"] = False + data["geoIntegration"] = False + data["primeIntegration"] = False + result = await self._client.async_set_pureboost(self._device_id, data) + return bool(result.get("status") == "success") diff --git a/homeassistant/components/sensibo/translations/en.json b/homeassistant/components/sensibo/translations/en.json index 7b7c8aab7f1..3f78e3be98d 100644 --- a/homeassistant/components/sensibo/translations/en.json +++ b/homeassistant/components/sensibo/translations/en.json @@ -15,11 +15,17 @@ "reauth_confirm": { "data": { "api_key": "API Key" + }, + "data_description": { + "api_key": "Follow the documentation to get a new api key." } }, "user": { "data": { "api_key": "API Key" + }, + "data_description": { + "api_key": "Follow the documentation to get your api key." } } } diff --git a/homeassistant/components/sensibo/util.py b/homeassistant/components/sensibo/util.py index 8a181cbe568..9070be3412a 100644 --- a/homeassistant/components/sensibo/util.py +++ b/homeassistant/components/sensibo/util.py @@ -12,7 +12,7 @@ from .const import LOGGER, SENSIBO_ERRORS, TIMEOUT async def async_validate_api(hass: HomeAssistant, api_key: str) -> str: - """Get data from API.""" + """Validate the api and return username.""" client = SensiboClient( api_key, session=async_get_clientsession(hass), diff --git a/tests/components/sensibo/test_button.py b/tests/components/sensibo/test_button.py index 66b7a1258b1..c0602525931 100644 --- a/tests/components/sensibo/test_button.py +++ b/tests/components/sensibo/test_button.py @@ -36,7 +36,9 @@ async def test_button( assert state_filter_clean.state is STATE_ON assert state_filter_last_reset.state == "2022-03-12T15:24:26+00:00" - freezer.move_to(datetime(2022, 6, 19, 20, 0, 0)) + today = datetime(datetime.now().year + 1, 6, 19, 20, 0, 0).replace(tzinfo=dt.UTC) + today_str = today.isoformat() + freezer.move_to(today) with patch( "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", @@ -53,13 +55,13 @@ async def test_button( }, blocking=True, ) - await hass.async_block_till_done() + await hass.async_block_till_done() monkeypatch.setattr(get_data.parsed["ABC999111"], "filter_clean", False) monkeypatch.setattr( get_data.parsed["ABC999111"], "filter_last_reset", - datetime(2022, 6, 19, 20, 0, 0, tzinfo=dt.UTC), + today, ) with patch( @@ -75,11 +77,9 @@ async def test_button( state_button = hass.states.get("button.hallway_reset_filter") state_filter_clean = hass.states.get("binary_sensor.hallway_filter_clean_required") state_filter_last_reset = hass.states.get("sensor.hallway_filter_last_reset") - assert ( - state_button.state == datetime(2022, 6, 19, 20, 0, 0, tzinfo=dt.UTC).isoformat() - ) + assert state_button.state == today_str assert state_filter_clean.state is STATE_OFF - assert state_filter_last_reset.state == "2022-06-19T20:00:00+00:00" + assert state_filter_last_reset.state == today_str async def test_button_failure( diff --git a/tests/components/sensibo/test_climate.py b/tests/components/sensibo/test_climate.py index e7a3c465f76..b66bbd14afb 100644 --- a/tests/components/sensibo/test_climate.py +++ b/tests/components/sensibo/test_climate.py @@ -115,6 +115,9 @@ async def test_climate_fan( assert state1.attributes["fan_mode"] == "high" with patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=get_data, + ), patch( "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", return_value={"result": {"status": "Success"}}, ): @@ -180,6 +183,9 @@ async def test_climate_swing( assert state1.attributes["swing_mode"] == "stopped" with patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=get_data, + ), patch( "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", return_value={"result": {"status": "Success"}}, ): @@ -189,7 +195,7 @@ async def test_climate_swing( {ATTR_ENTITY_ID: state1.entity_id, ATTR_SWING_MODE: "fixedTop"}, blocking=True, ) - await hass.async_block_till_done() + await hass.async_block_till_done() state2 = hass.states.get("climate.hallway") assert state2.attributes["swing_mode"] == "fixedTop" @@ -244,6 +250,9 @@ async def test_climate_temperatures( assert state1.attributes["temperature"] == 25 with patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=get_data, + ), patch( "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", return_value={"result": {"status": "Success"}}, ): @@ -259,6 +268,9 @@ async def test_climate_temperatures( assert state2.attributes["temperature"] == 20 with patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=get_data, + ), patch( "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", return_value={"result": {"status": "Success"}}, ): @@ -274,6 +286,9 @@ async def test_climate_temperatures( assert state2.attributes["temperature"] == 16 with patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=get_data, + ), patch( "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", return_value={"result": {"status": "Success"}}, ): @@ -289,6 +304,9 @@ async def test_climate_temperatures( assert state2.attributes["temperature"] == 19 with patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=get_data, + ), patch( "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", return_value={"result": {"status": "Success"}}, ): @@ -304,6 +322,9 @@ async def test_climate_temperatures( assert state2.attributes["temperature"] == 20 with patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=get_data, + ), patch( "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", return_value={"result": {"status": "Success"}}, ): @@ -481,7 +502,7 @@ async def test_climate_hvac_mode( {ATTR_ENTITY_ID: state1.entity_id, ATTR_HVAC_MODE: "off"}, blocking=True, ) - await hass.async_block_till_done() + await hass.async_block_till_done() state2 = hass.states.get("climate.hallway") assert state2.state == "off" @@ -540,6 +561,9 @@ async def test_climate_on_off( assert state1.state == "heat" with patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=get_data, + ), patch( "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", return_value={"result": {"status": "Success"}}, ): @@ -549,12 +573,15 @@ async def test_climate_on_off( {ATTR_ENTITY_ID: state1.entity_id}, blocking=True, ) - await hass.async_block_till_done() + await hass.async_block_till_done() state2 = hass.states.get("climate.hallway") assert state2.state == "off" with patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=get_data, + ), patch( "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", return_value={"result": {"status": "Success"}}, ): diff --git a/tests/components/sensibo/test_entity.py b/tests/components/sensibo/test_entity.py index bf512f9f220..818d9ddb924 100644 --- a/tests/components/sensibo/test_entity.py +++ b/tests/components/sensibo/test_entity.py @@ -1,7 +1,7 @@ """The test for the sensibo entity.""" from __future__ import annotations -from unittest.mock import AsyncMock, patch +from unittest.mock import patch from pysensibo.model import SensiboData import pytest @@ -11,11 +11,6 @@ from homeassistant.components.climate.const import ( DOMAIN as CLIMATE_DOMAIN, SERVICE_SET_FAN_MODE, ) -from homeassistant.components.number.const import ( - ATTR_VALUE, - DOMAIN as NUMBER_DOMAIN, - SERVICE_SET_VALUE, -) from homeassistant.components.sensibo.const import SENSIBO_ERRORS from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ENTITY_ID @@ -51,7 +46,7 @@ async def test_entity( @pytest.mark.parametrize("p_error", SENSIBO_ERRORS) -async def test_entity_send_command( +async def test_entity_failed_service_calls( hass: HomeAssistant, p_error: Exception, load_int: ConfigEntry, @@ -91,29 +86,3 @@ async def test_entity_send_command( state = hass.states.get("climate.hallway") assert state.attributes["fan_mode"] == "low" - - -async def test_entity_send_command_calibration( - hass: HomeAssistant, - entity_registry_enabled_by_default: AsyncMock, - load_int: ConfigEntry, - get_data: SensiboData, -) -> None: - """Test the Sensibo send command for calibration.""" - - state = hass.states.get("number.hallway_temperature_calibration") - assert state.state == "0.1" - - with patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_calibration", - return_value={"status": "success"}, - ): - await hass.services.async_call( - NUMBER_DOMAIN, - SERVICE_SET_VALUE, - {ATTR_ENTITY_ID: state.entity_id, ATTR_VALUE: 0.2}, - blocking=True, - ) - - state = hass.states.get("number.hallway_temperature_calibration") - assert state.state == "0.2" diff --git a/tests/components/sensibo/test_switch.py b/tests/components/sensibo/test_switch.py index 2a24751d70b..7b7b35cdd4e 100644 --- a/tests/components/sensibo/test_switch.py +++ b/tests/components/sensibo/test_switch.py @@ -8,7 +8,6 @@ from pysensibo.model import SensiboData import pytest from pytest import MonkeyPatch -from homeassistant.components.sensibo.switch import build_params from homeassistant.components.switch.const import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -134,6 +133,7 @@ async def test_switch_pure_boost( await hass.async_block_till_done() monkeypatch.setattr(get_data.parsed["AAZZAAZZ"], "pure_boost_enabled", True) + monkeypatch.setattr(get_data.parsed["AAZZAAZZ"], "pure_measure_integration", None) with patch( "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", @@ -223,28 +223,3 @@ async def test_switch_command_failure( }, blocking=True, ) - - -async def test_build_params( - hass: HomeAssistant, - load_int: ConfigEntry, - monkeypatch: MonkeyPatch, - get_data: SensiboData, -) -> None: - """Test the build params method.""" - - assert build_params("set_timer", get_data.parsed["ABC999111"]) == { - "minutesFromNow": 60, - "acState": {**get_data.parsed["ABC999111"].ac_states, "on": False}, - } - - monkeypatch.setattr(get_data.parsed["AAZZAAZZ"], "pure_measure_integration", None) - assert build_params("set_pure_boost", get_data.parsed["AAZZAAZZ"]) == { - "enabled": True, - "sensitivity": "N", - "measurementsIntegration": True, - "acIntegration": False, - "geoIntegration": False, - "primeIntegration": False, - } - assert build_params("incorrect_command", get_data.parsed["ABC999111"]) is None From f51b33034f05630dad37543702a13d34079830b2 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 4 Sep 2022 21:43:38 +0200 Subject: [PATCH 084/955] Bump yale_smart_alarm_client to 0.3.9 (#77797) --- homeassistant/components/yale_smart_alarm/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/yale_smart_alarm/manifest.json b/homeassistant/components/yale_smart_alarm/manifest.json index 0b1a5a94da0..865751d18e0 100644 --- a/homeassistant/components/yale_smart_alarm/manifest.json +++ b/homeassistant/components/yale_smart_alarm/manifest.json @@ -2,7 +2,7 @@ "domain": "yale_smart_alarm", "name": "Yale Smart Living", "documentation": "https://www.home-assistant.io/integrations/yale_smart_alarm", - "requirements": ["yalesmartalarmclient==0.3.8"], + "requirements": ["yalesmartalarmclient==0.3.9"], "codeowners": ["@gjohansson-ST"], "config_flow": true, "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 06169cef6d0..3ec6026ad8f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2542,7 +2542,7 @@ xmltodict==0.13.0 xs1-api-client==3.0.0 # homeassistant.components.yale_smart_alarm -yalesmartalarmclient==0.3.8 +yalesmartalarmclient==0.3.9 # homeassistant.components.yalexs_ble yalexs-ble==1.6.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0db5d027900..c1d2a7bf815 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1746,7 +1746,7 @@ xknx==1.0.2 xmltodict==0.13.0 # homeassistant.components.yale_smart_alarm -yalesmartalarmclient==0.3.8 +yalesmartalarmclient==0.3.9 # homeassistant.components.yalexs_ble yalexs-ble==1.6.4 From 14fc7c75959213cb038b59389b00be483f091498 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sun, 4 Sep 2022 22:41:18 +0200 Subject: [PATCH 085/955] Improve type hints in kodi media player (#77653) --- homeassistant/components/kodi/media_player.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index f4074678825..80331794114 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -21,6 +21,7 @@ from homeassistant.components.media_player import ( MediaPlayerEntityFeature, ) from homeassistant.components.media_player.browse_media import ( + BrowseMedia, async_process_play_media_url, ) from homeassistant.components.media_player.const import ( @@ -902,7 +903,9 @@ class KodiEntity(MediaPlayerEntity): return sorted(out, key=lambda out: out[1], reverse=True) - async def async_browse_media(self, media_content_type=None, media_content_id=None): + async def async_browse_media( + self, media_content_type: str | None = None, media_content_id: str | None = None + ) -> BrowseMedia: """Implement the websocket media browsing helper.""" is_internal = is_internal_request(self.hass) @@ -924,7 +927,7 @@ class KodiEntity(MediaPlayerEntity): if media_content_type in [None, "library"]: return await library_payload(self.hass) - if media_source.is_media_source_id(media_content_id): + if media_content_id and media_source.is_media_source_id(media_content_id): return await media_source.async_browse_media( self.hass, media_content_id, content_filter=media_source_content_filter ) From be07bb797617ab799a75e175f403329b3acf04c6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sun, 4 Sep 2022 22:41:46 +0200 Subject: [PATCH 086/955] Improve type hints in file and huawei_lte notify (#77648) --- homeassistant/components/file/notify.py | 6 ++++-- homeassistant/components/huawei_lte/notify.py | 5 +++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/file/notify.py b/homeassistant/components/file/notify.py index 4e9a2b39d1b..eb9a512b230 100644 --- a/homeassistant/components/file/notify.py +++ b/homeassistant/components/file/notify.py @@ -15,7 +15,7 @@ from homeassistant.components.notify import ( from homeassistant.const import CONF_FILENAME 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 import homeassistant.util.dt as dt_util CONF_TIMESTAMP = "timestamp" @@ -29,7 +29,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def get_service( - hass: HomeAssistant, config: ConfigType, discovery_info=None + hass: HomeAssistant, + config: ConfigType, + discovery_info: DiscoveryInfoType | None = None, ) -> FileNotificationService: """Get the file notification service.""" filename: str = config[CONF_FILENAME] diff --git a/homeassistant/components/huawei_lte/notify.py b/homeassistant/components/huawei_lte/notify.py index 6e01158a800..d47249ccc51 100644 --- a/homeassistant/components/huawei_lte/notify.py +++ b/homeassistant/components/huawei_lte/notify.py @@ -11,6 +11,7 @@ from huawei_lte_api.exceptions import ResponseErrorException from homeassistant.components.notify import ATTR_TARGET, BaseNotificationService from homeassistant.const import CONF_RECIPIENT from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import Router from .const import ATTR_UNIQUE_ID, DOMAIN @@ -20,8 +21,8 @@ _LOGGER = logging.getLogger(__name__) async def async_get_service( hass: HomeAssistant, - config: dict[str, Any], - discovery_info: dict[str, Any] | None = None, + config: ConfigType, + discovery_info: DiscoveryInfoType | None = None, ) -> HuaweiLteSmsNotificationService | None: """Get the notification service.""" if discovery_info is None: From 23a579d1c9cd4e64910cac1b16e20c0a86fc7254 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sun, 4 Sep 2022 22:45:41 +0200 Subject: [PATCH 087/955] Improve type hints in lastfm sensor (#77657) --- homeassistant/components/lastfm/sensor.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/lastfm/sensor.py b/homeassistant/components/lastfm/sensor.py index 9c158b244cb..2675371f033 100644 --- a/homeassistant/components/lastfm/sensor.py +++ b/homeassistant/components/lastfm/sensor.py @@ -91,7 +91,7 @@ class LastfmSensor(SensorEntity): """Return the state of the sensor.""" return self._state - def update(self): + def update(self) -> None: """Update device state.""" self._cover = self._user.get_image() self._playcount = self._user.get_playcount() @@ -101,10 +101,11 @@ class LastfmSensor(SensorEntity): self._lastplayed = f"{last.track.artist} - {last.track.title}" if top_tracks := self._user.get_top_tracks(limit=1): - top = top_tracks[0] - toptitle = re.search("', '(.+?)',", str(top)) - topartist = re.search("'(.+?)',", str(top)) - self._topplayed = f"{topartist.group(1)} - {toptitle.group(1)}" + top = str(top_tracks[0]) + if (toptitle := re.search("', '(.+?)',", top)) and ( + topartist := re.search("'(.+?)',", top) + ): + self._topplayed = f"{topartist.group(1)} - {toptitle.group(1)}" if (now_playing := self._user.get_now_playing()) is None: self._state = STATE_NOT_SCROBBLING From 168d122db4e6332fae087d760b3662212b0c9a16 Mon Sep 17 00:00:00 2001 From: Avi Miller Date: Mon, 5 Sep 2022 10:04:36 +1000 Subject: [PATCH 088/955] Add set_hev_cycle_state service to LIFX integration (#77546) Co-authored-by: J. Nick Koston --- homeassistant/components/lifx/coordinator.py | 10 +++- homeassistant/components/lifx/light.py | 48 +++++++++++++--- homeassistant/components/lifx/services.yaml | 26 +++++++++ tests/components/lifx/__init__.py | 6 +- tests/components/lifx/test_light.py | 60 ++++++++++++++++++++ 5 files changed, 135 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/lifx/coordinator.py b/homeassistant/components/lifx/coordinator.py index 37e753c27a3..d30af851e7d 100644 --- a/homeassistant/components/lifx/coordinator.py +++ b/homeassistant/components/lifx/coordinator.py @@ -118,9 +118,6 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator): await self.async_update_color_zones() if lifx_features(self.device)["hev"]: - if self.device.hev_cycle_configuration is None: - self.device.get_hev_configuration() - await self.async_get_hev_cycle() async def async_update_color_zones(self) -> None: @@ -195,3 +192,10 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator): apply=apply, ) ) + + async def async_set_hev_cycle_state(self, enable: bool, duration: int = 0) -> None: + """Start or stop an HEV cycle on a LIFX Clean bulb.""" + if lifx_features(self.device)["hev"]: + await async_execute_lifx( + partial(self.device.set_hev_cycle, enable=enable, duration=duration) + ) diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index 36d3b480f74..4df04f2d1e7 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -28,7 +28,14 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_point_in_utc_time import homeassistant.util.color as color_util -from .const import ATTR_INFRARED, ATTR_POWER, ATTR_ZONES, DATA_LIFX_MANAGER, DOMAIN +from .const import ( + ATTR_DURATION, + ATTR_INFRARED, + ATTR_POWER, + ATTR_ZONES, + DATA_LIFX_MANAGER, + DOMAIN, +) from .coordinator import LIFXUpdateCoordinator from .entity import LIFXEntity from .manager import ( @@ -43,14 +50,20 @@ LIFX_STATE_SETTLE_DELAY = 0.3 SERVICE_LIFX_SET_STATE = "set_state" -LIFX_SET_STATE_SCHEMA = cv.make_entity_service_schema( - { - **LIGHT_TURN_ON_SCHEMA, - ATTR_INFRARED: vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255)), - ATTR_ZONES: vol.All(cv.ensure_list, [cv.positive_int]), - ATTR_POWER: cv.boolean, - } -) +LIFX_SET_STATE_SCHEMA = { + **LIGHT_TURN_ON_SCHEMA, + ATTR_INFRARED: vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255)), + ATTR_ZONES: vol.All(cv.ensure_list, [cv.positive_int]), + ATTR_POWER: cv.boolean, +} + + +SERVICE_LIFX_SET_HEV_CYCLE_STATE = "set_hev_cycle_state" + +LIFX_SET_HEV_CYCLE_STATE_SCHEMA = { + ATTR_POWER: vol.Required(cv.boolean), + ATTR_DURATION: vol.All(vol.Coerce(float), vol.Clamp(min=0, max=86400)), +} HSBK_HUE = 0 HSBK_SATURATION = 1 @@ -74,6 +87,11 @@ async def async_setup_entry( LIFX_SET_STATE_SCHEMA, "set_state", ) + platform.async_register_entity_service( + SERVICE_LIFX_SET_HEV_CYCLE_STATE, + LIFX_SET_HEV_CYCLE_STATE_SCHEMA, + "set_hev_cycle_state", + ) if lifx_features(device)["multizone"]: entity: LIFXLight = LIFXStrip(coordinator, manager, entry) elif lifx_features(device)["color"]: @@ -237,6 +255,18 @@ class LIFXLight(LIFXEntity, LightEntity): # Update when the transition starts and ends await self.update_during_transition(fade) + async def set_hev_cycle_state( + self, power: bool, duration: int | None = None + ) -> None: + """Set the state of the HEV LEDs on a LIFX Clean bulb.""" + if lifx_features(self.bulb)["hev"] is False: + raise HomeAssistantError( + "This device does not support setting HEV cycle state" + ) + + await self.coordinator.async_set_hev_cycle_state(power, duration or 0) + await self.update_during_transition(duration or 0) + async def set_power( self, pwr: bool, diff --git a/homeassistant/components/lifx/services.yaml b/homeassistant/components/lifx/services.yaml index e499ad1b3b8..5208be89638 100644 --- a/homeassistant/components/lifx/services.yaml +++ b/homeassistant/components/lifx/services.yaml @@ -1,3 +1,29 @@ +set_hev_cycle_state: + name: Set HEV cycle state + description: Control the HEV LEDs on a LIFX Clean bulb. + target: + entity: + integration: lifx + domain: light + fields: + power: + name: enable + description: Start or stop a Clean cycle. + required: true + example: true + selector: + boolean: + duration: + name: Duration + description: How long the HEV LEDs will remain on. Uses the configured default duration if not specified. + required: false + default: 7200 + example: 3600 + selector: + number: + min: 0 + max: 86400 + unit_of_measurement: seconds set_state: name: Set State description: Set a color/brightness and possibly turn the light on/off. diff --git a/tests/components/lifx/__init__.py b/tests/components/lifx/__init__.py index 9e137c8532a..05d7e9a1ddf 100644 --- a/tests/components/lifx/__init__.py +++ b/tests/components/lifx/__init__.py @@ -118,9 +118,9 @@ def _mocked_brightness_bulb() -> Light: def _mocked_clean_bulb() -> Light: bulb = _mocked_bulb() - bulb.get_hev_cycle = MockLifxCommand( - bulb, duration=7200, remaining=0, last_power=False - ) + bulb.get_hev_cycle = MockLifxCommand(bulb) + bulb.set_hev_cycle = MockLifxCommand(bulb) + bulb.hev_cycle_configuration = {"duration": 7200, "indication": False} bulb.hev_cycle = { "duration": 7200, "remaining": 30, diff --git a/tests/components/lifx/test_light.py b/tests/components/lifx/test_light.py index 6229e130a40..6555e483f5f 100644 --- a/tests/components/lifx/test_light.py +++ b/tests/components/lifx/test_light.py @@ -8,6 +8,7 @@ import pytest from homeassistant.components import lifx from homeassistant.components.lifx import DOMAIN +from homeassistant.components.lifx.const import ATTR_POWER from homeassistant.components.lifx.light import ATTR_INFRARED, ATTR_ZONES from homeassistant.components.lifx.manager import SERVICE_EFFECT_COLORLOOP from homeassistant.components.light import ( @@ -40,6 +41,7 @@ from . import ( _mocked_brightness_bulb, _mocked_bulb, _mocked_bulb_new_firmware, + _mocked_clean_bulb, _mocked_light_strip, _mocked_white_bulb, _patch_config_flow_try_connect, @@ -997,3 +999,61 @@ async def test_color_bulb_is_actually_off(hass: HomeAssistant) -> None: ) assert bulb.set_color.calls[0][0][0] == [0, 0, 25700, 3500] assert len(bulb.set_power.calls) == 1 + + +async def test_clean_bulb(hass: HomeAssistant) -> None: + """Test setting HEV cycle state on Clean bulbs.""" + config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=SERIAL + ) + config_entry.add_to_hass(hass) + bulb = _mocked_clean_bulb() + bulb.power_level = 0 + bulb.hev_cycle = {"duration": 7200, "remaining": 0, "last_power": False} + 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 == "off" + await hass.services.async_call( + DOMAIN, + "set_hev_cycle_state", + {ATTR_ENTITY_ID: entity_id, ATTR_POWER: True}, + blocking=True, + ) + + call_dict = bulb.set_hev_cycle.calls[0][1] + call_dict.pop("callb") + assert call_dict == {"duration": 0, "enable": True} + bulb.set_hev_cycle.reset_mock() + + +async def test_set_hev_cycle_state_fails_for_color_bulb(hass: HomeAssistant) -> None: + """Test that set_hev_cycle_state fails for a non-Clean bulb.""" + config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=SERIAL + ) + config_entry.add_to_hass(hass) + bulb = _mocked_bulb() + bulb.power_level = 0 + 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 == "off" + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + DOMAIN, + "set_hev_cycle_state", + {ATTR_ENTITY_ID: entity_id, ATTR_POWER: True}, + blocking=True, + ) From c5100d2895a84eb3c317b6e100630718c320b5b3 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 5 Sep 2022 00:31:16 +0000 Subject: [PATCH 089/955] [ci skip] Translation update --- .../bluemaestro/translations/bn.json | 22 ++++++++ .../bluemaestro/translations/ca.json | 22 ++++++++ .../bluemaestro/translations/de.json | 22 ++++++++ .../bluemaestro/translations/el.json | 22 ++++++++ .../bluemaestro/translations/es.json | 22 ++++++++ .../bluemaestro/translations/fr.json | 22 ++++++++ .../bluemaestro/translations/hu.json | 22 ++++++++ .../bluemaestro/translations/pt-BR.json | 22 ++++++++ .../bluemaestro/translations/ru.json | 22 ++++++++ .../bluemaestro/translations/zh-Hant.json | 22 ++++++++ .../components/nobo_hub/translations/bn.json | 44 +++++++++++++++ .../components/nobo_hub/translations/ca.json | 20 +++++++ .../components/nobo_hub/translations/id.json | 25 ++++++++- .../components/nobo_hub/translations/ru.json | 44 +++++++++++++++ .../components/overkiz/translations/bn.json | 7 +++ .../components/sensibo/translations/bn.json | 16 ++++++ .../components/sensibo/translations/es.json | 6 +++ .../components/sensibo/translations/fr.json | 6 +++ .../components/sensibo/translations/hu.json | 6 +++ .../components/sensor/translations/bn.json | 10 ++++ .../volvooncall/translations/bn.json | 7 +++ .../volvooncall/translations/ca.json | 3 +- .../components/zha/translations/bn.json | 54 +++++++++++++++++++ 23 files changed, 466 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/bluemaestro/translations/bn.json create mode 100644 homeassistant/components/bluemaestro/translations/ca.json create mode 100644 homeassistant/components/bluemaestro/translations/de.json create mode 100644 homeassistant/components/bluemaestro/translations/el.json create mode 100644 homeassistant/components/bluemaestro/translations/es.json create mode 100644 homeassistant/components/bluemaestro/translations/fr.json create mode 100644 homeassistant/components/bluemaestro/translations/hu.json create mode 100644 homeassistant/components/bluemaestro/translations/pt-BR.json create mode 100644 homeassistant/components/bluemaestro/translations/ru.json create mode 100644 homeassistant/components/bluemaestro/translations/zh-Hant.json create mode 100644 homeassistant/components/nobo_hub/translations/bn.json create mode 100644 homeassistant/components/nobo_hub/translations/ca.json create mode 100644 homeassistant/components/nobo_hub/translations/ru.json create mode 100644 homeassistant/components/overkiz/translations/bn.json create mode 100644 homeassistant/components/sensibo/translations/bn.json create mode 100644 homeassistant/components/sensor/translations/bn.json create mode 100644 homeassistant/components/volvooncall/translations/bn.json create mode 100644 homeassistant/components/zha/translations/bn.json diff --git a/homeassistant/components/bluemaestro/translations/bn.json b/homeassistant/components/bluemaestro/translations/bn.json new file mode 100644 index 00000000000..cfef9be6dac --- /dev/null +++ b/homeassistant/components/bluemaestro/translations/bn.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u09a1\u09bf\u09ad\u09be\u0987\u09b8 \u0987\u09a4\u09bf\u09ae\u09a7\u09cd\u09af\u09c7 \u0995\u09a8\u09ab\u09bf\u0997\u09be\u09b0 \u0995\u09b0\u09be \u0986\u099b\u09c7", + "already_in_progress": "\u0995\u09a8\u09ab\u09bf\u0997\u09be\u09b0\u09c7\u09b6\u09a8 \u09aa\u09cd\u09b0\u09ac\u09be\u09b9 \u0987\u09a4\u09bf\u09ae\u09a7\u09cd\u09af\u09c7\u0987 \u099a\u09b2\u099b\u09c7", + "no_devices_found": "\u09a8\u09c7\u099f\u0993\u09af\u09bc\u09be\u09b0\u09cd\u0995\u09c7 \u0995\u09cb\u09a8\u09cb \u09a1\u09bf\u09ad\u09be\u0987\u09b8 \u09aa\u09be\u0993\u09af\u09bc\u09be \u09af\u09be\u09af\u09bc\u09a8\u09bf", + "not_supported": "\u09a1\u09bf\u09ad\u09be\u0987\u09b8 \u09b8\u09ae\u09b0\u09cd\u09a5\u09bf\u09a4 \u09a8\u09af\u09bc" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0986\u09aa\u09a8\u09bf \u0995\u09bf {name} \u09b8\u09c7\u099f\u0986\u09aa \u0995\u09b0\u09a4\u09c7 \u099a\u09be\u09a8?" + }, + "user": { + "data": { + "address": "\u09a1\u09bf\u09ad\u09be\u0987\u09b8" + }, + "description": "\u09b8\u09c7\u099f\u0986\u09aa \u0995\u09b0\u09be\u09b0 \u099c\u09a8\u09cd\u09af \u098f\u0995\u099f\u09bf \u09a1\u09bf\u09ad\u09be\u0987\u09b8 \u099a\u09af\u09bc\u09a8 \u0995\u09b0\u09c1\u09a8" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluemaestro/translations/ca.json b/homeassistant/components/bluemaestro/translations/ca.json new file mode 100644 index 00000000000..c121ff7408c --- /dev/null +++ b/homeassistant/components/bluemaestro/translations/ca.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", + "no_devices_found": "No s'han trobat dispositius a la xarxa", + "not_supported": "Dispositiu no compatible" + }, + "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/bluemaestro/translations/de.json b/homeassistant/components/bluemaestro/translations/de.json new file mode 100644 index 00000000000..4c5720ec6fb --- /dev/null +++ b/homeassistant/components/bluemaestro/translations/de.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden", + "not_supported": "Ger\u00e4t nicht unterst\u00fctzt" + }, + "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/bluemaestro/translations/el.json b/homeassistant/components/bluemaestro/translations/el.json new file mode 100644 index 00000000000..cdb57c8ac1b --- /dev/null +++ b/homeassistant/components/bluemaestro/translations/el.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "already_in_progress": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", + "no_devices_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf", + "not_supported": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name};" + }, + "user": { + "data": { + "address": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b3\u03b9\u03b1 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluemaestro/translations/es.json b/homeassistant/components/bluemaestro/translations/es.json new file mode 100644 index 00000000000..ae0ab01acdf --- /dev/null +++ b/homeassistant/components/bluemaestro/translations/es.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", + "no_devices_found": "No se encontraron dispositivos en la red", + "not_supported": "Dispositivo no compatible" + }, + "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/bluemaestro/translations/fr.json b/homeassistant/components/bluemaestro/translations/fr.json new file mode 100644 index 00000000000..8ddb4af4dbc --- /dev/null +++ b/homeassistant/components/bluemaestro/translations/fr.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", + "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau", + "not_supported": "Appareil non pris en charge" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Voulez-vous configurer {name}\u00a0?" + }, + "user": { + "data": { + "address": "Appareil" + }, + "description": "S\u00e9lectionnez l'appareil \u00e0 configurer" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluemaestro/translations/hu.json b/homeassistant/components/bluemaestro/translations/hu.json new file mode 100644 index 00000000000..97fbb5b9408 --- /dev/null +++ b/homeassistant/components/bluemaestro/translations/hu.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", + "not_supported": "Eszk\u00f6z nem t\u00e1mogatott" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name}?" + }, + "user": { + "data": { + "address": "Eszk\u00f6z" + }, + "description": "V\u00e1lassza ki a be\u00e1ll\u00edtani k\u00edv\u00e1nt eszk\u00f6zt" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluemaestro/translations/pt-BR.json b/homeassistant/components/bluemaestro/translations/pt-BR.json new file mode 100644 index 00000000000..0da7639fa2a --- /dev/null +++ b/homeassistant/components/bluemaestro/translations/pt-BR.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "A configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "no_devices_found": "Nenhum dispositivo encontrado na rede", + "not_supported": "Dispositivo n\u00e3o suportado" + }, + "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/bluemaestro/translations/ru.json b/homeassistant/components/bluemaestro/translations/ru.json new file mode 100644 index 00000000000..887499e5f2e --- /dev/null +++ b/homeassistant/components/bluemaestro/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.", + "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.", + "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.", + "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." + }, + "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/bluemaestro/translations/zh-Hant.json b/homeassistant/components/bluemaestro/translations/zh-Hant.json new file mode 100644 index 00000000000..64ae1f19094 --- /dev/null +++ b/homeassistant/components/bluemaestro/translations/zh-Hant.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", + "not_supported": "\u88dd\u7f6e\u4e0d\u652f\u63f4" + }, + "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/nobo_hub/translations/bn.json b/homeassistant/components/nobo_hub/translations/bn.json new file mode 100644 index 00000000000..9c61784e29a --- /dev/null +++ b/homeassistant/components/nobo_hub/translations/bn.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "\u09a1\u09bf\u09ad\u09be\u0987\u09b8 \u0987\u09a4\u09bf\u09ae\u09a7\u09cd\u09af\u09c7 \u0995\u09a8\u09ab\u09bf\u0997\u09be\u09b0 \u0995\u09b0\u09be \u0986\u099b\u09c7" + }, + "error": { + "cannot_connect": "\u09b8\u0982\u09af\u09cb\u0997 \u0995\u09b0\u09a4\u09c7 \u09ac\u09cd\u09af\u09b0\u09cd\u09a5 - \u0995\u09cd\u09b0\u09ae\u09bf\u0995 \u09b8\u0982\u0996\u09cd\u09af\u09be \u09aa\u09b0\u09c0\u0995\u09cd\u09b7\u09be \u0995\u09b0\u09c1\u09a8", + "invalid_ip": "\u0985\u0995\u09be\u09b0\u09cd\u09af\u0995\u09b0 \u0986\u0987\u09aa\u09bf \u09a0\u09bf\u0995\u09be\u09a8\u09be", + "invalid_serial": "\u0985\u0995\u09be\u09b0\u09cd\u09af\u0995\u09b0 \u0995\u09cd\u09b0\u09ae\u09bf\u0995 \u09b8\u0982\u0996\u09cd\u09af\u09be", + "unknown": "\u0985\u09aa\u09cd\u09b0\u09a4\u09cd\u09af\u09be\u09b6\u09bf\u09a4 \u09a4\u09cd\u09b0\u09c1\u099f\u09bf" + }, + "step": { + "manual": { + "data": { + "ip_address": "\u0986\u0987\u09aa\u09bf \u09a0\u09bf\u0995\u09be\u09a8\u09be", + "serial": "\u0995\u09cd\u09b0\u09ae\u09bf\u0995 \u09b8\u0982\u0996\u09cd\u09af\u09be (\u09e7\u09e8 \u099f\u09bf \u09b8\u0982\u0996\u09cd\u09af\u09be)" + }, + "description": "\u0986\u09aa\u09a8\u09be\u09b0 \u09b8\u09cd\u09a5\u09be\u09a8\u09c0\u09af\u09bc \u09a8\u09c7\u099f\u0993\u09af\u09bc\u09be\u09b0\u09cd\u0995\u09c7 \u0986\u09ac\u09bf\u09b7\u09cd\u0995\u09c3\u09a4 \u09a8\u09af\u09bc \u098f\u09ae\u09a8 \u098f\u0995\u099f\u09bf \u09a8\u09cb\u09ac\u09cb \u0987\u0995\u09cb\u09b9\u09be\u09ac \u0995\u09a8\u09ab\u09bf\u0997\u09be\u09b0 \u0995\u09b0\u09c1\u09a8\u0964 \u09af\u09a6\u09bf \u0986\u09aa\u09a8\u09be\u09b0 \u09b9\u09be\u09ac\u099f\u09bf \u0985\u09a8\u09cd\u09af \u09a8\u09c7\u099f\u0993\u09af\u09bc\u09be\u09b0\u09cd\u0995\u09c7 \u09a5\u09be\u0995\u09c7 \u09a4\u09ac\u09c7 \u0986\u09aa\u09a8\u09bf \u098f\u0996\u09a8\u0993 \u09b8\u09ae\u09cd\u09aa\u09c2\u09b0\u09cd\u09a3 \u0995\u09cd\u09b0\u09ae\u09bf\u0995 \u09a8\u09ae\u09cd\u09ac\u09b0 (\u09e7\u09e8 \u099f\u09bf \u09b8\u0982\u0996\u09cd\u09af\u09be) \u098f\u09ac\u0982 \u098f\u09b0 \u0986\u0987\u09aa\u09bf \u09a0\u09bf\u0995\u09be\u09a8\u09be \u09b2\u09bf\u0996\u09c7 \u098f\u099f\u09bf\u09b0 \u09b8\u09be\u09a5\u09c7 \u09b8\u0982\u09af\u09cb\u0997 \u0995\u09b0\u09a4\u09c7 \u09aa\u09be\u09b0\u09c7\u09a8\u0964" + }, + "selected": { + "data": { + "serial_suffix": "\u0995\u09cd\u09b0\u09ae\u09bf\u0995 \u09b8\u0982\u0996\u09cd\u09af\u09be \u09aa\u09cd\u09b0\u09a4\u09cd\u09af\u09af\u09bc (\u09e9 \u099f\u09bf \u09b8\u0982\u0996\u09cd\u09af\u09be)" + }, + "description": "{hub} \u0995\u09a8\u09ab\u09bf\u0997\u09be\u09b0 \u0995\u09b0\u09be \u09b9\u099a\u09cd\u099b\u09c7\u0964\n\n\u09b9\u09be\u09ac\u09c7\u09b0 \u09b8\u09be\u09a5\u09c7 \u09b8\u0982\u09af\u09cb\u0997 \u0995\u09b0\u09a4\u09c7, \u0986\u09aa\u09a8\u09be\u0995\u09c7 \u09b9\u09be\u09ac\u09c7\u09b0 \u0995\u09cd\u09b0\u09ae\u09bf\u0995 \u09a8\u09ae\u09cd\u09ac\u09b0\u09c7\u09b0 \u09b6\u09c7\u09b7 \u09e9 \u099f\u09bf \u09b8\u0982\u0996\u09cd\u09af\u09be \u09b2\u09bf\u0996\u09a4\u09c7 \u09b9\u09ac\u09c7\u0964" + }, + "user": { + "data": { + "device": "\u0986\u09ac\u09bf\u09b7\u09cd\u0995\u09c3\u09a4 \u09b9\u09be\u09ac" + }, + "description": "\u0995\u09a8\u09ab\u09bf\u0997\u09be\u09b0 \u0995\u09b0\u09a4\u09c7 Nob\u00f8 Ecohub \u09a8\u09bf\u09b0\u09cd\u09ac\u09be\u099a\u09a8 \u0995\u09b0\u09c1\u09a8\u0964" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "override_type": "\u0985\u0997\u09cd\u09b0\u09be\u09a7\u09bf\u0995\u09be\u09b0 \u09a7\u09b0\u09a3" + }, + "description": "\u09aa\u09b0\u09c7\u09b0 \u09b8\u09aa\u09cd\u09a4\u09be\u09b9\u09c7\u09b0 \u09aa\u09cd\u09b0\u09cb\u09ab\u09be\u0987\u09b2 \u09aa\u09b0\u09bf\u09ac\u09b0\u09cd\u09a4\u09a8\u09c7 \u0993\u09ad\u09be\u09b0\u09b0\u09be\u0987\u09a1 \u09b6\u09c7\u09b7 \u0995\u09b0\u09a4\u09c7 \u0993\u09ad\u09be\u09b0\u09b0\u09be\u0987\u09a1 \u099f\u09be\u0987\u09aa \"\u098f\u0996\u09a8\" \u09a8\u09bf\u09b0\u09cd\u09ac\u09be\u099a\u09a8 \u0995\u09b0\u09c1\u09a8\u0964" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nobo_hub/translations/ca.json b/homeassistant/components/nobo_hub/translations/ca.json new file mode 100644 index 00000000000..f3f3bc63837 --- /dev/null +++ b/homeassistant/components/nobo_hub/translations/ca.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "invalid_ip": "Adre\u00e7a IP inv\u00e0lida", + "invalid_serial": "N\u00famero de s\u00e8rie inv\u00e0lid", + "unknown": "Error inesperat" + }, + "step": { + "manual": { + "data": { + "ip_address": "Adre\u00e7a IP", + "serial": "N\u00famero de s\u00e8rie (12 d\u00edgits)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nobo_hub/translations/id.json b/homeassistant/components/nobo_hub/translations/id.json index dc48a44b0ae..28cfbbe8a55 100644 --- a/homeassistant/components/nobo_hub/translations/id.json +++ b/homeassistant/components/nobo_hub/translations/id.json @@ -11,8 +11,31 @@ "step": { "manual": { "data": { - "ip_address": "Alamat IP" + "ip_address": "Alamat IP", + "serial": "Nomor seri (12 digit)" } + }, + "selected": { + "data": { + "serial_suffix": "Akhiran nomor seri (3 digit)" + }, + "description": "Mengonfigurasi {hub}.\n\nUntuk terhubung ke hub, Anda harus memasukkan 3 digit terakhir dari nomor seri hub." + }, + "user": { + "data": { + "device": "Hub yang ditemukan" + }, + "description": "Pilih Nob\u00f8 Ecohub untuk dikonfigurasi." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "override_type": "Tipe penimpaan" + }, + "description": "Pilih tipe penimpaan \"Now\" untuk mengakhiri penimpaan pada perubahan profil minggu depan." } } } diff --git a/homeassistant/components/nobo_hub/translations/ru.json b/homeassistant/components/nobo_hub/translations/ru.json new file mode 100644 index 00000000000..e5932ac1e03 --- /dev/null +++ b/homeassistant/components/nobo_hub/translations/ru.json @@ -0,0 +1,44 @@ +{ + "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 - \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0441\u0435\u0440\u0438\u0439\u043d\u044b\u0439 \u043d\u043e\u043c\u0435\u0440.", + "invalid_ip": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441.", + "invalid_serial": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0441\u0435\u0440\u0438\u0439\u043d\u044b\u0439 \u043d\u043e\u043c\u0435\u0440.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "manual": { + "data": { + "ip_address": "IP-\u0430\u0434\u0440\u0435\u0441", + "serial": "\u0421\u0435\u0440\u0438\u0439\u043d\u044b\u0439 \u043d\u043e\u043c\u0435\u0440 (12 \u0446\u0438\u0444\u0440)" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Nob\u00f8 Ecohub, \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043d\u043e\u0433\u043e \u0432 \u0412\u0430\u0448\u0435\u0439 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e\u0439 \u0441\u0435\u0442\u0438. \u0415\u0441\u043b\u0438 \u0412\u0430\u0448 \u0431\u043b\u043e\u043a \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u043d\u0430\u0445\u043e\u0434\u0438\u0442\u0441\u044f \u0432 \u0434\u0440\u0443\u0433\u043e\u0439 \u0441\u0435\u0442\u0438, \u0412\u044b \u0432\u0441\u0435 \u0440\u0430\u0432\u043d\u043e \u043c\u043e\u0436\u0435\u0442\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u043d\u0435\u043c\u0443, \u0432\u0432\u0435\u0434\u044f \u043f\u043e\u043b\u043d\u044b\u0439 \u0441\u0435\u0440\u0438\u0439\u043d\u044b\u0439 \u043d\u043e\u043c\u0435\u0440 (12 \u0446\u0438\u0444\u0440) \u0438 \u0435\u0433\u043e IP-\u0430\u0434\u0440\u0435\u0441." + }, + "selected": { + "data": { + "serial_suffix": "\u0421\u0443\u0444\u0444\u0438\u043a\u0441 \u0441\u0435\u0440\u0438\u0439\u043d\u043e\u0433\u043e \u043d\u043e\u043c\u0435\u0440\u0430 (3 \u0446\u0438\u0444\u0440\u044b)" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 {hub}. \u0414\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0432\u0432\u0435\u0441\u0442\u0438 \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0438\u0435 3 \u0446\u0438\u0444\u0440\u044b \u0441\u0435\u0440\u0438\u0439\u043d\u043e\u0433\u043e \u043d\u043e\u043c\u0435\u0440\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430." + }, + "user": { + "data": { + "device": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043d\u044b\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 Nob\u00f8 Ecohub \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "override_type": "\u0422\u0438\u043f \u043f\u0435\u0440\u0435\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u044f" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u0438\u043f \u043f\u0435\u0440\u0435\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u044f \u00abNow\u00bb, \u0447\u0442\u043e\u0431\u044b \u043e\u0442\u043c\u0435\u043d\u0438\u0442\u044c \u043f\u0435\u0440\u0435\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u0435 \u043f\u0440\u0438 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0438 \u043f\u0440\u043e\u0444\u0438\u043b\u044f \u043d\u0430 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0435\u0439 \u043d\u0435\u0434\u0435\u043b\u0435." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/bn.json b/homeassistant/components/overkiz/translations/bn.json new file mode 100644 index 00000000000..de652521c3c --- /dev/null +++ b/homeassistant/components/overkiz/translations/bn.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "unknown_user": "\u0985\u09aa\u09b0\u09bf\u099a\u09bf\u09a4 \u09ac\u09cd\u09af\u09ac\u09b9\u09be\u09b0\u0995\u09be\u09b0\u09c0\u0964 Somfy Protect \u0985\u09cd\u09af\u09be\u0995\u09be\u0989\u09a8\u09cd\u099f\u0997\u09c1\u09b2\u09bf \u098f\u0987 \u0987\u09a8\u09cd\u099f\u09bf\u0997\u09cd\u09b0\u09c7\u09b6\u09a8 \u09a6\u09cd\u09ac\u09be\u09b0\u09be \u09b8\u09ae\u09b0\u09cd\u09a5\u09bf\u09a4 \u09a8\u09af\u09bc\u0964" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/bn.json b/homeassistant/components/sensibo/translations/bn.json new file mode 100644 index 00000000000..8aacefc17d7 --- /dev/null +++ b/homeassistant/components/sensibo/translations/bn.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data_description": { + "api_key": "\u098f\u0995\u099f\u09bf \u09a8\u09a4\u09c1\u09a8 \u098f\u09aa\u09bf\u0986\u0987 \u0995\u09c0 \u09aa\u09c7\u09a4\u09c7 \u09a1\u0995\u09c1\u09ae\u09c7\u09a8\u09cd\u099f\u09c7\u09b6\u09a8 \u0985\u09a8\u09c1\u09b8\u09b0\u09a3 \u0995\u09b0\u09c1\u09a8\u0964" + } + }, + "user": { + "data_description": { + "api_key": "\u0986\u09aa\u09a8\u09be\u09b0 \u098f\u09aa\u09bf\u0986\u0987 \u0995\u09c0 \u09aa\u09c7\u09a4\u09c7 \u09a1\u0995\u09c1\u09ae\u09c7\u09a8\u09cd\u099f\u09c7\u09b6\u09a8 \u0985\u09a8\u09c1\u09b8\u09b0\u09a3 \u0995\u09b0\u09c1\u09a8\u0964" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/es.json b/homeassistant/components/sensibo/translations/es.json index 37c2f022039..8c2c6bea7f3 100644 --- a/homeassistant/components/sensibo/translations/es.json +++ b/homeassistant/components/sensibo/translations/es.json @@ -15,11 +15,17 @@ "reauth_confirm": { "data": { "api_key": "Clave API" + }, + "data_description": { + "api_key": "Sigue la documentaci\u00f3n para obtener una nueva clave API." } }, "user": { "data": { "api_key": "Clave API" + }, + "data_description": { + "api_key": "Sigue la documentaci\u00f3n para obtener tu clave API." } } } diff --git a/homeassistant/components/sensibo/translations/fr.json b/homeassistant/components/sensibo/translations/fr.json index f674ad2c9b2..5d53f8daa37 100644 --- a/homeassistant/components/sensibo/translations/fr.json +++ b/homeassistant/components/sensibo/translations/fr.json @@ -15,11 +15,17 @@ "reauth_confirm": { "data": { "api_key": "Cl\u00e9 d'API" + }, + "data_description": { + "api_key": "Consultez la documentation pour obtenir une nouvelle cl\u00e9 d'API." } }, "user": { "data": { "api_key": "Cl\u00e9 d'API" + }, + "data_description": { + "api_key": "Consultez la documentation pour obtenir votre cl\u00e9 d'API." } } } diff --git a/homeassistant/components/sensibo/translations/hu.json b/homeassistant/components/sensibo/translations/hu.json index 14c7cd756d0..66d277eecf5 100644 --- a/homeassistant/components/sensibo/translations/hu.json +++ b/homeassistant/components/sensibo/translations/hu.json @@ -15,11 +15,17 @@ "reauth_confirm": { "data": { "api_key": "API kulcs" + }, + "data_description": { + "api_key": "K\u00f6vesse a dokument\u00e1ci\u00f3t az \u00faj API-kulcs beszerz\u00e9s\u00e9hez." } }, "user": { "data": { "api_key": "API kulcs" + }, + "data_description": { + "api_key": "K\u00f6vesse a dokument\u00e1ci\u00f3t az API-kulcs beszerz\u00e9s\u00e9hez." } } } diff --git a/homeassistant/components/sensor/translations/bn.json b/homeassistant/components/sensor/translations/bn.json new file mode 100644 index 00000000000..3469fbd327b --- /dev/null +++ b/homeassistant/components/sensor/translations/bn.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "condition_type": { + "is_moisture": "\u09ac\u09b0\u09cd\u09a4\u09ae\u09be\u09a8 {entity_name} \u0986\u09b0\u09cd\u09a6\u09cd\u09b0\u09a4\u09be" + }, + "trigger_type": { + "moisture": "{entity_name} \u0986\u09b0\u09cd\u09a6\u09cd\u09b0\u09a4\u09be \u09aa\u09b0\u09bf\u09ac\u09b0\u09cd\u09a4\u09a8" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/volvooncall/translations/bn.json b/homeassistant/components/volvooncall/translations/bn.json new file mode 100644 index 00000000000..7cd09ad31be --- /dev/null +++ b/homeassistant/components/volvooncall/translations/bn.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "reauth_successful": "\u09aa\u09c1\u09a8\u09b0\u09be\u09af\u09bc \u09aa\u09cd\u09b0\u09ae\u09be\u09a3\u09c0\u0995\u09b0\u09a3 \u09b8\u09ab\u09b2 \u09b9\u09af\u09bc\u09c7\u099b\u09c7" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/volvooncall/translations/ca.json b/homeassistant/components/volvooncall/translations/ca.json index 12012ffc522..26a287a5584 100644 --- a/homeassistant/components/volvooncall/translations/ca.json +++ b/homeassistant/components/volvooncall/translations/ca.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "El compte ja est\u00e0 configurat" + "already_configured": "El compte ja est\u00e0 configurat", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "error": { "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", diff --git a/homeassistant/components/zha/translations/bn.json b/homeassistant/components/zha/translations/bn.json new file mode 100644 index 00000000000..e4c12e5f1c6 --- /dev/null +++ b/homeassistant/components/zha/translations/bn.json @@ -0,0 +1,54 @@ +{ + "config": { + "step": { + "manual_pick_radio_type": { + "data": { + "radio_type": "\u09b0\u09c7\u09a1\u09bf\u0993\u09b0 \u09a7\u09b0\u09a8" + }, + "description": "\u0986\u09aa\u09a8\u09be\u09b0 Zigbee \u09b0\u09c7\u09a1\u09bf\u0993 \u09aa\u09cd\u09b0\u0995\u09be\u09b0 \u099a\u09af\u09bc\u09a8 \u0995\u09b0\u09c1\u09a8", + "title": "\u09b0\u09c7\u09a1\u09bf\u0993\u09b0 \u09a7\u09b0\u09a8" + }, + "manual_port_config": { + "data": { + "baudrate": "\u09aa\u09cb\u09b0\u09cd\u099f\u09c7\u09b0 \u0997\u09a4\u09bf", + "flow_control": "\u09a4\u09a5\u09cd\u09af \u09aa\u09cd\u09b0\u09ac\u09be\u09b9 \u09a8\u09bf\u09af\u09bc\u09a8\u09cd\u09a4\u09cd\u09b0\u09a3", + "path": "\u09b8\u09bf\u09b0\u09bf\u09af\u09bc\u09be\u09b2 \u09a1\u09bf\u09ad\u09be\u0987\u09b8 \u09aa\u09a5" + }, + "description": "\u09b8\u09bf\u09b0\u09bf\u09af\u09bc\u09be\u09b2 \u09aa\u09cb\u09b0\u09cd\u099f \u09b8\u09c7\u099f\u09bf\u0982\u09b8 \u09b2\u09bf\u0996\u09c1\u09a8", + "title": "\u09b8\u09bf\u09b0\u09bf\u09af\u09bc\u09be\u09b2 \u09aa\u09cb\u09b0\u09cd\u099f \u09b8\u09c7\u099f\u09bf\u0982\u09b8" + }, + "maybe_confirm_ezsp_restore": { + "data": { + "overwrite_coordinator_ieee": "\u09b8\u09cd\u09a5\u09be\u09af\u09bc\u09c0\u09ad\u09be\u09ac\u09c7 \u09b0\u09c7\u09a1\u09bf\u0993 IEEE \u09a0\u09bf\u0995\u09be\u09a8\u09be \u09aa\u09cd\u09b0\u09a4\u09bf\u09b8\u09cd\u09a5\u09be\u09aa\u09a8 \u0995\u09b0\u09c1\u09a8" + }, + "title": "\u09b0\u09c7\u09a1\u09bf\u0993 IEEE \u09a0\u09bf\u0995\u09be\u09a8\u09be \u0993\u09ad\u09be\u09b0\u09b0\u09be\u0987\u099f \u0995\u09b0\u09c1\u09a8" + }, + "upload_manual_backup": { + "data": { + "uploaded_backup_file": "\u098f\u0995\u099f\u09bf \u09ab\u09be\u0987\u09b2 \u0986\u09aa\u09b2\u09cb\u09a1 \u0995\u09b0\u09c1\u09a8" + }, + "title": "\u098f\u0995\u099f\u09bf \u09ae\u09cd\u09af\u09be\u09a8\u09c1\u09af\u09bc\u09be\u09b2 \u09ac\u09cd\u09af\u09be\u0995\u0986\u09aa \u0986\u09aa\u09b2\u09cb\u09a1 \u0995\u09b0\u09c1\u09a8" + } + } + }, + "options": { + "step": { + "init": { + "description": "ZHA \u09ac\u09a8\u09cd\u09a7 \u0995\u09b0\u09c7 \u09a6\u09c7\u0993\u09af\u09bc\u09be \u09b9\u09ac\u09c7\u0964 \u0986\u09aa\u09a8\u09bf \u0995\u09bf \u099a\u09be\u09b2\u09bf\u09af\u09bc\u09c7 \u09af\u09c7\u09a4\u09c7 \u099a\u09be\u09a8?", + "title": "ZHA \u09aa\u09c1\u09a8\u09b0\u09be\u09af\u09bc \u0995\u09a8\u09ab\u09bf\u0997\u09be\u09b0 \u0995\u09b0\u09c1\u09a8" + }, + "manual_pick_radio_type": { + "data": { + "radio_type": "\u09b0\u09c7\u09a1\u09bf\u0993 \u09aa\u09cd\u09b0\u0995\u09be\u09b0" + }, + "description": "\u0986\u09aa\u09a8\u09be\u09b0 \u099c\u09bf\u0997\u09ac\u09bf \u09b0\u09c7\u09a1\u09bf\u0993 \u099f\u09be\u0987\u09aa \u099a\u09af\u09bc\u09a8 \u0995\u09b0\u09c1\u09a8", + "title": "\u09b0\u09c7\u09a1\u09bf\u0993 \u09aa\u09cd\u09b0\u0995\u09be\u09b0" + }, + "manual_port_config": { + "data": { + "baudrate": "\u09aa\u09cb\u09b0\u09cd\u099f \u0997\u09a4\u09bf" + } + } + } + } +} \ No newline at end of file From 804e4ab9890436338d345c14d5b5b022bbd0025f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 4 Sep 2022 20:57:40 -0400 Subject: [PATCH 090/955] Prefilter noisy apple devices from bluetooth (#77808) --- homeassistant/components/bluetooth/manager.py | 18 ++- tests/components/bluetooth/test_init.py | 128 ++++++++++++------ 2 files changed, 106 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index d274939c610..9fc00aa159b 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -54,6 +54,10 @@ if TYPE_CHECKING: FILTER_UUIDS: Final = "UUIDs" +APPLE_MFR_ID: Final = 76 +APPLE_HOMEKIT_START_BYTE: Final = 0x06 # homekit_controller +APPLE_DEVICE_ID_START_BYTE: Final = 0x10 # bluetooth_le_tracker +APPLE_START_BYTES_WANTED: Final = {APPLE_DEVICE_ID_START_BYTE, APPLE_HOMEKIT_START_BYTE} RSSI_SWITCH_THRESHOLD = 6 @@ -290,6 +294,19 @@ class BluetoothManager: than the source from the history or the timestamp in the history is older than 180s """ + + # Pre-filter noisy apple devices as they can account for 20-35% of the + # traffic on a typical network. + advertisement_data = service_info.advertisement + manufacturer_data = advertisement_data.manufacturer_data + if ( + len(manufacturer_data) == 1 + and (apple_data := manufacturer_data.get(APPLE_MFR_ID)) + and apple_data[0] not in APPLE_START_BYTES_WANTED + and not advertisement_data.service_data + ): + return + device = service_info.device connectable = service_info.connectable address = device.address @@ -299,7 +316,6 @@ class BluetoothManager: return self._history[address] = service_info - advertisement_data = service_info.advertisement source = service_info.source if connectable: diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index ade68fdb94d..e4b84b943b4 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -1291,16 +1291,16 @@ async def test_register_callback_by_manufacturer_id( cancel = bluetooth.async_register_callback( hass, _fake_subscriber, - {MANUFACTURER_ID: 76}, + {MANUFACTURER_ID: 21}, BluetoothScanningMode.ACTIVE, ) assert len(mock_bleak_scanner_start.mock_calls) == 1 - apple_device = BLEDevice("44:44:33:11:23:45", "apple") + apple_device = BLEDevice("44:44:33:11:23:45", "rtx") apple_adv = AdvertisementData( - local_name="apple", - manufacturer_data={76: b"\xd8.\xad\xcd\r\x85"}, + local_name="rtx", + manufacturer_data={21: b"\xd8.\xad\xcd\r\x85"}, ) inject_advertisement(hass, apple_device, apple_adv) @@ -1316,9 +1316,59 @@ async def test_register_callback_by_manufacturer_id( assert len(callbacks) == 1 service_info: BluetoothServiceInfo = callbacks[0][0] - assert service_info.name == "apple" - assert service_info.manufacturer == "Apple, Inc." - assert service_info.manufacturer_id == 76 + assert service_info.name == "rtx" + assert service_info.manufacturer == "RTX Telecom A/S" + assert service_info.manufacturer_id == 21 + + +async def test_filtering_noisy_apple_devices( + hass, mock_bleak_scanner_start, enable_bluetooth +): + """Test filtering noisy apple devices.""" + mock_bt = [] + callbacks = [] + + def _fake_subscriber( + service_info: BluetoothServiceInfo, change: BluetoothChange + ) -> None: + """Fake subscriber for the BleakScanner.""" + callbacks.append((service_info, change)) + + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ): + await async_setup_with_default_adapter(hass) + + with patch.object(hass.config_entries.flow, "async_init"): + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + cancel = bluetooth.async_register_callback( + hass, + _fake_subscriber, + {MANUFACTURER_ID: 21}, + BluetoothScanningMode.ACTIVE, + ) + + assert len(mock_bleak_scanner_start.mock_calls) == 1 + + apple_device = BLEDevice("44:44:33:11:23:45", "rtx") + apple_adv = AdvertisementData( + local_name="noisy", + manufacturer_data={76: b"\xd8.\xad\xcd\r\x85"}, + ) + + inject_advertisement(hass, apple_device, apple_adv) + + empty_device = BLEDevice("11:22:33:44:55:66", "empty") + empty_adv = AdvertisementData(local_name="empty") + + inject_advertisement(hass, empty_device, empty_adv) + await hass.async_block_till_done() + + cancel() + + assert len(callbacks) == 0 async def test_register_callback_by_address_connectable_manufacturer_id( @@ -1346,21 +1396,21 @@ async def test_register_callback_by_address_connectable_manufacturer_id( cancel = bluetooth.async_register_callback( hass, _fake_subscriber, - {MANUFACTURER_ID: 76, CONNECTABLE: False, ADDRESS: "44:44:33:11:23:45"}, + {MANUFACTURER_ID: 21, CONNECTABLE: False, ADDRESS: "44:44:33:11:23:45"}, BluetoothScanningMode.ACTIVE, ) assert len(mock_bleak_scanner_start.mock_calls) == 1 - apple_device = BLEDevice("44:44:33:11:23:45", "apple") + apple_device = BLEDevice("44:44:33:11:23:45", "rtx") apple_adv = AdvertisementData( - local_name="apple", - manufacturer_data={76: b"\xd8.\xad\xcd\r\x85"}, + local_name="rtx", + manufacturer_data={21: b"\xd8.\xad\xcd\r\x85"}, ) inject_advertisement(hass, apple_device, apple_adv) - apple_device_wrong_address = BLEDevice("44:44:33:11:23:46", "apple") + apple_device_wrong_address = BLEDevice("44:44:33:11:23:46", "rtx") inject_advertisement(hass, apple_device_wrong_address, apple_adv) await hass.async_block_till_done() @@ -1370,9 +1420,9 @@ async def test_register_callback_by_address_connectable_manufacturer_id( assert len(callbacks) == 1 service_info: BluetoothServiceInfo = callbacks[0][0] - assert service_info.name == "apple" - assert service_info.manufacturer == "Apple, Inc." - assert service_info.manufacturer_id == 76 + assert service_info.name == "rtx" + assert service_info.manufacturer == "RTX Telecom A/S" + assert service_info.manufacturer_id == 21 async def test_register_callback_by_manufacturer_id_and_address( @@ -1400,19 +1450,19 @@ async def test_register_callback_by_manufacturer_id_and_address( cancel = bluetooth.async_register_callback( hass, _fake_subscriber, - {MANUFACTURER_ID: 76, ADDRESS: "44:44:33:11:23:45"}, + {MANUFACTURER_ID: 21, ADDRESS: "44:44:33:11:23:45"}, BluetoothScanningMode.ACTIVE, ) assert len(mock_bleak_scanner_start.mock_calls) == 1 - apple_device = BLEDevice("44:44:33:11:23:45", "apple") - apple_adv = AdvertisementData( - local_name="apple", - manufacturer_data={76: b"\xd8.\xad\xcd\r\x85"}, + rtx_device = BLEDevice("44:44:33:11:23:45", "rtx") + rtx_adv = AdvertisementData( + local_name="rtx", + manufacturer_data={21: b"\xd8.\xad\xcd\r\x85"}, ) - inject_advertisement(hass, apple_device, apple_adv) + inject_advertisement(hass, rtx_device, rtx_adv) yale_device = BLEDevice("44:44:33:11:23:45", "apple") yale_adv = AdvertisementData( @@ -1426,7 +1476,7 @@ async def test_register_callback_by_manufacturer_id_and_address( other_apple_device = BLEDevice("44:44:33:11:23:22", "apple") other_apple_adv = AdvertisementData( local_name="apple", - manufacturer_data={76: b"\xd8.\xad\xcd\r\x85"}, + manufacturer_data={21: b"\xd8.\xad\xcd\r\x85"}, ) inject_advertisement(hass, other_apple_device, other_apple_adv) @@ -1435,9 +1485,9 @@ async def test_register_callback_by_manufacturer_id_and_address( assert len(callbacks) == 1 service_info: BluetoothServiceInfo = callbacks[0][0] - assert service_info.name == "apple" - assert service_info.manufacturer == "Apple, Inc." - assert service_info.manufacturer_id == 76 + assert service_info.name == "rtx" + assert service_info.manufacturer == "RTX Telecom A/S" + assert service_info.manufacturer_id == 21 async def test_register_callback_by_service_uuid_and_address( @@ -1603,31 +1653,31 @@ async def test_register_callback_by_local_name( cancel = bluetooth.async_register_callback( hass, _fake_subscriber, - {LOCAL_NAME: "apple"}, + {LOCAL_NAME: "rtx"}, BluetoothScanningMode.ACTIVE, ) assert len(mock_bleak_scanner_start.mock_calls) == 1 - apple_device = BLEDevice("44:44:33:11:23:45", "apple") - apple_adv = AdvertisementData( - local_name="apple", - manufacturer_data={76: b"\xd8.\xad\xcd\r\x85"}, + rtx_device = BLEDevice("44:44:33:11:23:45", "rtx") + rtx_adv = AdvertisementData( + local_name="rtx", + manufacturer_data={21: b"\xd8.\xad\xcd\r\x85"}, ) - inject_advertisement(hass, apple_device, apple_adv) + inject_advertisement(hass, rtx_device, rtx_adv) empty_device = BLEDevice("11:22:33:44:55:66", "empty") empty_adv = AdvertisementData(local_name="empty") inject_advertisement(hass, empty_device, empty_adv) - apple_device_2 = BLEDevice("44:44:33:11:23:45", "apple") - apple_adv_2 = AdvertisementData( - local_name="apple2", - manufacturer_data={76: b"\xd8.\xad\xcd\r\x85"}, + rtx_device_2 = BLEDevice("44:44:33:11:23:45", "rtx") + rtx_adv_2 = AdvertisementData( + local_name="rtx2", + manufacturer_data={21: b"\xd8.\xad\xcd\r\x85"}, ) - inject_advertisement(hass, apple_device_2, apple_adv_2) + inject_advertisement(hass, rtx_device_2, rtx_adv_2) await hass.async_block_till_done() @@ -1636,9 +1686,9 @@ async def test_register_callback_by_local_name( assert len(callbacks) == 1 service_info: BluetoothServiceInfo = callbacks[0][0] - assert service_info.name == "apple" - assert service_info.manufacturer == "Apple, Inc." - assert service_info.manufacturer_id == 76 + assert service_info.name == "rtx" + assert service_info.manufacturer == "RTX Telecom A/S" + assert service_info.manufacturer_id == 21 async def test_register_callback_by_local_name_overly_broad( From 016a59ac94b1e732ca75dfc53021b5ff343deb79 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 4 Sep 2022 22:57:43 -0500 Subject: [PATCH 091/955] Add support for subscribing to config entry changes (#77803) --- .../components/config/config_entries.py | 50 +++ homeassistant/config_entries.py | 80 +++-- .../components/config/test_config_entries.py | 314 ++++++++++++++++++ 3 files changed, 416 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index 54132080f08..fb96a99827d 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -20,6 +20,7 @@ from homeassistant.helpers.data_entry_flow import ( FlowManagerIndexView, FlowManagerResourceView, ) +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.loader import ( Integration, IntegrationNotFound, @@ -43,6 +44,7 @@ async def async_setup(hass): websocket_api.async_register_command(hass, config_entries_get) websocket_api.async_register_command(hass, config_entry_disable) websocket_api.async_register_command(hass, config_entry_update) + websocket_api.async_register_command(hass, config_entries_subscribe) websocket_api.async_register_command(hass, config_entries_progress) websocket_api.async_register_command(hass, ignore_config_flow) @@ -408,6 +410,54 @@ async def config_entries_get( ) +@websocket_api.websocket_command( + { + vol.Required("type"): "config_entries/subscribe", + vol.Optional("type_filter"): str, + } +) +@websocket_api.async_response +async def config_entries_subscribe( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: + """Subscribe to config entry updates.""" + type_filter = msg.get("type_filter") + + async def async_forward_config_entry_changes( + change: config_entries.ConfigEntryChange, entry: config_entries.ConfigEntry + ) -> None: + """Forward config entry state events to websocket.""" + if type_filter: + integration = await async_get_integration(hass, entry.domain) + if integration.integration_type != type_filter: + return + + connection.send_message( + websocket_api.event_message( + msg["id"], + [ + { + "type": change, + "entry": entry_json(entry), + } + ], + ) + ) + + current_entries = await async_matching_config_entries(hass, type_filter, None) + connection.subscriptions[msg["id"]] = async_dispatcher_connect( + hass, + config_entries.SIGNAL_CONFIG_ENTRY_CHANGED, + async_forward_config_entry_changes, + ) + connection.send_result(msg["id"]) + connection.send_message( + websocket_api.event_message( + msg["id"], [{"type": None, "entry": entry} for entry in current_entries] + ) + ) + + async def async_matching_config_entries( hass: HomeAssistant, type_filter: str | None, domain: str | None ) -> list[dict[str, Any]]: diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index f37efb6c627..e7960646ecb 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -19,6 +19,7 @@ from .const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP, Platfo 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.event import async_call_later from .helpers.frame import report from .helpers.typing import UNDEFINED, ConfigType, DiscoveryInfoType, UndefinedType @@ -136,6 +137,16 @@ RECONFIGURE_NOTIFICATION_ID = "config_entry_reconfigure" EVENT_FLOW_DISCOVERED = "config_entry_discovered" +SIGNAL_CONFIG_ENTRY_CHANGED = "config_entry_changed" + + +class ConfigEntryChange(StrEnum): + """What was changed in a config entry.""" + + ADDED = "added" + REMOVED = "removed" + UPDATED = "updated" + class ConfigEntryDisabler(StrEnum): """What disabled a config entry.""" @@ -310,7 +321,7 @@ class ConfigEntry: # Only store setup result as state if it was not forwarded. if self.domain == integration.domain: - self.state = ConfigEntryState.SETUP_IN_PROGRESS + self.async_set_state(hass, ConfigEntryState.SETUP_IN_PROGRESS, None) self.supports_unload = await support_entry_unload(hass, self.domain) self.supports_remove_device = await support_remove_from_device( @@ -327,8 +338,7 @@ class ConfigEntry: err, ) if self.domain == integration.domain: - self.state = ConfigEntryState.SETUP_ERROR - self.reason = "Import error" + self.async_set_state(hass, ConfigEntryState.SETUP_ERROR, "Import error") return if self.domain == integration.domain: @@ -341,14 +351,12 @@ class ConfigEntry: self.domain, err, ) - self.state = ConfigEntryState.SETUP_ERROR - self.reason = "Import error" + self.async_set_state(hass, ConfigEntryState.SETUP_ERROR, "Import error") return # Perform migration if not await self.async_migrate(hass): - self.state = ConfigEntryState.MIGRATION_ERROR - self.reason = None + self.async_set_state(hass, ConfigEntryState.MIGRATION_ERROR, None) return error_reason = None @@ -378,8 +386,7 @@ class ConfigEntry: self.async_start_reauth(hass) result = False except ConfigEntryNotReady as ex: - self.state = ConfigEntryState.SETUP_RETRY - self.reason = str(ex) or None + self.async_set_state(hass, ConfigEntryState.SETUP_RETRY, str(ex) or None) wait_time = 2 ** min(tries, 4) * 5 tries += 1 message = str(ex) @@ -427,11 +434,9 @@ class ConfigEntry: return if result: - self.state = ConfigEntryState.LOADED - self.reason = None + self.async_set_state(hass, ConfigEntryState.LOADED, None) else: - self.state = ConfigEntryState.SETUP_ERROR - self.reason = error_reason + self.async_set_state(hass, ConfigEntryState.SETUP_ERROR, error_reason) async def async_shutdown(self) -> None: """Call when Home Assistant is stopping.""" @@ -452,8 +457,7 @@ class ConfigEntry: Returns if unload is possible and was successful. """ if self.source == SOURCE_IGNORE: - self.state = ConfigEntryState.NOT_LOADED - self.reason = None + self.async_set_state(hass, ConfigEntryState.NOT_LOADED, None) return True if self.state == ConfigEntryState.NOT_LOADED: @@ -467,8 +471,7 @@ class ConfigEntry: # that was uninstalled, or an integration # that has been renamed without removing the config # entry. - self.state = ConfigEntryState.NOT_LOADED - self.reason = None + self.async_set_state(hass, ConfigEntryState.NOT_LOADED, None) return True component = integration.get_component() @@ -479,17 +482,16 @@ class ConfigEntry: if self.state is not ConfigEntryState.LOADED: self.async_cancel_retry_setup() - - self.state = ConfigEntryState.NOT_LOADED - self.reason = None + self.async_set_state(hass, ConfigEntryState.NOT_LOADED, None) return True supports_unload = hasattr(component, "async_unload_entry") if not supports_unload: if integration.domain == self.domain: - self.state = ConfigEntryState.FAILED_UNLOAD - self.reason = "Unload not supported" + self.async_set_state( + hass, ConfigEntryState.FAILED_UNLOAD, "Unload not supported" + ) return False try: @@ -499,20 +501,20 @@ class ConfigEntry: # Only adjust state if we unloaded the component if result and integration.domain == self.domain: - self.state = ConfigEntryState.NOT_LOADED - self.reason = None + self.async_set_state(hass, ConfigEntryState.NOT_LOADED, None) await self._async_process_on_unload() # https://github.com/python/mypy/issues/11839 return result # type: ignore[no-any-return] - except Exception: # pylint: disable=broad-except + except Exception as ex: # pylint: disable=broad-except _LOGGER.exception( "Error unloading entry %s for %s", self.title, integration.domain ) if integration.domain == self.domain: - self.state = ConfigEntryState.FAILED_UNLOAD - self.reason = "Unknown error" + self.async_set_state( + hass, ConfigEntryState.FAILED_UNLOAD, str(ex) or "Unknown error" + ) return False async def async_remove(self, hass: HomeAssistant) -> None: @@ -541,6 +543,17 @@ class ConfigEntry: integration.domain, ) + @callback + def async_set_state( + self, hass: HomeAssistant, state: ConfigEntryState, reason: str | None + ) -> None: + """Set the state of the config entry.""" + self.state = state + self.reason = reason + async_dispatcher_send( + hass, SIGNAL_CONFIG_ENTRY_CHANGED, ConfigEntryChange.UPDATED, self + ) + async def async_migrate(self, hass: HomeAssistant) -> bool: """Migrate an entry. @@ -895,6 +908,7 @@ class ConfigEntries: ) self._entries[entry.entry_id] = entry self._domain_index.setdefault(entry.domain, []).append(entry.entry_id) + self._async_dispatch(ConfigEntryChange.ADDED, entry) await self.async_setup(entry.entry_id) self._async_schedule_save() @@ -950,6 +964,7 @@ class ConfigEntries: ) ) + self._async_dispatch(ConfigEntryChange.REMOVED, entry) return {"require_restart": not unload_success} async def _async_shutdown(self, event: Event) -> None: @@ -1161,9 +1176,18 @@ class ConfigEntries: self.hass.async_create_task(listener(self.hass, entry)) self._async_schedule_save() - + self._async_dispatch(ConfigEntryChange.UPDATED, entry) return True + @callback + def _async_dispatch( + self, change_type: ConfigEntryChange, entry: ConfigEntry + ) -> None: + """Dispatch a config entry change.""" + async_dispatcher_send( + self.hass, SIGNAL_CONFIG_ENTRY_CHANGED, change_type, entry + ) + @callback def async_setup_platforms( self, entry: ConfigEntry, platforms: Iterable[Platform | str] diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 0de4bf44401..d36f7282bdb 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -1303,3 +1303,317 @@ async def test_get_entries_ws(hass, hass_ws_client, clear_handlers): assert response["id"] == 8 assert response["success"] is False + + +async def test_subscribe_entries_ws(hass, hass_ws_client, clear_handlers): + """Test subscribe entries with the websocket api.""" + assert await async_setup_component(hass, "config", {}) + mock_integration(hass, MockModule("comp1")) + mock_integration( + hass, MockModule("comp2", partial_manifest={"integration_type": "helper"}) + ) + mock_integration(hass, MockModule("comp3")) + entry = MockConfigEntry( + domain="comp1", + title="Test 1", + source="bla", + ) + entry.add_to_hass(hass) + MockConfigEntry( + domain="comp2", + title="Test 2", + source="bla2", + state=core_ce.ConfigEntryState.SETUP_ERROR, + reason="Unsupported API", + ).add_to_hass(hass) + MockConfigEntry( + domain="comp3", + title="Test 3", + source="bla3", + disabled_by=core_ce.ConfigEntryDisabler.USER, + ).add_to_hass(hass) + + ws_client = await hass_ws_client(hass) + + await ws_client.send_json( + { + "id": 5, + "type": "config_entries/subscribe", + } + ) + response = await ws_client.receive_json() + assert response["id"] == 5 + assert response["result"] is None + assert response["success"] is True + assert response["type"] == "result" + response = await ws_client.receive_json() + assert response["id"] == 5 + assert response["event"] == [ + { + "type": None, + "entry": { + "disabled_by": None, + "domain": "comp1", + "entry_id": ANY, + "pref_disable_new_entities": False, + "pref_disable_polling": False, + "reason": None, + "source": "bla", + "state": "not_loaded", + "supports_options": False, + "supports_remove_device": False, + "supports_unload": False, + "title": "Test 1", + }, + }, + { + "type": None, + "entry": { + "disabled_by": None, + "domain": "comp2", + "entry_id": ANY, + "pref_disable_new_entities": False, + "pref_disable_polling": False, + "reason": "Unsupported API", + "source": "bla2", + "state": "setup_error", + "supports_options": False, + "supports_remove_device": False, + "supports_unload": False, + "title": "Test 2", + }, + }, + { + "type": None, + "entry": { + "disabled_by": "user", + "domain": "comp3", + "entry_id": ANY, + "pref_disable_new_entities": False, + "pref_disable_polling": False, + "reason": None, + "source": "bla3", + "state": "not_loaded", + "supports_options": False, + "supports_remove_device": False, + "supports_unload": False, + "title": "Test 3", + }, + }, + ] + assert hass.config_entries.async_update_entry(entry, title="changed") + response = await ws_client.receive_json() + assert response["id"] == 5 + assert response["event"] == [ + { + "entry": { + "disabled_by": None, + "domain": "comp1", + "entry_id": ANY, + "pref_disable_new_entities": False, + "pref_disable_polling": False, + "reason": None, + "source": "bla", + "state": "not_loaded", + "supports_options": False, + "supports_remove_device": False, + "supports_unload": False, + "title": "changed", + }, + "type": "updated", + } + ] + await hass.config_entries.async_remove(entry.entry_id) + response = await ws_client.receive_json() + assert response["id"] == 5 + assert response["event"] == [ + { + "entry": { + "disabled_by": None, + "domain": "comp1", + "entry_id": ANY, + "pref_disable_new_entities": False, + "pref_disable_polling": False, + "reason": None, + "source": "bla", + "state": "not_loaded", + "supports_options": False, + "supports_remove_device": False, + "supports_unload": False, + "title": "changed", + }, + "type": "removed", + } + ] + await hass.config_entries.async_add(entry) + response = await ws_client.receive_json() + assert response["id"] == 5 + assert response["event"] == [ + { + "entry": { + "disabled_by": None, + "domain": "comp1", + "entry_id": ANY, + "pref_disable_new_entities": False, + "pref_disable_polling": False, + "reason": None, + "source": "bla", + "state": "not_loaded", + "supports_options": False, + "supports_remove_device": False, + "supports_unload": False, + "title": "changed", + }, + "type": "added", + } + ] + + +async def test_subscribe_entries_ws_filtered(hass, hass_ws_client, clear_handlers): + """Test subscribe entries with the websocket api with a type filter.""" + assert await async_setup_component(hass, "config", {}) + mock_integration(hass, MockModule("comp1")) + mock_integration( + hass, MockModule("comp2", partial_manifest={"integration_type": "helper"}) + ) + mock_integration(hass, MockModule("comp3")) + entry = MockConfigEntry( + domain="comp1", + title="Test 1", + source="bla", + ) + entry.add_to_hass(hass) + entry2 = MockConfigEntry( + domain="comp2", + title="Test 2", + source="bla2", + state=core_ce.ConfigEntryState.SETUP_ERROR, + reason="Unsupported API", + ) + entry2.add_to_hass(hass) + MockConfigEntry( + domain="comp3", + title="Test 3", + source="bla3", + disabled_by=core_ce.ConfigEntryDisabler.USER, + ).add_to_hass(hass) + + ws_client = await hass_ws_client(hass) + + await ws_client.send_json( + { + "id": 5, + "type": "config_entries/subscribe", + "type_filter": "integration", + } + ) + response = await ws_client.receive_json() + assert response["id"] == 5 + assert response["result"] is None + assert response["success"] is True + assert response["type"] == "result" + response = await ws_client.receive_json() + assert response["id"] == 5 + assert response["event"] == [ + { + "type": None, + "entry": { + "disabled_by": None, + "domain": "comp1", + "entry_id": ANY, + "pref_disable_new_entities": False, + "pref_disable_polling": False, + "reason": None, + "source": "bla", + "state": "not_loaded", + "supports_options": False, + "supports_remove_device": False, + "supports_unload": False, + "title": "Test 1", + }, + }, + { + "type": None, + "entry": { + "disabled_by": "user", + "domain": "comp3", + "entry_id": ANY, + "pref_disable_new_entities": False, + "pref_disable_polling": False, + "reason": None, + "source": "bla3", + "state": "not_loaded", + "supports_options": False, + "supports_remove_device": False, + "supports_unload": False, + "title": "Test 3", + }, + }, + ] + assert hass.config_entries.async_update_entry(entry, title="changed") + response = await ws_client.receive_json() + assert response["id"] == 5 + assert response["event"] == [ + { + "entry": { + "disabled_by": None, + "domain": "comp1", + "entry_id": ANY, + "pref_disable_new_entities": False, + "pref_disable_polling": False, + "reason": None, + "source": "bla", + "state": "not_loaded", + "supports_options": False, + "supports_remove_device": False, + "supports_unload": False, + "title": "changed", + }, + "type": "updated", + } + ] + await hass.config_entries.async_remove(entry.entry_id) + await hass.config_entries.async_remove(entry2.entry_id) + response = await ws_client.receive_json() + assert response["id"] == 5 + assert response["event"] == [ + { + "entry": { + "disabled_by": None, + "domain": "comp1", + "entry_id": ANY, + "pref_disable_new_entities": False, + "pref_disable_polling": False, + "reason": None, + "source": "bla", + "state": "not_loaded", + "supports_options": False, + "supports_remove_device": False, + "supports_unload": False, + "title": "changed", + }, + "type": "removed", + } + ] + await hass.config_entries.async_add(entry) + response = await ws_client.receive_json() + assert response["id"] == 5 + assert response["event"] == [ + { + "entry": { + "disabled_by": None, + "domain": "comp1", + "entry_id": ANY, + "pref_disable_new_entities": False, + "pref_disable_polling": False, + "reason": None, + "source": "bla", + "state": "not_loaded", + "supports_options": False, + "supports_remove_device": False, + "supports_unload": False, + "title": "changed", + }, + "type": "added", + } + ] From 6044e2b05447ce3a0f30b2fec6e28e17b23dd3c4 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 5 Sep 2022 06:37:25 +0200 Subject: [PATCH 092/955] Improve type hints in kulersky light (#77652) --- homeassistant/components/kulersky/light.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/kulersky/light.py b/homeassistant/components/kulersky/light.py index c1cc68c9035..bfd5eec1aab 100644 --- a/homeassistant/components/kulersky/light.py +++ b/homeassistant/components/kulersky/light.py @@ -139,7 +139,7 @@ class KulerskyLight(LightEntity): """Instruct the light to turn off.""" await self._light.set_color(0, 0, 0, 0) - async def async_update(self): + async def async_update(self) -> None: """Fetch new state data for this light.""" try: if not self._available: @@ -156,8 +156,8 @@ class KulerskyLight(LightEntity): self._available = True brightness = max(rgbw) if not brightness: - rgbw_normalized = [0, 0, 0, 0] + self._attr_rgbw_color = (0, 0, 0, 0) else: - rgbw_normalized = [round(x * 255 / brightness) for x in rgbw] + rgbw_normalized = tuple(round(x * 255 / brightness) for x in rgbw) + self._attr_rgbw_color = rgbw_normalized # type:ignore[assignment] self._attr_brightness = brightness - self._attr_rgbw_color = tuple(rgbw_normalized) From ddf668d1cb45213bdc1143f4255b68aa255dffc4 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 4 Sep 2022 22:25:43 -0700 Subject: [PATCH 093/955] Remove CalendarEventDevice which was deprecated in 2022.5 (#77809) --- homeassistant/components/calendar/__init__.py | 83 ++----------------- homeassistant/components/demo/calendar.py | 48 +---------- tests/components/calendar/test_init.py | 22 ----- 3 files changed, 7 insertions(+), 146 deletions(-) diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py index 54da2a1cb02..ee1bb866e6a 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -189,71 +189,6 @@ def is_offset_reached( return start + offset_time <= dt.now(start.tzinfo) -class CalendarEventDevice(Entity): - """Legacy API for calendar event entities.""" - - def __init_subclass__(cls, **kwargs: Any) -> None: - """Print deprecation warning.""" - super().__init_subclass__(**kwargs) - _LOGGER.warning( - "CalendarEventDevice is deprecated, modify %s to extend CalendarEntity", - cls.__name__, - ) - - @property - def event(self) -> dict[str, Any] | None: - """Return the next upcoming event.""" - raise NotImplementedError() - - @final - @property - def state_attributes(self) -> dict[str, Any] | None: - """Return the entity state attributes.""" - - if (event := self.event) is None: - return None - - event = normalize_event(event) - return { - "message": event["message"], - "all_day": event["all_day"], - "start_time": event["start"], - "end_time": event["end"], - "location": event["location"], - "description": event["description"], - } - - @final - @property - def state(self) -> str: - """Return the state of the calendar event.""" - if (event := self.event) is None: - return STATE_OFF - - event = normalize_event(event) - start = event["dt_start"] - end = event["dt_end"] - - if start is None or end is None: - return STATE_OFF - - now = dt.now() - - if start <= now < end: - return STATE_ON - - return STATE_OFF - - async def async_get_events( - self, - hass: HomeAssistant, - start_date: datetime.datetime, - end_date: datetime.datetime, - ) -> list[dict[str, Any]]: - """Return calendar events within a datetime range.""" - raise NotImplementedError() - - class CalendarEntity(Entity): """Base class for calendar event entities.""" @@ -314,10 +249,14 @@ class CalendarEventView(http.HomeAssistantView): async def get(self, request: web.Request, entity_id: str) -> web.Response: """Return calendar events.""" - entity = self.component.get_entity(entity_id) + if not (entity := self.component.get_entity(entity_id)) or not isinstance( + entity, CalendarEntity + ): + return web.Response(status=HTTPStatus.BAD_REQUEST) + start = request.query.get("start") end = request.query.get("end") - if start is None or end is None or entity is None: + if start is None or end is None: return web.Response(status=HTTPStatus.BAD_REQUEST) try: start_date = dt.parse_datetime(start) @@ -327,16 +266,6 @@ class CalendarEventView(http.HomeAssistantView): if start_date is None or end_date is None: return web.Response(status=HTTPStatus.BAD_REQUEST) - # Compatibility shim for old API - if isinstance(entity, CalendarEventDevice): - event_list = await entity.async_get_events( - request.app["hass"], start_date, end_date - ) - return self.json(event_list) - - if not isinstance(entity, CalendarEntity): - return web.Response(status=HTTPStatus.BAD_REQUEST) - try: calendar_event_list = await entity.async_get_events( request.app["hass"], start_date, end_date diff --git a/homeassistant/components/demo/calendar.py b/homeassistant/components/demo/calendar.py index 415ed0dbb8d..ae546361d8f 100644 --- a/homeassistant/components/demo/calendar.py +++ b/homeassistant/components/demo/calendar.py @@ -1,16 +1,9 @@ """Demo platform that has two fake binary sensors.""" from __future__ import annotations -import copy import datetime -from typing import Any -from homeassistant.components.calendar import ( - CalendarEntity, - CalendarEvent, - CalendarEventDevice, - get_date, -) +from homeassistant.components.calendar import CalendarEntity, CalendarEvent from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -28,7 +21,6 @@ def setup_platform( [ DemoCalendar(calendar_data_future(), "Calendar 1"), DemoCalendar(calendar_data_current(), "Calendar 2"), - LegacyDemoCalendar("Calendar 3"), ] ) @@ -76,41 +68,3 @@ class DemoCalendar(CalendarEntity): ) -> list[CalendarEvent]: """Return calendar events within a datetime range.""" return [self._event] - - -class LegacyDemoCalendar(CalendarEventDevice): - """Calendar for exercising shim API.""" - - def __init__(self, name: str) -> None: - """Initialize demo calendar.""" - self._attr_name = name - one_hour_from_now = dt_util.now() + datetime.timedelta(minutes=30) - self._event = { - "start": {"dateTime": one_hour_from_now.isoformat()}, - "end": { - "dateTime": ( - one_hour_from_now + datetime.timedelta(minutes=60) - ).isoformat() - }, - "summary": "Future Event", - "description": "Future Description", - "location": "Future Location", - } - - @property - def event(self) -> dict[str, Any]: - """Return the next upcoming event.""" - return self._event - - async def async_get_events( - self, - hass: HomeAssistant, - start_date: datetime.datetime, - end_date: datetime.datetime, - ) -> list[dict[str, Any]]: - """Get all events in a specific time frame.""" - event = copy.copy(self.event) - event["title"] = event["summary"] - event["start"] = get_date(event["start"]).isoformat() - event["end"] = get_date(event["end"]).isoformat() - return [event] diff --git a/tests/components/calendar/test_init.py b/tests/components/calendar/test_init.py index 97bfd89f465..02c7f01b42d 100644 --- a/tests/components/calendar/test_init.py +++ b/tests/components/calendar/test_init.py @@ -66,26 +66,4 @@ async def test_calendars_http_api(hass, hass_client): assert data == [ {"entity_id": "calendar.calendar_1", "name": "Calendar 1"}, {"entity_id": "calendar.calendar_2", "name": "Calendar 2"}, - {"entity_id": "calendar.calendar_3", "name": "Calendar 3"}, ] - - -async def test_events_http_api_shim(hass, hass_client): - """Test the legacy shim calendar demo view.""" - await async_setup_component(hass, "calendar", {"calendar": {"platform": "demo"}}) - await hass.async_block_till_done() - client = await hass_client() - response = await client.get("/api/calendars/calendar.calendar_3") - assert response.status == HTTPStatus.BAD_REQUEST - start = dt_util.now() - end = start + timedelta(days=1) - response = await client.get( - "/api/calendars/calendar.calendar_1?start={}&end={}".format( - start.isoformat(), end.isoformat() - ) - ) - assert response.status == HTTPStatus.OK - events = await response.json() - assert events[0]["summary"] == "Future Event" - assert events[0]["description"] == "Future Description" - assert events[0]["location"] == "Future Location" From 8e0b9e1f9885bab2e176c489f89db5acb666c2d6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 5 Sep 2022 01:45:35 -0500 Subject: [PATCH 094/955] Fix isy994 calling sync api in async context (#77812) --- homeassistant/components/isy994/entity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/isy994/entity.py b/homeassistant/components/isy994/entity.py index 54ee9a2ded5..61f42a60a6e 100644 --- a/homeassistant/components/isy994/entity.py +++ b/homeassistant/components/isy994/entity.py @@ -75,7 +75,7 @@ class ISYEntity(Entity): # New state attributes may be available, update the state. self.async_write_ha_state() - self.hass.bus.fire("isy994_control", event_data) + self.hass.bus.async_fire("isy994_control", event_data) @property def device_info(self) -> DeviceInfo | None: From bbf77ed46f4380bfc7c0e228a46a9f8d6ca11cf1 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 5 Sep 2022 10:19:57 +0200 Subject: [PATCH 095/955] Adjust type hint in mediaroom (#77817) --- homeassistant/components/mediaroom/media_player.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mediaroom/media_player.py b/homeassistant/components/mediaroom/media_player.py index b3a800c489e..f3139f9c491 100644 --- a/homeassistant/components/mediaroom/media_player.py +++ b/homeassistant/components/mediaroom/media_player.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from pymediaroom import ( COMMANDS, @@ -189,27 +190,31 @@ class MediaroomDevice(MediaPlayerEntity): ) ) - async def async_play_media(self, media_type, media_id, **kwargs): + async def async_play_media( + self, media_type: str, media_id: str, **kwargs: Any + ) -> None: """Play media.""" _LOGGER.debug( "STB(%s) Play media: %s (%s)", self.stb.stb_ip, media_id, media_type ) + command: str | int if media_type == MEDIA_TYPE_CHANNEL: if not media_id.isdigit(): _LOGGER.error("Invalid media_id %s: Must be a channel number", media_id) return - media_id = int(media_id) + command = int(media_id) elif media_type == MEDIA_TYPE_MEDIAROOM: if media_id not in COMMANDS: _LOGGER.error("Invalid media_id %s: Must be a command", media_id) return + command = media_id else: _LOGGER.error("Invalid media type %s", media_type) return try: - await self.stb.send_cmd(media_id) + await self.stb.send_cmd(command) if self._optimistic: self._state = STATE_PLAYING self._available = True From 5739712a0d96f219d79627a612d6a1a902c494d5 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 5 Sep 2022 10:47:01 +0200 Subject: [PATCH 096/955] Adjust type hint in meteoalarm (#77818) --- homeassistant/components/meteoalarm/binary_sensor.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/meteoalarm/binary_sensor.py b/homeassistant/components/meteoalarm/binary_sensor.py index c4b6e49a636..246074ce585 100644 --- a/homeassistant/components/meteoalarm/binary_sensor.py +++ b/homeassistant/components/meteoalarm/binary_sensor.py @@ -74,15 +74,14 @@ class MeteoAlertBinarySensor(BinarySensorEntity): self._attr_name = name self._api = api - def update(self): + def update(self) -> None: """Update device state.""" - self._attr_extra_state_attributes = None + self._attr_extra_state_attributes = {} self._attr_is_on = False if alert := self._api.get_alert(): expiration_date = dt_util.parse_datetime(alert["expires"]) - now = dt_util.utcnow() - if expiration_date > now: + if expiration_date is not None and expiration_date > dt_util.utcnow(): self._attr_extra_state_attributes = alert self._attr_is_on = True From ce6e57da5d866e5933c4668105ab2810fbdc2833 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Mon, 5 Sep 2022 10:50:00 +0200 Subject: [PATCH 097/955] Bump fritzconnection from 1.8.0 to 1.10.1 (#77751) bump fritzconnection from 1.8.0 to 1.10.1 --- homeassistant/components/fritz/manifest.json | 2 +- homeassistant/components/fritzbox_callmonitor/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/fritz/manifest.json b/homeassistant/components/fritz/manifest.json index e5828ba76cf..b6aaf4d9896 100644 --- a/homeassistant/components/fritz/manifest.json +++ b/homeassistant/components/fritz/manifest.json @@ -2,7 +2,7 @@ "domain": "fritz", "name": "AVM FRITZ!Box Tools", "documentation": "https://www.home-assistant.io/integrations/fritz", - "requirements": ["fritzconnection==1.8.0", "xmltodict==0.13.0"], + "requirements": ["fritzconnection==1.10.1", "xmltodict==0.13.0"], "dependencies": ["network"], "codeowners": ["@mammuth", "@AaronDavidSchneider", "@chemelli74", "@mib1185"], "config_flow": true, diff --git a/homeassistant/components/fritzbox_callmonitor/manifest.json b/homeassistant/components/fritzbox_callmonitor/manifest.json index 3c89f68dc11..2d0fc96e0db 100644 --- a/homeassistant/components/fritzbox_callmonitor/manifest.json +++ b/homeassistant/components/fritzbox_callmonitor/manifest.json @@ -3,7 +3,7 @@ "name": "AVM FRITZ!Box Call Monitor", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/fritzbox_callmonitor", - "requirements": ["fritzconnection==1.8.0"], + "requirements": ["fritzconnection==1.10.1"], "codeowners": ["@cdce8p"], "iot_class": "local_polling", "loggers": ["fritzconnection"] diff --git a/requirements_all.txt b/requirements_all.txt index 3ec6026ad8f..e47fbf90259 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -705,7 +705,7 @@ freesms==0.2.0 # homeassistant.components.fritz # homeassistant.components.fritzbox_callmonitor -fritzconnection==1.8.0 +fritzconnection==1.10.1 # homeassistant.components.google_translate gTTS==2.2.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c1d2a7bf815..b0ce1392ed8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -518,7 +518,7 @@ freebox-api==0.0.10 # homeassistant.components.fritz # homeassistant.components.fritzbox_callmonitor -fritzconnection==1.8.0 +fritzconnection==1.10.1 # homeassistant.components.google_translate gTTS==2.2.4 From 6355e682fa4aeb526570597d919ad1fb76755b9a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 5 Sep 2022 10:59:36 +0200 Subject: [PATCH 098/955] Improve entity type hints [m] (#77816) --- .../components/magicseaweed/sensor.py | 2 +- .../components/maxcube/binary_sensor.py | 2 +- homeassistant/components/maxcube/climate.py | 7 +-- homeassistant/components/mazda/switch.py | 6 ++- homeassistant/components/meater/sensor.py | 2 +- .../components/mediaroom/media_player.py | 22 ++++----- homeassistant/components/melcloud/climate.py | 6 +-- homeassistant/components/melcloud/sensor.py | 2 +- .../components/melcloud/water_heater.py | 8 ++-- homeassistant/components/melissa/climate.py | 7 +-- homeassistant/components/mfi/sensor.py | 2 +- homeassistant/components/mfi/switch.py | 7 +-- homeassistant/components/mill/climate.py | 8 ++-- homeassistant/components/min_max/sensor.py | 2 +- .../components/mobile_app/device_tracker.py | 4 +- .../components/moehlenhoff_alpha2/climate.py | 3 +- .../components/mold_indicator/sensor.py | 4 +- .../components/monoprice/media_player.py | 18 +++---- .../components/motion_blinds/sensor.py | 12 ++--- homeassistant/components/mpd/media_player.py | 48 +++++++++++-------- homeassistant/components/mqtt/scene.py | 3 +- homeassistant/components/mqtt_room/sensor.py | 4 +- homeassistant/components/mvglive/sensor.py | 2 +- homeassistant/components/mystrom/switch.py | 7 +-- 24 files changed, 103 insertions(+), 85 deletions(-) diff --git a/homeassistant/components/magicseaweed/sensor.py b/homeassistant/components/magicseaweed/sensor.py index 38559f607a4..b0b2e92ada5 100644 --- a/homeassistant/components/magicseaweed/sensor.py +++ b/homeassistant/components/magicseaweed/sensor.py @@ -157,7 +157,7 @@ class MagicSeaweedSensor(SensorEntity): """Return the unit system of this entity.""" return self._unit_system - def update(self): + def update(self) -> None: """Get the latest data from Magicseaweed and updates the states.""" self.data.update() if self.hour is None: diff --git a/homeassistant/components/maxcube/binary_sensor.py b/homeassistant/components/maxcube/binary_sensor.py index f674ec38d37..c6c9ba8ebb0 100644 --- a/homeassistant/components/maxcube/binary_sensor.py +++ b/homeassistant/components/maxcube/binary_sensor.py @@ -43,7 +43,7 @@ class MaxCubeBinarySensorBase(BinarySensorEntity): self._device = device self._room = handler.cube.room_by_id(device.room_id) - def update(self): + def update(self) -> None: """Get latest data from MAX! Cube.""" self._cubehandle.update() diff --git a/homeassistant/components/maxcube/climate.py b/homeassistant/components/maxcube/climate.py index c5d04ae599c..61abde40a37 100644 --- a/homeassistant/components/maxcube/climate.py +++ b/homeassistant/components/maxcube/climate.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging import socket +from typing import Any from maxcube.device import ( MAX_DEVICE_MODE_AUTOMATIC, @@ -183,7 +184,7 @@ class MaxCubeClimate(ClimateEntity): return None return temp - def set_temperature(self, **kwargs): + def set_temperature(self, **kwargs: Any) -> None: """Set new target temperatures.""" if (temp := kwargs.get(ATTR_TEMPERATURE)) is None: raise ValueError( @@ -207,7 +208,7 @@ class MaxCubeClimate(ClimateEntity): return PRESET_AWAY return PRESET_NONE - def set_preset_mode(self, preset_mode): + def set_preset_mode(self, preset_mode: str) -> None: """Set new operation mode.""" if preset_mode == PRESET_COMFORT: self._set_target(MAX_DEVICE_MODE_MANUAL, self._device.comfort_temperature) @@ -231,6 +232,6 @@ class MaxCubeClimate(ClimateEntity): return {} return {ATTR_VALVE_POSITION: self._device.valve_position} - def update(self): + def update(self) -> None: """Get latest data from MAX! Cube.""" self._cubehandle.update() diff --git a/homeassistant/components/mazda/switch.py b/homeassistant/components/mazda/switch.py index 844585720d7..7097237bc5d 100644 --- a/homeassistant/components/mazda/switch.py +++ b/homeassistant/components/mazda/switch.py @@ -1,4 +1,6 @@ """Platform for Mazda switch integration.""" +from typing import Any + from pymazda import Client as MazdaAPIClient from homeassistant.components.switch import SwitchEntity @@ -57,13 +59,13 @@ class MazdaChargingSwitch(MazdaEntity, SwitchEntity): self.async_write_ha_state() - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Start charging the vehicle.""" await self.client.start_charging(self.vehicle_id) await self.refresh_status_and_write_state() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Stop charging the vehicle.""" await self.client.stop_charging(self.vehicle_id) diff --git a/homeassistant/components/meater/sensor.py b/homeassistant/components/meater/sensor.py index a2753a42307..8322b9a0202 100644 --- a/homeassistant/components/meater/sensor.py +++ b/homeassistant/components/meater/sensor.py @@ -215,7 +215,7 @@ class MeaterProbeTemperature( return self.entity_description.value(device) @property - def available(self): + def available(self) -> bool: """Return if entity is available.""" # See if the device was returned from the API. If not, it's offline return ( diff --git a/homeassistant/components/mediaroom/media_player.py b/homeassistant/components/mediaroom/media_player.py index f3139f9c491..6dd267e7a12 100644 --- a/homeassistant/components/mediaroom/media_player.py +++ b/homeassistant/components/mediaroom/media_player.py @@ -170,7 +170,7 @@ class MediaroomDevice(MediaPlayerEntity): """Return True if entity is available.""" return self._available - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Retrieve latest state.""" async def async_notify_received(notify): @@ -247,7 +247,7 @@ class MediaroomDevice(MediaPlayerEntity): """Channel currently playing.""" return self._channel - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Turn on the receiver.""" try: @@ -259,7 +259,7 @@ class MediaroomDevice(MediaPlayerEntity): self._available = False self.async_write_ha_state() - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Turn off the receiver.""" try: @@ -271,7 +271,7 @@ class MediaroomDevice(MediaPlayerEntity): self._available = False self.async_write_ha_state() - async def async_media_play(self): + async def async_media_play(self) -> None: """Send play command.""" try: @@ -284,7 +284,7 @@ class MediaroomDevice(MediaPlayerEntity): self._available = False self.async_write_ha_state() - async def async_media_pause(self): + async def async_media_pause(self) -> None: """Send pause command.""" try: @@ -296,7 +296,7 @@ class MediaroomDevice(MediaPlayerEntity): self._available = False self.async_write_ha_state() - async def async_media_stop(self): + async def async_media_stop(self) -> None: """Send stop command.""" try: @@ -308,7 +308,7 @@ class MediaroomDevice(MediaPlayerEntity): self._available = False self.async_write_ha_state() - async def async_media_previous_track(self): + async def async_media_previous_track(self) -> None: """Send Program Down command.""" try: @@ -320,7 +320,7 @@ class MediaroomDevice(MediaPlayerEntity): self._available = False self.async_write_ha_state() - async def async_media_next_track(self): + async def async_media_next_track(self) -> None: """Send Program Up command.""" try: @@ -332,7 +332,7 @@ class MediaroomDevice(MediaPlayerEntity): self._available = False self.async_write_ha_state() - async def async_volume_up(self): + async def async_volume_up(self) -> None: """Send volume up command.""" try: @@ -342,7 +342,7 @@ class MediaroomDevice(MediaPlayerEntity): self._available = False self.async_write_ha_state() - async def async_volume_down(self): + async def async_volume_down(self) -> None: """Send volume up command.""" try: @@ -351,7 +351,7 @@ class MediaroomDevice(MediaPlayerEntity): self._available = False self.async_write_ha_state() - async def async_mute_volume(self, mute): + async def async_mute_volume(self, mute: bool) -> None: """Send mute command.""" try: diff --git a/homeassistant/components/melcloud/climate.py b/homeassistant/components/melcloud/climate.py index a0ffe3a68bb..7f2e2e5c6ca 100644 --- a/homeassistant/components/melcloud/climate.py +++ b/homeassistant/components/melcloud/climate.py @@ -105,7 +105,7 @@ class MelCloudClimate(ClimateEntity): self.api = device self._base_device = self.api.device - async def async_update(self): + async def async_update(self) -> None: """Update state from MELCloud.""" await self.api.async_update() @@ -257,7 +257,7 @@ class AtaDeviceClimate(MelCloudClimate): """Return vertical vane position or mode.""" return self._device.vane_vertical - async def async_set_swing_mode(self, swing_mode) -> None: + async def async_set_swing_mode(self, swing_mode: str) -> None: """Set vertical vane position or mode.""" await self.async_set_vane_vertical(swing_mode) @@ -362,7 +362,7 @@ class AtwDeviceZoneClimate(MelCloudClimate): """Return the temperature we try to reach.""" return self._zone.target_temperature - async def async_set_temperature(self, **kwargs) -> None: + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" await self._zone.set_target_temperature( kwargs.get("temperature", self.target_temperature) diff --git a/homeassistant/components/melcloud/sensor.py b/homeassistant/components/melcloud/sensor.py index ed0eac98989..f9fdfa3a161 100644 --- a/homeassistant/components/melcloud/sensor.py +++ b/homeassistant/components/melcloud/sensor.py @@ -165,7 +165,7 @@ class MelDeviceSensor(SensorEntity): """Return the state of the sensor.""" return self.entity_description.value_fn(self._api) - async def async_update(self): + async def async_update(self) -> None: """Retrieve latest state.""" await self._api.async_update() diff --git a/homeassistant/components/melcloud/water_heater.py b/homeassistant/components/melcloud/water_heater.py index 58da39b1a61..49c642d562d 100644 --- a/homeassistant/components/melcloud/water_heater.py +++ b/homeassistant/components/melcloud/water_heater.py @@ -1,6 +1,8 @@ """Platform for water_heater integration.""" from __future__ import annotations +from typing import Any + from pymelcloud import DEVICE_TYPE_ATW, AtwDevice from pymelcloud.atw_device import ( PROPERTY_OPERATION_MODE, @@ -51,7 +53,7 @@ class AtwWaterHeater(WaterHeaterEntity): self._device = device self._name = device.name - async def async_update(self): + async def async_update(self) -> None: """Update state from MELCloud.""" await self._api.async_update() @@ -109,7 +111,7 @@ class AtwWaterHeater(WaterHeaterEntity): """Return the temperature we try to reach.""" return self._device.target_tank_temperature - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" await self._device.set( { @@ -119,7 +121,7 @@ class AtwWaterHeater(WaterHeaterEntity): } ) - async def async_set_operation_mode(self, operation_mode): + async def async_set_operation_mode(self, operation_mode: str) -> None: """Set new target operation mode.""" await self._device.set({PROPERTY_OPERATION_MODE: operation_mode}) diff --git a/homeassistant/components/melissa/climate.py b/homeassistant/components/melissa/climate.py index 7dae7c2dad6..56278710fd2 100644 --- a/homeassistant/components/melissa/climate.py +++ b/homeassistant/components/melissa/climate.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( @@ -135,12 +136,12 @@ class MelissaClimate(ClimateEntity): """Return the maximum supported temperature for the thermostat.""" return 30 - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" temp = kwargs.get(ATTR_TEMPERATURE) await self.async_send({self._api.TEMP: temp}) - async def async_set_fan_mode(self, fan_mode): + async def async_set_fan_mode(self, fan_mode: str) -> None: """Set fan mode.""" melissa_fan_mode = self.hass_fan_to_melissa(fan_mode) await self.async_send({self._api.FAN: melissa_fan_mode}) @@ -168,7 +169,7 @@ class MelissaClimate(ClimateEntity): ): self._cur_settings = old_value - async def async_update(self): + async def async_update(self) -> None: """Get latest data from Melissa.""" try: self._data = (await self._api.async_status(cached=True))[ diff --git a/homeassistant/components/mfi/sensor.py b/homeassistant/components/mfi/sensor.py index 34a56641d2d..de7e661d9d2 100644 --- a/homeassistant/components/mfi/sensor.py +++ b/homeassistant/components/mfi/sensor.py @@ -143,6 +143,6 @@ class MfiSensor(SensorEntity): return "State" return tag - def update(self): + def update(self) -> None: """Get the latest data.""" self._port.refresh() diff --git a/homeassistant/components/mfi/switch.py b/homeassistant/components/mfi/switch.py index 76a55e46ba1..a25d0dfc87b 100644 --- a/homeassistant/components/mfi/switch.py +++ b/homeassistant/components/mfi/switch.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from mficlient.client import FailedToLogin, MFiClient import requests @@ -94,19 +95,19 @@ class MfiSwitch(SwitchEntity): """Return true if the device is on.""" return self._port.output - def update(self): + def update(self) -> None: """Get the latest state and update the state.""" self._port.refresh() if self._target_state is not None: self._port.data["output"] = float(self._target_state) self._target_state = None - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" self._port.control(True) self._target_state = True - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" self._port.control(False) self._target_state = False diff --git a/homeassistant/components/mill/climate.py b/homeassistant/components/mill/climate.py index 44c7f980274..fbe4ad44710 100644 --- a/homeassistant/components/mill/climate.py +++ b/homeassistant/components/mill/climate.py @@ -1,4 +1,6 @@ """Support for mill wifi-enabled home heaters.""" +from typing import Any + import mill import voluptuous as vol @@ -123,7 +125,7 @@ class MillHeater(CoordinatorEntity, ClimateEntity): self._update_attr(heater) - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: return @@ -132,7 +134,7 @@ class MillHeater(CoordinatorEntity, ClimateEntity): ) await self.coordinator.async_request_refresh() - async def async_set_fan_mode(self, fan_mode): + async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new target fan mode.""" fan_status = 1 if fan_mode == FAN_ON else 0 await self.coordinator.mill_data_connection.heater_control( @@ -221,7 +223,7 @@ class LocalMillHeater(CoordinatorEntity, ClimateEntity): self._update_attr() - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: return diff --git a/homeassistant/components/min_max/sensor.py b/homeassistant/components/min_max/sensor.py index c5a51cdda7a..615aebc8e39 100644 --- a/homeassistant/components/min_max/sensor.py +++ b/homeassistant/components/min_max/sensor.py @@ -184,7 +184,7 @@ class MinMaxSensor(SensorEntity): self.count_sensors = len(self._entity_ids) self.states = {} - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Handle added to Hass.""" self.async_on_remove( async_track_state_change_event( diff --git a/homeassistant/components/mobile_app/device_tracker.py b/homeassistant/components/mobile_app/device_tracker.py index d0f1db6caff..ef1acdaf32d 100644 --- a/homeassistant/components/mobile_app/device_tracker.py +++ b/homeassistant/components/mobile_app/device_tracker.py @@ -112,7 +112,7 @@ class MobileAppEntity(TrackerEntity, RestoreEntity): """Return the device info.""" return device_info(self._entry.data) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Call when entity about to be added to Home Assistant.""" await super().async_added_to_hass() self._dispatch_unsub = async_dispatcher_connect( @@ -138,7 +138,7 @@ class MobileAppEntity(TrackerEntity, RestoreEntity): data.update({key: attr[key] for key in attr if key in ATTR_KEYS}) self._data = data - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Call when entity is being removed from hass.""" await super().async_will_remove_from_hass() diff --git a/homeassistant/components/moehlenhoff_alpha2/climate.py b/homeassistant/components/moehlenhoff_alpha2/climate.py index e14f4801661..b68e48f83c7 100644 --- a/homeassistant/components/moehlenhoff_alpha2/climate.py +++ b/homeassistant/components/moehlenhoff_alpha2/climate.py @@ -1,5 +1,6 @@ """Support for Alpha2 room control unit via Alpha2 base.""" import logging +from typing import Any from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( @@ -110,7 +111,7 @@ class Alpha2Climate(CoordinatorEntity[Alpha2BaseCoordinator], ClimateEntity): self.coordinator.data["heat_areas"][self.heat_area_id].get("T_TARGET", 0.0) ) - async def async_set_temperature(self, **kwargs) -> None: + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperatures.""" if (target_temperature := kwargs.get(ATTR_TEMPERATURE)) is None: return diff --git a/homeassistant/components/mold_indicator/sensor.py b/homeassistant/components/mold_indicator/sensor.py index db4f8d069ab..23c5e639d7f 100644 --- a/homeassistant/components/mold_indicator/sensor.py +++ b/homeassistant/components/mold_indicator/sensor.py @@ -112,7 +112,7 @@ class MoldIndicator(SensorEntity): self._indoor_hum = None self._crit_temp = None - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" @callback @@ -273,7 +273,7 @@ class MoldIndicator(SensorEntity): return hum - async def async_update(self): + async def async_update(self) -> None: """Calculate latest state.""" _LOGGER.debug("Update state for %s", self.entity_id) # check all sensors diff --git a/homeassistant/components/monoprice/media_player.py b/homeassistant/components/monoprice/media_player.py index 627d95427e2..19692b43854 100644 --- a/homeassistant/components/monoprice/media_player.py +++ b/homeassistant/components/monoprice/media_player.py @@ -142,7 +142,7 @@ class MonopriceZone(MediaPlayerEntity): self._mute = None self._update_success = True - def update(self): + def update(self) -> None: """Retrieve latest state.""" try: state = self._monoprice.zone_status(self._zone_id) @@ -165,7 +165,7 @@ class MonopriceZone(MediaPlayerEntity): self._source = None @property - def entity_registry_enabled_default(self): + def entity_registry_enabled_default(self) -> bool: """Return if the entity should be enabled when first added to the entity registry.""" return self._zone_id < 20 or self._update_success @@ -231,36 +231,36 @@ class MonopriceZone(MediaPlayerEntity): self._monoprice.restore_zone(self._snapshot) self.schedule_update_ha_state(True) - def select_source(self, source): + def select_source(self, source: str) -> None: """Set input source.""" if source not in self._source_name_id: return idx = self._source_name_id[source] self._monoprice.set_source(self._zone_id, idx) - def turn_on(self): + def turn_on(self) -> None: """Turn the media player on.""" self._monoprice.set_power(self._zone_id, True) - def turn_off(self): + def turn_off(self) -> None: """Turn the media player off.""" self._monoprice.set_power(self._zone_id, False) - def mute_volume(self, mute): + def mute_volume(self, mute: bool) -> None: """Mute (true) or unmute (false) media player.""" self._monoprice.set_mute(self._zone_id, mute) - def set_volume_level(self, volume): + def set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" self._monoprice.set_volume(self._zone_id, int(volume * 38)) - def volume_up(self): + def volume_up(self) -> None: """Volume up the media player.""" if self._volume is None: return self._monoprice.set_volume(self._zone_id, min(self._volume + 1, 38)) - def volume_down(self): + def volume_down(self) -> None: """Volume down media player.""" if self._volume is None: return diff --git a/homeassistant/components/motion_blinds/sensor.py b/homeassistant/components/motion_blinds/sensor.py index 3a6f775092e..04b7a91fe31 100644 --- a/homeassistant/components/motion_blinds/sensor.py +++ b/homeassistant/components/motion_blinds/sensor.py @@ -61,7 +61,7 @@ class MotionBatterySensor(CoordinatorEntity, SensorEntity): self._attr_unique_id = f"{blind.mac}-battery" @property - def available(self): + def available(self) -> bool: """Return True if entity is available.""" if self.coordinator.data is None: return False @@ -81,12 +81,12 @@ class MotionBatterySensor(CoordinatorEntity, SensorEntity): """Return device specific state attributes.""" return {ATTR_BATTERY_VOLTAGE: self._blind.battery_voltage} - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Subscribe to multicast pushes.""" self._blind.Register_callback(self.unique_id, self.schedule_update_ha_state) await super().async_added_to_hass() - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Unsubscribe when removed.""" self._blind.Remove_callback(self.unique_id) await super().async_will_remove_from_hass() @@ -145,7 +145,7 @@ class MotionSignalStrengthSensor(CoordinatorEntity, SensorEntity): self._attr_name = name @property - def available(self): + def available(self) -> bool: """Return True if entity is available.""" if self.coordinator.data is None: return False @@ -164,12 +164,12 @@ class MotionSignalStrengthSensor(CoordinatorEntity, SensorEntity): """Return the state of the sensor.""" return self._device.RSSI - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Subscribe to multicast pushes.""" self._device.Register_callback(self.unique_id, self.schedule_update_ha_state) await super().async_added_to_hass() - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Unsubscribe when removed.""" self._device.Remove_callback(self.unique_id) await super().async_will_remove_from_hass() diff --git a/homeassistant/components/mpd/media_player.py b/homeassistant/components/mpd/media_player.py index ecee057a653..4680870c3ec 100644 --- a/homeassistant/components/mpd/media_player.py +++ b/homeassistant/components/mpd/media_player.py @@ -6,6 +6,7 @@ from datetime import timedelta import hashlib import logging import os +from typing import Any import mpd from mpd.asyncio import MPDClient @@ -18,6 +19,7 @@ from homeassistant.components.media_player import ( MediaPlayerEntityFeature, ) from homeassistant.components.media_player.browse_media import ( + BrowseMedia, async_process_play_media_url, ) from homeassistant.components.media_player.const import ( @@ -164,7 +166,7 @@ class MpdDevice(MediaPlayerEntity): """Return true if MPD is available and connected.""" return self._is_connected - async def async_update(self): + async def async_update(self) -> None: """Get the latest data and update the state.""" try: if not self._is_connected: @@ -273,7 +275,7 @@ class MpdDevice(MediaPlayerEntity): """Hash value for media image.""" return self._media_image_hash - async def async_get_media_image(self): + async def async_get_media_image(self) -> tuple[bytes | None, str | None]: """Fetch media image of current playing track.""" if not (file := self._currentsong.get("file")): return None, None @@ -380,12 +382,12 @@ class MpdDevice(MediaPlayerEntity): """Return the list of available input sources.""" return self._playlists - async def async_select_source(self, source): + async def async_select_source(self, source: str) -> None: """Choose a different available playlist and play it.""" await self.async_play_media(MEDIA_TYPE_PLAYLIST, source) @Throttle(PLAYLIST_UPDATE_INTERVAL) - async def _update_playlists(self, **kwargs): + async def _update_playlists(self, **kwargs: Any) -> None: """Update available MPD playlists.""" try: self._playlists = [] @@ -395,12 +397,12 @@ class MpdDevice(MediaPlayerEntity): self._playlists = None _LOGGER.warning("Playlists could not be updated: %s:", error) - async def async_set_volume_level(self, volume): + async def async_set_volume_level(self, volume: float) -> None: """Set volume of media player.""" if "volume" in self._status: await self._client.setvol(int(volume * 100)) - async def async_volume_up(self): + async def async_volume_up(self) -> None: """Service to send the MPD the command for volume up.""" if "volume" in self._status: current_volume = int(self._status["volume"]) @@ -408,7 +410,7 @@ class MpdDevice(MediaPlayerEntity): if current_volume <= 100: self._client.setvol(current_volume + 5) - async def async_volume_down(self): + async def async_volume_down(self) -> None: """Service to send the MPD the command for volume down.""" if "volume" in self._status: current_volume = int(self._status["volume"]) @@ -416,30 +418,30 @@ class MpdDevice(MediaPlayerEntity): if current_volume >= 0: await self._client.setvol(current_volume - 5) - async def async_media_play(self): + async def async_media_play(self) -> None: """Service to send the MPD the command for play/pause.""" if self._status["state"] == "pause": await self._client.pause(0) else: await self._client.play() - async def async_media_pause(self): + async def async_media_pause(self) -> None: """Service to send the MPD the command for play/pause.""" await self._client.pause(1) - async def async_media_stop(self): + async def async_media_stop(self) -> None: """Service to send the MPD the command for stop.""" await self._client.stop() - async def async_media_next_track(self): + async def async_media_next_track(self) -> None: """Service to send the MPD the command for next track.""" await self._client.next() - async def async_media_previous_track(self): + async def async_media_previous_track(self) -> None: """Service to send the MPD the command for previous track.""" await self._client.previous() - async def async_mute_volume(self, mute): + async def async_mute_volume(self, mute: bool) -> None: """Mute. Emulated with set_volume_level.""" if "volume" in self._status: if mute: @@ -449,7 +451,9 @@ class MpdDevice(MediaPlayerEntity): await self.async_set_volume_level(self._muted_volume) self._muted = mute - async def async_play_media(self, media_type, media_id, **kwargs): + async def async_play_media( + self, media_type: str, media_id: str, **kwargs: Any + ) -> None: """Send the media player the command for playing a playlist.""" if media_source.is_media_source_id(media_id): media_type = MEDIA_TYPE_MUSIC @@ -483,7 +487,7 @@ class MpdDevice(MediaPlayerEntity): return REPEAT_MODE_ALL return REPEAT_MODE_OFF - async def async_set_repeat(self, repeat): + async def async_set_repeat(self, repeat: str) -> None: """Set repeat mode.""" if repeat == REPEAT_MODE_OFF: await self._client.repeat(0) @@ -500,28 +504,30 @@ class MpdDevice(MediaPlayerEntity): """Boolean if shuffle is enabled.""" return bool(int(self._status["random"])) - async def async_set_shuffle(self, shuffle): + async def async_set_shuffle(self, shuffle: bool) -> None: """Enable/disable shuffle mode.""" await self._client.random(int(shuffle)) - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Service to send the MPD the command to stop playing.""" await self._client.stop() - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Service to send the MPD the command to start playing.""" await self._client.play() await self._update_playlists(no_throttle=True) - async def async_clear_playlist(self): + async def async_clear_playlist(self) -> None: """Clear players playlist.""" await self._client.clear() - async def async_media_seek(self, position): + async def async_media_seek(self, position: float) -> None: """Send seek command.""" await self._client.seekcur(position) - async def async_browse_media(self, media_content_type=None, media_content_id=None): + async def async_browse_media( + self, media_content_type: str | None = None, media_content_id: str | None = None + ) -> BrowseMedia: """Implement the websocket media browsing helper.""" return await media_source.async_browse_media( self.hass, diff --git a/homeassistant/components/mqtt/scene.py b/homeassistant/components/mqtt/scene.py index 62de54505eb..70a4cad7f37 100644 --- a/homeassistant/components/mqtt/scene.py +++ b/homeassistant/components/mqtt/scene.py @@ -2,6 +2,7 @@ from __future__ import annotations import functools +from typing import Any import voluptuous as vol @@ -125,7 +126,7 @@ class MqttScene( async def _subscribe_topics(self): """(Re)Subscribe to topics.""" - async def async_activate(self, **kwargs): + async def async_activate(self, **kwargs: Any) -> None: """Activate the scene. This method is a coroutine. diff --git a/homeassistant/components/mqtt_room/sensor.py b/homeassistant/components/mqtt_room/sensor.py index 276695d8edd..c1461157886 100644 --- a/homeassistant/components/mqtt_room/sensor.py +++ b/homeassistant/components/mqtt_room/sensor.py @@ -95,7 +95,7 @@ class MQTTRoomSensor(SensorEntity): self._distance = None self._updated = None - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Subscribe to MQTT events.""" @callback @@ -152,7 +152,7 @@ class MQTTRoomSensor(SensorEntity): """Return the current room of the entity.""" return self._state - def update(self): + def update(self) -> None: """Update the state for absent devices.""" if ( self._updated diff --git a/homeassistant/components/mvglive/sensor.py b/homeassistant/components/mvglive/sensor.py index d3405d0cce7..089d7ac5fbc 100644 --- a/homeassistant/components/mvglive/sensor.py +++ b/homeassistant/components/mvglive/sensor.py @@ -141,7 +141,7 @@ class MVGLiveSensor(SensorEntity): """Return the unit this state is expressed in.""" return TIME_MINUTES - def update(self): + def update(self) -> None: """Get the latest data and update the state.""" self.data.update() if not self.data.departures: diff --git a/homeassistant/components/mystrom/switch.py b/homeassistant/components/mystrom/switch.py index 41303b25a9f..7bce3000424 100644 --- a/homeassistant/components/mystrom/switch.py +++ b/homeassistant/components/mystrom/switch.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from pymystrom.exceptions import MyStromConnectionError from pymystrom.switch import MyStromSwitch as _MyStromSwitch @@ -77,21 +78,21 @@ class MyStromSwitch(SwitchEntity): """Could the device be accessed during the last update call.""" return self._available - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" try: await self.plug.turn_on() except MyStromConnectionError: _LOGGER.error("No route to myStrom plug") - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" try: await self.plug.turn_off() except MyStromConnectionError: _LOGGER.error("No route to myStrom plug") - async def async_update(self): + async def async_update(self) -> None: """Get the latest data from the device and update the data.""" try: await self.plug.get_state() From 363f95c9540ccbb3f4a645e3fe6d2867e435bc71 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 5 Sep 2022 05:13:05 -0500 Subject: [PATCH 099/955] Remove auto lowercasing from async_track_entity_registry_updated_event (#77740) --- homeassistant/helpers/event.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 2a34773a413..1cea1860b38 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -344,10 +344,14 @@ def async_track_entity_registry_updated_event( ) -> CALLBACK_TYPE: """Track specific entity registry updated events indexed by entity_id. + Entities must be lower case. + Similar to async_track_state_change_event. """ - if not (entity_ids := _async_string_to_lower_list(entity_ids)): + if not entity_ids: return _remove_empty_listener + if isinstance(entity_ids, str): + entity_ids = [entity_ids] entity_callbacks: dict[str, list[HassJob[[Event], Any]]] = hass.data.setdefault( TRACK_ENTITY_REGISTRY_UPDATED_CALLBACKS, {} From d6cf416c63491183199ac29b2c1fb13b54038b37 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Mon, 5 Sep 2022 06:15:14 -0400 Subject: [PATCH 100/955] Fix device info for zwave_js device entities (#77821) --- homeassistant/components/zwave_js/button.py | 10 ++++------ homeassistant/components/zwave_js/helpers.py | 13 +++++++++++++ homeassistant/components/zwave_js/sensor.py | 10 ++++------ homeassistant/components/zwave_js/update.py | 13 +++---------- 4 files changed, 24 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/zwave_js/button.py b/homeassistant/components/zwave_js/button.py index cef64f1724a..1d97ed05da5 100644 --- a/homeassistant/components/zwave_js/button.py +++ b/homeassistant/components/zwave_js/button.py @@ -9,11 +9,11 @@ from homeassistant.components.button import ButtonEntity 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, EntityCategory +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DATA_CLIENT, DOMAIN, LOGGER -from .helpers import get_device_id, get_valueless_base_unique_id +from .helpers import get_device_info, get_valueless_base_unique_id PARALLEL_UPDATES = 0 @@ -58,10 +58,8 @@ class ZWaveNodePingButton(ButtonEntity): self._attr_name = f"{name}: Ping" self._base_unique_id = get_valueless_base_unique_id(driver, node) self._attr_unique_id = f"{self._base_unique_id}.ping" - # device is precreated in main handler - self._attr_device_info = DeviceInfo( - identifiers={get_device_id(driver, node)}, - ) + # device may not be precreated in main handler yet + self._attr_device_info = get_device_info(driver, node) async def async_poll_value(self, _: bool) -> None: """Poll a value.""" diff --git a/homeassistant/components/zwave_js/helpers.py b/homeassistant/components/zwave_js/helpers.py index c047a3a9903..6175b7db353 100644 --- a/homeassistant/components/zwave_js/helpers.py +++ b/homeassistant/components/zwave_js/helpers.py @@ -30,6 +30,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.typing import ConfigType from .const import ( @@ -413,3 +414,15 @@ def get_value_state_schema( vol.Coerce(int), vol.Range(min=value.metadata.min, max=value.metadata.max), ) + + +def get_device_info(driver: Driver, node: ZwaveNode) -> DeviceInfo: + """Get DeviceInfo for node.""" + return DeviceInfo( + identifiers={get_device_id(driver, node)}, + sw_version=node.firmware_version, + name=node.name or node.device_config.description or f"Node {node.node_id}", + model=node.device_config.label, + manufacturer=node.device_config.manufacturer, + suggested_area=node.location if node.location else None, + ) diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index 22fbfdab728..75d8066d595 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -28,7 +28,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_platform from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo, EntityCategory +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( @@ -63,7 +63,7 @@ from .discovery_data_template import ( NumericSensorDataTemplateData, ) from .entity import ZWaveBaseEntity -from .helpers import get_device_id, get_valueless_base_unique_id +from .helpers import get_device_info, get_valueless_base_unique_id PARALLEL_UPDATES = 0 @@ -493,10 +493,8 @@ class ZWaveNodeStatusSensor(SensorEntity): self._attr_name = f"{name}: Node Status" self._base_unique_id = get_valueless_base_unique_id(driver, node) self._attr_unique_id = f"{self._base_unique_id}.node_status" - # device is precreated in main handler - self._attr_device_info = DeviceInfo( - identifiers={get_device_id(driver, self.node)}, - ) + # device may not be precreated in main handler yet + self._attr_device_info = get_device_info(driver, node) self._attr_native_value: str = node.status.name.lower() async def async_poll_value(self, _: bool) -> None: diff --git a/homeassistant/components/zwave_js/update.py b/homeassistant/components/zwave_js/update.py index 1f04c3acc47..7f25788e0be 100644 --- a/homeassistant/components/zwave_js/update.py +++ b/homeassistant/components/zwave_js/update.py @@ -19,11 +19,11 @@ 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, EntityCategory +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import API_KEY_FIRMWARE_UPDATE_SERVICE, DATA_CLIENT, DOMAIN, LOGGER -from .helpers import get_device_id, get_valueless_base_unique_id +from .helpers import get_device_info, get_valueless_base_unique_id PARALLEL_UPDATES = 1 SCAN_INTERVAL = timedelta(days=1) @@ -75,14 +75,7 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): self._base_unique_id = get_valueless_base_unique_id(driver, node) self._attr_unique_id = f"{self._base_unique_id}.firmware_update" # device may not be precreated in main handler yet - self._attr_device_info = DeviceInfo( - identifiers={get_device_id(driver, node)}, - sw_version=node.firmware_version, - name=node.name or node.device_config.description or f"Node {node.node_id}", - model=node.device_config.label, - manufacturer=node.device_config.manufacturer, - suggested_area=node.location if node.location else None, - ) + self._attr_device_info = get_device_info(driver, node) self._attr_installed_version = self._attr_latest_version = node.firmware_version From 42393db9f33d6abc0fd5d5812f81c23964c349ed Mon Sep 17 00:00:00 2001 From: Ernst Klamer Date: Mon, 5 Sep 2022 13:52:50 +0200 Subject: [PATCH 101/955] Rename BThome to BTHome (#77807) Co-authored-by: Paulus Schoutsen Co-authored-by: J. Nick Koston --- homeassistant/components/bthome/__init__.py | 10 +++++----- homeassistant/components/bthome/config_flow.py | 10 +++++----- homeassistant/components/bthome/const.py | 2 +- homeassistant/components/bthome/device.py | 2 +- homeassistant/components/bthome/manifest.json | 4 ++-- homeassistant/components/bthome/sensor.py | 10 +++++----- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/bthome/__init__.py | 2 +- tests/components/bthome/test_config_flow.py | 6 +++--- tests/components/bthome/test_sensor.py | 2 +- 11 files changed, 26 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/bthome/__init__.py b/homeassistant/components/bthome/__init__.py index 4cc3b5cf4da..93ebd7b288f 100644 --- a/homeassistant/components/bthome/__init__.py +++ b/homeassistant/components/bthome/__init__.py @@ -1,9 +1,9 @@ -"""The BThome Bluetooth integration.""" +"""The BTHome Bluetooth integration.""" from __future__ import annotations import logging -from bthome_ble import BThomeBluetoothDeviceData, SensorUpdate +from bthome_ble import BTHomeBluetoothDeviceData, SensorUpdate from bthome_ble.parser import EncryptionScheme from homeassistant.components.bluetooth import ( @@ -27,7 +27,7 @@ _LOGGER = logging.getLogger(__name__) def process_service_info( hass: HomeAssistant, entry: ConfigEntry, - data: BThomeBluetoothDeviceData, + data: BTHomeBluetoothDeviceData, service_info: BluetoothServiceInfoBleak, ) -> SensorUpdate: """Process a BluetoothServiceInfoBleak, running side effects and returning sensor data.""" @@ -40,14 +40,14 @@ def process_service_info( async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Set up BThome Bluetooth from a config entry.""" + """Set up BTHome Bluetooth from a config entry.""" address = entry.unique_id assert address is not None kwargs = {} if bindkey := entry.data.get("bindkey"): kwargs["bindkey"] = bytes.fromhex(bindkey) - data = BThomeBluetoothDeviceData(**kwargs) + data = BTHomeBluetoothDeviceData(**kwargs) coordinator = hass.data.setdefault(DOMAIN, {})[ entry.entry_id diff --git a/homeassistant/components/bthome/config_flow.py b/homeassistant/components/bthome/config_flow.py index e8e49cab566..6514f2c5396 100644 --- a/homeassistant/components/bthome/config_flow.py +++ b/homeassistant/components/bthome/config_flow.py @@ -1,11 +1,11 @@ -"""Config flow for BThome Bluetooth integration.""" +"""Config flow for BTHome Bluetooth integration.""" from __future__ import annotations from collections.abc import Mapping import dataclasses from typing import Any -from bthome_ble import BThomeBluetoothDeviceData as DeviceData +from bthome_ble import BTHomeBluetoothDeviceData as DeviceData from bthome_ble.parser import EncryptionScheme import voluptuous as vol @@ -34,8 +34,8 @@ def _title(discovery_info: BluetoothServiceInfo, device: DeviceData) -> str: return device.title or device.get_device_name() or discovery_info.name -class BThomeConfigFlow(ConfigFlow, domain=DOMAIN): - """Handle a config flow for BThome Bluetooth.""" +class BTHomeConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for BTHome Bluetooth.""" VERSION = 1 @@ -68,7 +68,7 @@ class BThomeConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_get_encryption_key( self, user_input: dict[str, Any] | None = None ) -> FlowResult: - """Enter a bindkey for an encrypted BThome device.""" + """Enter a bindkey for an encrypted BTHome device.""" assert self._discovery_info assert self._discovered_device diff --git a/homeassistant/components/bthome/const.py b/homeassistant/components/bthome/const.py index e397e288071..e46aa50e148 100644 --- a/homeassistant/components/bthome/const.py +++ b/homeassistant/components/bthome/const.py @@ -1,3 +1,3 @@ -"""Constants for the BThome Bluetooth integration.""" +"""Constants for the BTHome Bluetooth integration.""" DOMAIN = "bthome" diff --git a/homeassistant/components/bthome/device.py b/homeassistant/components/bthome/device.py index f16b2f49998..bd011752db1 100644 --- a/homeassistant/components/bthome/device.py +++ b/homeassistant/components/bthome/device.py @@ -1,4 +1,4 @@ -"""Support for BThome Bluetooth devices.""" +"""Support for BTHome Bluetooth devices.""" from __future__ import annotations from bthome_ble import DeviceKey, SensorDeviceInfo diff --git a/homeassistant/components/bthome/manifest.json b/homeassistant/components/bthome/manifest.json index bdb4b75bfa9..597d52c72e4 100644 --- a/homeassistant/components/bthome/manifest.json +++ b/homeassistant/components/bthome/manifest.json @@ -1,6 +1,6 @@ { "domain": "bthome", - "name": "BThome", + "name": "BTHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/bthome", "bluetooth": [ @@ -13,7 +13,7 @@ "service_data_uuid": "0000181e-0000-1000-8000-00805f9b34fb" } ], - "requirements": ["bthome-ble==0.5.2"], + "requirements": ["bthome-ble==1.0.0"], "dependencies": ["bluetooth"], "codeowners": ["@Ernst79"], "iot_class": "local_push" diff --git a/homeassistant/components/bthome/sensor.py b/homeassistant/components/bthome/sensor.py index 71601fa24c0..a0068596b01 100644 --- a/homeassistant/components/bthome/sensor.py +++ b/homeassistant/components/bthome/sensor.py @@ -1,4 +1,4 @@ -"""Support for BThome sensors.""" +"""Support for BTHome sensors.""" from __future__ import annotations from typing import Optional, Union @@ -202,26 +202,26 @@ async def async_setup_entry( entry: config_entries.ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up the BThome BLE sensors.""" + """Set up the BTHome 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( - BThomeBluetoothSensorEntity, async_add_entities + BTHomeBluetoothSensorEntity, async_add_entities ) ) entry.async_on_unload(coordinator.async_register_processor(processor)) -class BThomeBluetoothSensorEntity( +class BTHomeBluetoothSensorEntity( PassiveBluetoothProcessorEntity[ PassiveBluetoothDataProcessor[Optional[Union[float, int]]] ], SensorEntity, ): - """Representation of a BThome BLE sensor.""" + """Representation of a BTHome BLE sensor.""" @property def native_value(self) -> int | float | None: diff --git a/requirements_all.txt b/requirements_all.txt index e47fbf90259..97503052108 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -464,7 +464,7 @@ bsblan==0.5.0 bt_proximity==0.2.1 # homeassistant.components.bthome -bthome-ble==0.5.2 +bthome-ble==1.0.0 # homeassistant.components.bt_home_hub_5 bthomehub5-devicelist==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b0ce1392ed8..213d10a20b4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -365,7 +365,7 @@ brunt==1.2.0 bsblan==0.5.0 # homeassistant.components.bthome -bthome-ble==0.5.2 +bthome-ble==1.0.0 # homeassistant.components.buienradar buienradar==1.0.5 diff --git a/tests/components/bthome/__init__.py b/tests/components/bthome/__init__.py index be59cd7e8cb..e480c0a3810 100644 --- a/tests/components/bthome/__init__.py +++ b/tests/components/bthome/__init__.py @@ -1,4 +1,4 @@ -"""Tests for the BThome integration.""" +"""Tests for the BTHome integration.""" from bleak.backends.device import BLEDevice from bleak.backends.scanner import AdvertisementData diff --git a/tests/components/bthome/test_config_flow.py b/tests/components/bthome/test_config_flow.py index fd8f8dfaa35..64a298e3460 100644 --- a/tests/components/bthome/test_config_flow.py +++ b/tests/components/bthome/test_config_flow.py @@ -1,8 +1,8 @@ -"""Test the BThome config flow.""" +"""Test the BTHome config flow.""" from unittest.mock import patch -from bthome_ble import BThomeBluetoothDeviceData as DeviceData +from bthome_ble import BTHomeBluetoothDeviceData as DeviceData from homeassistant import config_entries from homeassistant.components.bluetooth import BluetoothChange @@ -167,7 +167,7 @@ async def test_async_step_user_no_devices_found_2(hass): """ Test setup from service info cache with no devices found. - This variant tests with a non-BThome device known to us. + This variant tests with a non-BTHome device known to us. """ with patch( "homeassistant.components.xiaomi_ble.config_flow.async_discovered_service_info", diff --git a/tests/components/bthome/test_sensor.py b/tests/components/bthome/test_sensor.py index f73d3bf379c..bb0c5b3f459 100644 --- a/tests/components/bthome/test_sensor.py +++ b/tests/components/bthome/test_sensor.py @@ -1,4 +1,4 @@ -"""Test the BThome sensors.""" +"""Test the BTHome sensors.""" from unittest.mock import patch From 420733a064286cfe6fc5cf11483835d15ff83462 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 5 Sep 2022 14:53:55 +0200 Subject: [PATCH 102/955] Improve entity type hints [n] (#77824) --- homeassistant/components/nad/media_player.py | 32 +++++++++---------- .../nederlandse_spoorwegen/sensor.py | 2 +- .../components/ness_alarm/binary_sensor.py | 2 +- homeassistant/components/netdata/sensor.py | 8 ++--- homeassistant/components/netgear/switch.py | 5 +-- homeassistant/components/netio/switch.py | 9 +++--- .../components/neurio_energy/sensor.py | 2 +- homeassistant/components/nexia/climate.py | 18 ++++++----- homeassistant/components/nextbus/sensor.py | 2 +- .../components/nextcloud/binary_sensor.py | 2 +- homeassistant/components/nextcloud/sensor.py | 2 +- homeassistant/components/nina/config_flow.py | 4 ++- homeassistant/components/nmbs/sensor.py | 4 +-- homeassistant/components/noaa_tides/sensor.py | 2 +- homeassistant/components/nuheat/climate.py | 9 +++--- .../components/nuki/binary_sensor.py | 2 +- .../components/numato/binary_sensor.py | 4 +-- homeassistant/components/numato/sensor.py | 2 +- homeassistant/components/numato/switch.py | 5 +-- homeassistant/components/nws/sensor.py | 2 +- homeassistant/components/nws/weather.py | 4 +-- homeassistant/components/nzbget/switch.py | 6 ++-- 22 files changed, 69 insertions(+), 59 deletions(-) diff --git a/homeassistant/components/nad/media_player.py b/homeassistant/components/nad/media_player.py index f031175a321..6304109c325 100644 --- a/homeassistant/components/nad/media_player.py +++ b/homeassistant/components/nad/media_player.py @@ -133,34 +133,34 @@ class NAD(MediaPlayerEntity): """Boolean if volume is currently muted.""" return self._mute - def turn_off(self): + def turn_off(self) -> None: """Turn the media player off.""" self._nad_receiver.main_power("=", "Off") - def turn_on(self): + def turn_on(self) -> None: """Turn the media player on.""" self._nad_receiver.main_power("=", "On") - def volume_up(self): + def volume_up(self) -> None: """Volume up the media player.""" self._nad_receiver.main_volume("+") - def volume_down(self): + def volume_down(self) -> None: """Volume down the media player.""" self._nad_receiver.main_volume("-") - def set_volume_level(self, volume): + def set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" self._nad_receiver.main_volume("=", self.calc_db(volume)) - def mute_volume(self, mute): + def mute_volume(self, mute: bool) -> None: """Mute (true) or unmute (false) media player.""" if mute: self._nad_receiver.main_mute("=", "On") else: self._nad_receiver.main_mute("=", "Off") - def select_source(self, source): + def select_source(self, source: str) -> None: """Select input source.""" self._nad_receiver.main_source("=", self._reverse_mapping.get(source)) @@ -175,7 +175,7 @@ class NAD(MediaPlayerEntity): return sorted(self._reverse_mapping) @property - def available(self): + def available(self) -> bool: """Return if device is available.""" return self._state is not None @@ -257,37 +257,37 @@ class NADtcp(MediaPlayerEntity): """Boolean if volume is currently muted.""" return self._mute - def turn_off(self): + def turn_off(self) -> None: """Turn the media player off.""" self._nad_receiver.power_off() - def turn_on(self): + def turn_on(self) -> None: """Turn the media player on.""" self._nad_receiver.power_on() - def volume_up(self): + def volume_up(self) -> None: """Step volume up in the configured increments.""" self._nad_receiver.set_volume(self._nad_volume + 2 * self._volume_step) - def volume_down(self): + def volume_down(self) -> None: """Step volume down in the configured increments.""" self._nad_receiver.set_volume(self._nad_volume - 2 * self._volume_step) - def set_volume_level(self, volume): + def set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" nad_volume_to_set = int( round(volume * (self._max_vol - self._min_vol) + self._min_vol) ) self._nad_receiver.set_volume(nad_volume_to_set) - def mute_volume(self, mute): + def mute_volume(self, mute: bool) -> None: """Mute (true) or unmute (false) media player.""" if mute: self._nad_receiver.mute() else: self._nad_receiver.unmute() - def select_source(self, source): + def select_source(self, source: str) -> None: """Select input source.""" self._nad_receiver.select_source(source) @@ -301,7 +301,7 @@ class NADtcp(MediaPlayerEntity): """List of available input sources.""" return self._nad_receiver.available_sources() - def update(self): + def update(self) -> None: """Get the latest details from the device.""" try: nad_status = self._nad_receiver.status() diff --git a/homeassistant/components/nederlandse_spoorwegen/sensor.py b/homeassistant/components/nederlandse_spoorwegen/sensor.py index 84fd1f0569b..063fd12f5e0 100644 --- a/homeassistant/components/nederlandse_spoorwegen/sensor.py +++ b/homeassistant/components/nederlandse_spoorwegen/sensor.py @@ -219,7 +219,7 @@ class NSDepartureSensor(SensorEntity): return attributes @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): + def update(self) -> None: """Get the trip information.""" # If looking for a specific trip time, update around that trip time only. diff --git a/homeassistant/components/ness_alarm/binary_sensor.py b/homeassistant/components/ness_alarm/binary_sensor.py index 4855ce28b72..117d65b0940 100644 --- a/homeassistant/components/ness_alarm/binary_sensor.py +++ b/homeassistant/components/ness_alarm/binary_sensor.py @@ -55,7 +55,7 @@ class NessZoneBinarySensor(BinarySensorEntity): self._type = zone_type self._state = 0 - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" self.async_on_remove( async_dispatcher_connect( diff --git a/homeassistant/components/netdata/sensor.py b/homeassistant/components/netdata/sensor.py index 97007ec076f..8c51a3fd9a6 100644 --- a/homeassistant/components/netdata/sensor.py +++ b/homeassistant/components/netdata/sensor.py @@ -140,11 +140,11 @@ class NetdataSensor(SensorEntity): return self._state @property - def available(self): + def available(self) -> bool: """Could the resource be accessed during the last update call.""" return self.netdata.available - async def async_update(self): + async def async_update(self) -> None: """Get the latest data from Netdata REST API.""" await self.netdata.async_update() resource_data = self.netdata.api.metrics.get(self._sensor) @@ -186,11 +186,11 @@ class NetdataAlarms(SensorEntity): return "mdi:crosshairs-question" @property - def available(self): + def available(self) -> bool: """Could the resource be accessed during the last update call.""" return self.netdata.available - async def async_update(self): + async def async_update(self) -> None: """Get the latest alarms from Netdata REST API.""" await self.netdata.async_update() alarms = self.netdata.api.alarms["alarms"] diff --git a/homeassistant/components/netgear/switch.py b/homeassistant/components/netgear/switch.py index b38179ccb2a..7eab606382d 100644 --- a/homeassistant/components/netgear/switch.py +++ b/homeassistant/components/netgear/switch.py @@ -1,5 +1,6 @@ """Support for Netgear switches.""" import logging +from typing import Any from pynetgear import ALLOW, BLOCK @@ -87,12 +88,12 @@ class NetgearAllowBlock(NetgearDeviceEntity, SwitchEntity): """Return true if switch is on.""" return self._state - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" await self._router.async_allow_block_device(self._mac, ALLOW) await self.coordinator.async_request_refresh() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" await self._router.async_allow_block_device(self._mac, BLOCK) await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/netio/switch.py b/homeassistant/components/netio/switch.py index cfd736c9538..546aa07e22d 100644 --- a/homeassistant/components/netio/switch.py +++ b/homeassistant/components/netio/switch.py @@ -4,6 +4,7 @@ from __future__ import annotations from collections import namedtuple from datetime import timedelta import logging +from typing import Any from pynetio import Netio import voluptuous as vol @@ -148,15 +149,15 @@ class NetioSwitch(SwitchEntity): return self._name @property - def available(self): + def available(self) -> bool: """Return true if entity is available.""" return not hasattr(self, "telnet") - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn switch on.""" self._set(True) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn switch off.""" self._set(False) @@ -172,6 +173,6 @@ class NetioSwitch(SwitchEntity): """Return the switch's status.""" return self.netio.states[int(self.outlet) - 1] - def update(self): + def update(self) -> None: """Update the state.""" self.netio.update() diff --git a/homeassistant/components/neurio_energy/sensor.py b/homeassistant/components/neurio_energy/sensor.py index 8270e89a33c..a1f6791fa5a 100644 --- a/homeassistant/components/neurio_energy/sensor.py +++ b/homeassistant/components/neurio_energy/sensor.py @@ -177,7 +177,7 @@ class NeurioEnergy(SensorEntity): """Icon to use in the frontend, if any.""" return ICON - def update(self): + def update(self) -> None: """Get the latest data, update state.""" self.update_sensor() diff --git a/homeassistant/components/nexia/climate.py b/homeassistant/components/nexia/climate.py index 33ad91e1561..7a487d0b975 100644 --- a/homeassistant/components/nexia/climate.py +++ b/homeassistant/components/nexia/climate.py @@ -1,6 +1,8 @@ """Support for Nexia / Trane XL thermostats.""" from __future__ import annotations +from typing import Any + from nexia.const import ( HOLD_PERMANENT, HOLD_RESUME_SCHEDULE, @@ -195,7 +197,7 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity): """Return the fan setting.""" return self._thermostat.get_fan_mode() - async def async_set_fan_mode(self, fan_mode): + async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new target fan mode.""" await self._thermostat.set_fan_mode(fan_mode) self._signal_thermostat_update() @@ -216,7 +218,7 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity): """Preset that is active.""" return self._zone.get_preset() - async def async_set_humidity(self, humidity): + async def async_set_humidity(self, humidity: int) -> None: """Dehumidify target.""" if self._thermostat.has_dehumidify_support(): await self.async_set_dehumidify_setpoint(humidity) @@ -303,7 +305,7 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity): return NEXIA_TO_HA_HVAC_MODE_MAP[mode] - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs: Any) -> None: """Set target temperature.""" new_heat_temp = kwargs.get(ATTR_TARGET_TEMP_LOW) new_cool_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH) @@ -364,27 +366,27 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity): attrs[ATTR_HUMIDIFY_SETPOINT] = humdify_setpoint return attrs - async def async_set_preset_mode(self, preset_mode: str): + async def async_set_preset_mode(self, preset_mode: str) -> None: """Set the preset mode.""" await self._zone.set_preset(preset_mode) self._signal_zone_update() - async def async_turn_aux_heat_off(self): + async def async_turn_aux_heat_off(self) -> None: """Turn Aux Heat off.""" await self._thermostat.set_emergency_heat(False) self._signal_thermostat_update() - async def async_turn_aux_heat_on(self): + async def async_turn_aux_heat_on(self) -> None: """Turn Aux Heat on.""" self._thermostat.set_emergency_heat(True) self._signal_thermostat_update() - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Turn off the zone.""" await self.async_set_hvac_mode(OPERATION_MODE_OFF) self._signal_zone_update() - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Turn on the zone.""" await self.async_set_hvac_mode(OPERATION_MODE_AUTO) self._signal_zone_update() diff --git a/homeassistant/components/nextbus/sensor.py b/homeassistant/components/nextbus/sensor.py index 12d43f32d07..5ab5d79caf7 100644 --- a/homeassistant/components/nextbus/sensor.py +++ b/homeassistant/components/nextbus/sensor.py @@ -169,7 +169,7 @@ class NextBusDepartureSensor(SensorEntity): """Return additional state attributes.""" return self._attributes - def update(self): + def update(self) -> None: """Update sensor with new departures times.""" # Note: using Multi because there is a bug with the single stop impl results = self._client.get_predictions_for_multi_stops( diff --git a/homeassistant/components/nextcloud/binary_sensor.py b/homeassistant/components/nextcloud/binary_sensor.py index fca0c7b44a7..e9d5b4a8d7f 100644 --- a/homeassistant/components/nextcloud/binary_sensor.py +++ b/homeassistant/components/nextcloud/binary_sensor.py @@ -53,6 +53,6 @@ class NextcloudBinarySensor(BinarySensorEntity): """Return the unique ID for this binary sensor.""" return f"{self.hass.data[DOMAIN]['instance']}#{self._name}" - def update(self): + def update(self) -> None: """Update the binary sensor.""" self._is_on = self.hass.data[DOMAIN][self._name] diff --git a/homeassistant/components/nextcloud/sensor.py b/homeassistant/components/nextcloud/sensor.py index e912bcdd806..31caa46028f 100644 --- a/homeassistant/components/nextcloud/sensor.py +++ b/homeassistant/components/nextcloud/sensor.py @@ -53,6 +53,6 @@ class NextcloudSensor(SensorEntity): """Return the unique ID for this sensor.""" return f"{self.hass.data[DOMAIN]['instance']}#{self._name}" - def update(self): + def update(self) -> None: """Update the sensor.""" self._state = self.hass.data[DOMAIN][self._name] diff --git a/homeassistant/components/nina/config_flow.py b/homeassistant/components/nina/config_flow.py index aa06b00e0ad..bdaf164fadb 100644 --- a/homeassistant/components/nina/config_flow.py +++ b/homeassistant/components/nina/config_flow.py @@ -153,7 +153,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> OptionsFlowHandler: """Get the options flow for this handler.""" return OptionsFlowHandler(config_entry) diff --git a/homeassistant/components/nmbs/sensor.py b/homeassistant/components/nmbs/sensor.py index fdb03652756..56fa0cd4a8d 100644 --- a/homeassistant/components/nmbs/sensor.py +++ b/homeassistant/components/nmbs/sensor.py @@ -158,7 +158,7 @@ class NMBSLiveBoard(SensorEntity): return attrs - def update(self): + def update(self) -> None: """Set the state equal to the next departure.""" liveboard = self._api_client.get_liveboard(self._station) @@ -278,7 +278,7 @@ class NMBSSensor(SensorEntity): return "vias" in self._attrs and int(self._attrs["vias"]["number"]) > 0 - def update(self): + def update(self) -> None: """Set the state to the duration of a connection.""" connections = self._api_client.get_connections( self._station_from, self._station_to diff --git a/homeassistant/components/noaa_tides/sensor.py b/homeassistant/components/noaa_tides/sensor.py index 6e398fa7183..49635973cf8 100644 --- a/homeassistant/components/noaa_tides/sensor.py +++ b/homeassistant/components/noaa_tides/sensor.py @@ -130,7 +130,7 @@ class NOAATidesAndCurrentsSensor(SensorEntity): return f"Low tide at {tidetime}" return None - def update(self): + def update(self) -> None: """Get the latest data from NOAA Tides and Currents API.""" begin = datetime.now() delta = timedelta(days=2) diff --git a/homeassistant/components/nuheat/climate.py b/homeassistant/components/nuheat/climate.py index 6cc70965ade..78e93ad5cea 100644 --- a/homeassistant/components/nuheat/climate.py +++ b/homeassistant/components/nuheat/climate.py @@ -2,6 +2,7 @@ from datetime import datetime import logging import time +from typing import Any from nuheat.config import SCHEDULE_HOLD, SCHEDULE_RUN, SCHEDULE_TEMPORARY_HOLD from nuheat.util import ( @@ -100,7 +101,7 @@ class NuHeatThermostat(CoordinatorEntity, ClimateEntity): return self._thermostat.room @property - def temperature_unit(self): + def temperature_unit(self) -> str: """Return the unit of measurement.""" if self._temperature_unit == "C": return TEMP_CELSIUS @@ -121,7 +122,7 @@ class NuHeatThermostat(CoordinatorEntity, ClimateEntity): return self._thermostat.serial_number @property - def available(self): + def available(self) -> bool: """Return the unique id.""" return self.coordinator.last_update_success and self._thermostat.online @@ -178,7 +179,7 @@ class NuHeatThermostat(CoordinatorEntity, ClimateEntity): """Return available preset modes.""" return PRESET_MODES - def set_preset_mode(self, preset_mode): + def set_preset_mode(self, preset_mode: str) -> None: """Update the hold mode of the thermostat.""" self._set_schedule_mode( PRESET_MODE_TO_SCHEDULE_MODE_MAP.get(preset_mode, SCHEDULE_RUN) @@ -191,7 +192,7 @@ class NuHeatThermostat(CoordinatorEntity, ClimateEntity): self._thermostat.schedule_mode = schedule_mode self._schedule_update() - def set_temperature(self, **kwargs): + def set_temperature(self, **kwargs: Any) -> None: """Set a new target temperature.""" self._set_temperature_and_mode( kwargs.get(ATTR_TEMPERATURE), hvac_mode=kwargs.get(ATTR_HVAC_MODE) diff --git a/homeassistant/components/nuki/binary_sensor.py b/homeassistant/components/nuki/binary_sensor.py index 4c59c121e6d..69c48133533 100644 --- a/homeassistant/components/nuki/binary_sensor.py +++ b/homeassistant/components/nuki/binary_sensor.py @@ -54,7 +54,7 @@ class NukiDoorsensorEntity(NukiEntity, BinarySensorEntity): return data @property - def available(self): + def available(self) -> bool: """Return true if door sensor is present and activated.""" return super().available and self._nuki_device.is_door_sensor_activated diff --git a/homeassistant/components/numato/binary_sensor.py b/homeassistant/components/numato/binary_sensor.py index b3881cc0493..c326a45d462 100644 --- a/homeassistant/components/numato/binary_sensor.py +++ b/homeassistant/components/numato/binary_sensor.py @@ -92,7 +92,7 @@ class NumatoGpioBinarySensor(BinarySensorEntity): self._state = None self._api = api - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Connect state update callback.""" self.async_on_remove( async_dispatcher_connect( @@ -118,7 +118,7 @@ class NumatoGpioBinarySensor(BinarySensorEntity): """Return the state of the entity.""" return self._state != self._invert_logic - def update(self): + def update(self) -> None: """Update the GPIO state.""" try: self._state = self._api.read_input(self._device_id, self._port) diff --git a/homeassistant/components/numato/sensor.py b/homeassistant/components/numato/sensor.py index 8183e4c6796..4ac28e07611 100644 --- a/homeassistant/components/numato/sensor.py +++ b/homeassistant/components/numato/sensor.py @@ -102,7 +102,7 @@ class NumatoGpioAdc(SensorEntity): """Return the icon to use in the frontend, if any.""" return ICON - def update(self): + def update(self) -> None: """Get the latest data and updates the state.""" try: adc_val = self._api.read_adc_input(self._device_id, self._port) diff --git a/homeassistant/components/numato/switch.py b/homeassistant/components/numato/switch.py index fb18866ae93..92fc7e0e2df 100644 --- a/homeassistant/components/numato/switch.py +++ b/homeassistant/components/numato/switch.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from numato_gpio import NumatoGpioError @@ -88,7 +89,7 @@ class NumatoGpioSwitch(SwitchEntity): """Return true if port is turned on.""" return self._state - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the port on.""" try: self._api.write_output( @@ -104,7 +105,7 @@ class NumatoGpioSwitch(SwitchEntity): err, ) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the port off.""" try: self._api.write_output( diff --git a/homeassistant/components/nws/sensor.py b/homeassistant/components/nws/sensor.py index 3bcfd19407d..ec53d909561 100644 --- a/homeassistant/components/nws/sensor.py +++ b/homeassistant/components/nws/sensor.py @@ -109,7 +109,7 @@ class NWSSensor(CoordinatorEntity, SensorEntity): return f"{base_unique_id(self._latitude, self._longitude)}_{self.entity_description.key}" @property - def available(self): + def available(self) -> bool: """Return if state is available.""" if self.coordinator.last_update_success_time: last_success_time = ( diff --git a/homeassistant/components/nws/weather.py b/homeassistant/components/nws/weather.py index 60f93f20177..eb6f7a4f39d 100644 --- a/homeassistant/components/nws/weather.py +++ b/homeassistant/components/nws/weather.py @@ -269,7 +269,7 @@ class NWSWeather(WeatherEntity): return f"{base_unique_id(self.latitude, self.longitude)}_{self.mode}" @property - def available(self): + def available(self) -> bool: """Return if state is available.""" last_success = ( self.coordinator_observation.last_update_success @@ -289,7 +289,7 @@ class NWSWeather(WeatherEntity): last_success_time = False return last_success or last_success_time - async def async_update(self): + async def async_update(self) -> None: """Update the entity. Only used by the generic entity update service. diff --git a/homeassistant/components/nzbget/switch.py b/homeassistant/components/nzbget/switch.py index 4e4cca34aa8..74b49b63501 100644 --- a/homeassistant/components/nzbget/switch.py +++ b/homeassistant/components/nzbget/switch.py @@ -1,6 +1,8 @@ """Support for NZBGet switches.""" from __future__ import annotations +from typing import Any + from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME @@ -61,12 +63,12 @@ class NZBGetDownloadSwitch(NZBGetEntity, SwitchEntity): """Return the state of the switch.""" return not self.coordinator.data["status"].get("DownloadPaused", False) - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Set downloads to enabled.""" await self.hass.async_add_executor_job(self.coordinator.nzbget.resumedownload) await self.coordinator.async_request_refresh() - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Set downloads to paused.""" await self.hass.async_add_executor_job(self.coordinator.nzbget.pausedownload) await self.coordinator.async_request_refresh() From 601fb5ebb509e955459dd67060f0d92bab1a7a14 Mon Sep 17 00:00:00 2001 From: rappenze Date: Mon, 5 Sep 2022 14:55:12 +0200 Subject: [PATCH 103/955] Add reauth flow to fibaro (#74300) --- homeassistant/components/fibaro/__init__.py | 10 +- .../components/fibaro/config_flow.py | 45 +++++ homeassistant/components/fibaro/strings.json | 10 +- tests/components/fibaro/test_config_flow.py | 162 ++++++++++++++++-- 4 files changed, 212 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/fibaro/__init__.py b/homeassistant/components/fibaro/__init__.py index 7ab83d796f2..9c2d252d77f 100644 --- a/homeassistant/components/fibaro/__init__.py +++ b/homeassistant/components/fibaro/__init__.py @@ -27,7 +27,11 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError +from homeassistant.exceptions import ( + ConfigEntryAuthFailed, + ConfigEntryNotReady, + HomeAssistantError, +) from homeassistant.helpers import device_registry as dr import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import DeviceInfo, Entity @@ -497,8 +501,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryNotReady( f"Could not connect to controller at {entry.data[CONF_URL]}" ) from connect_ex - except FibaroAuthFailed: - return False + except FibaroAuthFailed as auth_ex: + raise ConfigEntryAuthFailed from auth_ex data: dict[str, Any] = {} hass.data.setdefault(DOMAIN, {})[entry.entry_id] = data diff --git a/homeassistant/components/fibaro/config_flow.py b/homeassistant/components/fibaro/config_flow.py index fd53bd5b94f..7a6d7422520 100644 --- a/homeassistant/components/fibaro/config_flow.py +++ b/homeassistant/components/fibaro/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Fibaro integration.""" from __future__ import annotations +from collections.abc import Mapping import logging from typing import Any @@ -58,6 +59,10 @@ class FibaroConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 + def __init__(self) -> None: + """Initialize.""" + self._reauth_entry: config_entries.ConfigEntry | None = None + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: @@ -83,3 +88,43 @@ class FibaroConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_import(self, import_config: ConfigType | None) -> FlowResult: """Import a config entry.""" return await self.async_step_user(import_config) + + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + """Handle reauthentication.""" + self._reauth_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initiated by reauthentication.""" + errors = {} + + assert self._reauth_entry + if user_input is not None: + new_data = self._reauth_entry.data | user_input + try: + await _validate_input(self.hass, new_data) + except FibaroConnectFailed: + errors["base"] = "cannot_connect" + except FibaroAuthFailed: + errors["base"] = "invalid_auth" + else: + self.hass.config_entries.async_update_entry( + self._reauth_entry, data=new_data + ) + self.hass.async_create_task( + 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="reauth_confirm", + data_schema=vol.Schema({vol.Required(CONF_PASSWORD): str}), + errors=errors, + description_placeholders={ + CONF_USERNAME: self._reauth_entry.data[CONF_USERNAME] + }, + ) diff --git a/homeassistant/components/fibaro/strings.json b/homeassistant/components/fibaro/strings.json index 99c25c9f6e0..de875176cdb 100644 --- a/homeassistant/components/fibaro/strings.json +++ b/homeassistant/components/fibaro/strings.json @@ -8,6 +8,13 @@ "password": "[%key:common::config_flow::data::password%]", "import_plugins": "Import entities from fibaro plugins?" } + }, + "reauth_confirm": { + "data": { + "password": "[%key:common::config_flow::data::password%]" + }, + "title": "[%key:common::config_flow::title::reauth%]", + "description": "Please update your password for {username}" } }, "error": { @@ -16,7 +23,8 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } } } diff --git a/tests/components/fibaro/test_config_flow.py b/tests/components/fibaro/test_config_flow.py index 14f28257588..f68bb5fe4ca 100644 --- a/tests/components/fibaro/test_config_flow.py +++ b/tests/components/fibaro/test_config_flow.py @@ -9,6 +9,8 @@ from homeassistant.components.fibaro import DOMAIN from homeassistant.components.fibaro.const import CONF_IMPORT_PLUGINS from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME +from tests.common import MockConfigEntry + TEST_SERIALNUMBER = "HC2-111111" TEST_NAME = "my_fibaro_home_center" TEST_URL = "http://192.168.1.1/api/" @@ -31,20 +33,27 @@ def fibaro_client_fixture(): client_mock = Mock() client_mock.base_url.return_value = TEST_URL - with patch("fiblary3.client.v4.client.Client.__init__", return_value=None,), patch( - "fiblary3.client.v4.client.Client.info", + with patch( + "homeassistant.components.fibaro.FibaroClientV4.__init__", + return_value=None, + ), patch( + "homeassistant.components.fibaro.FibaroClientV4.info", info_mock, create=True, - ), patch("fiblary3.client.v4.client.Client.rooms", array_mock, create=True,), patch( - "fiblary3.client.v4.client.Client.devices", + ), patch( + "homeassistant.components.fibaro.FibaroClientV4.rooms", array_mock, create=True, ), patch( - "fiblary3.client.v4.client.Client.scenes", + "homeassistant.components.fibaro.FibaroClientV4.devices", array_mock, create=True, ), patch( - "fiblary3.client.v4.client.Client.client", + "homeassistant.components.fibaro.FibaroClientV4.scenes", + array_mock, + create=True, + ), patch( + "homeassistant.components.fibaro.FibaroClientV4.client", client_mock, create=True, ): @@ -64,7 +73,7 @@ async def test_config_flow_user_initiated_success(hass): login_mock = Mock() login_mock.get.return_value = Mock(status=True) with patch( - "fiblary3.client.v4.client.Client.login", login_mock, create=True + "homeassistant.components.fibaro.FibaroClientV4.login", login_mock, create=True ), patch( "homeassistant.components.fibaro.async_setup_entry", return_value=True, @@ -100,7 +109,9 @@ async def test_config_flow_user_initiated_connect_failure(hass): login_mock = Mock() login_mock.get.return_value = Mock(status=False) - with patch("fiblary3.client.v4.client.Client.login", login_mock, create=True): + with patch( + "homeassistant.components.fibaro.FibaroClientV4.login", login_mock, create=True + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -127,7 +138,9 @@ async def test_config_flow_user_initiated_auth_failure(hass): login_mock = Mock() login_mock.get.side_effect = HTTPException(details="Forbidden") - with patch("fiblary3.client.v4.client.Client.login", login_mock, create=True): + with patch( + "homeassistant.components.fibaro.FibaroClientV4.login", login_mock, create=True + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -154,7 +167,9 @@ async def test_config_flow_user_initiated_unknown_failure_1(hass): login_mock = Mock() login_mock.get.side_effect = HTTPException(details="Any") - with patch("fiblary3.client.v4.client.Client.login", login_mock, create=True): + with patch( + "homeassistant.components.fibaro.FibaroClientV4.login", login_mock, create=True + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -198,7 +213,7 @@ async def test_config_flow_import(hass): login_mock = Mock() login_mock.get.return_value = Mock(status=True) with patch( - "fiblary3.client.v4.client.Client.login", login_mock, create=True + "homeassistant.components.fibaro.FibaroClientV4.login", login_mock, create=True ), patch( "homeassistant.components.fibaro.async_setup_entry", return_value=True, @@ -222,3 +237,128 @@ async def test_config_flow_import(hass): CONF_PASSWORD: TEST_PASSWORD, CONF_IMPORT_PLUGINS: False, } + + +async def test_reauth_success(hass): + """Successful reauth flow initialized by the user.""" + mock_config = MockConfigEntry( + domain=DOMAIN, + entry_id=TEST_SERIALNUMBER, + data={ + CONF_URL: TEST_URL, + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + CONF_IMPORT_PLUGINS: False, + }, + ) + mock_config.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": mock_config.entry_id, + }, + ) + + assert result["type"] == "form" + assert result["step_id"] == "reauth_confirm" + assert result["errors"] == {} + + login_mock = Mock() + login_mock.get.return_value = Mock(status=True) + with patch( + "homeassistant.components.fibaro.FibaroClientV4.login", login_mock, create=True + ), patch( + "homeassistant.components.fibaro.async_setup_entry", + return_value=True, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_PASSWORD: "other_fake_password"}, + ) + + assert result["type"] == "abort" + assert result["reason"] == "reauth_successful" + + +async def test_reauth_connect_failure(hass): + """Successful reauth flow initialized by the user.""" + mock_config = MockConfigEntry( + domain=DOMAIN, + entry_id=TEST_SERIALNUMBER, + data={ + CONF_URL: TEST_URL, + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + CONF_IMPORT_PLUGINS: False, + }, + ) + mock_config.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": mock_config.entry_id, + }, + ) + + assert result["type"] == "form" + assert result["step_id"] == "reauth_confirm" + assert result["errors"] == {} + + login_mock = Mock() + login_mock.get.return_value = Mock(status=False) + with patch( + "homeassistant.components.fibaro.FibaroClientV4.login", login_mock, create=True + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_PASSWORD: "other_fake_password"}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "reauth_confirm" + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_reauth_auth_failure(hass): + """Successful reauth flow initialized by the user.""" + mock_config = MockConfigEntry( + domain=DOMAIN, + entry_id=TEST_SERIALNUMBER, + data={ + CONF_URL: TEST_URL, + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + CONF_IMPORT_PLUGINS: False, + }, + ) + mock_config.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": mock_config.entry_id, + }, + ) + + assert result["type"] == "form" + assert result["step_id"] == "reauth_confirm" + assert result["errors"] == {} + + login_mock = Mock() + login_mock.get.side_effect = HTTPException(details="Forbidden") + with patch( + "homeassistant.components.fibaro.FibaroClientV4.login", login_mock, create=True + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_PASSWORD: "other_fake_password"}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "reauth_confirm" + assert result["errors"] == {"base": "invalid_auth"} From b0c093ac70d36bfa3c7225208e751d5330f517b6 Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Mon, 5 Sep 2022 09:20:37 -0400 Subject: [PATCH 104/955] Add remoteAdminPasswordEnd to redacted keys in fully_kiosk diagnostics (#77837) Add remoteAdminPasswordEnd to redacted keys in diagnostics --- homeassistant/components/fully_kiosk/diagnostics.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/fully_kiosk/diagnostics.py b/homeassistant/components/fully_kiosk/diagnostics.py index 89a894d5353..121621186cd 100644 --- a/homeassistant/components/fully_kiosk/diagnostics.py +++ b/homeassistant/components/fully_kiosk/diagnostics.py @@ -51,6 +51,7 @@ SETTINGS_TO_REDACT = { "sebExamKey", "sebConfigKey", "kioskPinEnc", + "remoteAdminPasswordEnc", } From c158575aa5b3a128242fc02aa5eae07dfddab5d0 Mon Sep 17 00:00:00 2001 From: Artem Draft Date: Mon, 5 Sep 2022 17:04:33 +0300 Subject: [PATCH 105/955] Bump pybravia to 0.2.1 (#77832) --- homeassistant/components/braviatv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/braviatv/manifest.json b/homeassistant/components/braviatv/manifest.json index 8a18cac5a99..fa172957781 100644 --- a/homeassistant/components/braviatv/manifest.json +++ b/homeassistant/components/braviatv/manifest.json @@ -2,7 +2,7 @@ "domain": "braviatv", "name": "Sony Bravia TV", "documentation": "https://www.home-assistant.io/integrations/braviatv", - "requirements": ["pybravia==0.2.0"], + "requirements": ["pybravia==0.2.1"], "codeowners": ["@bieniu", "@Drafteed"], "config_flow": true, "iot_class": "local_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 97503052108..4cffd96ae4e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1443,7 +1443,7 @@ pyblackbird==0.5 pybotvac==0.0.23 # homeassistant.components.braviatv -pybravia==0.2.0 +pybravia==0.2.1 # homeassistant.components.nissan_leaf pycarwings2==2.13 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 213d10a20b4..7c094018318 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1019,7 +1019,7 @@ pyblackbird==0.5 pybotvac==0.0.23 # homeassistant.components.braviatv -pybravia==0.2.0 +pybravia==0.2.1 # homeassistant.components.cloudflare pycfdns==1.2.2 From 4a0cbfb5508c1c6824d48842aea081d0a05e6354 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 5 Sep 2022 16:18:49 +0200 Subject: [PATCH 106/955] Add the hardware integration to default_config (#77840) --- homeassistant/components/default_config/manifest.json | 3 ++- homeassistant/package_constraints.txt | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/default_config/manifest.json b/homeassistant/components/default_config/manifest.json index 593ac26dbc9..6701e62c71f 100644 --- a/homeassistant/components/default_config/manifest.json +++ b/homeassistant/components/default_config/manifest.json @@ -11,8 +11,9 @@ "dhcp", "energy", "frontend", - "homeassistant_alerts", + "hardware", "history", + "homeassistant_alerts", "input_boolean", "input_button", "input_datetime", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 2e41ae13458..8b65b9c0285 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -28,6 +28,7 @@ orjson==3.7.11 paho-mqtt==1.6.1 pillow==9.2.0 pip>=21.0,<22.3 +psutil-home-assistant==0.0.1 pyserial==3.5 python-slugify==4.0.1 pyudev==0.23.2 From 61f4040d567df612f6d36c1ab2780465152eb647 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 5 Sep 2022 16:19:34 +0200 Subject: [PATCH 107/955] Address late review on kulersky light (#77838) --- homeassistant/components/kulersky/light.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/kulersky/light.py b/homeassistant/components/kulersky/light.py index bfd5eec1aab..c6763e6d9f6 100644 --- a/homeassistant/components/kulersky/light.py +++ b/homeassistant/components/kulersky/light.py @@ -158,6 +158,11 @@ class KulerskyLight(LightEntity): if not brightness: self._attr_rgbw_color = (0, 0, 0, 0) else: - rgbw_normalized = tuple(round(x * 255 / brightness) for x in rgbw) - self._attr_rgbw_color = rgbw_normalized # type:ignore[assignment] + rgbw_normalized = [round(x * 255 / brightness) for x in rgbw] + self._attr_rgbw_color = ( + rgbw_normalized[0], + rgbw_normalized[1], + rgbw_normalized[2], + rgbw_normalized[3], + ) self._attr_brightness = brightness From a641bbc352b7559c9491971fa6fc06437f4ab964 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Mon, 5 Sep 2022 15:33:10 +0100 Subject: [PATCH 108/955] Less verbose error logs for bleak connection errors in ActiveBluetoothProcessorCoordinator (#77839) Co-authored-by: J. Nick Koston --- .../bluetooth/active_update_coordinator.py | 9 +++ .../test_active_update_coordinator.py | 76 +++++++++++++++++++ 2 files changed, 85 insertions(+) diff --git a/homeassistant/components/bluetooth/active_update_coordinator.py b/homeassistant/components/bluetooth/active_update_coordinator.py index e73414fe79f..b207f6fa2e1 100644 --- a/homeassistant/components/bluetooth/active_update_coordinator.py +++ b/homeassistant/components/bluetooth/active_update_coordinator.py @@ -6,6 +6,8 @@ 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 @@ -109,6 +111,13 @@ class ActiveBluetoothProcessorCoordinator( try: update = await self._async_poll_data(self._last_service_info) + except BleakError as exc: + if self.last_poll_successful: + self.logger.error( + "%s: Bluetooth error whilst polling: %s", self.address, str(exc) + ) + self.last_poll_successful = False + return except Exception: # pylint: disable=broad-except if self.last_poll_successful: self.logger.exception("%s: Failure while polling", self.address) diff --git a/tests/components/bluetooth/test_active_update_coordinator.py b/tests/components/bluetooth/test_active_update_coordinator.py index 24ad96c523e..7677584e890 100644 --- a/tests/components/bluetooth/test_active_update_coordinator.py +++ b/tests/components/bluetooth/test_active_update_coordinator.py @@ -5,6 +5,8 @@ import asyncio import logging from unittest.mock import MagicMock, call, patch +from bleak import BleakError + from homeassistant.components.bluetooth import ( DOMAIN, BluetoothChange, @@ -162,6 +164,80 @@ async def test_poll_can_be_skipped(hass: HomeAssistant, mock_bleak_scanner_start cancel() +async def test_bleak_error_and_recover( + hass: HomeAssistant, mock_bleak_scanner_start, caplog +): + """Test bleak error handling and recovery.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + + flag = True + + def _update_method(service_info: BluetoothServiceInfoBleak): + return {"testdata": None} + + def _poll_needed(*args, **kwargs): + return True + + async def _poll(*args, **kwargs): + nonlocal flag + if flag: + raise BleakError("Connection was aborted") + return {"testdata": flag} + + coordinator = ActiveBluetoothProcessorCoordinator( + hass, + _LOGGER, + address="aa:bb:cc:dd:ee:ff", + mode=BluetoothScanningMode.ACTIVE, + update_method=_update_method, + needs_poll_method=_poll_needed, + poll_method=_poll, + poll_debouncer=Debouncer( + hass, + _LOGGER, + cooldown=0, + immediate=True, + ), + ) + assert coordinator.available is False # no data yet + saved_callback = None + + processor = MagicMock() + coordinator.async_register_processor(processor) + async_handle_update = processor.async_handle_update + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + cancel = coordinator.async_start() + + assert saved_callback is not None + + # First poll fails + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + await hass.async_block_till_done() + assert async_handle_update.mock_calls[-1] == call({"testdata": None}) + + assert ( + "aa:bb:cc:dd:ee:ff: Bluetooth error whilst polling: Connection was aborted" + in caplog.text + ) + + # Second poll works + flag = False + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + await hass.async_block_till_done() + assert async_handle_update.mock_calls[-1] == call({"testdata": False}) + + cancel() + + async def test_poll_failure_and_recover(hass: HomeAssistant, mock_bleak_scanner_start): """Test error handling and recovery.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) From 33dd8a4a45ba0536f5c43061d7bfef61640377a1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 5 Sep 2022 13:05:37 -0500 Subject: [PATCH 109/955] Bump led-ble to 0.7.0 (#77845) --- homeassistant/components/led_ble/manifest.json | 5 +++-- homeassistant/generated/bluetooth.py | 4 ++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/led_ble/manifest.json b/homeassistant/components/led_ble/manifest.json index a0f5e3481d5..273fbfedc04 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/ble_ble", - "requirements": ["led-ble==0.6.0"], + "requirements": ["led-ble==0.7.0"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "bluetooth": [ @@ -13,7 +13,8 @@ { "local_name": "Triones*" }, { "local_name": "LEDBlue*" }, { "local_name": "Dream~*" }, - { "local_name": "QHM-*" } + { "local_name": "QHM-*" }, + { "local_name": "AP-*" } ], "iot_class": "local_polling" } diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 83bfd3ab5eb..d7230213302 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -164,6 +164,10 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ "domain": "led_ble", "local_name": "QHM-*" }, + { + "domain": "led_ble", + "local_name": "AP-*" + }, { "domain": "melnor", "manufacturer_data_start": [ diff --git a/requirements_all.txt b/requirements_all.txt index 4cffd96ae4e..a3b0892312d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -968,7 +968,7 @@ lakeside==0.12 laundrify_aio==1.1.2 # homeassistant.components.led_ble -led-ble==0.6.0 +led-ble==0.7.0 # homeassistant.components.foscam libpyfoscam==1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7c094018318..956516430dd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -706,7 +706,7 @@ lacrosse-view==0.0.9 laundrify_aio==1.1.2 # homeassistant.components.led_ble -led-ble==0.6.0 +led-ble==0.7.0 # homeassistant.components.foscam libpyfoscam==1.0 From 6fbc0a81032f9bb6ae0d0299469cb98710c28c76 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 5 Sep 2022 13:05:53 -0500 Subject: [PATCH 110/955] Bump govee-ble to 0.17.2 (#77849) --- homeassistant/components/govee_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/govee_ble/__init__.py | 4 ++-- tests/components/govee_ble/test_config_flow.py | 6 +++--- tests/components/govee_ble/test_sensor.py | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/govee_ble/manifest.json b/homeassistant/components/govee_ble/manifest.json index e24e3bfea14..2ce68498968 100644 --- a/homeassistant/components/govee_ble/manifest.json +++ b/homeassistant/components/govee_ble/manifest.json @@ -53,7 +53,7 @@ "connectable": false } ], - "requirements": ["govee-ble==0.17.1"], + "requirements": ["govee-ble==0.17.2"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index a3b0892312d..ac0f9a43f8f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -772,7 +772,7 @@ googlemaps==2.5.1 goslide-api==0.5.1 # homeassistant.components.govee_ble -govee-ble==0.17.1 +govee-ble==0.17.2 # homeassistant.components.remote_rpi_gpio gpiozero==1.6.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 956516430dd..9d70f106685 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -573,7 +573,7 @@ google-nest-sdm==2.0.0 googlemaps==2.5.1 # homeassistant.components.govee_ble -govee-ble==0.17.1 +govee-ble==0.17.2 # homeassistant.components.gree greeclimate==1.3.0 diff --git a/tests/components/govee_ble/__init__.py b/tests/components/govee_ble/__init__.py index 3baea5e1140..c440317fa43 100644 --- a/tests/components/govee_ble/__init__.py +++ b/tests/components/govee_ble/__init__.py @@ -14,7 +14,7 @@ NOT_GOVEE_SERVICE_INFO = BluetoothServiceInfo( ) GVH5075_SERVICE_INFO = BluetoothServiceInfo( - name="GVH5075_2762", + name="GVH5075 2762", address="61DE521B-F0BF-9F44-64D4-75BBE1738105", rssi=-63, manufacturer_data={ @@ -26,7 +26,7 @@ GVH5075_SERVICE_INFO = BluetoothServiceInfo( ) GVH5177_SERVICE_INFO = BluetoothServiceInfo( - name="GVH5177_2EC8", + name="GVH5177 2EC8", address="4125DDBA-2774-4851-9889-6AADDD4CAC3D", rssi=-56, manufacturer_data={ diff --git a/tests/components/govee_ble/test_config_flow.py b/tests/components/govee_ble/test_config_flow.py index 188672cdf18..73cbb903f31 100644 --- a/tests/components/govee_ble/test_config_flow.py +++ b/tests/components/govee_ble/test_config_flow.py @@ -27,7 +27,7 @@ async def test_async_step_bluetooth_valid_device(hass): result["flow_id"], user_input={} ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "H5075_2762" + assert result2["title"] == "H5075 2762" assert result2["data"] == {} assert result2["result"].unique_id == "61DE521B-F0BF-9F44-64D4-75BBE1738105" @@ -73,7 +73,7 @@ async def test_async_step_user_with_found_devices(hass): user_input={"address": "4125DDBA-2774-4851-9889-6AADDD4CAC3D"}, ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "H5177_2EC8" + assert result2["title"] == "H5177 2EC8" assert result2["data"] == {} assert result2["result"].unique_id == "4125DDBA-2774-4851-9889-6AADDD4CAC3D" @@ -192,7 +192,7 @@ async def test_async_step_user_takes_precedence_over_discovery(hass): user_input={"address": "4125DDBA-2774-4851-9889-6AADDD4CAC3D"}, ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "H5177_2EC8" + assert result2["title"] == "H5177 2EC8" assert result2["data"] == {} assert result2["result"].unique_id == "4125DDBA-2774-4851-9889-6AADDD4CAC3D" diff --git a/tests/components/govee_ble/test_sensor.py b/tests/components/govee_ble/test_sensor.py index da67d32e681..e7828fdc496 100644 --- a/tests/components/govee_ble/test_sensor.py +++ b/tests/components/govee_ble/test_sensor.py @@ -42,7 +42,7 @@ async def test_sensors(hass): temp_sensor = hass.states.get("sensor.h5075_2762_temperature") temp_sensor_attribtes = temp_sensor.attributes assert temp_sensor.state == "21.34" - assert temp_sensor_attribtes[ATTR_FRIENDLY_NAME] == "H5075_2762 Temperature" + assert temp_sensor_attribtes[ATTR_FRIENDLY_NAME] == "H5075 2762 Temperature" assert temp_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "°C" assert temp_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" From 73ba7a989b0cae6fba3564947d819e1eeb423f54 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Mon, 5 Sep 2022 14:12:37 -0400 Subject: [PATCH 111/955] Make Sonos typing more complete (#68072) --- homeassistant/components/sonos/__init__.py | 38 ++++++---- .../components/sonos/binary_sensor.py | 2 +- homeassistant/components/sonos/diagnostics.py | 15 ++-- .../components/sonos/household_coordinator.py | 3 +- homeassistant/components/sonos/media.py | 5 +- .../components/sonos/media_browser.py | 59 ++++++++------ .../components/sonos/media_player.py | 76 +++++++++---------- homeassistant/components/sonos/number.py | 11 ++- homeassistant/components/sonos/sensor.py | 2 +- homeassistant/components/sonos/speaker.py | 60 ++++++++------- homeassistant/components/sonos/statistics.py | 2 +- homeassistant/components/sonos/switch.py | 37 +++++---- mypy.ini | 36 --------- script/hassfest/mypy_config.py | 15 +--- 14 files changed, 168 insertions(+), 193 deletions(-) diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index d94b49e52f2..f0dd8e668fa 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -8,6 +8,7 @@ import datetime from functools import partial import logging import socket +from typing import TYPE_CHECKING, Any, Optional, cast from urllib.parse import urlparse from soco import events_asyncio @@ -21,7 +22,7 @@ from homeassistant.components import ssdp from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOSTS, EVENT_HOMEASSISTANT_STOP -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers.dispatcher import async_dispatcher_send, dispatcher_send from homeassistant.helpers.event import async_track_time_interval, call_later @@ -93,7 +94,7 @@ class SonosData: self.favorites: dict[str, SonosFavorites] = {} self.alarms: dict[str, SonosAlarms] = {} self.topology_condition = asyncio.Condition() - self.hosts_heartbeat = None + self.hosts_heartbeat: CALLBACK_TYPE | None = None self.discovery_known: set[str] = set() self.boot_counts: dict[str, int] = {} self.mdns_names: dict[str, str] = {} @@ -168,10 +169,10 @@ class SonosDiscoveryManager: self.data = data self.hosts = set(hosts) self.discovery_lock = asyncio.Lock() - self._known_invisible = set() + self._known_invisible: set[SoCo] = set() self._manual_config_required = bool(hosts) - async def async_shutdown(self): + async def async_shutdown(self) -> None: """Stop all running tasks.""" await self._async_stop_event_listener() self._stop_manual_heartbeat() @@ -236,6 +237,8 @@ class SonosDiscoveryManager: (SonosAlarms, self.data.alarms), (SonosFavorites, self.data.favorites), ): + if TYPE_CHECKING: + coord_dict = cast(dict[str, Any], coord_dict) if soco.household_id not in coord_dict: new_coordinator = coordinator(self.hass, soco.household_id) new_coordinator.setup(soco) @@ -298,7 +301,7 @@ class SonosDiscoveryManager: ) async def _async_handle_discovery_message( - self, uid: str, discovered_ip: str, boot_seqnum: int + self, uid: str, discovered_ip: str, boot_seqnum: int | None ) -> None: """Handle discovered player creation and activity.""" async with self.discovery_lock: @@ -338,22 +341,27 @@ class SonosDiscoveryManager: async_dispatcher_send(self.hass, f"{SONOS_VANISHED}-{uid}", reason) return - discovered_ip = urlparse(info.ssdp_location).hostname - boot_seqnum = info.ssdp_headers.get("X-RINCON-BOOTSEQ") self.async_discovered_player( "SSDP", info, - discovered_ip, + cast(str, urlparse(info.ssdp_location).hostname), uid, - boot_seqnum, - info.upnp.get(ssdp.ATTR_UPNP_MODEL_NAME), + info.ssdp_headers.get("X-RINCON-BOOTSEQ"), + cast(str, info.upnp.get(ssdp.ATTR_UPNP_MODEL_NAME)), None, ) @callback def async_discovered_player( - self, source, info, discovered_ip, uid, boot_seqnum, model, mdns_name - ): + self, + source: str, + info: ssdp.SsdpServiceInfo, + discovered_ip: str, + uid: str, + boot_seqnum: str | int | None, + model: str, + mdns_name: str | None, + ) -> None: """Handle discovery via ssdp or zeroconf.""" if self._manual_config_required: _LOGGER.warning( @@ -376,10 +384,12 @@ class SonosDiscoveryManager: _LOGGER.debug("New %s discovery uid=%s: %s", source, uid, info) self.data.discovery_known.add(uid) asyncio.create_task( - self._async_handle_discovery_message(uid, discovered_ip, boot_seqnum) + self._async_handle_discovery_message( + uid, discovered_ip, cast(Optional[int], boot_seqnum) + ) ) - async def setup_platforms_and_discovery(self): + async def setup_platforms_and_discovery(self) -> None: """Set up platforms and discovery.""" await self.hass.config_entries.async_forward_entry_setups(self.entry, PLATFORMS) self.entry.async_on_unload( diff --git a/homeassistant/components/sonos/binary_sensor.py b/homeassistant/components/sonos/binary_sensor.py index e890c1c64a8..3f736f83922 100644 --- a/homeassistant/components/sonos/binary_sensor.py +++ b/homeassistant/components/sonos/binary_sensor.py @@ -109,6 +109,6 @@ class SonosMicrophoneSensorEntity(SonosEntity, BinarySensorEntity): self.speaker.mic_enabled = self.soco.mic_enabled @property - def is_on(self) -> bool: + def is_on(self) -> bool | None: """Return the state of the binary sensor.""" return self.speaker.mic_enabled diff --git a/homeassistant/components/sonos/diagnostics.py b/homeassistant/components/sonos/diagnostics.py index 463884e1ea8..fda96b86215 100644 --- a/homeassistant/components/sonos/diagnostics.py +++ b/homeassistant/components/sonos/diagnostics.py @@ -47,11 +47,11 @@ async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - payload = {"current_timestamp": time.monotonic()} + payload: dict[str, Any] = {"current_timestamp": time.monotonic()} for section in ("discovered", "discovery_known"): payload[section] = {} - data = getattr(hass.data[DATA_SONOS], section) + data: set[Any] | dict[str, Any] = getattr(hass.data[DATA_SONOS], section) if isinstance(data, set): payload[section] = data continue @@ -60,7 +60,6 @@ async def async_get_config_entry_diagnostics( payload[section][key] = await async_generate_speaker_info(hass, value) else: payload[section][key] = value - return payload @@ -85,12 +84,12 @@ async def async_generate_media_info( hass: HomeAssistant, speaker: SonosSpeaker ) -> dict[str, Any]: """Generate a diagnostic payload for current media metadata.""" - payload = {} + payload: dict[str, Any] = {} for attrib in MEDIA_DIAGNOSTIC_ATTRIBUTES: payload[attrib] = getattr(speaker.media, attrib) - def poll_current_track_info(): + def poll_current_track_info() -> dict[str, Any] | str: try: return speaker.soco.avTransport.GetPositionInfo( [("InstanceID", 0), ("Channel", "Master")], @@ -110,9 +109,11 @@ async def async_generate_speaker_info( hass: HomeAssistant, speaker: SonosSpeaker ) -> dict[str, Any]: """Generate the diagnostic payload for a specific speaker.""" - payload = {} + payload: dict[str, Any] = {} - def get_contents(item): + def get_contents( + item: int | float | str | dict[str, Any] + ) -> int | float | str | dict[str, Any]: if isinstance(item, (int, float, str)): return item if isinstance(item, dict): diff --git a/homeassistant/components/sonos/household_coordinator.py b/homeassistant/components/sonos/household_coordinator.py index 51d7e9cec8c..29b9a005552 100644 --- a/homeassistant/components/sonos/household_coordinator.py +++ b/homeassistant/components/sonos/household_coordinator.py @@ -20,13 +20,14 @@ _LOGGER = logging.getLogger(__name__) class SonosHouseholdCoordinator: """Base class for Sonos household-level storage.""" + cache_update_lock: asyncio.Lock + def __init__(self, hass: HomeAssistant, household_id: str) -> None: """Initialize the data.""" self.hass = hass self.household_id = household_id self.async_poll: Callable[[], Coroutine[None, None, None]] | None = None self.last_processed_event_id: int | None = None - self.cache_update_lock: asyncio.Lock | None = None def setup(self, soco: SoCo) -> None: """Set up the SonosAlarm instance.""" diff --git a/homeassistant/components/sonos/media.py b/homeassistant/components/sonos/media.py index 9608356ba64..24233b1316f 100644 --- a/homeassistant/components/sonos/media.py +++ b/homeassistant/components/sonos/media.py @@ -2,7 +2,6 @@ from __future__ import annotations import datetime -import logging from typing import Any from soco.core import ( @@ -43,8 +42,6 @@ UNAVAILABLE_VALUES = {"", "NOT_IMPLEMENTED", None} DURATION_SECONDS = "duration_in_s" POSITION_SECONDS = "position_in_s" -_LOGGER = logging.getLogger(__name__) - def _timespan_secs(timespan: str | None) -> None | float: """Parse a time-span into number of seconds.""" @@ -106,7 +103,7 @@ class SonosMedia: @soco_error() def poll_track_info(self) -> dict[str, Any]: """Poll the speaker for current track info, add converted position values, and return.""" - track_info = self.soco.get_current_track_info() + track_info: dict[str, Any] = self.soco.get_current_track_info() track_info[DURATION_SECONDS] = _timespan_secs(track_info.get("duration")) track_info[POSITION_SECONDS] = _timespan_secs(track_info.get("position")) return track_info diff --git a/homeassistant/components/sonos/media_browser.py b/homeassistant/components/sonos/media_browser.py index b2d881e8bf2..95ff08cb87b 100644 --- a/homeassistant/components/sonos/media_browser.py +++ b/homeassistant/components/sonos/media_browser.py @@ -5,8 +5,13 @@ from collections.abc import Callable from contextlib import suppress from functools import partial import logging +from typing import cast from urllib.parse import quote_plus, unquote +from soco.data_structures import DidlFavorite, DidlObject +from soco.ms_data_structures import MusicServiceItem +from soco.music_library import MusicLibrary + from homeassistant.components import media_source, plex, spotify from homeassistant.components.media_player import BrowseMedia from homeassistant.components.media_player.const import ( @@ -50,12 +55,12 @@ def get_thumbnail_url_full( ) -> str | None: """Get thumbnail URL.""" if is_internal: - item = get_media( # type: ignore[no-untyped-call] + item = get_media( media.library, media_content_id, media_content_type, ) - return getattr(item, "album_art_uri", None) # type: ignore[no-any-return] + return getattr(item, "album_art_uri", None) return get_browse_image_url( media_content_type, @@ -64,19 +69,19 @@ def get_thumbnail_url_full( ) -def media_source_filter(item: BrowseMedia): +def media_source_filter(item: BrowseMedia) -> bool: """Filter media sources.""" return item.media_content_type.startswith("audio/") async def async_browse_media( - hass, + hass: HomeAssistant, speaker: SonosSpeaker, media: SonosMedia, get_browse_image_url: GetBrowseImageUrlType, media_content_id: str | None, media_content_type: str | None, -): +) -> BrowseMedia: """Browse media.""" if media_content_id is None: @@ -86,6 +91,7 @@ async def async_browse_media( media, get_browse_image_url, ) + assert media_content_type is not None if media_source.is_media_source_id(media_content_id): return await media_source.async_browse_media( @@ -150,7 +156,9 @@ async def async_browse_media( return response -def build_item_response(media_library, payload, get_thumbnail_url=None): +def build_item_response( + media_library: MusicLibrary, payload: dict[str, str], get_thumbnail_url=None +) -> BrowseMedia | None: """Create response payload for the provided media query.""" if payload["search_type"] == MEDIA_TYPE_ALBUM and payload["idstring"].startswith( ("A:GENRE", "A:COMPOSER") @@ -166,7 +174,7 @@ def build_item_response(media_library, payload, get_thumbnail_url=None): "Unknown media type received when building item response: %s", payload["search_type"], ) - return + return None media = media_library.browse_by_idstring( search_type, @@ -176,7 +184,7 @@ def build_item_response(media_library, payload, get_thumbnail_url=None): ) if media is None: - return + return None thumbnail = None title = None @@ -222,7 +230,7 @@ def build_item_response(media_library, payload, get_thumbnail_url=None): ) -def item_payload(item, get_thumbnail_url=None): +def item_payload(item: DidlObject, get_thumbnail_url=None) -> BrowseMedia: """ Create response payload for a single media item. @@ -256,9 +264,9 @@ async def root_payload( speaker: SonosSpeaker, media: SonosMedia, get_browse_image_url: GetBrowseImageUrlType, -): +) -> BrowseMedia: """Return root payload for Sonos.""" - children = [] + children: list[BrowseMedia] = [] if speaker.favorites: children.append( @@ -303,14 +311,15 @@ async def root_payload( if "spotify" in hass.config.components: result = await spotify.async_browse_media(hass, None, None) - children.extend(result.children) + if result.children: + children.extend(result.children) try: item = await media_source.async_browse_media( hass, None, content_filter=media_source_filter ) # If domain is None, it's overview of available sources - if item.domain is None: + if item.domain is None and item.children is not None: children.extend(item.children) else: children.append(item) @@ -338,7 +347,7 @@ async def root_payload( ) -def library_payload(media_library, get_thumbnail_url=None): +def library_payload(media_library: MusicLibrary, get_thumbnail_url=None) -> BrowseMedia: """ Create response payload to describe contents of a specific library. @@ -360,7 +369,7 @@ def library_payload(media_library, get_thumbnail_url=None): ) -def favorites_payload(favorites): +def favorites_payload(favorites: list[DidlFavorite]) -> BrowseMedia: """ Create response payload to describe contents of a specific library. @@ -398,7 +407,9 @@ def favorites_payload(favorites): ) -def favorites_folder_payload(favorites, media_content_id): +def favorites_folder_payload( + favorites: list[DidlFavorite], media_content_id: str +) -> BrowseMedia: """Create response payload to describe all items of a type of favorite. Used by async_browse_media. @@ -432,7 +443,7 @@ def favorites_folder_payload(favorites, media_content_id): ) -def get_media_type(item): +def get_media_type(item: DidlObject) -> str: """Extract media type of item.""" if item.item_class == "object.item.audioItem.musicTrack": return SONOS_TRACKS @@ -450,7 +461,7 @@ def get_media_type(item): return SONOS_TYPES_MAPPING.get(item.item_id.split("/")[0], item.item_class) -def can_play(item): +def can_play(item: DidlObject) -> bool: """ Test if playable. @@ -459,7 +470,7 @@ def can_play(item): return SONOS_TO_MEDIA_TYPES.get(item) in PLAYABLE_MEDIA_TYPES -def can_expand(item): +def can_expand(item: DidlObject) -> bool: """ Test if expandable. @@ -474,14 +485,16 @@ def can_expand(item): return SONOS_TYPES_MAPPING.get(item.item_id) in EXPANDABLE_MEDIA_TYPES -def get_content_id(item): +def get_content_id(item: DidlObject) -> str: """Extract content id or uri.""" if item.item_class == "object.item.audioItem.musicTrack": - return item.get_uri() - return item.item_id + return cast(str, item.get_uri()) + return cast(str, item.item_id) -def get_media(media_library, item_id, search_type): +def get_media( + media_library: MusicLibrary, item_id: str, search_type: str +) -> MusicServiceItem: """Fetch media/album.""" search_type = MEDIA_TYPES_TO_SONOS.get(search_type, search_type) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 1f57cafbf09..14e0693f55a 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -130,11 +130,11 @@ async def async_setup_entry( if service_call.service == SERVICE_SNAPSHOT: await SonosSpeaker.snapshot_multi( - hass, speakers, service_call.data[ATTR_WITH_GROUP] # type: ignore[arg-type] + hass, speakers, service_call.data[ATTR_WITH_GROUP] ) elif service_call.service == SERVICE_RESTORE: await SonosSpeaker.restore_multi( - hass, speakers, service_call.data[ATTR_WITH_GROUP] # type: ignore[arg-type] + hass, speakers, service_call.data[ATTR_WITH_GROUP] ) config_entry.async_on_unload( @@ -153,7 +153,7 @@ async def async_setup_entry( SONOS_DOMAIN, SERVICE_RESTORE, async_service_handle, join_unjoin_schema ) - platform.async_register_entity_service( # type: ignore + platform.async_register_entity_service( SERVICE_SET_TIMER, { vol.Required(ATTR_SLEEP_TIME): vol.All( @@ -163,9 +163,9 @@ async def async_setup_entry( "set_sleep_timer", ) - platform.async_register_entity_service(SERVICE_CLEAR_TIMER, {}, "clear_sleep_timer") # type: ignore + platform.async_register_entity_service(SERVICE_CLEAR_TIMER, {}, "clear_sleep_timer") - platform.async_register_entity_service( # type: ignore + platform.async_register_entity_service( SERVICE_UPDATE_ALARM, { vol.Required(ATTR_ALARM_ID): cv.positive_int, @@ -177,13 +177,13 @@ async def async_setup_entry( "set_alarm", ) - platform.async_register_entity_service( # type: ignore + platform.async_register_entity_service( SERVICE_PLAY_QUEUE, {vol.Optional(ATTR_QUEUE_POSITION): cv.positive_int}, "play_queue", ) - platform.async_register_entity_service( # type: ignore + platform.async_register_entity_service( SERVICE_REMOVE_FROM_QUEUE, {vol.Optional(ATTR_QUEUE_POSITION): cv.positive_int}, "remove_from_queue", @@ -239,8 +239,8 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): """Return if the media_player is available.""" return ( self.speaker.available - and self.speaker.sonos_group_entities - and self.media.playback_status + and bool(self.speaker.sonos_group_entities) + and self.media.playback_status is not None ) @property @@ -257,7 +257,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): """Return a hash of self.""" return hash(self.unique_id) - @property # type: ignore[misc] + @property def state(self) -> str: """Return the state of the entity.""" if self.media.playback_status in ( @@ -300,13 +300,12 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): """Return true if volume is muted.""" return self.speaker.muted - @property # type: ignore[misc] - def shuffle(self) -> str | None: + @property + def shuffle(self) -> bool | None: """Shuffling state.""" - shuffle: str = PLAY_MODES[self.media.play_mode][0] - return shuffle + return PLAY_MODES[self.media.play_mode][0] - @property # type: ignore[misc] + @property def repeat(self) -> str | None: """Return current repeat mode.""" sonos_repeat = PLAY_MODES[self.media.play_mode][1] @@ -317,32 +316,32 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): """Return the SonosMedia object from the coordinator speaker.""" return self.coordinator.media - @property # type: ignore[misc] + @property def media_content_id(self) -> str | None: """Content id of current playing media.""" return self.media.uri - @property # type: ignore[misc] - def media_duration(self) -> float | None: + @property + def media_duration(self) -> int | None: """Duration of current playing media in seconds.""" - return self.media.duration + return int(self.media.duration) if self.media.duration else None - @property # type: ignore[misc] - def media_position(self) -> float | None: + @property + def media_position(self) -> int | None: """Position of current playing media in seconds.""" - return self.media.position + return int(self.media.position) if self.media.position else None - @property # type: ignore[misc] + @property def media_position_updated_at(self) -> datetime.datetime | None: """When was the position of the current playing media valid.""" return self.media.position_updated_at - @property # type: ignore[misc] + @property def media_image_url(self) -> str | None: """Image url of current playing media.""" return self.media.image_url or None - @property # type: ignore[misc] + @property def media_channel(self) -> str | None: """Channel currently playing.""" return self.media.channel or None @@ -352,22 +351,22 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): """Title of playlist currently playing.""" return self.media.playlist_name - @property # type: ignore[misc] + @property def media_artist(self) -> str | None: """Artist of current playing media, music track only.""" return self.media.artist or None - @property # type: ignore[misc] + @property def media_album_name(self) -> str | None: """Album name of current playing media, music track only.""" return self.media.album_name or None - @property # type: ignore[misc] + @property def media_title(self) -> str | None: """Title of current playing media.""" return self.media.title or None - @property # type: ignore[misc] + @property def source(self) -> str | None: """Name of the current input source.""" return self.media.source_name or None @@ -383,12 +382,12 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): self.soco.volume -= VOLUME_INCREMENT @soco_error() - def set_volume_level(self, volume: str) -> None: + def set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" self.soco.volume = str(int(volume * 100)) @soco_error(UPNP_ERRORS_TO_IGNORE) - def set_shuffle(self, shuffle: str) -> None: + def set_shuffle(self, shuffle: bool) -> None: """Enable/Disable shuffle mode.""" sonos_shuffle = shuffle sonos_repeat = PLAY_MODES[self.media.play_mode][1] @@ -486,7 +485,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): self.coordinator.soco.previous() @soco_error(UPNP_ERRORS_TO_IGNORE) - def media_seek(self, position: str) -> None: + def media_seek(self, position: float) -> None: """Send seek command.""" self.coordinator.soco.seek(str(datetime.timedelta(seconds=int(position)))) @@ -606,7 +605,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): soco.play_uri(media_id, force_radio=is_radio) elif media_type == MEDIA_TYPE_PLAYLIST: if media_id.startswith("S:"): - item = media_browser.get_media(self.media.library, media_id, media_type) # type: ignore[no-untyped-call] + item = media_browser.get_media(self.media.library, media_id, media_type) soco.play_uri(item.get_uri()) return try: @@ -619,7 +618,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): soco.add_to_queue(playlist) soco.play_from_queue(0) elif media_type in PLAYABLE_MEDIA_TYPES: - item = media_browser.get_media(self.media.library, media_id, media_type) # type: ignore[no-untyped-call] + item = media_browser.get_media(self.media.library, media_id, media_type) if not item: _LOGGER.error('Could not find "%s" in the library', media_id) @@ -649,7 +648,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): include_linked_zones: bool | None = None, ) -> None: """Set the alarm clock on the player.""" - alarm = None + alarm: alarms.Alarm | None = None for one_alarm in alarms.get_alarms(self.coordinator.soco): if one_alarm.alarm_id == str(alarm_id): alarm = one_alarm @@ -710,8 +709,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): MEDIA_TYPES_TO_SONOS[media_content_type], ) if image_url := getattr(item, "album_art_uri", None): - result = await self._async_fetch_image(image_url) # type: ignore[no-untyped-call] - return result # type: ignore + return await self._async_fetch_image(image_url) return (None, None) @@ -728,7 +726,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): media_content_type, ) - async def async_join_players(self, group_members): + async def async_join_players(self, group_members: list[str]) -> None: """Join `group_members` as a player group with the current player.""" speakers = [] for entity_id in group_members: @@ -739,7 +737,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): await SonosSpeaker.join_multi(self.hass, self.speaker, speakers) - async def async_unjoin_player(self): + async def async_unjoin_player(self) -> None: """Remove this player from any group. Coalesces all calls within UNJOIN_SERVICE_TIMEOUT to allow use of SonosSpeaker.unjoin_multi() diff --git a/homeassistant/components/sonos/number.py b/homeassistant/components/sonos/number.py index ccbcbc3c339..7a6edb0d293 100644 --- a/homeassistant/components/sonos/number.py +++ b/homeassistant/components/sonos/number.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import cast from homeassistant.components.number import NumberEntity from homeassistant.config_entries import ConfigEntry @@ -24,6 +25,8 @@ LEVEL_TYPES = { "music_surround_level": (-15, 15), } +SocoFeatures = list[tuple[str, tuple[int, int]]] + _LOGGER = logging.getLogger(__name__) @@ -34,8 +37,8 @@ async def async_setup_entry( ) -> None: """Set up the Sonos number platform from a config entry.""" - def available_soco_attributes(speaker: SonosSpeaker) -> list[str]: - features = [] + def available_soco_attributes(speaker: SonosSpeaker) -> SocoFeatures: + features: SocoFeatures = [] for level_type, valid_range in LEVEL_TYPES.items(): if (state := getattr(speaker.soco, level_type, None)) is not None: setattr(speaker, level_type, state) @@ -67,7 +70,7 @@ class SonosLevelEntity(SonosEntity, NumberEntity): _attr_entity_category = EntityCategory.CONFIG def __init__( - self, speaker: SonosSpeaker, level_type: str, valid_range: tuple[int] + self, speaker: SonosSpeaker, level_type: str, valid_range: tuple[int, int] ) -> None: """Initialize the level entity.""" super().__init__(speaker) @@ -94,4 +97,4 @@ class SonosLevelEntity(SonosEntity, NumberEntity): @property def native_value(self) -> float: """Return the current value.""" - return getattr(self.speaker, self.level_type) + return cast(float, getattr(self.speaker, self.level_type)) diff --git a/homeassistant/components/sonos/sensor.py b/homeassistant/components/sonos/sensor.py index 8477e523a40..d1705fb030d 100644 --- a/homeassistant/components/sonos/sensor.py +++ b/homeassistant/components/sonos/sensor.py @@ -100,7 +100,7 @@ class SonosBatteryEntity(SonosEntity, SensorEntity): @property def available(self) -> bool: """Return whether this device is available.""" - return self.speaker.available and self.speaker.power_source + return self.speaker.available and self.speaker.power_source is not None class SonosAudioInputFormatSensorEntity(SonosPollingEntity, SensorEntity): diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 0c5bec06dfb..516a431295a 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -8,7 +8,7 @@ import datetime from functools import partial import logging import time -from typing import Any +from typing import Any, cast import async_timeout import defusedxml.ElementTree as ET @@ -97,17 +97,17 @@ class SonosSpeaker: self.media = SonosMedia(hass, soco) self._plex_plugin: PlexPlugin | None = None self._share_link_plugin: ShareLinkPlugin | None = None - self.available = True + self.available: bool = True # Device information - self.hardware_version = speaker_info["hardware_version"] - self.software_version = speaker_info["software_version"] - self.mac_address = speaker_info["mac_address"] - self.model_name = speaker_info["model_name"] - self.model_number = speaker_info["model_number"] - self.uid = speaker_info["uid"] - self.version = speaker_info["display_version"] - self.zone_name = speaker_info["zone_name"] + self.hardware_version: str = speaker_info["hardware_version"] + self.software_version: str = speaker_info["software_version"] + self.mac_address: str = speaker_info["mac_address"] + self.model_name: str = speaker_info["model_name"] + self.model_number: str = speaker_info["model_number"] + self.uid: str = speaker_info["uid"] + self.version: str = speaker_info["display_version"] + self.zone_name: str = speaker_info["zone_name"] # Subscriptions and events self.subscriptions_failed: bool = False @@ -160,12 +160,12 @@ class SonosSpeaker: self.sonos_group: list[SonosSpeaker] = [self] self.sonos_group_entities: list[str] = [] self.soco_snapshot: Snapshot | None = None - self.snapshot_group: list[SonosSpeaker] | None = None + self.snapshot_group: list[SonosSpeaker] = [] self._group_members_missing: set[str] = set() async def async_setup_dispatchers(self, entry: ConfigEntry) -> None: """Connect dispatchers in async context during setup.""" - dispatch_pairs = ( + dispatch_pairs: tuple[tuple[str, Callable[..., Any]], ...] = ( (SONOS_CHECK_ACTIVITY, self.async_check_activity), (SONOS_SPEAKER_ADDED, self.update_group_for_uid), (f"{SONOS_REBOOTED}-{self.soco.uid}", self.async_rebooted), @@ -283,18 +283,17 @@ class SonosSpeaker: return self._share_link_plugin @property - def subscription_address(self) -> str | None: - """Return the current subscription callback address if any.""" - if self._subscriptions: - addr, port = self._subscriptions[0].event_listener.address - return ":".join([addr, str(port)]) - return None + def subscription_address(self) -> str: + """Return the current subscription callback address.""" + assert len(self._subscriptions) > 0 + addr, port = self._subscriptions[0].event_listener.address + return ":".join([addr, str(port)]) # # Subscription handling and event dispatchers # def log_subscription_result( - self, result: Any, event: str, level: str = logging.DEBUG + self, result: Any, event: str, level: int = logging.DEBUG ) -> None: """Log a message if a subscription action (create/renew/stop) results in an exception.""" if not isinstance(result, Exception): @@ -304,7 +303,7 @@ class SonosSpeaker: message = "Request timed out" exc_info = None else: - message = result + message = str(result) exc_info = result if not str(result) else None _LOGGER.log( @@ -554,7 +553,7 @@ class SonosSpeaker: ) @callback - def speaker_activity(self, source): + def speaker_activity(self, source: str) -> None: """Track the last activity on this speaker, set availability and resubscribe.""" if self._resub_cooldown_expires_at: if time.monotonic() < self._resub_cooldown_expires_at: @@ -593,6 +592,7 @@ class SonosSpeaker: async def async_offline(self) -> None: """Handle removal of speaker when unavailable.""" + assert self._subscription_lock is not None async with self._subscription_lock: await self._async_offline() @@ -826,8 +826,8 @@ class SonosSpeaker: if speaker: self._group_members_missing.discard(uid) sonos_group.append(speaker) - entity_id = entity_registry.async_get_entity_id( - MP_DOMAIN, DOMAIN, uid + entity_id = cast( + str, entity_registry.async_get_entity_id(MP_DOMAIN, DOMAIN, uid) ) sonos_group_entities.append(entity_id) else: @@ -850,7 +850,9 @@ class SonosSpeaker: self.async_write_entity_states() for joined_uid in group[1:]: - joined_speaker = self.hass.data[DATA_SONOS].discovered.get(joined_uid) + joined_speaker: SonosSpeaker = self.hass.data[ + DATA_SONOS + ].discovered.get(joined_uid) if joined_speaker: joined_speaker.coordinator = self joined_speaker.sonos_group = sonos_group @@ -936,7 +938,7 @@ class SonosSpeaker: if with_group: self.snapshot_group = self.sonos_group.copy() else: - self.snapshot_group = None + self.snapshot_group = [] @staticmethod async def snapshot_multi( @@ -969,7 +971,7 @@ class SonosSpeaker: _LOGGER.warning("Error on restore %s: %s", self.zone_name, ex) self.soco_snapshot = None - self.snapshot_group = None + self.snapshot_group = [] @staticmethod async def restore_multi( @@ -996,7 +998,7 @@ class SonosSpeaker: exc_info=exc, ) - groups = [] + groups: list[list[SonosSpeaker]] = [] if not with_group: return groups @@ -1022,7 +1024,7 @@ class SonosSpeaker: # Bring back the original group topology for speaker in (s for s in speakers if s.snapshot_group): - assert speaker.snapshot_group is not None + assert len(speaker.snapshot_group) if speaker.snapshot_group[0] == speaker: if speaker.snapshot_group not in (speaker.sonos_group, [speaker]): speaker.join(speaker.snapshot_group) @@ -1047,7 +1049,7 @@ class SonosSpeaker: if with_group: for speaker in [s for s in speakers_set if s.snapshot_group]: - assert speaker.snapshot_group is not None + assert len(speaker.snapshot_group) speakers_set.update(speaker.snapshot_group) async with hass.data[DATA_SONOS].topology_condition: diff --git a/homeassistant/components/sonos/statistics.py b/homeassistant/components/sonos/statistics.py index a850e5a8caf..b761469aea5 100644 --- a/homeassistant/components/sonos/statistics.py +++ b/homeassistant/components/sonos/statistics.py @@ -14,7 +14,7 @@ class SonosStatistics: def __init__(self, zone_name: str, kind: str) -> None: """Initialize SonosStatistics.""" - self._stats = {} + self._stats: dict[str, dict[str, int | float]] = {} self._stat_type = kind self.zone_name = zone_name diff --git a/homeassistant/components/sonos/switch.py b/homeassistant/components/sonos/switch.py index a348b40cb0f..acf33ea34aa 100644 --- a/homeassistant/components/sonos/switch.py +++ b/homeassistant/components/sonos/switch.py @@ -3,8 +3,9 @@ from __future__ import annotations import datetime import logging -from typing import Any +from typing import Any, cast +from soco.alarms import Alarm from soco.exceptions import SoCoSlaveException, SoCoUPnPException from homeassistant.components.switch import ENTITY_ID_FORMAT, SwitchEntity @@ -183,14 +184,14 @@ class SonosSwitchEntity(SonosPollingEntity, SwitchEntity): def is_on(self) -> bool: """Return True if entity is on.""" if self.needs_coordinator and not self.speaker.is_coordinator: - return getattr(self.speaker.coordinator, self.feature_type) - return getattr(self.speaker, self.feature_type) + return cast(bool, getattr(self.speaker.coordinator, self.feature_type)) + return cast(bool, getattr(self.speaker, self.feature_type)) - def turn_on(self, **kwargs) -> None: + def turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" self.send_command(True) - def turn_off(self, **kwargs) -> None: + def turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" self.send_command(False) @@ -233,7 +234,7 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity): ) @property - def alarm(self): + def alarm(self) -> Alarm: """Return the alarm instance.""" return self.hass.data[DATA_SONOS].alarms[self.household_id].get(self.alarm_id) @@ -247,7 +248,7 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity): await self.hass.data[DATA_SONOS].alarms[self.household_id].async_poll() @callback - def async_check_if_available(self): + def async_check_if_available(self) -> bool: """Check if alarm exists and remove alarm entity if not available.""" if self.alarm: return True @@ -279,7 +280,7 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity): self.async_write_ha_state() @callback - def _async_update_device(self): + def _async_update_device(self) -> None: """Update the device, since this alarm moved to a different player.""" device_registry = dr.async_get(self.hass) entity_registry = er.async_get(self.hass) @@ -288,22 +289,20 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity): if entity is None: raise RuntimeError("Alarm has been deleted by accident.") - entry_id = entity.config_entry_id - new_device = device_registry.async_get_or_create( - config_entry_id=entry_id, + config_entry_id=cast(str, entity.config_entry_id), identifiers={(SONOS_DOMAIN, self.soco.uid)}, connections={(dr.CONNECTION_NETWORK_MAC, self.speaker.mac_address)}, ) - if not entity_registry.async_get(self.entity_id).device_id == new_device.id: + if ( + device := entity_registry.async_get(self.entity_id) + ) and device.device_id != new_device.id: _LOGGER.debug("%s is moving to %s", self.entity_id, new_device.name) - # pylint: disable=protected-access - entity_registry._async_update_entity( - self.entity_id, device_id=new_device.id - ) + entity_registry.async_update_entity(self.entity_id, device_id=new_device.id) @property - def _is_today(self): + def _is_today(self) -> bool: + """Return whether this alarm is scheduled for today.""" recurrence = self.alarm.recurrence timestr = int(datetime.datetime.today().strftime("%w")) return ( @@ -321,12 +320,12 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity): return (self.alarm is not None) and self.speaker.available @property - def is_on(self): + def is_on(self) -> bool: """Return state of Sonos alarm switch.""" return self.alarm.enabled @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return attributes of Sonos alarm switch.""" return { ATTR_ID: str(self.alarm_id), diff --git a/mypy.ini b/mypy.ini index d6665cb40c8..957da7254eb 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2589,39 +2589,3 @@ disallow_untyped_decorators = false disallow_untyped_defs = false warn_return_any = false warn_unreachable = false - -[mypy-homeassistant.components.sonos] -ignore_errors = true - -[mypy-homeassistant.components.sonos.alarms] -ignore_errors = true - -[mypy-homeassistant.components.sonos.binary_sensor] -ignore_errors = true - -[mypy-homeassistant.components.sonos.diagnostics] -ignore_errors = true - -[mypy-homeassistant.components.sonos.entity] -ignore_errors = true - -[mypy-homeassistant.components.sonos.favorites] -ignore_errors = true - -[mypy-homeassistant.components.sonos.media_browser] -ignore_errors = true - -[mypy-homeassistant.components.sonos.media_player] -ignore_errors = true - -[mypy-homeassistant.components.sonos.number] -ignore_errors = true - -[mypy-homeassistant.components.sonos.sensor] -ignore_errors = true - -[mypy-homeassistant.components.sonos.speaker] -ignore_errors = true - -[mypy-homeassistant.components.sonos.statistics] -ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index b6c31751e12..0c598df9cd1 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -15,20 +15,7 @@ from .model import Config, Integration # If you are an author of component listed here, please fix these errors and # remove your component from this list to enable type checks. # Do your best to not add anything new here. -IGNORED_MODULES: Final[list[str]] = [ - "homeassistant.components.sonos", - "homeassistant.components.sonos.alarms", - "homeassistant.components.sonos.binary_sensor", - "homeassistant.components.sonos.diagnostics", - "homeassistant.components.sonos.entity", - "homeassistant.components.sonos.favorites", - "homeassistant.components.sonos.media_browser", - "homeassistant.components.sonos.media_player", - "homeassistant.components.sonos.number", - "homeassistant.components.sonos.sensor", - "homeassistant.components.sonos.speaker", - "homeassistant.components.sonos.statistics", -] +IGNORED_MODULES: Final[list[str]] = [] # Component modules which should set no_implicit_reexport = true. NO_IMPLICIT_REEXPORT_MODULES: set[str] = { From 6867029062e30967d74b56d7258def9711bb5564 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 5 Sep 2022 20:27:48 +0200 Subject: [PATCH 112/955] Update frontend to 20220905.0 (#77854) --- 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 8459d08eab7..416634053d6 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==20220902.0"], + "requirements": ["home-assistant-frontend==20220905.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 8b65b9c0285..3bf00427954 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -19,7 +19,7 @@ cryptography==37.0.4 fnvhash==0.1.0 hass-nabucasa==0.55.0 home-assistant-bluetooth==1.3.0 -home-assistant-frontend==20220902.0 +home-assistant-frontend==20220905.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index ac0f9a43f8f..1c458d114c4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -851,7 +851,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220902.0 +home-assistant-frontend==20220905.0 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9d70f106685..edf5f262844 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -628,7 +628,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220902.0 +home-assistant-frontend==20220905.0 # homeassistant.components.home_connect homeconnect==0.7.2 From 6644f62ad2c00f3d686f5042765a339f119a9e69 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 5 Sep 2022 13:56:27 -0500 Subject: [PATCH 113/955] Fix history stats device class when type is not time (#77855) --- .../components/history_stats/sensor.py | 3 +- tests/components/history_stats/test_sensor.py | 45 ++++++++++++++++++- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/history_stats/sensor.py b/homeassistant/components/history_stats/sensor.py index a42c516f12b..642c327e29d 100644 --- a/homeassistant/components/history_stats/sensor.py +++ b/homeassistant/components/history_stats/sensor.py @@ -143,7 +143,6 @@ class HistoryStatsSensorBase( class HistoryStatsSensor(HistoryStatsSensorBase): """A HistoryStats sensor.""" - _attr_device_class = SensorDeviceClass.DURATION _attr_state_class = SensorStateClass.MEASUREMENT def __init__( @@ -157,6 +156,8 @@ class HistoryStatsSensor(HistoryStatsSensorBase): self._attr_native_unit_of_measurement = UNITS[sensor_type] self._type = sensor_type self._process_update() + if self._type == CONF_TYPE_TIME: + self._attr_device_class = SensorDeviceClass.DURATION @callback def _process_update(self) -> None: diff --git a/tests/components/history_stats/test_sensor.py b/tests/components/history_stats/test_sensor.py index 8907f381a6c..5de74f71d1e 100644 --- a/tests/components/history_stats/test_sensor.py +++ b/tests/components/history_stats/test_sensor.py @@ -9,7 +9,7 @@ import pytest from homeassistant import config as hass_config from homeassistant.components.history_stats import DOMAIN -from homeassistant.const import SERVICE_RELOAD, STATE_UNKNOWN +from homeassistant.const import ATTR_DEVICE_CLASS, SERVICE_RELOAD, STATE_UNKNOWN import homeassistant.core as ha from homeassistant.helpers.entity_component import async_update_entity from homeassistant.setup import async_setup_component, setup_component @@ -1496,3 +1496,46 @@ async def test_end_time_with_microseconds_zeroed(time_zone, hass, recorder_mock) async_fire_time_changed(hass, rolled_to_next_day_plus_18) await hass.async_block_till_done() assert hass.states.get("sensor.heatpump_compressor_today").state == "16.0" + + +async def test_device_classes(hass, recorder_mock): + """Test the device classes.""" + await async_setup_component( + hass, + "sensor", + { + "sensor": [ + { + "platform": "history_stats", + "entity_id": "binary_sensor.test_id", + "name": "time", + "state": "on", + "start": "{{ as_timestamp(now()) - 3600 }}", + "end": "{{ as_timestamp(now()) + 3600 }}", + "type": "time", + }, + { + "platform": "history_stats", + "entity_id": "binary_sensor.test_id", + "name": "count", + "state": "on", + "start": "{{ as_timestamp(now()) - 3600 }}", + "end": "{{ as_timestamp(now()) + 3600 }}", + "type": "count", + }, + { + "platform": "history_stats", + "entity_id": "binary_sensor.test_id", + "name": "ratio", + "state": "on", + "start": "{{ as_timestamp(now()) - 3600 }}", + "end": "{{ as_timestamp(now()) + 3600 }}", + "type": "ratio", + }, + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get("sensor.time").attributes[ATTR_DEVICE_CLASS] == "duration" + assert ATTR_DEVICE_CLASS not in hass.states.get("sensor.ratio").attributes + assert ATTR_DEVICE_CLASS not in hass.states.get("sensor.count").attributes From 8280b8422c88bb33f89695912dc8f0b0b5979d18 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 5 Sep 2022 12:12:38 -0700 Subject: [PATCH 114/955] Remove google calendar configuration.yaml deprecated in 2022.6 (#77814) --- homeassistant/components/google/__init__.py | 98 +---------- homeassistant/components/google/strings.json | 10 -- tests/components/google/conftest.py | 77 ++------- tests/components/google/test_config_flow.py | 167 ++++--------------- tests/components/google/test_init.py | 158 ++---------------- 5 files changed, 61 insertions(+), 449 deletions(-) diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py index 7416a9d7793..33a1216085d 100644 --- a/homeassistant/components/google/__init__.py +++ b/homeassistant/components/google/__init__.py @@ -10,20 +10,12 @@ import aiohttp from gcal_sync.api import GoogleCalendarService from gcal_sync.exceptions import ApiException, AuthException from gcal_sync.model import DateOrDatetime, Event -from oauth2client.file import Storage import voluptuous as vol from voluptuous.error import Error as VoluptuousError import yaml -from homeassistant import config_entries -from homeassistant.components.application_credentials import ( - ClientCredential, - async_import_client_credential, -) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - CONF_CLIENT_ID, - CONF_CLIENT_SECRET, CONF_DEVICE_ID, CONF_ENTITIES, CONF_NAME, @@ -40,15 +32,10 @@ from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import generate_entity_id -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue -from homeassistant.helpers.typing import ConfigType from .api import ApiAuthImpl, get_feature_access from .const import ( - CONF_CALENDAR_ACCESS, - DATA_CONFIG, DATA_SERVICE, - DEVICE_AUTH_IMPL, DOMAIN, EVENT_DESCRIPTION, EVENT_END_DATE, @@ -79,37 +66,15 @@ DEFAULT_CONF_OFFSET = "!!" EVENT_CALENDAR_ID = "calendar_id" -NOTIFICATION_ID = "google_calendar_notification" -NOTIFICATION_TITLE = "Google Calendar Setup" -GROUP_NAME_ALL_CALENDARS = "Google Calendar Sensors" - SERVICE_ADD_EVENT = "add_event" YAML_DEVICES = f"{DOMAIN}_calendars.yaml" -TOKEN_FILE = f".{DOMAIN}.token" - PLATFORMS = [Platform.CALENDAR] -CONFIG_SCHEMA = vol.Schema( - vol.All( - cv.deprecated(DOMAIN), - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_CLIENT_ID): cv.string, - vol.Required(CONF_CLIENT_SECRET): cv.string, - vol.Optional(CONF_TRACK_NEW, default=True): cv.boolean, - vol.Optional(CONF_CALENDAR_ACCESS, default="read_write"): cv.enum( - FeatureAccess - ), - } - ) - }, - ), - extra=vol.ALLOW_EXTRA, -) +CONFIG_SCHEMA = vol.Schema(cv.removed(DOMAIN), extra=vol.ALLOW_EXTRA) + _SINGLE_CALSEARCH_CONFIG = vol.All( cv.deprecated(CONF_MAX_RESULTS), @@ -171,65 +136,6 @@ ADD_EVENT_SERVICE_SCHEMA = vol.All( ) -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the Google component.""" - if DOMAIN not in config: - return True - - conf = config.get(DOMAIN, {}) - hass.data[DOMAIN] = {DATA_CONFIG: conf} - - if CONF_CLIENT_ID in conf and CONF_CLIENT_SECRET in conf: - await async_import_client_credential( - hass, - DOMAIN, - ClientCredential( - conf[CONF_CLIENT_ID], - conf[CONF_CLIENT_SECRET], - ), - DEVICE_AUTH_IMPL, - ) - - # Import credentials from the old token file into the new way as - # a ConfigEntry managed by home assistant. - storage = Storage(hass.config.path(TOKEN_FILE)) - creds = await hass.async_add_executor_job(storage.get) - if creds and get_feature_access(hass).scope in creds.scopes: - _LOGGER.debug("Importing configuration entry with credentials") - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - "creds": creds, - }, - ) - ) - async_create_issue( - hass, - DOMAIN, - "deprecated_yaml", - breaks_in_ha_version="2022.9.0", # Warning first added in 2022.6.0 - is_fixable=False, - severity=IssueSeverity.WARNING, - translation_key="deprecated_yaml", - ) - if conf.get(CONF_TRACK_NEW) is False: - # The track_new as False would previously result in new entries - # in google_calendars.yaml with track set to False which is - # handled at calendar entity creation time. - async_create_issue( - hass, - DOMAIN, - "removed_track_new_yaml", - breaks_in_ha_version="2022.6.0", - is_fixable=False, - severity=IssueSeverity.WARNING, - translation_key="removed_track_new_yaml", - ) - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Google from a config entry.""" hass.data.setdefault(DOMAIN, {}) diff --git a/homeassistant/components/google/strings.json b/homeassistant/components/google/strings.json index 58e5cedd98d..b4c5270e003 100644 --- a/homeassistant/components/google/strings.json +++ b/homeassistant/components/google/strings.json @@ -41,15 +41,5 @@ }, "application_credentials": { "description": "Follow the [instructions]({more_info_url}) for [OAuth consent screen]({oauth_consent_url}) to give Home Assistant access to your Google Calendar. You also need to create Application Credentials linked to your Calendar:\n1. Go to [Credentials]({oauth_creds_url}) and click **Create Credentials**.\n1. From the drop-down list select **OAuth client ID**.\n1. Select **TV and Limited Input devices** for the Application Type.\n\n" - }, - "issues": { - "deprecated_yaml": { - "title": "The Google Calendar YAML configuration is being removed", - "description": "Configuring the Google Calendar in configuration.yaml is being removed in Home Assistant 2022.9.\n\nYour existing OAuth Application Credentials and access settings have been imported into the UI automatically. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." - }, - "removed_track_new_yaml": { - "title": "Google Calendar entity tracking has changed", - "description": "You have disabled entity tracking for Google Calendar in configuration.yaml, which is no longer supported. You must manually change the integration System Options in the UI to disable newly discovered entities going forward. Remove the track_new setting from configuration.yaml and restart Home Assistant to fix this issue." - } } } diff --git a/tests/components/google/conftest.py b/tests/components/google/conftest.py index 7e668de2891..e6b7c26ca84 100644 --- a/tests/components/google/conftest.py +++ b/tests/components/google/conftest.py @@ -9,12 +9,15 @@ from unittest.mock import Mock, mock_open, patch from aiohttp.client_exceptions import ClientError from gcal_sync.auth import API_BASE_URL -from oauth2client.client import Credentials, OAuth2Credentials +from oauth2client.client import OAuth2Credentials import pytest import yaml -from homeassistant.components.google import CONF_TRACK_NEW, DOMAIN -from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET +from homeassistant.components.application_credentials import ( + ClientCredential, + async_import_client_credential, +) +from homeassistant.components.google import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -115,22 +118,6 @@ def mock_calendars_yaml( yield mocked_open_function -class FakeStorage: - """A fake storage object for persiting creds.""" - - def __init__(self) -> None: - """Initialize FakeStorage.""" - self._creds: Credentials | None = None - - def get(self) -> Credentials | None: - """Get credentials from storage.""" - return self._creds - - def put(self, creds: Credentials) -> None: - """Put credentials in storage.""" - self._creds = creds - - @pytest.fixture def token_scopes() -> list[str]: """Fixture for scopes used during test.""" @@ -163,14 +150,6 @@ def creds( ) -@pytest.fixture(autouse=True) -def storage() -> YieldFixture[FakeStorage]: - """Fixture to populate an existing token file for read on startup.""" - storage = FakeStorage() - with patch("homeassistant.components.google.Storage", return_value=storage): - yield storage - - @pytest.fixture def config_entry_token_expiry(token_expiry: datetime.datetime) -> float: """Fixture for token expiration value stored in the config entry.""" @@ -214,16 +193,6 @@ def config_entry( ) -@pytest.fixture -def mock_token_read( - hass: HomeAssistant, - creds: OAuth2Credentials, - storage: FakeStorage, -) -> None: - """Fixture to populate an existing token file for read on startup.""" - storage.put(creds) - - @pytest.fixture def mock_events_list( aioclient_mock: AiohttpClientMocker, @@ -327,33 +296,17 @@ def set_time_zone(hass): @pytest.fixture -def google_config_track_new() -> None: - """Fixture for tests to set the 'track_new' configuration.yaml setting.""" - return None - - -@pytest.fixture -def google_config(google_config_track_new: bool | None) -> dict[str, Any]: - """Fixture for overriding component config.""" - google_config = {CONF_CLIENT_ID: CLIENT_ID, CONF_CLIENT_SECRET: CLIENT_SECRET} - if google_config_track_new is not None: - google_config[CONF_TRACK_NEW] = google_config_track_new - return google_config - - -@pytest.fixture -def config(google_config: dict[str, Any]) -> dict[str, Any]: - """Fixture for overriding component config.""" - return {DOMAIN: google_config} if google_config else {} - - -@pytest.fixture -def component_setup(hass: HomeAssistant, config: dict[str, Any]) -> ComponentSetup: +def component_setup( + hass: HomeAssistant, config_entry: MockConfigEntry +) -> ComponentSetup: """Fixture for setting up the integration.""" async def _setup_func() -> bool: - result = await async_setup_component(hass, DOMAIN, config) - await hass.async_block_till_done() - return result + assert await async_setup_component(hass, "application_credentials", {}) + await async_import_client_credential( + hass, DOMAIN, ClientCredential("client-id", "client-secret"), "device_auth" + ) + config_entry.add_to_hass(hass) + return await hass.config_entries.async_setup(config_entry.entry_id) return _setup_func diff --git a/tests/components/google/test_config_flow.py b/tests/components/google/test_config_flow.py index 5c373fb2219..d8ddd6fe588 100644 --- a/tests/components/google/test_config_flow.py +++ b/tests/components/google/test_config_flow.py @@ -4,11 +4,9 @@ from __future__ import annotations from collections.abc import Callable import datetime -from typing import Any from unittest.mock import Mock, patch from aiohttp.client_exceptions import ClientError -from freezegun.api import FrozenDateTimeFactory from oauth2client.client import ( DeviceFlowInfo, FlowExchangeError, @@ -26,15 +24,10 @@ from homeassistant.components.google.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.helpers import config_entry_oauth2_flow +from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow -from .conftest import ( - CLIENT_ID, - CLIENT_SECRET, - EMAIL_ADDRESS, - ComponentSetup, - YieldFixture, -) +from .conftest import CLIENT_ID, CLIENT_SECRET, EMAIL_ADDRESS, YieldFixture from tests.common import MockConfigEntry, async_fire_time_changed @@ -48,6 +41,12 @@ async def request_setup(current_request_with_host) -> None: return +@pytest.fixture(autouse=True) +async def setup_app_creds(hass: HomeAssistant) -> None: + """Fixture to setup application credentials component.""" + await async_setup_component(hass, "application_credentials", {}) + + @pytest.fixture async def code_expiration_delta() -> datetime.timedelta: """Fixture for code expiration time, defaulting to the future.""" @@ -117,74 +116,12 @@ async def fire_alarm(hass, point_in_time): await hass.async_block_till_done() -async def test_full_flow_yaml_creds( - hass: HomeAssistant, - mock_code_flow: Mock, - mock_exchange: Mock, - component_setup: ComponentSetup, - freezer: FrozenDateTimeFactory, -) -> None: - """Test successful creds setup.""" - assert await component_setup() - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result.get("type") == "progress" - assert result.get("step_id") == "auth" - assert "description_placeholders" in result - assert "url" in result["description_placeholders"] - - with patch( - "homeassistant.components.google.async_setup_entry", return_value=True - ) as mock_setup: - # Run one tick to invoke the credential exchange check - now = utcnow() - await fire_alarm(hass, now + CODE_CHECK_ALARM_TIMEDELTA) - result = await hass.config_entries.flow.async_configure( - flow_id=result["flow_id"] - ) - - assert result.get("type") == "create_entry" - assert result.get("title") == EMAIL_ADDRESS - assert "data" in result - data = result["data"] - assert "token" in data - assert 0 < data["token"]["expires_in"] <= 60 * 60 - assert ( - datetime.datetime.now().timestamp() - <= data["token"]["expires_at"] - < (datetime.datetime.now() + datetime.timedelta(days=8)).timestamp() - ) - data["token"].pop("expires_at") - data["token"].pop("expires_in") - assert data == { - "auth_implementation": "device_auth", - "token": { - "access_token": "ACCESS_TOKEN", - "refresh_token": "REFRESH_TOKEN", - "scope": "https://www.googleapis.com/auth/calendar", - "token_type": "Bearer", - }, - } - assert result.get("options") == {"calendar_access": "read_write"} - - assert len(mock_setup.mock_calls) == 1 - entries = hass.config_entries.async_entries(DOMAIN) - assert len(entries) == 1 - - -@pytest.mark.parametrize("google_config", [None]) async def test_full_flow_application_creds( hass: HomeAssistant, mock_code_flow: Mock, mock_exchange: Mock, - config: dict[str, Any], - component_setup: ComponentSetup, ) -> None: """Test successful creds setup.""" - assert await component_setup() - await async_import_client_credential( hass, DOMAIN, ClientCredential(CLIENT_ID, CLIENT_SECRET), "imported-cred" ) @@ -240,10 +177,11 @@ async def test_full_flow_application_creds( async def test_code_error( hass: HomeAssistant, mock_code_flow: Mock, - component_setup: ComponentSetup, ) -> None: """Test server error setting up the oauth flow.""" - assert await component_setup() + await async_import_client_credential( + hass, DOMAIN, ClientCredential(CLIENT_ID, CLIENT_SECRET), "imported-cred" + ) with patch( "homeassistant.components.google.api.OAuth2WebServerFlow.step1_get_device_and_user_codes", @@ -259,10 +197,11 @@ async def test_code_error( async def test_timeout_error( hass: HomeAssistant, mock_code_flow: Mock, - component_setup: ComponentSetup, ) -> None: """Test timeout error setting up the oauth flow.""" - assert await component_setup() + await async_import_client_credential( + hass, DOMAIN, ClientCredential(CLIENT_ID, CLIENT_SECRET), "imported-cred" + ) with patch( "homeassistant.components.google.api.OAuth2WebServerFlow.step1_get_device_and_user_codes", @@ -279,10 +218,11 @@ async def test_timeout_error( async def test_expired_after_exchange( hass: HomeAssistant, mock_code_flow: Mock, - component_setup: ComponentSetup, ) -> None: """Test credential exchange expires.""" - assert await component_setup() + await async_import_client_credential( + hass, DOMAIN, ClientCredential(CLIENT_ID, CLIENT_SECRET), "imported-cred" + ) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -310,10 +250,11 @@ async def test_exchange_error( hass: HomeAssistant, mock_code_flow: Mock, mock_exchange: Mock, - component_setup: ComponentSetup, ) -> None: """Test an error while exchanging the code for credentials.""" - assert await component_setup() + await async_import_client_credential( + hass, DOMAIN, ClientCredential(CLIENT_ID, CLIENT_SECRET), "device_auth" + ) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -371,17 +312,13 @@ async def test_exchange_error( assert len(entries) == 1 -@pytest.mark.parametrize("google_config", [None]) async def test_duplicate_config_entries( hass: HomeAssistant, mock_code_flow: Mock, mock_exchange: Mock, - config: dict[str, Any], config_entry: MockConfigEntry, - component_setup: ComponentSetup, ) -> None: """Test that the same account cannot be setup twice.""" - assert await component_setup() await async_import_client_credential( hass, DOMAIN, ClientCredential(CLIENT_ID, CLIENT_SECRET), "imported-cred" ) @@ -416,19 +353,14 @@ async def test_duplicate_config_entries( assert result.get("reason") == "already_configured" -@pytest.mark.parametrize( - "google_config,primary_calendar_email", [(None, "another-email@example.com")] -) +@pytest.mark.parametrize("primary_calendar_email", ["another-email@example.com"]) async def test_multiple_config_entries( hass: HomeAssistant, mock_code_flow: Mock, mock_exchange: Mock, - config: dict[str, Any], config_entry: MockConfigEntry, - component_setup: ComponentSetup, ) -> None: """Test that multiple config entries can be set at once.""" - assert await component_setup() await async_import_client_credential( hass, DOMAIN, ClientCredential(CLIENT_ID, CLIENT_SECRET), "imported-cred" ) @@ -483,21 +415,6 @@ async def test_missing_configuration( assert result.get("reason") == "missing_credentials" -@pytest.mark.parametrize("google_config", [None]) -async def test_missing_configuration_yaml_empty( - hass: HomeAssistant, - component_setup: ComponentSetup, -) -> None: - """Test setup with an empty yaml configuration and no credentials.""" - assert await component_setup() - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result.get("type") == "abort" - assert result.get("reason") == "missing_credentials" - - async def test_wrong_configuration( hass: HomeAssistant, ) -> None: @@ -524,36 +441,10 @@ async def test_wrong_configuration( assert result.get("reason") == "oauth_error" -async def test_import_config_entry_from_existing_token( - hass: HomeAssistant, - mock_token_read: None, - component_setup: ComponentSetup, -) -> None: - """Test setup with an existing token file.""" - assert await component_setup() - - entries = hass.config_entries.async_entries(DOMAIN) - assert len(entries) == 1 - data = entries[0].data - assert "token" in data - data["token"].pop("expires_at") - data["token"].pop("expires_in") - assert data == { - "auth_implementation": "device_auth", - "token": { - "access_token": "ACCESS_TOKEN", - "refresh_token": "REFRESH_TOKEN", - "scope": "https://www.googleapis.com/auth/calendar", - "token_type": "Bearer", - }, - } - - async def test_reauth_flow( hass: HomeAssistant, mock_code_flow: Mock, mock_exchange: Mock, - component_setup: ComponentSetup, ) -> None: """Test can't configure when config entry already exists.""" config_entry = MockConfigEntry( @@ -564,12 +455,13 @@ async def test_reauth_flow( }, ) config_entry.add_to_hass(hass) + await async_import_client_credential( + hass, DOMAIN, ClientCredential(CLIENT_ID, CLIENT_SECRET), "device_auth" + ) entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 - assert await component_setup() - result = await hass.config_entries.flow.async_init( DOMAIN, context={ @@ -628,10 +520,11 @@ async def test_calendar_lookup_failure( hass: HomeAssistant, mock_code_flow: Mock, mock_exchange: Mock, - component_setup: ComponentSetup, ) -> None: """Test successful config flow and title fetch fails gracefully.""" - assert await component_setup() + await async_import_client_credential( + hass, DOMAIN, ClientCredential(CLIENT_ID, CLIENT_SECRET), "device_auth" + ) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -656,7 +549,6 @@ async def test_calendar_lookup_failure( async def test_options_flow_triggers_reauth( hass: HomeAssistant, - component_setup: ComponentSetup, config_entry: MockConfigEntry, ) -> None: """Test load and unload of a ConfigEntry.""" @@ -665,7 +557,7 @@ async def test_options_flow_triggers_reauth( with patch( "homeassistant.components.google.async_setup_entry", return_value=True ) as mock_setup: - await component_setup() + await hass.config_entries.async_setup(config_entry.entry_id) mock_setup.assert_called_once() assert config_entry.state is ConfigEntryState.LOADED @@ -689,7 +581,6 @@ async def test_options_flow_triggers_reauth( async def test_options_flow_no_changes( hass: HomeAssistant, - component_setup: ComponentSetup, config_entry: MockConfigEntry, ) -> None: """Test load and unload of a ConfigEntry.""" @@ -698,7 +589,7 @@ async def test_options_flow_no_changes( with patch( "homeassistant.components.google.async_setup_entry", return_value=True ) as mock_setup: - await component_setup() + await hass.config_entries.async_setup(config_entry.entry_id) mock_setup.assert_called_once() assert config_entry.state is ConfigEntryState.LOADED diff --git a/tests/components/google/test_init.py b/tests/components/google/test_init.py index f6c1f8c611f..613aa6dbb70 100644 --- a/tests/components/google/test_init.py +++ b/tests/components/google/test_init.py @@ -12,10 +12,6 @@ from aiohttp.client_exceptions import ClientError import pytest import voluptuous as vol -from homeassistant.components.application_credentials import ( - ClientCredential, - async_import_client_credential, -) from homeassistant.components.google import DOMAIN, SERVICE_ADD_EVENT from homeassistant.components.google.calendar import SERVICE_CREATE_EVENT from homeassistant.components.google.const import CONF_CALENDAR_ACCESS @@ -23,7 +19,6 @@ from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ATTR_FRIENDLY_NAME, STATE_OFF from homeassistant.core import HomeAssistant, State from homeassistant.exceptions import HomeAssistantError -from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow from .conftest import ( @@ -32,7 +27,6 @@ from .conftest import ( TEST_API_ENTITY, TEST_API_ENTITY_NAME, TEST_YAML_ENTITY, - TEST_YAML_ENTITY_NAME, ApiResult, ComponentSetup, ) @@ -59,15 +53,6 @@ def assert_state(actual: State | None, expected: State | None) -> None: assert actual.attributes == expected.attributes -@pytest.fixture -def setup_config_entry( - hass: HomeAssistant, config_entry: MockConfigEntry -) -> MockConfigEntry: - """Fixture to initialize the config entry.""" - config_entry.add_to_hass(hass) - return config_entry - - @pytest.fixture( params=[ ( @@ -110,7 +95,6 @@ def add_event_call_service( async def test_unload_entry( hass: HomeAssistant, component_setup: ComponentSetup, - setup_config_entry: MockConfigEntry, ) -> None: """Test load and unload of a ConfigEntry.""" await component_setup() @@ -134,8 +118,7 @@ async def test_existing_token_missing_scope( config_entry: MockConfigEntry, ) -> None: """Test setup where existing token does not have sufficient scopes.""" - config_entry.add_to_hass(hass) - assert await component_setup() + await component_setup() entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 @@ -154,8 +137,7 @@ async def test_config_entry_scope_reauth( config_entry: MockConfigEntry, ) -> None: """Test setup where the config entry options requires reauth to match the scope.""" - config_entry.add_to_hass(hass) - assert await component_setup() + await component_setup() assert config_entry.state is ConfigEntryState.SETUP_ERROR @@ -170,7 +152,6 @@ async def test_calendar_yaml_missing_required_fields( component_setup: ComponentSetup, calendars_config: list[dict[str, Any]], mock_calendars_yaml: None, - setup_config_entry: MockConfigEntry, ) -> None: """Test setup with a missing schema fields, ignores the error and continues.""" assert await component_setup() @@ -187,7 +168,6 @@ async def test_invalid_calendar_yaml( mock_calendars_list: ApiResult, test_api_calendar: dict[str, Any], mock_events_list: ApiResult, - setup_config_entry: MockConfigEntry, ) -> None: """Test setup with missing entity id fields fails to load the platform.""" mock_calendars_list({"items": [test_api_calendar]}) @@ -210,7 +190,6 @@ async def test_calendar_yaml_error( mock_calendars_list: ApiResult, test_api_calendar: dict[str, Any], mock_events_list: ApiResult, - setup_config_entry: MockConfigEntry, ) -> None: """Test setup with yaml file not found.""" mock_calendars_list({"items": [test_api_calendar]}) @@ -229,7 +208,6 @@ async def test_init_calendar( mock_calendars_list: ApiResult, test_api_calendar: dict[str, Any], mock_events_list: ApiResult, - setup_config_entry: MockConfigEntry, ) -> None: """Test finding a calendar from the API.""" @@ -246,37 +224,6 @@ async def test_init_calendar( assert not hass.states.get(TEST_YAML_ENTITY) -@pytest.mark.parametrize( - "google_config,config_entry_options", - [({}, {CONF_CALENDAR_ACCESS: "read_write"})], -) -async def test_load_application_credentials( - hass: HomeAssistant, - component_setup: ComponentSetup, - mock_calendars_list: ApiResult, - test_api_calendar: dict[str, Any], - mock_events_list: ApiResult, - setup_config_entry: MockConfigEntry, -) -> None: - """Test loading an application credentials and a config entry.""" - assert await async_setup_component(hass, "application_credentials", {}) - await async_import_client_credential( - hass, DOMAIN, ClientCredential("client-id", "client-secret"), "device_auth" - ) - - mock_calendars_list({"items": [test_api_calendar]}) - mock_events_list({}) - assert await component_setup() - - state = hass.states.get(TEST_API_ENTITY) - assert state - assert state.name == TEST_API_ENTITY_NAME - assert state.state == STATE_OFF - - # No yaml config loaded that overwrites the entity name - assert not hass.states.get(TEST_YAML_ENTITY) - - async def test_multiple_config_entries( hass: HomeAssistant, component_setup: ComponentSetup, @@ -330,71 +277,6 @@ async def test_multiple_config_entries( assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Example calendar 2" -@pytest.mark.parametrize( - "calendars_config_track,expected_state,google_config_track_new", - [ - ( - True, - State( - TEST_YAML_ENTITY, - STATE_OFF, - attributes={ - "offset_reached": False, - "friendly_name": TEST_YAML_ENTITY_NAME, - }, - ), - None, - ), - ( - True, - State( - TEST_YAML_ENTITY, - STATE_OFF, - attributes={ - "offset_reached": False, - "friendly_name": TEST_YAML_ENTITY_NAME, - }, - ), - True, - ), - ( - True, - State( - TEST_YAML_ENTITY, - STATE_OFF, - attributes={ - "offset_reached": False, - "friendly_name": TEST_YAML_ENTITY_NAME, - }, - ), - False, # Has no effect - ), - (False, None, None), - (False, None, True), - (False, None, False), - ], -) -async def test_calendar_config_track_new( - hass: HomeAssistant, - component_setup: ComponentSetup, - mock_calendars_yaml: None, - mock_calendars_list: ApiResult, - mock_events_list: ApiResult, - test_api_calendar: dict[str, Any], - calendars_config_track: bool, - expected_state: State, - setup_config_entry: MockConfigEntry, -) -> None: - """Test calendar config that overrides whether or not a calendar is tracked.""" - - mock_calendars_list({"items": [test_api_calendar]}) - mock_events_list({}) - assert await component_setup() - - state = hass.states.get(TEST_YAML_ENTITY) - assert_state(state, expected_state) - - @pytest.mark.parametrize( "date_fields,expected_error,error_match", [ @@ -519,7 +401,6 @@ async def test_add_event_invalid_params( mock_calendars_list: ApiResult, test_api_calendar: dict[str, Any], mock_events_list: ApiResult, - setup_config_entry: MockConfigEntry, add_event_call_service: Callable[dict[str, Any], Awaitable[None]], date_fields: dict[str, Any], expected_error: type[Exception], @@ -561,7 +442,6 @@ async def test_add_event_date_in_x( date_fields: dict[str, Any], start_timedelta: datetime.timedelta, end_timedelta: datetime.timedelta, - setup_config_entry: MockConfigEntry, aioclient_mock: AiohttpClientMocker, add_event_call_service: Callable[dict[str, Any], Awaitable[None]], ) -> None: @@ -597,7 +477,6 @@ async def test_add_event_date( test_api_calendar: dict[str, Any], mock_insert_event: Callable[[str, dict[str, Any]], None], mock_events_list: ApiResult, - setup_config_entry: MockConfigEntry, aioclient_mock: AiohttpClientMocker, add_event_call_service: Callable[dict[str, Any], Awaitable[None]], ) -> None: @@ -638,7 +517,6 @@ async def test_add_event_date_time( mock_insert_event: Callable[[str, dict[str, Any]], None], test_api_calendar: dict[str, Any], mock_events_list: ApiResult, - setup_config_entry: MockConfigEntry, aioclient_mock: AiohttpClientMocker, add_event_call_service: Callable[dict[str, Any], Awaitable[None]], ) -> None: @@ -685,7 +563,6 @@ async def test_add_event_failure( test_api_calendar: dict[str, Any], mock_events_list: ApiResult, mock_insert_event: Callable[[..., dict[str, Any]], None], - setup_config_entry: MockConfigEntry, add_event_call_service: Callable[dict[str, Any], Awaitable[None]], ) -> None: """Test service calls with incorrect fields.""" @@ -711,7 +588,6 @@ async def test_add_event_failure( async def test_invalid_token_expiry_in_config_entry( hass: HomeAssistant, component_setup: ComponentSetup, - setup_config_entry: MockConfigEntry, aioclient_mock: AiohttpClientMocker, ) -> None: """Exercise case in issue #69623 with invalid token expiration persisted.""" @@ -743,7 +619,6 @@ async def test_invalid_token_expiry_in_config_entry( async def test_expired_token_refresh_internal_error( hass: HomeAssistant, component_setup: ComponentSetup, - setup_config_entry: MockConfigEntry, aioclient_mock: AiohttpClientMocker, ) -> None: """Generic errors on reauth are treated as a retryable setup error.""" @@ -753,7 +628,7 @@ async def test_expired_token_refresh_internal_error( status=http.HTTPStatus.INTERNAL_SERVER_ERROR, ) - assert await component_setup() + await component_setup() entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 @@ -767,7 +642,6 @@ async def test_expired_token_refresh_internal_error( async def test_expired_token_requires_reauth( hass: HomeAssistant, component_setup: ComponentSetup, - setup_config_entry: MockConfigEntry, aioclient_mock: AiohttpClientMocker, ) -> None: """Test case where reauth is required for token that cannot be refreshed.""" @@ -777,7 +651,7 @@ async def test_expired_token_requires_reauth( status=http.HTTPStatus.BAD_REQUEST, ) - assert await component_setup() + await component_setup() entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 @@ -811,7 +685,6 @@ async def test_calendar_yaml_update( mock_calendars_list: ApiResult, test_api_calendar: dict[str, Any], mock_events_list: ApiResult, - setup_config_entry: MockConfigEntry, calendars_config: dict[str, Any], expect_write_calls: bool, ) -> None: @@ -836,7 +709,6 @@ async def test_calendar_yaml_update( async def test_update_will_reload( hass: HomeAssistant, component_setup: ComponentSetup, - setup_config_entry: Any, mock_calendars_list: ApiResult, test_api_calendar: dict[str, Any], mock_events_list: ApiResult, @@ -887,12 +759,12 @@ async def test_assign_unique_id( test_api_calendar: dict[str, Any], mock_events_list: ApiResult, mock_calendar_get: Callable[[...], None], - setup_config_entry: MockConfigEntry, + config_entry: MockConfigEntry, ) -> None: """Test an existing config is updated to have unique id if it does not exist.""" - assert setup_config_entry.state is ConfigEntryState.NOT_LOADED - assert setup_config_entry.unique_id is None + assert config_entry.state is ConfigEntryState.NOT_LOADED + assert config_entry.unique_id is None mock_calendar_get( "primary", @@ -903,8 +775,8 @@ async def test_assign_unique_id( mock_events_list({}) assert await component_setup() - assert setup_config_entry.state is ConfigEntryState.LOADED - assert setup_config_entry.unique_id == EMAIL_ADDRESS + assert config_entry.state is ConfigEntryState.LOADED + assert config_entry.unique_id == EMAIL_ADDRESS @pytest.mark.parametrize( @@ -923,16 +795,16 @@ async def test_assign_unique_id_failure( component_setup: ComponentSetup, mock_calendars_list: ApiResult, test_api_calendar: dict[str, Any], + config_entry: MockConfigEntry, mock_events_list: ApiResult, mock_calendar_get: Callable[[...], None], - setup_config_entry: MockConfigEntry, request_status: http.HTTPStatus, config_entry_status: ConfigEntryState, ) -> None: """Test lookup failures during unique id assignment are handled gracefully.""" - assert setup_config_entry.state is ConfigEntryState.NOT_LOADED - assert setup_config_entry.unique_id is None + assert config_entry.state is ConfigEntryState.NOT_LOADED + assert config_entry.unique_id is None mock_calendar_get( "primary", @@ -942,7 +814,7 @@ async def test_assign_unique_id_failure( mock_calendars_list({"items": [test_api_calendar]}) mock_events_list({}) - assert await component_setup() + await component_setup() - assert setup_config_entry.state is config_entry_status - assert setup_config_entry.unique_id is None + assert config_entry.state is config_entry_status + assert config_entry.unique_id is None From 76006ce9d7d056674826142bf45e5eef252bb31b Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 5 Sep 2022 21:50:47 +0200 Subject: [PATCH 115/955] Allow empty db in SQL options flow (#77777) --- homeassistant/components/sql/config_flow.py | 7 +- tests/components/sql/test_config_flow.py | 75 ++++++++++++++++++--- 2 files changed, 71 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/sql/config_flow.py b/homeassistant/components/sql/config_flow.py index dc3a839ef1d..bcbece9f7f6 100644 --- a/homeassistant/components/sql/config_flow.py +++ b/homeassistant/components/sql/config_flow.py @@ -147,9 +147,12 @@ class SQLOptionsFlowHandler(config_entries.OptionsFlow): ) -> FlowResult: """Manage SQL options.""" errors = {} + db_url_default = DEFAULT_URL.format( + hass_config_path=self.hass.config.path(DEFAULT_DB_FILE) + ) if user_input is not None: - db_url = user_input[CONF_DB_URL] + db_url = user_input.get(CONF_DB_URL, db_url_default) query = user_input[CONF_QUERY] column = user_input[CONF_COLUMN_NAME] @@ -176,7 +179,7 @@ class SQLOptionsFlowHandler(config_entries.OptionsFlow): step_id="init", data_schema=vol.Schema( { - vol.Required( + vol.Optional( CONF_DB_URL, description={ "suggested_value": self.entry.options[CONF_DB_URL] diff --git a/tests/components/sql/test_config_flow.py b/tests/components/sql/test_config_flow.py index 7c3571f8f19..96402e1bc7a 100644 --- a/tests/components/sql/test_config_flow.py +++ b/tests/components/sql/test_config_flow.py @@ -20,7 +20,7 @@ from . import ( from tests.common import MockConfigEntry -async def test_form(hass: HomeAssistant) -> None: +async def test_form(hass: HomeAssistant, recorder_mock) -> None: """Test we get the form.""" result = await hass.config_entries.flow.async_init( @@ -52,7 +52,7 @@ async def test_form(hass: HomeAssistant) -> None: assert len(mock_setup_entry.mock_calls) == 1 -async def test_import_flow_success(hass: HomeAssistant) -> None: +async def test_import_flow_success(hass: HomeAssistant, recorder_mock) -> None: """Test a successful import of yaml.""" with patch( @@ -79,7 +79,7 @@ async def test_import_flow_success(hass: HomeAssistant) -> None: assert len(mock_setup_entry.mock_calls) == 1 -async def test_import_flow_already_exist(hass: HomeAssistant) -> None: +async def test_import_flow_already_exist(hass: HomeAssistant, recorder_mock) -> None: """Test import of yaml already exist.""" MockConfigEntry( @@ -102,7 +102,7 @@ async def test_import_flow_already_exist(hass: HomeAssistant) -> None: assert result3["reason"] == "already_configured" -async def test_flow_fails_db_url(hass: HomeAssistant) -> None: +async def test_flow_fails_db_url(hass: HomeAssistant, recorder_mock) -> None: """Test config flow fails incorrect db url.""" result4 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -123,7 +123,7 @@ async def test_flow_fails_db_url(hass: HomeAssistant) -> None: assert result4["errors"] == {"db_url": "db_url_invalid"} -async def test_flow_fails_invalid_query(hass: HomeAssistant) -> None: +async def test_flow_fails_invalid_query(hass: HomeAssistant, recorder_mock) -> None: """Test config flow fails incorrect db url.""" result4 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -169,7 +169,7 @@ async def test_flow_fails_invalid_query(hass: HomeAssistant) -> None: } -async def test_options_flow(hass: HomeAssistant) -> None: +async def test_options_flow(hass: HomeAssistant, recorder_mock) -> None: """Test options config flow.""" entry = MockConfigEntry( domain=DOMAIN, @@ -218,7 +218,9 @@ async def test_options_flow(hass: HomeAssistant) -> None: } -async def test_options_flow_name_previously_removed(hass: HomeAssistant) -> None: +async def test_options_flow_name_previously_removed( + hass: HomeAssistant, recorder_mock +) -> None: """Test options config flow where the name was missing.""" entry = MockConfigEntry( domain=DOMAIN, @@ -269,7 +271,7 @@ async def test_options_flow_name_previously_removed(hass: HomeAssistant) -> None } -async def test_options_flow_fails_db_url(hass: HomeAssistant) -> None: +async def test_options_flow_fails_db_url(hass: HomeAssistant, recorder_mock) -> None: """Test options flow fails incorrect db url.""" entry = MockConfigEntry( domain=DOMAIN, @@ -312,7 +314,7 @@ async def test_options_flow_fails_db_url(hass: HomeAssistant) -> None: async def test_options_flow_fails_invalid_query( - hass: HomeAssistant, + hass: HomeAssistant, recorder_mock ) -> None: """Test options flow fails incorrect query and template.""" entry = MockConfigEntry( @@ -367,3 +369,58 @@ async def test_options_flow_fails_invalid_query( "column": "size", "unit_of_measurement": "MiB", } + + +async def test_options_flow_db_url_empty(hass: HomeAssistant, recorder_mock) -> None: + """Test options config flow with leaving db_url empty.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={}, + options={ + "db_url": "sqlite://", + "name": "Get Value", + "query": "SELECT 5 as value", + "column": "value", + "unit_of_measurement": "MiB", + "value_template": None, + }, + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.sql.async_setup_entry", + return_value=True, + ): + assert 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"] == FlowResultType.FORM + assert result["step_id"] == "init" + + with patch( + "homeassistant.components.sql.async_setup_entry", + return_value=True, + ), patch( + "homeassistant.components.sql.config_flow.sqlalchemy.create_engine", + ): + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "query": "SELECT 5 as size", + "column": "size", + "unit_of_measurement": "MiB", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"] == { + "name": "Get Value", + "db_url": "sqlite://", + "query": "SELECT 5 as size", + "column": "size", + "value_template": None, + "unit_of_measurement": "MiB", + } From 024a4f39b02feee600d6a44a79e5a18d1d7216c0 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 5 Sep 2022 22:20:56 +0200 Subject: [PATCH 116/955] Use attributes in nightscout (#77825) --- homeassistant/components/nightscout/sensor.py | 70 +++++-------------- 1 file changed, 17 insertions(+), 53 deletions(-) diff --git a/homeassistant/components/nightscout/sensor.py b/homeassistant/components/nightscout/sensor.py index 4ee75f66959..e8aaa11f23d 100644 --- a/homeassistant/components/nightscout/sensor.py +++ b/homeassistant/components/nightscout/sensor.py @@ -40,71 +40,40 @@ class NightscoutSensor(SensorEntity): def __init__(self, api: NightscoutAPI, name, unique_id): """Initialize the Nightscout sensor.""" self.api = api - self._unique_id = unique_id - self._name = name - self._state = None - self._attributes: dict[str, Any] = {} - self._unit_of_measurement = "mg/dL" - self._icon = "mdi:cloud-question" - self._available = False + self._attr_unique_id = unique_id + self._attr_name = name + self._attr_extra_state_attributes: dict[str, Any] = {} + self._attr_native_unit_of_measurement = "mg/dL" + self._attr_icon = "mdi:cloud-question" + self._attr_available = False - @property - def unique_id(self): - """Return the unique ID of the sensor.""" - return self._unique_id - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def native_unit_of_measurement(self): - """Return the unit the value is expressed in.""" - return self._unit_of_measurement - - @property - def available(self): - """Return if the sensor data are available.""" - return self._available - - @property - def native_value(self): - """Return the state of the device.""" - return self._state - - @property - def icon(self): - """Return the icon to use in the frontend, if any.""" - return self._icon - - async def async_update(self): + async def async_update(self) -> None: """Fetch the latest data from Nightscout REST API and update the state.""" try: values = await self.api.get_sgvs() except (ClientError, AsyncIOTimeoutError, OSError) as error: _LOGGER.error("Error fetching data. Failed with %s", error) - self._available = False + self._attr_available = False return - self._available = True - self._attributes = {} - self._state = None + self._attr_available = True + self._attr_extra_state_attributes = {} + self._attr_native_value = None if values: value = values[0] - self._attributes = { + self._attr_extra_state_attributes = { ATTR_DEVICE: value.device, ATTR_DATE: value.date, ATTR_DELTA: value.delta, ATTR_DIRECTION: value.direction, } - self._state = value.sgv - self._icon = self._parse_icon() + self._attr_native_value = value.sgv + self._attr_icon = self._parse_icon(value.direction) else: - self._available = False + self._attr_available = False _LOGGER.warning("Empty reply found when expecting JSON data") - def _parse_icon(self) -> str: + def _parse_icon(self, direction: str) -> str: """Update the icon based on the direction attribute.""" switcher = { "Flat": "mdi:arrow-right", @@ -115,9 +84,4 @@ class NightscoutSensor(SensorEntity): "FortyFiveUp": "mdi:arrow-top-right", "DoubleUp": "mdi:chevron-triple-up", } - return switcher.get(self._attributes[ATTR_DIRECTION], "mdi:cloud-question") - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - return self._attributes + return switcher.get(direction, "mdi:cloud-question") From 7f8e2fa5d475caa061b95a34f9a3606aef2fe2d6 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 5 Sep 2022 23:39:42 +0200 Subject: [PATCH 117/955] Pin astroid to fix pylint (#77862) --- requirements_test.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements_test.txt b/requirements_test.txt index d15431a1d85..99e2f8d8402 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -7,6 +7,7 @@ -c homeassistant/package_constraints.txt -r requirements_test_pre_commit.txt +astroid==2.12.5 codecov==2.1.12 coverage==6.4.4 freezegun==1.2.1 From 50933fa3aed60b014d71f0cb80143aa03125e8c8 Mon Sep 17 00:00:00 2001 From: Justin Vanderhooft Date: Mon, 5 Sep 2022 19:34:59 -0400 Subject: [PATCH 118/955] Move Melnor Bluetooth switches to sub-services off the main device (#77842) Co-authored-by: J. Nick Koston --- .coveragerc | 1 - homeassistant/components/melnor/models.py | 25 ++++++- homeassistant/components/melnor/sensor.py | 2 +- homeassistant/components/melnor/switch.py | 81 +++++++++++++++++------ tests/components/melnor/conftest.py | 19 ++---- tests/components/melnor/test_switch.py | 61 +++++++++++++++++ 6 files changed, 151 insertions(+), 38 deletions(-) create mode 100644 tests/components/melnor/test_switch.py diff --git a/.coveragerc b/.coveragerc index 2e5b3c96b41..7fded7dd8b2 100644 --- a/.coveragerc +++ b/.coveragerc @@ -728,7 +728,6 @@ omit = homeassistant/components/melnor/__init__.py homeassistant/components/melnor/const.py homeassistant/components/melnor/models.py - homeassistant/components/melnor/switch.py homeassistant/components/message_bird/notify.py homeassistant/components/met/weather.py homeassistant/components/met_eireann/__init__.py diff --git a/homeassistant/components/melnor/models.py b/homeassistant/components/melnor/models.py index c050c7f680b..e783f829c11 100644 --- a/homeassistant/components/melnor/models.py +++ b/homeassistant/components/melnor/models.py @@ -3,7 +3,7 @@ from datetime import timedelta import logging -from melnor_bluetooth.device import Device +from melnor_bluetooth.device import Device, Valve from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import DeviceInfo @@ -71,3 +71,26 @@ class MelnorBluetoothBaseEntity(CoordinatorEntity[MelnorDataUpdateCoordinator]): def available(self) -> bool: """Return True if entity is available.""" return self._device.is_connected + + +class MelnorZoneEntity(MelnorBluetoothBaseEntity): + """Base class for valves that define themselves as child devices.""" + + _valve: Valve + + def __init__( + self, + coordinator: MelnorDataUpdateCoordinator, + valve: Valve, + ) -> None: + """Initialize a valve entity.""" + super().__init__(coordinator) + + self._valve = valve + + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, f"{self._device.mac}-zone{self._valve.id}")}, + manufacturer="Melnor", + name=f"Zone {valve.id + 1}", + via_device=(DOMAIN, self._device.mac), + ) diff --git a/homeassistant/components/melnor/sensor.py b/homeassistant/components/melnor/sensor.py index 567f9dc6f2c..bda70dfee3b 100644 --- a/homeassistant/components/melnor/sensor.py +++ b/homeassistant/components/melnor/sensor.py @@ -1,4 +1,4 @@ -"""Support for Melnor RainCloud sprinkler water timer.""" +"""Sensor support for Melnor Bluetooth water timer.""" from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/melnor/switch.py b/homeassistant/components/melnor/switch.py index 125d3ffde8c..34e0ca331b1 100644 --- a/homeassistant/components/melnor/switch.py +++ b/homeassistant/components/melnor/switch.py @@ -1,18 +1,44 @@ -"""Support for Melnor RainCloud sprinkler water timer.""" +"""Switch support for Melnor Bluetooth water timer.""" from __future__ import annotations -from typing import Any, cast +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any from melnor_bluetooth.device import Valve -from homeassistant.components.switch import SwitchEntity +from homeassistant.components.switch import ( + SwitchDeviceClass, + SwitchEntity, + SwitchEntityDescription, +) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .models import MelnorBluetoothBaseEntity, MelnorDataUpdateCoordinator +from .models import MelnorDataUpdateCoordinator, MelnorZoneEntity + + +def set_is_watering(valve: Valve, value: bool) -> None: + """Set the is_watering state of a valve.""" + valve.is_watering = value + + +@dataclass +class MelnorSwitchEntityDescriptionMixin: + """Mixin for required keys.""" + + on_off_fn: Callable[[Valve, bool], Any] + state_fn: Callable[[Valve], Any] + + +@dataclass +class MelnorSwitchEntityDescription( + SwitchEntityDescription, MelnorSwitchEntityDescriptionMixin +): + """Describes Melnor switch entity.""" async def async_setup_entry( @@ -21,52 +47,63 @@ async def async_setup_entry( async_add_devices: AddEntitiesCallback, ) -> None: """Set up the switch platform.""" - switches = [] + entities: list[MelnorZoneSwitch] = [] coordinator: MelnorDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] # This device may not have 4 valves total, but the library will only expose the right number of valves for i in range(1, 5): - if coordinator.data[f"zone{i}"] is not None: - switches.append(MelnorSwitch(coordinator, i)) + valve = coordinator.data[f"zone{i}"] + if valve is not None: - async_add_devices(switches) + entities.append( + MelnorZoneSwitch( + coordinator, + valve, + MelnorSwitchEntityDescription( + device_class=SwitchDeviceClass.SWITCH, + icon="mdi:sprinkler", + key="manual", + name="Manual", + on_off_fn=set_is_watering, + state_fn=lambda valve: valve.is_watering, + ), + ) + ) + + async_add_devices(entities) -class MelnorSwitch(MelnorBluetoothBaseEntity, SwitchEntity): +class MelnorZoneSwitch(MelnorZoneEntity, SwitchEntity): """A switch implementation for a melnor device.""" - _attr_icon = "mdi:sprinkler" + entity_description: MelnorSwitchEntityDescription def __init__( self, coordinator: MelnorDataUpdateCoordinator, - valve_index: int, + valve: Valve, + entity_description: MelnorSwitchEntityDescription, ) -> None: """Initialize a switch for a melnor device.""" - super().__init__(coordinator) - self._valve_index = valve_index + super().__init__(coordinator, valve) - valve_id = self._valve().id - self._attr_name = f"Zone {valve_id+1}" - self._attr_unique_id = f"{self._device.mac}-zone{valve_id}-manual" + self._attr_unique_id = f"{self._device.mac}-zone{valve.id}-manual" + self.entity_description = entity_description @property def is_on(self) -> bool: """Return true if device is on.""" - return self._valve().is_watering + return self.entity_description.state_fn(self._valve) async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" - self._valve().is_watering = True + self.entity_description.on_off_fn(self._valve, True) await self._device.push_state() self.async_write_ha_state() async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" - self._valve().is_watering = False + self.entity_description.on_off_fn(self._valve, False) await self._device.push_state() self.async_write_ha_state() - - def _valve(self) -> Valve: - return cast(Valve, self._device[f"zone{self._valve_index}"]) diff --git a/tests/components/melnor/conftest.py b/tests/components/melnor/conftest.py index 403ae83bb67..5aee6264501 100644 --- a/tests/components/melnor/conftest.py +++ b/tests/components/melnor/conftest.py @@ -2,7 +2,7 @@ from __future__ import annotations -from unittest.mock import AsyncMock, Mock, patch +from unittest.mock import AsyncMock, patch from bleak.backends.device import BLEDevice from bleak.backends.scanner import AdvertisementData @@ -65,14 +65,6 @@ def mock_config_entry(hass: HomeAssistant): return entry -def mock_melnor_valve(identifier: int): - """Return a mocked Melnor valve.""" - valve = Mock(spec=Valve) - valve.id = identifier - - return valve - - def mock_melnor_device(): """Return a mocked Melnor device.""" @@ -83,6 +75,7 @@ def mock_melnor_device(): device.connect = AsyncMock(return_value=True) device.disconnect = AsyncMock(return_value=True) device.fetch_state = AsyncMock(return_value=device) + device.push_state = AsyncMock(return_value=None) device.battery_level = 80 device.mac = FAKE_ADDRESS_1 @@ -90,10 +83,10 @@ def mock_melnor_device(): device.name = "test_melnor" device.rssi = -50 - device.zone1 = mock_melnor_valve(1) - device.zone2 = mock_melnor_valve(2) - device.zone3 = mock_melnor_valve(3) - device.zone4 = mock_melnor_valve(4) + device.zone1 = Valve(0, device) + device.zone2 = Valve(1, device) + device.zone3 = Valve(2, device) + device.zone4 = Valve(3, device) device.__getitem__.side_effect = lambda key: getattr(device, key) diff --git a/tests/components/melnor/test_switch.py b/tests/components/melnor/test_switch.py new file mode 100644 index 00000000000..ffe043f53a8 --- /dev/null +++ b/tests/components/melnor/test_switch.py @@ -0,0 +1,61 @@ +"""Test the Melnor sensors.""" + +from __future__ import annotations + +from homeassistant.components.switch import SwitchDeviceClass +from homeassistant.const import STATE_OFF, STATE_ON + +from .conftest import ( + mock_config_entry, + patch_async_ble_device_from_address, + patch_async_register_callback, + patch_melnor_device, +) + + +async def test_manual_watering_switch_metadata(hass): + """Test the manual watering switch.""" + + entry = mock_config_entry(hass) + + with patch_async_ble_device_from_address(), patch_melnor_device(), patch_async_register_callback(): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + switch = hass.states.get("switch.zone_1_manual") + assert switch.attributes["device_class"] == SwitchDeviceClass.SWITCH + assert switch.attributes["icon"] == "mdi:sprinkler" + + +async def test_manual_watering_switch_on_off(hass): + """Test the manual watering switch.""" + + entry = mock_config_entry(hass) + + with patch_async_ble_device_from_address(), patch_melnor_device(), patch_async_register_callback(): + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + switch = hass.states.get("switch.zone_1_manual") + assert switch.state is STATE_OFF + + await hass.services.async_call( + "switch", + "turn_on", + {"entity_id": "switch.zone_1_manual"}, + blocking=True, + ) + + switch = hass.states.get("switch.zone_1_manual") + assert switch.state is STATE_ON + + await hass.services.async_call( + "switch", + "turn_off", + {"entity_id": "switch.zone_1_manual"}, + blocking=True, + ) + + switch = hass.states.get("switch.zone_1_manual") + assert switch.state is STATE_OFF From 5c30b33ee2b24abd9c9043a61942b137edce57de Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 6 Sep 2022 00:27:55 +0000 Subject: [PATCH 119/955] [ci skip] Translation update --- .../binary_sensor/translations/hu.json | 2 +- .../bluemaestro/translations/et.json | 22 ++++++++++ .../bluemaestro/translations/no.json | 22 ++++++++++ .../components/fibaro/translations/ca.json | 10 ++++- .../components/fibaro/translations/de.json | 10 ++++- .../components/fibaro/translations/el.json | 10 ++++- .../components/fibaro/translations/en.json | 10 ++++- .../components/fibaro/translations/es.json | 10 ++++- .../components/fibaro/translations/fr.json | 10 ++++- .../components/fibaro/translations/hu.json | 10 ++++- .../components/fibaro/translations/pt-BR.json | 10 ++++- .../fibaro/translations/zh-Hant.json | 10 ++++- .../components/nobo_hub/translations/ca.json | 15 +++++++ .../components/nobo_hub/translations/et.json | 44 +++++++++++++++++++ .../components/sensibo/translations/ca.json | 6 +++ .../components/sensibo/translations/de.json | 6 +++ .../components/sensibo/translations/el.json | 6 +++ .../components/sensibo/translations/et.json | 6 +++ .../components/sensibo/translations/no.json | 6 +++ .../sensibo/translations/pt-BR.json | 6 +++ .../sensibo/translations/zh-Hant.json | 6 +++ .../components/sensor/translations/et.json | 2 + .../components/sensor/translations/hu.json | 4 +- .../components/zha/translations/et.json | 4 ++ 24 files changed, 235 insertions(+), 12 deletions(-) create mode 100644 homeassistant/components/bluemaestro/translations/et.json create mode 100644 homeassistant/components/bluemaestro/translations/no.json create mode 100644 homeassistant/components/nobo_hub/translations/et.json diff --git a/homeassistant/components/binary_sensor/translations/hu.json b/homeassistant/components/binary_sensor/translations/hu.json index 56df2994766..ad5d5d93254 100644 --- a/homeassistant/components/binary_sensor/translations/hu.json +++ b/homeassistant/components/binary_sensor/translations/hu.json @@ -110,7 +110,7 @@ "cold": "h\u0171t\u00e9s", "gas": "g\u00e1z", "heat": "f\u0171t\u00e9s", - "moisture": "nedvess\u00e9g", + "moisture": "nedvess\u00e9gtartalom", "motion": "mozg\u00e1s", "occupancy": "foglalts\u00e1g", "power": "teljes\u00edtm\u00e9ny", diff --git a/homeassistant/components/bluemaestro/translations/et.json b/homeassistant/components/bluemaestro/translations/et.json new file mode 100644 index 00000000000..2cfcdd2b591 --- /dev/null +++ b/homeassistant/components/bluemaestro/translations/et.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "already_in_progress": "H\u00e4\u00e4lestamine juba k\u00e4ib", + "no_devices_found": "V\u00f5rgust seadmeid ei leitud", + "not_supported": "Seadet ei toetata" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Kas h\u00e4\u00e4lestada seade {name}?" + }, + "user": { + "data": { + "address": "Seade" + }, + "description": "Vali h\u00e4\u00e4lestatav seade" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluemaestro/translations/no.json b/homeassistant/components/bluemaestro/translations/no.json new file mode 100644 index 00000000000..0bf8b1695ec --- /dev/null +++ b/homeassistant/components/bluemaestro/translations/no.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", + "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket", + "not_supported": "Enheten st\u00f8ttes ikke" + }, + "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/fibaro/translations/ca.json b/homeassistant/components/fibaro/translations/ca.json index 8e04b3567f8..01ffd644a3c 100644 --- a/homeassistant/components/fibaro/translations/ca.json +++ b/homeassistant/components/fibaro/translations/ca.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "El dispositiu ja est\u00e0 configurat" + "already_configured": "El dispositiu ja est\u00e0 configurat", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", @@ -9,6 +10,13 @@ "unknown": "Error inesperat" }, "step": { + "reauth_confirm": { + "data": { + "password": "Contrasenya" + }, + "description": "Si us plau, actualitza la contrasenya de {username}", + "title": "Reautenticaci\u00f3 de la integraci\u00f3" + }, "user": { "data": { "import_plugins": "Vols importar les entitats dels complements Fibaro?", diff --git a/homeassistant/components/fibaro/translations/de.json b/homeassistant/components/fibaro/translations/de.json index 831abc85929..e16bd4a56a7 100644 --- a/homeassistant/components/fibaro/translations/de.json +++ b/homeassistant/components/fibaro/translations/de.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Ger\u00e4t ist bereits konfiguriert" + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", @@ -9,6 +10,13 @@ "unknown": "Unerwarteter Fehler" }, "step": { + "reauth_confirm": { + "data": { + "password": "Passwort" + }, + "description": "Bitte \u00e4ndere Dein Passwort f\u00fcr {username}", + "title": "Integration erneut authentifizieren" + }, "user": { "data": { "import_plugins": "Entit\u00e4ten aus Fibaro-Plugins importieren?", diff --git a/homeassistant/components/fibaro/translations/el.json b/homeassistant/components/fibaro/translations/el.json index d2a2659646f..528ae82b54f 100644 --- a/homeassistant/components/fibaro/translations/el.json +++ b/homeassistant/components/fibaro/translations/el.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2" }, "error": { "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", @@ -9,6 +10,13 @@ "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" + }, + "description": "\u0395\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03ae\u03c2 \u03c3\u03b1\u03c2 \u03b3\u03b9\u03b1 {username}", + "title": "\u0395\u03c0\u03b1\u03bd\u03b1\u03bb\u03b7\u03c0\u03c4\u03b9\u03ba\u03cc\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2" + }, "user": { "data": { "import_plugins": "\u0395\u03b9\u03c3\u03b1\u03b3\u03c9\u03b3\u03ae \u03bf\u03bd\u03c4\u03bf\u03c4\u03ae\u03c4\u03c9\u03bd \u03b1\u03c0\u03cc \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03b1 fibaro;", diff --git a/homeassistant/components/fibaro/translations/en.json b/homeassistant/components/fibaro/translations/en.json index 6bcff530798..e762ef718a0 100644 --- a/homeassistant/components/fibaro/translations/en.json +++ b/homeassistant/components/fibaro/translations/en.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Device is already configured" + "already_configured": "Device is already configured", + "reauth_successful": "Re-authentication was successful" }, "error": { "cannot_connect": "Failed to connect", @@ -9,6 +10,13 @@ "unknown": "Unexpected error" }, "step": { + "reauth_confirm": { + "data": { + "password": "Password" + }, + "description": "Please update your password for {username}", + "title": "Reauthenticate Integration" + }, "user": { "data": { "import_plugins": "Import entities from fibaro plugins?", diff --git a/homeassistant/components/fibaro/translations/es.json b/homeassistant/components/fibaro/translations/es.json index 0bf8f1aaa76..3b1f7d81147 100644 --- a/homeassistant/components/fibaro/translations/es.json +++ b/homeassistant/components/fibaro/translations/es.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "El dispositivo ya est\u00e1 configurado" + "already_configured": "El dispositivo ya est\u00e1 configurado", + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "cannot_connect": "No se pudo conectar", @@ -9,6 +10,13 @@ "unknown": "Error inesperado" }, "step": { + "reauth_confirm": { + "data": { + "password": "Contrase\u00f1a" + }, + "description": "Por favor, actualiza tu contrase\u00f1a para {username}", + "title": "Volver a autenticar la integraci\u00f3n" + }, "user": { "data": { "import_plugins": "\u00bfImportar entidades desde los plugins de fibaro?", diff --git a/homeassistant/components/fibaro/translations/fr.json b/homeassistant/components/fibaro/translations/fr.json index 8dee1529959..9b33a524e73 100644 --- a/homeassistant/components/fibaro/translations/fr.json +++ b/homeassistant/components/fibaro/translations/fr.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { "cannot_connect": "\u00c9chec de connexion", @@ -9,6 +10,13 @@ "unknown": "Erreur inattendue" }, "step": { + "reauth_confirm": { + "data": { + "password": "Mot de passe" + }, + "description": "Veuillez mettre \u00e0 jour votre mot de passe pour {username}", + "title": "R\u00e9-authentifier l'int\u00e9gration" + }, "user": { "data": { "import_plugins": "Importer les entit\u00e9s \u00e0 partir des plugins fibaro\u00a0?", diff --git a/homeassistant/components/fibaro/translations/hu.json b/homeassistant/components/fibaro/translations/hu.json index d976d4b1a96..a5f340715c5 100644 --- a/homeassistant/components/fibaro/translations/hu.json +++ b/homeassistant/components/fibaro/translations/hu.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", @@ -9,6 +10,13 @@ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { + "reauth_confirm": { + "data": { + "password": "Jelsz\u00f3" + }, + "description": "K\u00e9rem, friss\u00edtse {username} jelszav\u00e1t", + "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" + }, "user": { "data": { "import_plugins": "Import\u00e1ln\u00e1 az entit\u00e1sokat a fibaro be\u00e9p\u00fcl\u0151 modulokb\u00f3l?", diff --git a/homeassistant/components/fibaro/translations/pt-BR.json b/homeassistant/components/fibaro/translations/pt-BR.json index 3e0f3139fa7..663100b571d 100644 --- a/homeassistant/components/fibaro/translations/pt-BR.json +++ b/homeassistant/components/fibaro/translations/pt-BR.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" }, "error": { "cannot_connect": "Falha ao conectar", @@ -9,6 +10,13 @@ "unknown": "Erro inesperado" }, "step": { + "reauth_confirm": { + "data": { + "password": "Senha" + }, + "description": "Atualize sua senha para {username}", + "title": "Reautenticar Integra\u00e7\u00e3o" + }, "user": { "data": { "import_plugins": "Importar entidades de plugins fibaro?", diff --git a/homeassistant/components/fibaro/translations/zh-Hant.json b/homeassistant/components/fibaro/translations/zh-Hant.json index e494fb1012f..36bfb518b35 100644 --- a/homeassistant/components/fibaro/translations/zh-Hant.json +++ b/homeassistant/components/fibaro/translations/zh-Hant.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", @@ -9,6 +10,13 @@ "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u5bc6\u78bc" + }, + "description": "\u8acb\u66f4\u65b0 {username} \u5bc6\u78bc", + "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" + }, "user": { "data": { "import_plugins": "\u5f9e fibaro \u5916\u639b\u532f\u5165\u5be6\u9ad4\uff1f", diff --git a/homeassistant/components/nobo_hub/translations/ca.json b/homeassistant/components/nobo_hub/translations/ca.json index f3f3bc63837..25fd161b93a 100644 --- a/homeassistant/components/nobo_hub/translations/ca.json +++ b/homeassistant/components/nobo_hub/translations/ca.json @@ -14,6 +14,21 @@ "ip_address": "Adre\u00e7a IP", "serial": "N\u00famero de s\u00e8rie (12 d\u00edgits)" } + }, + "user": { + "data": { + "device": "Hubs descoberts" + }, + "description": "Selecciona el Nob\u00f8 Ecohub a configurar" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "override_type": "Tipus de substituci\u00f3" + } } } } diff --git a/homeassistant/components/nobo_hub/translations/et.json b/homeassistant/components/nobo_hub/translations/et.json new file mode 100644 index 00000000000..bd1cf7c5bc0 --- /dev/null +++ b/homeassistant/components/nobo_hub/translations/et.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus - kontrolli seerianumbrit", + "invalid_ip": "Sobimatu IP-aadress", + "invalid_serial": "Sobimatu seerianumber", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "manual": { + "data": { + "ip_address": "IP aadress", + "serial": "Seerianumber (12 numbrit)" + }, + "description": "Seadista Nob\u00f8 Ecohub, mida ei ole teie kohalikus v\u00f5rgus avastatud. Kui hub asub teises v\u00f5rgus, saad sellega ikkagi \u00fchendust luua, sisestades t\u00e4ieliku seerianumbri (12 numbrit) ja IP-aadressi." + }, + "selected": { + "data": { + "serial_suffix": "Seerianumbri j\u00e4relliide (3 numbrit)" + }, + "description": "{hub}-i seadistamine.\n\nJaoturiga \u00fchenduse loomiseks pead sisestama jaoturi seerianumbri viimased 3 numbrit." + }, + "user": { + "data": { + "device": "Avastatud s\u00f5lmpunktid" + }, + "description": "Vali konfigureeritav Nob\u00f8 Ecohub." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "override_type": "Alistamise t\u00fc\u00fcp" + }, + "description": "Vali alistamise t\u00fc\u00fcp \"N\u00fc\u00fcd\", et l\u00f5petada alistamine j\u00e4rgmisel n\u00e4dalal profiili muutmisel." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/ca.json b/homeassistant/components/sensibo/translations/ca.json index 1634e0b8e40..9429650dbee 100644 --- a/homeassistant/components/sensibo/translations/ca.json +++ b/homeassistant/components/sensibo/translations/ca.json @@ -15,11 +15,17 @@ "reauth_confirm": { "data": { "api_key": "Clau API" + }, + "data_description": { + "api_key": "Consulta la documentaci\u00f3 per obtenir una nova clau API." } }, "user": { "data": { "api_key": "Clau API" + }, + "data_description": { + "api_key": "Consulta la documentaci\u00f3 per obtenir la teva clau API." } } } diff --git a/homeassistant/components/sensibo/translations/de.json b/homeassistant/components/sensibo/translations/de.json index d5900f0f25e..f6145f49998 100644 --- a/homeassistant/components/sensibo/translations/de.json +++ b/homeassistant/components/sensibo/translations/de.json @@ -15,11 +15,17 @@ "reauth_confirm": { "data": { "api_key": "API-Schl\u00fcssel" + }, + "data_description": { + "api_key": "Folge der Dokumentation, um einen neuen Api-Schl\u00fcssel zu erhalten." } }, "user": { "data": { "api_key": "API-Schl\u00fcssel" + }, + "data_description": { + "api_key": "Folge der Dokumentation, um deinen Api-Schl\u00fcssel zu erhalten." } } } diff --git a/homeassistant/components/sensibo/translations/el.json b/homeassistant/components/sensibo/translations/el.json index 7fd7ee26f97..4b5e30f94be 100644 --- a/homeassistant/components/sensibo/translations/el.json +++ b/homeassistant/components/sensibo/translations/el.json @@ -15,11 +15,17 @@ "reauth_confirm": { "data": { "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API" + }, + "data_description": { + "api_key": "\u0391\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bb\u03ac\u03b2\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03bd\u03ad\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af api." } }, "user": { "data": { "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API" + }, + "data_description": { + "api_key": "\u0391\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bb\u03ac\u03b2\u03b5\u03c4\u03b5 \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af api \u03c3\u03b1\u03c2." } } } diff --git a/homeassistant/components/sensibo/translations/et.json b/homeassistant/components/sensibo/translations/et.json index b216ecc3260..acd3eb5ac1a 100644 --- a/homeassistant/components/sensibo/translations/et.json +++ b/homeassistant/components/sensibo/translations/et.json @@ -15,11 +15,17 @@ "reauth_confirm": { "data": { "api_key": "API v\u00f5ti" + }, + "data_description": { + "api_key": "Uue API-v\u00f5tme saamiseks j\u00e4rgi dokumentatsiooni." } }, "user": { "data": { "api_key": "API v\u00f5ti" + }, + "data_description": { + "api_key": "API-v\u00f5tme hankimiseks j\u00e4rgi dokumentatsiooni." } } } diff --git a/homeassistant/components/sensibo/translations/no.json b/homeassistant/components/sensibo/translations/no.json index 43f17f52f2e..ec40be62e89 100644 --- a/homeassistant/components/sensibo/translations/no.json +++ b/homeassistant/components/sensibo/translations/no.json @@ -15,11 +15,17 @@ "reauth_confirm": { "data": { "api_key": "API-n\u00f8kkel" + }, + "data_description": { + "api_key": "F\u00f8lg dokumentasjonen for \u00e5 f\u00e5 en ny api-n\u00f8kkel." } }, "user": { "data": { "api_key": "API-n\u00f8kkel" + }, + "data_description": { + "api_key": "F\u00f8lg dokumentasjonen for \u00e5 f\u00e5 api-n\u00f8kkelen din." } } } diff --git a/homeassistant/components/sensibo/translations/pt-BR.json b/homeassistant/components/sensibo/translations/pt-BR.json index 89b8984ac49..591424dcaf6 100644 --- a/homeassistant/components/sensibo/translations/pt-BR.json +++ b/homeassistant/components/sensibo/translations/pt-BR.json @@ -15,11 +15,17 @@ "reauth_confirm": { "data": { "api_key": "Chave da API" + }, + "data_description": { + "api_key": "Siga a documenta\u00e7\u00e3o para obter uma nova chave de API." } }, "user": { "data": { "api_key": "Chave da API" + }, + "data_description": { + "api_key": "Siga a documenta\u00e7\u00e3o para obter sua chave de API." } } } diff --git a/homeassistant/components/sensibo/translations/zh-Hant.json b/homeassistant/components/sensibo/translations/zh-Hant.json index c2639ea2ab9..4ad41a2e3f3 100644 --- a/homeassistant/components/sensibo/translations/zh-Hant.json +++ b/homeassistant/components/sensibo/translations/zh-Hant.json @@ -15,11 +15,17 @@ "reauth_confirm": { "data": { "api_key": "API \u91d1\u9470" + }, + "data_description": { + "api_key": "\u8acb\u8ddf\u96a8\u6587\u4ef6\u4ee5\u53d6\u5f97\u65b0 API \u91d1\u9470\u3002" } }, "user": { "data": { "api_key": "API \u91d1\u9470" + }, + "data_description": { + "api_key": "\u8acb\u8ddf\u96a8\u6587\u4ef6\u4ee5\u53d6\u5f97 API \u91d1\u9470\u3002" } } } diff --git a/homeassistant/components/sensor/translations/et.json b/homeassistant/components/sensor/translations/et.json index 664ca6cf6f5..bbc6880dcee 100644 --- a/homeassistant/components/sensor/translations/et.json +++ b/homeassistant/components/sensor/translations/et.json @@ -11,6 +11,7 @@ "is_gas": "Praegune {entity_name} gaas", "is_humidity": "Praegune {entity_name} niiskus", "is_illuminance": "Praegune {entity_name} valgustatus", + "is_moisture": "Praegune {entity_name} niiskus", "is_nitrogen_dioxide": "Praegune {entity_name} l\u00e4mmastikdioksiidi kontsentratsioonitase", "is_nitrogen_monoxide": "Praegune {entity_name} l\u00e4mmastikmonooksiidi kontsentratsioonitase", "is_nitrous_oxide": "Praegune {entity_name} dil\u00e4mmastikoksiidi kontsentratsioonitase", @@ -40,6 +41,7 @@ "gas": "{entity_name} gaasivahetus", "humidity": "{entity_name} niiskus muutub", "illuminance": "{entity_name} valgustustugevus muutub", + "moisture": "{entity_name} niiskus muutus", "nitrogen_dioxide": "{entity_name} l\u00e4mmastikdioksiidi kontsentratsiooni muutused", "nitrogen_monoxide": "{entity_name} l\u00e4mmastikmonooksiidi kontsentratsiooni muutused", "nitrous_oxide": "{entity_name} l\u00e4mmastikoksiidi kontsentratsiooni muutused", diff --git a/homeassistant/components/sensor/translations/hu.json b/homeassistant/components/sensor/translations/hu.json index 76e5a6cd23d..48fe4a651b8 100644 --- a/homeassistant/components/sensor/translations/hu.json +++ b/homeassistant/components/sensor/translations/hu.json @@ -11,7 +11,7 @@ "is_gas": "Jelenlegi {entity_name} g\u00e1z", "is_humidity": "{entity_name} aktu\u00e1lis p\u00e1ratartalma", "is_illuminance": "{entity_name} aktu\u00e1lis megvil\u00e1g\u00edt\u00e1sa", - "is_moisture": "{entity_name} aktu\u00e1lis p\u00e1ratartalom", + "is_moisture": "{entity_name} aktu\u00e1lis nedvess\u00e9gtartalom", "is_nitrogen_dioxide": "Jelenlegi {entity_name} nitrog\u00e9n-dioxid-koncentr\u00e1ci\u00f3 szint", "is_nitrogen_monoxide": "Jelenlegi {entity_name} nitrog\u00e9n-monoxid-koncentr\u00e1ci\u00f3 szint", "is_nitrous_oxide": "Jelenlegi {entity_name} dinitrog\u00e9n-oxid-koncentr\u00e1ci\u00f3 szint", @@ -41,7 +41,7 @@ "gas": "{entity_name} g\u00e1z v\u00e1ltoz\u00e1sok", "humidity": "{entity_name} p\u00e1ratartalma v\u00e1ltozik", "illuminance": "{entity_name} megvil\u00e1g\u00edt\u00e1sa v\u00e1ltozik", - "moisture": "{entity_name} p\u00e1ratartalom-v\u00e1ltoz\u00e1s", + "moisture": "{entity_name} nedvess\u00e9gv\u00e1ltoz\u00e1s", "nitrogen_dioxide": "{entity_name} nitrog\u00e9n-dioxid koncentr\u00e1ci\u00f3 v\u00e1ltozik", "nitrogen_monoxide": "{entity_name} nitrog\u00e9n-monoxid koncentr\u00e1ci\u00f3 v\u00e1ltozik", "nitrous_oxide": "{entity_name} dinitrog\u00e9n-oxid koncentr\u00e1ci\u00f3ja v\u00e1ltozik", diff --git a/homeassistant/components/zha/translations/et.json b/homeassistant/components/zha/translations/et.json index 2953c94f4cf..9363efb6b53 100644 --- a/homeassistant/components/zha/translations/et.json +++ b/homeassistant/components/zha/translations/et.json @@ -117,6 +117,10 @@ }, "options": { "step": { + "init": { + "description": "ZHA peatatakse. Kas soovid j\u00e4tkata?", + "title": "Seadista ZHA uuesti" + }, "upload_manual_backup": { "title": "Lae k\u00e4sitsi loodud varukoopia \u00fcles" } From bb77af71ff8174dba4c3b401492d95a3f4b7ef44 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Tue, 6 Sep 2022 13:30:55 +0800 Subject: [PATCH 120/955] Use fragmented mp4 in stream recorder (#77822) --- homeassistant/components/stream/recorder.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/stream/recorder.py b/homeassistant/components/stream/recorder.py index 3bda8acfa7b..3910a2d2fed 100644 --- a/homeassistant/components/stream/recorder.py +++ b/homeassistant/components/stream/recorder.py @@ -104,7 +104,11 @@ class RecorderOutput(StreamOutput): "w", format=RECORDER_CONTAINER_FORMAT, container_options={ - "video_track_timescale": str(int(1 / source_v.time_base)) + "video_track_timescale": str(int(1 / source_v.time_base)), + "movflags": "frag_keyframe", + "min_frag_duration": str( + self.stream_settings.min_segment_duration + ), }, ) From 852b0caf5be4bba0dcaaf5f6a38221d1590c4ed9 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Tue, 6 Sep 2022 13:31:36 +0800 Subject: [PATCH 121/955] Add orientation transforms to stream (#77439) --- homeassistant/components/camera/__init__.py | 15 ++- homeassistant/components/camera/const.py | 1 + homeassistant/components/camera/prefs.py | 60 ++++++++---- homeassistant/components/stream/__init__.py | 14 ++- homeassistant/components/stream/core.py | 33 ++++++- homeassistant/components/stream/fmp4utils.py | 50 +++++++++- homeassistant/components/stream/hls.py | 4 +- homeassistant/components/stream/recorder.py | 19 +++- tests/components/camera/common.py | 11 --- tests/components/camera/test_init.py | 99 ++++++++++++++++---- tests/components/stream/common.py | 20 ++++ tests/components/stream/test_hls.py | 46 ++++++++- tests/components/stream/test_recorder.py | 46 ++++++++- tests/components/stream/test_worker.py | 44 ++++++++- 14 files changed, 391 insertions(+), 71 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 5aa348c9fb8..afc6be48144 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -65,6 +65,8 @@ from .const import ( # noqa: F401 DATA_CAMERA_PREFS, DATA_RTSP_TO_WEB_RTC, DOMAIN, + PREF_ORIENTATION, + PREF_PRELOAD_STREAM, SERVICE_RECORD, STREAM_TYPE_HLS, STREAM_TYPE_WEB_RTC, @@ -874,7 +876,8 @@ async def websocket_get_prefs( { vol.Required("type"): "camera/update_prefs", vol.Required("entity_id"): cv.entity_id, - vol.Optional("preload_stream"): bool, + vol.Optional(PREF_PRELOAD_STREAM): bool, + vol.Optional(PREF_ORIENTATION): vol.All(int, vol.Range(min=1, max=8)), } ) @websocket_api.async_response @@ -888,9 +891,12 @@ async def websocket_update_prefs( changes.pop("id") changes.pop("type") entity_id = changes.pop("entity_id") - await prefs.async_update(entity_id, **changes) - - connection.send_result(msg["id"], prefs.get(entity_id).as_dict()) + try: + entity_prefs = await prefs.async_update(entity_id, **changes) + connection.send_result(msg["id"], entity_prefs) + except HomeAssistantError as ex: + _LOGGER.error("Error setting camera preferences: %s", ex) + connection.send_error(msg["id"], "update_failed", str(ex)) async def async_handle_snapshot_service( @@ -959,6 +965,7 @@ async def _async_stream_endpoint_url( # Update keepalive setting which manages idle shutdown camera_prefs = hass.data[DATA_CAMERA_PREFS].get(camera.entity_id) stream.keepalive = camera_prefs.preload_stream + stream.orientation = camera_prefs.orientation stream.add_provider(fmt) await stream.start() diff --git a/homeassistant/components/camera/const.py b/homeassistant/components/camera/const.py index fafed8a4266..ab5832e48ab 100644 --- a/homeassistant/components/camera/const.py +++ b/homeassistant/components/camera/const.py @@ -9,6 +9,7 @@ DATA_CAMERA_PREFS: Final = "camera_prefs" DATA_RTSP_TO_WEB_RTC: Final = "rtsp_to_web_rtc" PREF_PRELOAD_STREAM: Final = "preload_stream" +PREF_ORIENTATION: Final = "orientation" SERVICE_RECORD: Final = "record" diff --git a/homeassistant/components/camera/prefs.py b/homeassistant/components/camera/prefs.py index 08c57631a1b..effc2f619bd 100644 --- a/homeassistant/components/camera/prefs.py +++ b/homeassistant/components/camera/prefs.py @@ -1,13 +1,15 @@ """Preference management for camera component.""" from __future__ import annotations -from typing import Final +from typing import Final, Union, cast from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.storage import Store from homeassistant.helpers.typing import UNDEFINED, UndefinedType -from .const import DOMAIN, PREF_PRELOAD_STREAM +from .const import DOMAIN, PREF_ORIENTATION, PREF_PRELOAD_STREAM STORAGE_KEY: Final = DOMAIN STORAGE_VERSION: Final = 1 @@ -16,18 +18,23 @@ STORAGE_VERSION: Final = 1 class CameraEntityPreferences: """Handle preferences for camera entity.""" - def __init__(self, prefs: dict[str, bool]) -> None: + def __init__(self, prefs: dict[str, bool | int]) -> None: """Initialize prefs.""" self._prefs = prefs - def as_dict(self) -> dict[str, bool]: + def as_dict(self) -> dict[str, bool | int]: """Return dictionary version.""" return self._prefs @property def preload_stream(self) -> bool: """Return if stream is loaded on hass start.""" - return self._prefs.get(PREF_PRELOAD_STREAM, False) + return cast(bool, self._prefs.get(PREF_PRELOAD_STREAM, False)) + + @property + def orientation(self) -> int: + """Return the current stream orientation settings.""" + return self._prefs.get(PREF_ORIENTATION, 1) class CameraPreferences: @@ -36,10 +43,13 @@ class CameraPreferences: def __init__(self, hass: HomeAssistant) -> None: """Initialize camera prefs.""" self._hass = hass - self._store = Store[dict[str, dict[str, bool]]]( + # 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]]]]( hass, STORAGE_VERSION, STORAGE_KEY ) - self._prefs: dict[str, dict[str, bool]] | None = None + # Local copy of the preload_stream prefs + self._prefs: dict[str, dict[str, bool | int]] | None = None async def async_initialize(self) -> None: """Finish initializing the preferences.""" @@ -53,22 +63,36 @@ class CameraPreferences: entity_id: str, *, preload_stream: bool | UndefinedType = UNDEFINED, + orientation: int | UndefinedType = UNDEFINED, stream_options: dict[str, str] | UndefinedType = UNDEFINED, - ) -> None: - """Update camera preferences.""" - # Prefs already initialized. - assert self._prefs is not None - if not self._prefs.get(entity_id): - self._prefs[entity_id] = {} + ) -> dict[str, bool | int]: + """Update camera preferences. - for key, value in ((PREF_PRELOAD_STREAM, preload_stream),): - if value is not UNDEFINED: - self._prefs[entity_id][key] = value + Returns a dict with the preferences on success or a string on error. + """ + 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) - await self._store.async_save(self._prefs) + if orientation is not UNDEFINED: + if (registry := er.async_get(self._hass)).async_get(entity_id): + registry.async_update_entity_options( + entity_id, DOMAIN, {PREF_ORIENTATION: orientation} + ) + else: + 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 - return CameraEntityPreferences(self._prefs.get(entity_id, {})) + 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) diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index 354f9a77672..559de094090 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -229,6 +229,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, ) else: hass.data[DOMAIN][ATTR_SETTINGS] = STREAM_SETTINGS_NON_LL_HLS @@ -280,7 +281,7 @@ class Stream: self._thread_quit = threading.Event() self._outputs: dict[str, StreamOutput] = {} self._fast_restart_once = False - self._keyframe_converter = KeyFrameConverter(hass) + self._keyframe_converter = KeyFrameConverter(hass, stream_settings) self._available: bool = True self._update_callback: Callable[[], None] | None = None self._logger = ( @@ -290,6 +291,16 @@ class Stream: ) self._diagnostics = Diagnostics() + @property + def orientation(self) -> int: + """Return the current orientation setting.""" + return self._stream_settings.orientation + + @orientation.setter + def orientation(self, value: int) -> 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: @@ -401,6 +412,7 @@ class Stream: 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.increment("start_worker") try: stream_worker( diff --git a/homeassistant/components/stream/core.py b/homeassistant/components/stream/core.py index 8c456af91aa..0fa57913269 100644 --- a/homeassistant/components/stream/core.py +++ b/homeassistant/components/stream/core.py @@ -11,6 +11,7 @@ from typing import TYPE_CHECKING, Any from aiohttp import web import async_timeout import attr +import numpy as np from homeassistant.components.http.view import HomeAssistantView from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback @@ -43,6 +44,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() STREAM_SETTINGS_NON_LL_HLS = StreamSettings( @@ -51,6 +53,7 @@ STREAM_SETTINGS_NON_LL_HLS = 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, ) @@ -383,6 +386,19 @@ class StreamView(HomeAssistantView): raise NotImplementedError() +TRANSFORM_IMAGE_FUNCTION = ( + lambda image: image, # Unused + lambda image: image, # No transform + lambda image: np.fliplr(image).copy(), # Mirror + lambda image: np.rot90(image, 2).copy(), # Rotate 180 + lambda image: np.flipud(image).copy(), # Flip + lambda image: np.flipud(np.rot90(image)).copy(), # Rotate left and flip + lambda image: np.rot90(image).copy(), # Rotate left + lambda image: np.flipud(np.rot90(image, -1)).copy(), # Rotate right and flip + lambda image: np.rot90(image, -1).copy(), # Rotate right +) + + class KeyFrameConverter: """ Enables generating and getting an image from the last keyframe seen in the stream. @@ -397,7 +413,7 @@ class KeyFrameConverter: If unsuccessful, get_image will return the previous image """ - def __init__(self, hass: HomeAssistant) -> None: + def __init__(self, hass: HomeAssistant, stream_settings: StreamSettings) -> None: """Initialize.""" # Keep import here so that we can import stream integration without installing reqs @@ -410,6 +426,7 @@ class KeyFrameConverter: self._turbojpeg = TurboJPEGSingleton.instance() self._lock = asyncio.Lock() self._codec_context: CodecContext | None = None + self._stream_settings = stream_settings def create_codec_context(self, codec_context: CodecContext) -> None: """ @@ -430,6 +447,11 @@ class KeyFrameConverter: self._codec_context.skip_frame = "NONKEY" self._codec_context.thread_type = "NONE" + @staticmethod + def transform_image(image: np.ndarray, orientation: int) -> np.ndarray: + """Transform image to a given orientation.""" + return TRANSFORM_IMAGE_FUNCTION[orientation](image) + def _generate_image(self, width: int | None, height: int | None) -> None: """ Generate the keyframe image. @@ -462,8 +484,13 @@ class KeyFrameConverter: if frames: frame = frames[0] if width and height: - frame = frame.reformat(width=width, height=height) - bgr_array = frame.to_ndarray(format="bgr24") + if self._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 + ) self._image = bytes(self._turbojpeg.encode(bgr_array)) async def async_get_image( diff --git a/homeassistant/components/stream/fmp4utils.py b/homeassistant/components/stream/fmp4utils.py index 313f5632841..ed9dd6a9724 100644 --- a/homeassistant/components/stream/fmp4utils.py +++ b/homeassistant/components/stream/fmp4utils.py @@ -5,7 +5,7 @@ from collections.abc import Generator from typing import TYPE_CHECKING if TYPE_CHECKING: - from io import BytesIO + from io import BufferedIOBase def find_box( @@ -141,9 +141,55 @@ def get_codec_string(mp4_bytes: bytes) -> str: return ",".join(codecs) -def read_init(bytes_io: BytesIO) -> bytes: +def read_init(bytes_io: BufferedIOBase) -> bytes: """Read the init from a mp4 file.""" bytes_io.seek(24) moov_len = int.from_bytes(bytes_io.read(4), byteorder="big") bytes_io.seek(0) return bytes_io.read(24 + moov_len) + + +ZERO32 = b"\x00\x00\x00\x00" +ONE32 = b"\x00\x01\x00\x00" +NEGONE32 = b"\xFF\xFF\x00\x00" +XYW_ROW = ZERO32 + ZERO32 + b"\x40\x00\x00\x00" +ROTATE_RIGHT = (ZERO32 + ONE32 + ZERO32) + (NEGONE32 + ZERO32 + ZERO32) +ROTATE_LEFT = (ZERO32 + NEGONE32 + ZERO32) + (ONE32 + ZERO32 + ZERO32) +ROTATE_180 = (NEGONE32 + ZERO32 + ZERO32) + (ZERO32 + NEGONE32 + ZERO32) +MIRROR = (NEGONE32 + ZERO32 + ZERO32) + (ZERO32 + ONE32 + ZERO32) +FLIP = (ONE32 + ZERO32 + ZERO32) + (ZERO32 + NEGONE32 + ZERO32) +# The two below do not seem to get applied properly +ROTATE_LEFT_FLIP = (ZERO32 + NEGONE32 + ZERO32) + (NEGONE32 + ZERO32 + ZERO32) +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, +) + + +def transform_init(init: bytes, orientation: int) -> bytes: + """Change the transformation matrix in the header.""" + if orientation == 1: + return init + # Find moov + moov_location = next(find_box(init, b"moov")) + mvhd_location = next(find_box(init, b"trak", moov_location)) + tkhd_location = next(find_box(init, b"tkhd", mvhd_location)) + tkhd_length = int.from_bytes( + init[tkhd_location : tkhd_location + 4], byteorder="big" + ) + return ( + init[: tkhd_location + tkhd_length - 44] + + TRANSFORM_MATRIX_TOP[orientation] + + XYW_ROW + + init[tkhd_location + tkhd_length - 8 :] + ) diff --git a/homeassistant/components/stream/hls.py b/homeassistant/components/stream/hls.py index d3bcbb360a6..e8920abcaa6 100644 --- a/homeassistant/components/stream/hls.py +++ b/homeassistant/components/stream/hls.py @@ -24,7 +24,7 @@ from .core import ( StreamSettings, StreamView, ) -from .fmp4utils import get_codec_string +from .fmp4utils import get_codec_string, transform_init if TYPE_CHECKING: from . import Stream @@ -339,7 +339,7 @@ class HlsInitView(StreamView): if not (segments := track.get_segments()) or not (body := segments[0].init): return web.HTTPNotFound() return web.Response( - body=body, + body=transform_init(body, stream.orientation), headers={"Content-Type": "video/mp4"}, ) diff --git a/homeassistant/components/stream/recorder.py b/homeassistant/components/stream/recorder.py index 3910a2d2fed..1eb7a6feedb 100644 --- a/homeassistant/components/stream/recorder.py +++ b/homeassistant/components/stream/recorder.py @@ -1,7 +1,7 @@ """Provide functionality to record stream.""" from __future__ import annotations -from io import BytesIO +from io import DEFAULT_BUFFER_SIZE, BytesIO import logging import os from typing import TYPE_CHECKING @@ -16,6 +16,7 @@ from .const import ( SEGMENT_CONTAINER_FORMAT, ) from .core import PROVIDERS, IdleTimer, Segment, StreamOutput, StreamSettings +from .fmp4utils import read_init, transform_init if TYPE_CHECKING: import deque @@ -147,6 +148,20 @@ class RecorderOutput(StreamOutput): source.close() + def write_transform_matrix_and_rename(video_path: str) -> None: + """Update the transform matrix and write to the desired filename.""" + with open(video_path + ".tmp", mode="rb") as in_file, open( + video_path, mode="wb" + ) as out_file: + init = transform_init( + read_init(in_file), self.stream_settings.orientation + ) + out_file.write(init) + in_file.seek(len(init)) + while chunk := in_file.read(DEFAULT_BUFFER_SIZE): + out_file.write(chunk) + os.remove(video_path + ".tmp") + def finish_writing( segments: deque[Segment], output: av.OutputContainer, video_path: str ) -> None: @@ -159,7 +174,7 @@ class RecorderOutput(StreamOutput): return output.close() try: - os.rename(video_path + ".tmp", video_path) + write_transform_matrix_and_rename(video_path) except FileNotFoundError: _LOGGER.error( "Error writing to '%s'. There are likely multiple recordings writing to the same file", diff --git a/tests/components/camera/common.py b/tests/components/camera/common.py index ee2a3cb2974..e30de46c07b 100644 --- a/tests/components/camera/common.py +++ b/tests/components/camera/common.py @@ -5,21 +5,10 @@ components. Instead call the service directly. """ from unittest.mock import Mock -from homeassistant.components.camera.const import DATA_CAMERA_PREFS, PREF_PRELOAD_STREAM - EMPTY_8_6_JPEG = b"empty_8_6" WEBRTC_ANSWER = "a=sendonly" -def mock_camera_prefs(hass, entity_id, prefs=None): - """Fixture for cloud component.""" - prefs_to_set = {PREF_PRELOAD_STREAM: True} - if prefs is not None: - prefs_to_set.update(prefs) - hass.data[DATA_CAMERA_PREFS]._prefs[entity_id] = prefs_to_set - return prefs_to_set - - def mock_turbo_jpeg( first_width=None, second_width=None, first_height=None, second_height=None ): diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index cea9c527946..71415284d35 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -7,7 +7,11 @@ from unittest.mock import AsyncMock, Mock, PropertyMock, mock_open, patch import pytest from homeassistant.components import camera -from homeassistant.components.camera.const import DOMAIN, PREF_PRELOAD_STREAM +from homeassistant.components.camera.const import ( + DOMAIN, + 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 @@ -17,9 +21,10 @@ from homeassistant.const import ( STATE_UNAVAILABLE, ) from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component -from .common import EMPTY_8_6_JPEG, WEBRTC_ANSWER, mock_camera_prefs, mock_turbo_jpeg +from .common import EMPTY_8_6_JPEG, WEBRTC_ANSWER, mock_turbo_jpeg STREAM_SOURCE = "rtsp://127.0.0.1/stream" HLS_STREAM_SOURCE = "http://127.0.0.1/example.m3u" @@ -34,12 +39,6 @@ def mock_stream_fixture(hass): ) -@pytest.fixture(name="setup_camera_prefs") -def setup_camera_prefs_fixture(hass): - """Initialize HTTP API.""" - return mock_camera_prefs(hass, "camera.demo_camera") - - @pytest.fixture(name="image_mock_url") async def image_mock_url_fixture(hass): """Fixture for get_image tests.""" @@ -294,30 +293,90 @@ async def test_websocket_get_prefs(hass, hass_ws_client, mock_camera): assert msg["success"] -async def test_websocket_update_prefs( - hass, hass_ws_client, mock_camera, setup_camera_prefs -): - """Test updating preference.""" - await async_setup_component(hass, "camera", {}) - assert setup_camera_prefs[PREF_PRELOAD_STREAM] +async def test_websocket_update_preload_prefs(hass, hass_ws_client, mock_camera): + """Test updating camera preferences.""" + client = await hass_ws_client(hass) + await client.send_json( + {"id": 7, "type": "camera/get_prefs", "entity_id": "camera.demo_camera"} + ) + msg = await client.receive_json() + + # There should be no preferences + assert not msg["result"] + + # Update the preference await client.send_json( { "id": 8, "type": "camera/update_prefs", "entity_id": "camera.demo_camera", - "preload_stream": False, + "preload_stream": True, + } + ) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"][PREF_PRELOAD_STREAM] is True + + # Check that the preference was saved + await client.send_json( + {"id": 9, "type": "camera/get_prefs", "entity_id": "camera.demo_camera"} + ) + msg = await client.receive_json() + # preload_stream entry for this camera should have been added + assert msg["result"][PREF_PRELOAD_STREAM] is True + + +async def test_websocket_update_orientation_prefs(hass, hass_ws_client, mock_camera): + """Test updating camera preferences.""" + + client = await hass_ws_client(hass) + + # Try sending orientation update for entity not in entity registry + await client.send_json( + { + "id": 10, + "type": "camera/update_prefs", + "entity_id": "camera.demo_uniquecamera", + "orientation": 3, } ) response = await client.receive_json() + assert not response["success"] + assert response["error"]["code"] == "update_failed" - assert response["success"] - assert not setup_camera_prefs[PREF_PRELOAD_STREAM] - assert ( - response["result"][PREF_PRELOAD_STREAM] - == setup_camera_prefs[PREF_PRELOAD_STREAM] + registry = er.async_get(hass) + assert not registry.async_get("camera.demo_uniquecamera") + # Since we don't have a unique id, we need to create a registry entry + registry.async_get_or_create(DOMAIN, "demo", "uniquecamera") + registry.async_update_entity_options( + "camera.demo_uniquecamera", + DOMAIN, + {}, ) + await client.send_json( + { + "id": 11, + "type": "camera/update_prefs", + "entity_id": "camera.demo_uniquecamera", + "orientation": 3, + } + ) + response = await client.receive_json() + assert response["success"] + + er_camera_prefs = registry.async_get("camera.demo_uniquecamera").options[DOMAIN] + assert er_camera_prefs[PREF_ORIENTATION] == 3 + assert response["result"][PREF_ORIENTATION] == er_camera_prefs[PREF_ORIENTATION] + # Check that the preference was saved + await client.send_json( + {"id": 12, "type": "camera/get_prefs", "entity_id": "camera.demo_uniquecamera"} + ) + msg = await client.receive_json() + # orientation entry for this camera should have been added + assert msg["result"]["orientation"] == 3 + async def test_play_stream_service_no_source(hass, mock_camera, mock_stream): """Test camera play_stream service.""" diff --git a/tests/components/stream/common.py b/tests/components/stream/common.py index 7fc25cb8478..de5b2c234eb 100644 --- a/tests/components/stream/common.py +++ b/tests/components/stream/common.py @@ -9,6 +9,11 @@ import av import numpy as np from homeassistant.components.stream.core import Segment +from homeassistant.components.stream.fmp4utils import ( + TRANSFORM_MATRIX_TOP, + XYW_ROW, + find_box, +) FAKE_TIME = datetime.utcnow() # Segment with defaults filled in for use in tests @@ -150,3 +155,18 @@ def remux_with_audio(source, container_format, audio_codec): output.seek(0) return output + + +def assert_mp4_has_transform_matrix(mp4: bytes, orientation: int): + """Assert that the mp4 (or init) has the proper transformation matrix.""" + # Find moov + moov_location = next(find_box(mp4, b"moov")) + mvhd_location = next(find_box(mp4, b"trak", moov_location)) + tkhd_location = next(find_box(mp4, b"tkhd", mvhd_location)) + tkhd_length = int.from_bytes( + mp4[tkhd_location : tkhd_location + 4], byteorder="big" + ) + assert ( + mp4[tkhd_location + tkhd_length - 44 : tkhd_location + tkhd_length - 8] + == TRANSFORM_MATRIX_TOP[orientation] + XYW_ROW + ) diff --git a/tests/components/stream/test_hls.py b/tests/components/stream/test_hls.py index 715e69fb889..ad430cb6e49 100644 --- a/tests/components/stream/test_hls.py +++ b/tests/components/stream/test_hls.py @@ -21,7 +21,11 @@ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from tests.common import async_fire_time_changed -from tests.components.stream.common import FAKE_TIME, DefaultSegment as Segment +from tests.components.stream.common import ( + FAKE_TIME, + DefaultSegment as Segment, + assert_mp4_has_transform_matrix, +) STREAM_SOURCE = "some-stream-source" INIT_BYTES = b"init" @@ -180,6 +184,7 @@ async def test_hls_stream( assert stream.get_diagnostics() == { "container_format": "mov,mp4,m4a,3gp,3g2,mj2", "keepalive": False, + "orientation": 1, "start_worker": 1, "video_codec": "h264", "worker_error": 1, @@ -515,3 +520,42 @@ async def test_remove_incomplete_segment_on_exit( assert segments[-1].complete assert len(segments) == 2 await stream.stop() + + +async def test_hls_stream_rotate( + hass, setup_component, hls_stream, stream_worker_sync, h264_video +): + """ + Test hls stream with rotation applied. + + Purposefully not mocking anything here to test full + integration with the stream component. + """ + + stream_worker_sync.pause() + + # Setup demo HLS track + stream = create_stream(hass, h264_video, {}) + + # Request stream + stream.add_provider(HLS_PROVIDER) + await stream.start() + + hls_client = await hls_stream(stream) + + # Fetch master playlist + master_playlist_response = await hls_client.get() + assert master_playlist_response.status == HTTPStatus.OK + + # Fetch rotated init + stream.orientation = 6 + 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) + + # Stop stream, if it hasn't quit already + await stream.stop() diff --git a/tests/components/stream/test_recorder.py b/tests/components/stream/test_recorder.py index a070f609129..c07675c7712 100644 --- a/tests/components/stream/test_recorder.py +++ b/tests/components/stream/test_recorder.py @@ -20,7 +20,12 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from .common import DefaultSegment as Segment, generate_h264_video, remux_with_audio +from .common import ( + DefaultSegment as Segment, + assert_mp4_has_transform_matrix, + generate_h264_video, + remux_with_audio, +) from tests.common import async_fire_time_changed @@ -72,7 +77,7 @@ async def test_record_stream(hass, filename, h264_video): async def test_record_lookback(hass, filename, h264_video): - """Exercise record with loopback.""" + """Exercise record with lookback.""" stream = create_stream(hass, h264_video, {}) @@ -252,3 +257,40 @@ async def test_recorder_log(hass, filename, caplog): await stream.async_record(filename) assert "https://abcd:efgh@foo.bar" not in caplog.text assert "https://****:****@foo.bar" in caplog.text + + +async def test_record_stream_rotate(hass, filename, h264_video): + """Test record stream with rotation.""" + + worker_finished = asyncio.Event() + + class MockStream(Stream): + """Mock Stream so we can patch remove_provider.""" + + async def remove_provider(self, provider): + """Add a finished event to Stream.remove_provider.""" + await Stream.remove_provider(self, provider) + worker_finished.set() + + with patch("homeassistant.components.stream.Stream", wraps=MockStream): + stream = create_stream(hass, h264_video, {}) + stream.orientation = 8 + + with patch.object(hass.config, "is_allowed_path", return_value=True): + make_recording = hass.async_create_task(stream.async_record(filename)) + + # In general usage the recorder will only include what has already been + # processed by the worker. To guarantee we have some output for the test, + # wait until the worker has finished before firing + await worker_finished.wait() + + # Fire the IdleTimer + future = dt_util.utcnow() + timedelta(seconds=30) + async_fire_time_changed(hass, future) + + await make_recording + + # Assert + assert os.path.exists(filename) + with open(filename, "rb") as rotated_mp4: + assert_mp4_has_transform_matrix(rotated_mp4.read(), stream.orientation) diff --git a/tests/components/stream/test_worker.py b/tests/components/stream/test_worker.py index 94d77e7657e..70769840dd7 100644 --- a/tests/components/stream/test_worker.py +++ b/tests/components/stream/test_worker.py @@ -22,6 +22,7 @@ import threading from unittest.mock import patch import av +import numpy as np import pytest from homeassistant.components.stream import KeyFrameConverter, Stream, create_stream @@ -88,6 +89,7 @@ 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, ) } @@ -284,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), + KeyFrameConverter(hass, 1), threading.Event(), ) @@ -897,24 +899,23 @@ 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, "start_worker": 1, "video_codec": "hevc", "worker_error": 1, } -async def test_get_image(hass, filename): +async def test_get_image(hass, h264_video, filename): """Test that the has_keyframe metadata matches the media.""" await async_setup_component(hass, "stream", {"stream": {}}) - source = generate_h264_video() - # Since libjpeg-turbo is not installed on the CI runner, we use a mock with patch( "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, source, {}) + stream = create_stream(hass, h264_video, {}) with patch.object(hass.config, "is_allowed_path", return_value=True): make_recording = hass.async_create_task(stream.async_record(filename)) @@ -935,6 +936,7 @@ 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" @@ -945,3 +947,35 @@ async def test_worker_disable_ll_hls(hass): stream_settings=stream_settings, ) assert stream_settings.ll_hls is False + + +async def test_get_image_rotated(hass, h264_video, filename): + """Test that the has_keyframe metadata matches the media.""" + await async_setup_component(hass, "stream", {"stream": {}}) + + # Since libjpeg-turbo is not installed on the CI runner, we use a mock + with patch( + "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 + + with patch.object(hass.config, "is_allowed_path", return_value=True): + make_recording = hass.async_create_task(stream.async_record(filename)) + await make_recording + assert stream._keyframe_converter._image is None + + assert await stream.async_get_image() == EMPTY_8_6_JPEG + await stream.stop() + assert ( + np.rot90( + mock_turbo_jpeg_singleton.instance.return_value.encode.call_args_list[ + 0 + ][0][0] + ) + == mock_turbo_jpeg_singleton.instance.return_value.encode.call_args_list[1][ + 0 + ][0] + ).all() From 7198273a42a1b582aa22776ebe9c729a6dc7628b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 6 Sep 2022 09:43:49 +0200 Subject: [PATCH 122/955] Improve entity type hints [q] (#77875) --- homeassistant/components/qbittorrent/sensor.py | 2 +- homeassistant/components/qnap/sensor.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/qbittorrent/sensor.py b/homeassistant/components/qbittorrent/sensor.py index 1b34bda7a44..151055a1688 100644 --- a/homeassistant/components/qbittorrent/sensor.py +++ b/homeassistant/components/qbittorrent/sensor.py @@ -113,7 +113,7 @@ class QBittorrentSensor(SensorEntity): self._attr_name = f"{client_name} {description.name}" self._attr_available = False - def update(self): + def update(self) -> None: """Get the latest data from qBittorrent and updates the state.""" try: data = self.client.sync_main_data() diff --git a/homeassistant/components/qnap/sensor.py b/homeassistant/components/qnap/sensor.py index 7366dc5dc41..8c093eb9232 100644 --- a/homeassistant/components/qnap/sensor.py +++ b/homeassistant/components/qnap/sensor.py @@ -330,7 +330,7 @@ class QNAPSensor(SensorEntity): ) return f"{server_name} {self.entity_description.name}" - def update(self): + def update(self) -> None: """Get the latest data for the states.""" self._api.update() From 6f564e4f514b56bce281ec7e82703cfbff87b417 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 6 Sep 2022 09:47:35 +0200 Subject: [PATCH 123/955] Improve entity type hints [r] (#77874) --- .../components/rachio/binary_sensor.py | 4 +- homeassistant/components/rachio/switch.py | 25 +++++------ .../components/raincloud/binary_sensor.py | 2 +- homeassistant/components/raincloud/sensor.py | 2 +- homeassistant/components/raincloud/switch.py | 7 ++-- .../components/random/binary_sensor.py | 2 +- homeassistant/components/random/sensor.py | 2 +- homeassistant/components/recswitch/switch.py | 7 ++-- homeassistant/components/reddit/sensor.py | 2 +- .../components/rejseplanen/sensor.py | 2 +- .../remote_rpi_gpio/binary_sensor.py | 4 +- .../components/remote_rpi_gpio/switch.py | 6 ++- homeassistant/components/rest/switch.py | 7 ++-- .../components/rflink/binary_sensor.py | 2 +- homeassistant/components/rflink/sensor.py | 2 +- .../components/rfxtrx/binary_sensor.py | 2 +- homeassistant/components/rfxtrx/sensor.py | 2 +- homeassistant/components/rfxtrx/switch.py | 7 ++-- .../components/ring/binary_sensor.py | 4 +- homeassistant/components/ring/camera.py | 6 +-- homeassistant/components/ring/sensor.py | 8 ++-- homeassistant/components/ring/switch.py | 5 ++- homeassistant/components/ripple/sensor.py | 2 +- .../components/rmvtransport/sensor.py | 2 +- homeassistant/components/roon/media_player.py | 42 +++++++++++-------- homeassistant/components/rova/sensor.py | 2 +- .../components/russound_rio/media_player.py | 10 ++--- .../components/russound_rnet/media_player.py | 12 +++--- 28 files changed, 97 insertions(+), 83 deletions(-) diff --git a/homeassistant/components/rachio/binary_sensor.py b/homeassistant/components/rachio/binary_sensor.py index 2fe99fb442e..294931b7538 100644 --- a/homeassistant/components/rachio/binary_sensor.py +++ b/homeassistant/components/rachio/binary_sensor.py @@ -119,7 +119,7 @@ class RachioControllerOnlineBinarySensor(RachioControllerBinarySensor): self.async_write_ha_state() - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Subscribe to updates.""" self._state = self._controller.init_data[KEY_STATUS] == STATUS_ONLINE @@ -165,7 +165,7 @@ class RachioRainSensor(RachioControllerBinarySensor): self.async_write_ha_state() - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Subscribe to updates.""" self._state = self._controller.init_data[KEY_RAIN_SENSOR_TRIPPED] diff --git a/homeassistant/components/rachio/switch.py b/homeassistant/components/rachio/switch.py index 227e8beaec3..5e91d339d0d 100644 --- a/homeassistant/components/rachio/switch.py +++ b/homeassistant/components/rachio/switch.py @@ -3,6 +3,7 @@ from abc import abstractmethod from contextlib import suppress from datetime import timedelta import logging +from typing import Any import voluptuous as vol @@ -237,15 +238,15 @@ class RachioStandbySwitch(RachioSwitch): self.async_write_ha_state() - def turn_on(self, **kwargs) -> None: + def turn_on(self, **kwargs: Any) -> None: """Put the controller in standby mode.""" self._controller.rachio.device.turn_off(self._controller.controller_id) - def turn_off(self, **kwargs) -> None: + def turn_off(self, **kwargs: Any) -> None: """Resume controller functionality.""" self._controller.rachio.device.turn_on(self._controller.controller_id) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Subscribe to updates.""" if KEY_ON in self._controller.init_data: self._state = not self._controller.init_data[KEY_ON] @@ -309,17 +310,17 @@ class RachioRainDelay(RachioSwitch): self._cancel_update = None self.async_write_ha_state() - def turn_on(self, **kwargs) -> None: + def turn_on(self, **kwargs: Any) -> None: """Activate a 24 hour rain delay on the controller.""" self._controller.rachio.device.rain_delay(self._controller.controller_id, 86400) _LOGGER.debug("Starting rain delay for 24 hours") - def turn_off(self, **kwargs) -> None: + def turn_off(self, **kwargs: Any) -> None: """Resume controller functionality.""" self._controller.rachio.device.rain_delay(self._controller.controller_id, 0) _LOGGER.debug("Canceling rain delay") - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Subscribe to updates.""" if KEY_RAIN_DELAY in self._controller.init_data: self._state = self._controller.init_data[ @@ -416,7 +417,7 @@ class RachioZone(RachioSwitch): props[ATTR_ZONE_SLOPE] = "Steep" return props - def turn_on(self, **kwargs) -> None: + def turn_on(self, **kwargs: Any) -> None: """Start watering this zone.""" # Stop other zones first self.turn_off() @@ -436,7 +437,7 @@ class RachioZone(RachioSwitch): str(manual_run_time), ) - def turn_off(self, **kwargs) -> None: + def turn_off(self, **kwargs: Any) -> None: """Stop watering all zones.""" self._controller.stop_watering() @@ -464,7 +465,7 @@ class RachioZone(RachioSwitch): self.async_write_ha_state() - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Subscribe to updates.""" self._state = self.zone_id == self._current_schedule.get(KEY_ZONE_ID) @@ -519,7 +520,7 @@ class RachioSchedule(RachioSwitch): """Return whether the schedule is allowed to run.""" return self._schedule_enabled - def turn_on(self, **kwargs) -> None: + def turn_on(self, **kwargs: Any) -> None: """Start this schedule.""" self._controller.rachio.schedulerule.start(self._schedule_id) _LOGGER.debug( @@ -528,7 +529,7 @@ class RachioSchedule(RachioSwitch): self._controller.name, ) - def turn_off(self, **kwargs) -> None: + def turn_off(self, **kwargs: Any) -> None: """Stop watering all zones.""" self._controller.stop_watering() @@ -548,7 +549,7 @@ class RachioSchedule(RachioSwitch): self.async_write_ha_state() - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Subscribe to updates.""" self._state = self._schedule_id == self._current_schedule.get(KEY_SCHEDULE_ID) diff --git a/homeassistant/components/raincloud/binary_sensor.py b/homeassistant/components/raincloud/binary_sensor.py index 888529b5c38..b3766cd02cd 100644 --- a/homeassistant/components/raincloud/binary_sensor.py +++ b/homeassistant/components/raincloud/binary_sensor.py @@ -58,7 +58,7 @@ class RainCloudBinarySensor(RainCloudEntity, BinarySensorEntity): """Return true if the binary sensor is on.""" return self._state - def update(self): + def update(self) -> None: """Get the latest data and updates the state.""" _LOGGER.debug("Updating RainCloud sensor: %s", self._name) self._state = getattr(self.data, self._sensor_type) diff --git a/homeassistant/components/raincloud/sensor.py b/homeassistant/components/raincloud/sensor.py index b07ccd1e7ac..4d21d36d069 100644 --- a/homeassistant/components/raincloud/sensor.py +++ b/homeassistant/components/raincloud/sensor.py @@ -66,7 +66,7 @@ class RainCloudSensor(RainCloudEntity, SensorEntity): """Return the units of measurement.""" return UNIT_OF_MEASUREMENT_MAP.get(self._sensor_type) - def update(self): + def update(self) -> None: """Get the latest data and updates the states.""" _LOGGER.debug("Updating RainCloud sensor: %s", self._name) if self._sensor_type == "battery": diff --git a/homeassistant/components/raincloud/switch.py b/homeassistant/components/raincloud/switch.py index b783a3d9375..89b2673f66e 100644 --- a/homeassistant/components/raincloud/switch.py +++ b/homeassistant/components/raincloud/switch.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any import voluptuous as vol @@ -68,7 +69,7 @@ class RainCloudSwitch(RainCloudEntity, SwitchEntity): """Return true if device is on.""" return self._state - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" if self._sensor_type == "manual_watering": self.data.watering_time = self._default_watering_timer @@ -76,7 +77,7 @@ class RainCloudSwitch(RainCloudEntity, SwitchEntity): self.data.auto_watering = True self._state = True - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" if self._sensor_type == "manual_watering": self.data.watering_time = "off" @@ -84,7 +85,7 @@ class RainCloudSwitch(RainCloudEntity, SwitchEntity): self.data.auto_watering = False self._state = False - def update(self): + def update(self) -> None: """Update device state.""" _LOGGER.debug("Updating RainCloud switch: %s", self._name) if self._sensor_type == "manual_watering": diff --git a/homeassistant/components/random/binary_sensor.py b/homeassistant/components/random/binary_sensor.py index 3a53e297d1f..5e688162124 100644 --- a/homeassistant/components/random/binary_sensor.py +++ b/homeassistant/components/random/binary_sensor.py @@ -63,7 +63,7 @@ class RandomSensor(BinarySensorEntity): """Return the sensor class of the sensor.""" return self._device_class - async def async_update(self): + async def async_update(self) -> None: """Get new state and update the sensor's state.""" self._state = bool(getrandbits(1)) diff --git a/homeassistant/components/random/sensor.py b/homeassistant/components/random/sensor.py index a877f5cf0a3..19cf403eab2 100644 --- a/homeassistant/components/random/sensor.py +++ b/homeassistant/components/random/sensor.py @@ -87,7 +87,7 @@ class RandomSensor(SensorEntity): """Return the attributes of the sensor.""" return {ATTR_MAXIMUM: self._maximum, ATTR_MINIMUM: self._minimum} - async def async_update(self): + async def async_update(self) -> None: """Get a new number and updates the states.""" self._state = randrange(self._minimum, self._maximum + 1) diff --git a/homeassistant/components/recswitch/switch.py b/homeassistant/components/recswitch/switch.py index cc98a922f50..43e71ef1df9 100644 --- a/homeassistant/components/recswitch/switch.py +++ b/homeassistant/components/recswitch/switch.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from pyrecswitch import RSNetwork, RSNetworkError import voluptuous as vol @@ -76,11 +77,11 @@ class RecSwitchSwitch(SwitchEntity): """Return true if switch is on.""" return self.gpio_state - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the switch.""" await self.async_set_gpio_status(True) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the switch.""" await self.async_set_gpio_status(False) @@ -93,7 +94,7 @@ class RecSwitchSwitch(SwitchEntity): except RSNetworkError as error: _LOGGER.error("Setting status to %s: %r", self.name, error) - async def async_update(self): + async def async_update(self) -> None: """Update the current switch status.""" try: diff --git a/homeassistant/components/reddit/sensor.py b/homeassistant/components/reddit/sensor.py index 0f4638634ce..0ae53ca8610 100644 --- a/homeassistant/components/reddit/sensor.py +++ b/homeassistant/components/reddit/sensor.py @@ -127,7 +127,7 @@ class RedditSensor(SensorEntity): """Return the icon to use in the frontend.""" return "mdi:reddit" - def update(self): + def update(self) -> None: """Update data from Reddit API.""" self._subreddit_data = [] diff --git a/homeassistant/components/rejseplanen/sensor.py b/homeassistant/components/rejseplanen/sensor.py index a6251bf62e7..6943e7f8aa3 100644 --- a/homeassistant/components/rejseplanen/sensor.py +++ b/homeassistant/components/rejseplanen/sensor.py @@ -150,7 +150,7 @@ class RejseplanenTransportSensor(SensorEntity): """Icon to use in the frontend, if any.""" return ICON - def update(self): + def update(self) -> None: """Get the latest data from rejseplanen.dk and update the states.""" self.data.update() self._times = self.data.info diff --git a/homeassistant/components/remote_rpi_gpio/binary_sensor.py b/homeassistant/components/remote_rpi_gpio/binary_sensor.py index 9af1a83b2e9..37994830c4d 100644 --- a/homeassistant/components/remote_rpi_gpio/binary_sensor.py +++ b/homeassistant/components/remote_rpi_gpio/binary_sensor.py @@ -75,7 +75,7 @@ class RemoteRPiGPIOBinarySensor(BinarySensorEntity): self._state = False self._sensor = sensor - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Run when entity about to be added to hass.""" def read_gpio(): @@ -101,7 +101,7 @@ class RemoteRPiGPIOBinarySensor(BinarySensorEntity): """Return the class of this sensor, from DEVICE_CLASSES.""" return - def update(self): + def update(self) -> None: """Update the GPIO state.""" try: self._state = remote_rpi_gpio.read_input(self._sensor) diff --git a/homeassistant/components/remote_rpi_gpio/switch.py b/homeassistant/components/remote_rpi_gpio/switch.py index 9e7aca37663..862efb0f89d 100644 --- a/homeassistant/components/remote_rpi_gpio/switch.py +++ b/homeassistant/components/remote_rpi_gpio/switch.py @@ -1,6 +1,8 @@ """Allows to configure a switch using RPi GPIO.""" from __future__ import annotations +from typing import Any + import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity @@ -75,13 +77,13 @@ class RemoteRPiGPIOSwitch(SwitchEntity): """Return true if device is on.""" return self._state - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" remote_rpi_gpio.write_output(self._switch, 1) self._state = True self.schedule_update_ha_state() - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" remote_rpi_gpio.write_output(self._switch, 0) self._state = False diff --git a/homeassistant/components/rest/switch.py b/homeassistant/components/rest/switch.py index c45eb581645..f2a5d93cd22 100644 --- a/homeassistant/components/rest/switch.py +++ b/homeassistant/components/rest/switch.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio from http import HTTPStatus import logging +from typing import Any import aiohttp import async_timeout @@ -153,7 +154,7 @@ class RestSwitch(TemplateEntity, SwitchEntity): """Return true if device is on.""" return self._state - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" body_on_t = self._body_on.async_render(parse_result=False) @@ -169,7 +170,7 @@ class RestSwitch(TemplateEntity, SwitchEntity): except (asyncio.TimeoutError, aiohttp.ClientError): _LOGGER.error("Error while switching on %s", self._resource) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" body_off_t = self._body_off.async_render(parse_result=False) @@ -201,7 +202,7 @@ class RestSwitch(TemplateEntity, SwitchEntity): ) return req - async def async_update(self): + async def async_update(self) -> None: """Get the current state, catching errors.""" try: await self.get_device_state(self.hass) diff --git a/homeassistant/components/rflink/binary_sensor.py b/homeassistant/components/rflink/binary_sensor.py index ce723e84b8c..7b095555376 100644 --- a/homeassistant/components/rflink/binary_sensor.py +++ b/homeassistant/components/rflink/binary_sensor.py @@ -83,7 +83,7 @@ class RflinkBinarySensor(RflinkDevice, BinarySensorEntity, RestoreEntity): self._delay_listener = None super().__init__(device_id, **kwargs) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Restore RFLink BinarySensor state.""" await super().async_added_to_hass() if (old_state := await self.async_get_last_state()) is not None: diff --git a/homeassistant/components/rflink/sensor.py b/homeassistant/components/rflink/sensor.py index 5aac1f6debe..2420e933653 100644 --- a/homeassistant/components/rflink/sensor.py +++ b/homeassistant/components/rflink/sensor.py @@ -123,7 +123,7 @@ class RflinkSensor(RflinkDevice, SensorEntity): """Domain specific event handler.""" self._state = event["value"] - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register update callback.""" # Remove temporary bogus entity_id if added tmp_entity = TMP_ENTITY.format(self._device_id) diff --git a/homeassistant/components/rfxtrx/binary_sensor.py b/homeassistant/components/rfxtrx/binary_sensor.py index 1a0fe698dcf..df79c17263c 100644 --- a/homeassistant/components/rfxtrx/binary_sensor.py +++ b/homeassistant/components/rfxtrx/binary_sensor.py @@ -147,7 +147,7 @@ class RfxtrxBinarySensor(RfxtrxEntity, BinarySensorEntity): self._cmd_on = cmd_on self._cmd_off = cmd_off - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Restore device state.""" await super().async_added_to_hass() diff --git a/homeassistant/components/rfxtrx/sensor.py b/homeassistant/components/rfxtrx/sensor.py index 86c3eabc922..c3524b022f8 100644 --- a/homeassistant/components/rfxtrx/sensor.py +++ b/homeassistant/components/rfxtrx/sensor.py @@ -284,7 +284,7 @@ class RfxtrxSensor(RfxtrxEntity, SensorEntity): self.entity_description = entity_description self._attr_unique_id = "_".join(x for x in (*device_id, entity_description.key)) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Restore device state.""" await super().async_added_to_hass() diff --git a/homeassistant/components/rfxtrx/switch.py b/homeassistant/components/rfxtrx/switch.py index f164e54b212..c73b0ba3b1d 100644 --- a/homeassistant/components/rfxtrx/switch.py +++ b/homeassistant/components/rfxtrx/switch.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any import RFXtrx as rfxtrxmod @@ -87,7 +88,7 @@ class RfxtrxSwitch(RfxtrxCommandEntity, SwitchEntity): self._cmd_on = cmd_on self._cmd_off = cmd_off - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Restore device state.""" await super().async_added_to_hass() @@ -134,7 +135,7 @@ class RfxtrxSwitch(RfxtrxCommandEntity, SwitchEntity): self.async_write_ha_state() - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" if self._cmd_on is not None: await self._async_send(self._device.send_command, self._cmd_on) @@ -143,7 +144,7 @@ class RfxtrxSwitch(RfxtrxCommandEntity, SwitchEntity): self._attr_is_on = True self.async_write_ha_state() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" if self._cmd_off is not None: await self._async_send(self._device.send_command, self._cmd_off) diff --git a/homeassistant/components/ring/binary_sensor.py b/homeassistant/components/ring/binary_sensor.py index efa92475551..fc47ad7cbf0 100644 --- a/homeassistant/components/ring/binary_sensor.py +++ b/homeassistant/components/ring/binary_sensor.py @@ -88,13 +88,13 @@ class RingBinarySensor(RingEntityMixin, BinarySensorEntity): self._attr_unique_id = f"{device.id}-{description.key}" self._update_alert() - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" await super().async_added_to_hass() self.ring_objects["dings_data"].async_add_listener(self._dings_update_callback) self._dings_update_callback() - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Disconnect callbacks.""" await super().async_will_remove_from_hass() self.ring_objects["dings_data"].async_remove_listener( diff --git a/homeassistant/components/ring/camera.py b/homeassistant/components/ring/camera.py index 5bf440cfcd9..f5d70a86cb3 100644 --- a/homeassistant/components/ring/camera.py +++ b/homeassistant/components/ring/camera.py @@ -60,7 +60,7 @@ class RingCam(RingEntityMixin, Camera): self._image = None self._expires_at = dt_util.utcnow() - FORCE_REFRESH_INTERVAL - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" await super().async_added_to_hass() @@ -68,7 +68,7 @@ class RingCam(RingEntityMixin, Camera): self._device, self._history_update_callback ) - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Disconnect callbacks.""" await super().async_will_remove_from_hass() @@ -144,7 +144,7 @@ class RingCam(RingEntityMixin, Camera): finally: await stream.close() - async def async_update(self): + async def async_update(self) -> None: """Update camera entity and refresh attributes.""" if self._last_event is None: return diff --git a/homeassistant/components/ring/sensor.py b/homeassistant/components/ring/sensor.py index 26068c149ce..1aaa073064f 100644 --- a/homeassistant/components/ring/sensor.py +++ b/homeassistant/components/ring/sensor.py @@ -82,7 +82,7 @@ class RingSensor(RingEntityMixin, SensorEntity): class HealthDataRingSensor(RingSensor): """Ring sensor that relies on health data.""" - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" await super().async_added_to_hass() @@ -90,7 +90,7 @@ class HealthDataRingSensor(RingSensor): self._device, self._health_update_callback ) - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Disconnect callbacks.""" await super().async_will_remove_from_hass() @@ -125,7 +125,7 @@ class HistoryRingSensor(RingSensor): _latest_event = None - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" await super().async_added_to_hass() @@ -133,7 +133,7 @@ class HistoryRingSensor(RingSensor): self._device, self._history_update_callback ) - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Disconnect callbacks.""" await super().async_will_remove_from_hass() diff --git a/homeassistant/components/ring/switch.py b/homeassistant/components/ring/switch.py index 682404e57f9..0fa6e3b1114 100644 --- a/homeassistant/components/ring/switch.py +++ b/homeassistant/components/ring/switch.py @@ -1,6 +1,7 @@ """This component provides HA switch support for Ring Door Bell/Chimes.""" from datetime import timedelta import logging +from typing import Any import requests @@ -97,11 +98,11 @@ class SirenSwitch(BaseRingSwitch): """If the switch is currently on or off.""" return self._siren_on - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the siren on for 30 seconds.""" self._set_switch(1) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the siren off.""" self._set_switch(0) diff --git a/homeassistant/components/ripple/sensor.py b/homeassistant/components/ripple/sensor.py index cbb72ab8e57..01f326b2a63 100644 --- a/homeassistant/components/ripple/sensor.py +++ b/homeassistant/components/ripple/sensor.py @@ -70,7 +70,7 @@ class RippleSensor(SensorEntity): """Return the state attributes of the sensor.""" return {ATTR_ATTRIBUTION: ATTRIBUTION} - def update(self): + def update(self) -> None: """Get the latest state of the sensor.""" if (balance := get_balance(self.address)) is not None: self._state = balance diff --git a/homeassistant/components/rmvtransport/sensor.py b/homeassistant/components/rmvtransport/sensor.py index 3394c4ef4d3..25f14ae6df3 100644 --- a/homeassistant/components/rmvtransport/sensor.py +++ b/homeassistant/components/rmvtransport/sensor.py @@ -186,7 +186,7 @@ class RMVDepartureSensor(SensorEntity): """Return the unit this state is expressed in.""" return TIME_MINUTES - async def async_update(self): + async def async_update(self) -> None: """Get the latest data and update the state.""" await self.data.async_update() diff --git a/homeassistant/components/roon/media_player.py b/homeassistant/components/roon/media_player.py index 673316f64a3..58737b30423 100644 --- a/homeassistant/components/roon/media_player.py +++ b/homeassistant/components/roon/media_player.py @@ -1,5 +1,8 @@ """MediaPlayer platform for Roon integration.""" +from __future__ import annotations + import logging +from typing import Any from roonapi import split_media_path import voluptuous as vol @@ -8,6 +11,7 @@ from homeassistant.components.media_player import ( MediaPlayerEntity, MediaPlayerEntityFeature, ) +from homeassistant.components.media_player.browse_media import BrowseMedia from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( DEVICE_DEFAULT_NAME, @@ -119,7 +123,7 @@ class RoonDevice(MediaPlayerEntity): self._volume_level = 0 self.update_data(player_data) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callback.""" self.async_on_remove( async_dispatcher_connect( @@ -376,38 +380,38 @@ class RoonDevice(MediaPlayerEntity): """Boolean if shuffle is enabled.""" return self._shuffle - def media_play(self): + def media_play(self) -> None: """Send play command to device.""" self._server.roonapi.playback_control(self.output_id, "play") - def media_pause(self): + def media_pause(self) -> None: """Send pause command to device.""" self._server.roonapi.playback_control(self.output_id, "pause") - def media_play_pause(self): + def media_play_pause(self) -> None: """Toggle play command to device.""" self._server.roonapi.playback_control(self.output_id, "playpause") - def media_stop(self): + def media_stop(self) -> None: """Send stop command to device.""" self._server.roonapi.playback_control(self.output_id, "stop") - def media_next_track(self): + def media_next_track(self) -> None: """Send next track command to device.""" self._server.roonapi.playback_control(self.output_id, "next") - def media_previous_track(self): + def media_previous_track(self) -> None: """Send previous track command to device.""" self._server.roonapi.playback_control(self.output_id, "previous") - def media_seek(self, position): + def media_seek(self, position: float) -> None: """Send seek command to device.""" self._server.roonapi.seek(self.output_id, position) # Seek doesn't cause an async update - so force one self._media_position = position self.schedule_update_ha_state() - def set_volume_level(self, volume): + def set_volume_level(self, volume: float) -> None: """Send new volume_level to device.""" volume = int(volume * 100) self._server.roonapi.change_volume(self.output_id, volume) @@ -416,15 +420,15 @@ class RoonDevice(MediaPlayerEntity): """Send mute/unmute to device.""" self._server.roonapi.mute(self.output_id, mute) - def volume_up(self): + def volume_up(self) -> None: """Send new volume_level to device.""" self._server.roonapi.change_volume(self.output_id, 3, "relative") - def volume_down(self): + def volume_down(self) -> None: """Send new volume_level to device.""" self._server.roonapi.change_volume(self.output_id, -3, "relative") - def turn_on(self): + def turn_on(self) -> None: """Turn on device (if supported).""" if not (self.supports_standby and "source_controls" in self.player_data): self.media_play() @@ -436,7 +440,7 @@ class RoonDevice(MediaPlayerEntity): ) return - def turn_off(self): + def turn_off(self) -> None: """Turn off device (if supported).""" if not (self.supports_standby and "source_controls" in self.player_data): self.media_stop() @@ -447,11 +451,11 @@ class RoonDevice(MediaPlayerEntity): self._server.roonapi.standby(self.output_id, source["control_key"]) return - def set_shuffle(self, shuffle): + def set_shuffle(self, shuffle: bool) -> None: """Set shuffle state.""" self._server.roonapi.shuffle(self.output_id, shuffle) - def play_media(self, media_type, media_id, **kwargs): + def play_media(self, media_type: str, media_id: str, **kwargs: Any) -> None: """Send the play_media command to the media player.""" _LOGGER.debug("Playback request for %s / %s", media_type, media_id) @@ -469,7 +473,7 @@ class RoonDevice(MediaPlayerEntity): path_list, ) - def join_players(self, group_members): + def join_players(self, group_members: list[str]) -> None: """Join `group_members` as a player group with the current player.""" zone_data = self._server.roonapi.zone_by_output_id(self._output_id) @@ -509,7 +513,7 @@ class RoonDevice(MediaPlayerEntity): [self._output_id] + [sync_available[name] for name in names] ) - def unjoin_player(self): + def unjoin_player(self) -> None: """Remove this player from any group.""" if not self._server.roonapi.is_grouped(self._output_id): @@ -548,7 +552,9 @@ class RoonDevice(MediaPlayerEntity): self._server.roonapi.transfer_zone, self._zone_id, transfer_id ) - async def async_browse_media(self, media_content_type=None, media_content_id=None): + async def async_browse_media( + self, media_content_type: str | None = None, media_content_id: str | None = None + ) -> BrowseMedia: """Implement the websocket media browsing helper.""" return await self.hass.async_add_executor_job( browse_media, diff --git a/homeassistant/components/rova/sensor.py b/homeassistant/components/rova/sensor.py index 1057d9b5ea8..26f6d67e697 100644 --- a/homeassistant/components/rova/sensor.py +++ b/homeassistant/components/rova/sensor.py @@ -117,7 +117,7 @@ class RovaSensor(SensorEntity): self._attr_name = f"{platform_name}_{description.name}" self._attr_device_class = SensorDeviceClass.TIMESTAMP - def update(self): + def update(self) -> None: """Get the latest data from the sensor and update the state.""" self.data_service.update() pickup_date = self.data_service.data.get(self.entity_description.key) diff --git a/homeassistant/components/russound_rio/media_player.py b/homeassistant/components/russound_rio/media_player.py index e905ab0c726..afd96289594 100644 --- a/homeassistant/components/russound_rio/media_player.py +++ b/homeassistant/components/russound_rio/media_player.py @@ -115,7 +115,7 @@ class RussoundZoneDevice(MediaPlayerEntity): if source_id == current: self.schedule_update_ha_state() - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callback handlers.""" self._russ.add_zone_callback(self._zone_callback_handler) self._russ.add_source_callback(self._source_callback_handler) @@ -178,20 +178,20 @@ class RussoundZoneDevice(MediaPlayerEntity): """ return float(self._zone_var("volume", 0)) / 50.0 - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Turn off the zone.""" await self._russ.send_zone_event(self._zone_id, "ZoneOff") - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Turn on the zone.""" await self._russ.send_zone_event(self._zone_id, "ZoneOn") - async def async_set_volume_level(self, volume): + async def async_set_volume_level(self, volume: float) -> None: """Set the volume level.""" rvol = int(volume * 50.0) await self._russ.send_zone_event(self._zone_id, "KeyPress", "Volume", rvol) - async def async_select_source(self, source): + async def async_select_source(self, source: str) -> None: """Select the source input for this zone.""" for source_id, name in self._sources: if name.lower() != source.lower(): diff --git a/homeassistant/components/russound_rnet/media_player.py b/homeassistant/components/russound_rnet/media_player.py index b97b431333f..6e1074c9837 100644 --- a/homeassistant/components/russound_rnet/media_player.py +++ b/homeassistant/components/russound_rnet/media_player.py @@ -90,7 +90,7 @@ class RussoundRNETDevice(MediaPlayerEntity): self._volume = None self._source = None - def update(self): + def update(self) -> None: """Retrieve latest state.""" # Updated this function to make a single call to get_zone_info, so that # with a single call we can get On/Off, Volume and Source, reducing the @@ -141,7 +141,7 @@ class RussoundRNETDevice(MediaPlayerEntity): """ return self._volume - def set_volume_level(self, volume): + def set_volume_level(self, volume: float) -> None: """Set volume level. Volume has a range (0..1). Translate this to a range of (0..100) as expected @@ -149,19 +149,19 @@ class RussoundRNETDevice(MediaPlayerEntity): """ self._russ.set_volume("1", self._zone_id, volume * 100) - def turn_on(self): + def turn_on(self) -> None: """Turn the media player on.""" self._russ.set_power("1", self._zone_id, "1") - def turn_off(self): + def turn_off(self) -> None: """Turn off media player.""" self._russ.set_power("1", self._zone_id, "0") - def mute_volume(self, mute): + def mute_volume(self, mute: bool) -> None: """Send mute command.""" self._russ.toggle_mute("1", self._zone_id) - def select_source(self, source): + def select_source(self, source: str) -> None: """Set the input source.""" if source in self._sources: index = self._sources.index(source) From 474844744bdd2b0dcba46b82d9d3fcd8e3dbad24 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 6 Sep 2022 09:51:33 +0200 Subject: [PATCH 124/955] Improve entity type hints [p] (#77871) --- .../panasonic_bluray/media_player.py | 12 +++--- .../panasonic_viera/media_player.py | 38 +++++++++++-------- .../components/panasonic_viera/remote.py | 11 ++++-- .../components/pandora/media_player.py | 12 +++--- homeassistant/components/pencom/switch.py | 7 ++-- .../components/philips_js/media_player.py | 34 +++++++++-------- homeassistant/components/philips_js/remote.py | 8 ++-- .../components/ping/binary_sensor.py | 2 +- .../components/pioneer/media_player.py | 14 +++---- .../components/pjlink/media_player.py | 10 ++--- homeassistant/components/plex/media_player.py | 27 +++++++------ homeassistant/components/plex/sensor.py | 8 ++-- .../components/pocketcasts/sensor.py | 2 +- .../components/point/binary_sensor.py | 4 +- .../components/progettihwsw/switch.py | 7 ++-- homeassistant/components/proliphix/climate.py | 6 ++- .../components/proxmoxve/binary_sensor.py | 2 +- homeassistant/components/ps4/media_player.py | 18 ++++----- .../components/pulseaudio_loopback/switch.py | 9 +++-- homeassistant/components/push/camera.py | 2 +- homeassistant/components/pushbullet/sensor.py | 2 +- homeassistant/components/pyload/sensor.py | 2 +- 22 files changed, 130 insertions(+), 107 deletions(-) diff --git a/homeassistant/components/panasonic_bluray/media_player.py b/homeassistant/components/panasonic_bluray/media_player.py index d87d2a8efc5..20e3bf3b901 100644 --- a/homeassistant/components/panasonic_bluray/media_player.py +++ b/homeassistant/components/panasonic_bluray/media_player.py @@ -100,7 +100,7 @@ class PanasonicBluRay(MediaPlayerEntity): """When was the position of the current playing media valid.""" return self._position_valid - def update(self): + def update(self) -> None: """Update the internal state by querying the device.""" # This can take 5+ seconds to complete state = self._device.get_play_status() @@ -125,7 +125,7 @@ class PanasonicBluRay(MediaPlayerEntity): self._position_valid = utcnow() self._duration = state[2] - def turn_off(self): + def turn_off(self) -> None: """ Instruct the device to turn standby. @@ -139,21 +139,21 @@ class PanasonicBluRay(MediaPlayerEntity): self._state = STATE_OFF - def turn_on(self): + def turn_on(self) -> None: """Wake the device back up from standby.""" if self._state == STATE_OFF: self._device.send_key("POWER") self._state = STATE_IDLE - def media_play(self): + def media_play(self) -> None: """Send play command.""" self._device.send_key("PLAYBACK") - def media_pause(self): + def media_pause(self) -> None: """Send pause command.""" self._device.send_key("PAUSE") - def media_stop(self): + def media_stop(self) -> None: """Send stop command.""" self._device.send_key("STOP") diff --git a/homeassistant/components/panasonic_viera/media_player.py b/homeassistant/components/panasonic_viera/media_player.py index 7b75809f827..4de18e5afe9 100644 --- a/homeassistant/components/panasonic_viera/media_player.py +++ b/homeassistant/components/panasonic_viera/media_player.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from panasonic_viera import Keys @@ -12,6 +13,7 @@ from homeassistant.components.media_player import ( MediaPlayerEntityFeature, ) from homeassistant.components.media_player.browse_media import ( + BrowseMedia, async_process_play_media_url, ) from homeassistant.components.media_player.const import MEDIA_TYPE_URL @@ -111,7 +113,7 @@ class PanasonicVieraTVEntity(MediaPlayerEntity): return self._remote.state @property - def available(self): + def available(self) -> bool: """Return True if the device is available.""" return self._remote.available @@ -125,35 +127,35 @@ class PanasonicVieraTVEntity(MediaPlayerEntity): """Boolean if volume is currently muted.""" return self._remote.muted - async def async_update(self): + async def async_update(self) -> None: """Retrieve the latest data.""" await self._remote.async_update() - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Turn on the media player.""" await self._remote.async_turn_on(context=self._context) - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Turn off media player.""" await self._remote.async_turn_off() - async def async_volume_up(self): + async def async_volume_up(self) -> None: """Volume up the media player.""" await self._remote.async_send_key(Keys.volume_up) - async def async_volume_down(self): + async def async_volume_down(self) -> None: """Volume down media player.""" await self._remote.async_send_key(Keys.volume_down) - async def async_mute_volume(self, mute): + async def async_mute_volume(self, mute: bool) -> None: """Send mute command.""" await self._remote.async_set_mute(mute) - async def async_set_volume_level(self, volume): + async def async_set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" await self._remote.async_set_volume(volume) - async def async_media_play_pause(self): + async def async_media_play_pause(self) -> None: """Simulate play pause media player.""" if self._remote.playing: await self._remote.async_send_key(Keys.pause) @@ -162,29 +164,31 @@ class PanasonicVieraTVEntity(MediaPlayerEntity): await self._remote.async_send_key(Keys.play) self._remote.playing = True - async def async_media_play(self): + async def async_media_play(self) -> None: """Send play command.""" await self._remote.async_send_key(Keys.play) self._remote.playing = True - async def async_media_pause(self): + async def async_media_pause(self) -> None: """Send pause command.""" await self._remote.async_send_key(Keys.pause) self._remote.playing = False - async def async_media_stop(self): + async def async_media_stop(self) -> None: """Stop playback.""" await self._remote.async_send_key(Keys.stop) - async def async_media_next_track(self): + async def async_media_next_track(self) -> None: """Send the fast forward command.""" await self._remote.async_send_key(Keys.fast_forward) - async def async_media_previous_track(self): + async def async_media_previous_track(self) -> None: """Send the rewind command.""" await self._remote.async_send_key(Keys.rewind) - async def async_play_media(self, media_type, media_id, **kwargs): + async def async_play_media( + self, media_type: str, media_id: str, **kwargs: Any + ) -> None: """Play media.""" if media_source.is_media_source_id(media_id): media_type = MEDIA_TYPE_URL @@ -200,6 +204,8 @@ class PanasonicVieraTVEntity(MediaPlayerEntity): media_id = async_process_play_media_url(self.hass, media_id) await self._remote.async_play_media(media_type, media_id) - async def async_browse_media(self, media_content_type=None, media_content_id=None): + async def async_browse_media( + self, media_content_type: str | None = None, media_content_id: str | None = None + ) -> BrowseMedia: """Implement the websocket media browsing helper.""" return await media_source.async_browse_media(self.hass, media_content_id) diff --git a/homeassistant/components/panasonic_viera/remote.py b/homeassistant/components/panasonic_viera/remote.py index bdb44a72a74..717ee090612 100644 --- a/homeassistant/components/panasonic_viera/remote.py +++ b/homeassistant/components/panasonic_viera/remote.py @@ -1,6 +1,9 @@ """Remote control support for Panasonic Viera TV.""" from __future__ import annotations +from collections.abc import Iterable +from typing import Any + from homeassistant.components.remote import RemoteEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, STATE_ON @@ -71,7 +74,7 @@ class PanasonicVieraRemoteEntity(RemoteEntity): return self._name @property - def available(self): + def available(self) -> bool: """Return True if the device is available.""" return self._remote.available @@ -80,15 +83,15 @@ class PanasonicVieraRemoteEntity(RemoteEntity): """Return true if device is on.""" return self._remote.state == STATE_ON - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" await self._remote.async_turn_on(context=self._context) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" await self._remote.async_turn_off() - async def async_send_command(self, command, **kwargs): + async def async_send_command(self, command: Iterable[str], **kwargs: Any) -> None: """Send a command to one device.""" for cmd in command: await self._remote.async_send_key(cmd) diff --git a/homeassistant/components/pandora/media_player.py b/homeassistant/components/pandora/media_player.py index 542d749c573..6c2b9379a3c 100644 --- a/homeassistant/components/pandora/media_player.py +++ b/homeassistant/components/pandora/media_player.py @@ -104,7 +104,7 @@ class PandoraMediaPlayer(MediaPlayerEntity): """Return the state of the player.""" return self._player_state - def turn_on(self): + def turn_on(self) -> None: """Turn the media player on.""" if self._player_state != STATE_OFF: return @@ -134,7 +134,7 @@ class PandoraMediaPlayer(MediaPlayerEntity): self._player_state = STATE_IDLE self.schedule_update_ha_state() - def turn_off(self): + def turn_off(self) -> None: """Turn the media player off.""" if self._pianobar is None: _LOGGER.info("Pianobar subprocess already stopped") @@ -151,19 +151,19 @@ class PandoraMediaPlayer(MediaPlayerEntity): self._player_state = STATE_OFF self.schedule_update_ha_state() - def media_play(self): + def media_play(self) -> None: """Send play command.""" self._send_pianobar_command(SERVICE_MEDIA_PLAY_PAUSE) self._player_state = STATE_PLAYING self.schedule_update_ha_state() - def media_pause(self): + def media_pause(self) -> None: """Send pause command.""" self._send_pianobar_command(SERVICE_MEDIA_PLAY_PAUSE) self._player_state = STATE_PAUSED self.schedule_update_ha_state() - def media_next_track(self): + def media_next_track(self) -> None: """Go to next track.""" self._send_pianobar_command(SERVICE_MEDIA_NEXT_TRACK) self.schedule_update_ha_state() @@ -204,7 +204,7 @@ class PandoraMediaPlayer(MediaPlayerEntity): """Duration of current playing media in seconds.""" return self._media_duration - def select_source(self, source): + def select_source(self, source: str) -> None: """Choose a different Pandora station and play it.""" try: station_index = self._stations.index(source) diff --git a/homeassistant/components/pencom/switch.py b/homeassistant/components/pencom/switch.py index 8f83259ef30..064ac43e6b8 100644 --- a/homeassistant/components/pencom/switch.py +++ b/homeassistant/components/pencom/switch.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from pencompy.pencompy import Pencompy import voluptuous as vol @@ -90,15 +91,15 @@ class PencomRelay(SwitchEntity): """Return a relay's state.""" return self._state - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn a relay on.""" self._hub.set(self._board, self._addr, True) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn a relay off.""" self._hub.set(self._board, self._addr, False) - def update(self): + def update(self) -> None: """Refresh a relay's state.""" self._state = self._hub.get(self._board, self._addr) diff --git a/homeassistant/components/philips_js/media_player.py b/homeassistant/components/philips_js/media_player.py index 8d864436ac5..8ef6fc8d4bd 100644 --- a/homeassistant/components/philips_js/media_player.py +++ b/homeassistant/components/philips_js/media_player.py @@ -1,6 +1,8 @@ """Media Player component to integrate TVs exposing the Joint Space API.""" from __future__ import annotations +from typing import Any + from haphilipsjs import ConnectionFailure from homeassistant.components.media_player import ( @@ -135,7 +137,7 @@ class PhilipsTVMediaPlayer( """List of available input sources.""" return list(self._sources.values()) - async def async_select_source(self, source): + async def async_select_source(self, source: str) -> None: """Set the input source.""" if source_id := _inverted(self._sources).get(source): await self._tv.setSource(source_id) @@ -151,7 +153,7 @@ class PhilipsTVMediaPlayer( """Boolean if volume is currently muted.""" return self._tv.muted - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Turn on the device.""" if self._tv.on and self._tv.powerstate: await self._tv.setPowerState("On") @@ -160,7 +162,7 @@ class PhilipsTVMediaPlayer( await self.coordinator.turn_on.async_run(self.hass, self._context) await self._async_update_soon() - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Turn off the device.""" if self._state == STATE_ON: await self._tv.sendKey("Standby") @@ -169,17 +171,17 @@ class PhilipsTVMediaPlayer( else: _LOGGER.debug("Ignoring turn off when already in expected state") - async def async_volume_up(self): + async def async_volume_up(self) -> None: """Send volume up command.""" await self._tv.sendKey("VolumeUp") await self._async_update_soon() - async def async_volume_down(self): + async def async_volume_down(self) -> None: """Send volume down command.""" await self._tv.sendKey("VolumeDown") await self._async_update_soon() - async def async_mute_volume(self, mute): + async def async_mute_volume(self, mute: bool) -> None: """Send mute command.""" if self._tv.muted != mute: await self._tv.sendKey("Mute") @@ -187,22 +189,22 @@ class PhilipsTVMediaPlayer( else: _LOGGER.debug("Ignoring request when already in expected state") - async def async_set_volume_level(self, volume): + async def async_set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" await self._tv.setVolume(volume, self._tv.muted) await self._async_update_soon() - async def async_media_previous_track(self): + async def async_media_previous_track(self) -> None: """Send rewind command.""" await self._tv.sendKey("Previous") await self._async_update_soon() - async def async_media_next_track(self): + async def async_media_next_track(self) -> None: """Send fast forward command.""" await self._tv.sendKey("Next") await self._async_update_soon() - async def async_media_play_pause(self): + async def async_media_play_pause(self) -> None: """Send pause command to media player.""" if self._tv.quirk_playpause_spacebar: await self._tv.sendUnicode(" ") @@ -210,17 +212,17 @@ class PhilipsTVMediaPlayer( await self._tv.sendKey("PlayPause") await self._async_update_soon() - async def async_media_play(self): + async def async_media_play(self) -> None: """Send pause command to media player.""" await self._tv.sendKey("Play") await self._async_update_soon() - async def async_media_pause(self): + async def async_media_pause(self) -> None: """Send play command to media player.""" await self._tv.sendKey("Pause") await self._async_update_soon() - async def async_media_stop(self): + async def async_media_stop(self) -> None: """Send play command to media player.""" await self._tv.sendKey("Stop") await self._async_update_soon() @@ -268,7 +270,9 @@ class PhilipsTVMediaPlayer( if app := self._tv.applications.get(self._tv.application_id): return app.get("label") - async def async_play_media(self, media_type, media_id, **kwargs): + async def async_play_media( + self, media_type: str, media_id: str, **kwargs: Any + ) -> None: """Play a piece of media.""" _LOGGER.debug("Call play media type <%s>, Id <%s>", media_type, media_id) @@ -460,7 +464,7 @@ class PhilipsTVMediaPlayer( _LOGGER.warning("Failed to fetch image") return None, None - async def async_get_media_image(self): + async def async_get_media_image(self) -> tuple[bytes | None, str | None]: """Serve album art. Returns (content, content_type).""" return await self.async_get_browse_image( self.media_content_type, self.media_content_id, None diff --git a/homeassistant/components/philips_js/remote.py b/homeassistant/components/philips_js/remote.py index 7e8c5448cca..02d5e512a33 100644 --- a/homeassistant/components/philips_js/remote.py +++ b/homeassistant/components/philips_js/remote.py @@ -1,5 +1,7 @@ """Remote control support for Apple TV.""" import asyncio +from collections.abc import Iterable +from typing import Any from homeassistant.components.remote import ( ATTR_DELAY_SECS, @@ -58,7 +60,7 @@ class PhilipsTVRemote(CoordinatorEntity[PhilipsTVDataUpdateCoordinator], RemoteE self._tv.on and (self._tv.powerstate == "On" or self._tv.powerstate is None) ) - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" if self._tv.on and self._tv.powerstate: await self._tv.setPowerState("On") @@ -66,7 +68,7 @@ class PhilipsTVRemote(CoordinatorEntity[PhilipsTVDataUpdateCoordinator], RemoteE await self.coordinator.turn_on.async_run(self.hass, self._context) self.async_write_ha_state() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" if self._tv.on: await self._tv.sendKey("Standby") @@ -74,7 +76,7 @@ class PhilipsTVRemote(CoordinatorEntity[PhilipsTVDataUpdateCoordinator], RemoteE else: LOGGER.debug("Tv was already turned off") - async def async_send_command(self, command, **kwargs): + async def async_send_command(self, command: Iterable[str], **kwargs: Any) -> None: """Send a command to one device.""" num_repeats = kwargs[ATTR_NUM_REPEATS] delay = kwargs.get(ATTR_DELAY_SECS, DEFAULT_DELAY_SECS) diff --git a/homeassistant/components/ping/binary_sensor.py b/homeassistant/components/ping/binary_sensor.py index ff88412a6ef..cbb511a4ae3 100644 --- a/homeassistant/components/ping/binary_sensor.py +++ b/homeassistant/components/ping/binary_sensor.py @@ -131,7 +131,7 @@ class PingBinarySensor(RestoreEntity, BinarySensorEntity): await self._ping.async_update() self._available = True - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Restore previous state on restart to avoid blocking startup.""" await super().async_added_to_hass() diff --git a/homeassistant/components/pioneer/media_player.py b/homeassistant/components/pioneer/media_player.py index 7e3931b1ae3..1836d433cf7 100644 --- a/homeassistant/components/pioneer/media_player.py +++ b/homeassistant/components/pioneer/media_player.py @@ -210,31 +210,31 @@ class PioneerDevice(MediaPlayerEntity): """Title of current playing media.""" return self._selected_source - def turn_off(self): + def turn_off(self) -> None: """Turn off media player.""" self.telnet_command("PF") - def volume_up(self): + def volume_up(self) -> None: """Volume up media player.""" self.telnet_command("VU") - def volume_down(self): + def volume_down(self) -> None: """Volume down media player.""" self.telnet_command("VD") - def set_volume_level(self, volume): + def set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" # 60dB max self.telnet_command(f"{round(volume * MAX_VOLUME):03}VL") - def mute_volume(self, mute): + def mute_volume(self, mute: bool) -> None: """Mute (true) or unmute (false) media player.""" self.telnet_command("MO" if mute else "MF") - def turn_on(self): + def turn_on(self) -> None: """Turn the media player on.""" self.telnet_command("PO") - def select_source(self, source): + def select_source(self, source: str) -> None: """Select input source.""" self.telnet_command(f"{self._source_name_to_number.get(source)}FN") diff --git a/homeassistant/components/pjlink/media_player.py b/homeassistant/components/pjlink/media_player.py index 73fb1888341..1f5641486ef 100644 --- a/homeassistant/components/pjlink/media_player.py +++ b/homeassistant/components/pjlink/media_player.py @@ -107,7 +107,7 @@ class PjLinkDevice(MediaPlayerEntity): projector.authenticate(self._password) return projector - def update(self): + def update(self) -> None: """Get the latest state from the device.""" with self.projector() as projector: @@ -161,22 +161,22 @@ class PjLinkDevice(MediaPlayerEntity): """Return all available input sources.""" return self._source_list - def turn_off(self): + def turn_off(self) -> None: """Turn projector off.""" with self.projector() as projector: projector.set_power("off") - def turn_on(self): + def turn_on(self) -> None: """Turn projector on.""" with self.projector() as projector: projector.set_power("on") - def mute_volume(self, mute): + def mute_volume(self, mute: bool) -> None: """Mute (true) of unmute (false) media player.""" with self.projector() as projector: projector.set_mute(MUTE_AUDIO, mute) - def select_source(self, source): + def select_source(self, source: str) -> None: """Set the input source.""" source = self._source_name_mapping[source] with self.projector() as projector: diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index 7d21fff3afe..9cab1d25fc2 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Callable from functools import wraps import logging -from typing import TypeVar +from typing import Any, TypeVar import plexapi.exceptions import requests.exceptions @@ -15,6 +15,7 @@ from homeassistant.components.media_player import ( MediaPlayerEntity, MediaPlayerEntityFeature, ) +from homeassistant.components.media_player.browse_media import BrowseMedia from homeassistant.components.media_player.const import MEDIA_TYPE_MUSIC from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_IDLE, STATE_PAUSED, STATE_PLAYING @@ -147,7 +148,7 @@ class PlexMediaPlayer(MediaPlayerEntity): # Initializes other attributes self.session = session - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Run when about to be added to hass.""" _LOGGER.debug("Added %s [%s]", self.entity_id, self.unique_id) self.async_on_remove( @@ -408,7 +409,7 @@ class PlexMediaPlayer(MediaPlayerEntity): MediaPlayerEntityFeature.BROWSE_MEDIA | MediaPlayerEntityFeature.PLAY_MEDIA ) - def set_volume_level(self, volume): + def set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" if self.device and "playback" in self._device_protocol_capabilities: self.device.setVolume(int(volume * 100), self._active_media_plexapi_type) @@ -432,7 +433,7 @@ class PlexMediaPlayer(MediaPlayerEntity): return self._volume_muted return None - def mute_volume(self, mute): + def mute_volume(self, mute: bool) -> None: """Mute the volume. Since we can't actually mute, we'll: @@ -449,37 +450,37 @@ class PlexMediaPlayer(MediaPlayerEntity): else: self.set_volume_level(self._previous_volume_level) - def media_play(self): + def media_play(self) -> None: """Send play command.""" if self.device and "playback" in self._device_protocol_capabilities: self.device.play(self._active_media_plexapi_type) - def media_pause(self): + def media_pause(self) -> None: """Send pause command.""" if self.device and "playback" in self._device_protocol_capabilities: self.device.pause(self._active_media_plexapi_type) - def media_stop(self): + def media_stop(self) -> None: """Send stop command.""" if self.device and "playback" in self._device_protocol_capabilities: self.device.stop(self._active_media_plexapi_type) - def media_seek(self, position): + def media_seek(self, position: float) -> None: """Send the seek command.""" if self.device and "playback" in self._device_protocol_capabilities: self.device.seekTo(position * 1000, self._active_media_plexapi_type) - def media_next_track(self): + def media_next_track(self) -> None: """Send next track command.""" if self.device and "playback" in self._device_protocol_capabilities: self.device.skipNext(self._active_media_plexapi_type) - def media_previous_track(self): + def media_previous_track(self) -> None: """Send previous track command.""" if self.device and "playback" in self._device_protocol_capabilities: self.device.skipPrevious(self._active_media_plexapi_type) - def play_media(self, media_type, media_id, **kwargs): + def play_media(self, media_type: str, media_id: str, **kwargs: Any) -> None: """Play a piece of media.""" if not (self.device and "playback" in self._device_protocol_capabilities): raise HomeAssistantError( @@ -538,7 +539,9 @@ class PlexMediaPlayer(MediaPlayerEntity): via_device=(PLEX_DOMAIN, self.plex_server.machine_identifier), ) - async def async_browse_media(self, media_content_type=None, media_content_id=None): + async def async_browse_media( + self, media_content_type: str | None = None, media_content_id: str | None = None + ) -> BrowseMedia: """Implement the websocket media browsing helper.""" is_internal = is_internal_request(self.hass) return await self.hass.async_add_executor_job( diff --git a/homeassistant/components/plex/sensor.py b/homeassistant/components/plex/sensor.py index 20e4b4ffb51..7bd57ae9711 100644 --- a/homeassistant/components/plex/sensor.py +++ b/homeassistant/components/plex/sensor.py @@ -89,7 +89,7 @@ class PlexSensor(SensorEntity): function=self._async_refresh_sensor, ).async_call - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Run when about to be added to hass.""" server_id = self._server.machine_identifier self.async_on_remove( @@ -100,7 +100,7 @@ class PlexSensor(SensorEntity): ) ) - async def _async_refresh_sensor(self): + async def _async_refresh_sensor(self) -> None: """Set instance object and trigger an entity state update.""" _LOGGER.debug("Refreshing sensor [%s]", self.unique_id) self._attr_native_value = len(self._server.sensor_attributes) @@ -147,7 +147,7 @@ class PlexLibrarySectionSensor(SensorEntity): self._attr_unique_id = f"library-{self.server_id}-{plex_library_section.uuid}" self._attr_native_unit_of_measurement = "Items" - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Run when about to be added to hass.""" self.async_on_remove( async_dispatcher_connect( @@ -158,7 +158,7 @@ class PlexLibrarySectionSensor(SensorEntity): ) await self.async_refresh_sensor() - async def async_refresh_sensor(self): + async def async_refresh_sensor(self) -> None: """Update state and attributes for the library sensor.""" _LOGGER.debug("Refreshing library sensor for '%s'", self.name) try: diff --git a/homeassistant/components/pocketcasts/sensor.py b/homeassistant/components/pocketcasts/sensor.py index 9769ec47f3b..3962ae4c060 100644 --- a/homeassistant/components/pocketcasts/sensor.py +++ b/homeassistant/components/pocketcasts/sensor.py @@ -68,7 +68,7 @@ class PocketCastsSensor(SensorEntity): """Return the icon for the sensor.""" return ICON - def update(self): + def update(self) -> None: """Update sensor values.""" try: self._state = len(self._api.new_releases) diff --git a/homeassistant/components/point/binary_sensor.py b/homeassistant/components/point/binary_sensor.py index a2d2efa9089..e284f2dbe7f 100644 --- a/homeassistant/components/point/binary_sensor.py +++ b/homeassistant/components/point/binary_sensor.py @@ -77,14 +77,14 @@ class MinutPointBinarySensor(MinutPointEntity, BinarySensorEntity): self._async_unsub_hook_dispatcher_connect = None self._events = EVENTS[device_name] - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Call when entity is added to HOme Assistant.""" await super().async_added_to_hass() self._async_unsub_hook_dispatcher_connect = async_dispatcher_connect( self.hass, SIGNAL_WEBHOOK, self._webhook_event ) - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Disconnect dispatcher listener when removed.""" await super().async_will_remove_from_hass() if self._async_unsub_hook_dispatcher_connect: diff --git a/homeassistant/components/progettihwsw/switch.py b/homeassistant/components/progettihwsw/switch.py index 529e73f13dd..0aab943e385 100644 --- a/homeassistant/components/progettihwsw/switch.py +++ b/homeassistant/components/progettihwsw/switch.py @@ -1,6 +1,7 @@ """Control switches.""" from datetime import timedelta import logging +from typing import Any from ProgettiHWSW.relay import Relay import async_timeout @@ -65,17 +66,17 @@ class ProgettihwswSwitch(CoordinatorEntity, SwitchEntity): self._switch = switch self._name = name - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" await self._switch.control(True) await self.coordinator.async_request_refresh() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" await self._switch.control(False) await self.coordinator.async_request_refresh() - async def async_toggle(self, **kwargs): + async def async_toggle(self, **kwargs: Any) -> None: """Toggle the state of switch.""" await self._switch.toggle() await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/proliphix/climate.py b/homeassistant/components/proliphix/climate.py index 1952a6f186a..e58ddda0605 100644 --- a/homeassistant/components/proliphix/climate.py +++ b/homeassistant/components/proliphix/climate.py @@ -1,6 +1,8 @@ """Support for Proliphix NT10e Thermostats.""" from __future__ import annotations +from typing import Any + import proliphix import voluptuous as vol @@ -63,7 +65,7 @@ class ProliphixThermostat(ClimateEntity): self._pdp = pdp self._name = None - def update(self): + def update(self) -> None: """Update the data from the thermostat.""" self._pdp.update() self._name = self._pdp.name @@ -114,7 +116,7 @@ class ProliphixThermostat(ClimateEntity): """Return available HVAC modes.""" return [] - def set_temperature(self, **kwargs): + def set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: return diff --git a/homeassistant/components/proxmoxve/binary_sensor.py b/homeassistant/components/proxmoxve/binary_sensor.py index 97995431778..fdc10eda2dd 100644 --- a/homeassistant/components/proxmoxve/binary_sensor.py +++ b/homeassistant/components/proxmoxve/binary_sensor.py @@ -93,7 +93,7 @@ class ProxmoxBinarySensor(ProxmoxEntity, BinarySensorEntity): return data["status"] == "running" @property - def available(self): + def available(self) -> bool: """Return sensor availability.""" return super().available and self.coordinator.data is not None diff --git a/homeassistant/components/ps4/media_player.py b/homeassistant/components/ps4/media_player.py index 1a1c93e210a..29bd0732029 100644 --- a/homeassistant/components/ps4/media_player.py +++ b/homeassistant/components/ps4/media_player.py @@ -130,12 +130,12 @@ class PS4Device(MediaPlayerEntity): self._region, ) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Subscribe PS4 events.""" self.hass.data[PS4_DATA].devices.append(self) self.check_region() - async def async_update(self): + async def async_update(self) -> None: """Retrieve the latest data.""" if self._ps4.ddp_protocol is not None: # Request Status with asyncio transport. @@ -365,7 +365,7 @@ class PS4Device(MediaPlayerEntity): self._unique_id = format_unique_id(self._creds, status["host-id"]) - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Remove Entity from Home Assistant.""" # Close TCP Transport. if self._ps4.connected: @@ -439,27 +439,27 @@ class PS4Device(MediaPlayerEntity): """List of available input sources.""" return self._source_list - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Turn off media player.""" await self._ps4.standby() - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Turn on the media player.""" self._ps4.wakeup() - async def async_toggle(self): + async def async_toggle(self) -> None: """Toggle media player.""" await self._ps4.toggle() - async def async_media_pause(self): + async def async_media_pause(self) -> None: """Send keypress ps to return to menu.""" await self.async_send_remote_control("ps") - async def async_media_stop(self): + async def async_media_stop(self) -> None: """Send keypress ps to return to menu.""" await self.async_send_remote_control("ps") - async def async_select_source(self, source): + async def async_select_source(self, source: str) -> None: """Select input source.""" for title_id, data in self._games.items(): game = data[ATTR_MEDIA_TITLE] diff --git a/homeassistant/components/pulseaudio_loopback/switch.py b/homeassistant/components/pulseaudio_loopback/switch.py index df28b0ff38c..92dfb235b1b 100644 --- a/homeassistant/components/pulseaudio_loopback/switch.py +++ b/homeassistant/components/pulseaudio_loopback/switch.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from pulsectl import Pulse, PulseError import voluptuous as vol @@ -100,7 +101,7 @@ class PALoopbackSwitch(SwitchEntity): return None @property - def available(self): + def available(self) -> bool: """Return true when connected to server.""" return self._pa_svr.connected @@ -114,7 +115,7 @@ class PALoopbackSwitch(SwitchEntity): """Return true if device is on.""" return self._module_idx is not None - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" if not self.is_on: self._pa_svr.module_load( @@ -124,13 +125,13 @@ class PALoopbackSwitch(SwitchEntity): else: _LOGGER.warning(IGNORED_SWITCH_WARN) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" if self.is_on: self._pa_svr.module_unload(self._module_idx) else: _LOGGER.warning(IGNORED_SWITCH_WARN) - def update(self): + def update(self) -> None: """Refresh state in case an alternate process modified this data.""" self._module_idx = self._get_module_idx() diff --git a/homeassistant/components/push/camera.py b/homeassistant/components/push/camera.py index f70ebd3a3a9..36c59732ee2 100644 --- a/homeassistant/components/push/camera.py +++ b/homeassistant/components/push/camera.py @@ -109,7 +109,7 @@ class PushCamera(Camera): self.webhook_id = webhook_id self.webhook_url = webhook.async_generate_url(hass, webhook_id) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Call when entity is added to hass.""" self.hass.data[PUSH_CAMERA_DATA][self.webhook_id] = self diff --git a/homeassistant/components/pushbullet/sensor.py b/homeassistant/components/pushbullet/sensor.py index a3d9b549e95..51a18f1aaea 100644 --- a/homeassistant/components/pushbullet/sensor.py +++ b/homeassistant/components/pushbullet/sensor.py @@ -114,7 +114,7 @@ class PushBulletNotificationSensor(SensorEntity): self._attr_name = f"Pushbullet {description.key}" - def update(self): + def update(self) -> None: """Fetch the latest data from the sensor. This will fetch the 'sensor reading' into self._state but also all diff --git a/homeassistant/components/pyload/sensor.py b/homeassistant/components/pyload/sensor.py index 81e1a02408a..be014a2a405 100644 --- a/homeassistant/components/pyload/sensor.py +++ b/homeassistant/components/pyload/sensor.py @@ -111,7 +111,7 @@ class PyLoadSensor(SensorEntity): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement - def update(self): + def update(self) -> None: """Update state of sensor.""" try: self.api.update() From 72f9e5f6ecf2ce9f5aa5427dab2cf9151b685dbe Mon Sep 17 00:00:00 2001 From: Artem Draft Date: Tue, 6 Sep 2022 10:52:27 +0300 Subject: [PATCH 125/955] Bump pybravia to 0.2.2 (#77867) --- homeassistant/components/braviatv/coordinator.py | 12 +++++++++++- homeassistant/components/braviatv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/braviatv/coordinator.py b/homeassistant/components/braviatv/coordinator.py index bdacddcdb2f..49c902e0d44 100644 --- a/homeassistant/components/braviatv/coordinator.py +++ b/homeassistant/components/braviatv/coordinator.py @@ -7,7 +7,13 @@ from functools import wraps import logging from typing import Any, Final, TypeVar -from pybravia import BraviaTV, BraviaTVError, BraviaTVNotFound +from pybravia import ( + BraviaTV, + BraviaTVConnectionError, + BraviaTVConnectionTimeout, + BraviaTVError, + BraviaTVNotFound, +) from typing_extensions import Concatenate, ParamSpec from homeassistant.components.media_player.const import ( @@ -130,6 +136,10 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): _LOGGER.debug("Update skipped, Bravia API service is reloading") return raise UpdateFailed("Error communicating with device") from err + except (BraviaTVConnectionError, BraviaTVConnectionTimeout): + self.is_on = False + self.connected = False + _LOGGER.debug("Update skipped, Bravia TV is off") except BraviaTVError as err: self.is_on = False self.connected = False diff --git a/homeassistant/components/braviatv/manifest.json b/homeassistant/components/braviatv/manifest.json index fa172957781..dca9d65cff0 100644 --- a/homeassistant/components/braviatv/manifest.json +++ b/homeassistant/components/braviatv/manifest.json @@ -2,7 +2,7 @@ "domain": "braviatv", "name": "Sony Bravia TV", "documentation": "https://www.home-assistant.io/integrations/braviatv", - "requirements": ["pybravia==0.2.1"], + "requirements": ["pybravia==0.2.2"], "codeowners": ["@bieniu", "@Drafteed"], "config_flow": true, "iot_class": "local_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 1c458d114c4..cebc6dde48b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1443,7 +1443,7 @@ pyblackbird==0.5 pybotvac==0.0.23 # homeassistant.components.braviatv -pybravia==0.2.1 +pybravia==0.2.2 # homeassistant.components.nissan_leaf pycarwings2==2.13 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index edf5f262844..1fc0f5cd9a3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1019,7 +1019,7 @@ pyblackbird==0.5 pybotvac==0.0.23 # homeassistant.components.braviatv -pybravia==0.2.1 +pybravia==0.2.2 # homeassistant.components.cloudflare pycfdns==1.2.2 From 0e369d5b2e41fa3641ef11bfb9fc21227799d37f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 6 Sep 2022 02:54:52 -0500 Subject: [PATCH 126/955] Add RSSI to the bluetooth debug log (#77860) --- homeassistant/components/bluetooth/manager.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index 9fc00aa159b..80817deb2a1 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -327,12 +327,13 @@ class BluetoothManager: matched_domains = self._integration_matcher.match_domains(service_info) _LOGGER.debug( - "%s: %s %s connectable: %s match: %s", + "%s: %s %s connectable: %s match: %s rssi: %s", source, address, advertisement_data, connectable, matched_domains, + device.rssi, ) for match in self._callback_index.match_callbacks(service_info): From ac8a12f99cf1255c8bb118fcc7e2d4ec0275bbb6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 6 Sep 2022 02:55:43 -0500 Subject: [PATCH 127/955] Bump thermopro-ble to 0.4.3 (#77863) * Bump thermopro-ble to 0.4.2 - Turns on rounding of long values - Uses bluetooth-data-tools under the hood - Adds the TP393 since it works without any changes to the parser Changelog: https://github.com/Bluetooth-Devices/thermopro-ble/compare/v0.4.0...v0.4.2 * bump again for device detection fix --- homeassistant/components/thermopro/manifest.json | 7 +++++-- homeassistant/generated/bluetooth.py | 5 +++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/thermopro/manifest.json b/homeassistant/components/thermopro/manifest.json index 912a070ccf1..dca643a28cf 100644 --- a/homeassistant/components/thermopro/manifest.json +++ b/homeassistant/components/thermopro/manifest.json @@ -3,9 +3,12 @@ "name": "ThermoPro", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/thermopro", - "bluetooth": [{ "local_name": "TP35*", "connectable": false }], + "bluetooth": [ + { "local_name": "TP35*", "connectable": false }, + { "local_name": "TP39*", "connectable": false } + ], "dependencies": ["bluetooth"], - "requirements": ["thermopro-ble==0.4.0"], + "requirements": ["thermopro-ble==0.4.3"], "codeowners": ["@bdraco"], "iot_class": "local_push" } diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index d7230213302..b2400f733da 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -269,6 +269,11 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ "local_name": "TP35*", "connectable": False }, + { + "domain": "thermopro", + "local_name": "TP39*", + "connectable": False + }, { "domain": "xiaomi_ble", "connectable": False, diff --git a/requirements_all.txt b/requirements_all.txt index cebc6dde48b..dafc3f798a0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2369,7 +2369,7 @@ tesla-wall-connector==1.0.2 thermobeacon-ble==0.3.1 # homeassistant.components.thermopro -thermopro-ble==0.4.0 +thermopro-ble==0.4.3 # homeassistant.components.thermoworks_smoke thermoworks_smoke==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1fc0f5cd9a3..399fa7427c9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1618,7 +1618,7 @@ tesla-wall-connector==1.0.2 thermobeacon-ble==0.3.1 # homeassistant.components.thermopro -thermopro-ble==0.4.0 +thermopro-ble==0.4.3 # homeassistant.components.todoist todoist-python==8.0.0 From b4669d89395f59682578429ccc70fbb9a8a30de5 Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Tue, 6 Sep 2022 10:10:46 +0200 Subject: [PATCH 128/955] Add has_entity_name for kraken (#77841) Add has_entity_name --- homeassistant/components/kraken/const.py | 16 ++++++++++++++++ homeassistant/components/kraken/sensor.py | 11 +++++------ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/kraken/const.py b/homeassistant/components/kraken/const.py index ae626349cb0..816fb35fadb 100644 --- a/homeassistant/components/kraken/const.py +++ b/homeassistant/components/kraken/const.py @@ -50,77 +50,93 @@ class KrakenSensorEntityDescription(SensorEntityDescription, KrakenRequiredKeysM SENSOR_TYPES: tuple[KrakenSensorEntityDescription, ...] = ( KrakenSensorEntityDescription( key="ask", + name="Ask", value_fn=lambda x, y: x.data[y]["ask"][0], ), KrakenSensorEntityDescription( key="ask_volume", + name="Ask Volume", value_fn=lambda x, y: x.data[y]["ask"][1], entity_registry_enabled_default=False, ), KrakenSensorEntityDescription( key="bid", + name="Bid", value_fn=lambda x, y: x.data[y]["bid"][0], ), KrakenSensorEntityDescription( key="bid_volume", + name="Bid Volume", value_fn=lambda x, y: x.data[y]["bid"][1], entity_registry_enabled_default=False, ), KrakenSensorEntityDescription( key="volume_today", + name="Volume Today", value_fn=lambda x, y: x.data[y]["volume"][0], entity_registry_enabled_default=False, ), KrakenSensorEntityDescription( key="volume_last_24h", + name="Volume last 24h", value_fn=lambda x, y: x.data[y]["volume"][1], entity_registry_enabled_default=False, ), KrakenSensorEntityDescription( key="volume_weighted_average_today", + name="Volume weighted average today", value_fn=lambda x, y: x.data[y]["volume_weighted_average"][0], entity_registry_enabled_default=False, ), KrakenSensorEntityDescription( key="volume_weighted_average_last_24h", + name="Volume weighted average last 24h", value_fn=lambda x, y: x.data[y]["volume_weighted_average"][1], entity_registry_enabled_default=False, ), KrakenSensorEntityDescription( key="number_of_trades_today", + name="Number of trades today", value_fn=lambda x, y: x.data[y]["number_of_trades"][0], entity_registry_enabled_default=False, ), KrakenSensorEntityDescription( key="number_of_trades_last_24h", + name="Number of trades last 24h", value_fn=lambda x, y: x.data[y]["number_of_trades"][1], entity_registry_enabled_default=False, ), KrakenSensorEntityDescription( key="last_trade_closed", + name="Last trade closed", value_fn=lambda x, y: x.data[y]["last_trade_closed"][0], entity_registry_enabled_default=False, ), KrakenSensorEntityDescription( key="low_today", + name="Low today", value_fn=lambda x, y: x.data[y]["low"][0], ), KrakenSensorEntityDescription( key="low_last_24h", + name="Low last 24h", value_fn=lambda x, y: x.data[y]["low"][1], entity_registry_enabled_default=False, ), KrakenSensorEntityDescription( key="high_today", + name="High today", value_fn=lambda x, y: x.data[y]["high"][0], ), KrakenSensorEntityDescription( key="high_last_24h", + name="High last 24h", value_fn=lambda x, y: x.data[y]["high"][1], entity_registry_enabled_default=False, ), KrakenSensorEntityDescription( key="opening_price_today", + name="Opening price today", value_fn=lambda x, y: x.data[y]["opening_price"], entity_registry_enabled_default=False, ), diff --git a/homeassistant/components/kraken/sensor.py b/homeassistant/components/kraken/sensor.py index f98a48d8dc3..d37ecfea889 100644 --- a/homeassistant/components/kraken/sensor.py +++ b/homeassistant/components/kraken/sensor.py @@ -109,19 +109,17 @@ class KrakenSensor( self.tracked_asset_pair_wsname = kraken_data.tradable_asset_pairs[ tracked_asset_pair ] - source_asset = tracked_asset_pair.split("/")[0] self._target_asset = tracked_asset_pair.split("/")[1] if "number_of" not in description.key: self._attr_native_unit_of_measurement = self._target_asset - self._device_name = f"{source_asset} {self._target_asset}" - self._attr_name = "_".join( + self._device_name = create_device_name(tracked_asset_pair) + self._attr_unique_id = "_".join( [ tracked_asset_pair.split("/")[0], tracked_asset_pair.split("/")[1], description.key, ] - ) - self._attr_unique_id = self._attr_name.lower() + ).lower() self._received_data_at_least_once = False self._available = True self._attr_state_class = SensorStateClass.MEASUREMENT @@ -129,10 +127,11 @@ class KrakenSensor( self._attr_device_info = DeviceInfo( configuration_url="https://www.kraken.com/", entry_type=device_registry.DeviceEntryType.SERVICE, - identifiers={(DOMAIN, f"{source_asset}_{self._target_asset}")}, + identifiers={(DOMAIN, "_".join(self._device_name.split(" ")))}, manufacturer="Kraken.com", name=self._device_name, ) + self._attr_has_entity_name = True async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" From 36f3028ec3350d5537f3813521f651639969f6ca Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 6 Sep 2022 10:12:16 +0200 Subject: [PATCH 129/955] Improve type hint in onvif (#77833) --- homeassistant/components/onvif/camera.py | 4 ++-- homeassistant/components/onvif/device.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index 528b9605bbc..758631e6e29 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -109,7 +109,7 @@ class ONVIFCameraEntity(ONVIFBaseEntity, Camera): device.config_entry.data.get(CONF_SNAPSHOT_AUTH) == HTTP_BASIC_AUTHENTICATION ) - self._stream_uri = None + self._stream_uri: str | None = None @property def name(self) -> str: @@ -185,7 +185,7 @@ class ONVIFCameraEntity(ONVIFBaseEntity, Camera): finally: await stream.close() - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Run when entity about to be added to hass.""" uri_no_auth = await self.device.async_get_stream_uri(self.profile) url = URL(uri_no_auth) diff --git a/homeassistant/components/onvif/device.py b/homeassistant/components/onvif/device.py index dda28e07a2a..d7f50f6744e 100644 --- a/homeassistant/components/onvif/device.py +++ b/homeassistant/components/onvif/device.py @@ -74,12 +74,12 @@ class ONVIFDevice: return self.config_entry.data[CONF_PORT] @property - def username(self) -> int: + def username(self) -> str: """Return the username of this device.""" return self.config_entry.data[CONF_USERNAME] @property - def password(self) -> int: + def password(self) -> str: """Return the password of this device.""" return self.config_entry.data[CONF_PASSWORD] From d6ca3544eecc35427b3fb18e9bbe775e91b648ac Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 6 Sep 2022 10:21:43 +0200 Subject: [PATCH 130/955] Improve type hint in opensky (#77829) --- homeassistant/components/opensky/sensor.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/opensky/sensor.py b/homeassistant/components/opensky/sensor.py index b4278bcce36..1b480e997d8 100644 --- a/homeassistant/components/opensky/sensor.py +++ b/homeassistant/components/opensky/sensor.py @@ -147,7 +147,7 @@ class OpenSkySensor(SensorEntity): } self._hass.bus.fire(event, data) - def update(self): + def update(self) -> None: """Update device state.""" currently_tracked = set() flight_metadata = {} @@ -159,18 +159,17 @@ class OpenSkySensor(SensorEntity): flight_metadata[callsign] = flight else: continue - missing_location = ( - flight.get(ATTR_LONGITUDE) is None or flight.get(ATTR_LATITUDE) is None - ) - if missing_location: - continue - if flight.get(ATTR_ON_GROUND): + if ( + (longitude := flight.get(ATTR_LONGITUDE)) is None + or (latitude := flight.get(ATTR_LATITUDE)) is None + or flight.get(ATTR_ON_GROUND) + ): continue distance = util_location.distance( self._latitude, self._longitude, - flight.get(ATTR_LATITUDE), - flight.get(ATTR_LONGITUDE), + latitude, + longitude, ) if distance is None or distance > self._radius: continue From ea71a462d6a31db93a3cfba1352c9b6ea7333efc Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 6 Sep 2022 10:25:35 +0200 Subject: [PATCH 131/955] Improve entity type hints [o] (#77826) --- .../components/oasa_telematics/sensor.py | 2 +- homeassistant/components/obihai/sensor.py | 2 +- homeassistant/components/oem/climate.py | 6 ++-- homeassistant/components/ohmconnect/sensor.py | 2 +- homeassistant/components/ombi/sensor.py | 2 +- .../components/onkyo/media_player.py | 35 ++++++++++--------- .../components/onvif/binary_sensor.py | 2 +- homeassistant/components/onvif/sensor.py | 2 +- homeassistant/components/openerz/sensor.py | 2 +- homeassistant/components/openevse/sensor.py | 2 +- .../components/openhardwaremonitor/sensor.py | 2 +- .../components/openhome/media_player.py | 35 +++++++++++-------- .../components/opentherm_gw/binary_sensor.py | 4 +-- .../components/opentherm_gw/climate.py | 9 ++--- .../components/opentherm_gw/sensor.py | 4 +-- homeassistant/components/opple/light.py | 2 +- homeassistant/components/oru/sensor.py | 2 +- homeassistant/components/orvibo/switch.py | 7 ++-- homeassistant/components/otp/sensor.py | 2 +- .../components/owntracks/device_tracker.py | 2 +- 20 files changed, 68 insertions(+), 58 deletions(-) diff --git a/homeassistant/components/oasa_telematics/sensor.py b/homeassistant/components/oasa_telematics/sensor.py index 47bdd24d192..3cb624190e7 100644 --- a/homeassistant/components/oasa_telematics/sensor.py +++ b/homeassistant/components/oasa_telematics/sensor.py @@ -127,7 +127,7 @@ class OASATelematicsSensor(SensorEntity): """Icon to use in the frontend, if any.""" return ICON - def update(self): + def update(self) -> None: """Get the latest data from OASA API and update the states.""" self.data.update() self._times = self.data.info diff --git a/homeassistant/components/obihai/sensor.py b/homeassistant/components/obihai/sensor.py index 1b1dc339af1..cff4e6232e7 100644 --- a/homeassistant/components/obihai/sensor.py +++ b/homeassistant/components/obihai/sensor.py @@ -146,7 +146,7 @@ class ObihaiServiceSensors(SensorEntity): return "mdi:restart-alert" return "mdi:phone" - def update(self): + def update(self) -> None: """Update the sensor.""" services = self._pyobihai.get_state() diff --git a/homeassistant/components/oem/climate.py b/homeassistant/components/oem/climate.py index 298ae965e17..3f0110699eb 100644 --- a/homeassistant/components/oem/climate.py +++ b/homeassistant/components/oem/climate.py @@ -1,6 +1,8 @@ """OpenEnergyMonitor Thermostat Support.""" from __future__ import annotations +from typing import Any + from oemthermostat import Thermostat import requests import voluptuous as vol @@ -122,12 +124,12 @@ class ThermostatDevice(ClimateEntity): elif hvac_mode == HVACMode.OFF: self.thermostat.mode = 0 - def set_temperature(self, **kwargs): + def set_temperature(self, **kwargs: Any) -> None: """Set the temperature.""" temp = kwargs.get(ATTR_TEMPERATURE) self.thermostat.setpoint = temp - def update(self): + def update(self) -> None: """Update local state.""" self._setpoint = self.thermostat.setpoint self._temperature = self.thermostat.temperature diff --git a/homeassistant/components/ohmconnect/sensor.py b/homeassistant/components/ohmconnect/sensor.py index 09105984a31..3d9dbd78419 100644 --- a/homeassistant/components/ohmconnect/sensor.py +++ b/homeassistant/components/ohmconnect/sensor.py @@ -70,7 +70,7 @@ class OhmconnectSensor(SensorEntity): return {"Address": self._data.get("address"), "ID": self._ohmid} @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): + def update(self) -> None: """Get the latest data from OhmConnect.""" try: url = f"https://login.ohmconnect.com/verify-ohm-hour/{self._ohmid}" diff --git a/homeassistant/components/ombi/sensor.py b/homeassistant/components/ombi/sensor.py index 9152af0f1bf..90410ea8da2 100644 --- a/homeassistant/components/ombi/sensor.py +++ b/homeassistant/components/ombi/sensor.py @@ -45,7 +45,7 @@ class OmbiSensor(SensorEntity): self._attr_name = f"Ombi {description.name}" - def update(self): + def update(self) -> None: """Update the sensor.""" try: sensor_type = self.entity_description.key diff --git a/homeassistant/components/onkyo/media_player.py b/homeassistant/components/onkyo/media_player.py index 381f48b0b16..82f662e1bad 100644 --- a/homeassistant/components/onkyo/media_player.py +++ b/homeassistant/components/onkyo/media_player.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any import eiscp from eiscp import eISCP @@ -295,7 +296,7 @@ class OnkyoDevice(MediaPlayerEntity): _LOGGER.debug("Result for %s: %s", command, result) return result - def update(self): + def update(self) -> None: """Get the latest state from the device.""" status = self.command("system-power query") @@ -396,11 +397,11 @@ class OnkyoDevice(MediaPlayerEntity): """Return device specific state attributes.""" return self._attributes - def turn_off(self): + def turn_off(self) -> None: """Turn the media player off.""" self.command("system-power standby") - def set_volume_level(self, volume): + def set_volume_level(self, volume: float) -> None: """ Set volume level, input is range 0..1. @@ -414,32 +415,32 @@ class OnkyoDevice(MediaPlayerEntity): f"volume {int(volume * (self._max_volume / 100) * self._receiver_max_volume)}" ) - def volume_up(self): + def volume_up(self) -> None: """Increase volume by 1 step.""" self.command("volume level-up") - def volume_down(self): + def volume_down(self) -> None: """Decrease volume by 1 step.""" self.command("volume level-down") - def mute_volume(self, mute): + def mute_volume(self, mute: bool) -> None: """Mute (true) or unmute (false) media player.""" if mute: self.command("audio-muting on") else: self.command("audio-muting off") - def turn_on(self): + def turn_on(self) -> None: """Turn the media player on.""" self.command("system-power on") - def select_source(self, source): + def select_source(self, source: str) -> None: """Set the input source.""" if source in self._source_list: source = self._reverse_mapping[source] self.command(f"input-selector {source}") - def play_media(self, media_type, media_id, **kwargs): + def play_media(self, media_type: str, media_id: str, **kwargs: Any) -> None: """Play radio station by preset number.""" source = self._reverse_mapping[self._current_source] if media_type.lower() == "radio" and source in DEFAULT_PLAYABLE_SOURCES: @@ -506,7 +507,7 @@ class OnkyoDeviceZone(OnkyoDevice): self._supports_volume = True super().__init__(receiver, sources, name, max_volume, receiver_max_volume) - def update(self): + def update(self) -> None: """Get the latest state from the device.""" status = self.command(f"zone{self._zone}.power=query") @@ -563,11 +564,11 @@ class OnkyoDeviceZone(OnkyoDevice): return SUPPORT_ONKYO return SUPPORT_ONKYO_WO_VOLUME - def turn_off(self): + def turn_off(self) -> None: """Turn the media player off.""" self.command(f"zone{self._zone}.power=standby") - def set_volume_level(self, volume): + def set_volume_level(self, volume: float) -> None: """ Set volume level, input is range 0..1. @@ -581,26 +582,26 @@ class OnkyoDeviceZone(OnkyoDevice): f"zone{self._zone}.volume={int(volume * (self._max_volume / 100) * self._receiver_max_volume)}" ) - def volume_up(self): + def volume_up(self) -> None: """Increase volume by 1 step.""" self.command(f"zone{self._zone}.volume=level-up") - def volume_down(self): + def volume_down(self) -> None: """Decrease volume by 1 step.""" self.command(f"zone{self._zone}.volume=level-down") - def mute_volume(self, mute): + def mute_volume(self, mute: bool) -> None: """Mute (true) or unmute (false) media player.""" if mute: self.command(f"zone{self._zone}.muting=on") else: self.command(f"zone{self._zone}.muting=off") - def turn_on(self): + def turn_on(self) -> None: """Turn the media player on.""" self.command(f"zone{self._zone}.power=on") - def select_source(self, source): + def select_source(self, source: str) -> None: """Set the input source.""" if source in self._source_list: source = self._reverse_mapping[source] diff --git a/homeassistant/components/onvif/binary_sensor.py b/homeassistant/components/onvif/binary_sensor.py index cd9af1d83b5..2a7f23c61ee 100644 --- a/homeassistant/components/onvif/binary_sensor.py +++ b/homeassistant/components/onvif/binary_sensor.py @@ -82,7 +82,7 @@ class ONVIFBinarySensor(ONVIFBaseEntity, RestoreEntity, BinarySensorEntity): return event.value return self._attr_is_on - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Connect to dispatcher listening for entity data notifications.""" self.async_on_remove( self.device.events.async_add_listener(self.async_write_ha_state) diff --git a/homeassistant/components/onvif/sensor.py b/homeassistant/components/onvif/sensor.py index b662dca1d5d..4ad4c11f175 100644 --- a/homeassistant/components/onvif/sensor.py +++ b/homeassistant/components/onvif/sensor.py @@ -81,7 +81,7 @@ class ONVIFSensor(ONVIFBaseEntity, RestoreSensor): return event.value return self._attr_native_value - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Connect to dispatcher listening for entity data notifications.""" self.async_on_remove( self.device.events.async_add_listener(self.async_write_ha_state) diff --git a/homeassistant/components/openerz/sensor.py b/homeassistant/components/openerz/sensor.py index c83611a303c..4bfe11ee264 100644 --- a/homeassistant/components/openerz/sensor.py +++ b/homeassistant/components/openerz/sensor.py @@ -58,7 +58,7 @@ class OpenERZSensor(SensorEntity): """Return the state of the sensor.""" return self._state - def update(self): + def update(self) -> None: """Fetch new state data for the sensor. This is the only method that should fetch new data for Home Assistant. diff --git a/homeassistant/components/openevse/sensor.py b/homeassistant/components/openevse/sensor.py index 3dcea4d0126..5bb469a3d99 100644 --- a/homeassistant/components/openevse/sensor.py +++ b/homeassistant/components/openevse/sensor.py @@ -118,7 +118,7 @@ class OpenEVSESensor(SensorEntity): self.entity_description = description self.charger = charger - def update(self): + def update(self) -> None: """Get the monitored data from the charger.""" try: sensor_type = self.entity_description.key diff --git a/homeassistant/components/openhardwaremonitor/sensor.py b/homeassistant/components/openhardwaremonitor/sensor.py index f21126d01a0..70dbbd38fc8 100644 --- a/homeassistant/components/openhardwaremonitor/sensor.py +++ b/homeassistant/components/openhardwaremonitor/sensor.py @@ -91,7 +91,7 @@ class OpenHardwareMonitorDevice(SensorEntity): """In some locales a decimal numbers uses ',' instead of '.'.""" return string.replace(",", ".") - def update(self): + def update(self) -> None: """Update the device from a new JSON object.""" self._data.update() diff --git a/homeassistant/components/openhome/media_player.py b/homeassistant/components/openhome/media_player.py index b6a0b549c40..d528bb8dad4 100644 --- a/homeassistant/components/openhome/media_player.py +++ b/homeassistant/components/openhome/media_player.py @@ -19,6 +19,7 @@ from homeassistant.components.media_player import ( MediaPlayerEntityFeature, ) from homeassistant.components.media_player.browse_media import ( + BrowseMedia, async_process_play_media_url, ) from homeassistant.components.media_player.const import MEDIA_TYPE_MUSIC @@ -133,7 +134,7 @@ class OpenhomeDevice(MediaPlayerEntity): """Device is available.""" return self._available - async def async_update(self): + async def async_update(self) -> None: """Update state of device.""" try: self._in_standby = await self._device.is_in_standby() @@ -195,17 +196,19 @@ class OpenhomeDevice(MediaPlayerEntity): self._available = False @catch_request_errors() - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Bring device out of standby.""" await self._device.set_standby(False) @catch_request_errors() - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Put device in standby.""" await self._device.set_standby(True) @catch_request_errors() - async def async_play_media(self, media_type, media_id, **kwargs): + async def async_play_media( + self, media_type: str, media_id: str, **kwargs: Any + ) -> None: """Send the play_media command to the media player.""" if media_source.is_media_source_id(media_id): media_type = MEDIA_TYPE_MUSIC @@ -228,32 +231,32 @@ class OpenhomeDevice(MediaPlayerEntity): await self._device.play_media(track_details) @catch_request_errors() - async def async_media_pause(self): + async def async_media_pause(self) -> None: """Send pause command.""" await self._device.pause() @catch_request_errors() - async def async_media_stop(self): + async def async_media_stop(self) -> None: """Send stop command.""" await self._device.stop() @catch_request_errors() - async def async_media_play(self): + async def async_media_play(self) -> None: """Send play command.""" await self._device.play() @catch_request_errors() - async def async_media_next_track(self): + async def async_media_next_track(self) -> None: """Send next track command.""" await self._device.skip(1) @catch_request_errors() - async def async_media_previous_track(self): + async def async_media_previous_track(self) -> None: """Send previous track command.""" await self._device.skip(-1) @catch_request_errors() - async def async_select_source(self, source): + async def async_select_source(self, source: str) -> None: """Select input source.""" await self._device.set_source(self._source_index[source]) @@ -325,26 +328,28 @@ class OpenhomeDevice(MediaPlayerEntity): return self._volume_muted @catch_request_errors() - async def async_volume_up(self): + async def async_volume_up(self) -> None: """Volume up media player.""" await self._device.increase_volume() @catch_request_errors() - async def async_volume_down(self): + async def async_volume_down(self) -> None: """Volume down media player.""" await self._device.decrease_volume() @catch_request_errors() - async def async_set_volume_level(self, volume): + async def async_set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" await self._device.set_volume(int(volume * 100)) @catch_request_errors() - async def async_mute_volume(self, mute): + async def async_mute_volume(self, mute: bool) -> None: """Mute (true) or unmute (false) media player.""" await self._device.set_mute(mute) - async def async_browse_media(self, media_content_type=None, media_content_id=None): + async def async_browse_media( + self, media_content_type: str | None = None, media_content_id: str | None = None + ) -> BrowseMedia: """Implement the websocket media browsing helper.""" return await media_source.async_browse_media( self.hass, diff --git a/homeassistant/components/opentherm_gw/binary_sensor.py b/homeassistant/components/opentherm_gw/binary_sensor.py index 083fb103481..194e047e50e 100644 --- a/homeassistant/components/opentherm_gw/binary_sensor.py +++ b/homeassistant/components/opentherm_gw/binary_sensor.py @@ -102,14 +102,14 @@ class OpenThermBinarySensor(BinarySensorEntity): self._friendly_name = friendly_name_format.format(gw_dev.name) self._unsub_updates = None - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Subscribe to updates from the component.""" _LOGGER.debug("Added OpenTherm Gateway binary sensor %s", self._friendly_name) self._unsub_updates = async_dispatcher_connect( self.hass, self._gateway.update_signal, self.receive_report ) - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Unsubscribe from updates from the component.""" _LOGGER.debug( "Removing OpenTherm Gateway binary sensor %s", self._friendly_name diff --git a/homeassistant/components/opentherm_gw/climate.py b/homeassistant/components/opentherm_gw/climate.py index a805cbacba0..fba28138f42 100644 --- a/homeassistant/components/opentherm_gw/climate.py +++ b/homeassistant/components/opentherm_gw/climate.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from pyotgw import vars as gw_vars @@ -101,7 +102,7 @@ class OpenThermClimate(ClimateEntity): self.temporary_ovrd_mode = entry.options[CONF_TEMPORARY_OVRD_MODE] self.async_write_ha_state() - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Connect to the OpenTherm Gateway device.""" _LOGGER.debug("Added OpenTherm Gateway climate device %s", self.friendly_name) self._unsub_updates = async_dispatcher_connect( @@ -111,7 +112,7 @@ class OpenThermClimate(ClimateEntity): self.hass, self._gateway.options_update_signal, self.update_options ) - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Unsubscribe from updates from the component.""" _LOGGER.debug("Removing OpenTherm Gateway climate %s", self.friendly_name) self._unsub_options() @@ -261,11 +262,11 @@ class OpenThermClimate(ClimateEntity): """Available preset modes to set.""" return [] - def set_preset_mode(self, preset_mode): + def set_preset_mode(self, preset_mode: str) -> None: """Set the preset mode.""" _LOGGER.warning("Changing preset mode is not supported") - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" if ATTR_TEMPERATURE in kwargs: temp = float(kwargs[ATTR_TEMPERATURE]) diff --git a/homeassistant/components/opentherm_gw/sensor.py b/homeassistant/components/opentherm_gw/sensor.py index 5eea4fca099..67b4eb138dd 100644 --- a/homeassistant/components/opentherm_gw/sensor.py +++ b/homeassistant/components/opentherm_gw/sensor.py @@ -106,14 +106,14 @@ class OpenThermSensor(SensorEntity): self._friendly_name = friendly_name_format.format(gw_dev.name) self._unsub_updates = None - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Subscribe to updates from the component.""" _LOGGER.debug("Added OpenTherm Gateway sensor %s", self._friendly_name) self._unsub_updates = async_dispatcher_connect( self.hass, self._gateway.update_signal, self.receive_report ) - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Unsubscribe from updates from the component.""" _LOGGER.debug("Removing OpenTherm Gateway sensor %s", self._friendly_name) self._unsub_updates() diff --git a/homeassistant/components/opple/light.py b/homeassistant/components/opple/light.py index 25bcf821fc9..80d0f8630dc 100644 --- a/homeassistant/components/opple/light.py +++ b/homeassistant/components/opple/light.py @@ -69,7 +69,7 @@ class OppleLight(LightEntity): self._color_temp = None @property - def available(self): + def available(self) -> bool: """Return True if light is available.""" return self._device.is_online diff --git a/homeassistant/components/oru/sensor.py b/homeassistant/components/oru/sensor.py index b31defd7171..5d99e782fdd 100644 --- a/homeassistant/components/oru/sensor.py +++ b/homeassistant/components/oru/sensor.py @@ -75,7 +75,7 @@ class CurrentEnergyUsageSensor(SensorEntity): """Return the state of the sensor.""" return self._state - def update(self): + def update(self) -> None: """Fetch new state data for the sensor.""" try: last_read = self.meter.last_read() diff --git a/homeassistant/components/orvibo/switch.py b/homeassistant/components/orvibo/switch.py index b526b9057bd..9e86d3787af 100644 --- a/homeassistant/components/orvibo/switch.py +++ b/homeassistant/components/orvibo/switch.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from orvibo.s20 import S20, S20Exception, discover import voluptuous as vol @@ -93,21 +94,21 @@ class S20Switch(SwitchEntity): """Return true if device is on.""" return self._state - def update(self): + def update(self) -> None: """Update device state.""" try: self._state = self._s20.on except self._exc: _LOGGER.exception("Error while fetching S20 state") - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" try: self._s20.on = True except self._exc: _LOGGER.exception("Error while turning on S20") - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" try: self._s20.on = False diff --git a/homeassistant/components/otp/sensor.py b/homeassistant/components/otp/sensor.py index ff5a2965795..499c9b129f1 100644 --- a/homeassistant/components/otp/sensor.py +++ b/homeassistant/components/otp/sensor.py @@ -53,7 +53,7 @@ class TOTPSensor(SensorEntity): self._state = None self._next_expiration = None - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Handle when an entity is about to be added to Home Assistant.""" self._call_loop() diff --git a/homeassistant/components/owntracks/device_tracker.py b/homeassistant/components/owntracks/device_tracker.py index 5119168e7ae..70b8bde6de1 100644 --- a/homeassistant/components/owntracks/device_tracker.py +++ b/homeassistant/components/owntracks/device_tracker.py @@ -124,7 +124,7 @@ class OwnTracksEntity(TrackerEntity, RestoreEntity): """Return the device info.""" return DeviceInfo(identifiers={(OT_DOMAIN, self._dev_id)}, name=self.name) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Call when entity about to be added to Home Assistant.""" await super().async_added_to_hass() From a4792998a2354e88ad8d4dc3ffd9935e156cf9d5 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 6 Sep 2022 11:02:15 +0200 Subject: [PATCH 132/955] Refactor MQTT tests to use modern platform schema part 1 (#77387) * Tests alarm_control_panel * Tests binary_sensor * Tests button * Tests camera * Tests Climate + corrections default config * Tests cover * Tests device_tracker * Tests fan * Tests humidifier * Fix test_supported_features test fan * Tests init * Tests legacy vacuum * Derive DEFAULT_CONFIG_LEGACY from DEFAULT_CONFIG * Commit suggestion comment changes --- .../mqtt/test_alarm_control_panel.py | 265 +-- tests/components/mqtt/test_binary_sensor.py | 341 ++-- tests/components/mqtt/test_button.py | 169 +- tests/components/mqtt/test_camera.py | 131 +- tests/components/mqtt/test_climate.py | 290 +-- tests/components/mqtt/test_cover.py | 1566 +++++++++-------- tests/components/mqtt/test_device_tracker.py | 122 +- .../mqtt/test_device_tracker_discovery.py | 39 +- tests/components/mqtt/test_fan.py | 954 +++++----- tests/components/mqtt/test_humidifier.py | 659 +++---- tests/components/mqtt/test_init.py | 35 +- tests/components/mqtt/test_legacy_vacuum.py | 284 +-- 12 files changed, 2659 insertions(+), 2196 deletions(-) diff --git a/tests/components/mqtt/test_alarm_control_panel.py b/tests/components/mqtt/test_alarm_control_panel.py index 7d127902f3d..e51ed9aeae9 100644 --- a/tests/components/mqtt/test_alarm_control_panel.py +++ b/tests/components/mqtt/test_alarm_control_panel.py @@ -5,7 +5,7 @@ from unittest.mock import patch import pytest -from homeassistant.components import alarm_control_panel +from homeassistant.components import alarm_control_panel, mqtt from homeassistant.components.mqtt.alarm_control_panel import ( MQTT_ALARM_ATTRIBUTES_BLOCKED, ) @@ -65,54 +65,65 @@ from .test_common import ( help_test_update_with_json_attrs_not_dict, ) -from tests.common import assert_setup_component, async_fire_mqtt_message +from tests.common import async_fire_mqtt_message from tests.components.alarm_control_panel import common CODE_NUMBER = "1234" CODE_TEXT = "HELLO_CODE" DEFAULT_CONFIG = { - alarm_control_panel.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "alarm/state", - "command_topic": "alarm/command", + mqtt.DOMAIN: { + alarm_control_panel.DOMAIN: { + "name": "test", + "state_topic": "alarm/state", + "command_topic": "alarm/command", + } } } DEFAULT_CONFIG_CODE = { - alarm_control_panel.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "alarm/state", - "command_topic": "alarm/command", - "code": "0123", - "code_arm_required": True, + mqtt.DOMAIN: { + alarm_control_panel.DOMAIN: { + "name": "test", + "state_topic": "alarm/state", + "command_topic": "alarm/command", + "code": "0123", + "code_arm_required": True, + } } } DEFAULT_CONFIG_REMOTE_CODE = { - alarm_control_panel.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "alarm/state", - "command_topic": "alarm/command", - "code": "REMOTE_CODE", - "code_arm_required": True, + mqtt.DOMAIN: { + alarm_control_panel.DOMAIN: { + "name": "test", + "state_topic": "alarm/state", + "command_topic": "alarm/command", + "code": "REMOTE_CODE", + "code_arm_required": True, + } } } DEFAULT_CONFIG_REMOTE_CODE_TEXT = { - alarm_control_panel.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "alarm/state", - "command_topic": "alarm/command", - "code": "REMOTE_CODE_TEXT", - "code_arm_required": True, + mqtt.DOMAIN: { + alarm_control_panel.DOMAIN: { + "name": "test", + "state_topic": "alarm/state", + "command_topic": "alarm/command", + "code": "REMOTE_CODE_TEXT", + "code_arm_required": True, + } } } +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +DEFAULT_CONFIG_LEGACY = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) +DEFAULT_CONFIG_LEGACY[alarm_control_panel.DOMAIN]["platform"] = mqtt.DOMAIN +DEFAULT_CONFIG_CODE_LEGACY = copy.deepcopy(DEFAULT_CONFIG_CODE[mqtt.DOMAIN]) +DEFAULT_CONFIG_CODE_LEGACY[alarm_control_panel.DOMAIN]["platform"] = mqtt.DOMAIN + @pytest.fixture(autouse=True) def alarm_control_panel_platform_only(): @@ -123,47 +134,62 @@ def alarm_control_panel_platform_only(): yield -async def test_fail_setup_without_state_topic(hass, mqtt_mock_entry_no_yaml_config): - """Test for failing with no state topic.""" - with assert_setup_component(0, alarm_control_panel.DOMAIN) as config: - assert await async_setup_component( - hass, - alarm_control_panel.DOMAIN, +@pytest.mark.parametrize( + "config,valid", + [ + ( { - alarm_control_panel.DOMAIN: { - "platform": "mqtt", - "command_topic": "alarm/command", + mqtt.DOMAIN: { + alarm_control_panel.DOMAIN: { + "name": "test", + "command_topic": "alarm/command", + } } }, - ) - await hass.async_block_till_done() - await mqtt_mock_entry_no_yaml_config() - assert not config[alarm_control_panel.DOMAIN] - - -async def test_fail_setup_without_command_topic(hass, mqtt_mock_entry_no_yaml_config): - """Test failing with no command topic.""" - with assert_setup_component(0, alarm_control_panel.DOMAIN) as config: - assert await async_setup_component( - hass, - alarm_control_panel.DOMAIN, + False, + ), + ( { - alarm_control_panel.DOMAIN: { - "platform": "mqtt", - "state_topic": "alarm/state", + mqtt.DOMAIN: { + alarm_control_panel.DOMAIN: { + "name": "test", + "state_topic": "alarm/state", + } } }, + False, + ), + ( + { + mqtt.DOMAIN: { + alarm_control_panel.DOMAIN: { + "name": "test", + "command_topic": "alarm/command", + "state_topic": "alarm/state", + } + } + }, + True, + ), + ], +) +async def test_fail_setup_without_state_or_command_topic(hass, config, valid): + """Test for failing setup with no state or command topic.""" + assert ( + await async_setup_component( + hass, + mqtt.DOMAIN, + config, ) - await hass.async_block_till_done() - await mqtt_mock_entry_no_yaml_config() - assert not config[alarm_control_panel.DOMAIN] + is valid + ) async def test_update_state_via_state_topic(hass, mqtt_mock_entry_with_yaml_config): """Test updating with via state topic.""" assert await async_setup_component( hass, - alarm_control_panel.DOMAIN, + mqtt.DOMAIN, DEFAULT_CONFIG, ) await hass.async_block_till_done() @@ -195,7 +221,7 @@ async def test_ignore_update_state_if_unknown_via_state_topic( """Test ignoring updates via state topic.""" assert await async_setup_component( hass, - alarm_control_panel.DOMAIN, + mqtt.DOMAIN, DEFAULT_CONFIG, ) await hass.async_block_till_done() @@ -227,7 +253,7 @@ async def test_publish_mqtt_no_code( """Test publishing of MQTT messages when no code is configured.""" assert await async_setup_component( hass, - alarm_control_panel.DOMAIN, + mqtt.DOMAIN, DEFAULT_CONFIG, ) await hass.async_block_till_done() @@ -261,7 +287,7 @@ async def test_publish_mqtt_with_code( """Test publishing of MQTT messages when code is configured.""" assert await async_setup_component( hass, - alarm_control_panel.DOMAIN, + mqtt.DOMAIN, DEFAULT_CONFIG_CODE, ) await hass.async_block_till_done() @@ -314,7 +340,7 @@ async def test_publish_mqtt_with_remote_code( """Test publishing of MQTT messages when remode code is configured.""" assert await async_setup_component( hass, - alarm_control_panel.DOMAIN, + mqtt.DOMAIN, DEFAULT_CONFIG_REMOTE_CODE, ) await hass.async_block_till_done() @@ -358,7 +384,7 @@ async def test_publish_mqtt_with_remote_code_text( """Test publishing of MQTT messages when remote text code is configured.""" assert await async_setup_component( hass, - alarm_control_panel.DOMAIN, + mqtt.DOMAIN, DEFAULT_CONFIG_REMOTE_CODE_TEXT, ) await hass.async_block_till_done() @@ -405,10 +431,10 @@ async def test_publish_mqtt_with_code_required_false( code_trigger_required = False """ config = copy.deepcopy(DEFAULT_CONFIG_CODE) - config[alarm_control_panel.DOMAIN][disable_code] = False + config[mqtt.DOMAIN][alarm_control_panel.DOMAIN][disable_code] = False assert await async_setup_component( hass, - alarm_control_panel.DOMAIN, + mqtt.DOMAIN, config, ) await hass.async_block_till_done() @@ -453,13 +479,13 @@ async def test_disarm_publishes_mqtt_with_template( When command_template set to output json """ config = copy.deepcopy(DEFAULT_CONFIG_CODE) - config[alarm_control_panel.DOMAIN]["code"] = "0123" - config[alarm_control_panel.DOMAIN][ + config[mqtt.DOMAIN][alarm_control_panel.DOMAIN]["code"] = "0123" + config[mqtt.DOMAIN][alarm_control_panel.DOMAIN][ "command_template" ] = '{"action":"{{ action }}","code":"{{ code }}"}' assert await async_setup_component( hass, - alarm_control_panel.DOMAIN, + mqtt.DOMAIN, config, ) await hass.async_block_till_done() @@ -477,19 +503,20 @@ async def test_update_state_via_state_topic_template( """Test updating with template_value via state topic.""" assert await async_setup_component( hass, - alarm_control_panel.DOMAIN, + mqtt.DOMAIN, { - alarm_control_panel.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "test-topic", - "state_topic": "test-topic", - "value_template": "\ + mqtt.DOMAIN: { + alarm_control_panel.DOMAIN: { + "name": "test", + "command_topic": "test-topic", + "state_topic": "test-topic", + "value_template": "\ {% if (value | int) == 100 %}\ armed_away\ {% else %}\ disarmed\ {% endif %}", + } } }, ) @@ -508,9 +535,9 @@ async def test_update_state_via_state_topic_template( async def test_attributes_code_number(hass, mqtt_mock_entry_with_yaml_config): """Test attributes which are not supported by the vacuum.""" config = copy.deepcopy(DEFAULT_CONFIG) - config[alarm_control_panel.DOMAIN]["code"] = CODE_NUMBER + config[mqtt.DOMAIN][alarm_control_panel.DOMAIN]["code"] = CODE_NUMBER - assert await async_setup_component(hass, alarm_control_panel.DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, config) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -524,9 +551,9 @@ async def test_attributes_code_number(hass, mqtt_mock_entry_with_yaml_config): async def test_attributes_remote_code_number(hass, mqtt_mock_entry_with_yaml_config): """Test attributes which are not supported by the vacuum.""" config = copy.deepcopy(DEFAULT_CONFIG_REMOTE_CODE) - config[alarm_control_panel.DOMAIN]["code"] = "REMOTE_CODE" + config[mqtt.DOMAIN][alarm_control_panel.DOMAIN]["code"] = "REMOTE_CODE" - assert await async_setup_component(hass, alarm_control_panel.DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, config) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -540,9 +567,9 @@ async def test_attributes_remote_code_number(hass, mqtt_mock_entry_with_yaml_con async def test_attributes_code_text(hass, mqtt_mock_entry_with_yaml_config): """Test attributes which are not supported by the vacuum.""" config = copy.deepcopy(DEFAULT_CONFIG) - config[alarm_control_panel.DOMAIN]["code"] = CODE_TEXT + config[mqtt.DOMAIN][alarm_control_panel.DOMAIN]["code"] = CODE_TEXT - assert await async_setup_component(hass, alarm_control_panel.DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, config) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -561,7 +588,7 @@ async def test_availability_when_connection_lost( hass, mqtt_mock_entry_with_yaml_config, alarm_control_panel.DOMAIN, - DEFAULT_CONFIG_CODE, + DEFAULT_CONFIG_CODE_LEGACY, ) @@ -571,7 +598,7 @@ async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config hass, mqtt_mock_entry_with_yaml_config, alarm_control_panel.DOMAIN, - DEFAULT_CONFIG_CODE, + DEFAULT_CONFIG_CODE_LEGACY, ) @@ -581,7 +608,7 @@ async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_conf hass, mqtt_mock_entry_with_yaml_config, alarm_control_panel.DOMAIN, - DEFAULT_CONFIG_CODE, + DEFAULT_CONFIG_CODE_LEGACY, ) @@ -591,7 +618,7 @@ async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_confi hass, mqtt_mock_entry_with_yaml_config, alarm_control_panel.DOMAIN, - DEFAULT_CONFIG_CODE, + DEFAULT_CONFIG_CODE_LEGACY, ) @@ -603,7 +630,7 @@ async def test_setting_attribute_via_mqtt_json_message( hass, mqtt_mock_entry_with_yaml_config, alarm_control_panel.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, ) @@ -615,7 +642,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( hass, mqtt_mock_entry_no_yaml_config, alarm_control_panel.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, MQTT_ALARM_ATTRIBUTES_BLOCKED, ) @@ -626,7 +653,7 @@ async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_c hass, mqtt_mock_entry_with_yaml_config, alarm_control_panel.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, ) @@ -639,7 +666,7 @@ async def test_update_with_json_attrs_not_dict( mqtt_mock_entry_with_yaml_config, caplog, alarm_control_panel.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, ) @@ -652,7 +679,7 @@ async def test_update_with_json_attrs_bad_JSON( mqtt_mock_entry_with_yaml_config, caplog, alarm_control_panel.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, ) @@ -663,7 +690,7 @@ async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplo mqtt_mock_entry_no_yaml_config, caplog, alarm_control_panel.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, ) @@ -694,7 +721,7 @@ async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): async def test_discovery_removal_alarm(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered alarm_control_panel.""" - data = json.dumps(DEFAULT_CONFIG[alarm_control_panel.DOMAIN]) + data = json.dumps(DEFAULT_CONFIG_LEGACY[alarm_control_panel.DOMAIN]) await help_test_discovery_removal( hass, mqtt_mock_entry_no_yaml_config, caplog, alarm_control_panel.DOMAIN, data ) @@ -704,8 +731,8 @@ async def test_discovery_update_alarm_topic_and_template( hass, mqtt_mock_entry_no_yaml_config, caplog ): """Test update of discovered alarm_control_panel.""" - config1 = copy.deepcopy(DEFAULT_CONFIG[alarm_control_panel.DOMAIN]) - config2 = copy.deepcopy(DEFAULT_CONFIG[alarm_control_panel.DOMAIN]) + config1 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[alarm_control_panel.DOMAIN]) + config2 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[alarm_control_panel.DOMAIN]) config1["name"] = "Beer" config2["name"] = "Milk" config1["state_topic"] = "alarm/state1" @@ -739,8 +766,8 @@ async def test_discovery_update_alarm_template( hass, mqtt_mock_entry_no_yaml_config, caplog ): """Test update of discovered alarm_control_panel.""" - config1 = copy.deepcopy(DEFAULT_CONFIG[alarm_control_panel.DOMAIN]) - config2 = copy.deepcopy(DEFAULT_CONFIG[alarm_control_panel.DOMAIN]) + config1 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[alarm_control_panel.DOMAIN]) + config2 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[alarm_control_panel.DOMAIN]) config1["name"] = "Beer" config2["name"] = "Milk" config1["state_topic"] = "alarm/state1" @@ -772,7 +799,7 @@ async def test_discovery_update_unchanged_alarm( hass, mqtt_mock_entry_no_yaml_config, caplog ): """Test update of discovered alarm_control_panel.""" - config1 = copy.deepcopy(DEFAULT_CONFIG[alarm_control_panel.DOMAIN]) + config1 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[alarm_control_panel.DOMAIN]) config1["name"] = "Beer" data1 = json.dumps(config1) @@ -824,7 +851,7 @@ async def test_encoding_subscribable_topics( mqtt_mock_entry_with_yaml_config, caplog, alarm_control_panel.DOMAIN, - DEFAULT_CONFIG[alarm_control_panel.DOMAIN], + DEFAULT_CONFIG_LEGACY[alarm_control_panel.DOMAIN], topic, value, ) @@ -836,7 +863,7 @@ async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_ hass, mqtt_mock_entry_no_yaml_config, alarm_control_panel.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, ) @@ -846,21 +873,27 @@ async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_ hass, mqtt_mock_entry_no_yaml_config, alarm_control_panel.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, ) async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock_entry_no_yaml_config, alarm_control_panel.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock_entry_no_yaml_config, alarm_control_panel.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) @@ -870,14 +903,17 @@ async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_co hass, mqtt_mock_entry_with_yaml_config, alarm_control_panel.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, ) async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock_entry_no_yaml_config, alarm_control_panel.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) @@ -887,7 +923,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): hass, mqtt_mock_entry_no_yaml_config, alarm_control_panel.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, alarm_control_panel.SERVICE_ALARM_DISARM, command_payload="DISARM", ) @@ -930,7 +966,7 @@ async def test_publishing_with_custom_encoding( ): """Test publishing MQTT payload with different encoding.""" domain = alarm_control_panel.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_publishing_with_custom_encoding( hass, @@ -951,7 +987,7 @@ async def test_publishing_with_custom_encoding( async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = alarm_control_panel.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) @@ -960,14 +996,14 @@ async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_pa async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = alarm_control_panel.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = alarm_control_panel.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG[platform]) + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) config["name"] = "test" del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) @@ -977,7 +1013,20 @@ async def test_setup_manual_entity_from_yaml(hass): async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): """Test unloading the config entry.""" domain = alarm_control_panel.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) + + +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +async def test_setup_with_legacy_schema(hass, mqtt_mock_entry_with_yaml_config): + """Test a setup with deprecated yaml platform schema.""" + domain = alarm_control_panel.DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) + config["name"] = "test" + assert await async_setup_component(hass, domain, {domain: config}) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + assert hass.states.get(f"{domain}.test") is not None diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py index 7d4edace988..bbe8b978707 100644 --- a/tests/components/mqtt/test_binary_sensor.py +++ b/tests/components/mqtt/test_binary_sensor.py @@ -6,7 +6,7 @@ from unittest.mock import patch import pytest -from homeassistant.components import binary_sensor +from homeassistant.components import binary_sensor, mqtt from homeassistant.const import ( EVENT_STATE_CHANGED, STATE_OFF, @@ -50,20 +50,25 @@ from .test_common import ( ) from tests.common import ( - assert_setup_component, async_fire_mqtt_message, async_fire_time_changed, mock_restore_cache, ) DEFAULT_CONFIG = { - binary_sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "test-topic", + mqtt.DOMAIN: { + binary_sensor.DOMAIN: { + "name": "test", + "state_topic": "test-topic", + } } } +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +DEFAULT_CONFIG_LEGACY = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) +DEFAULT_CONFIG_LEGACY[binary_sensor.DOMAIN]["platform"] = mqtt.DOMAIN + @pytest.fixture(autouse=True) def binary_sensor_platform_only(): @@ -78,15 +83,16 @@ async def test_setting_sensor_value_expires_availability_topic( """Test the expiration of the value.""" assert await async_setup_component( hass, - binary_sensor.DOMAIN, + mqtt.DOMAIN, { - binary_sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "test-topic", - "expire_after": 4, - "force_update": True, - "availability_topic": "availability-topic", + mqtt.DOMAIN: { + binary_sensor.DOMAIN: { + "name": "test", + "state_topic": "test-topic", + "expire_after": 4, + "force_update": True, + "availability_topic": "availability-topic", + } } }, ) @@ -270,14 +276,15 @@ async def test_setting_sensor_value_via_mqtt_message( """Test the setting of the value via MQTT.""" assert await async_setup_component( hass, - binary_sensor.DOMAIN, + mqtt.DOMAIN, { - binary_sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "test-topic", - "payload_on": "ON", - "payload_off": "OFF", + mqtt.DOMAIN: { + binary_sensor.DOMAIN: { + "name": "test", + "state_topic": "test-topic", + "payload_on": "ON", + "payload_off": "OFF", + } } }, ) @@ -307,14 +314,15 @@ async def test_invalid_sensor_value_via_mqtt_message( """Test the setting of the value via MQTT.""" assert await async_setup_component( hass, - binary_sensor.DOMAIN, + mqtt.DOMAIN, { - binary_sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "test-topic", - "payload_on": "ON", - "payload_off": "OFF", + mqtt.DOMAIN: { + binary_sensor.DOMAIN: { + "name": "test", + "state_topic": "test-topic", + "payload_on": "ON", + "payload_off": "OFF", + } } }, ) @@ -348,16 +356,17 @@ async def test_setting_sensor_value_via_mqtt_message_and_template( """Test the setting of the value via MQTT.""" assert await async_setup_component( hass, - binary_sensor.DOMAIN, + mqtt.DOMAIN, { - binary_sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "test-topic", - "payload_on": "ON", - "payload_off": "OFF", - "value_template": '{%if is_state(entity_id,"on")-%}OFF' - "{%-else-%}ON{%-endif%}", + mqtt.DOMAIN: { + binary_sensor.DOMAIN: { + "name": "test", + "state_topic": "test-topic", + "payload_on": "ON", + "payload_off": "OFF", + "value_template": '{%if is_state(entity_id,"on")-%}OFF' + "{%-else-%}ON{%-endif%}", + } } }, ) @@ -382,15 +391,16 @@ async def test_setting_sensor_value_via_mqtt_message_and_template2( """Test the setting of the value via MQTT.""" assert await async_setup_component( hass, - binary_sensor.DOMAIN, + mqtt.DOMAIN, { - binary_sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "test-topic", - "payload_on": "ON", - "payload_off": "OFF", - "value_template": "{{value | upper}}", + mqtt.DOMAIN: { + binary_sensor.DOMAIN: { + "name": "test", + "state_topic": "test-topic", + "payload_on": "ON", + "payload_off": "OFF", + "value_template": "{{value | upper}}", + } } }, ) @@ -420,16 +430,17 @@ async def test_setting_sensor_value_via_mqtt_message_and_template_and_raw_state_ """Test processing a raw value via MQTT.""" assert await async_setup_component( hass, - binary_sensor.DOMAIN, + mqtt.DOMAIN, { - binary_sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "encoding": "", - "state_topic": "test-topic", - "payload_on": "ON", - "payload_off": "OFF", - "value_template": "{%if value|unpack('b')-%}ON{%else%}OFF{%-endif-%}", + mqtt.DOMAIN: { + binary_sensor.DOMAIN: { + "name": "test", + "encoding": "", + "state_topic": "test-topic", + "payload_on": "ON", + "payload_off": "OFF", + "value_template": "{%if value|unpack('b')-%}ON{%else%}OFF{%-endif-%}", + } } }, ) @@ -454,15 +465,16 @@ async def test_setting_sensor_value_via_mqtt_message_empty_template( """Test the setting of the value via MQTT.""" assert await async_setup_component( hass, - binary_sensor.DOMAIN, + mqtt.DOMAIN, { - binary_sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "test-topic", - "payload_on": "ON", - "payload_off": "OFF", - "value_template": '{%if value == "ABC"%}ON{%endif%}', + mqtt.DOMAIN: { + binary_sensor.DOMAIN: { + "name": "test", + "state_topic": "test-topic", + "payload_on": "ON", + "payload_off": "OFF", + "value_template": '{%if value == "ABC"%}ON{%endif%}', + } } }, ) @@ -486,13 +498,14 @@ async def test_valid_device_class(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of a valid sensor class.""" assert await async_setup_component( hass, - binary_sensor.DOMAIN, + mqtt.DOMAIN, { - binary_sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "device_class": "motion", - "state_topic": "test-topic", + mqtt.DOMAIN: { + binary_sensor.DOMAIN: { + "name": "test", + "device_class": "motion", + "state_topic": "test-topic", + } } }, ) @@ -503,25 +516,22 @@ async def test_valid_device_class(hass, mqtt_mock_entry_with_yaml_config): assert state.attributes.get("device_class") == "motion" -async def test_invalid_device_class(hass, mqtt_mock_entry_no_yaml_config): +async def test_invalid_device_class(hass, caplog): """Test the setting of an invalid sensor class.""" - assert await async_setup_component( + assert not await async_setup_component( hass, - binary_sensor.DOMAIN, + mqtt.DOMAIN, { - binary_sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "device_class": "abc123", - "state_topic": "test-topic", + mqtt.DOMAIN: { + binary_sensor.DOMAIN: { + "name": "test", + "device_class": "abc123", + "state_topic": "test-topic", + } } }, ) - await hass.async_block_till_done() - await mqtt_mock_entry_no_yaml_config() - - state = hass.states.get("binary_sensor.test") - assert state is None + assert "Invalid config for [mqtt]: expected BinarySensorDeviceClass" in caplog.text async def test_availability_when_connection_lost( @@ -529,28 +539,40 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + binary_sensor.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock_entry_with_yaml_config, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + binary_sensor.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock_entry_with_yaml_config, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + binary_sensor.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock_entry_with_yaml_config, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + binary_sensor.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) @@ -558,14 +580,15 @@ async def test_force_update_disabled(hass, mqtt_mock_entry_with_yaml_config): """Test force update option.""" assert await async_setup_component( hass, - binary_sensor.DOMAIN, + mqtt.DOMAIN, { - binary_sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "test-topic", - "payload_on": "ON", - "payload_off": "OFF", + mqtt.DOMAIN: { + binary_sensor.DOMAIN: { + "name": "test", + "state_topic": "test-topic", + "payload_on": "ON", + "payload_off": "OFF", + } } }, ) @@ -594,15 +617,16 @@ async def test_force_update_enabled(hass, mqtt_mock_entry_with_yaml_config): """Test force update option.""" assert await async_setup_component( hass, - binary_sensor.DOMAIN, + mqtt.DOMAIN, { - binary_sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "test-topic", - "payload_on": "ON", - "payload_off": "OFF", - "force_update": True, + mqtt.DOMAIN: { + binary_sensor.DOMAIN: { + "name": "test", + "state_topic": "test-topic", + "payload_on": "ON", + "payload_off": "OFF", + "force_update": True, + } } }, ) @@ -631,16 +655,17 @@ async def test_off_delay(hass, mqtt_mock_entry_with_yaml_config): """Test off_delay option.""" assert await async_setup_component( hass, - binary_sensor.DOMAIN, + mqtt.DOMAIN, { - binary_sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "test-topic", - "payload_on": "ON", - "payload_off": "OFF", - "off_delay": 30, - "force_update": True, + mqtt.DOMAIN: { + binary_sensor.DOMAIN: { + "name": "test", + "state_topic": "test-topic", + "payload_on": "ON", + "payload_off": "OFF", + "off_delay": 30, + "force_update": True, + } } }, ) @@ -680,14 +705,20 @@ async def test_setting_attribute_via_mqtt_json_message( ): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock_entry_with_yaml_config, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + binary_sensor.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock_entry_with_yaml_config, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + binary_sensor.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) @@ -700,7 +731,7 @@ async def test_update_with_json_attrs_not_dict( mqtt_mock_entry_with_yaml_config, caplog, binary_sensor.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, ) @@ -713,7 +744,7 @@ async def test_update_with_json_attrs_bad_JSON( mqtt_mock_entry_with_yaml_config, caplog, binary_sensor.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, ) @@ -724,7 +755,7 @@ async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplo mqtt_mock_entry_no_yaml_config, caplog, binary_sensor.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, ) @@ -755,7 +786,7 @@ async def test_discovery_removal_binary_sensor( hass, mqtt_mock_entry_no_yaml_config, caplog ): """Test removal of discovered binary_sensor.""" - data = json.dumps(DEFAULT_CONFIG[binary_sensor.DOMAIN]) + data = json.dumps(DEFAULT_CONFIG_LEGACY[binary_sensor.DOMAIN]) await help_test_discovery_removal( hass, mqtt_mock_entry_no_yaml_config, caplog, binary_sensor.DOMAIN, data ) @@ -765,8 +796,8 @@ async def test_discovery_update_binary_sensor_topic_template( hass, mqtt_mock_entry_no_yaml_config, caplog ): """Test update of discovered binary_sensor.""" - config1 = copy.deepcopy(DEFAULT_CONFIG[binary_sensor.DOMAIN]) - config2 = copy.deepcopy(DEFAULT_CONFIG[binary_sensor.DOMAIN]) + config1 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[binary_sensor.DOMAIN]) + config2 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[binary_sensor.DOMAIN]) config1["name"] = "Beer" config2["name"] = "Milk" config1["state_topic"] = "sensor/state1" @@ -802,8 +833,8 @@ async def test_discovery_update_binary_sensor_template( hass, mqtt_mock_entry_no_yaml_config, caplog ): """Test update of discovered binary_sensor.""" - config1 = copy.deepcopy(DEFAULT_CONFIG[binary_sensor.DOMAIN]) - config2 = copy.deepcopy(DEFAULT_CONFIG[binary_sensor.DOMAIN]) + config1 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[binary_sensor.DOMAIN]) + config2 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[binary_sensor.DOMAIN]) config1["name"] = "Beer" config2["name"] = "Milk" config1["state_topic"] = "sensor/state1" @@ -861,7 +892,7 @@ async def test_encoding_subscribable_topics( mqtt_mock_entry_with_yaml_config, caplog, binary_sensor.DOMAIN, - DEFAULT_CONFIG[binary_sensor.DOMAIN], + DEFAULT_CONFIG_LEGACY[binary_sensor.DOMAIN], topic, value, attribute, @@ -873,7 +904,7 @@ async def test_discovery_update_unchanged_binary_sensor( hass, mqtt_mock_entry_no_yaml_config, caplog ): """Test update of discovered binary_sensor.""" - config1 = copy.deepcopy(DEFAULT_CONFIG[binary_sensor.DOMAIN]) + config1 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[binary_sensor.DOMAIN]) config1["name"] = "Beer" data1 = json.dumps(config1) @@ -908,42 +939,60 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT binary sensor device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock_entry_no_yaml_config, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + binary_sensor.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT binary sensor device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock_entry_no_yaml_config, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + binary_sensor.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock_entry_no_yaml_config, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + binary_sensor.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock_entry_no_yaml_config, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + binary_sensor.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock_entry_with_yaml_config, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + binary_sensor.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock_entry_no_yaml_config, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + binary_sensor.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) @@ -953,7 +1002,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): hass, mqtt_mock_entry_no_yaml_config, binary_sensor.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, None, ) @@ -961,7 +1010,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = binary_sensor.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) @@ -970,7 +1019,7 @@ async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_pa async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = binary_sensor.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) @@ -991,11 +1040,11 @@ async def test_cleanup_triggers_and_restoring_state( ): """Test cleanup old triggers at reloading and restoring the state.""" domain = binary_sensor.DOMAIN - config1 = copy.deepcopy(DEFAULT_CONFIG[domain]) + config1 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) config1["name"] = "test1" config1["expire_after"] = 30 config1["state_topic"] = "test-topic1" - config2 = copy.deepcopy(DEFAULT_CONFIG[domain]) + config2 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) config2["name"] = "test2" config2["expire_after"] = 5 config2["state_topic"] = "test-topic2" @@ -1053,7 +1102,7 @@ async def test_skip_restoring_state_with_over_due_expire_trigger( freezer.move_to("2022-02-02 12:02:00+01:00") domain = binary_sensor.DOMAIN - config3 = copy.deepcopy(DEFAULT_CONFIG[domain]) + config3 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][domain]) config3["name"] = "test3" config3["expire_after"] = 10 config3["state_topic"] = "test-topic3" @@ -1065,17 +1114,18 @@ async def test_skip_restoring_state_with_over_due_expire_trigger( ) mock_restore_cache(hass, (fake_state,)) - with assert_setup_component(1, domain): - assert await async_setup_component(hass, domain, {domain: config3}) - await hass.async_block_till_done() - await mqtt_mock_entry_with_yaml_config() + assert await async_setup_component( + hass, mqtt.DOMAIN, {mqtt.DOMAIN: {domain: config3}} + ) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() assert "Skip state recovery after reload for binary_sensor.test3" in caplog.text async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = binary_sensor.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG[platform]) + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) config["name"] = "test" del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) @@ -1085,7 +1135,20 @@ async def test_setup_manual_entity_from_yaml(hass): async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): """Test unloading the config entry.""" domain = binary_sensor.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) + + +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +async def test_setup_with_legacy_schema(hass, mqtt_mock_entry_with_yaml_config): + """Test a setup with deprecated yaml platform schema.""" + domain = binary_sensor.DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) + config["name"] = "test" + assert await async_setup_component(hass, domain, {domain: config}) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + assert hass.states.get(f"{domain}.test") is not None diff --git a/tests/components/mqtt/test_button.py b/tests/components/mqtt/test_button.py index 68db846c91c..1274c700800 100644 --- a/tests/components/mqtt/test_button.py +++ b/tests/components/mqtt/test_button.py @@ -4,7 +4,7 @@ from unittest.mock import patch import pytest -from homeassistant.components import button +from homeassistant.components import button, mqtt from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, @@ -43,9 +43,14 @@ from .test_common import ( ) DEFAULT_CONFIG = { - button.DOMAIN: {"platform": "mqtt", "name": "test", "command_topic": "test-topic"} + mqtt.DOMAIN: {button.DOMAIN: {"name": "test", "command_topic": "test-topic"}} } +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +DEFAULT_CONFIG_LEGACY = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) +DEFAULT_CONFIG_LEGACY[button.DOMAIN]["platform"] = mqtt.DOMAIN + @pytest.fixture(autouse=True) def button_platform_only(): @@ -59,15 +64,16 @@ async def test_sending_mqtt_commands(hass, mqtt_mock_entry_with_yaml_config): """Test the sending MQTT commands.""" assert await async_setup_component( hass, - button.DOMAIN, + mqtt.DOMAIN, { - button.DOMAIN: { - "command_topic": "command-topic", - "name": "test", - "object_id": "test_button", - "payload_press": "beer press", - "platform": "mqtt", - "qos": "2", + mqtt.DOMAIN: { + button.DOMAIN: { + "command_topic": "command-topic", + "name": "test", + "object_id": "test_button", + "payload_press": "beer press", + "qos": "2", + } } }, ) @@ -97,14 +103,15 @@ async def test_command_template(hass, mqtt_mock_entry_with_yaml_config): """Test the sending of MQTT commands through a command template.""" assert await async_setup_component( hass, - button.DOMAIN, + mqtt.DOMAIN, { - button.DOMAIN: { - "command_topic": "command-topic", - "command_template": '{ "{{ value }}": "{{ entity_id }}" }', - "name": "test", - "payload_press": "milky_way_press", - "platform": "mqtt", + mqtt.DOMAIN: { + button.DOMAIN: { + "command_topic": "command-topic", + "command_template": '{ "{{ value }}": "{{ entity_id }}" }', + "name": "test", + "payload_press": "milky_way_press", + } } }, ) @@ -133,14 +140,14 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, button.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, button.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock_entry_with_yaml_config, button.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, button.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -193,7 +200,7 @@ async def test_setting_attribute_via_mqtt_json_message( ): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock_entry_with_yaml_config, button.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, button.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -202,14 +209,14 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( ): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, mqtt_mock_entry_no_yaml_config, button.DOMAIN, DEFAULT_CONFIG, None + hass, mqtt_mock_entry_no_yaml_config, button.DOMAIN, DEFAULT_CONFIG_LEGACY, None ) async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock_entry_with_yaml_config, button.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, button.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -218,7 +225,11 @@ async def test_update_with_json_attrs_not_dict( ): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock_entry_with_yaml_config, caplog, button.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + button.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) @@ -227,14 +238,22 @@ async def test_update_with_json_attrs_bad_JSON( ): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock_entry_with_yaml_config, caplog, button.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + button.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock_entry_no_yaml_config, caplog, button.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + button.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) @@ -271,8 +290,8 @@ async def test_discovery_removal_button(hass, mqtt_mock_entry_no_yaml_config, ca async def test_discovery_update_button(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered button.""" - config1 = copy.deepcopy(DEFAULT_CONFIG[button.DOMAIN]) - config2 = copy.deepcopy(DEFAULT_CONFIG[button.DOMAIN]) + config1 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[button.DOMAIN]) + config2 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[button.DOMAIN]) config1["name"] = "Beer" config2["name"] = "Milk" @@ -321,35 +340,35 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT button device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock_entry_no_yaml_config, button.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, button.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT button device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock_entry_no_yaml_config, button.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, button.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock_entry_no_yaml_config, button.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, button.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock_entry_no_yaml_config, button.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, button.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock_entry_no_yaml_config, button.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, button.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -359,59 +378,54 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): hass, mqtt_mock_entry_no_yaml_config, button.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, button.SERVICE_PRESS, command_payload="PRESS", state_topic=None, ) -async def test_invalid_device_class(hass, mqtt_mock_entry_no_yaml_config): +async def test_invalid_device_class(hass): """Test device_class option with invalid value.""" - assert await async_setup_component( + assert not await async_setup_component( hass, - button.DOMAIN, + mqtt.DOMAIN, { - button.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "test-topic", - "device_class": "foobarnotreal", + mqtt.DOMAIN: { + button.DOMAIN: { + "name": "test", + "state_topic": "test-topic", + "device_class": "foobarnotreal", + } } }, ) - await hass.async_block_till_done() - await mqtt_mock_entry_no_yaml_config() - - state = hass.states.get("button.test") - assert state is None async def test_valid_device_class(hass, mqtt_mock_entry_with_yaml_config): """Test device_class option with valid values.""" assert await async_setup_component( hass, - button.DOMAIN, + mqtt.DOMAIN, { - button.DOMAIN: [ - { - "platform": "mqtt", - "name": "Test 1", - "command_topic": "test-topic", - "device_class": "update", - }, - { - "platform": "mqtt", - "name": "Test 2", - "command_topic": "test-topic", - "device_class": "restart", - }, - { - "platform": "mqtt", - "name": "Test 3", - "command_topic": "test-topic", - }, - ] + mqtt.DOMAIN: { + button.DOMAIN: [ + { + "name": "Test 1", + "command_topic": "test-topic", + "device_class": "update", + }, + { + "name": "Test 2", + "command_topic": "test-topic", + "device_class": "restart", + }, + { + "name": "Test 3", + "command_topic": "test-topic", + }, + ] + } }, ) await hass.async_block_till_done() @@ -443,7 +457,7 @@ async def test_publishing_with_custom_encoding( ): """Test publishing MQTT payload with different encoding.""" domain = button.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_publishing_with_custom_encoding( hass, @@ -462,7 +476,7 @@ async def test_publishing_with_custom_encoding( async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = button.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) @@ -471,14 +485,14 @@ async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_pa async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = button.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = button.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG[platform]) + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) config["name"] = "test" del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) @@ -488,7 +502,20 @@ async def test_setup_manual_entity_from_yaml(hass): async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): """Test unloading the config entry.""" domain = button.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) + + +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +async def test_setup_with_legacy_schema(hass, mqtt_mock_entry_with_yaml_config): + """Test a setup with deprecated yaml platform schema.""" + domain = button.DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) + config["name"] = "test" + assert await async_setup_component(hass, domain, {domain: config}) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + assert hass.states.get(f"{domain}.test") is not None diff --git a/tests/components/mqtt/test_camera.py b/tests/components/mqtt/test_camera.py index a76025a608a..4d0b2fbfca0 100644 --- a/tests/components/mqtt/test_camera.py +++ b/tests/components/mqtt/test_camera.py @@ -7,7 +7,7 @@ from unittest.mock import patch import pytest -from homeassistant.components import camera +from homeassistant.components import camera, mqtt from homeassistant.components.mqtt.camera import MQTT_CAMERA_ATTRIBUTES_BLOCKED from homeassistant.const import Platform from homeassistant.setup import async_setup_component @@ -43,9 +43,12 @@ from .test_common import ( from tests.common import async_fire_mqtt_message -DEFAULT_CONFIG = { - camera.DOMAIN: {"platform": "mqtt", "name": "test", "topic": "test_topic"} -} +DEFAULT_CONFIG = {mqtt.DOMAIN: {camera.DOMAIN: {"name": "test", "topic": "test_topic"}}} + +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +DEFAULT_CONFIG_LEGACY = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) +DEFAULT_CONFIG_LEGACY[camera.DOMAIN]["platform"] = mqtt.DOMAIN @pytest.fixture(autouse=True) @@ -62,8 +65,8 @@ async def test_run_camera_setup( topic = "test/camera" await async_setup_component( hass, - "camera", - {"camera": {"platform": "mqtt", "topic": topic, "name": "Test Camera"}}, + mqtt.DOMAIN, + {mqtt.DOMAIN: {camera.DOMAIN: {"topic": topic, "name": "Test Camera"}}}, ) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -86,13 +89,14 @@ async def test_run_camera_b64_encoded( topic = "test/camera" await async_setup_component( hass, - "camera", + mqtt.DOMAIN, { - "camera": { - "platform": "mqtt", - "topic": topic, - "name": "Test Camera", - "encoding": "b64", + mqtt.DOMAIN: { + camera.DOMAIN: { + "topic": topic, + "name": "Test Camera", + "encoding": "b64", + } } }, ) @@ -110,7 +114,7 @@ async def test_run_camera_b64_encoded( assert body == "grass" -# Using CONF_ENCODING to set b64 encoding for images is deprecated Home Assistant 2022.9, use CONF_IMAGE_ENCODING instead +# Using CONF_ENCODING to set b64 encoding for images is deprecated in Home Assistant 2022.9, use CONF_IMAGE_ENCODING instead async def test_legacy_camera_b64_encoded_with_availability( hass, hass_client_no_auth, mqtt_mock_entry_with_yaml_config ): @@ -119,14 +123,15 @@ async def test_legacy_camera_b64_encoded_with_availability( topic_availability = "test/camera_availability" await async_setup_component( hass, - "camera", + mqtt.DOMAIN, { - "camera": { - "platform": "mqtt", - "topic": topic, - "name": "Test Camera", - "encoding": "b64", - "availability": {"topic": topic_availability}, + mqtt.DOMAIN: { + camera.DOMAIN: { + "topic": topic, + "name": "Test Camera", + "encoding": "b64", + "availability": {"topic": topic_availability}, + } } }, ) @@ -155,15 +160,16 @@ async def test_camera_b64_encoded_with_availability( topic_availability = "test/camera_availability" await async_setup_component( hass, - "camera", + mqtt.DOMAIN, { - "camera": { - "platform": "mqtt", - "topic": topic, - "name": "Test Camera", - "encoding": "utf-8", - "image_encoding": "b64", - "availability": {"topic": topic_availability}, + mqtt.DOMAIN: { + "camera": { + "topic": topic, + "name": "Test Camera", + "encoding": "utf-8", + "image_encoding": "b64", + "availability": {"topic": topic_availability}, + } } }, ) @@ -189,28 +195,28 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, camera.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, camera.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock_entry_with_yaml_config, camera.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, camera.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock_entry_with_yaml_config, camera.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, camera.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock_entry_with_yaml_config, camera.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, camera.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -219,7 +225,7 @@ async def test_setting_attribute_via_mqtt_json_message( ): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock_entry_with_yaml_config, camera.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, camera.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -231,7 +237,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( hass, mqtt_mock_entry_no_yaml_config, camera.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, MQTT_CAMERA_ATTRIBUTES_BLOCKED, ) @@ -239,7 +245,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock_entry_with_yaml_config, camera.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, camera.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -248,7 +254,11 @@ async def test_update_with_json_attrs_not_dict( ): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock_entry_with_yaml_config, caplog, camera.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + camera.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) @@ -257,14 +267,22 @@ async def test_update_with_json_attrs_bad_JSON( ): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock_entry_with_yaml_config, caplog, camera.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + camera.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock_entry_no_yaml_config, caplog, camera.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + camera.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) @@ -293,7 +311,7 @@ async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): async def test_discovery_removal_camera(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered camera.""" - data = json.dumps(DEFAULT_CONFIG[camera.DOMAIN]) + data = json.dumps(DEFAULT_CONFIG_LEGACY[camera.DOMAIN]) await help_test_discovery_removal( hass, mqtt_mock_entry_no_yaml_config, caplog, camera.DOMAIN, data ) @@ -341,28 +359,28 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT camera device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock_entry_no_yaml_config, camera.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, camera.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT camera device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock_entry_no_yaml_config, camera.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, camera.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock_entry_no_yaml_config, camera.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, camera.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock_entry_no_yaml_config, camera.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, camera.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -372,7 +390,7 @@ async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_co hass, mqtt_mock_entry_with_yaml_config, camera.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, ["test_topic"], ) @@ -380,7 +398,7 @@ async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_co async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock_entry_no_yaml_config, camera.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, camera.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -390,7 +408,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): hass, mqtt_mock_entry_no_yaml_config, camera.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, None, state_topic="test_topic", state_payload=b"ON", @@ -400,7 +418,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = camera.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) @@ -409,14 +427,14 @@ async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_pa async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = camera.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = camera.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG[platform]) + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) config["name"] = "test" del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) @@ -426,7 +444,20 @@ async def test_setup_manual_entity_from_yaml(hass): async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): """Test unloading the config entry.""" domain = camera.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) + + +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +async def test_setup_with_legacy_schema(hass, mqtt_mock_entry_with_yaml_config): + """Test a setup with deprecated yaml platform schema.""" + domain = camera.DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) + config["name"] = "test" + assert await async_setup_component(hass, domain, {domain: config}) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + assert hass.states.get(f"{domain}.test") is not None diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index d5164e85718..14bd9084abe 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -6,7 +6,7 @@ from unittest.mock import call, patch import pytest import voluptuous as vol -from homeassistant.components import climate +from homeassistant.components import climate, mqtt from homeassistant.components.climate import DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP from homeassistant.components.climate.const import ( ATTR_AUX_HEAT, @@ -16,10 +16,9 @@ from homeassistant.components.climate.const import ( ATTR_SWING_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, - CURRENT_HVAC_ACTIONS, - DOMAIN as CLIMATE_DOMAIN, PRESET_ECO, ClimateEntityFeature, + HVACAction, HVACMode, ) from homeassistant.components.mqtt.climate import MQTT_CLIMATE_ATTRIBUTES_BLOCKED @@ -62,30 +61,37 @@ from tests.components.climate import common ENTITY_CLIMATE = "climate.test" + DEFAULT_CONFIG = { - CLIMATE_DOMAIN: { - "platform": "mqtt", - "name": "test", - "mode_command_topic": "mode-topic", - "temperature_command_topic": "temperature-topic", - "temperature_low_command_topic": "temperature-low-topic", - "temperature_high_command_topic": "temperature-high-topic", - "fan_mode_command_topic": "fan-mode-topic", - "swing_mode_command_topic": "swing-mode-topic", - "aux_command_topic": "aux-topic", - "preset_mode_command_topic": "preset-mode-topic", - "preset_modes": [ - "eco", - "away", - "boost", - "comfort", - "home", - "sleep", - "activity", - ], + mqtt.DOMAIN: { + climate.DOMAIN: { + "name": "test", + "mode_command_topic": "mode-topic", + "temperature_command_topic": "temperature-topic", + "temperature_low_command_topic": "temperature-low-topic", + "temperature_high_command_topic": "temperature-high-topic", + "fan_mode_command_topic": "fan-mode-topic", + "swing_mode_command_topic": "swing-mode-topic", + "aux_command_topic": "aux-topic", + "preset_mode_command_topic": "preset-mode-topic", + "preset_modes": [ + "eco", + "away", + "boost", + "comfort", + "home", + "sleep", + "activity", + ], + } } } +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +DEFAULT_CONFIG_LEGACY = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) +DEFAULT_CONFIG_LEGACY[climate.DOMAIN]["platform"] = mqtt.DOMAIN + @pytest.fixture(autouse=True) def climate_platform_only(): @@ -96,7 +102,7 @@ def climate_platform_only(): async def test_setup_params(hass, mqtt_mock_entry_with_yaml_config): """Test the initial parameters.""" - assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) + assert await async_setup_component(hass, mqtt.DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -109,18 +115,14 @@ async def test_setup_params(hass, mqtt_mock_entry_with_yaml_config): assert state.attributes.get("max_temp") == DEFAULT_MAX_TEMP -async def test_preset_none_in_preset_modes( - hass, mqtt_mock_entry_no_yaml_config, caplog -): +async def test_preset_none_in_preset_modes(hass, caplog): """Test the preset mode payload reset configuration.""" - config = copy.deepcopy(DEFAULT_CONFIG[CLIMATE_DOMAIN]) + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][climate.DOMAIN]) config["preset_modes"].append("none") - assert await async_setup_component(hass, CLIMATE_DOMAIN, {CLIMATE_DOMAIN: config}) - await hass.async_block_till_done() - await mqtt_mock_entry_no_yaml_config() - assert "Invalid config for [climate.mqtt]: not a valid value" in caplog.text - state = hass.states.get(ENTITY_CLIMATE) - assert state is None + assert not await async_setup_component( + hass, mqtt.DOMAIN, {mqtt.DOMAIN: {climate.DOMAIN: config}} + ) + assert "Invalid config for [mqtt]: not a valid value" in caplog.text # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 @@ -136,22 +138,19 @@ async def test_preset_none_in_preset_modes( ("hold_mode_state_template", "{{ value_json }}"), ], ) -async def test_preset_modes_deprecation_guard( - hass, mqtt_mock_entry_no_yaml_config, caplog, parameter, config_value -): +async def test_preset_modes_deprecation_guard(hass, caplog, parameter, config_value): """Test the configuration for invalid legacy parameters.""" - config = copy.deepcopy(DEFAULT_CONFIG[CLIMATE_DOMAIN]) + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][climate.DOMAIN]) config[parameter] = config_value - assert await async_setup_component(hass, CLIMATE_DOMAIN, {CLIMATE_DOMAIN: config}) - await hass.async_block_till_done() - await mqtt_mock_entry_no_yaml_config() - state = hass.states.get(ENTITY_CLIMATE) - assert state is None + assert not await async_setup_component( + hass, mqtt.DOMAIN, {mqtt.DOMAIN: {climate.DOMAIN: config}} + ) + assert f"[{parameter}] is an invalid option for [mqtt]. Check: mqtt->mqtt->climate->0->{parameter}" async def test_supported_features(hass, mqtt_mock_entry_with_yaml_config): """Test the supported_features.""" - assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) + assert await async_setup_component(hass, mqtt.DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -170,7 +169,7 @@ async def test_supported_features(hass, mqtt_mock_entry_with_yaml_config): async def test_get_hvac_modes(hass, mqtt_mock_entry_with_yaml_config): """Test that the operation list returns the correct modes.""" - assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) + assert await async_setup_component(hass, mqtt.DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -193,7 +192,7 @@ async def test_set_operation_bad_attr_and_state( Also check the state. """ - assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) + assert await async_setup_component(hass, mqtt.DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -210,7 +209,7 @@ async def test_set_operation_bad_attr_and_state( async def test_set_operation(hass, mqtt_mock_entry_with_yaml_config): """Test setting of new operation mode.""" - assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) + assert await async_setup_component(hass, mqtt.DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -225,9 +224,9 @@ async def test_set_operation(hass, mqtt_mock_entry_with_yaml_config): async def test_set_operation_pessimistic(hass, mqtt_mock_entry_with_yaml_config): """Test setting operation mode in pessimistic mode.""" - config = copy.deepcopy(DEFAULT_CONFIG) + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) config["climate"]["mode_state_topic"] = "mode-state" - assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -249,9 +248,9 @@ async def test_set_operation_pessimistic(hass, mqtt_mock_entry_with_yaml_config) async def test_set_operation_with_power_command(hass, mqtt_mock_entry_with_yaml_config): """Test setting of new operation mode with power command enabled.""" - config = copy.deepcopy(DEFAULT_CONFIG) + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) config["climate"]["power_command_topic"] = "power-command" - assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -276,7 +275,7 @@ async def test_set_operation_with_power_command(hass, mqtt_mock_entry_with_yaml_ async def test_set_fan_mode_bad_attr(hass, mqtt_mock_entry_with_yaml_config, caplog): """Test setting fan mode without required attribute.""" - assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) + assert await async_setup_component(hass, mqtt.DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -293,9 +292,9 @@ async def test_set_fan_mode_bad_attr(hass, mqtt_mock_entry_with_yaml_config, cap async def test_set_fan_mode_pessimistic(hass, mqtt_mock_entry_with_yaml_config): """Test setting of new fan mode in pessimistic mode.""" - config = copy.deepcopy(DEFAULT_CONFIG) + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) config["climate"]["fan_mode_state_topic"] = "fan-state" - assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -317,7 +316,7 @@ async def test_set_fan_mode_pessimistic(hass, mqtt_mock_entry_with_yaml_config): async def test_set_fan_mode(hass, mqtt_mock_entry_with_yaml_config): """Test setting of new fan mode.""" - assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) + assert await async_setup_component(hass, mqtt.DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -331,7 +330,7 @@ async def test_set_fan_mode(hass, mqtt_mock_entry_with_yaml_config): async def test_set_swing_mode_bad_attr(hass, mqtt_mock_entry_with_yaml_config, caplog): """Test setting swing mode without required attribute.""" - assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) + assert await async_setup_component(hass, mqtt.DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -348,9 +347,9 @@ async def test_set_swing_mode_bad_attr(hass, mqtt_mock_entry_with_yaml_config, c async def test_set_swing_pessimistic(hass, mqtt_mock_entry_with_yaml_config): """Test setting swing mode in pessimistic mode.""" - config = copy.deepcopy(DEFAULT_CONFIG) + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) config["climate"]["swing_mode_state_topic"] = "swing-state" - assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -372,7 +371,7 @@ async def test_set_swing_pessimistic(hass, mqtt_mock_entry_with_yaml_config): async def test_set_swing(hass, mqtt_mock_entry_with_yaml_config): """Test setting of new swing mode.""" - assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) + assert await async_setup_component(hass, mqtt.DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -386,7 +385,7 @@ async def test_set_swing(hass, mqtt_mock_entry_with_yaml_config): async def test_set_target_temperature(hass, mqtt_mock_entry_with_yaml_config): """Test setting the target temperature.""" - assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) + assert await async_setup_component(hass, mqtt.DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -425,9 +424,9 @@ async def test_set_target_temperature_pessimistic( hass, mqtt_mock_entry_with_yaml_config ): """Test setting the target temperature.""" - config = copy.deepcopy(DEFAULT_CONFIG) + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) config["climate"]["temperature_state_topic"] = "temperature-state" - assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -449,7 +448,7 @@ async def test_set_target_temperature_pessimistic( async def test_set_target_temperature_low_high(hass, mqtt_mock_entry_with_yaml_config): """Test setting the low/high target temperature.""" - assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) + assert await async_setup_component(hass, mqtt.DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -467,10 +466,10 @@ async def test_set_target_temperature_low_highpessimistic( hass, mqtt_mock_entry_with_yaml_config ): """Test setting the low/high target temperature.""" - config = copy.deepcopy(DEFAULT_CONFIG) + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) config["climate"]["temperature_low_state_topic"] = "temperature-low-state" config["climate"]["temperature_high_state_topic"] = "temperature-high-state" - assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -505,9 +504,9 @@ async def test_set_target_temperature_low_highpessimistic( async def test_receive_mqtt_temperature(hass, mqtt_mock_entry_with_yaml_config): """Test getting the current temperature via MQTT.""" - config = copy.deepcopy(DEFAULT_CONFIG) + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) config["climate"]["current_temperature_topic"] = "current_temperature" - assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -518,9 +517,9 @@ async def test_receive_mqtt_temperature(hass, mqtt_mock_entry_with_yaml_config): async def test_handle_action_received(hass, mqtt_mock_entry_with_yaml_config): """Test getting the action received via MQTT.""" - config = copy.deepcopy(DEFAULT_CONFIG) + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) config["climate"]["action_topic"] = "action" - assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -531,7 +530,7 @@ async def test_handle_action_received(hass, mqtt_mock_entry_with_yaml_config): assert hvac_action is None # Redefine actions according to https://developers.home-assistant.io/docs/core/entity/climate/#hvac-action actions = ["off", "heating", "cooling", "drying", "idle", "fan"] - assert all(elem in actions for elem in CURRENT_HVAC_ACTIONS) + assert all(elem in actions for elem in HVACAction) for action in actions: async_fire_mqtt_message(hass, "action", action) state = hass.states.get(ENTITY_CLIMATE) @@ -543,8 +542,8 @@ async def test_set_preset_mode_optimistic( hass, mqtt_mock_entry_with_yaml_config, caplog ): """Test setting of the preset mode.""" - config = copy.deepcopy(DEFAULT_CONFIG) - assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -591,9 +590,9 @@ async def test_set_preset_mode_pessimistic( hass, mqtt_mock_entry_with_yaml_config, caplog ): """Test setting of the preset mode.""" - config = copy.deepcopy(DEFAULT_CONFIG) + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) config["climate"]["preset_mode_state_topic"] = "preset-mode-state" - assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -636,9 +635,9 @@ async def test_set_preset_mode_pessimistic( async def test_set_aux_pessimistic(hass, mqtt_mock_entry_with_yaml_config): """Test setting of the aux heating in pessimistic mode.""" - config = copy.deepcopy(DEFAULT_CONFIG) + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) config["climate"]["aux_state_topic"] = "aux-state" - assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -664,7 +663,7 @@ async def test_set_aux_pessimistic(hass, mqtt_mock_entry_with_yaml_config): async def test_set_aux(hass, mqtt_mock_entry_with_yaml_config): """Test setting of the aux heating.""" - assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) + assert await async_setup_component(hass, mqtt.DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -687,28 +686,28 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, climate.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock_entry_with_yaml_config, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, climate.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock_entry_with_yaml_config, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, climate.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock_entry_with_yaml_config, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, climate.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -716,13 +715,13 @@ async def test_get_target_temperature_low_high_with_templates( hass, mqtt_mock_entry_with_yaml_config, caplog ): """Test getting temperature high/low with templates.""" - config = copy.deepcopy(DEFAULT_CONFIG) + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) config["climate"]["temperature_low_state_topic"] = "temperature-state" config["climate"]["temperature_high_state_topic"] = "temperature-state" config["climate"]["temperature_low_state_template"] = "{{ value_json.temp_low }}" config["climate"]["temperature_high_state_template"] = "{{ value_json.temp_high }}" - assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -751,7 +750,7 @@ async def test_get_target_temperature_low_high_with_templates( async def test_get_with_templates(hass, mqtt_mock_entry_with_yaml_config, caplog): """Test getting various attributes with templates.""" - config = copy.deepcopy(DEFAULT_CONFIG) + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) # By default, just unquote the JSON-strings config["climate"]["value_template"] = "{{ value_json }}" config["climate"]["action_template"] = "{{ value_json }}" @@ -768,7 +767,7 @@ async def test_get_with_templates(hass, mqtt_mock_entry_with_yaml_config, caplog config["climate"]["aux_state_topic"] = "aux-state" config["climate"]["current_temperature_topic"] = "current-temperature" config["climate"]["preset_mode_state_topic"] = "current-preset-mode" - assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -850,7 +849,7 @@ async def test_get_with_templates(hass, mqtt_mock_entry_with_yaml_config, caplog async def test_set_and_templates(hass, mqtt_mock_entry_with_yaml_config, caplog): """Test setting various attributes with templates.""" - config = copy.deepcopy(DEFAULT_CONFIG) + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) # Create simple templates config["climate"]["fan_mode_command_template"] = "fan_mode: {{ value }}" config["climate"]["preset_mode_command_template"] = "preset_mode: {{ value }}" @@ -860,7 +859,7 @@ async def test_set_and_templates(hass, mqtt_mock_entry_with_yaml_config, caplog) config["climate"]["temperature_high_command_template"] = "temp_hi: {{ value }}" config["climate"]["temperature_low_command_template"] = "temp_lo: {{ value }}" - assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -928,10 +927,10 @@ async def test_set_and_templates(hass, mqtt_mock_entry_with_yaml_config, caplog) async def test_min_temp_custom(hass, mqtt_mock_entry_with_yaml_config): """Test a custom min temp.""" - config = copy.deepcopy(DEFAULT_CONFIG) + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) config["climate"]["min_temp"] = 26 - assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -944,10 +943,10 @@ async def test_min_temp_custom(hass, mqtt_mock_entry_with_yaml_config): async def test_max_temp_custom(hass, mqtt_mock_entry_with_yaml_config): """Test a custom max temp.""" - config = copy.deepcopy(DEFAULT_CONFIG) + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) config["climate"]["max_temp"] = 60 - assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -960,10 +959,10 @@ async def test_max_temp_custom(hass, mqtt_mock_entry_with_yaml_config): async def test_temp_step_custom(hass, mqtt_mock_entry_with_yaml_config): """Test a custom temp step.""" - config = copy.deepcopy(DEFAULT_CONFIG) + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) config["climate"]["temp_step"] = 0.01 - assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -976,11 +975,11 @@ async def test_temp_step_custom(hass, mqtt_mock_entry_with_yaml_config): async def test_temperature_unit(hass, mqtt_mock_entry_with_yaml_config): """Test that setting temperature unit converts temperature values.""" - config = copy.deepcopy(DEFAULT_CONFIG) + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) config["climate"]["temperature_unit"] = "F" config["climate"]["current_temperature_topic"] = "current_temperature" - assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -995,7 +994,7 @@ async def test_setting_attribute_via_mqtt_json_message( ): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock_entry_with_yaml_config, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, climate.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -1006,8 +1005,8 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( await help_test_setting_blocked_attribute_via_mqtt_json_message( hass, mqtt_mock_entry_no_yaml_config, - CLIMATE_DOMAIN, - DEFAULT_CONFIG, + climate.DOMAIN, + DEFAULT_CONFIG_LEGACY, MQTT_CLIMATE_ATTRIBUTES_BLOCKED, ) @@ -1015,7 +1014,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock_entry_with_yaml_config, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, climate.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -1024,7 +1023,11 @@ async def test_update_with_json_attrs_not_dict( ): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock_entry_with_yaml_config, caplog, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + climate.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) @@ -1033,21 +1036,29 @@ async def test_update_with_json_attrs_bad_JSON( ): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock_entry_with_yaml_config, caplog, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + climate.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock_entry_no_yaml_config, caplog, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + climate.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one climate per unique_id.""" config = { - CLIMATE_DOMAIN: [ + climate.DOMAIN: [ { "platform": "mqtt", "name": "Test 1", @@ -1065,7 +1076,7 @@ async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): ] } await help_test_unique_id( - hass, mqtt_mock_entry_with_yaml_config, CLIMATE_DOMAIN, config + hass, mqtt_mock_entry_with_yaml_config, climate.DOMAIN, config ) @@ -1095,12 +1106,12 @@ async def test_encoding_subscribable_topics( attribute_value, ): """Test handling of incoming encoded payload.""" - config = copy.deepcopy(DEFAULT_CONFIG[CLIMATE_DOMAIN]) + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[climate.DOMAIN]) await help_test_encoding_subscribable_topics( hass, mqtt_mock_entry_with_yaml_config, caplog, - CLIMATE_DOMAIN, + climate.DOMAIN, config, topic, value, @@ -1111,9 +1122,9 @@ async def test_encoding_subscribable_topics( async def test_discovery_removal_climate(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered climate.""" - data = json.dumps(DEFAULT_CONFIG[CLIMATE_DOMAIN]) + data = json.dumps(DEFAULT_CONFIG_LEGACY[climate.DOMAIN]) await help_test_discovery_removal( - hass, mqtt_mock_entry_no_yaml_config, caplog, CLIMATE_DOMAIN, data + hass, mqtt_mock_entry_no_yaml_config, caplog, climate.DOMAIN, data ) @@ -1122,7 +1133,7 @@ async def test_discovery_update_climate(hass, mqtt_mock_entry_no_yaml_config, ca config1 = {"name": "Beer"} config2 = {"name": "Milk"} await help_test_discovery_update( - hass, mqtt_mock_entry_no_yaml_config, caplog, CLIMATE_DOMAIN, config1, config2 + hass, mqtt_mock_entry_no_yaml_config, caplog, climate.DOMAIN, config1, config2 ) @@ -1138,7 +1149,7 @@ async def test_discovery_update_unchanged_climate( hass, mqtt_mock_entry_no_yaml_config, caplog, - CLIMATE_DOMAIN, + climate.DOMAIN, data1, discovery_update, ) @@ -1150,42 +1161,42 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): data1 = '{ "name": "Beer", "power_command_topic": "test_topic#" }' data2 = '{ "name": "Milk", "power_command_topic": "test_topic" }' await help_test_discovery_broken( - hass, mqtt_mock_entry_no_yaml_config, caplog, CLIMATE_DOMAIN, data1, data2 + hass, mqtt_mock_entry_no_yaml_config, caplog, climate.DOMAIN, data1, data2 ) async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT climate device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock_entry_no_yaml_config, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, climate.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT climate device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock_entry_no_yaml_config, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, climate.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock_entry_no_yaml_config, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, climate.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock_entry_no_yaml_config, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, climate.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" config = { - CLIMATE_DOMAIN: { + climate.DOMAIN: { "platform": "mqtt", "name": "test", "mode_state_topic": "test-topic", @@ -1195,7 +1206,7 @@ async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_co await help_test_entity_id_update_subscriptions( hass, mqtt_mock_entry_with_yaml_config, - CLIMATE_DOMAIN, + climate.DOMAIN, config, ["test-topic", "avty-topic"], ) @@ -1204,14 +1215,14 @@ async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_co async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock_entry_no_yaml_config, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, climate.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" config = { - CLIMATE_DOMAIN: { + climate.DOMAIN: { "platform": "mqtt", "name": "test", "mode_command_topic": "command-topic", @@ -1221,7 +1232,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): await help_test_entity_debug_info_message( hass, mqtt_mock_entry_no_yaml_config, - CLIMATE_DOMAIN, + climate.DOMAIN, config, climate.SERVICE_TURN_ON, command_topic="command-topic", @@ -1232,7 +1243,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): async def test_precision_default(hass, mqtt_mock_entry_with_yaml_config): """Test that setting precision to tenths works as intended.""" - assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) + assert await async_setup_component(hass, mqtt.DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -1246,9 +1257,9 @@ async def test_precision_default(hass, mqtt_mock_entry_with_yaml_config): async def test_precision_halves(hass, mqtt_mock_entry_with_yaml_config): """Test that setting precision to halves works as intended.""" - config = copy.deepcopy(DEFAULT_CONFIG) + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) config["climate"]["precision"] = 0.5 - assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -1262,9 +1273,9 @@ async def test_precision_halves(hass, mqtt_mock_entry_with_yaml_config): async def test_precision_whole(hass, mqtt_mock_entry_with_yaml_config): """Test that setting precision to whole works as intended.""" - config = copy.deepcopy(DEFAULT_CONFIG) + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) config["climate"]["precision"] = 1.0 - assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -1364,7 +1375,7 @@ async def test_publishing_with_custom_encoding( ): """Test publishing MQTT payload with different encoding.""" domain = climate.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG[domain]) + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) if topic != "preset_mode_command_topic": del config["preset_mode_command_topic"] del config["preset_modes"] @@ -1385,8 +1396,8 @@ async def test_publishing_with_custom_encoding( async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" - domain = CLIMATE_DOMAIN - config = DEFAULT_CONFIG[domain] + domain = climate.DOMAIN + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) @@ -1394,15 +1405,15 @@ async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_pa async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" - domain = CLIMATE_DOMAIN - config = DEFAULT_CONFIG[domain] + domain = climate.DOMAIN + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" - platform = CLIMATE_DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG[platform]) + platform = climate.DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) config["name"] = "test" del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) @@ -1412,7 +1423,20 @@ async def test_setup_manual_entity_from_yaml(hass): async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): """Test unloading the config entry.""" domain = climate.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) + + +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +async def test_setup_with_legacy_schema(hass, mqtt_mock_entry_with_yaml_config): + """Test a setup with deprecated yaml platform schema.""" + domain = climate.DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) + config["name"] = "test" + assert await async_setup_component(hass, domain, {domain: config}) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + assert hass.states.get(f"{domain}.test") is not None diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index b1c162073ed..a91f0ecc6ca 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -5,7 +5,7 @@ from unittest.mock import patch import pytest -from homeassistant.components import cover +from homeassistant.components import cover, mqtt from homeassistant.components.cover import ( ATTR_CURRENT_POSITION, ATTR_CURRENT_TILT_POSITION, @@ -81,9 +81,14 @@ from .test_common import ( from tests.common import async_fire_mqtt_message DEFAULT_CONFIG = { - cover.DOMAIN: {"platform": "mqtt", "name": "test", "state_topic": "test-topic"} + mqtt.DOMAIN: {cover.DOMAIN: {"name": "test", "state_topic": "test-topic"}} } +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +DEFAULT_CONFIG_LEGACY = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) +DEFAULT_CONFIG_LEGACY[cover.DOMAIN]["platform"] = mqtt.DOMAIN + @pytest.fixture(autouse=True) def cover_platform_only(): @@ -96,17 +101,18 @@ async def test_state_via_state_topic(hass, mqtt_mock_entry_with_yaml_config): """Test the controlling state via topic.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "qos": 0, - "payload_open": "OPEN", - "payload_close": "CLOSE", - "payload_stop": "STOP", + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "qos": 0, + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + } } }, ) @@ -134,19 +140,20 @@ async def test_opening_and_closing_state_via_custom_state_payload( """Test the controlling opening and closing state via a custom payload.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "qos": 0, - "payload_open": "OPEN", - "payload_close": "CLOSE", - "payload_stop": "STOP", - "state_opening": "34", - "state_closing": "--43", + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "qos": 0, + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + "state_opening": "34", + "state_closing": "--43", + } } }, ) @@ -179,18 +186,19 @@ async def test_open_closed_state_from_position_optimistic( """Test the state after setting the position using optimistic mode.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "position_topic": "position-topic", - "set_position_topic": "set-position-topic", - "qos": 0, - "payload_open": "OPEN", - "payload_close": "CLOSE", - "payload_stop": "STOP", - "optimistic": True, + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "position_topic": "position-topic", + "set_position_topic": "set-position-topic", + "qos": 0, + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + "optimistic": True, + } } }, ) @@ -227,19 +235,20 @@ async def test_position_via_position_topic(hass, mqtt_mock_entry_with_yaml_confi """Test the controlling state via topic.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "position_topic": "get-position-topic", - "position_open": 100, - "position_closed": 0, - "command_topic": "command-topic", - "qos": 0, - "payload_open": "OPEN", - "payload_close": "CLOSE", - "payload_stop": "STOP", + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "position_topic": "get-position-topic", + "position_open": 100, + "position_closed": 0, + "command_topic": "command-topic", + "qos": 0, + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + } } }, ) @@ -265,20 +274,21 @@ async def test_state_via_template(hass, mqtt_mock_entry_with_yaml_config): """Test the controlling state via topic.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "qos": 0, - "value_template": "\ + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "qos": 0, + "value_template": "\ {% if (value | multiply(0.01) | int) == 0 %}\ closed\ {% else %}\ open\ {% endif %}", + } } }, ) @@ -303,20 +313,21 @@ async def test_state_via_template_and_entity_id(hass, mqtt_mock_entry_with_yaml_ """Test the controlling state via topic.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "qos": 0, - "value_template": '\ + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "qos": 0, + "value_template": '\ {% if value == "open" or value == "closed" %}\ {{ value }}\ {% else %}\ {{ states(entity_id) }}\ {% endif %}', + } } }, ) @@ -345,15 +356,16 @@ async def test_state_via_template_with_json_value( """Test the controlling state via topic with JSON value.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "qos": 0, - "value_template": "{{ value_json.Var1 }}", + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "qos": 0, + "value_template": "{{ value_json.Var1 }}", + } } }, ) @@ -387,20 +399,21 @@ async def test_position_via_template_and_entity_id( """Test the controlling state via topic.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "position_topic": "get-position-topic", - "command_topic": "command-topic", - "qos": 0, - "position_template": '\ + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "position_topic": "get-position-topic", + "command_topic": "command-topic", + "qos": 0, + "position_template": '\ {% if state_attr(entity_id, "current_position") == None %}\ {{ value }}\ {% else %}\ {{ state_attr(entity_id, "current_position") + value | int }}\ {% endif %}', + } } }, ) @@ -442,8 +455,8 @@ async def test_optimistic_flag( """Test assumed_state is set correctly.""" assert await async_setup_component( hass, - cover.DOMAIN, - {cover.DOMAIN: {**config, "platform": "mqtt", "name": "test", "qos": 0}}, + mqtt.DOMAIN, + {mqtt.DOMAIN: {cover.DOMAIN: {**config, "name": "test", "qos": 0}}}, ) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -460,13 +473,14 @@ async def test_optimistic_state_change(hass, mqtt_mock_entry_with_yaml_config): """Test changing state optimistically.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "command-topic", - "qos": 0, + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "command_topic": "command-topic", + "qos": 0, + } } }, ) @@ -519,15 +533,16 @@ async def test_optimistic_state_change_with_position( """Test changing state optimistically.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "optimistic": True, - "command_topic": "command-topic", - "position_topic": "position-topic", - "qos": 0, + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "optimistic": True, + "command_topic": "command-topic", + "position_topic": "position-topic", + "qos": 0, + } } }, ) @@ -583,14 +598,15 @@ async def test_send_open_cover_command(hass, mqtt_mock_entry_with_yaml_config): """Test the sending of open_cover.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "qos": 2, + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "qos": 2, + } } }, ) @@ -613,14 +629,15 @@ async def test_send_close_cover_command(hass, mqtt_mock_entry_with_yaml_config): """Test the sending of close_cover.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "qos": 2, + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "qos": 2, + } } }, ) @@ -643,14 +660,15 @@ async def test_send_stop__cover_command(hass, mqtt_mock_entry_with_yaml_config): """Test the sending of stop_cover.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "qos": 2, + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "qos": 2, + } } }, ) @@ -673,18 +691,19 @@ async def test_current_cover_position(hass, mqtt_mock_entry_with_yaml_config): """Test the current cover position.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "position_topic": "get-position-topic", - "command_topic": "command-topic", - "position_open": 100, - "position_closed": 0, - "payload_open": "OPEN", - "payload_close": "CLOSE", - "payload_stop": "STOP", + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "position_topic": "get-position-topic", + "command_topic": "command-topic", + "position_open": 100, + "position_closed": 0, + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + } } }, ) @@ -725,18 +744,19 @@ async def test_current_cover_position_inverted(hass, mqtt_mock_entry_with_yaml_c """Test the current cover position.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "position_topic": "get-position-topic", - "command_topic": "command-topic", - "position_open": 0, - "position_closed": 100, - "payload_open": "OPEN", - "payload_close": "CLOSE", - "payload_stop": "STOP", + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "position_topic": "get-position-topic", + "command_topic": "command-topic", + "position_open": 0, + "position_closed": 100, + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + } } }, ) @@ -784,44 +804,45 @@ async def test_current_cover_position_inverted(hass, mqtt_mock_entry_with_yaml_c assert hass.states.get("cover.test").state == STATE_CLOSED -async def test_optimistic_position(hass, mqtt_mock_entry_no_yaml_config): +async def test_optimistic_position(hass, caplog): """Test optimistic position is not supported.""" - assert await async_setup_component( + assert not await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "command-topic", - "set_position_topic": "set-position-topic", + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "command_topic": "command-topic", + "set_position_topic": "set-position-topic", + } } }, ) - await hass.async_block_till_done() - await mqtt_mock_entry_no_yaml_config() - - state = hass.states.get("cover.test") - assert state is None + assert ( + "Invalid config for [mqtt]: 'set_position_topic' must be set together with 'position_topic'" + in caplog.text + ) async def test_position_update(hass, mqtt_mock_entry_with_yaml_config): """Test cover position update from received MQTT message.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "position_topic": "get-position-topic", - "command_topic": "command-topic", - "set_position_topic": "set-position-topic", - "position_open": 100, - "position_closed": 0, - "payload_open": "OPEN", - "payload_close": "CLOSE", - "payload_stop": "STOP", + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "position_topic": "get-position-topic", + "command_topic": "command-topic", + "set_position_topic": "set-position-topic", + "position_open": 100, + "position_closed": 0, + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + } } }, ) @@ -853,20 +874,21 @@ async def test_set_position_templated( """Test setting cover position via template.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "position_topic": "get-position-topic", - "command_topic": "command-topic", - "position_open": 100, - "position_closed": 0, - "set_position_topic": "set-position-topic", - "set_position_template": pos_template, - "payload_open": "OPEN", - "payload_close": "CLOSE", - "payload_stop": "STOP", + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "position_topic": "get-position-topic", + "command_topic": "command-topic", + "position_open": 100, + "position_closed": 0, + "set_position_topic": "set-position-topic", + "set_position_template": pos_template, + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + } } }, ) @@ -891,17 +913,17 @@ async def test_set_position_templated_and_attributes( """Test setting cover position via template and using entities attributes.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "position_topic": "get-position-topic", - "command_topic": "command-topic", - "position_open": 100, - "position_closed": 0, - "set_position_topic": "set-position-topic", - "set_position_template": '\ + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "position_topic": "get-position-topic", + "command_topic": "command-topic", + "position_open": 100, + "position_closed": 0, + "set_position_topic": "set-position-topic", + "set_position_template": '\ {% if position > 99 %}\ {% if state_attr(entity_id, "current_position") == None %}\ {{ 5 }}\ @@ -911,9 +933,10 @@ async def test_set_position_templated_and_attributes( {% else %}\ {{ 42 }}\ {% endif %}', - "payload_open": "OPEN", - "payload_close": "CLOSE", - "payload_stop": "STOP", + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + } } }, ) @@ -934,22 +957,23 @@ async def test_set_tilt_templated(hass, mqtt_mock_entry_with_yaml_config): """Test setting cover tilt position via template.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "position_topic": "get-position-topic", - "command_topic": "command-topic", - "tilt_command_topic": "tilt-command-topic", - "position_open": 100, - "position_closed": 0, - "set_position_topic": "set-position-topic", - "set_position_template": "{{position-1}}", - "tilt_command_template": "{{tilt_position+1}}", - "payload_open": "OPEN", - "payload_close": "CLOSE", - "payload_stop": "STOP", + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "position_topic": "get-position-topic", + "command_topic": "command-topic", + "tilt_command_topic": "tilt-command-topic", + "position_open": 100, + "position_closed": 0, + "set_position_topic": "set-position-topic", + "set_position_template": "{{position-1}}", + "tilt_command_template": "{{tilt_position+1}}", + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + } } }, ) @@ -974,26 +998,27 @@ async def test_set_tilt_templated_and_attributes( """Test setting cover tilt position via template and using entities attributes.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "position_topic": "get-position-topic", - "command_topic": "command-topic", - "tilt_command_topic": "tilt-command-topic", - "position_open": 100, - "position_closed": 0, - "set_position_topic": "set-position-topic", - "set_position_template": "{{position-1}}", - "tilt_command_template": "{" - '"entity_id": "{{ entity_id }}",' - '"value": {{ value }},' - '"tilt_position": {{ tilt_position }}' - "}", - "payload_open": "OPEN", - "payload_close": "CLOSE", - "payload_stop": "STOP", + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "position_topic": "get-position-topic", + "command_topic": "command-topic", + "tilt_command_topic": "tilt-command-topic", + "position_open": 100, + "position_closed": 0, + "set_position_topic": "set-position-topic", + "set_position_template": "{{position-1}}", + "tilt_command_template": "{" + '"entity_id": "{{ entity_id }}",' + '"value": {{ value }},' + '"tilt_position": {{ tilt_position }}' + "}", + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + } } }, ) @@ -1061,17 +1086,18 @@ async def test_set_position_untemplated(hass, mqtt_mock_entry_with_yaml_config): """Test setting cover position via template.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "position_topic": "state-topic", - "command_topic": "command-topic", - "set_position_topic": "position-topic", - "payload_open": "OPEN", - "payload_close": "CLOSE", - "payload_stop": "STOP", + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "position_topic": "state-topic", + "command_topic": "command-topic", + "set_position_topic": "position-topic", + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + } } }, ) @@ -1094,19 +1120,20 @@ async def test_set_position_untemplated_custom_percentage_range( """Test setting cover position via template.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "position_topic": "state-topic", - "command_topic": "command-topic", - "set_position_topic": "position-topic", - "position_open": 0, - "position_closed": 100, - "payload_open": "OPEN", - "payload_close": "CLOSE", - "payload_stop": "STOP", + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "position_topic": "state-topic", + "command_topic": "command-topic", + "set_position_topic": "position-topic", + "position_open": 0, + "position_closed": 100, + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + } } }, ) @@ -1127,17 +1154,18 @@ async def test_no_command_topic(hass, mqtt_mock_entry_with_yaml_config): """Test with no command topic.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "qos": 0, - "payload_open": "OPEN", - "payload_close": "CLOSE", - "payload_stop": "STOP", - "tilt_command_topic": "tilt-command", - "tilt_status_topic": "tilt-status", + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "qos": 0, + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + "tilt_command_topic": "tilt-command", + "tilt_status_topic": "tilt-status", + } } }, ) @@ -1151,16 +1179,17 @@ async def test_no_payload_close(hass, mqtt_mock_entry_with_yaml_config): """Test with no close payload.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "command-topic", - "qos": 0, - "payload_open": "OPEN", - "payload_close": None, - "payload_stop": "STOP", + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "command_topic": "command-topic", + "qos": 0, + "payload_open": "OPEN", + "payload_close": None, + "payload_stop": "STOP", + } } }, ) @@ -1174,16 +1203,17 @@ async def test_no_payload_open(hass, mqtt_mock_entry_with_yaml_config): """Test with no open payload.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "command-topic", - "qos": 0, - "payload_open": None, - "payload_close": "CLOSE", - "payload_stop": "STOP", + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "command_topic": "command-topic", + "qos": 0, + "payload_open": None, + "payload_close": "CLOSE", + "payload_stop": "STOP", + } } }, ) @@ -1197,16 +1227,17 @@ async def test_no_payload_stop(hass, mqtt_mock_entry_with_yaml_config): """Test with no stop payload.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "command-topic", - "qos": 0, - "payload_open": "OPEN", - "payload_close": "CLOSE", - "payload_stop": None, + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "command_topic": "command-topic", + "qos": 0, + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": None, + } } }, ) @@ -1220,18 +1251,19 @@ async def test_with_command_topic_and_tilt(hass, mqtt_mock_entry_with_yaml_confi """Test with command topic and tilt config.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "command_topic": "test", - "platform": "mqtt", - "name": "test", - "qos": 0, - "payload_open": "OPEN", - "payload_close": "CLOSE", - "payload_stop": "STOP", - "tilt_command_topic": "tilt-command", - "tilt_status_topic": "tilt-status", + mqtt.DOMAIN: { + cover.DOMAIN: { + "command_topic": "test", + "name": "test", + "qos": 0, + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + "tilt_command_topic": "tilt-command", + "tilt_status_topic": "tilt-status", + } } }, ) @@ -1245,19 +1277,20 @@ async def test_tilt_defaults(hass, mqtt_mock_entry_with_yaml_config): """Test the defaults.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "qos": 0, - "payload_open": "OPEN", - "payload_close": "CLOSE", - "payload_stop": "STOP", - "tilt_command_topic": "tilt-command", - "tilt_status_topic": "tilt-status", + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "qos": 0, + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + "tilt_command_topic": "tilt-command", + "tilt_status_topic": "tilt-status", + } } }, ) @@ -1273,19 +1306,20 @@ async def test_tilt_via_invocation_defaults(hass, mqtt_mock_entry_with_yaml_conf """Test tilt defaults on close/open.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "qos": 0, - "payload_open": "OPEN", - "payload_close": "CLOSE", - "payload_stop": "STOP", - "tilt_command_topic": "tilt-command-topic", - "tilt_status_topic": "tilt-status-topic", + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "qos": 0, + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + "tilt_command_topic": "tilt-command-topic", + "tilt_status_topic": "tilt-status-topic", + } } }, ) @@ -1356,21 +1390,22 @@ async def test_tilt_given_value(hass, mqtt_mock_entry_with_yaml_config): """Test tilting to a given value.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "qos": 0, - "payload_open": "OPEN", - "payload_close": "CLOSE", - "payload_stop": "STOP", - "tilt_command_topic": "tilt-command-topic", - "tilt_status_topic": "tilt-status-topic", - "tilt_opened_value": 80, - "tilt_closed_value": 25, + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "qos": 0, + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + "tilt_command_topic": "tilt-command-topic", + "tilt_status_topic": "tilt-status-topic", + "tilt_opened_value": 80, + "tilt_closed_value": 25, + } } }, ) @@ -1445,22 +1480,23 @@ async def test_tilt_given_value_optimistic(hass, mqtt_mock_entry_with_yaml_confi """Test tilting to a given value.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "qos": 0, - "payload_open": "OPEN", - "payload_close": "CLOSE", - "payload_stop": "STOP", - "tilt_command_topic": "tilt-command-topic", - "tilt_status_topic": "tilt-status-topic", - "tilt_opened_value": 80, - "tilt_closed_value": 25, - "tilt_optimistic": True, + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "qos": 0, + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + "tilt_command_topic": "tilt-command-topic", + "tilt_status_topic": "tilt-status-topic", + "tilt_opened_value": 80, + "tilt_closed_value": 25, + "tilt_optimistic": True, + } } }, ) @@ -1522,24 +1558,25 @@ async def test_tilt_given_value_altered_range(hass, mqtt_mock_entry_with_yaml_co """Test tilting to a given value.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "qos": 0, - "payload_open": "OPEN", - "payload_close": "CLOSE", - "payload_stop": "STOP", - "tilt_command_topic": "tilt-command-topic", - "tilt_status_topic": "tilt-status-topic", - "tilt_opened_value": 25, - "tilt_closed_value": 0, - "tilt_min": 0, - "tilt_max": 50, - "tilt_optimistic": True, + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "qos": 0, + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + "tilt_command_topic": "tilt-command-topic", + "tilt_status_topic": "tilt-status-topic", + "tilt_opened_value": 25, + "tilt_closed_value": 0, + "tilt_min": 0, + "tilt_max": 50, + "tilt_optimistic": True, + } } }, ) @@ -1599,19 +1636,20 @@ async def test_tilt_via_topic(hass, mqtt_mock_entry_with_yaml_config): """Test tilt by updating status via MQTT.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "qos": 0, - "payload_open": "OPEN", - "payload_close": "CLOSE", - "payload_stop": "STOP", - "tilt_command_topic": "tilt-command-topic", - "tilt_status_topic": "tilt-status-topic", + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "qos": 0, + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + "tilt_command_topic": "tilt-command-topic", + "tilt_status_topic": "tilt-status-topic", + } } }, ) @@ -1637,22 +1675,23 @@ async def test_tilt_via_topic_template(hass, mqtt_mock_entry_with_yaml_config): """Test tilt by updating status via MQTT and template.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "qos": 0, - "payload_open": "OPEN", - "payload_close": "CLOSE", - "payload_stop": "STOP", - "tilt_command_topic": "tilt-command-topic", - "tilt_status_topic": "tilt-status-topic", - "tilt_status_template": "{{ (value | multiply(0.01)) | int }}", - "tilt_opened_value": 400, - "tilt_closed_value": 125, + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "qos": 0, + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + "tilt_command_topic": "tilt-command-topic", + "tilt_status_topic": "tilt-status-topic", + "tilt_status_template": "{{ (value | multiply(0.01)) | int }}", + "tilt_opened_value": 400, + "tilt_closed_value": 125, + } } }, ) @@ -1680,22 +1719,23 @@ async def test_tilt_via_topic_template_json_value( """Test tilt by updating status via MQTT and template with JSON value.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "qos": 0, - "payload_open": "OPEN", - "payload_close": "CLOSE", - "payload_stop": "STOP", - "tilt_command_topic": "tilt-command-topic", - "tilt_status_topic": "tilt-status-topic", - "tilt_status_template": "{{ value_json.Var1 }}", - "tilt_opened_value": 400, - "tilt_closed_value": 125, + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "qos": 0, + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + "tilt_command_topic": "tilt-command-topic", + "tilt_status_topic": "tilt-status-topic", + "tilt_status_template": "{{ value_json.Var1 }}", + "tilt_opened_value": 400, + "tilt_closed_value": 125, + } } }, ) @@ -1727,21 +1767,22 @@ async def test_tilt_via_topic_altered_range(hass, mqtt_mock_entry_with_yaml_conf """Test tilt status via MQTT with altered tilt range.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "qos": 0, - "payload_open": "OPEN", - "payload_close": "CLOSE", - "payload_stop": "STOP", - "tilt_command_topic": "tilt-command-topic", - "tilt_status_topic": "tilt-status-topic", - "tilt_min": 0, - "tilt_max": 50, + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "qos": 0, + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + "tilt_command_topic": "tilt-command-topic", + "tilt_status_topic": "tilt-status-topic", + "tilt_min": 0, + "tilt_max": 50, + } } }, ) @@ -1776,21 +1817,22 @@ async def test_tilt_status_out_of_range_warning( """Test tilt status via MQTT tilt out of range warning message.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "qos": 0, - "payload_open": "OPEN", - "payload_close": "CLOSE", - "payload_stop": "STOP", - "tilt_command_topic": "tilt-command-topic", - "tilt_status_topic": "tilt-status-topic", - "tilt_min": 0, - "tilt_max": 50, + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "qos": 0, + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + "tilt_command_topic": "tilt-command-topic", + "tilt_status_topic": "tilt-status-topic", + "tilt_min": 0, + "tilt_max": 50, + } } }, ) @@ -1810,21 +1852,22 @@ async def test_tilt_status_not_numeric_warning( """Test tilt status via MQTT tilt not numeric warning message.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "qos": 0, - "payload_open": "OPEN", - "payload_close": "CLOSE", - "payload_stop": "STOP", - "tilt_command_topic": "tilt-command-topic", - "tilt_status_topic": "tilt-status-topic", - "tilt_min": 0, - "tilt_max": 50, + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "qos": 0, + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + "tilt_command_topic": "tilt-command-topic", + "tilt_status_topic": "tilt-status-topic", + "tilt_min": 0, + "tilt_max": 50, + } } }, ) @@ -1842,21 +1885,22 @@ async def test_tilt_via_topic_altered_range_inverted( """Test tilt status via MQTT with altered tilt range and inverted tilt position.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "qos": 0, - "payload_open": "OPEN", - "payload_close": "CLOSE", - "payload_stop": "STOP", - "tilt_command_topic": "tilt-command-topic", - "tilt_status_topic": "tilt-status-topic", - "tilt_min": 50, - "tilt_max": 0, + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "qos": 0, + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + "tilt_command_topic": "tilt-command-topic", + "tilt_status_topic": "tilt-status-topic", + "tilt_min": 50, + "tilt_max": 0, + } } }, ) @@ -1891,24 +1935,25 @@ async def test_tilt_via_topic_template_altered_range( """Test tilt status via MQTT and template with altered tilt range.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "qos": 0, - "payload_open": "OPEN", - "payload_close": "CLOSE", - "payload_stop": "STOP", - "tilt_command_topic": "tilt-command-topic", - "tilt_status_topic": "tilt-status-topic", - "tilt_status_template": "{{ (value | multiply(0.01)) | int }}", - "tilt_opened_value": 400, - "tilt_closed_value": 125, - "tilt_min": 0, - "tilt_max": 50, + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "qos": 0, + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + "tilt_command_topic": "tilt-command-topic", + "tilt_status_topic": "tilt-status-topic", + "tilt_status_template": "{{ (value | multiply(0.01)) | int }}", + "tilt_opened_value": 400, + "tilt_closed_value": 125, + "tilt_min": 0, + "tilt_max": 50, + } } }, ) @@ -1941,19 +1986,20 @@ async def test_tilt_position(hass, mqtt_mock_entry_with_yaml_config): """Test tilt via method invocation.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "qos": 0, - "payload_open": "OPEN", - "payload_close": "CLOSE", - "payload_stop": "STOP", - "tilt_command_topic": "tilt-command-topic", - "tilt_status_topic": "tilt-status-topic", + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "qos": 0, + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + "tilt_command_topic": "tilt-command-topic", + "tilt_status_topic": "tilt-status-topic", + } } }, ) @@ -1976,20 +2022,21 @@ async def test_tilt_position_templated(hass, mqtt_mock_entry_with_yaml_config): """Test tilt position via template.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "qos": 0, - "payload_open": "OPEN", - "payload_close": "CLOSE", - "payload_stop": "STOP", - "tilt_command_topic": "tilt-command-topic", - "tilt_status_topic": "tilt-status-topic", - "tilt_command_template": "{{100-32}}", + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "qos": 0, + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + "tilt_command_topic": "tilt-command-topic", + "tilt_status_topic": "tilt-status-topic", + "tilt_command_template": "{{100-32}}", + } } }, ) @@ -2012,23 +2059,24 @@ async def test_tilt_position_altered_range(hass, mqtt_mock_entry_with_yaml_confi """Test tilt via method invocation with altered range.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "qos": 0, - "payload_open": "OPEN", - "payload_close": "CLOSE", - "payload_stop": "STOP", - "tilt_command_topic": "tilt-command-topic", - "tilt_status_topic": "tilt-status-topic", - "tilt_opened_value": 400, - "tilt_closed_value": 125, - "tilt_min": 0, - "tilt_max": 50, + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "qos": 0, + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + "tilt_command_topic": "tilt-command-topic", + "tilt_status_topic": "tilt-status-topic", + "tilt_opened_value": 400, + "tilt_closed_value": 125, + "tilt_min": 0, + "tilt_max": 50, + } } }, ) @@ -2396,28 +2444,28 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, cover.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock_entry_with_yaml_config, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, cover.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock_entry_with_yaml_config, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, cover.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock_entry_with_yaml_config, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, cover.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -2425,13 +2473,14 @@ async def test_valid_device_class(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of a valid device class.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "device_class": "garage", - "state_topic": "test-topic", + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "device_class": "garage", + "state_topic": "test-topic", + } } }, ) @@ -2442,25 +2491,22 @@ async def test_valid_device_class(hass, mqtt_mock_entry_with_yaml_config): assert state.attributes.get("device_class") == "garage" -async def test_invalid_device_class(hass, mqtt_mock_entry_no_yaml_config): +async def test_invalid_device_class(hass, caplog): """Test the setting of an invalid device class.""" - assert await async_setup_component( + assert not await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "device_class": "abc123", - "state_topic": "test-topic", + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "device_class": "abc123", + "state_topic": "test-topic", + } } }, ) - await hass.async_block_till_done() - await mqtt_mock_entry_no_yaml_config() - - state = hass.states.get("cover.test") - assert state is None + assert "Invalid config for [mqtt]: expected CoverDeviceClass" in caplog.text async def test_setting_attribute_via_mqtt_json_message( @@ -2468,7 +2514,7 @@ async def test_setting_attribute_via_mqtt_json_message( ): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock_entry_with_yaml_config, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, cover.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -2480,7 +2526,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( hass, mqtt_mock_entry_no_yaml_config, cover.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, MQTT_COVER_ATTRIBUTES_BLOCKED, ) @@ -2488,7 +2534,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock_entry_with_yaml_config, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, cover.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -2497,7 +2543,11 @@ async def test_update_with_json_attrs_not_dict( ): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock_entry_with_yaml_config, caplog, cover.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + cover.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) @@ -2506,14 +2556,22 @@ async def test_update_with_json_attrs_bad_json( ): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock_entry_with_yaml_config, caplog, cover.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + cover.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock_entry_no_yaml_config, caplog, cover.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + cover.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) @@ -2588,42 +2646,42 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT cover device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock_entry_no_yaml_config, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, cover.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT cover device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock_entry_no_yaml_config, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, cover.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock_entry_no_yaml_config, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, cover.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock_entry_no_yaml_config, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, cover.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock_entry_with_yaml_config, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, cover.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock_entry_no_yaml_config, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, cover.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -2633,7 +2691,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): hass, mqtt_mock_entry_no_yaml_config, cover.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, SERVICE_OPEN_COVER, command_payload="OPEN", ) @@ -2645,19 +2703,20 @@ async def test_state_and_position_topics_state_not_set_via_position_topic( """Test state is not set via position topic when both state and position topics are set.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "position_topic": "get-position-topic", - "position_open": 100, - "position_closed": 0, - "state_open": "OPEN", - "state_closed": "CLOSE", - "command_topic": "command-topic", - "qos": 0, + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "position_topic": "get-position-topic", + "position_open": 100, + "position_closed": 0, + "state_open": "OPEN", + "state_closed": "CLOSE", + "command_topic": "command-topic", + "qos": 0, + } } }, ) @@ -2705,20 +2764,21 @@ async def test_set_state_via_position_using_stopped_state( """Test the controlling state via position topic using stopped state.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "position_topic": "get-position-topic", - "position_open": 100, - "position_closed": 0, - "state_open": "OPEN", - "state_closed": "CLOSE", - "state_stopped": "STOPPED", - "command_topic": "command-topic", - "qos": 0, + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "position_topic": "get-position-topic", + "position_open": 100, + "position_closed": 0, + "state_open": "OPEN", + "state_closed": "CLOSE", + "state_stopped": "STOPPED", + "command_topic": "command-topic", + "qos": 0, + } } }, ) @@ -2761,16 +2821,17 @@ async def test_position_via_position_topic_template( """Test position by updating status via position template.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "set_position_topic": "set-position-topic", - "position_topic": "get-position-topic", - "position_template": "{{ (value | multiply(0.01)) | int }}", + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "set_position_topic": "set-position-topic", + "position_topic": "get-position-topic", + "position_template": "{{ (value | multiply(0.01)) | int }}", + } } }, ) @@ -2798,16 +2859,17 @@ async def test_position_via_position_topic_template_json_value( """Test position by updating status via position template with a JSON value.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "set_position_topic": "set-position-topic", - "position_topic": "get-position-topic", - "position_template": "{{ value_json.Var1 }}", + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "set_position_topic": "set-position-topic", + "position_topic": "get-position-topic", + "position_template": "{{ value_json.Var1 }}", + } } }, ) @@ -2839,21 +2901,22 @@ async def test_position_template_with_entity_id(hass, mqtt_mock_entry_with_yaml_ """Test position by updating status via position template.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "set_position_topic": "set-position-topic", - "position_topic": "get-position-topic", - "position_template": '\ + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "set_position_topic": "set-position-topic", + "position_topic": "get-position-topic", + "position_template": '\ {% if state_attr(entity_id, "current_position") != None %}\ {{ value | int + state_attr(entity_id, "current_position") }} \ {% else %} \ {{ value }} \ {% endif %}', + } } }, ) @@ -2881,16 +2944,17 @@ async def test_position_via_position_topic_template_return_json( """Test position by updating status via position template and returning json.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "set_position_topic": "set-position-topic", - "position_topic": "get-position-topic", - "position_template": '{{ {"position" : value} | tojson }}', + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "set_position_topic": "set-position-topic", + "position_topic": "get-position-topic", + "position_template": '{{ {"position" : value} | tojson }}', + } } }, ) @@ -2911,16 +2975,17 @@ async def test_position_via_position_topic_template_return_json_warning( """Test position by updating status via position template returning json without position attribute.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "set_position_topic": "set-position-topic", - "position_topic": "get-position-topic", - "position_template": '{{ {"pos" : value} | tojson }}', + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "set_position_topic": "set-position-topic", + "position_topic": "get-position-topic", + "position_template": '{{ {"pos" : value} | tojson }}', + } } }, ) @@ -2941,17 +3006,18 @@ async def test_position_and_tilt_via_position_topic_template_return_json( """Test position and tilt by updating the position via position template.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "set_position_topic": "set-position-topic", - "position_topic": "get-position-topic", - "position_template": '\ + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "set_position_topic": "set-position-topic", + "position_topic": "get-position-topic", + "position_template": '\ {{ {"position" : value, "tilt_position" : (value | int / 2)| int } | tojson }}', + } } }, ) @@ -2984,27 +3050,28 @@ async def test_position_via_position_topic_template_all_variables( """Test position by updating status via position template.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "set_position_topic": "set-position-topic", - "position_topic": "get-position-topic", - "tilt_command_topic": "tilt-command-topic", - "position_open": 99, - "position_closed": 1, - "tilt_min": 11, - "tilt_max": 22, - "position_template": "\ + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "set_position_topic": "set-position-topic", + "position_topic": "get-position-topic", + "tilt_command_topic": "tilt-command-topic", + "position_open": 99, + "position_closed": 1, + "tilt_min": 11, + "tilt_max": 22, + "position_template": "\ {% if value | int < tilt_max %}\ {{ tilt_min }}\ {% endif %}\ {% if value | int > position_closed %}\ {{ position_open }}\ {% endif %}", + } } }, ) @@ -3031,20 +3098,21 @@ async def test_set_state_via_stopped_state_no_position_topic( """Test the controlling state via stopped state when no position topic.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "state_open": "OPEN", - "state_closed": "CLOSE", - "state_stopped": "STOPPED", - "state_opening": "OPENING", - "state_closing": "CLOSING", - "command_topic": "command-topic", - "qos": 0, - "optimistic": False, + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "state_open": "OPEN", + "state_closed": "CLOSE", + "state_stopped": "STOPPED", + "state_opening": "OPENING", + "state_closing": "CLOSING", + "command_topic": "command-topic", + "qos": 0, + "optimistic": False, + } } }, ) @@ -3083,16 +3151,17 @@ async def test_position_via_position_topic_template_return_invalid_json( """Test position by updating status via position template and returning invalid json.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "set_position_topic": "set-position-topic", - "position_topic": "get-position-topic", - "position_template": '{{ {"position" : invalid_json} }}', + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "set_position_topic": "set-position-topic", + "position_topic": "get-position-topic", + "position_template": '{{ {"position" : invalid_json} }}', + } } }, ) @@ -3104,74 +3173,65 @@ async def test_position_via_position_topic_template_return_invalid_json( assert ("Payload '{'position': Undefined}' is not numeric") in caplog.text -async def test_set_position_topic_without_get_position_topic_error( - hass, caplog, mqtt_mock_entry_no_yaml_config -): +async def test_set_position_topic_without_get_position_topic_error(hass, caplog): """Test error when set_position_topic is used without position_topic.""" - assert await async_setup_component( + assert not await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "command-topic", - "set_position_topic": "set-position-topic", - "value_template": "{{100-62}}", + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "command_topic": "command-topic", + "set_position_topic": "set-position-topic", + "value_template": "{{100-62}}", + } } }, ) - await hass.async_block_till_done() - await mqtt_mock_entry_no_yaml_config() - assert ( f"'{CONF_SET_POSITION_TOPIC}' must be set together with '{CONF_GET_POSITION_TOPIC}'." ) in caplog.text async def test_value_template_without_state_topic_error( - hass, caplog, mqtt_mock_entry_no_yaml_config + hass, + caplog, ): """Test error when value_template is used and state_topic is missing.""" - assert await async_setup_component( + assert not await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "command-topic", - "value_template": "{{100-62}}", + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "command_topic": "command-topic", + "value_template": "{{100-62}}", + } } }, ) - await hass.async_block_till_done() - await mqtt_mock_entry_no_yaml_config() - assert ( f"'{CONF_VALUE_TEMPLATE}' must be set together with '{CONF_STATE_TOPIC}'." ) in caplog.text -async def test_position_template_without_position_topic_error( - hass, caplog, mqtt_mock_entry_no_yaml_config -): +async def test_position_template_without_position_topic_error(hass, caplog): """Test error when position_template is used and position_topic is missing.""" - assert await async_setup_component( + assert not await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "command-topic", - "position_template": "{{100-52}}", + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "command_topic": "command-topic", + "position_template": "{{100-52}}", + } } }, ) - await hass.async_block_till_done() - await mqtt_mock_entry_no_yaml_config() - assert ( f"'{CONF_GET_POSITION_TEMPLATE}' must be set together with '{CONF_GET_POSITION_TOPIC}'." in caplog.text @@ -3179,74 +3239,65 @@ async def test_position_template_without_position_topic_error( async def test_set_position_template_without_set_position_topic( - hass, caplog, mqtt_mock_entry_no_yaml_config + hass, + caplog, ): """Test error when set_position_template is used and set_position_topic is missing.""" - assert await async_setup_component( + assert not await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "command-topic", - "set_position_template": "{{100-42}}", + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "command_topic": "command-topic", + "set_position_template": "{{100-42}}", + } } }, ) - await hass.async_block_till_done() - await mqtt_mock_entry_no_yaml_config() - assert ( f"'{CONF_SET_POSITION_TEMPLATE}' must be set together with '{CONF_SET_POSITION_TOPIC}'." in caplog.text ) -async def test_tilt_command_template_without_tilt_command_topic( - hass, caplog, mqtt_mock_entry_no_yaml_config -): +async def test_tilt_command_template_without_tilt_command_topic(hass, caplog): """Test error when tilt_command_template is used and tilt_command_topic is missing.""" - assert await async_setup_component( + assert not await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "command-topic", - "tilt_command_template": "{{100-32}}", + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "command_topic": "command-topic", + "tilt_command_template": "{{100-32}}", + } } }, ) - await hass.async_block_till_done() - await mqtt_mock_entry_no_yaml_config() - assert ( f"'{CONF_TILT_COMMAND_TEMPLATE}' must be set together with '{CONF_TILT_COMMAND_TOPIC}'." in caplog.text ) -async def test_tilt_status_template_without_tilt_status_topic_topic( - hass, caplog, mqtt_mock_entry_no_yaml_config -): +async def test_tilt_status_template_without_tilt_status_topic_topic(hass, caplog): """Test error when tilt_status_template is used and tilt_status_topic is missing.""" - assert await async_setup_component( + assert not await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "command-topic", - "tilt_status_template": "{{100-22}}", + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "command_topic": "command-topic", + "tilt_status_template": "{{100-22}}", + } } }, ) - await hass.async_block_till_done() - await mqtt_mock_entry_no_yaml_config() - assert ( f"'{CONF_TILT_STATUS_TEMPLATE}' must be set together with '{CONF_TILT_STATUS_TOPIC}'." in caplog.text @@ -3291,7 +3342,7 @@ async def test_publishing_with_custom_encoding( ): """Test publishing MQTT payload with different encoding.""" domain = cover.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] config["position_topic"] = "some-position-topic" await help_test_publishing_with_custom_encoding( @@ -3311,7 +3362,7 @@ async def test_publishing_with_custom_encoding( async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = cover.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) @@ -3320,7 +3371,7 @@ async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_pa async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = cover.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) @@ -3348,7 +3399,7 @@ async def test_encoding_subscribable_topics( mqtt_mock_entry_with_yaml_config, caplog, cover.DOMAIN, - DEFAULT_CONFIG[cover.DOMAIN], + DEFAULT_CONFIG_LEGACY[cover.DOMAIN], topic, value, attribute, @@ -3360,7 +3411,7 @@ async def test_encoding_subscribable_topics( async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = cover.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG[platform]) + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) config["name"] = "test" del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) @@ -3370,7 +3421,20 @@ async def test_setup_manual_entity_from_yaml(hass): async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): """Test unloading the config entry.""" domain = cover.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) + + +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +async def test_setup_with_legacy_schema(hass, mqtt_mock_entry_with_yaml_config): + """Test a setup with deprecated yaml platform schema.""" + domain = cover.DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) + config["name"] = "test" + assert await async_setup_component(hass, domain, {domain: config}) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + assert hass.states.get(f"{domain}.test") is not None diff --git a/tests/components/mqtt/test_device_tracker.py b/tests/components/mqtt/test_device_tracker.py index 6708703ddbb..db6e0a292d7 100644 --- a/tests/components/mqtt/test_device_tracker.py +++ b/tests/components/mqtt/test_device_tracker.py @@ -1,10 +1,11 @@ -"""The tests for the MQTT device tracker platform using configuration.yaml.""" +"""The tests for the MQTT device tracker platform using configuration.yaml with legacy schema.""" import json from unittest.mock import patch import pytest -from homeassistant.components.device_tracker.const import DOMAIN, SOURCE_TYPE_BLUETOOTH +from homeassistant.components import device_tracker +from homeassistant.components.device_tracker import SourceType from homeassistant.config_entries import ConfigEntryDisabler from homeassistant.const import CONF_PLATFORM, STATE_HOME, STATE_NOT_HOME, Platform from homeassistant.setup import async_setup_component @@ -12,7 +13,6 @@ from homeassistant.setup import async_setup_component from .test_common import ( MockConfigEntry, help_test_entry_reload_with_new_config, - help_test_setup_manual_entity_from_yaml, help_test_unload_config_entry, ) @@ -45,7 +45,14 @@ async def test_legacy_ensure_device_tracker_platform_validation( dev_id = "paulus" topic = "/location/paulus" assert await async_setup_component( - hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "mqtt", "devices": {dev_id: topic}}} + hass, + device_tracker.DOMAIN, + { + device_tracker.DOMAIN: { + CONF_PLATFORM: "mqtt", + "devices": {dev_id: topic}, + } + }, ) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -59,13 +66,15 @@ async def test_legacy_new_message( """Test new message.""" await mqtt_mock_entry_no_yaml_config() dev_id = "paulus" - entity_id = f"{DOMAIN}.{dev_id}" + entity_id = f"{device_tracker.DOMAIN}.{dev_id}" topic = "/location/paulus" location = "work" hass.config.components = {"mqtt", "zone"} assert await async_setup_component( - hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "mqtt", "devices": {dev_id: topic}}} + hass, + device_tracker.DOMAIN, + {device_tracker.DOMAIN: {CONF_PLATFORM: "mqtt", "devices": {dev_id: topic}}}, ) async_fire_mqtt_message(hass, topic, location) await hass.async_block_till_done() @@ -79,7 +88,7 @@ async def test_legacy_single_level_wildcard_topic( """Test single level wildcard topic.""" await mqtt_mock_entry_no_yaml_config() dev_id = "paulus" - entity_id = f"{DOMAIN}.{dev_id}" + entity_id = f"{device_tracker.DOMAIN}.{dev_id}" subscription = "/location/+/paulus" topic = "/location/room/paulus" location = "work" @@ -87,8 +96,13 @@ async def test_legacy_single_level_wildcard_topic( hass.config.components = {"mqtt", "zone"} assert await async_setup_component( hass, - DOMAIN, - {DOMAIN: {CONF_PLATFORM: "mqtt", "devices": {dev_id: subscription}}}, + device_tracker.DOMAIN, + { + device_tracker.DOMAIN: { + CONF_PLATFORM: "mqtt", + "devices": {dev_id: subscription}, + } + }, ) async_fire_mqtt_message(hass, topic, location) await hass.async_block_till_done() @@ -102,7 +116,7 @@ async def test_legacy_multi_level_wildcard_topic( """Test multi level wildcard topic.""" await mqtt_mock_entry_no_yaml_config() dev_id = "paulus" - entity_id = f"{DOMAIN}.{dev_id}" + entity_id = f"{device_tracker.DOMAIN}.{dev_id}" subscription = "/location/#" topic = "/location/room/paulus" location = "work" @@ -110,8 +124,13 @@ async def test_legacy_multi_level_wildcard_topic( hass.config.components = {"mqtt", "zone"} assert await async_setup_component( hass, - DOMAIN, - {DOMAIN: {CONF_PLATFORM: "mqtt", "devices": {dev_id: subscription}}}, + device_tracker.DOMAIN, + { + device_tracker.DOMAIN: { + CONF_PLATFORM: "mqtt", + "devices": {dev_id: subscription}, + } + }, ) async_fire_mqtt_message(hass, topic, location) await hass.async_block_till_done() @@ -125,7 +144,7 @@ async def test_legacy_single_level_wildcard_topic_not_matching( """Test not matching single level wildcard topic.""" await mqtt_mock_entry_no_yaml_config() dev_id = "paulus" - entity_id = f"{DOMAIN}.{dev_id}" + entity_id = f"{device_tracker.DOMAIN}.{dev_id}" subscription = "/location/+/paulus" topic = "/location/paulus" location = "work" @@ -133,8 +152,13 @@ async def test_legacy_single_level_wildcard_topic_not_matching( hass.config.components = {"mqtt", "zone"} assert await async_setup_component( hass, - DOMAIN, - {DOMAIN: {CONF_PLATFORM: "mqtt", "devices": {dev_id: subscription}}}, + device_tracker.DOMAIN, + { + device_tracker.DOMAIN: { + CONF_PLATFORM: "mqtt", + "devices": {dev_id: subscription}, + } + }, ) async_fire_mqtt_message(hass, topic, location) await hass.async_block_till_done() @@ -148,7 +172,7 @@ async def test_legacy_multi_level_wildcard_topic_not_matching( """Test not matching multi level wildcard topic.""" await mqtt_mock_entry_no_yaml_config() dev_id = "paulus" - entity_id = f"{DOMAIN}.{dev_id}" + entity_id = f"{device_tracker.DOMAIN}.{dev_id}" subscription = "/location/#" topic = "/somewhere/room/paulus" location = "work" @@ -156,8 +180,13 @@ async def test_legacy_multi_level_wildcard_topic_not_matching( hass.config.components = {"mqtt", "zone"} assert await async_setup_component( hass, - DOMAIN, - {DOMAIN: {CONF_PLATFORM: "mqtt", "devices": {dev_id: subscription}}}, + device_tracker.DOMAIN, + { + device_tracker.DOMAIN: { + CONF_PLATFORM: "mqtt", + "devices": {dev_id: subscription}, + } + }, ) async_fire_mqtt_message(hass, topic, location) await hass.async_block_till_done() @@ -171,7 +200,7 @@ async def test_legacy_matching_custom_payload_for_home_and_not_home( """Test custom payload_home sets state to home and custom payload_not_home sets state to not_home.""" await mqtt_mock_entry_no_yaml_config() dev_id = "paulus" - entity_id = f"{DOMAIN}.{dev_id}" + entity_id = f"{device_tracker.DOMAIN}.{dev_id}" topic = "/location/paulus" payload_home = "present" payload_not_home = "not present" @@ -179,9 +208,9 @@ async def test_legacy_matching_custom_payload_for_home_and_not_home( hass.config.components = {"mqtt", "zone"} assert await async_setup_component( hass, - DOMAIN, + device_tracker.DOMAIN, { - DOMAIN: { + device_tracker.DOMAIN: { CONF_PLATFORM: "mqtt", "devices": {dev_id: topic}, "payload_home": payload_home, @@ -205,7 +234,7 @@ async def test_legacy_not_matching_custom_payload_for_home_and_not_home( """Test not matching payload does not set state to home or not_home.""" await mqtt_mock_entry_no_yaml_config() dev_id = "paulus" - entity_id = f"{DOMAIN}.{dev_id}" + entity_id = f"{device_tracker.DOMAIN}.{dev_id}" topic = "/location/paulus" payload_home = "present" payload_not_home = "not present" @@ -214,9 +243,9 @@ async def test_legacy_not_matching_custom_payload_for_home_and_not_home( hass.config.components = {"mqtt", "zone"} assert await async_setup_component( hass, - DOMAIN, + device_tracker.DOMAIN, { - DOMAIN: { + device_tracker.DOMAIN: { CONF_PLATFORM: "mqtt", "devices": {dev_id: topic}, "payload_home": payload_home, @@ -237,17 +266,17 @@ async def test_legacy_matching_source_type( """Test setting source type.""" await mqtt_mock_entry_no_yaml_config() dev_id = "paulus" - entity_id = f"{DOMAIN}.{dev_id}" + entity_id = f"{device_tracker.DOMAIN}.{dev_id}" topic = "/location/paulus" - source_type = SOURCE_TYPE_BLUETOOTH + source_type = SourceType.BLUETOOTH location = "work" hass.config.components = {"mqtt", "zone"} assert await async_setup_component( hass, - DOMAIN, + device_tracker.DOMAIN, { - DOMAIN: { + device_tracker.DOMAIN: { CONF_PLATFORM: "mqtt", "devices": {dev_id: topic}, "source_type": source_type, @@ -257,23 +286,10 @@ async def test_legacy_matching_source_type( async_fire_mqtt_message(hass, topic, location) await hass.async_block_till_done() - assert hass.states.get(entity_id).attributes["source_type"] == SOURCE_TYPE_BLUETOOTH - - -async def test_setup_with_modern_schema(hass, mock_device_tracker_conf): - """Test setup using the modern schema.""" - dev_id = "jan" - entity_id = f"{DOMAIN}.{dev_id}" - topic = "/location/jan" - - hass.config.components = {"zone"} - config = {"name": dev_id, "state_topic": topic} - - await help_test_setup_manual_entity_from_yaml(hass, DOMAIN, config) - - assert hass.states.get(entity_id) is not None + assert hass.states.get(entity_id).attributes["source_type"] == SourceType.BLUETOOTH +# Deprecated in HA Core 2022.6 async def test_unload_entry( hass, mock_device_tracker_conf, mqtt_mock_entry_no_yaml_config, tmp_path ): @@ -281,13 +297,15 @@ async def test_unload_entry( # setup through configuration.yaml await mqtt_mock_entry_no_yaml_config() dev_id = "jan" - entity_id = f"{DOMAIN}.{dev_id}" + entity_id = f"{device_tracker.DOMAIN}.{dev_id}" topic = "/location/jan" location = "home" hass.config.components = {"mqtt", "zone"} assert await async_setup_component( - hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "mqtt", "devices": {dev_id: topic}}} + hass, + device_tracker.DOMAIN, + {device_tracker.DOMAIN: {CONF_PLATFORM: "mqtt", "devices": {dev_id: topic}}}, ) async_fire_mqtt_message(hass, topic, location) await hass.async_block_till_done() @@ -296,7 +314,7 @@ async def test_unload_entry( # setup through discovery dev_id = "piet" subscription = "/location/#" - domain = DOMAIN + domain = device_tracker.DOMAIN discovery_config = { "devices": {dev_id: subscription}, "state_topic": "some-state", @@ -330,21 +348,22 @@ async def test_unload_entry( assert discovery_setup_entity is None +# Deprecated in HA Core 2022.6 async def test_reload_entry_legacy( hass, mock_device_tracker_conf, mqtt_mock_entry_no_yaml_config, tmp_path ): """Test reloading the config entry with manual MQTT items.""" # setup through configuration.yaml await mqtt_mock_entry_no_yaml_config() - entity_id = f"{DOMAIN}.jan" + entity_id = f"{device_tracker.DOMAIN}.jan" topic = "location/jan" location = "home" config = { - DOMAIN: {CONF_PLATFORM: "mqtt", "devices": {"jan": topic}}, + device_tracker.DOMAIN: {CONF_PLATFORM: "mqtt", "devices": {"jan": topic}}, } hass.config.components = {"mqtt", "zone"} - assert await async_setup_component(hass, DOMAIN, config) + assert await async_setup_component(hass, device_tracker.DOMAIN, config) await hass.async_block_till_done() async_fire_mqtt_message(hass, topic, location) @@ -360,6 +379,7 @@ async def test_reload_entry_legacy( assert hass.states.get(entity_id).state == location +# Deprecated in HA Core 2022.6 async def test_setup_with_disabled_entry( hass, mock_device_tracker_conf, caplog ) -> None: @@ -372,11 +392,11 @@ async def test_setup_with_disabled_entry( topic = "location/jan" config = { - DOMAIN: {CONF_PLATFORM: "mqtt", "devices": {"jan": topic}}, + device_tracker.DOMAIN: {CONF_PLATFORM: "mqtt", "devices": {"jan": topic}}, } hass.config.components = {"mqtt", "zone"} - await async_setup_component(hass, DOMAIN, config) + await async_setup_component(hass, device_tracker.DOMAIN, config) await hass.async_block_till_done() assert ( diff --git a/tests/components/mqtt/test_device_tracker_discovery.py b/tests/components/mqtt/test_device_tracker_discovery.py index ac4058c9372..923ae7c9f75 100644 --- a/tests/components/mqtt/test_device_tracker_discovery.py +++ b/tests/components/mqtt/test_device_tracker_discovery.py @@ -1,27 +1,37 @@ -"""The tests for the MQTT device_tracker discovery platform.""" +"""The tests for the MQTT device_tracker platform.""" +import copy from unittest.mock import patch import pytest -from homeassistant.components import device_tracker +from homeassistant.components import device_tracker, mqtt from homeassistant.components.mqtt.const import DOMAIN as MQTT_DOMAIN from homeassistant.components.mqtt.discovery import ALREADY_DISCOVERED from homeassistant.const import STATE_HOME, STATE_NOT_HOME, STATE_UNKNOWN, Platform from homeassistant.setup import async_setup_component -from .test_common import help_test_setting_blocked_attribute_via_mqtt_json_message +from .test_common import ( + help_test_setting_blocked_attribute_via_mqtt_json_message, + help_test_setup_manual_entity_from_yaml, +) from tests.common import async_fire_mqtt_message, mock_device_registry, mock_registry DEFAULT_CONFIG = { - device_tracker.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "test-topic", + mqtt.DOMAIN: { + device_tracker.DOMAIN: { + "name": "test", + "state_topic": "test-topic", + } } } +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +DEFAULT_CONFIG_LEGACY = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) +DEFAULT_CONFIG_LEGACY[device_tracker.DOMAIN]["platform"] = mqtt.DOMAIN + @pytest.fixture(autouse=True) def device_tracker_platform_only(): @@ -430,6 +440,19 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( hass, mqtt_mock_entry_no_yaml_config, device_tracker.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, None, ) + + +async def test_setup_with_modern_schema(hass, mock_device_tracker_conf): + """Test setup using the modern schema.""" + dev_id = "jan" + entity_id = f"{device_tracker.DOMAIN}.{dev_id}" + topic = "/location/jan" + + config = {"name": dev_id, "state_topic": topic} + + await help_test_setup_manual_entity_from_yaml(hass, device_tracker.DOMAIN, config) + + assert hass.states.get(entity_id) is not None diff --git a/tests/components/mqtt/test_fan.py b/tests/components/mqtt/test_fan.py index 37dcefc9d3f..efe38234aee 100644 --- a/tests/components/mqtt/test_fan.py +++ b/tests/components/mqtt/test_fan.py @@ -5,7 +5,7 @@ from unittest.mock import patch import pytest from voluptuous.error import MultipleInvalid -from homeassistant.components import fan +from homeassistant.components import fan, mqtt from homeassistant.components.fan import ( ATTR_OSCILLATING, ATTR_PERCENTAGE, @@ -67,14 +67,20 @@ from tests.common import async_fire_mqtt_message from tests.components.fan import common DEFAULT_CONFIG = { - fan.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", + mqtt.DOMAIN: { + fan.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + } } } +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +DEFAULT_CONFIG_LEGACY = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) +DEFAULT_CONFIG_LEGACY[fan.DOMAIN]["platform"] = mqtt.DOMAIN + @pytest.fixture(autouse=True) def fan_platform_only(): @@ -83,18 +89,15 @@ def fan_platform_only(): yield -async def test_fail_setup_if_no_command_topic( - hass, caplog, mqtt_mock_entry_no_yaml_config -): +async def test_fail_setup_if_no_command_topic(hass, caplog): """Test if command fails with command topic.""" - assert await async_setup_component( - hass, fan.DOMAIN, {fan.DOMAIN: {"platform": "mqtt", "name": "test"}} + assert not await async_setup_component( + hass, + mqtt.DOMAIN, + {mqtt.DOMAIN: {fan.DOMAIN: {"name": "test"}}}, ) - await hass.async_block_till_done() - await mqtt_mock_entry_no_yaml_config() - assert hass.states.get("fan.test") is None assert ( - "Invalid config for [fan.mqtt]: required key not provided @ data['command_topic']" + "Invalid config for [mqtt]: required key not provided @ data['mqtt']['fan'][0]['command_topic']" in caplog.text ) @@ -105,35 +108,36 @@ async def test_controlling_state_via_topic( """Test the controlling state via topic.""" assert await async_setup_component( hass, - fan.DOMAIN, + mqtt.DOMAIN, { - fan.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "payload_off": "StAtE_OfF", - "payload_on": "StAtE_On", - "oscillation_state_topic": "oscillation-state-topic", - "oscillation_command_topic": "oscillation-command-topic", - "payload_oscillation_off": "OsC_OfF", - "payload_oscillation_on": "OsC_On", - "percentage_state_topic": "percentage-state-topic", - "percentage_command_topic": "percentage-command-topic", - "preset_mode_state_topic": "preset-mode-state-topic", - "preset_mode_command_topic": "preset-mode-command-topic", - "preset_modes": [ - "auto", - "smart", - "whoosh", - "eco", - "breeze", - "silent", - ], - "speed_range_min": 1, - "speed_range_max": 200, - "payload_reset_percentage": "rEset_percentage", - "payload_reset_preset_mode": "rEset_preset_mode", + mqtt.DOMAIN: { + fan.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "payload_off": "StAtE_OfF", + "payload_on": "StAtE_On", + "oscillation_state_topic": "oscillation-state-topic", + "oscillation_command_topic": "oscillation-command-topic", + "payload_oscillation_off": "OsC_OfF", + "payload_oscillation_on": "OsC_On", + "percentage_state_topic": "percentage-state-topic", + "percentage_command_topic": "percentage-command-topic", + "preset_mode_state_topic": "preset-mode-state-topic", + "preset_mode_command_topic": "preset-mode-command-topic", + "preset_modes": [ + "auto", + "smart", + "whoosh", + "eco", + "breeze", + "silent", + ], + "speed_range_min": 1, + "speed_range_max": 200, + "payload_reset_percentage": "rEset_percentage", + "payload_reset_preset_mode": "rEset_preset_mode", + } } }, ) @@ -226,37 +230,36 @@ async def test_controlling_state_via_topic_with_different_speed_range( """Test the controlling state via topic using an alternate speed range.""" assert await async_setup_component( hass, - fan.DOMAIN, + mqtt.DOMAIN, { - fan.DOMAIN: [ - { - "platform": "mqtt", - "name": "test1", - "command_topic": "command-topic", - "percentage_state_topic": "percentage-state-topic1", - "percentage_command_topic": "percentage-command-topic1", - "speed_range_min": 1, - "speed_range_max": 100, - }, - { - "platform": "mqtt", - "name": "test2", - "command_topic": "command-topic", - "percentage_state_topic": "percentage-state-topic2", - "percentage_command_topic": "percentage-command-topic2", - "speed_range_min": 1, - "speed_range_max": 200, - }, - { - "platform": "mqtt", - "name": "test3", - "command_topic": "command-topic", - "percentage_state_topic": "percentage-state-topic3", - "percentage_command_topic": "percentage-command-topic3", - "speed_range_min": 81, - "speed_range_max": 1023, - }, - ] + mqtt.DOMAIN: { + fan.DOMAIN: [ + { + "name": "test1", + "command_topic": "command-topic", + "percentage_state_topic": "percentage-state-topic1", + "percentage_command_topic": "percentage-command-topic1", + "speed_range_min": 1, + "speed_range_max": 100, + }, + { + "name": "test2", + "command_topic": "command-topic", + "percentage_state_topic": "percentage-state-topic2", + "percentage_command_topic": "percentage-command-topic2", + "speed_range_min": 1, + "speed_range_max": 200, + }, + { + "name": "test3", + "command_topic": "command-topic", + "percentage_state_topic": "percentage-state-topic3", + "percentage_command_topic": "percentage-command-topic3", + "speed_range_min": 81, + "speed_range_max": 1023, + }, + ] + } }, ) await hass.async_block_till_done() @@ -289,22 +292,23 @@ async def test_controlling_state_via_topic_no_percentage_topics( """Test the controlling state via topic without percentage topics.""" assert await async_setup_component( hass, - fan.DOMAIN, + mqtt.DOMAIN, { - fan.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "preset_mode_state_topic": "preset-mode-state-topic", - "preset_mode_command_topic": "preset-mode-command-topic", - "preset_modes": [ - "auto", - "smart", - "whoosh", - "eco", - "breeze", - ], + mqtt.DOMAIN: { + fan.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "preset_mode_state_topic": "preset-mode-state-topic", + "preset_mode_command_topic": "preset-mode-command-topic", + "preset_modes": [ + "auto", + "smart", + "whoosh", + "eco", + "breeze", + ], + } } }, ) @@ -345,33 +349,34 @@ async def test_controlling_state_via_topic_and_json_message( """Test the controlling state via topic and JSON message (percentage mode).""" assert await async_setup_component( hass, - fan.DOMAIN, + mqtt.DOMAIN, { - fan.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "oscillation_state_topic": "oscillation-state-topic", - "oscillation_command_topic": "oscillation-command-topic", - "percentage_state_topic": "percentage-state-topic", - "percentage_command_topic": "percentage-command-topic", - "preset_mode_state_topic": "preset-mode-state-topic", - "preset_mode_command_topic": "preset-mode-command-topic", - "preset_modes": [ - "auto", - "smart", - "whoosh", - "eco", - "breeze", - "silent", - ], - "state_value_template": "{{ value_json.val }}", - "oscillation_value_template": "{{ value_json.val }}", - "percentage_value_template": "{{ value_json.val }}", - "preset_mode_value_template": "{{ value_json.val }}", - "speed_range_min": 1, - "speed_range_max": 100, + mqtt.DOMAIN: { + fan.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "oscillation_state_topic": "oscillation-state-topic", + "oscillation_command_topic": "oscillation-command-topic", + "percentage_state_topic": "percentage-state-topic", + "percentage_command_topic": "percentage-command-topic", + "preset_mode_state_topic": "preset-mode-state-topic", + "preset_mode_command_topic": "preset-mode-command-topic", + "preset_modes": [ + "auto", + "smart", + "whoosh", + "eco", + "breeze", + "silent", + ], + "state_value_template": "{{ value_json.val }}", + "oscillation_value_template": "{{ value_json.val }}", + "percentage_value_template": "{{ value_json.val }}", + "preset_mode_value_template": "{{ value_json.val }}", + "speed_range_min": 1, + "speed_range_max": 100, + } } }, ) @@ -450,33 +455,34 @@ async def test_controlling_state_via_topic_and_json_message_shared_topic( """Test the controlling state via topic and JSON message using a shared topic.""" assert await async_setup_component( hass, - fan.DOMAIN, + mqtt.DOMAIN, { - fan.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "shared-state-topic", - "command_topic": "command-topic", - "oscillation_state_topic": "shared-state-topic", - "oscillation_command_topic": "oscillation-command-topic", - "percentage_state_topic": "shared-state-topic", - "percentage_command_topic": "percentage-command-topic", - "preset_mode_state_topic": "shared-state-topic", - "preset_mode_command_topic": "preset-mode-command-topic", - "preset_modes": [ - "auto", - "smart", - "whoosh", - "eco", - "breeze", - "silent", - ], - "state_value_template": "{{ value_json.state }}", - "oscillation_value_template": "{{ value_json.oscillation }}", - "percentage_value_template": "{{ value_json.percentage }}", - "preset_mode_value_template": "{{ value_json.preset_mode }}", - "speed_range_min": 1, - "speed_range_max": 100, + mqtt.DOMAIN: { + fan.DOMAIN: { + "name": "test", + "state_topic": "shared-state-topic", + "command_topic": "command-topic", + "oscillation_state_topic": "shared-state-topic", + "oscillation_command_topic": "oscillation-command-topic", + "percentage_state_topic": "shared-state-topic", + "percentage_command_topic": "percentage-command-topic", + "preset_mode_state_topic": "shared-state-topic", + "preset_mode_command_topic": "preset-mode-command-topic", + "preset_modes": [ + "auto", + "smart", + "whoosh", + "eco", + "breeze", + "silent", + ], + "state_value_template": "{{ value_json.state }}", + "oscillation_value_template": "{{ value_json.oscillation }}", + "percentage_value_template": "{{ value_json.percentage }}", + "preset_mode_value_template": "{{ value_json.preset_mode }}", + "speed_range_min": 1, + "speed_range_max": 100, + } } }, ) @@ -540,24 +546,25 @@ async def test_sending_mqtt_commands_and_optimistic( """Test optimistic mode without state topic.""" assert await async_setup_component( hass, - fan.DOMAIN, + mqtt.DOMAIN, { - fan.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "command-topic", - "payload_off": "StAtE_OfF", - "payload_on": "StAtE_On", - "oscillation_command_topic": "oscillation-command-topic", - "payload_oscillation_off": "OsC_OfF", - "payload_oscillation_on": "OsC_On", - "percentage_command_topic": "percentage-command-topic", - "preset_mode_command_topic": "preset-mode-command-topic", - "preset_modes": [ - "whoosh", - "breeze", - "silent", - ], + mqtt.DOMAIN: { + fan.DOMAIN: { + "name": "test", + "command_topic": "command-topic", + "payload_off": "StAtE_OfF", + "payload_on": "StAtE_On", + "oscillation_command_topic": "oscillation-command-topic", + "payload_oscillation_off": "OsC_OfF", + "payload_oscillation_on": "OsC_On", + "percentage_command_topic": "percentage-command-topic", + "preset_mode_command_topic": "preset-mode-command-topic", + "preset_modes": [ + "whoosh", + "breeze", + "silent", + ], + } } }, ) @@ -664,37 +671,36 @@ async def test_sending_mqtt_commands_with_alternate_speed_range( """Test the controlling state via topic using an alternate speed range.""" assert await async_setup_component( hass, - fan.DOMAIN, + mqtt.DOMAIN, { - fan.DOMAIN: [ - { - "platform": "mqtt", - "name": "test1", - "command_topic": "command-topic", - "percentage_state_topic": "percentage-state-topic1", - "percentage_command_topic": "percentage-command-topic1", - "speed_range_min": 1, - "speed_range_max": 3, - }, - { - "platform": "mqtt", - "name": "test2", - "command_topic": "command-topic", - "percentage_state_topic": "percentage-state-topic2", - "percentage_command_topic": "percentage-command-topic2", - "speed_range_min": 1, - "speed_range_max": 200, - }, - { - "platform": "mqtt", - "name": "test3", - "command_topic": "command-topic", - "percentage_state_topic": "percentage-state-topic3", - "percentage_command_topic": "percentage-command-topic3", - "speed_range_min": 81, - "speed_range_max": 1023, - }, - ] + mqtt.DOMAIN: { + fan.DOMAIN: [ + { + "name": "test1", + "command_topic": "command-topic", + "percentage_state_topic": "percentage-state-topic1", + "percentage_command_topic": "percentage-command-topic1", + "speed_range_min": 1, + "speed_range_max": 3, + }, + { + "name": "test2", + "command_topic": "command-topic", + "percentage_state_topic": "percentage-state-topic2", + "percentage_command_topic": "percentage-command-topic2", + "speed_range_min": 1, + "speed_range_max": 200, + }, + { + "name": "test3", + "command_topic": "command-topic", + "percentage_state_topic": "percentage-state-topic3", + "percentage_command_topic": "percentage-command-topic3", + "speed_range_min": 81, + "speed_range_max": 1023, + }, + ] + } }, ) await hass.async_block_till_done() @@ -771,19 +777,20 @@ async def test_sending_mqtt_commands_and_optimistic_no_legacy( """Test optimistic mode without state topic without legacy speed command topic.""" assert await async_setup_component( hass, - fan.DOMAIN, + mqtt.DOMAIN, { - fan.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "command-topic", - "percentage_command_topic": "percentage-command-topic", - "preset_mode_command_topic": "preset-mode-command-topic", - "preset_modes": [ - "whoosh", - "breeze", - "silent", - ], + mqtt.DOMAIN: { + fan.DOMAIN: { + "name": "test", + "command_topic": "command-topic", + "percentage_command_topic": "percentage-command-topic", + "preset_mode_command_topic": "preset-mode-command-topic", + "preset_modes": [ + "whoosh", + "breeze", + "silent", + ], + } } }, ) @@ -902,24 +909,25 @@ async def test_sending_mqtt_command_templates_( """Test optimistic mode without state topic without legacy speed command topic.""" assert await async_setup_component( hass, - fan.DOMAIN, + mqtt.DOMAIN, { - fan.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "command-topic", - "command_template": "state: {{ value }}", - "oscillation_command_topic": "oscillation-command-topic", - "oscillation_command_template": "oscillation: {{ value }}", - "percentage_command_topic": "percentage-command-topic", - "percentage_command_template": "percentage: {{ value }}", - "preset_mode_command_topic": "preset-mode-command-topic", - "preset_mode_command_template": "preset_mode: {{ value }}", - "preset_modes": [ - "whoosh", - "breeze", - "silent", - ], + mqtt.DOMAIN: { + fan.DOMAIN: { + "name": "test", + "command_topic": "command-topic", + "command_template": "state: {{ value }}", + "oscillation_command_topic": "oscillation-command-topic", + "oscillation_command_template": "oscillation: {{ value }}", + "percentage_command_topic": "percentage-command-topic", + "percentage_command_template": "percentage: {{ value }}", + "preset_mode_command_topic": "preset-mode-command-topic", + "preset_mode_command_template": "preset_mode: {{ value }}", + "preset_modes": [ + "whoosh", + "breeze", + "silent", + ], + } } }, ) @@ -1044,20 +1052,21 @@ async def test_sending_mqtt_commands_and_optimistic_no_percentage_topic( """Test optimistic mode without state topic without percentage command topic.""" assert await async_setup_component( hass, - fan.DOMAIN, + mqtt.DOMAIN, { - fan.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "command-topic", - "preset_mode_command_topic": "preset-mode-command-topic", - "preset_mode_state_topic": "preset-mode-state-topic", - "preset_modes": [ - "whoosh", - "breeze", - "silent", - "high", - ], + mqtt.DOMAIN: { + fan.DOMAIN: { + "name": "test", + "command_topic": "command-topic", + "preset_mode_command_topic": "preset-mode-command-topic", + "preset_mode_state_topic": "preset-mode-state-topic", + "preset_modes": [ + "whoosh", + "breeze", + "silent", + "high", + ], + } } }, ) @@ -1105,25 +1114,26 @@ async def test_sending_mqtt_commands_and_explicit_optimistic( """Test optimistic mode with state topic and turn on attributes.""" assert await async_setup_component( hass, - fan.DOMAIN, + mqtt.DOMAIN, { - fan.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "oscillation_state_topic": "oscillation-state-topic", - "oscillation_command_topic": "oscillation-command-topic", - "percentage_state_topic": "percentage-state-topic", - "percentage_command_topic": "percentage-command-topic", - "preset_mode_command_topic": "preset-mode-command-topic", - "preset_mode_state_topic": "preset-mode-state-topic", - "preset_modes": [ - "whoosh", - "breeze", - "silent", - ], - "optimistic": True, + mqtt.DOMAIN: { + fan.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "oscillation_state_topic": "oscillation-state-topic", + "oscillation_command_topic": "oscillation-command-topic", + "percentage_state_topic": "percentage-state-topic", + "percentage_command_topic": "percentage-command-topic", + "preset_mode_command_topic": "preset-mode-command-topic", + "preset_mode_state_topic": "preset-mode-state-topic", + "preset_modes": [ + "whoosh", + "breeze", + "silent", + ], + "optimistic": True, + } } }, ) @@ -1355,7 +1365,7 @@ async def test_encoding_subscribable_topics( attribute_value, ): """Test handling of incoming encoded payload.""" - config = copy.deepcopy(DEFAULT_CONFIG[fan.DOMAIN]) + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[fan.DOMAIN]) config[ATTR_PRESET_MODES] = ["eco", "auto"] config[CONF_PRESET_MODE_COMMAND_TOPIC] = "fan/some_preset_mode_command_topic" config[CONF_PERCENTAGE_COMMAND_TOPIC] = "fan/some_percentage_command_topic" @@ -1377,19 +1387,20 @@ async def test_attributes(hass, mqtt_mock_entry_with_yaml_config, caplog): """Test attributes.""" assert await async_setup_component( hass, - fan.DOMAIN, + mqtt.DOMAIN, { - fan.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "command-topic", - "oscillation_command_topic": "oscillation-command-topic", - "preset_mode_command_topic": "preset-mode-command-topic", - "percentage_command_topic": "percentage-command-topic", - "preset_modes": [ - "breeze", - "silent", - ], + mqtt.DOMAIN: { + fan.DOMAIN: { + "name": "test", + "command_topic": "command-topic", + "oscillation_command_topic": "oscillation-command-topic", + "preset_mode_command_topic": "preset-mode-command-topic", + "percentage_command_topic": "percentage-command-topic", + "preset_modes": [ + "breeze", + "silent", + ], + } } }, ) @@ -1424,177 +1435,203 @@ async def test_attributes(hass, mqtt_mock_entry_with_yaml_config, caplog): assert state.attributes.get(fan.ATTR_OSCILLATING) is False -async def test_supported_features(hass, mqtt_mock_entry_with_yaml_config): +@pytest.mark.parametrize( + "name,config,success,features", + [ + ( + "test1", + { + "name": "test1", + "command_topic": "command-topic", + }, + True, + 0, + ), + ( + "test2", + { + "name": "test2", + "command_topic": "command-topic", + "oscillation_command_topic": "oscillation-command-topic", + }, + True, + fan.SUPPORT_OSCILLATE, + ), + ( + "test3", + { + "name": "test3", + "command_topic": "command-topic", + "percentage_command_topic": "percentage-command-topic", + }, + True, + fan.SUPPORT_SET_SPEED, + ), + ( + "test4", + { + "name": "test4", + "command_topic": "command-topic", + "preset_mode_command_topic": "preset-mode-command-topic", + }, + False, + None, + ), + ( + "test5", + { + "name": "test5", + "command_topic": "command-topic", + "preset_mode_command_topic": "preset-mode-command-topic", + "preset_modes": ["eco", "auto"], + }, + True, + fan.SUPPORT_PRESET_MODE, + ), + ( + "test6", + { + "name": "test6", + "command_topic": "command-topic", + "preset_mode_command_topic": "preset-mode-command-topic", + "preset_modes": ["eco", "smart", "auto"], + }, + True, + fan.SUPPORT_PRESET_MODE, + ), + ( + "test7", + { + "name": "test7", + "command_topic": "command-topic", + "percentage_command_topic": "percentage-command-topic", + }, + True, + fan.SUPPORT_SET_SPEED, + ), + ( + "test8", + { + "name": "test8", + "command_topic": "command-topic", + "oscillation_command_topic": "oscillation-command-topic", + "percentage_command_topic": "percentage-command-topic", + }, + True, + fan.SUPPORT_OSCILLATE | fan.SUPPORT_SET_SPEED, + ), + ( + "test9", + { + "name": "test9", + "command_topic": "command-topic", + "preset_mode_command_topic": "preset-mode-command-topic", + "preset_modes": ["Mode1", "Mode2", "Mode3"], + }, + True, + fan.SUPPORT_PRESET_MODE, + ), + ( + "test10", + { + "name": "test10", + "command_topic": "command-topic", + "preset_mode_command_topic": "preset-mode-command-topic", + "preset_modes": ["whoosh", "silent", "auto"], + }, + True, + fan.SUPPORT_PRESET_MODE, + ), + ( + "test11", + { + "name": "test11", + "command_topic": "command-topic", + "oscillation_command_topic": "oscillation-command-topic", + "preset_mode_command_topic": "preset-mode-command-topic", + "preset_modes": ["Mode1", "Mode2", "Mode3"], + }, + True, + fan.SUPPORT_PRESET_MODE | fan.SUPPORT_OSCILLATE, + ), + ( + "test12", + { + "name": "test12", + "command_topic": "command-topic", + "percentage_command_topic": "percentage-command-topic", + "speed_range_min": 1, + "speed_range_max": 40, + }, + True, + fan.SUPPORT_SET_SPEED, + ), + ( + "test13", + { + "name": "test13", + "command_topic": "command-topic", + "percentage_command_topic": "percentage-command-topic", + "speed_range_min": 50, + "speed_range_max": 40, + }, + False, + None, + ), + ( + "test14", + { + "name": "test14", + "command_topic": "command-topic", + "percentage_command_topic": "percentage-command-topic", + "speed_range_min": 0, + "speed_range_max": 40, + }, + False, + None, + ), + ( + "test15", + { + "name": "test7reset_payload_in_preset_modes_a", + "command_topic": "command-topic", + "preset_mode_command_topic": "preset-mode-command-topic", + "preset_modes": ["auto", "smart", "normal", "None"], + }, + False, + None, + ), + ( + "test16", + { + "name": "test16", + "command_topic": "command-topic", + "preset_mode_command_topic": "preset-mode-command-topic", + "preset_modes": ["whoosh", "silent", "auto", "None"], + "payload_reset_preset_mode": "normal", + }, + True, + fan.SUPPORT_PRESET_MODE, + ), + ], +) +async def test_supported_features( + hass, mqtt_mock_entry_with_yaml_config, name, config, success, features +): """Test optimistic mode without state topic.""" - assert await async_setup_component( - hass, - fan.DOMAIN, - { - fan.DOMAIN: [ - { - "platform": "mqtt", - "name": "test1", - "command_topic": "command-topic", - }, - { - "platform": "mqtt", - "name": "test2", - "command_topic": "command-topic", - "oscillation_command_topic": "oscillation-command-topic", - }, - { - "platform": "mqtt", - "name": "test3b", - "command_topic": "command-topic", - "percentage_command_topic": "percentage-command-topic", - }, - { - "platform": "mqtt", - "name": "test3c1", - "command_topic": "command-topic", - "preset_mode_command_topic": "preset-mode-command-topic", - }, - { - "platform": "mqtt", - "name": "test3c2", - "command_topic": "command-topic", - "preset_mode_command_topic": "preset-mode-command-topic", - "preset_modes": ["eco", "auto"], - }, - { - "platform": "mqtt", - "name": "test3c3", - "command_topic": "command-topic", - "preset_mode_command_topic": "preset-mode-command-topic", - "preset_modes": ["eco", "smart", "auto"], - }, - { - "platform": "mqtt", - "name": "test4pcta", - "command_topic": "command-topic", - "percentage_command_topic": "percentage-command-topic", - }, - { - "platform": "mqtt", - "name": "test4pctb", - "command_topic": "command-topic", - "oscillation_command_topic": "oscillation-command-topic", - "percentage_command_topic": "percentage-command-topic", - }, - { - "platform": "mqtt", - "name": "test5pr_ma", - "command_topic": "command-topic", - "preset_mode_command_topic": "preset-mode-command-topic", - "preset_modes": ["Mode1", "Mode2", "Mode3"], - }, - { - "platform": "mqtt", - "name": "test5pr_mb", - "command_topic": "command-topic", - "preset_mode_command_topic": "preset-mode-command-topic", - "preset_modes": ["whoosh", "silent", "auto"], - }, - { - "platform": "mqtt", - "name": "test5pr_mc", - "command_topic": "command-topic", - "oscillation_command_topic": "oscillation-command-topic", - "preset_mode_command_topic": "preset-mode-command-topic", - "preset_modes": ["Mode1", "Mode2", "Mode3"], - }, - { - "platform": "mqtt", - "name": "test6spd_range_a", - "command_topic": "command-topic", - "percentage_command_topic": "percentage-command-topic", - "speed_range_min": 1, - "speed_range_max": 40, - }, - { - "platform": "mqtt", - "name": "test6spd_range_b", - "command_topic": "command-topic", - "percentage_command_topic": "percentage-command-topic", - "speed_range_min": 50, - "speed_range_max": 40, - }, - { - "platform": "mqtt", - "name": "test6spd_range_c", - "command_topic": "command-topic", - "percentage_command_topic": "percentage-command-topic", - "speed_range_min": 0, - "speed_range_max": 40, - }, - { - "platform": "mqtt", - "name": "test7reset_payload_in_preset_modes_a", - "command_topic": "command-topic", - "preset_mode_command_topic": "preset-mode-command-topic", - "preset_modes": ["auto", "smart", "normal", "None"], - }, - { - "platform": "mqtt", - "name": "test7reset_payload_in_preset_modes_b", - "command_topic": "command-topic", - "preset_mode_command_topic": "preset-mode-command-topic", - "preset_modes": ["whoosh", "silent", "auto", "None"], - "payload_reset_preset_mode": "normal", - }, - ] - }, - ) - await hass.async_block_till_done() - await mqtt_mock_entry_with_yaml_config() - state = hass.states.get("fan.test1") - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 0 - state = hass.states.get("fan.test2") - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == fan.SUPPORT_OSCILLATE - - state = hass.states.get("fan.test3b") - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == fan.SUPPORT_SET_SPEED - - state = hass.states.get("fan.test3c1") - assert state is None - - state = hass.states.get("fan.test3c2") - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == fan.SUPPORT_PRESET_MODE - state = hass.states.get("fan.test3c3") - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == fan.SUPPORT_PRESET_MODE - - state = hass.states.get("fan.test4pcta") - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == fan.SUPPORT_SET_SPEED - state = hass.states.get("fan.test4pctb") assert ( - state.attributes.get(ATTR_SUPPORTED_FEATURES) - == fan.SUPPORT_OSCILLATE | fan.SUPPORT_SET_SPEED + await async_setup_component( + hass, mqtt.DOMAIN, {mqtt.DOMAIN: {fan.DOMAIN: config}} + ) + is success ) + if success: + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() - state = hass.states.get("fan.test5pr_ma") - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == fan.SUPPORT_PRESET_MODE - state = hass.states.get("fan.test5pr_mb") - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == fan.SUPPORT_PRESET_MODE - - state = hass.states.get("fan.test5pr_mc") - assert ( - state.attributes.get(ATTR_SUPPORTED_FEATURES) - == fan.SUPPORT_OSCILLATE | fan.SUPPORT_PRESET_MODE - ) - - state = hass.states.get("fan.test6spd_range_a") - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == fan.SUPPORT_SET_SPEED - assert state.attributes.get("percentage_step") == 2.5 - state = hass.states.get("fan.test6spd_range_b") - assert state is None - state = hass.states.get("fan.test6spd_range_c") - assert state is None - - state = hass.states.get("fan.test7reset_payload_in_preset_modes_a") - assert state is None - state = hass.states.get("fan.test7reset_payload_in_preset_modes_b") - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == fan.SUPPORT_PRESET_MODE + state = hass.states.get(f"fan.{name}") + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == features async def test_availability_when_connection_lost( @@ -1602,14 +1639,14 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, fan.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, fan.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock_entry_with_yaml_config, fan.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, fan.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -1619,7 +1656,7 @@ async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_conf hass, mqtt_mock_entry_with_yaml_config, fan.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, True, "state-topic", "1", @@ -1632,7 +1669,7 @@ async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_confi hass, mqtt_mock_entry_with_yaml_config, fan.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, True, "state-topic", "1", @@ -1644,7 +1681,7 @@ async def test_setting_attribute_via_mqtt_json_message( ): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock_entry_with_yaml_config, fan.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, fan.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -1656,7 +1693,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( hass, mqtt_mock_entry_no_yaml_config, fan.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, MQTT_FAN_ATTRIBUTES_BLOCKED, ) @@ -1664,7 +1701,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock_entry_with_yaml_config, fan.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, fan.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -1673,7 +1710,11 @@ async def test_update_with_json_attrs_not_dict( ): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock_entry_with_yaml_config, caplog, fan.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + fan.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) @@ -1682,14 +1723,18 @@ async def test_update_with_json_attrs_bad_json( ): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock_entry_with_yaml_config, caplog, fan.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + fan.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock_entry_no_yaml_config, caplog, fan.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, caplog, fan.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -1767,42 +1812,42 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT fan device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock_entry_no_yaml_config, fan.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, fan.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT fan device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock_entry_no_yaml_config, fan.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, fan.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock_entry_no_yaml_config, fan.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, fan.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock_entry_no_yaml_config, fan.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, fan.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock_entry_with_yaml_config, fan.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, fan.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock_entry_no_yaml_config, fan.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, fan.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -1812,7 +1857,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): hass, mqtt_mock_entry_no_yaml_config, fan.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, fan.SERVICE_TURN_ON, ) @@ -1869,7 +1914,7 @@ async def test_publishing_with_custom_encoding( ): """Test publishing MQTT payload with different encoding.""" domain = fan.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG[domain]) + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) if topic == "preset_mode_command_topic": config["preset_modes"] = ["auto", "eco"] @@ -1890,7 +1935,7 @@ async def test_publishing_with_custom_encoding( async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = fan.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) @@ -1899,14 +1944,14 @@ async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_pa async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = fan.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = fan.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG[platform]) + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) config["name"] = "test" del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) @@ -1916,7 +1961,20 @@ async def test_setup_manual_entity_from_yaml(hass): async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): """Test unloading the config entry.""" domain = fan.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) + + +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +async def test_setup_with_legacy_schema(hass, mqtt_mock_entry_with_yaml_config): + """Test a setup with deprecated yaml platform schema.""" + domain = fan.DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) + config["name"] = "test" + assert await async_setup_component(hass, domain, {domain: config}) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + assert hass.states.get(f"{domain}.test") is not None diff --git a/tests/components/mqtt/test_humidifier.py b/tests/components/mqtt/test_humidifier.py index 38dc634578f..0cc2be638bf 100644 --- a/tests/components/mqtt/test_humidifier.py +++ b/tests/components/mqtt/test_humidifier.py @@ -5,7 +5,7 @@ from unittest.mock import patch import pytest from voluptuous.error import MultipleInvalid -from homeassistant.components import humidifier +from homeassistant.components import humidifier, mqtt from homeassistant.components.humidifier import ( ATTR_HUMIDITY, ATTR_MODE, @@ -68,15 +68,21 @@ from .test_common import ( from tests.common import async_fire_mqtt_message DEFAULT_CONFIG = { - humidifier.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "target_humidity_command_topic": "humidity-command-topic", + mqtt.DOMAIN: { + humidifier.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "target_humidity_command_topic": "humidity-command-topic", + } } } +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +DEFAULT_CONFIG_LEGACY = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) +DEFAULT_CONFIG_LEGACY[humidifier.DOMAIN]["platform"] = mqtt.DOMAIN + @pytest.fixture(autouse=True) def humidifer_platform_only(): @@ -126,16 +132,17 @@ async def async_set_humidity( await hass.services.async_call(DOMAIN, SERVICE_SET_HUMIDITY, data, blocking=True) -async def test_fail_setup_if_no_command_topic(hass, mqtt_mock_entry_no_yaml_config): +async def test_fail_setup_if_no_command_topic(hass, caplog): """Test if command fails with command topic.""" - assert await async_setup_component( + assert not await async_setup_component( hass, - humidifier.DOMAIN, - {humidifier.DOMAIN: {"platform": "mqtt", "name": "test"}}, + mqtt.DOMAIN, + {mqtt.DOMAIN: {humidifier.DOMAIN: {"name": "test"}}}, + ) + assert ( + "Invalid config for [mqtt]: required key not provided @ data['mqtt']['humidifier'][0]['command_topic']. Got None" + in caplog.text ) - await hass.async_block_till_done() - await mqtt_mock_entry_no_yaml_config() - assert hass.states.get("humidifier.test") is None async def test_controlling_state_via_topic( @@ -144,29 +151,30 @@ async def test_controlling_state_via_topic( """Test the controlling state via topic.""" assert await async_setup_component( hass, - humidifier.DOMAIN, + mqtt.DOMAIN, { - humidifier.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "payload_off": "StAtE_OfF", - "payload_on": "StAtE_On", - "target_humidity_state_topic": "humidity-state-topic", - "target_humidity_command_topic": "humidity-command-topic", - "mode_state_topic": "mode-state-topic", - "mode_command_topic": "mode-command-topic", - "modes": [ - "auto", - "comfort", - "home", - "eco", - "sleep", - "baby", - ], - "payload_reset_humidity": "rEset_humidity", - "payload_reset_mode": "rEset_mode", + mqtt.DOMAIN: { + humidifier.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "payload_off": "StAtE_OfF", + "payload_on": "StAtE_On", + "target_humidity_state_topic": "humidity-state-topic", + "target_humidity_command_topic": "humidity-command-topic", + "mode_state_topic": "mode-state-topic", + "mode_command_topic": "mode-command-topic", + "modes": [ + "auto", + "comfort", + "home", + "eco", + "sleep", + "baby", + ], + "payload_reset_humidity": "rEset_humidity", + "payload_reset_mode": "rEset_mode", + } } }, ) @@ -248,25 +256,26 @@ async def test_controlling_state_via_topic_and_json_message( """Test the controlling state via topic and JSON message.""" assert await async_setup_component( hass, - humidifier.DOMAIN, + mqtt.DOMAIN, { - humidifier.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "target_humidity_state_topic": "humidity-state-topic", - "target_humidity_command_topic": "humidity-command-topic", - "mode_state_topic": "mode-state-topic", - "mode_command_topic": "mode-command-topic", - "modes": [ - "auto", - "eco", - "baby", - ], - "state_value_template": "{{ value_json.val }}", - "target_humidity_state_template": "{{ value_json.val }}", - "mode_state_template": "{{ value_json.val }}", + mqtt.DOMAIN: { + humidifier.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "target_humidity_state_topic": "humidity-state-topic", + "target_humidity_command_topic": "humidity-command-topic", + "mode_state_topic": "mode-state-topic", + "mode_command_topic": "mode-command-topic", + "modes": [ + "auto", + "eco", + "baby", + ], + "state_value_template": "{{ value_json.val }}", + "target_humidity_state_template": "{{ value_json.val }}", + "mode_state_template": "{{ value_json.val }}", + } } }, ) @@ -336,25 +345,26 @@ async def test_controlling_state_via_topic_and_json_message_shared_topic( """Test the controlling state via topic and JSON message using a shared topic.""" assert await async_setup_component( hass, - humidifier.DOMAIN, + mqtt.DOMAIN, { - humidifier.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "shared-state-topic", - "command_topic": "command-topic", - "target_humidity_state_topic": "shared-state-topic", - "target_humidity_command_topic": "percentage-command-topic", - "mode_state_topic": "shared-state-topic", - "mode_command_topic": "mode-command-topic", - "modes": [ - "auto", - "eco", - "baby", - ], - "state_value_template": "{{ value_json.state }}", - "target_humidity_state_template": "{{ value_json.humidity }}", - "mode_state_template": "{{ value_json.mode }}", + mqtt.DOMAIN: { + humidifier.DOMAIN: { + "name": "test", + "state_topic": "shared-state-topic", + "command_topic": "command-topic", + "target_humidity_state_topic": "shared-state-topic", + "target_humidity_command_topic": "percentage-command-topic", + "mode_state_topic": "shared-state-topic", + "mode_command_topic": "mode-command-topic", + "modes": [ + "auto", + "eco", + "baby", + ], + "state_value_template": "{{ value_json.state }}", + "target_humidity_state_template": "{{ value_json.humidity }}", + "mode_state_template": "{{ value_json.mode }}", + } } }, ) @@ -414,21 +424,22 @@ async def test_sending_mqtt_commands_and_optimistic( """Test optimistic mode without state topic.""" assert await async_setup_component( hass, - humidifier.DOMAIN, + mqtt.DOMAIN, { - humidifier.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "command-topic", - "payload_off": "StAtE_OfF", - "payload_on": "StAtE_On", - "target_humidity_command_topic": "humidity-command-topic", - "mode_command_topic": "mode-command-topic", - "modes": [ - "eco", - "auto", - "baby", - ], + mqtt.DOMAIN: { + humidifier.DOMAIN: { + "name": "test", + "command_topic": "command-topic", + "payload_off": "StAtE_OfF", + "payload_on": "StAtE_On", + "target_humidity_command_topic": "humidity-command-topic", + "mode_command_topic": "mode-command-topic", + "modes": [ + "eco", + "auto", + "baby", + ], + } } }, ) @@ -510,22 +521,23 @@ async def test_sending_mqtt_command_templates_( """Testing command templates with optimistic mode without state topic.""" assert await async_setup_component( hass, - humidifier.DOMAIN, + mqtt.DOMAIN, { - humidifier.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "command-topic", - "command_template": "state: {{ value }}", - "target_humidity_command_topic": "humidity-command-topic", - "target_humidity_command_template": "humidity: {{ value }}", - "mode_command_topic": "mode-command-topic", - "mode_command_template": "mode: {{ value }}", - "modes": [ - "auto", - "eco", - "sleep", - ], + mqtt.DOMAIN: { + humidifier.DOMAIN: { + "name": "test", + "command_topic": "command-topic", + "command_template": "state: {{ value }}", + "target_humidity_command_topic": "humidity-command-topic", + "target_humidity_command_template": "humidity: {{ value }}", + "mode_command_topic": "mode-command-topic", + "mode_command_template": "mode: {{ value }}", + "modes": [ + "auto", + "eco", + "sleep", + ], + } } }, ) @@ -607,23 +619,24 @@ async def test_sending_mqtt_commands_and_explicit_optimistic( """Test optimistic mode with state topic and turn on attributes.""" assert await async_setup_component( hass, - humidifier.DOMAIN, + mqtt.DOMAIN, { - humidifier.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "target_humidity_state_topic": "humidity-state-topic", - "target_humidity_command_topic": "humidity-command-topic", - "mode_command_topic": "mode-command-topic", - "mode_state_topic": "mode-state-topic", - "modes": [ - "auto", - "eco", - "baby", - ], - "optimistic": True, + mqtt.DOMAIN: { + humidifier.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "target_humidity_state_topic": "humidity-state-topic", + "target_humidity_command_topic": "humidity-command-topic", + "mode_command_topic": "mode-command-topic", + "mode_state_topic": "mode-state-topic", + "modes": [ + "auto", + "eco", + "baby", + ], + "optimistic": True, + } } }, ) @@ -737,7 +750,7 @@ async def test_encoding_subscribable_topics( attribute_value, ): """Test handling of incoming encoded payload.""" - config = copy.deepcopy(DEFAULT_CONFIG[humidifier.DOMAIN]) + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[humidifier.DOMAIN]) config["modes"] = ["eco", "auto"] config[CONF_MODE_COMMAND_TOPIC] = "humidifier/some_mode_command_topic" await help_test_encoding_subscribable_topics( @@ -757,18 +770,19 @@ async def test_attributes(hass, mqtt_mock_entry_with_yaml_config, caplog): """Test attributes.""" assert await async_setup_component( hass, - humidifier.DOMAIN, + mqtt.DOMAIN, { - humidifier.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "command-topic", - "mode_command_topic": "mode-command-topic", - "target_humidity_command_topic": "humidity-command-topic", - "modes": [ - "eco", - "baby", - ], + mqtt.DOMAIN: { + humidifier.DOMAIN: { + "name": "test", + "command_topic": "command-topic", + "mode_command_topic": "mode-command-topic", + "target_humidity_command_topic": "humidity-command-topic", + "modes": [ + "eco", + "baby", + ], + } } }, ) @@ -799,157 +813,182 @@ async def test_attributes(hass, mqtt_mock_entry_with_yaml_config, caplog): assert state.attributes.get(humidifier.ATTR_MODE) is None -async def test_invalid_configurations(hass, mqtt_mock_entry_with_yaml_config, caplog): - """Test invalid configurations.""" - assert await async_setup_component( - hass, - humidifier.DOMAIN, - { - humidifier.DOMAIN: [ - { - "platform": "mqtt", - "name": "test_valid_1", - "command_topic": "command-topic", - "target_humidity_command_topic": "humidity-command-topic", - }, - { - "platform": "mqtt", - "name": "test_valid_2", - "command_topic": "command-topic", - "target_humidity_command_topic": "humidity-command-topic", - "device_class": "humidifier", - }, - { - "platform": "mqtt", - "name": "test_valid_3", - "command_topic": "command-topic", - "target_humidity_command_topic": "humidity-command-topic", - "device_class": "dehumidifier", - }, - { - "platform": "mqtt", - "name": "test_invalid_device_class", - "command_topic": "command-topic", - "target_humidity_command_topic": "humidity-command-topic", - "device_class": "notsupporedSpeci@l", - }, - { - "platform": "mqtt", - "name": "test_mode_command_without_modes", - "command_topic": "command-topic", - "target_humidity_command_topic": "humidity-command-topic", - "mode_command_topic": "mode-command-topic", - }, - { - "platform": "mqtt", - "name": "test_invalid_humidity_min_max_1", - "command_topic": "command-topic", - "target_humidity_command_topic": "humidity-command-topic", - "min_humidity": 0, - "max_humidity": 101, - }, - { - "platform": "mqtt", - "name": "test_invalid_humidity_min_max_2", - "command_topic": "command-topic", - "target_humidity_command_topic": "humidity-command-topic", - "max_humidity": 20, - "min_humidity": 40, - }, - { - "platform": "mqtt", - "name": "test_invalid_mode_is_reset", - "command_topic": "command-topic", - "target_humidity_command_topic": "humidity-command-topic", - "mode_command_topic": "mode-command-topic", - "modes": ["eco", "None"], - }, - ] - }, +@pytest.mark.parametrize( + "config,valid", + [ + ( + { + "name": "test_valid_1", + "command_topic": "command-topic", + "target_humidity_command_topic": "humidity-command-topic", + }, + True, + ), + ( + { + "name": "test_valid_2", + "command_topic": "command-topic", + "target_humidity_command_topic": "humidity-command-topic", + "device_class": "humidifier", + }, + True, + ), + ( + { + "name": "test_valid_3", + "command_topic": "command-topic", + "target_humidity_command_topic": "humidity-command-topic", + "device_class": "dehumidifier", + }, + True, + ), + ( + { + "name": "test_invalid_device_class", + "command_topic": "command-topic", + "target_humidity_command_topic": "humidity-command-topic", + "device_class": "notsupporedSpeci@l", + }, + False, + ), + ( + { + "name": "test_mode_command_without_modes", + "command_topic": "command-topic", + "target_humidity_command_topic": "humidity-command-topic", + "mode_command_topic": "mode-command-topic", + }, + False, + ), + ( + { + "name": "test_invalid_humidity_min_max_1", + "command_topic": "command-topic", + "target_humidity_command_topic": "humidity-command-topic", + "min_humidity": 0, + "max_humidity": 101, + }, + False, + ), + ( + { + "name": "test_invalid_humidity_min_max_2", + "command_topic": "command-topic", + "target_humidity_command_topic": "humidity-command-topic", + "max_humidity": 20, + "min_humidity": 40, + }, + False, + ), + ( + { + "name": "test_invalid_mode_is_reset", + "command_topic": "command-topic", + "target_humidity_command_topic": "humidity-command-topic", + "mode_command_topic": "mode-command-topic", + "modes": ["eco", "None"], + }, + False, + ), + ], +) +async def test_validity_configurations(hass, config, valid): + """Test validity of configurations.""" + assert ( + await async_setup_component( + hass, + mqtt.DOMAIN, + {mqtt.DOMAIN: {humidifier.DOMAIN: config}}, + ) + is valid ) - await hass.async_block_till_done() - await mqtt_mock_entry_with_yaml_config() - assert hass.states.get("humidifier.test_valid_1") is not None - assert hass.states.get("humidifier.test_valid_2") is not None - assert hass.states.get("humidifier.test_valid_3") is not None - assert hass.states.get("humidifier.test_invalid_device_class") is None - assert hass.states.get("humidifier.test_mode_command_without_modes") is None - assert "not all values in the same group of inclusion" in caplog.text - caplog.clear() - - assert hass.states.get("humidifier.test_invalid_humidity_min_max_1") is None - assert hass.states.get("humidifier.test_invalid_humidity_min_max_2") is None - assert hass.states.get("humidifier.test_invalid_mode_is_reset") is None -async def test_supported_features(hass, mqtt_mock_entry_with_yaml_config): +@pytest.mark.parametrize( + "name,config,success,features", + [ + ( + "test1", + { + "name": "test1", + "command_topic": "command-topic", + "target_humidity_command_topic": "humidity-command-topic", + }, + True, + 0, + ), + ( + "test2", + { + "name": "test2", + "command_topic": "command-topic", + "target_humidity_command_topic": "humidity-command-topic", + "mode_command_topic": "mode-command-topic", + "modes": ["eco", "auto"], + }, + True, + humidifier.SUPPORT_MODES, + ), + ( + "test3", + { + "name": "test3", + "command_topic": "command-topic", + "target_humidity_command_topic": "humidity-command-topic", + }, + True, + 0, + ), + ( + "test4", + { + "name": "test4", + "command_topic": "command-topic", + "target_humidity_command_topic": "humidity-command-topic", + "mode_command_topic": "mode-command-topic", + "modes": ["eco", "auto"], + }, + True, + humidifier.SUPPORT_MODES, + ), + ( + "test5", + { + "name": "test5", + "command_topic": "command-topic", + }, + False, + None, + ), + ( + "test6", + { + "name": "test6", + "target_humidity_command_topic": "humidity-command-topic", + }, + False, + None, + ), + ], +) +async def test_supported_features( + hass, mqtt_mock_entry_with_yaml_config, name, config, success, features +): """Test supported features.""" - assert await async_setup_component( - hass, - humidifier.DOMAIN, - { - humidifier.DOMAIN: [ - { - "platform": "mqtt", - "name": "test1", - "command_topic": "command-topic", - "target_humidity_command_topic": "humidity-command-topic", - }, - { - "platform": "mqtt", - "name": "test2", - "command_topic": "command-topic", - "target_humidity_command_topic": "humidity-command-topic", - "mode_command_topic": "mode-command-topic", - "modes": ["eco", "auto"], - }, - { - "platform": "mqtt", - "name": "test3", - "command_topic": "command-topic", - "target_humidity_command_topic": "humidity-command-topic", - }, - { - "platform": "mqtt", - "name": "test4", - "command_topic": "command-topic", - "target_humidity_command_topic": "humidity-command-topic", - "mode_command_topic": "mode-command-topic", - "modes": ["eco", "auto"], - }, - { - "platform": "mqtt", - "name": "test5", - "command_topic": "command-topic", - }, - { - "platform": "mqtt", - "name": "test6", - "target_humidity_command_topic": "humidity-command-topic", - }, - ] - }, + assert ( + await async_setup_component( + hass, + mqtt.DOMAIN, + {mqtt.DOMAIN: {humidifier.DOMAIN: config}}, + ) + is success ) - await hass.async_block_till_done() - await mqtt_mock_entry_with_yaml_config() + if success: + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() - state = hass.states.get("humidifier.test1") - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 0 - - state = hass.states.get("humidifier.test2") - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == humidifier.SUPPORT_MODES - - state = hass.states.get("humidifier.test3") - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 0 - - state = hass.states.get("humidifier.test4") - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == humidifier.SUPPORT_MODES - - state = hass.states.get("humidifier.test5") - assert state is None - - state = hass.states.get("humidifier.test6") - assert state is None + state = hass.states.get(f"humidifier.{name}") + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == features async def test_availability_when_connection_lost( @@ -957,14 +996,14 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock_entry_with_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -974,7 +1013,7 @@ async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_conf hass, mqtt_mock_entry_with_yaml_config, humidifier.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, True, "state-topic", "1", @@ -987,7 +1026,7 @@ async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_confi hass, mqtt_mock_entry_with_yaml_config, humidifier.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, True, "state-topic", "1", @@ -999,7 +1038,7 @@ async def test_setting_attribute_via_mqtt_json_message( ): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock_entry_with_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -1011,7 +1050,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( hass, mqtt_mock_entry_no_yaml_config, humidifier.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, MQTT_HUMIDIFIER_ATTRIBUTES_BLOCKED, ) @@ -1019,7 +1058,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock_entry_with_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -1032,7 +1071,7 @@ async def test_update_with_json_attrs_not_dict( mqtt_mock_entry_with_yaml_config, caplog, humidifier.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, ) @@ -1045,14 +1084,18 @@ async def test_update_with_json_attrs_bad_json( mqtt_mock_entry_with_yaml_config, caplog, humidifier.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, ) async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock_entry_no_yaml_config, caplog, humidifier.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + humidifier.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) @@ -1148,42 +1191,42 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT fan device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock_entry_no_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT fan device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock_entry_no_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock_entry_no_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock_entry_no_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock_entry_with_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock_entry_no_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -1193,7 +1236,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): hass, mqtt_mock_entry_no_yaml_config, humidifier.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, humidifier.SERVICE_TURN_ON, ) @@ -1243,7 +1286,7 @@ async def test_publishing_with_custom_encoding( ): """Test publishing MQTT payload with different encoding.""" domain = humidifier.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG[domain]) + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) if topic == "mode_command_topic": config["modes"] = ["auto", "eco"] @@ -1264,7 +1307,7 @@ async def test_publishing_with_custom_encoding( async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = humidifier.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) @@ -1273,14 +1316,14 @@ async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_pa async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = humidifier.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = humidifier.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG[platform]) + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) config["name"] = "test" del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) @@ -1288,21 +1331,33 @@ async def test_setup_manual_entity_from_yaml(hass): async def test_config_schema_validation(hass): - """Test invalid platform options in the config schema do pass the config validation.""" + """Test invalid platform options in the config schema do not pass the config validation.""" platform = humidifier.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG[platform]) + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][platform]) config["name"] = "test" - del config["platform"] - CONFIG_SCHEMA({DOMAIN: {platform: config}}) - CONFIG_SCHEMA({DOMAIN: {platform: [config]}}) + CONFIG_SCHEMA({mqtt.DOMAIN: {platform: config}}) + CONFIG_SCHEMA({mqtt.DOMAIN: {platform: [config]}}) with pytest.raises(MultipleInvalid): - CONFIG_SCHEMA({"mqtt": {"humidifier": [{"bla": "bla"}]}}) + CONFIG_SCHEMA({mqtt.DOMAIN: {platform: [{"bla": "bla"}]}}) async def test_unload_config_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): """Test unloading the config entry.""" domain = humidifier.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) + + +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +async def test_setup_with_legacy_schema(hass, mqtt_mock_entry_with_yaml_config): + """Test a setup with deprecated yaml platform schema.""" + domain = humidifier.DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) + config["name"] = "test" + assert await async_setup_component(hass, domain, {domain: config}) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + assert hass.states.get(f"{domain}.test") is not None diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 020fae1732b..b794d9260ba 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -281,6 +281,7 @@ async def test_command_template_value(hass): assert cmd_tpl.async_render(None, variables=variables) == "beer" +@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.SELECT]) async def test_command_template_variables(hass, mqtt_mock_entry_with_yaml_config): """Test the rendering of entity variables.""" topic = "test/select" @@ -290,14 +291,15 @@ async def test_command_template_variables(hass, mqtt_mock_entry_with_yaml_config assert await async_setup_component( hass, - "select", + mqtt.DOMAIN, { - "select": { - "platform": "mqtt", - "command_topic": topic, - "name": "Test Select", - "options": ["milk", "beer"], - "command_template": '{"option": "{{ value }}", "entity_id": "{{ entity_id }}", "name": "{{ name }}", "this_object_state": "{{ this.state }}"}', + mqtt.DOMAIN: { + "select": { + "command_topic": topic, + "name": "Test Select", + "options": ["milk", "beer"], + "command_template": '{"option": "{{ value }}", "entity_id": "{{ entity_id }}", "name": "{{ name }}", "this_object_state": "{{ this.state }}"}', + } } }, ) @@ -2087,20 +2089,19 @@ async def test_mqtt_ws_get_device_debug_info( await mqtt_mock_entry_no_yaml_config() config_sensor = { "device": {"identifiers": ["0AFFD2"]}, - "platform": "mqtt", "state_topic": "foobar/sensor", "unique_id": "unique", } config_trigger = { "automation_type": "trigger", "device": {"identifiers": ["0AFFD2"]}, - "platform": "mqtt", "topic": "test-topic1", "type": "foo", "subtype": "bar", } data_sensor = json.dumps(config_sensor) data_trigger = json.dumps(config_trigger) + config_sensor["platform"] = config_trigger["platform"] = mqtt.DOMAIN async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data_sensor) async_fire_mqtt_message( @@ -2151,11 +2152,11 @@ async def test_mqtt_ws_get_device_debug_info_binary( await mqtt_mock_entry_no_yaml_config() config = { "device": {"identifiers": ["0AFFD2"]}, - "platform": "mqtt", "topic": "foobar/image", "unique_id": "unique", } data = json.dumps(config) + config["platform"] = mqtt.DOMAIN async_fire_mqtt_message(hass, "homeassistant/camera/bla/config", data) await hass.async_block_till_done() @@ -2397,7 +2398,9 @@ async def test_debug_info_non_mqtt( device_id=device_entry.id, ) - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {"platform": "test"}}) + assert await async_setup_component( + hass, mqtt.DOMAIN, {mqtt.DOMAIN: {DOMAIN: {"platform": "test"}}} + ) debug_info_data = debug_info.info_for_device(hass, device_entry.id) assert len(debug_info_data["entities"]) == 0 @@ -2409,7 +2412,6 @@ async def test_debug_info_wildcard(hass, mqtt_mock_entry_no_yaml_config): await mqtt_mock_entry_no_yaml_config() config = { "device": {"identifiers": ["helloworld"]}, - "platform": "mqtt", "name": "test", "state_topic": "sensor/#", "unique_id": "veryunique", @@ -2456,7 +2458,6 @@ async def test_debug_info_filter_same(hass, mqtt_mock_entry_no_yaml_config): await mqtt_mock_entry_no_yaml_config() config = { "device": {"identifiers": ["helloworld"]}, - "platform": "mqtt", "name": "test", "state_topic": "sensor/#", "unique_id": "veryunique", @@ -2515,7 +2516,6 @@ async def test_debug_info_same_topic(hass, mqtt_mock_entry_no_yaml_config): await mqtt_mock_entry_no_yaml_config() config = { "device": {"identifiers": ["helloworld"]}, - "platform": "mqtt", "name": "test", "state_topic": "sensor/status", "availability_topic": "sensor/status", @@ -2568,7 +2568,6 @@ async def test_debug_info_qos_retain(hass, mqtt_mock_entry_no_yaml_config): await mqtt_mock_entry_no_yaml_config() config = { "device": {"identifiers": ["helloworld"]}, - "platform": "mqtt", "name": "test", "state_topic": "sensor/#", "unique_id": "veryunique", @@ -2708,6 +2707,8 @@ async def test_subscribe_connection_status( assert mqtt_connected_calls[1] is False +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 async def test_one_deprecation_warning_per_platform( hass, mqtt_mock_entry_with_yaml_config, caplog ): @@ -2801,6 +2802,8 @@ async def test_reload_entry_with_new_config(hass, tmp_path): "mqtt": { "light": [{"name": "test_new_modern", "command_topic": "test-topic_new"}] }, + # Test deprecated YAML configuration under the platform key + # Scheduled to be removed in HA core 2022.12 "light": [ { "platform": "mqtt", @@ -2826,6 +2829,8 @@ async def test_disabling_and_enabling_entry(hass, tmp_path, caplog): "mqtt": { "light": [{"name": "test_new_modern", "command_topic": "test-topic_new"}] }, + # Test deprecated YAML configuration under the platform key + # Scheduled to be removed in HA core 2022.12 "light": [ { "platform": "mqtt", diff --git a/tests/components/mqtt/test_legacy_vacuum.py b/tests/components/mqtt/test_legacy_vacuum.py index 224e81781cf..ffe9183fe4c 100644 --- a/tests/components/mqtt/test_legacy_vacuum.py +++ b/tests/components/mqtt/test_legacy_vacuum.py @@ -5,7 +5,7 @@ from unittest.mock import patch import pytest -from homeassistant.components import vacuum +from homeassistant.components import mqtt, vacuum from homeassistant.components.mqtt.const import CONF_COMMAND_TOPIC from homeassistant.components.mqtt.vacuum import schema_legacy as mqttvacuum from homeassistant.components.mqtt.vacuum.schema import services_to_strings @@ -66,27 +66,37 @@ from tests.common import async_fire_mqtt_message from tests.components.vacuum import common DEFAULT_CONFIG = { - CONF_PLATFORM: "mqtt", - CONF_NAME: "mqtttest", - CONF_COMMAND_TOPIC: "vacuum/command", - mqttvacuum.CONF_SEND_COMMAND_TOPIC: "vacuum/send_command", - mqttvacuum.CONF_BATTERY_LEVEL_TOPIC: "vacuum/state", - mqttvacuum.CONF_BATTERY_LEVEL_TEMPLATE: "{{ value_json.battery_level }}", - mqttvacuum.CONF_CHARGING_TOPIC: "vacuum/state", - mqttvacuum.CONF_CHARGING_TEMPLATE: "{{ value_json.charging }}", - mqttvacuum.CONF_CLEANING_TOPIC: "vacuum/state", - mqttvacuum.CONF_CLEANING_TEMPLATE: "{{ value_json.cleaning }}", - mqttvacuum.CONF_DOCKED_TOPIC: "vacuum/state", - mqttvacuum.CONF_DOCKED_TEMPLATE: "{{ value_json.docked }}", - mqttvacuum.CONF_ERROR_TOPIC: "vacuum/state", - mqttvacuum.CONF_ERROR_TEMPLATE: "{{ value_json.error }}", - mqttvacuum.CONF_FAN_SPEED_TOPIC: "vacuum/state", - mqttvacuum.CONF_FAN_SPEED_TEMPLATE: "{{ value_json.fan_speed }}", - mqttvacuum.CONF_SET_FAN_SPEED_TOPIC: "vacuum/set_fan_speed", - mqttvacuum.CONF_FAN_SPEED_LIST: ["min", "medium", "high", "max"], + mqtt.DOMAIN: { + vacuum.DOMAIN: { + CONF_NAME: "mqtttest", + CONF_COMMAND_TOPIC: "vacuum/command", + mqttvacuum.CONF_SEND_COMMAND_TOPIC: "vacuum/send_command", + mqttvacuum.CONF_BATTERY_LEVEL_TOPIC: "vacuum/state", + mqttvacuum.CONF_BATTERY_LEVEL_TEMPLATE: "{{ value_json.battery_level }}", + mqttvacuum.CONF_CHARGING_TOPIC: "vacuum/state", + mqttvacuum.CONF_CHARGING_TEMPLATE: "{{ value_json.charging }}", + mqttvacuum.CONF_CLEANING_TOPIC: "vacuum/state", + mqttvacuum.CONF_CLEANING_TEMPLATE: "{{ value_json.cleaning }}", + mqttvacuum.CONF_DOCKED_TOPIC: "vacuum/state", + mqttvacuum.CONF_DOCKED_TEMPLATE: "{{ value_json.docked }}", + mqttvacuum.CONF_ERROR_TOPIC: "vacuum/state", + mqttvacuum.CONF_ERROR_TEMPLATE: "{{ value_json.error }}", + mqttvacuum.CONF_FAN_SPEED_TOPIC: "vacuum/state", + mqttvacuum.CONF_FAN_SPEED_TEMPLATE: "{{ value_json.fan_speed }}", + mqttvacuum.CONF_SET_FAN_SPEED_TOPIC: "vacuum/set_fan_speed", + mqttvacuum.CONF_FAN_SPEED_LIST: ["min", "medium", "high", "max"], + } + } } -DEFAULT_CONFIG_2 = {vacuum.DOMAIN: {"platform": "mqtt", "name": "test"}} +DEFAULT_CONFIG_2 = {mqtt.DOMAIN: {vacuum.DOMAIN: {"name": "test"}}} + +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +DEFAULT_CONFIG_LEGACY = deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) +DEFAULT_CONFIG_LEGACY[vacuum.DOMAIN][CONF_PLATFORM] = mqtt.DOMAIN +DEFAULT_CONFIG_2_LEGACY = deepcopy(DEFAULT_CONFIG_2[mqtt.DOMAIN]) +DEFAULT_CONFIG_2_LEGACY[vacuum.DOMAIN][CONF_PLATFORM] = mqtt.DOMAIN @pytest.fixture(autouse=True) @@ -98,9 +108,7 @@ def vacuum_platform_only(): async def test_default_supported_features(hass, mqtt_mock_entry_with_yaml_config): """Test that the correct supported features.""" - assert await async_setup_component( - hass, vacuum.DOMAIN, {vacuum.DOMAIN: DEFAULT_CONFIG} - ) + assert await async_setup_component(hass, vacuum.DOMAIN, DEFAULT_CONFIG_LEGACY) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() entity = hass.states.get("vacuum.mqtttest") @@ -120,12 +128,14 @@ async def test_default_supported_features(hass, mqtt_mock_entry_with_yaml_config async def test_all_commands(hass, mqtt_mock_entry_with_yaml_config): """Test simple commands to the vacuum.""" - config = deepcopy(DEFAULT_CONFIG) + config = deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][vacuum.DOMAIN]) config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( ALL_SERVICES, SERVICE_TO_STRING ) - assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) + assert await async_setup_component( + hass, mqtt.DOMAIN, {mqtt.DOMAIN: {vacuum.DOMAIN: config}} + ) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -202,13 +212,15 @@ async def test_commands_without_supported_features( hass, mqtt_mock_entry_with_yaml_config ): """Test commands which are not supported by the vacuum.""" - config = deepcopy(DEFAULT_CONFIG) + config = deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][vacuum.DOMAIN]) services = mqttvacuum.STRING_TO_SERVICE["status"] config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( services, SERVICE_TO_STRING ) - assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) + assert await async_setup_component( + hass, mqtt.DOMAIN, {mqtt.DOMAIN: {vacuum.DOMAIN: config}} + ) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -253,13 +265,15 @@ async def test_attributes_without_supported_features( hass, mqtt_mock_entry_with_yaml_config ): """Test attributes which are not supported by the vacuum.""" - config = deepcopy(DEFAULT_CONFIG) + config = deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][vacuum.DOMAIN]) services = mqttvacuum.STRING_TO_SERVICE["turn_on"] config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( services, SERVICE_TO_STRING ) - assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) + assert await async_setup_component( + hass, mqtt.DOMAIN, {mqtt.DOMAIN: {vacuum.DOMAIN: config}} + ) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -281,12 +295,14 @@ async def test_attributes_without_supported_features( async def test_status(hass, mqtt_mock_entry_with_yaml_config): """Test status updates from the vacuum.""" - config = deepcopy(DEFAULT_CONFIG) + config = deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][vacuum.DOMAIN]) config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( ALL_SERVICES, SERVICE_TO_STRING ) - assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) + assert await async_setup_component( + hass, mqtt.DOMAIN, {mqtt.DOMAIN: {vacuum.DOMAIN: config}} + ) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -322,12 +338,14 @@ async def test_status(hass, mqtt_mock_entry_with_yaml_config): async def test_status_battery(hass, mqtt_mock_entry_with_yaml_config): """Test status updates from the vacuum.""" - config = deepcopy(DEFAULT_CONFIG) + config = deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][vacuum.DOMAIN]) config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( ALL_SERVICES, SERVICE_TO_STRING ) - assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) + assert await async_setup_component( + hass, mqtt.DOMAIN, {mqtt.DOMAIN: {vacuum.DOMAIN: config}} + ) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -341,12 +359,14 @@ async def test_status_battery(hass, mqtt_mock_entry_with_yaml_config): async def test_status_cleaning(hass, mqtt_mock_entry_with_yaml_config): """Test status updates from the vacuum.""" - config = deepcopy(DEFAULT_CONFIG) + config = deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][vacuum.DOMAIN]) config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( ALL_SERVICES, SERVICE_TO_STRING ) - assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) + assert await async_setup_component( + hass, mqtt.DOMAIN, {mqtt.DOMAIN: {vacuum.DOMAIN: config}} + ) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -360,12 +380,14 @@ async def test_status_cleaning(hass, mqtt_mock_entry_with_yaml_config): async def test_status_docked(hass, mqtt_mock_entry_with_yaml_config): """Test status updates from the vacuum.""" - config = deepcopy(DEFAULT_CONFIG) + config = deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][vacuum.DOMAIN]) config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( ALL_SERVICES, SERVICE_TO_STRING ) - assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) + assert await async_setup_component( + hass, mqtt.DOMAIN, {mqtt.DOMAIN: {vacuum.DOMAIN: config}} + ) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -379,12 +401,14 @@ async def test_status_docked(hass, mqtt_mock_entry_with_yaml_config): async def test_status_charging(hass, mqtt_mock_entry_with_yaml_config): """Test status updates from the vacuum.""" - config = deepcopy(DEFAULT_CONFIG) + config = deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][vacuum.DOMAIN]) config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( ALL_SERVICES, SERVICE_TO_STRING ) - assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) + assert await async_setup_component( + hass, mqtt.DOMAIN, {mqtt.DOMAIN: {vacuum.DOMAIN: config}} + ) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -398,12 +422,14 @@ async def test_status_charging(hass, mqtt_mock_entry_with_yaml_config): async def test_status_fan_speed(hass, mqtt_mock_entry_with_yaml_config): """Test status updates from the vacuum.""" - config = deepcopy(DEFAULT_CONFIG) + config = deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][vacuum.DOMAIN]) config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( ALL_SERVICES, SERVICE_TO_STRING ) - assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) + assert await async_setup_component( + hass, mqtt.DOMAIN, {mqtt.DOMAIN: {vacuum.DOMAIN: config}} + ) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -417,12 +443,14 @@ async def test_status_fan_speed(hass, mqtt_mock_entry_with_yaml_config): async def test_status_fan_speed_list(hass, mqtt_mock_entry_with_yaml_config): """Test status updates from the vacuum.""" - config = deepcopy(DEFAULT_CONFIG) + config = deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][vacuum.DOMAIN]) config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( ALL_SERVICES, SERVICE_TO_STRING ) - assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) + assert await async_setup_component( + hass, mqtt.DOMAIN, {mqtt.DOMAIN: {vacuum.DOMAIN: config}} + ) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -435,13 +463,15 @@ async def test_status_no_fan_speed_list(hass, mqtt_mock_entry_with_yaml_config): If the vacuum doesn't support fan speed, fan speed list should be None. """ - config = deepcopy(DEFAULT_CONFIG) + config = deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][vacuum.DOMAIN]) services = ALL_SERVICES - VacuumEntityFeature.FAN_SPEED config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( services, SERVICE_TO_STRING ) - assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) + assert await async_setup_component( + hass, mqtt.DOMAIN, {mqtt.DOMAIN: {vacuum.DOMAIN: config}} + ) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -451,12 +481,14 @@ async def test_status_no_fan_speed_list(hass, mqtt_mock_entry_with_yaml_config): async def test_status_error(hass, mqtt_mock_entry_with_yaml_config): """Test status updates from the vacuum.""" - config = deepcopy(DEFAULT_CONFIG) + config = deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][vacuum.DOMAIN]) config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( ALL_SERVICES, SERVICE_TO_STRING ) - assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) + assert await async_setup_component( + hass, mqtt.DOMAIN, {mqtt.DOMAIN: {vacuum.DOMAIN: config}} + ) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -477,7 +509,7 @@ async def test_status_error(hass, mqtt_mock_entry_with_yaml_config): async def test_battery_template(hass, mqtt_mock_entry_with_yaml_config): """Test that you can use non-default templates for battery_level.""" - config = deepcopy(DEFAULT_CONFIG) + config = deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][vacuum.DOMAIN]) config.update( { mqttvacuum.CONF_SUPPORTED_FEATURES: services_to_strings( @@ -488,7 +520,9 @@ async def test_battery_template(hass, mqtt_mock_entry_with_yaml_config): } ) - assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) + assert await async_setup_component( + hass, mqtt.DOMAIN, {mqtt.DOMAIN: {vacuum.DOMAIN: config}} + ) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -500,12 +534,14 @@ async def test_battery_template(hass, mqtt_mock_entry_with_yaml_config): async def test_status_invalid_json(hass, mqtt_mock_entry_with_yaml_config): """Test to make sure nothing breaks if the vacuum sends bad JSON.""" - config = deepcopy(DEFAULT_CONFIG) + config = deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][vacuum.DOMAIN]) config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( ALL_SERVICES, SERVICE_TO_STRING ) - assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) + assert await async_setup_component( + hass, mqtt.DOMAIN, {mqtt.DOMAIN: {vacuum.DOMAIN: config}} + ) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -515,82 +551,64 @@ async def test_status_invalid_json(hass, mqtt_mock_entry_with_yaml_config): assert state.attributes.get(ATTR_STATUS) == "Stopped" -async def test_missing_battery_template(hass, mqtt_mock_entry_no_yaml_config): +async def test_missing_battery_template(hass): """Test to make sure missing template is not allowed.""" - config = deepcopy(DEFAULT_CONFIG) + config = deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][vacuum.DOMAIN]) config.pop(mqttvacuum.CONF_BATTERY_LEVEL_TEMPLATE) - assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) - await hass.async_block_till_done() - await mqtt_mock_entry_no_yaml_config() - - state = hass.states.get("vacuum.mqtttest") - assert state is None + assert not await async_setup_component( + hass, mqtt.DOMAIN, {mqtt.DOMAIN: {vacuum.DOMAIN: config}} + ) -async def test_missing_charging_template(hass, mqtt_mock_entry_no_yaml_config): +async def test_missing_charging_template(hass): """Test to make sure missing template is not allowed.""" - config = deepcopy(DEFAULT_CONFIG) + config = deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][vacuum.DOMAIN]) config.pop(mqttvacuum.CONF_CHARGING_TEMPLATE) - assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) - await hass.async_block_till_done() - await mqtt_mock_entry_no_yaml_config() - - state = hass.states.get("vacuum.mqtttest") - assert state is None + assert not await async_setup_component( + hass, mqtt.DOMAIN, {mqtt.DOMAIN: {vacuum.DOMAIN: config}} + ) -async def test_missing_cleaning_template(hass, mqtt_mock_entry_no_yaml_config): +async def test_missing_cleaning_template(hass): """Test to make sure missing template is not allowed.""" - config = deepcopy(DEFAULT_CONFIG) + config = deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][vacuum.DOMAIN]) config.pop(mqttvacuum.CONF_CLEANING_TEMPLATE) - assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) - await hass.async_block_till_done() - await mqtt_mock_entry_no_yaml_config() - - state = hass.states.get("vacuum.mqtttest") - assert state is None + assert not await async_setup_component( + hass, mqtt.DOMAIN, {mqtt.DOMAIN: {vacuum.DOMAIN: config}} + ) -async def test_missing_docked_template(hass, mqtt_mock_entry_no_yaml_config): +async def test_missing_docked_template(hass): """Test to make sure missing template is not allowed.""" - config = deepcopy(DEFAULT_CONFIG) + config = deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][vacuum.DOMAIN]) config.pop(mqttvacuum.CONF_DOCKED_TEMPLATE) - assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) - await hass.async_block_till_done() - await mqtt_mock_entry_no_yaml_config() - - state = hass.states.get("vacuum.mqtttest") - assert state is None + assert not await async_setup_component( + hass, mqtt.DOMAIN, {mqtt.DOMAIN: {vacuum.DOMAIN: config}} + ) -async def test_missing_error_template(hass, mqtt_mock_entry_no_yaml_config): +async def test_missing_error_template(hass): """Test to make sure missing template is not allowed.""" - config = deepcopy(DEFAULT_CONFIG) + config = deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][vacuum.DOMAIN]) config.pop(mqttvacuum.CONF_ERROR_TEMPLATE) - assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) - await hass.async_block_till_done() - await mqtt_mock_entry_no_yaml_config() - - state = hass.states.get("vacuum.mqtttest") - assert state is None + assert not await async_setup_component( + hass, mqtt.DOMAIN, {mqtt.DOMAIN: {vacuum.DOMAIN: config}} + ) -async def test_missing_fan_speed_template(hass, mqtt_mock_entry_no_yaml_config): +async def test_missing_fan_speed_template(hass): """Test to make sure missing template is not allowed.""" - config = deepcopy(DEFAULT_CONFIG) + config = deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][vacuum.DOMAIN]) config.pop(mqttvacuum.CONF_FAN_SPEED_TEMPLATE) - assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) - await hass.async_block_till_done() - await mqtt_mock_entry_no_yaml_config() - - state = hass.states.get("vacuum.mqtttest") - assert state is None + assert not await async_setup_component( + hass, mqtt.DOMAIN, {mqtt.DOMAIN: {vacuum.DOMAIN: config}} + ) async def test_availability_when_connection_lost( @@ -598,28 +616,28 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY ) async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY ) async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY ) async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY ) @@ -628,7 +646,7 @@ async def test_setting_attribute_via_mqtt_json_message( ): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY ) @@ -640,7 +658,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, - DEFAULT_CONFIG_2, + DEFAULT_CONFIG_2_LEGACY, MQTT_LEGACY_VACUUM_ATTRIBUTES_BLOCKED, ) @@ -648,7 +666,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY ) @@ -657,7 +675,11 @@ async def test_update_with_json_attrs_not_dict( ): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock_entry_with_yaml_config, caplog, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + vacuum.DOMAIN, + DEFAULT_CONFIG_2_LEGACY, ) @@ -666,14 +688,22 @@ async def test_update_with_json_attrs_bad_JSON( ): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock_entry_with_yaml_config, caplog, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + vacuum.DOMAIN, + DEFAULT_CONFIG_2_LEGACY, ) async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock_entry_no_yaml_config, caplog, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + vacuum.DOMAIN, + DEFAULT_CONFIG_2_LEGACY, ) @@ -702,7 +732,7 @@ async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): async def test_discovery_removal_vacuum(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered vacuum.""" - data = json.dumps(DEFAULT_CONFIG_2[vacuum.DOMAIN]) + data = json.dumps(DEFAULT_CONFIG_2_LEGACY[vacuum.DOMAIN]) await help_test_discovery_removal( hass, mqtt_mock_entry_no_yaml_config, caplog, vacuum.DOMAIN, data ) @@ -748,28 +778,28 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT vacuum device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY ) async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT vacuum device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY ) async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY ) async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY ) @@ -797,7 +827,7 @@ async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_co async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY ) @@ -874,7 +904,7 @@ async def test_publishing_with_custom_encoding( ): """Test publishing MQTT payload with different encoding.""" domain = vacuum.DOMAIN - config = deepcopy(DEFAULT_CONFIG) + config = deepcopy(DEFAULT_CONFIG_LEGACY[domain]) config["supported_features"] = [ "turn_on", "turn_off", @@ -900,7 +930,7 @@ async def test_publishing_with_custom_encoding( async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = vacuum.DOMAIN - config = DEFAULT_CONFIG + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) @@ -909,7 +939,7 @@ async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_pa async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = vacuum.DOMAIN - config = DEFAULT_CONFIG + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) @@ -944,7 +974,8 @@ async def test_encoding_subscribable_topics( attribute_value, ): """Test handling of incoming encoded payload.""" - config = deepcopy(DEFAULT_CONFIG) + domain = vacuum.DOMAIN + config = deepcopy(DEFAULT_CONFIG_LEGACY[domain]) config[CONF_SUPPORTED_FEATURES] = [ "turn_on", "turn_off", @@ -976,8 +1007,21 @@ async def test_encoding_subscribable_topics( async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = vacuum.DOMAIN - config = deepcopy(DEFAULT_CONFIG) + config = deepcopy(DEFAULT_CONFIG_LEGACY[platform]) config["name"] = "test" del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None + + +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +async def test_setup_with_legacy_schema(hass, mqtt_mock_entry_with_yaml_config): + """Test a setup with deprecated yaml platform schema.""" + domain = vacuum.DOMAIN + config = deepcopy(DEFAULT_CONFIG_LEGACY[domain]) + config["name"] = "test" + assert await async_setup_component(hass, domain, {domain: config}) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + assert hass.states.get(f"{domain}.test") is not None From 13188a5c6324758270573e30706518be408ee7b3 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 6 Sep 2022 11:03:02 +0200 Subject: [PATCH 133/955] Refactor MQTT tests to use modern platform schema part 2 (#77525) * Tests light json * Tests light template * Missed test light json * Tests light * Tests lock * Tests number * Tests scene * Tests select * Tests sensor * Tests siren * Tests state vacuuum * Tests switch * Derive DEFAULT_CONFIG_LEGACY from DEFAULT_CONFIG * Suggested comment changes --- tests/components/mqtt/test_light.py | 506 ++++++++-------- tests/components/mqtt/test_light_json.py | 601 ++++++++++--------- tests/components/mqtt/test_light_template.py | 335 ++++++----- tests/components/mqtt/test_lock.py | 306 +++++----- tests/components/mqtt/test_number.py | 255 ++++---- tests/components/mqtt/test_scene.py | 62 +- tests/components/mqtt/test_select.py | 213 ++++--- tests/components/mqtt/test_sensor.py | 419 +++++++------ tests/components/mqtt/test_siren.py | 193 +++--- tests/components/mqtt/test_state_vacuum.py | 135 +++-- tests/components/mqtt/test_switch.py | 171 +++--- 11 files changed, 1782 insertions(+), 1414 deletions(-) diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index e916276cb4a..b34406f42bc 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -2,169 +2,170 @@ Configuration for RGB Version with brightness: -light: - platform: mqtt - name: "Office Light RGB" - state_topic: "office/rgb1/light/status" - command_topic: "office/rgb1/light/switch" - brightness_state_topic: "office/rgb1/brightness/status" - brightness_command_topic: "office/rgb1/brightness/set" - rgb_state_topic: "office/rgb1/rgb/status" - rgb_command_topic: "office/rgb1/rgb/set" - qos: 0 - payload_on: "on" - payload_off: "off" +mqtt: + light: + - name: "Office Light RGB" + state_topic: "office/rgb1/light/status" + command_topic: "office/rgb1/light/switch" + brightness_state_topic: "office/rgb1/brightness/status" + brightness_command_topic: "office/rgb1/brightness/set" + rgb_state_topic: "office/rgb1/rgb/status" + rgb_command_topic: "office/rgb1/rgb/set" + qos: 0 + payload_on: "on" + payload_off: "off" Configuration for XY Version with brightness: -light: - platform: mqtt - name: "Office Light XY" - state_topic: "office/xy1/light/status" - command_topic: "office/xy1/light/switch" - brightness_state_topic: "office/xy1/brightness/status" - brightness_command_topic: "office/xy1/brightness/set" - xy_state_topic: "office/xy1/xy/status" - xy_command_topic: "office/xy1/xy/set" - qos: 0 - payload_on: "on" - payload_off: "off" +mqtt: + light: + - platform: mqtt + name: "Office Light XY" + state_topic: "office/xy1/light/status" + command_topic: "office/xy1/light/switch" + brightness_state_topic: "office/xy1/brightness/status" + brightness_command_topic: "office/xy1/brightness/set" + xy_state_topic: "office/xy1/xy/status" + xy_command_topic: "office/xy1/xy/set" + qos: 0 + payload_on: "on" + payload_off: "off" config without RGB: -light: - platform: mqtt - name: "Office Light" - state_topic: "office/rgb1/light/status" - command_topic: "office/rgb1/light/switch" - brightness_state_topic: "office/rgb1/brightness/status" - brightness_command_topic: "office/rgb1/brightness/set" - qos: 0 - payload_on: "on" - payload_off: "off" +mqtt: + light: + - name: "Office Light" + state_topic: "office/rgb1/light/status" + command_topic: "office/rgb1/light/switch" + brightness_state_topic: "office/rgb1/brightness/status" + brightness_command_topic: "office/rgb1/brightness/set" + qos: 0 + payload_on: "on" + payload_off: "off" config without RGB and brightness: -light: - platform: mqtt - name: "Office Light" - state_topic: "office/rgb1/light/status" - command_topic: "office/rgb1/light/switch" - qos: 0 - payload_on: "on" - payload_off: "off" +mqtt: + light: + - name: "Office Light" + state_topic: "office/rgb1/light/status" + command_topic: "office/rgb1/light/switch" + qos: 0 + payload_on: "on" + payload_off: "off" config for RGB Version with brightness and scale: -light: - platform: mqtt - name: "Office Light RGB" - state_topic: "office/rgb1/light/status" - command_topic: "office/rgb1/light/switch" - brightness_state_topic: "office/rgb1/brightness/status" - brightness_command_topic: "office/rgb1/brightness/set" - brightness_scale: 99 - rgb_state_topic: "office/rgb1/rgb/status" - rgb_command_topic: "office/rgb1/rgb/set" - rgb_scale: 99 - qos: 0 - payload_on: "on" - payload_off: "off" +mqtt: + light: + - name: "Office Light RGB" + state_topic: "office/rgb1/light/status" + command_topic: "office/rgb1/light/switch" + brightness_state_topic: "office/rgb1/brightness/status" + brightness_command_topic: "office/rgb1/brightness/set" + brightness_scale: 99 + rgb_state_topic: "office/rgb1/rgb/status" + rgb_command_topic: "office/rgb1/rgb/set" + rgb_scale: 99 + qos: 0 + payload_on: "on" + payload_off: "off" config with brightness and color temp -light: - platform: mqtt - name: "Office Light Color Temp" - state_topic: "office/rgb1/light/status" - command_topic: "office/rgb1/light/switch" - brightness_state_topic: "office/rgb1/brightness/status" - brightness_command_topic: "office/rgb1/brightness/set" - brightness_scale: 99 - color_temp_state_topic: "office/rgb1/color_temp/status" - color_temp_command_topic: "office/rgb1/color_temp/set" - qos: 0 - payload_on: "on" - payload_off: "off" +mqtt: + light: + - name: "Office Light Color Temp" + state_topic: "office/rgb1/light/status" + command_topic: "office/rgb1/light/switch" + brightness_state_topic: "office/rgb1/brightness/status" + brightness_command_topic: "office/rgb1/brightness/set" + brightness_scale: 99 + color_temp_state_topic: "office/rgb1/color_temp/status" + color_temp_command_topic: "office/rgb1/color_temp/set" + qos: 0 + payload_on: "on" + payload_off: "off" config with brightness and effect -light: - platform: mqtt - name: "Office Light Color Temp" - state_topic: "office/rgb1/light/status" - command_topic: "office/rgb1/light/switch" - brightness_state_topic: "office/rgb1/brightness/status" - brightness_command_topic: "office/rgb1/brightness/set" - brightness_scale: 99 - effect_state_topic: "office/rgb1/effect/status" - effect_command_topic: "office/rgb1/effect/set" - effect_list: - - rainbow - - colorloop - qos: 0 - payload_on: "on" - payload_off: "off" +mqtt: + light: + - name: "Office Light Color Temp" + state_topic: "office/rgb1/light/status" + command_topic: "office/rgb1/light/switch" + brightness_state_topic: "office/rgb1/brightness/status" + brightness_command_topic: "office/rgb1/brightness/set" + brightness_scale: 99 + effect_state_topic: "office/rgb1/effect/status" + effect_command_topic: "office/rgb1/effect/set" + effect_list: + - rainbow + - colorloop + qos: 0 + payload_on: "on" + payload_off: "off" config for RGB Version with RGB command template: -light: - platform: mqtt - name: "Office Light RGB" - state_topic: "office/rgb1/light/status" - command_topic: "office/rgb1/light/switch" - rgb_state_topic: "office/rgb1/rgb/status" - rgb_command_topic: "office/rgb1/rgb/set" - rgb_command_template: "{{ '#%02x%02x%02x' | format(red, green, blue)}}" - qos: 0 - payload_on: "on" - payload_off: "off" +mqtt: + light: + - name: "Office Light RGB" + state_topic: "office/rgb1/light/status" + command_topic: "office/rgb1/light/switch" + rgb_state_topic: "office/rgb1/rgb/status" + rgb_command_topic: "office/rgb1/rgb/set" + rgb_command_template: "{{ '#%02x%02x%02x' | format(red, green, blue)}}" + qos: 0 + payload_on: "on" + payload_off: "off" Configuration for HS Version with brightness: -light: - platform: mqtt - name: "Office Light HS" - state_topic: "office/hs1/light/status" - command_topic: "office/hs1/light/switch" - brightness_state_topic: "office/hs1/brightness/status" - brightness_command_topic: "office/hs1/brightness/set" - hs_state_topic: "office/hs1/hs/status" - hs_command_topic: "office/hs1/hs/set" - qos: 0 - payload_on: "on" - payload_off: "off" +mqtt: + light: + - name: "Office Light HS" + state_topic: "office/hs1/light/status" + command_topic: "office/hs1/light/switch" + brightness_state_topic: "office/hs1/brightness/status" + brightness_command_topic: "office/hs1/brightness/set" + hs_state_topic: "office/hs1/hs/status" + hs_command_topic: "office/hs1/hs/set" + qos: 0 + payload_on: "on" + payload_off: "off" Configuration with brightness command template: -light: - platform: mqtt - name: "Office Light" - state_topic: "office/rgb1/light/status" - command_topic: "office/rgb1/light/switch" - brightness_state_topic: "office/rgb1/brightness/status" - brightness_command_topic: "office/rgb1/brightness/set" - brightness_command_template: '{ "brightness": "{{ value }}" }' - qos: 0 - payload_on: "on" - payload_off: "off" +mqtt: + light: + - name: "Office Light" + state_topic: "office/rgb1/light/status" + command_topic: "office/rgb1/light/switch" + brightness_state_topic: "office/rgb1/brightness/status" + brightness_command_topic: "office/rgb1/brightness/set" + brightness_command_template: '{ "brightness": "{{ value }}" }' + qos: 0 + payload_on: "on" + payload_off: "off" Configuration with effect command template: -light: - platform: mqtt - name: "Office Light Color Temp" - state_topic: "office/rgb1/light/status" - command_topic: "office/rgb1/light/switch" - effect_state_topic: "office/rgb1/effect/status" - effect_command_topic: "office/rgb1/effect/set" - effect_command_template: '{ "effect": "{{ value }}" }' - effect_list: - - rainbow - - colorloop - qos: 0 - payload_on: "on" - payload_off: "off" +mqtt: + light: + - name: "Office Light Color Temp" + state_topic: "office/rgb1/light/status" + command_topic: "office/rgb1/light/switch" + effect_state_topic: "office/rgb1/effect/status" + effect_command_topic: "office/rgb1/effect/set" + effect_command_template: '{ "effect": "{{ value }}" }' + effect_list: + - rainbow + - colorloop + qos: 0 + payload_on: "on" + payload_off: "off" """ import copy @@ -172,7 +173,7 @@ from unittest.mock import call, patch import pytest -from homeassistant.components import light +from homeassistant.components import light, mqtt from homeassistant.components.mqtt.light.schema_basic import ( CONF_BRIGHTNESS_COMMAND_TOPIC, CONF_COLOR_TEMP_COMMAND_TOPIC, @@ -226,17 +227,18 @@ from .test_common import ( help_test_update_with_json_attrs_not_dict, ) -from tests.common import ( - assert_setup_component, - async_fire_mqtt_message, - mock_restore_cache, -) +from tests.common import async_fire_mqtt_message, mock_restore_cache from tests.components.light import common DEFAULT_CONFIG = { - light.DOMAIN: {"platform": "mqtt", "name": "test", "command_topic": "test-topic"} + mqtt.DOMAIN: {light.DOMAIN: {"name": "test", "command_topic": "test-topic"}} } +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +DEFAULT_CONFIG_LEGACY = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) +DEFAULT_CONFIG_LEGACY[light.DOMAIN]["platform"] = mqtt.DOMAIN + @pytest.fixture(autouse=True) def light_platform_only(): @@ -245,14 +247,15 @@ def light_platform_only(): yield -async def test_fail_setup_if_no_command_topic(hass, mqtt_mock_entry_no_yaml_config): +async def test_fail_setup_if_no_command_topic(hass, caplog): """Test if command fails with command topic.""" - assert await async_setup_component( - hass, light.DOMAIN, {light.DOMAIN: {"platform": "mqtt", "name": "test"}} + assert not await async_setup_component( + hass, mqtt.DOMAIN, {mqtt.DOMAIN: {light.DOMAIN: {"name": "test"}}} + ) + assert ( + "Invalid config for [mqtt]: required key not provided @ data['mqtt']['light'][0]['command_topic']. Got None." + in caplog.text ) - await hass.async_block_till_done() - await mqtt_mock_entry_no_yaml_config() - assert hass.states.get("light.test") is None async def test_no_color_brightness_color_temp_hs_white_xy_if_no_topics( @@ -261,13 +264,14 @@ async def test_no_color_brightness_color_temp_hs_white_xy_if_no_topics( """Test if there is no color and brightness if no topic.""" assert await async_setup_component( hass, - light.DOMAIN, + mqtt.DOMAIN, { - light.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "test_light_rgb/status", - "command_topic": "test_light_rgb/set", + mqtt.DOMAIN: { + light.DOMAIN: { + "name": "test", + "state_topic": "test_light_rgb/status", + "command_topic": "test_light_rgb/set", + } } }, ) @@ -317,7 +321,6 @@ async def test_controlling_state_via_topic(hass, mqtt_mock_entry_with_yaml_confi """Test the controlling of the state via topic.""" config = { light.DOMAIN: { - "platform": "mqtt", "name": "test", "state_topic": "test_light_rgb/status", "command_topic": "test_light_rgb/set", @@ -344,7 +347,7 @@ async def test_controlling_state_via_topic(hass, mqtt_mock_entry_with_yaml_confi } color_modes = ["color_temp", "hs", "rgb", "rgbw", "rgbww", "xy"] - assert await async_setup_component(hass, light.DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -437,7 +440,6 @@ async def test_invalid_state_via_topic(hass, mqtt_mock_entry_with_yaml_config, c """Test handling of empty data via topic.""" config = { light.DOMAIN: { - "platform": "mqtt", "name": "test", "state_topic": "test_light_rgb/status", "command_topic": "test_light_rgb/set", @@ -464,7 +466,7 @@ async def test_invalid_state_via_topic(hass, mqtt_mock_entry_with_yaml_config, c } } - assert await async_setup_component(hass, light.DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -570,13 +572,12 @@ async def test_invalid_state_via_topic(hass, mqtt_mock_entry_with_yaml_config, c async def test_brightness_controlling_scale(hass, mqtt_mock_entry_with_yaml_config): """Test the brightness controlling scale.""" - with assert_setup_component(1, light.DOMAIN): - assert await async_setup_component( - hass, - light.DOMAIN, - { + assert await async_setup_component( + hass, + mqtt.DOMAIN, + { + mqtt.DOMAIN: { light.DOMAIN: { - "platform": "mqtt", "name": "test", "state_topic": "test_scale/status", "command_topic": "test_scale/set", @@ -587,10 +588,11 @@ async def test_brightness_controlling_scale(hass, mqtt_mock_entry_with_yaml_conf "payload_on": "on", "payload_off": "off", } - }, - ) - await hass.async_block_till_done() - await mqtt_mock_entry_with_yaml_config() + } + }, + ) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -620,13 +622,12 @@ async def test_brightness_from_rgb_controlling_scale( hass, mqtt_mock_entry_with_yaml_config ): """Test the brightness controlling scale.""" - with assert_setup_component(1, light.DOMAIN): - assert await async_setup_component( - hass, - light.DOMAIN, - { + assert await async_setup_component( + hass, + mqtt.DOMAIN, + { + mqtt.DOMAIN: { light.DOMAIN: { - "platform": "mqtt", "name": "test", "state_topic": "test_scale_rgb/status", "command_topic": "test_scale_rgb/set", @@ -636,10 +637,11 @@ async def test_brightness_from_rgb_controlling_scale( "payload_on": "on", "payload_off": "off", } - }, - ) - await hass.async_block_till_done() - await mqtt_mock_entry_with_yaml_config() + } + }, + ) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -664,7 +666,6 @@ async def test_controlling_state_via_topic_with_templates( """Test the setting of the state with a template.""" config = { light.DOMAIN: { - "platform": "mqtt", "name": "test", "state_topic": "test_light_rgb/status", "command_topic": "test_light_rgb/set", @@ -697,7 +698,7 @@ async def test_controlling_state_via_topic_with_templates( } color_modes = ["color_temp", "hs", "rgb", "rgbw", "rgbww", "xy"] - assert await async_setup_component(hass, light.DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -767,7 +768,6 @@ async def test_sending_mqtt_commands_and_optimistic( """Test the sending of command in optimistic mode.""" config = { light.DOMAIN: { - "platform": "mqtt", "name": "test", "command_topic": "test_light_rgb/set", "brightness_command_topic": "test_light_rgb/brightness/set", @@ -798,10 +798,9 @@ async def test_sending_mqtt_commands_and_optimistic( ) mock_restore_cache(hass, (fake_state,)) - with assert_setup_component(1, light.DOMAIN): - assert await async_setup_component(hass, light.DOMAIN, config) - await hass.async_block_till_done() - mqtt_mock = await mqtt_mock_entry_with_yaml_config() + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) + await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_ON @@ -958,7 +957,6 @@ async def test_sending_mqtt_rgb_command_with_template( """Test the sending of RGB command with template.""" config = { light.DOMAIN: { - "platform": "mqtt", "name": "test", "command_topic": "test_light_rgb/set", "rgb_command_topic": "test_light_rgb/rgb/set", @@ -970,7 +968,7 @@ async def test_sending_mqtt_rgb_command_with_template( } } - assert await async_setup_component(hass, light.DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -998,7 +996,6 @@ async def test_sending_mqtt_rgbw_command_with_template( """Test the sending of RGBW command with template.""" config = { light.DOMAIN: { - "platform": "mqtt", "name": "test", "command_topic": "test_light_rgb/set", "rgbw_command_topic": "test_light_rgb/rgbw/set", @@ -1010,7 +1007,7 @@ async def test_sending_mqtt_rgbw_command_with_template( } } - assert await async_setup_component(hass, light.DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -1038,7 +1035,6 @@ async def test_sending_mqtt_rgbww_command_with_template( """Test the sending of RGBWW command with template.""" config = { light.DOMAIN: { - "platform": "mqtt", "name": "test", "command_topic": "test_light_rgb/set", "rgbww_command_topic": "test_light_rgb/rgbww/set", @@ -1050,7 +1046,7 @@ async def test_sending_mqtt_rgbww_command_with_template( } } - assert await async_setup_component(hass, light.DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -1078,7 +1074,6 @@ async def test_sending_mqtt_color_temp_command_with_template( """Test the sending of Color Temp command with template.""" config = { light.DOMAIN: { - "platform": "mqtt", "name": "test", "command_topic": "test_light_color_temp/set", "color_temp_command_topic": "test_light_color_temp/color_temp/set", @@ -1089,7 +1084,7 @@ async def test_sending_mqtt_color_temp_command_with_template( } } - assert await async_setup_component(hass, light.DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -1115,7 +1110,6 @@ async def test_on_command_first(hass, mqtt_mock_entry_with_yaml_config): """Test on command being sent before brightness.""" config = { light.DOMAIN: { - "platform": "mqtt", "name": "test", "command_topic": "test_light/set", "brightness_command_topic": "test_light/bright", @@ -1123,7 +1117,7 @@ async def test_on_command_first(hass, mqtt_mock_entry_with_yaml_config): } } - assert await async_setup_component(hass, light.DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -1152,14 +1146,13 @@ async def test_on_command_last(hass, mqtt_mock_entry_with_yaml_config): """Test on command being sent after brightness.""" config = { light.DOMAIN: { - "platform": "mqtt", "name": "test", "command_topic": "test_light/set", "brightness_command_topic": "test_light/bright", } } - assert await async_setup_component(hass, light.DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -1188,7 +1181,6 @@ async def test_on_command_brightness(hass, mqtt_mock_entry_with_yaml_config): """Test on command being sent as only brightness.""" config = { light.DOMAIN: { - "platform": "mqtt", "name": "test", "command_topic": "test_light/set", "brightness_command_topic": "test_light/bright", @@ -1197,7 +1189,7 @@ async def test_on_command_brightness(hass, mqtt_mock_entry_with_yaml_config): } } - assert await async_setup_component(hass, light.DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -1244,7 +1236,6 @@ async def test_on_command_brightness_scaled(hass, mqtt_mock_entry_with_yaml_conf """Test brightness scale.""" config = { light.DOMAIN: { - "platform": "mqtt", "name": "test", "command_topic": "test_light/set", "brightness_command_topic": "test_light/bright", @@ -1254,7 +1245,7 @@ async def test_on_command_brightness_scaled(hass, mqtt_mock_entry_with_yaml_conf } } - assert await async_setup_component(hass, light.DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -1315,14 +1306,13 @@ async def test_on_command_rgb(hass, mqtt_mock_entry_with_yaml_config): """Test on command in RGB brightness mode.""" config = { light.DOMAIN: { - "platform": "mqtt", "name": "test", "command_topic": "test_light/set", "rgb_command_topic": "test_light/rgb", } } - assert await async_setup_component(hass, light.DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -1406,14 +1396,13 @@ async def test_on_command_rgbw(hass, mqtt_mock_entry_with_yaml_config): """Test on command in RGBW brightness mode.""" config = { light.DOMAIN: { - "platform": "mqtt", "name": "test", "command_topic": "test_light/set", "rgbw_command_topic": "test_light/rgbw", } } - assert await async_setup_component(hass, light.DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -1497,14 +1486,13 @@ async def test_on_command_rgbww(hass, mqtt_mock_entry_with_yaml_config): """Test on command in RGBWW brightness mode.""" config = { light.DOMAIN: { - "platform": "mqtt", "name": "test", "command_topic": "test_light/set", "rgbww_command_topic": "test_light/rgbww", } } - assert await async_setup_component(hass, light.DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -1588,7 +1576,6 @@ async def test_on_command_rgb_template(hass, mqtt_mock_entry_with_yaml_config): """Test on command in RGB brightness mode with RGB template.""" config = { light.DOMAIN: { - "platform": "mqtt", "name": "test", "command_topic": "test_light/set", "rgb_command_topic": "test_light/rgb", @@ -1596,7 +1583,7 @@ async def test_on_command_rgb_template(hass, mqtt_mock_entry_with_yaml_config): } } - assert await async_setup_component(hass, light.DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -1626,7 +1613,6 @@ async def test_on_command_rgbw_template(hass, mqtt_mock_entry_with_yaml_config): """Test on command in RGBW brightness mode with RGBW template.""" config = { light.DOMAIN: { - "platform": "mqtt", "name": "test", "command_topic": "test_light/set", "rgbw_command_topic": "test_light/rgbw", @@ -1634,7 +1620,7 @@ async def test_on_command_rgbw_template(hass, mqtt_mock_entry_with_yaml_config): } } - assert await async_setup_component(hass, light.DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -1663,7 +1649,6 @@ async def test_on_command_rgbww_template(hass, mqtt_mock_entry_with_yaml_config) """Test on command in RGBWW brightness mode with RGBWW template.""" config = { light.DOMAIN: { - "platform": "mqtt", "name": "test", "command_topic": "test_light/set", "rgbww_command_topic": "test_light/rgbww", @@ -1671,7 +1656,7 @@ async def test_on_command_rgbww_template(hass, mqtt_mock_entry_with_yaml_config) } } - assert await async_setup_component(hass, light.DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -1701,7 +1686,6 @@ async def test_on_command_white(hass, mqtt_mock_entry_with_yaml_config): """Test sending commands for RGB + white light.""" config = { light.DOMAIN: { - "platform": "mqtt", "name": "test", "command_topic": "tasmota_B94927/cmnd/POWER", "state_value_template": "{{ value_json.POWER }}", @@ -1721,7 +1705,7 @@ async def test_on_command_white(hass, mqtt_mock_entry_with_yaml_config): } color_modes = ["rgb", "white"] - assert await async_setup_component(hass, light.DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -1779,7 +1763,6 @@ async def test_explicit_color_mode(hass, mqtt_mock_entry_with_yaml_config): """Test explicit color mode over mqtt.""" config = { light.DOMAIN: { - "platform": "mqtt", "name": "test", "state_topic": "test_light_rgb/status", "command_topic": "test_light_rgb/set", @@ -1807,7 +1790,7 @@ async def test_explicit_color_mode(hass, mqtt_mock_entry_with_yaml_config): } color_modes = ["color_temp", "hs", "rgb", "rgbw", "rgbww", "xy"] - assert await async_setup_component(hass, light.DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -1928,7 +1911,6 @@ async def test_explicit_color_mode_templated(hass, mqtt_mock_entry_with_yaml_con """Test templated explicit color mode over mqtt.""" config = { light.DOMAIN: { - "platform": "mqtt", "name": "test", "state_topic": "test_light_rgb/status", "command_topic": "test_light_rgb/set", @@ -1947,7 +1929,7 @@ async def test_explicit_color_mode_templated(hass, mqtt_mock_entry_with_yaml_con } color_modes = ["color_temp", "hs"] - assert await async_setup_component(hass, light.DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -2010,7 +1992,6 @@ async def test_white_state_update(hass, mqtt_mock_entry_with_yaml_config): """Test state updates for RGB + white light.""" config = { light.DOMAIN: { - "platform": "mqtt", "name": "test", "state_topic": "tasmota_B94927/tele/STATE", "command_topic": "tasmota_B94927/cmnd/POWER", @@ -2034,7 +2015,7 @@ async def test_white_state_update(hass, mqtt_mock_entry_with_yaml_config): } color_modes = ["rgb", "white"] - assert await async_setup_component(hass, light.DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -2075,7 +2056,6 @@ async def test_effect(hass, mqtt_mock_entry_with_yaml_config): """Test effect.""" config = { light.DOMAIN: { - "platform": "mqtt", "name": "test", "command_topic": "test_light/set", "effect_command_topic": "test_light/effect/set", @@ -2083,7 +2063,7 @@ async def test_effect(hass, mqtt_mock_entry_with_yaml_config): } } - assert await async_setup_component(hass, light.DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -2114,28 +2094,28 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -2144,7 +2124,7 @@ async def test_setting_attribute_via_mqtt_json_message( ): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -2156,7 +2136,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, MQTT_LIGHT_ATTRIBUTES_BLOCKED, ) @@ -2164,7 +2144,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -2173,7 +2153,11 @@ async def test_update_with_json_attrs_not_dict( ): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock_entry_with_yaml_config, caplog, light.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + light.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) @@ -2182,14 +2166,22 @@ async def test_update_with_json_attrs_bad_JSON( ): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock_entry_with_yaml_config, caplog, light.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + light.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock_entry_no_yaml_config, caplog, light.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + light.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) @@ -2230,6 +2222,8 @@ async def test_discovery_removal_light(hass, mqtt_mock_entry_no_yaml_config, cap ) +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 async def test_discovery_deprecated(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test discovery of mqtt light with deprecated platform option.""" await mqtt_mock_entry_no_yaml_config() @@ -2750,42 +2744,42 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT light device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT light device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -2795,7 +2789,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, light.SERVICE_TURN_ON, ) @@ -2804,7 +2798,6 @@ async def test_max_mireds(hass, mqtt_mock_entry_with_yaml_config): """Test setting min_mireds and max_mireds.""" config = { light.DOMAIN: { - "platform": "mqtt", "name": "test", "command_topic": "test_max_mireds/set", "color_temp_command_topic": "test_max_mireds/color_temp/set", @@ -2812,7 +2805,7 @@ async def test_max_mireds(hass, mqtt_mock_entry_with_yaml_config): } } - assert await async_setup_component(hass, light.DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -2921,7 +2914,7 @@ async def test_publishing_with_custom_encoding( ): """Test publishing MQTT payload with different encoding.""" domain = light.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG[domain]) + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) if topic == "effect_command_topic": config["effect_list"] = ["random", "color_loop"] elif topic == "white_command_topic": @@ -2946,7 +2939,7 @@ async def test_publishing_with_custom_encoding( async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = light.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) @@ -2955,7 +2948,7 @@ async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_pa async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = light.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) @@ -3000,7 +2993,7 @@ async def test_encoding_subscribable_topics( init_payload, ): """Test handling of incoming encoded payload.""" - config = copy.deepcopy(DEFAULT_CONFIG[light.DOMAIN]) + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[light.DOMAIN]) config[CONF_EFFECT_COMMAND_TOPIC] = "light/CONF_EFFECT_COMMAND_TOPIC" config[CONF_RGB_COMMAND_TOPIC] = "light/CONF_RGB_COMMAND_TOPIC" config[CONF_BRIGHTNESS_COMMAND_TOPIC] = "light/CONF_BRIGHTNESS_COMMAND_TOPIC" @@ -3043,7 +3036,7 @@ async def test_encoding_subscribable_topics_brightness( init_payload, ): """Test handling of incoming encoded payload for a brightness only light.""" - config = copy.deepcopy(DEFAULT_CONFIG[light.DOMAIN]) + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[light.DOMAIN]) config[CONF_BRIGHTNESS_COMMAND_TOPIC] = "light/CONF_BRIGHTNESS_COMMAND_TOPIC" await help_test_encoding_subscribable_topics( @@ -3066,7 +3059,6 @@ async def test_sending_mqtt_brightness_command_with_template( """Test the sending of Brightness command with template.""" config = { light.DOMAIN: { - "platform": "mqtt", "name": "test", "command_topic": "test_light_brightness/set", "brightness_command_topic": "test_light_brightness/brightness/set", @@ -3077,7 +3069,7 @@ async def test_sending_mqtt_brightness_command_with_template( } } - assert await async_setup_component(hass, light.DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -3105,7 +3097,6 @@ async def test_sending_mqtt_effect_command_with_template( """Test the sending of Effect command with template.""" config = { light.DOMAIN: { - "platform": "mqtt", "name": "test", "command_topic": "test_light_brightness/set", "brightness_command_topic": "test_light_brightness/brightness/set", @@ -3118,7 +3109,7 @@ async def test_sending_mqtt_effect_command_with_template( } } - assert await async_setup_component(hass, light.DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -3147,7 +3138,7 @@ async def test_sending_mqtt_effect_command_with_template( async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = light.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG[platform]) + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) config["name"] = "test" del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) @@ -3157,7 +3148,20 @@ async def test_setup_manual_entity_from_yaml(hass): async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): """Test unloading the config entry.""" domain = light.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) + + +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +async def test_setup_with_legacy_schema(hass, mqtt_mock_entry_with_yaml_config): + """Test a setup with deprecated yaml platform schema.""" + domain = light.DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) + config["name"] = "test" + assert await async_setup_component(hass, domain, {domain: config}) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + assert hass.states.get(f"{domain}.test") is not None diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index 8fac9092e3f..e61d4e77286 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -2,58 +2,63 @@ Configuration with RGB, brightness, color temp, effect, and XY: -light: - platform: mqtt_json - name: mqtt_json_light_1 - state_topic: "home/rgb1" - command_topic: "home/rgb1/set" - brightness: true - color_temp: true - effect: true - rgb: true - xy: true +mqtt: + light: + schema: json + name: mqtt_json_light_1 + state_topic: "home/rgb1" + command_topic: "home/rgb1/set" + brightness: true + color_temp: true + effect: true + rgb: true + xy: true Configuration with RGB, brightness, color temp and effect: -light: - platform: mqtt_json - name: mqtt_json_light_1 - state_topic: "home/rgb1" - command_topic: "home/rgb1/set" - brightness: true - color_temp: true - effect: true - rgb: true +mqtt: + light: + schema: json + name: mqtt_json_light_1 + state_topic: "home/rgb1" + command_topic: "home/rgb1/set" + brightness: true + color_temp: true + effect: true + rgb: true Configuration with RGB, brightness and color temp: -light: - platform: mqtt_json - name: mqtt_json_light_1 - state_topic: "home/rgb1" - command_topic: "home/rgb1/set" - brightness: true - rgb: true - color_temp: true +mqtt: + light: + schema: json + name: mqtt_json_light_1 + state_topic: "home/rgb1" + command_topic: "home/rgb1/set" + brightness: true + rgb: true + color_temp: true Configuration with RGB, brightness: -light: - platform: mqtt_json - name: mqtt_json_light_1 - state_topic: "home/rgb1" - command_topic: "home/rgb1/set" - brightness: true - rgb: true +mqtt: + light: + schema: json + name: mqtt_json_light_1 + state_topic: "home/rgb1" + command_topic: "home/rgb1/set" + brightness: true + rgb: true Config without RGB: -light: - platform: mqtt_json - name: mqtt_json_light_1 - state_topic: "home/rgb1" - command_topic: "home/rgb1/set" - brightness: true +mqtt: + light: + schema: json + name: mqtt_json_light_1 + state_topic: "home/rgb1" + command_topic: "home/rgb1/set" + brightness: true Config without RGB and brightness: @@ -79,7 +84,7 @@ from unittest.mock import call, patch import pytest -from homeassistant.components import light +from homeassistant.components import light, mqtt from homeassistant.components.mqtt.light.schema_basic import ( MQTT_LIGHT_ATTRIBUTES_BLOCKED, ) @@ -128,14 +133,20 @@ from tests.common import async_fire_mqtt_message, mock_restore_cache from tests.components.light import common DEFAULT_CONFIG = { - light.DOMAIN: { - "platform": "mqtt", - "schema": "json", - "name": "test", - "command_topic": "test-topic", + mqtt.DOMAIN: { + light.DOMAIN: { + "schema": "json", + "name": "test", + "command_topic": "test-topic", + } } } +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +DEFAULT_CONFIG_LEGACY = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) +DEFAULT_CONFIG_LEGACY[light.DOMAIN]["platform"] = mqtt.DOMAIN + @pytest.fixture(autouse=True) def light_platform_only(): @@ -156,22 +167,21 @@ class JsonValidator: return json.loads(self.jsondata) == json.loads(other) -async def test_fail_setup_if_no_command_topic(hass, mqtt_mock_entry_no_yaml_config): +async def test_fail_setup_if_no_command_topic(hass, caplog): """Test if setup fails with no command topic.""" - assert await async_setup_component( + assert not await async_setup_component( hass, - light.DOMAIN, - {light.DOMAIN: {"platform": "mqtt", "schema": "json", "name": "test"}}, + mqtt.DOMAIN, + {mqtt.DOMAIN: {light.DOMAIN: {"schema": "json", "name": "test"}}}, + ) + assert ( + "Invalid config for [mqtt]: required key not provided @ data['mqtt']['light'][0]['command_topic']. Got None." + in caplog.text ) - await hass.async_block_till_done() - await mqtt_mock_entry_no_yaml_config() - assert hass.states.get("light.test") is None @pytest.mark.parametrize("deprecated", ("color_temp", "hs", "rgb", "xy")) -async def test_fail_setup_if_color_mode_deprecated( - hass, mqtt_mock_entry_no_yaml_config, deprecated -): +async def test_fail_setup_if_color_mode_deprecated(hass, caplog, deprecated): """Test if setup fails if color mode is combined with deprecated config keys.""" supported_color_modes = ["color_temp", "hs", "rgb", "rgbw", "rgbww", "xy"] @@ -181,27 +191,32 @@ async def test_fail_setup_if_color_mode_deprecated( "color_mode": True, "command_topic": "test_light_rgb/set", "name": "test", - "platform": "mqtt", "schema": "json", "supported_color_modes": supported_color_modes, } } config[light.DOMAIN][deprecated] = True - assert await async_setup_component( + assert not await async_setup_component( hass, - light.DOMAIN, - config, + mqtt.DOMAIN, + {mqtt.DOMAIN: config}, + ) + assert ( + "Invalid config for [mqtt]: color_mode must not be combined with any of" + in caplog.text ) - await hass.async_block_till_done() - await mqtt_mock_entry_no_yaml_config() - assert hass.states.get("light.test") is None @pytest.mark.parametrize( - "supported_color_modes", [["onoff", "rgb"], ["brightness", "rgb"], ["unknown"]] + "supported_color_modes,error", + [ + (["onoff", "rgb"], "Unknown error calling mqtt CONFIG_SCHEMA"), + (["brightness", "rgb"], "Unknown error calling mqtt CONFIG_SCHEMA"), + (["unknown"], "Invalid config for [mqtt]: value must be one of [ - on,{{ brightness|d }},{{ red|d }}-{{ green|d }}-{{ blue|d }} - command_off_template: 'off' - state_template: '{{ value.split(",")[0] }}' - brightness_template: '{{ value.split(",")[1] }}' - color_temp_template: '{{ value.split(",")[2] }}' - red_template: '{{ value.split(",")[4].split("-")[0] }}' - green_template: '{{ value.split(",")[4].split("-")[1] }}' - blue_template: '{{ value.split(",")[4].split("-")[2] }}' +mqtt: + light: + schema: template + name: mqtt_template_light_1 + state_topic: 'home/rgb1' + command_topic: 'home/rgb1/set' + command_on_template: > + on,{{ brightness|d }},{{ red|d }}-{{ green|d }}-{{ blue|d }} + command_off_template: 'off' + state_template: '{{ value.split(",")[0] }}' + brightness_template: '{{ value.split(",")[1] }}' + color_temp_template: '{{ value.split(",")[2] }}' + red_template: '{{ value.split(",")[4].split("-")[0] }}' + green_template: '{{ value.split(",")[4].split("-")[1] }}' + blue_template: '{{ value.split(",")[4].split("-")[2] }}' If your light doesn't support brightness feature, omit `brightness_template`. @@ -28,7 +29,7 @@ from unittest.mock import patch import pytest -from homeassistant.components import light +from homeassistant.components import light, mqtt from homeassistant.components.mqtt.light.schema_basic import ( MQTT_LIGHT_ATTRIBUTES_BLOCKED, ) @@ -74,24 +75,26 @@ from .test_common import ( help_test_update_with_json_attrs_not_dict, ) -from tests.common import ( - assert_setup_component, - async_fire_mqtt_message, - mock_restore_cache, -) +from tests.common import async_fire_mqtt_message, mock_restore_cache from tests.components.light import common DEFAULT_CONFIG = { - light.DOMAIN: { - "platform": "mqtt", - "schema": "template", - "name": "test", - "command_topic": "test-topic", - "command_on_template": "on,{{ transition }}", - "command_off_template": "off,{{ transition|d }}", + mqtt.DOMAIN: { + light.DOMAIN: { + "schema": "template", + "name": "test", + "command_topic": "test-topic", + "command_on_template": "on,{{ transition }}", + "command_off_template": "off,{{ transition|d }}", + } } } +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +DEFAULT_CONFIG_LEGACY = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) +DEFAULT_CONFIG_LEGACY[light.DOMAIN]["platform"] = mqtt.DOMAIN + @pytest.fixture(autouse=True) def light_platform_only(): @@ -103,10 +106,9 @@ def light_platform_only(): @pytest.mark.parametrize( "test_config", [ - ({"platform": "mqtt", "schema": "template", "name": "test"},), + ({"schema": "template", "name": "test"},), ( { - "platform": "mqtt", "schema": "template", "name": "test", "command_topic": "test_topic", @@ -114,7 +116,6 @@ def light_platform_only(): ), ( { - "platform": "mqtt", "schema": "template", "name": "test", "command_topic": "test_topic", @@ -123,7 +124,6 @@ def light_platform_only(): ), ( { - "platform": "mqtt", "schema": "template", "name": "test", "command_topic": "test_topic", @@ -132,36 +132,33 @@ def light_platform_only(): ), ], ) -async def test_setup_fails(hass, mqtt_mock_entry_no_yaml_config, test_config): +async def test_setup_fails(hass, caplog, test_config): """Test that setup fails with missing required configuration items.""" - with assert_setup_component(0, light.DOMAIN) as setup_config: - assert await async_setup_component( - hass, - light.DOMAIN, - {light.DOMAIN: test_config}, - ) - await hass.async_block_till_done() - await mqtt_mock_entry_no_yaml_config() - assert not setup_config[light.DOMAIN] - assert hass.states.get("light.test") is None + assert not await async_setup_component( + hass, + mqtt.DOMAIN, + {mqtt.DOMAIN: {light.DOMAIN: test_config}}, + ) + assert "Invalid config for [mqtt]" in caplog.text async def test_rgb_light(hass, mqtt_mock_entry_with_yaml_config): """Test RGB light flags brightness support.""" assert await async_setup_component( hass, - light.DOMAIN, + mqtt.DOMAIN, { - light.DOMAIN: { - "platform": "mqtt", - "schema": "template", - "name": "test", - "command_topic": "test_light_rgb/set", - "command_on_template": "on", - "command_off_template": "off", - "red_template": '{{ value.split(",")[4].' 'split("-")[0] }}', - "green_template": '{{ value.split(",")[4].' 'split("-")[1] }}', - "blue_template": '{{ value.split(",")[4].' 'split("-")[2] }}', + mqtt.DOMAIN: { + light.DOMAIN: { + "schema": "template", + "name": "test", + "command_topic": "test_light_rgb/set", + "command_on_template": "on", + "command_off_template": "off", + "red_template": '{{ value.split(",")[4].' 'split("-")[0] }}', + "green_template": '{{ value.split(",")[4].' 'split("-")[1] }}', + "blue_template": '{{ value.split(",")[4].' 'split("-")[2] }}', + } } }, ) @@ -181,13 +178,12 @@ async def test_rgb_light(hass, mqtt_mock_entry_with_yaml_config): async def test_state_change_via_topic(hass, mqtt_mock_entry_with_yaml_config): """Test state change via topic.""" - with assert_setup_component(1, light.DOMAIN): - assert await async_setup_component( - hass, - light.DOMAIN, - { + assert await async_setup_component( + hass, + mqtt.DOMAIN, + { + mqtt.DOMAIN: { light.DOMAIN: { - "platform": "mqtt", "schema": "template", "name": "test", "state_topic": "test_light_rgb", @@ -201,10 +197,11 @@ async def test_state_change_via_topic(hass, mqtt_mock_entry_with_yaml_config): "command_off_template": "off", "state_template": '{{ value.split(",")[0] }}', } - }, - ) - await hass.async_block_till_done() - await mqtt_mock_entry_with_yaml_config() + } + }, + ) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -236,13 +233,12 @@ async def test_state_brightness_color_effect_temp_change_via_topic( hass, mqtt_mock_entry_with_yaml_config ): """Test state, bri, color, effect, color temp change.""" - with assert_setup_component(1, light.DOMAIN): - assert await async_setup_component( - hass, - light.DOMAIN, - { + assert await async_setup_component( + hass, + mqtt.DOMAIN, + { + mqtt.DOMAIN: { light.DOMAIN: { - "platform": "mqtt", "schema": "template", "name": "test", "effect_list": ["rainbow", "colorloop"], @@ -264,10 +260,11 @@ async def test_state_brightness_color_effect_temp_change_via_topic( "blue_template": '{{ value.split(",")[3].' 'split("-")[2] }}', "effect_template": '{{ value.split(",")[4] }}', } - }, - ) - await hass.async_block_till_done() - await mqtt_mock_entry_with_yaml_config() + } + }, + ) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -340,13 +337,12 @@ async def test_sending_mqtt_commands_and_optimistic( ) mock_restore_cache(hass, (fake_state,)) - with assert_setup_component(1, light.DOMAIN): - assert await async_setup_component( - hass, - light.DOMAIN, - { + assert await async_setup_component( + hass, + mqtt.DOMAIN, + { + mqtt.DOMAIN: { light.DOMAIN: { - "platform": "mqtt", "schema": "template", "name": "test", "command_topic": "test_light_rgb/set", @@ -369,10 +365,11 @@ async def test_sending_mqtt_commands_and_optimistic( "effect_template": '{{ value.split(",")[4] }}', "qos": 2, } - }, - ) - await hass.async_block_till_done() - mqtt_mock = await mqtt_mock_entry_with_yaml_config() + } + }, + ) + await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_ON @@ -470,13 +467,12 @@ async def test_sending_mqtt_commands_non_optimistic_brightness_template( hass, mqtt_mock_entry_with_yaml_config ): """Test the sending of command in optimistic mode.""" - with assert_setup_component(1, light.DOMAIN): - assert await async_setup_component( - hass, - light.DOMAIN, - { + assert await async_setup_component( + hass, + mqtt.DOMAIN, + { + mqtt.DOMAIN: { light.DOMAIN: { - "platform": "mqtt", "schema": "template", "name": "test", "effect_list": ["rainbow", "colorloop"], @@ -499,10 +495,11 @@ async def test_sending_mqtt_commands_non_optimistic_brightness_template( "blue_template": '{{ value.split(",")[3].' 'split("-")[2] }}', "effect_template": '{{ value.split(",")[4] }}', } - }, - ) - await hass.async_block_till_done() - mqtt_mock = await mqtt_mock_entry_with_yaml_config() + } + }, + ) + await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -591,13 +588,12 @@ async def test_sending_mqtt_commands_non_optimistic_brightness_template( async def test_effect(hass, mqtt_mock_entry_with_yaml_config): """Test effect sent over MQTT in optimistic mode.""" - with assert_setup_component(1, light.DOMAIN): - assert await async_setup_component( - hass, - light.DOMAIN, - { + assert await async_setup_component( + hass, + mqtt.DOMAIN, + { + mqtt.DOMAIN: { light.DOMAIN: { - "platform": "mqtt", "schema": "template", "effect_list": ["rainbow", "colorloop"], "name": "test", @@ -606,10 +602,11 @@ async def test_effect(hass, mqtt_mock_entry_with_yaml_config): "command_off_template": "off", "qos": 0, } - }, - ) - await hass.async_block_till_done() - mqtt_mock = await mqtt_mock_entry_with_yaml_config() + } + }, + ) + await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -644,13 +641,12 @@ async def test_effect(hass, mqtt_mock_entry_with_yaml_config): async def test_flash(hass, mqtt_mock_entry_with_yaml_config): """Test flash sent over MQTT in optimistic mode.""" - with assert_setup_component(1, light.DOMAIN): - assert await async_setup_component( - hass, - light.DOMAIN, - { + assert await async_setup_component( + hass, + mqtt.DOMAIN, + { + mqtt.DOMAIN: { light.DOMAIN: { - "platform": "mqtt", "schema": "template", "name": "test", "command_topic": "test_light_rgb/set", @@ -658,10 +654,11 @@ async def test_flash(hass, mqtt_mock_entry_with_yaml_config): "command_off_template": "off", "qos": 0, } - }, - ) - await hass.async_block_till_done() - mqtt_mock = await mqtt_mock_entry_with_yaml_config() + } + }, + ) + await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -693,13 +690,12 @@ async def test_flash(hass, mqtt_mock_entry_with_yaml_config): async def test_transition(hass, mqtt_mock_entry_with_yaml_config): """Test for transition time being sent when included.""" - with assert_setup_component(1, light.DOMAIN): - assert await async_setup_component( - hass, - light.DOMAIN, - { + assert await async_setup_component( + hass, + mqtt.DOMAIN, + { + mqtt.DOMAIN: { light.DOMAIN: { - "platform": "mqtt", "schema": "template", "name": "test", "command_topic": "test_light_rgb/set", @@ -707,10 +703,11 @@ async def test_transition(hass, mqtt_mock_entry_with_yaml_config): "command_off_template": "off,{{ transition|int|d }}", "qos": 1, } - }, - ) - await hass.async_block_till_done() - mqtt_mock = await mqtt_mock_entry_with_yaml_config() + } + }, + ) + await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -735,13 +732,12 @@ async def test_transition(hass, mqtt_mock_entry_with_yaml_config): async def test_invalid_values(hass, mqtt_mock_entry_with_yaml_config): """Test that invalid values are ignored.""" - with assert_setup_component(1, light.DOMAIN): - assert await async_setup_component( - hass, - light.DOMAIN, - { + assert await async_setup_component( + hass, + mqtt.DOMAIN, + { + mqtt.DOMAIN: { light.DOMAIN: { - "platform": "mqtt", "schema": "template", "name": "test", "effect_list": ["rainbow", "colorloop"], @@ -763,10 +759,11 @@ async def test_invalid_values(hass, mqtt_mock_entry_with_yaml_config): "blue_template": '{{ value.split(",")[3].' 'split("-")[2] }}', "effect_template": '{{ value.split(",")[4] }}', } - }, - ) - await hass.async_block_till_done() - await mqtt_mock_entry_with_yaml_config() + } + }, + ) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -827,28 +824,28 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -857,7 +854,7 @@ async def test_setting_attribute_via_mqtt_json_message( ): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -869,7 +866,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, MQTT_LIGHT_ATTRIBUTES_BLOCKED, ) @@ -877,7 +874,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -886,7 +883,11 @@ async def test_update_with_json_attrs_not_dict( ): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock_entry_with_yaml_config, caplog, light.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + light.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) @@ -895,14 +896,22 @@ async def test_update_with_json_attrs_bad_JSON( ): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock_entry_with_yaml_config, caplog, light.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + light.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock_entry_no_yaml_config, caplog, light.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + light.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) @@ -1017,42 +1026,42 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT light device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT light device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -1082,7 +1091,6 @@ async def test_max_mireds(hass, mqtt_mock_entry_with_yaml_config): """Test setting min_mireds and max_mireds.""" config = { light.DOMAIN: { - "platform": "mqtt", "schema": "template", "name": "test", "command_topic": "test_max_mireds/set", @@ -1093,7 +1101,7 @@ async def test_max_mireds(hass, mqtt_mock_entry_with_yaml_config): } } - assert await async_setup_component(hass, light.DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -1139,7 +1147,7 @@ async def test_publishing_with_custom_encoding( ): """Test publishing MQTT payload with different encoding.""" domain = light.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG[domain]) + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) if topic == "effect_command_topic": config["effect_list"] = ["random", "color_loop"] @@ -1162,7 +1170,7 @@ async def test_publishing_with_custom_encoding( async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = light.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) @@ -1171,7 +1179,7 @@ async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_pa async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = light.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) @@ -1192,7 +1200,7 @@ async def test_encoding_subscribable_topics( init_payload, ): """Test handling of incoming encoded payload.""" - config = copy.deepcopy(DEFAULT_CONFIG[light.DOMAIN]) + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[light.DOMAIN]) config["state_template"] = "{{ value }}" await help_test_encoding_subscribable_topics( hass, @@ -1211,7 +1219,7 @@ async def test_encoding_subscribable_topics( async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = light.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG[platform]) + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) config["name"] = "test" del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) @@ -1221,7 +1229,20 @@ async def test_setup_manual_entity_from_yaml(hass): async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): """Test unloading the config entry.""" domain = light.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) + + +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +async def test_setup_with_legacy_schema(hass, mqtt_mock_entry_with_yaml_config): + """Test a setup with deprecated yaml platform schema.""" + domain = light.DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) + config["name"] = "test" + assert await async_setup_component(hass, domain, {domain: config}) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + assert hass.states.get(f"{domain}.test") is not None diff --git a/tests/components/mqtt/test_lock.py b/tests/components/mqtt/test_lock.py index f6dc4a0ed6d..de97de23a1e 100644 --- a/tests/components/mqtt/test_lock.py +++ b/tests/components/mqtt/test_lock.py @@ -4,14 +4,14 @@ from unittest.mock import patch import pytest +from homeassistant.components import lock, mqtt from homeassistant.components.lock import ( - DOMAIN as LOCK_DOMAIN, SERVICE_LOCK, SERVICE_OPEN, SERVICE_UNLOCK, STATE_LOCKED, STATE_UNLOCKED, - SUPPORT_OPEN, + LockEntityFeature, ) from homeassistant.components.mqtt.lock import MQTT_LOCK_ATTRIBUTES_BLOCKED from homeassistant.const import ( @@ -56,9 +56,14 @@ from .test_common import ( from tests.common import async_fire_mqtt_message DEFAULT_CONFIG = { - LOCK_DOMAIN: {"platform": "mqtt", "name": "test", "command_topic": "test-topic"} + mqtt.DOMAIN: {lock.DOMAIN: {"name": "test", "command_topic": "test-topic"}} } +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +DEFAULT_CONFIG_LEGACY = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) +DEFAULT_CONFIG_LEGACY[lock.DOMAIN]["platform"] = mqtt.DOMAIN + @pytest.fixture(autouse=True) def lock_platform_only(): @@ -71,17 +76,18 @@ async def test_controlling_state_via_topic(hass, mqtt_mock_entry_with_yaml_confi """Test the controlling state via topic.""" assert await async_setup_component( hass, - LOCK_DOMAIN, + mqtt.DOMAIN, { - LOCK_DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "payload_lock": "LOCK", - "payload_unlock": "UNLOCK", - "state_locked": "LOCKED", - "state_unlocked": "UNLOCKED", + mqtt.DOMAIN: { + lock.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "payload_lock": "LOCK", + "payload_unlock": "UNLOCK", + "state_locked": "LOCKED", + "state_unlocked": "UNLOCKED", + } } }, ) @@ -110,17 +116,18 @@ async def test_controlling_non_default_state_via_topic( """Test the controlling state via topic.""" assert await async_setup_component( hass, - LOCK_DOMAIN, + mqtt.DOMAIN, { - LOCK_DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "payload_lock": "LOCK", - "payload_unlock": "UNLOCK", - "state_locked": "closed", - "state_unlocked": "open", + mqtt.DOMAIN: { + lock.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "payload_lock": "LOCK", + "payload_unlock": "UNLOCK", + "state_locked": "closed", + "state_unlocked": "open", + } } }, ) @@ -148,18 +155,19 @@ async def test_controlling_state_via_topic_and_json_message( """Test the controlling state via topic and JSON message.""" assert await async_setup_component( hass, - LOCK_DOMAIN, + mqtt.DOMAIN, { - LOCK_DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "payload_lock": "LOCK", - "payload_unlock": "UNLOCK", - "state_locked": "LOCKED", - "state_unlocked": "UNLOCKED", - "value_template": "{{ value_json.val }}", + mqtt.DOMAIN: { + lock.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "payload_lock": "LOCK", + "payload_unlock": "UNLOCK", + "state_locked": "LOCKED", + "state_unlocked": "UNLOCKED", + "value_template": "{{ value_json.val }}", + } } }, ) @@ -186,18 +194,19 @@ async def test_controlling_non_default_state_via_topic_and_json_message( """Test the controlling state via topic and JSON message.""" assert await async_setup_component( hass, - LOCK_DOMAIN, + mqtt.DOMAIN, { - LOCK_DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "payload_lock": "LOCK", - "payload_unlock": "UNLOCK", - "state_locked": "closed", - "state_unlocked": "open", - "value_template": "{{ value_json.val }}", + mqtt.DOMAIN: { + lock.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "payload_lock": "LOCK", + "payload_unlock": "UNLOCK", + "state_locked": "closed", + "state_unlocked": "open", + "value_template": "{{ value_json.val }}", + } } }, ) @@ -224,16 +233,17 @@ async def test_sending_mqtt_commands_and_optimistic( """Test optimistic mode without state topic.""" assert await async_setup_component( hass, - LOCK_DOMAIN, + mqtt.DOMAIN, { - LOCK_DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "command-topic", - "payload_lock": "LOCK", - "payload_unlock": "UNLOCK", - "state_locked": "LOCKED", - "state_unlocked": "UNLOCKED", + mqtt.DOMAIN: { + lock.DOMAIN: { + "name": "test", + "command_topic": "command-topic", + "payload_lock": "LOCK", + "payload_unlock": "UNLOCK", + "state_locked": "LOCKED", + "state_unlocked": "UNLOCKED", + } } }, ) @@ -245,7 +255,7 @@ async def test_sending_mqtt_commands_and_optimistic( assert state.attributes.get(ATTR_ASSUMED_STATE) await hass.services.async_call( - LOCK_DOMAIN, SERVICE_LOCK, {ATTR_ENTITY_ID: "lock.test"}, blocking=True + lock.DOMAIN, SERVICE_LOCK, {ATTR_ENTITY_ID: "lock.test"}, blocking=True ) mqtt_mock.async_publish.assert_called_once_with("command-topic", "LOCK", 0, False) @@ -255,7 +265,7 @@ async def test_sending_mqtt_commands_and_optimistic( assert state.attributes.get(ATTR_ASSUMED_STATE) await hass.services.async_call( - LOCK_DOMAIN, SERVICE_UNLOCK, {ATTR_ENTITY_ID: "lock.test"}, blocking=True + lock.DOMAIN, SERVICE_UNLOCK, {ATTR_ENTITY_ID: "lock.test"}, blocking=True ) mqtt_mock.async_publish.assert_called_once_with("command-topic", "UNLOCK", 0, False) @@ -271,18 +281,19 @@ async def test_sending_mqtt_commands_and_explicit_optimistic( """Test optimistic mode without state topic.""" assert await async_setup_component( hass, - LOCK_DOMAIN, + mqtt.DOMAIN, { - LOCK_DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "payload_lock": "LOCK", - "payload_unlock": "UNLOCK", - "state_locked": "LOCKED", - "state_unlocked": "UNLOCKED", - "optimistic": True, + mqtt.DOMAIN: { + lock.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "payload_lock": "LOCK", + "payload_unlock": "UNLOCK", + "state_locked": "LOCKED", + "state_unlocked": "UNLOCKED", + "optimistic": True, + } } }, ) @@ -294,7 +305,7 @@ async def test_sending_mqtt_commands_and_explicit_optimistic( assert state.attributes.get(ATTR_ASSUMED_STATE) await hass.services.async_call( - LOCK_DOMAIN, SERVICE_LOCK, {ATTR_ENTITY_ID: "lock.test"}, blocking=True + lock.DOMAIN, SERVICE_LOCK, {ATTR_ENTITY_ID: "lock.test"}, blocking=True ) mqtt_mock.async_publish.assert_called_once_with("command-topic", "LOCK", 0, False) @@ -304,7 +315,7 @@ async def test_sending_mqtt_commands_and_explicit_optimistic( assert state.attributes.get(ATTR_ASSUMED_STATE) await hass.services.async_call( - LOCK_DOMAIN, SERVICE_UNLOCK, {ATTR_ENTITY_ID: "lock.test"}, blocking=True + lock.DOMAIN, SERVICE_UNLOCK, {ATTR_ENTITY_ID: "lock.test"}, blocking=True ) mqtt_mock.async_publish.assert_called_once_with("command-topic", "UNLOCK", 0, False) @@ -320,17 +331,18 @@ async def test_sending_mqtt_commands_support_open_and_optimistic( """Test open function of the lock without state topic.""" assert await async_setup_component( hass, - LOCK_DOMAIN, + mqtt.DOMAIN, { - LOCK_DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "command-topic", - "payload_lock": "LOCK", - "payload_unlock": "UNLOCK", - "payload_open": "OPEN", - "state_locked": "LOCKED", - "state_unlocked": "UNLOCKED", + mqtt.DOMAIN: { + lock.DOMAIN: { + "name": "test", + "command_topic": "command-topic", + "payload_lock": "LOCK", + "payload_unlock": "UNLOCK", + "payload_open": "OPEN", + "state_locked": "LOCKED", + "state_unlocked": "UNLOCKED", + } } }, ) @@ -340,10 +352,10 @@ async def test_sending_mqtt_commands_support_open_and_optimistic( state = hass.states.get("lock.test") assert state.state is STATE_UNLOCKED assert state.attributes.get(ATTR_ASSUMED_STATE) - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == SUPPORT_OPEN + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == LockEntityFeature.OPEN await hass.services.async_call( - LOCK_DOMAIN, SERVICE_LOCK, {ATTR_ENTITY_ID: "lock.test"}, blocking=True + lock.DOMAIN, SERVICE_LOCK, {ATTR_ENTITY_ID: "lock.test"}, blocking=True ) mqtt_mock.async_publish.assert_called_once_with("command-topic", "LOCK", 0, False) @@ -353,7 +365,7 @@ async def test_sending_mqtt_commands_support_open_and_optimistic( assert state.attributes.get(ATTR_ASSUMED_STATE) await hass.services.async_call( - LOCK_DOMAIN, SERVICE_UNLOCK, {ATTR_ENTITY_ID: "lock.test"}, blocking=True + lock.DOMAIN, SERVICE_UNLOCK, {ATTR_ENTITY_ID: "lock.test"}, blocking=True ) mqtt_mock.async_publish.assert_called_once_with("command-topic", "UNLOCK", 0, False) @@ -363,7 +375,7 @@ async def test_sending_mqtt_commands_support_open_and_optimistic( assert state.attributes.get(ATTR_ASSUMED_STATE) await hass.services.async_call( - LOCK_DOMAIN, SERVICE_OPEN, {ATTR_ENTITY_ID: "lock.test"}, blocking=True + lock.DOMAIN, SERVICE_OPEN, {ATTR_ENTITY_ID: "lock.test"}, blocking=True ) mqtt_mock.async_publish.assert_called_once_with("command-topic", "OPEN", 0, False) @@ -379,19 +391,20 @@ async def test_sending_mqtt_commands_support_open_and_explicit_optimistic( """Test open function of the lock without state topic.""" assert await async_setup_component( hass, - LOCK_DOMAIN, + mqtt.DOMAIN, { - LOCK_DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "payload_lock": "LOCK", - "payload_unlock": "UNLOCK", - "payload_open": "OPEN", - "state_locked": "LOCKED", - "state_unlocked": "UNLOCKED", - "optimistic": True, + mqtt.DOMAIN: { + lock.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "payload_lock": "LOCK", + "payload_unlock": "UNLOCK", + "payload_open": "OPEN", + "state_locked": "LOCKED", + "state_unlocked": "UNLOCKED", + "optimistic": True, + } } }, ) @@ -401,10 +414,10 @@ async def test_sending_mqtt_commands_support_open_and_explicit_optimistic( state = hass.states.get("lock.test") assert state.state is STATE_UNLOCKED assert state.attributes.get(ATTR_ASSUMED_STATE) - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == SUPPORT_OPEN + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == LockEntityFeature.OPEN await hass.services.async_call( - LOCK_DOMAIN, SERVICE_LOCK, {ATTR_ENTITY_ID: "lock.test"}, blocking=True + lock.DOMAIN, SERVICE_LOCK, {ATTR_ENTITY_ID: "lock.test"}, blocking=True ) mqtt_mock.async_publish.assert_called_once_with("command-topic", "LOCK", 0, False) @@ -414,7 +427,7 @@ async def test_sending_mqtt_commands_support_open_and_explicit_optimistic( assert state.attributes.get(ATTR_ASSUMED_STATE) await hass.services.async_call( - LOCK_DOMAIN, SERVICE_UNLOCK, {ATTR_ENTITY_ID: "lock.test"}, blocking=True + lock.DOMAIN, SERVICE_UNLOCK, {ATTR_ENTITY_ID: "lock.test"}, blocking=True ) mqtt_mock.async_publish.assert_called_once_with("command-topic", "UNLOCK", 0, False) @@ -424,7 +437,7 @@ async def test_sending_mqtt_commands_support_open_and_explicit_optimistic( assert state.attributes.get(ATTR_ASSUMED_STATE) await hass.services.async_call( - LOCK_DOMAIN, SERVICE_OPEN, {ATTR_ENTITY_ID: "lock.test"}, blocking=True + lock.DOMAIN, SERVICE_OPEN, {ATTR_ENTITY_ID: "lock.test"}, blocking=True ) mqtt_mock.async_publish.assert_called_once_with("command-topic", "OPEN", 0, False) @@ -439,28 +452,28 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, lock.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock_entry_with_yaml_config, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, lock.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock_entry_with_yaml_config, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, lock.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock_entry_with_yaml_config, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, lock.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -469,7 +482,7 @@ async def test_setting_attribute_via_mqtt_json_message( ): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock_entry_with_yaml_config, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, lock.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -480,8 +493,8 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( await help_test_setting_blocked_attribute_via_mqtt_json_message( hass, mqtt_mock_entry_no_yaml_config, - LOCK_DOMAIN, - DEFAULT_CONFIG, + lock.DOMAIN, + DEFAULT_CONFIG_LEGACY, MQTT_LOCK_ATTRIBUTES_BLOCKED, ) @@ -489,7 +502,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock_entry_with_yaml_config, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, lock.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -498,7 +511,11 @@ async def test_update_with_json_attrs_not_dict( ): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock_entry_with_yaml_config, caplog, LOCK_DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + lock.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) @@ -507,21 +524,25 @@ async def test_update_with_json_attrs_bad_json( ): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock_entry_with_yaml_config, caplog, LOCK_DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + lock.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock_entry_no_yaml_config, caplog, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, caplog, lock.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one lock per unique_id.""" config = { - LOCK_DOMAIN: [ + lock.DOMAIN: [ { "platform": "mqtt", "name": "Test 1", @@ -539,7 +560,7 @@ async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): ] } await help_test_unique_id( - hass, mqtt_mock_entry_with_yaml_config, LOCK_DOMAIN, config + hass, mqtt_mock_entry_with_yaml_config, lock.DOMAIN, config ) @@ -547,7 +568,7 @@ async def test_discovery_removal_lock(hass, mqtt_mock_entry_no_yaml_config, capl """Test removal of discovered lock.""" data = '{ "name": "test",' ' "command_topic": "test_topic" }' await help_test_discovery_removal( - hass, mqtt_mock_entry_no_yaml_config, caplog, LOCK_DOMAIN, data + hass, mqtt_mock_entry_no_yaml_config, caplog, lock.DOMAIN, data ) @@ -566,7 +587,7 @@ async def test_discovery_update_lock(hass, mqtt_mock_entry_no_yaml_config, caplo "availability_topic": "availability_topic2", } await help_test_discovery_update( - hass, mqtt_mock_entry_no_yaml_config, caplog, LOCK_DOMAIN, config1, config2 + hass, mqtt_mock_entry_no_yaml_config, caplog, lock.DOMAIN, config1, config2 ) @@ -586,7 +607,7 @@ async def test_discovery_update_unchanged_lock( hass, mqtt_mock_entry_no_yaml_config, caplog, - LOCK_DOMAIN, + lock.DOMAIN, data1, discovery_update, ) @@ -598,49 +619,49 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): data1 = '{ "name": "Beer" }' data2 = '{ "name": "Milk",' ' "command_topic": "test_topic" }' await help_test_discovery_broken( - hass, mqtt_mock_entry_no_yaml_config, caplog, LOCK_DOMAIN, data1, data2 + hass, mqtt_mock_entry_no_yaml_config, caplog, lock.DOMAIN, data1, data2 ) async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT lock device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock_entry_no_yaml_config, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, lock.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT lock device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock_entry_no_yaml_config, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, lock.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock_entry_no_yaml_config, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, lock.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock_entry_no_yaml_config, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, lock.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock_entry_with_yaml_config, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, lock.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock_entry_no_yaml_config, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, lock.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -649,8 +670,8 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): await help_test_entity_debug_info_message( hass, mqtt_mock_entry_no_yaml_config, - LOCK_DOMAIN, - DEFAULT_CONFIG, + lock.DOMAIN, + DEFAULT_CONFIG_LEGACY, SERVICE_LOCK, command_payload="LOCK", ) @@ -679,8 +700,8 @@ async def test_publishing_with_custom_encoding( template, ): """Test publishing MQTT payload with different encoding.""" - domain = LOCK_DOMAIN - config = DEFAULT_CONFIG[domain] + domain = lock.DOMAIN + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_publishing_with_custom_encoding( hass, @@ -698,8 +719,8 @@ async def test_publishing_with_custom_encoding( async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" - domain = LOCK_DOMAIN - config = DEFAULT_CONFIG[domain] + domain = lock.DOMAIN + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) @@ -707,8 +728,8 @@ async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_pa async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" - domain = LOCK_DOMAIN - config = DEFAULT_CONFIG[domain] + domain = lock.DOMAIN + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) @@ -732,8 +753,8 @@ async def test_encoding_subscribable_topics( hass, mqtt_mock_entry_with_yaml_config, caplog, - LOCK_DOMAIN, - DEFAULT_CONFIG[LOCK_DOMAIN], + lock.DOMAIN, + DEFAULT_CONFIG_LEGACY[lock.DOMAIN], topic, value, attribute, @@ -743,8 +764,8 @@ async def test_encoding_subscribable_topics( async def test_setup_manual_entity_from_yaml(hass, caplog, tmp_path): """Test setup manual configured MQTT entity.""" - platform = LOCK_DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG[platform]) + platform = lock.DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) config["name"] = "test" del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) @@ -753,8 +774,21 @@ async def test_setup_manual_entity_from_yaml(hass, caplog, tmp_path): async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): """Test unloading the config entry.""" - domain = LOCK_DOMAIN - config = DEFAULT_CONFIG[domain] + domain = lock.DOMAIN + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) + + +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +async def test_setup_with_legacy_schema(hass, mqtt_mock_entry_with_yaml_config): + """Test a setup with deprecated yaml platform schema.""" + domain = lock.DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) + config["name"] = "test" + assert await async_setup_component(hass, domain, {domain: config}) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + assert hass.states.get(f"{domain}.test") is not None diff --git a/tests/components/mqtt/test_number.py b/tests/components/mqtt/test_number.py index 603984cffad..69b0473fb9d 100644 --- a/tests/components/mqtt/test_number.py +++ b/tests/components/mqtt/test_number.py @@ -66,9 +66,14 @@ from .test_common import ( from tests.common import async_fire_mqtt_message, mock_restore_cache_with_extra_data DEFAULT_CONFIG = { - number.DOMAIN: {"platform": "mqtt", "name": "test", "command_topic": "test-topic"} + mqtt.DOMAIN: {number.DOMAIN: {"name": "test", "command_topic": "test-topic"}} } +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +DEFAULT_CONFIG_LEGACY = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) +DEFAULT_CONFIG_LEGACY[number.DOMAIN]["platform"] = mqtt.DOMAIN + @pytest.fixture(autouse=True) def number_platform_only(): @@ -82,16 +87,17 @@ async def test_run_number_setup(hass, mqtt_mock_entry_with_yaml_config): topic = "test/number" await async_setup_component( hass, - "number", + mqtt.DOMAIN, { - "number": { - "platform": "mqtt", - "state_topic": topic, - "command_topic": topic, - "name": "Test Number", - "device_class": "temperature", - "unit_of_measurement": TEMP_FAHRENHEIT, - "payload_reset": "reset!", + mqtt.DOMAIN: { + number.DOMAIN: { + "state_topic": topic, + "command_topic": topic, + "name": "Test Number", + "device_class": "temperature", + "unit_of_measurement": TEMP_FAHRENHEIT, + "payload_reset": "reset!", + } } }, ) @@ -131,14 +137,15 @@ async def test_value_template(hass, mqtt_mock_entry_with_yaml_config): topic = "test/number" await async_setup_component( hass, - "number", + mqtt.DOMAIN, { - "number": { - "platform": "mqtt", - "state_topic": topic, - "command_topic": topic, - "name": "Test Number", - "value_template": "{{ value_json.val }}", + mqtt.DOMAIN: { + number.DOMAIN: { + "state_topic": topic, + "command_topic": topic, + "name": "Test Number", + "value_template": "{{ value_json.val }}", + } } }, ) @@ -184,14 +191,15 @@ async def test_restore_native_value(hass, mqtt_mock_entry_with_yaml_config): ) assert await async_setup_component( hass, - number.DOMAIN, + mqtt.DOMAIN, { - "number": { - "platform": "mqtt", - "command_topic": topic, - "device_class": "temperature", - "unit_of_measurement": TEMP_FAHRENHEIT, - "name": "Test Number", + mqtt.DOMAIN: { + number.DOMAIN: { + "command_topic": topic, + "device_class": "temperature", + "unit_of_measurement": TEMP_FAHRENHEIT, + "name": "Test Number", + } } }, ) @@ -220,12 +228,13 @@ async def test_run_number_service_optimistic(hass, mqtt_mock_entry_with_yaml_con ) assert await async_setup_component( hass, - number.DOMAIN, + mqtt.DOMAIN, { - "number": { - "platform": "mqtt", - "command_topic": topic, - "name": "Test Number", + mqtt.DOMAIN: { + number.DOMAIN: { + "command_topic": topic, + "name": "Test Number", + } } }, ) @@ -295,13 +304,14 @@ async def test_run_number_service_optimistic_with_command_template( ) assert await async_setup_component( hass, - number.DOMAIN, + mqtt.DOMAIN, { - "number": { - "platform": "mqtt", - "command_topic": topic, - "name": "Test Number", - "command_template": '{"number": {{ value }} }', + mqtt.DOMAIN: { + number.DOMAIN: { + "command_topic": topic, + "name": "Test Number", + "command_template": '{"number": {{ value }} }', + } } }, ) @@ -361,13 +371,14 @@ async def test_run_number_service(hass, mqtt_mock_entry_with_yaml_config): assert await async_setup_component( hass, - number.DOMAIN, + mqtt.DOMAIN, { - "number": { - "platform": "mqtt", - "command_topic": cmd_topic, - "state_topic": state_topic, - "name": "Test Number", + mqtt.DOMAIN: { + number.DOMAIN: { + "command_topic": cmd_topic, + "state_topic": state_topic, + "name": "Test Number", + } } }, ) @@ -398,14 +409,15 @@ async def test_run_number_service_with_command_template( assert await async_setup_component( hass, - number.DOMAIN, + mqtt.DOMAIN, { - "number": { - "platform": "mqtt", - "command_topic": cmd_topic, - "state_topic": state_topic, - "name": "Test Number", - "command_template": '{"number": {{ value }} }', + mqtt.DOMAIN: { + number.DOMAIN: { + "command_topic": cmd_topic, + "state_topic": state_topic, + "name": "Test Number", + "command_template": '{"number": {{ value }} }', + } } }, ) @@ -434,28 +446,28 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, number.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock_entry_with_yaml_config, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, number.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock_entry_with_yaml_config, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, number.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock_entry_with_yaml_config, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, number.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -464,7 +476,7 @@ async def test_setting_attribute_via_mqtt_json_message( ): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock_entry_with_yaml_config, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, number.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -476,7 +488,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( hass, mqtt_mock_entry_no_yaml_config, number.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, MQTT_NUMBER_ATTRIBUTES_BLOCKED, ) @@ -484,7 +496,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock_entry_with_yaml_config, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, number.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -493,7 +505,11 @@ async def test_update_with_json_attrs_not_dict( ): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock_entry_with_yaml_config, caplog, number.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + number.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) @@ -502,14 +518,22 @@ async def test_update_with_json_attrs_bad_JSON( ): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock_entry_with_yaml_config, caplog, number.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + number.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock_entry_no_yaml_config, caplog, number.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + number.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) @@ -540,7 +564,7 @@ async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): async def test_discovery_removal_number(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered number.""" - data = json.dumps(DEFAULT_CONFIG[number.DOMAIN]) + data = json.dumps(DEFAULT_CONFIG_LEGACY[number.DOMAIN]) await help_test_discovery_removal( hass, mqtt_mock_entry_no_yaml_config, caplog, number.DOMAIN, data ) @@ -600,42 +624,42 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT number device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock_entry_no_yaml_config, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, number.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT number device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock_entry_no_yaml_config, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, number.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock_entry_no_yaml_config, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, number.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock_entry_no_yaml_config, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, number.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock_entry_with_yaml_config, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, number.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock_entry_no_yaml_config, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, number.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -645,7 +669,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): hass, mqtt_mock_entry_no_yaml_config, number.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, SERVICE_SET_VALUE, service_parameters={ATTR_VALUE: 45}, command_payload="45", @@ -658,16 +682,17 @@ async def test_min_max_step_attributes(hass, mqtt_mock_entry_with_yaml_config): topic = "test/number" await async_setup_component( hass, - "number", + mqtt.DOMAIN, { - "number": { - "platform": "mqtt", - "state_topic": topic, - "command_topic": topic, - "name": "Test Number", - "min": 5, - "max": 110, - "step": 20, + mqtt.DOMAIN: { + number.DOMAIN: { + "state_topic": topic, + "command_topic": topic, + "name": "Test Number", + "min": 5, + "max": 110, + "step": 20, + } } }, ) @@ -680,25 +705,24 @@ async def test_min_max_step_attributes(hass, mqtt_mock_entry_with_yaml_config): assert state.attributes.get(ATTR_STEP) == 20 -async def test_invalid_min_max_attributes(hass, caplog, mqtt_mock_entry_no_yaml_config): +async def test_invalid_min_max_attributes(hass, caplog): """Test invalid min/max attributes.""" topic = "test/number" - await async_setup_component( + assert not await async_setup_component( hass, - "number", + mqtt.DOMAIN, { - "number": { - "platform": "mqtt", - "state_topic": topic, - "command_topic": topic, - "name": "Test Number", - "min": 35, - "max": 10, + mqtt.DOMAIN: { + number.DOMAIN: { + "state_topic": topic, + "command_topic": topic, + "name": "Test Number", + "min": 35, + "max": 10, + } } }, ) - await hass.async_block_till_done() - await mqtt_mock_entry_no_yaml_config() assert f"'{CONF_MAX}' must be > '{CONF_MIN}'" in caplog.text @@ -779,15 +803,16 @@ async def test_mqtt_payload_not_a_number_warning( ): """Test warning for MQTT payload which is not a number.""" topic = "test/number" - await async_setup_component( + assert await async_setup_component( hass, - "number", + mqtt.DOMAIN, { - "number": { - "platform": "mqtt", - "state_topic": topic, - "command_topic": topic, - "name": "Test Number", + mqtt.DOMAIN: { + number.DOMAIN: { + "state_topic": topic, + "command_topic": topic, + "name": "Test Number", + } } }, ) @@ -808,15 +833,16 @@ async def test_mqtt_payload_out_of_range_error( topic = "test/number" await async_setup_component( hass, - "number", + mqtt.DOMAIN, { - "number": { - "platform": "mqtt", - "state_topic": topic, - "command_topic": topic, - "name": "Test Number", - "min": 5, - "max": 110, + mqtt.DOMAIN: { + number.DOMAIN: { + "state_topic": topic, + "command_topic": topic, + "name": "Test Number", + "min": 5, + "max": 110, + } } }, ) @@ -856,7 +882,7 @@ async def test_publishing_with_custom_encoding( ): """Test publishing MQTT payload with different encoding.""" domain = NUMBER_DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_publishing_with_custom_encoding( hass, @@ -875,7 +901,7 @@ async def test_publishing_with_custom_encoding( async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = number.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) @@ -884,7 +910,7 @@ async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_pa async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = number.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) @@ -910,7 +936,7 @@ async def test_encoding_subscribable_topics( mqtt_mock_entry_with_yaml_config, caplog, "number", - DEFAULT_CONFIG["number"], + DEFAULT_CONFIG_LEGACY["number"], topic, value, attribute, @@ -921,7 +947,7 @@ async def test_encoding_subscribable_topics( async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = number.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG[platform]) + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) config["name"] = "test" del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) @@ -931,7 +957,20 @@ async def test_setup_manual_entity_from_yaml(hass): async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): """Test unloading the config entry.""" domain = number.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) + + +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +async def test_setup_with_legacy_schema(hass, mqtt_mock_entry_with_yaml_config): + """Test a setup with deprecated yaml platform schema.""" + domain = number.DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) + config["name"] = "test" + assert await async_setup_component(hass, domain, {domain: config}) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + assert hass.states.get(f"{domain}.test") is not None diff --git a/tests/components/mqtt/test_scene.py b/tests/components/mqtt/test_scene.py index d676429bf3e..a1c1644cc37 100644 --- a/tests/components/mqtt/test_scene.py +++ b/tests/components/mqtt/test_scene.py @@ -4,7 +4,7 @@ from unittest.mock import patch import pytest -from homeassistant.components import scene +from homeassistant.components import mqtt, scene from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON, STATE_UNKNOWN, Platform import homeassistant.core as ha from homeassistant.setup import async_setup_component @@ -28,14 +28,20 @@ from .test_common import ( from tests.common import mock_restore_cache DEFAULT_CONFIG = { - scene.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "test-topic", - "payload_on": "test-payload-on", + mqtt.DOMAIN: { + scene.DOMAIN: { + "name": "test", + "command_topic": "test-topic", + "payload_on": "test-payload-on", + } } } +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +DEFAULT_CONFIG_LEGACY = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) +DEFAULT_CONFIG_LEGACY[scene.DOMAIN]["platform"] = mqtt.DOMAIN + @pytest.fixture(autouse=True) def scene_platform_only(): @@ -51,14 +57,15 @@ async def test_sending_mqtt_commands(hass, mqtt_mock_entry_with_yaml_config): assert await async_setup_component( hass, - scene.DOMAIN, + mqtt.DOMAIN, { - scene.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "command-topic", - "payload_on": "beer on", - }, + mqtt.DOMAIN: { + scene.DOMAIN: { + "name": "test", + "command_topic": "command-topic", + "payload_on": "beer on", + }, + } }, ) await hass.async_block_till_done() @@ -80,14 +87,14 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, scene.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, scene.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock_entry_with_yaml_config, scene.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, scene.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -168,8 +175,8 @@ async def test_discovery_removal_scene(hass, mqtt_mock_entry_no_yaml_config, cap async def test_discovery_update_payload(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered scene.""" - config1 = copy.deepcopy(DEFAULT_CONFIG[scene.DOMAIN]) - config2 = copy.deepcopy(DEFAULT_CONFIG[scene.DOMAIN]) + config1 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[scene.DOMAIN]) + config2 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[scene.DOMAIN]) config1["name"] = "Beer" config2["name"] = "Milk" config1["payload_on"] = "ON" @@ -216,7 +223,7 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = scene.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) @@ -225,14 +232,14 @@ async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_pa async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = scene.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = scene.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG[platform]) + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) config["name"] = "test" del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) @@ -242,7 +249,20 @@ async def test_setup_manual_entity_from_yaml(hass): async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): """Test unloading the config entry.""" domain = scene.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) + + +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +async def test_setup_with_legacy_schema(hass, mqtt_mock_entry_with_yaml_config): + """Test a setup with deprecated yaml platform schema.""" + domain = scene.DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) + config["name"] = "test" + assert await async_setup_component(hass, domain, {domain: config}) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + assert hass.states.get(f"{domain}.test") is not None diff --git a/tests/components/mqtt/test_select.py b/tests/components/mqtt/test_select.py index 085c4d5df00..c66638a18af 100644 --- a/tests/components/mqtt/test_select.py +++ b/tests/components/mqtt/test_select.py @@ -5,7 +5,7 @@ from unittest.mock import patch import pytest -from homeassistant.components import select +from homeassistant.components import mqtt, select from homeassistant.components.mqtt.select import MQTT_SELECT_ATTRIBUTES_BLOCKED from homeassistant.components.select import ( ATTR_OPTION, @@ -56,14 +56,20 @@ from .test_common import ( from tests.common import async_fire_mqtt_message, mock_restore_cache DEFAULT_CONFIG = { - select.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "test-topic", - "options": ["milk", "beer"], + mqtt.DOMAIN: { + select.DOMAIN: { + "name": "test", + "command_topic": "test-topic", + "options": ["milk", "beer"], + } } } +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +DEFAULT_CONFIG_LEGACY = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) +DEFAULT_CONFIG_LEGACY[select.DOMAIN]["platform"] = mqtt.DOMAIN + @pytest.fixture(autouse=True) def select_platform_only(): @@ -77,14 +83,15 @@ async def test_run_select_setup(hass, mqtt_mock_entry_with_yaml_config): topic = "test/select" await async_setup_component( hass, - "select", + mqtt.DOMAIN, { - "select": { - "platform": "mqtt", - "state_topic": topic, - "command_topic": topic, - "name": "Test Select", - "options": ["milk", "beer"], + mqtt.DOMAIN: { + select.DOMAIN: { + "state_topic": topic, + "command_topic": topic, + "name": "Test Select", + "options": ["milk", "beer"], + } } }, ) @@ -111,15 +118,16 @@ async def test_value_template(hass, mqtt_mock_entry_with_yaml_config): topic = "test/select" await async_setup_component( hass, - "select", + mqtt.DOMAIN, { - "select": { - "platform": "mqtt", - "state_topic": topic, - "command_topic": topic, - "name": "Test Select", - "options": ["milk", "beer"], - "value_template": "{{ value_json.val }}", + mqtt.DOMAIN: { + select.DOMAIN: { + "state_topic": topic, + "command_topic": topic, + "name": "Test Select", + "options": ["milk", "beer"], + "value_template": "{{ value_json.val }}", + } } }, ) @@ -157,13 +165,14 @@ async def test_run_select_service_optimistic(hass, mqtt_mock_entry_with_yaml_con assert await async_setup_component( hass, - select.DOMAIN, + mqtt.DOMAIN, { - "select": { - "platform": "mqtt", - "command_topic": topic, - "name": "Test Select", - "options": ["milk", "beer"], + mqtt.DOMAIN: { + select.DOMAIN: { + "command_topic": topic, + "name": "Test Select", + "options": ["milk", "beer"], + } } }, ) @@ -198,14 +207,15 @@ async def test_run_select_service_optimistic_with_command_template( assert await async_setup_component( hass, - select.DOMAIN, + mqtt.DOMAIN, { - "select": { - "platform": "mqtt", - "command_topic": topic, - "name": "Test Select", - "options": ["milk", "beer"], - "command_template": '{"option": "{{ value }}"}', + mqtt.DOMAIN: { + select.DOMAIN: { + "command_topic": topic, + "name": "Test Select", + "options": ["milk", "beer"], + "command_template": '{"option": "{{ value }}"}', + } } }, ) @@ -238,14 +248,15 @@ async def test_run_select_service(hass, mqtt_mock_entry_with_yaml_config): assert await async_setup_component( hass, - select.DOMAIN, + mqtt.DOMAIN, { - "select": { - "platform": "mqtt", - "command_topic": cmd_topic, - "state_topic": state_topic, - "name": "Test Select", - "options": ["milk", "beer"], + mqtt.DOMAIN: { + select.DOMAIN: { + "command_topic": cmd_topic, + "state_topic": state_topic, + "name": "Test Select", + "options": ["milk", "beer"], + } } }, ) @@ -276,15 +287,16 @@ async def test_run_select_service_with_command_template( assert await async_setup_component( hass, - select.DOMAIN, + mqtt.DOMAIN, { - "select": { - "platform": "mqtt", - "command_topic": cmd_topic, - "state_topic": state_topic, - "name": "Test Select", - "options": ["milk", "beer"], - "command_template": '{"option": "{{ value }}"}', + mqtt.DOMAIN: { + select.DOMAIN: { + "command_topic": cmd_topic, + "state_topic": state_topic, + "name": "Test Select", + "options": ["milk", "beer"], + "command_template": '{"option": "{{ value }}"}', + } } }, ) @@ -311,28 +323,28 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, select.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock_entry_with_yaml_config, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, select.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock_entry_with_yaml_config, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, select.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock_entry_with_yaml_config, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, select.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -341,7 +353,7 @@ async def test_setting_attribute_via_mqtt_json_message( ): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock_entry_with_yaml_config, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, select.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -353,7 +365,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( hass, mqtt_mock_entry_no_yaml_config, select.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, MQTT_SELECT_ATTRIBUTES_BLOCKED, ) @@ -361,7 +373,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock_entry_with_yaml_config, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, select.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -370,7 +382,11 @@ async def test_update_with_json_attrs_not_dict( ): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock_entry_with_yaml_config, caplog, select.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + select.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) @@ -379,14 +395,22 @@ async def test_update_with_json_attrs_bad_JSON( ): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock_entry_with_yaml_config, caplog, select.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + select.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock_entry_no_yaml_config, caplog, select.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + select.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) @@ -419,7 +443,7 @@ async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): async def test_discovery_removal_select(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered select.""" - data = json.dumps(DEFAULT_CONFIG[select.DOMAIN]) + data = json.dumps(DEFAULT_CONFIG_LEGACY[select.DOMAIN]) await help_test_discovery_removal( hass, mqtt_mock_entry_no_yaml_config, caplog, select.DOMAIN, data ) @@ -477,42 +501,42 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT select device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock_entry_no_yaml_config, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, select.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT select device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock_entry_no_yaml_config, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, select.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock_entry_no_yaml_config, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, select.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock_entry_no_yaml_config, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, select.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock_entry_with_yaml_config, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, select.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock_entry_no_yaml_config, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, select.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -522,7 +546,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): hass, mqtt_mock_entry_no_yaml_config, select.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, select.SERVICE_SELECT_OPTION, service_parameters={ATTR_OPTION: "beer"}, command_payload="beer", @@ -536,14 +560,15 @@ async def test_options_attributes(hass, mqtt_mock_entry_with_yaml_config, option topic = "test/select" await async_setup_component( hass, - "select", + mqtt.DOMAIN, { - "select": { - "platform": "mqtt", - "state_topic": topic, - "command_topic": topic, - "name": "Test select", - "options": options, + mqtt.DOMAIN: { + select.DOMAIN: { + "state_topic": topic, + "command_topic": topic, + "name": "Test select", + "options": options, + } } }, ) @@ -561,14 +586,15 @@ async def test_mqtt_payload_not_an_option_warning( topic = "test/select" await async_setup_component( hass, - "select", + mqtt.DOMAIN, { - "select": { - "platform": "mqtt", - "state_topic": topic, - "command_topic": topic, - "name": "Test Select", - "options": ["milk", "beer"], + mqtt.DOMAIN: { + select.DOMAIN: { + "state_topic": topic, + "command_topic": topic, + "name": "Test Select", + "options": ["milk", "beer"], + } } }, ) @@ -609,7 +635,7 @@ async def test_publishing_with_custom_encoding( ): """Test publishing MQTT payload with different encoding.""" domain = select.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] config["options"] = ["milk", "beer"] await help_test_publishing_with_custom_encoding( @@ -629,7 +655,7 @@ async def test_publishing_with_custom_encoding( async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = select.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) @@ -638,7 +664,7 @@ async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_pa async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = select.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) @@ -659,7 +685,7 @@ async def test_encoding_subscribable_topics( attribute_value, ): """Test handling of incoming encoded payload.""" - config = copy.deepcopy(DEFAULT_CONFIG["select"]) + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY["select"]) config["options"] = ["milk", "beer"] await help_test_encoding_subscribable_topics( hass, @@ -677,7 +703,7 @@ async def test_encoding_subscribable_topics( async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = select.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG[platform]) + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) config["name"] = "test" del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) @@ -687,7 +713,20 @@ async def test_setup_manual_entity_from_yaml(hass): async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): """Test unloading the config entry.""" domain = select.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) + + +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +async def test_setup_with_legacy_schema(hass, mqtt_mock_entry_with_yaml_config): + """Test a setup with deprecated yaml platform schema.""" + domain = select.DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) + config["name"] = "test" + assert await async_setup_component(hass, domain, {domain: config}) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + assert hass.states.get(f"{domain}.test") is not None diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index b446b1a8b76..c5cf377cf1a 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -6,8 +6,8 @@ from unittest.mock import MagicMock, patch import pytest +from homeassistant.components import mqtt, sensor from homeassistant.components.mqtt.sensor import MQTT_SENSOR_ATTRIBUTES_BLOCKED -import homeassistant.components.sensor as sensor from homeassistant.const import ( EVENT_STATE_CHANGED, STATE_UNAVAILABLE, @@ -64,16 +64,20 @@ from .test_common import ( ) from tests.common import ( - assert_setup_component, async_fire_mqtt_message, async_fire_time_changed, mock_restore_cache_with_extra_data, ) DEFAULT_CONFIG = { - sensor.DOMAIN: {"platform": "mqtt", "name": "test", "state_topic": "test-topic"} + mqtt.DOMAIN: {sensor.DOMAIN: {"name": "test", "state_topic": "test-topic"}} } +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +DEFAULT_CONFIG_LEGACY = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) +DEFAULT_CONFIG_LEGACY[sensor.DOMAIN]["platform"] = mqtt.DOMAIN + @pytest.fixture(autouse=True) def sensor_platform_only(): @@ -88,13 +92,14 @@ async def test_setting_sensor_value_via_mqtt_message( """Test the setting of the value via MQTT.""" assert await async_setup_component( hass, - sensor.DOMAIN, + mqtt.DOMAIN, { - sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "test-topic", - "unit_of_measurement": "fav unit", + mqtt.DOMAIN: { + sensor.DOMAIN: { + "name": "test", + "state_topic": "test-topic", + "unit_of_measurement": "fav unit", + } } }, ) @@ -146,13 +151,14 @@ async def test_setting_sensor_native_value_handling_via_mqtt_message( """Test the setting of the value via MQTT.""" assert await async_setup_component( hass, - sensor.DOMAIN, + mqtt.DOMAIN, { - sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "test-topic", - "device_class": device_class, + mqtt.DOMAIN: { + sensor.DOMAIN: { + "name": "test", + "state_topic": "test-topic", + "device_class": device_class, + } } }, ) @@ -173,15 +179,16 @@ async def test_setting_sensor_value_expires_availability_topic( """Test the expiration of the value.""" assert await async_setup_component( hass, - sensor.DOMAIN, + mqtt.DOMAIN, { - sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "test-topic", - "expire_after": 4, - "force_update": True, - "availability_topic": "availability-topic", + mqtt.DOMAIN: { + sensor.DOMAIN: { + "name": "test", + "state_topic": "test-topic", + "expire_after": 4, + "force_update": True, + "availability_topic": "availability-topic", + } } }, ) @@ -206,15 +213,16 @@ async def test_setting_sensor_value_expires( """Test the expiration of the value.""" assert await async_setup_component( hass, - sensor.DOMAIN, + mqtt.DOMAIN, { - sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "test-topic", - "unit_of_measurement": "fav unit", - "expire_after": "4", - "force_update": True, + mqtt.DOMAIN: { + sensor.DOMAIN: { + "name": "test", + "state_topic": "test-topic", + "unit_of_measurement": "fav unit", + "expire_after": "4", + "force_update": True, + } } }, ) @@ -285,14 +293,15 @@ async def test_setting_sensor_value_via_mqtt_json_message( """Test the setting of the value via MQTT with JSON payload.""" assert await async_setup_component( hass, - sensor.DOMAIN, + mqtt.DOMAIN, { - sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "test-topic", - "unit_of_measurement": "fav unit", - "value_template": "{{ value_json.val }}", + mqtt.DOMAIN: { + sensor.DOMAIN: { + "name": "test", + "state_topic": "test-topic", + "unit_of_measurement": "fav unit", + "value_template": "{{ value_json.val }}", + } } }, ) @@ -311,14 +320,15 @@ async def test_setting_sensor_value_via_mqtt_json_message_and_default_current_st """Test the setting of the value via MQTT with fall back to current state.""" assert await async_setup_component( hass, - sensor.DOMAIN, + mqtt.DOMAIN, { - sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "test-topic", - "unit_of_measurement": "fav unit", - "value_template": "{{ value_json.val | is_defined }}-{{ value_json.par }}", + mqtt.DOMAIN: { + sensor.DOMAIN: { + "name": "test", + "state_topic": "test-topic", + "unit_of_measurement": "fav unit", + "value_template": "{{ value_json.val | is_defined }}-{{ value_json.par }}", + } } }, ) @@ -344,15 +354,16 @@ async def test_setting_sensor_last_reset_via_mqtt_message( """Test the setting of the last_reset property via MQTT.""" assert await async_setup_component( hass, - sensor.DOMAIN, + mqtt.DOMAIN, { - sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_class": "total", - "state_topic": "test-topic", - "unit_of_measurement": "fav unit", - "last_reset_topic": "last-reset-topic", + mqtt.DOMAIN: { + sensor.DOMAIN: { + "name": "test", + "state_class": "total", + "state_topic": "test-topic", + "unit_of_measurement": "fav unit", + "last_reset_topic": "last-reset-topic", + } } }, ) @@ -376,15 +387,16 @@ async def test_setting_sensor_bad_last_reset_via_mqtt_message( """Test the setting of the last_reset property via MQTT.""" assert await async_setup_component( hass, - sensor.DOMAIN, + mqtt.DOMAIN, { - sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_class": "total", - "state_topic": "test-topic", - "unit_of_measurement": "fav unit", - "last_reset_topic": "last-reset-topic", + mqtt.DOMAIN: { + sensor.DOMAIN: { + "name": "test", + "state_class": "total", + "state_topic": "test-topic", + "unit_of_measurement": "fav unit", + "last_reset_topic": "last-reset-topic", + } } }, ) @@ -403,15 +415,16 @@ async def test_setting_sensor_empty_last_reset_via_mqtt_message( """Test the setting of the last_reset property via MQTT.""" assert await async_setup_component( hass, - sensor.DOMAIN, + mqtt.DOMAIN, { - sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_class": "total", - "state_topic": "test-topic", - "unit_of_measurement": "fav unit", - "last_reset_topic": "last-reset-topic", + mqtt.DOMAIN: { + sensor.DOMAIN: { + "name": "test", + "state_class": "total", + "state_topic": "test-topic", + "unit_of_measurement": "fav unit", + "last_reset_topic": "last-reset-topic", + } } }, ) @@ -430,16 +443,17 @@ async def test_setting_sensor_last_reset_via_mqtt_json_message( """Test the setting of the value via MQTT with JSON payload.""" assert await async_setup_component( hass, - sensor.DOMAIN, + mqtt.DOMAIN, { - sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_class": "total", - "state_topic": "test-topic", - "unit_of_measurement": "fav unit", - "last_reset_topic": "last-reset-topic", - "last_reset_value_template": "{{ value_json.last_reset }}", + mqtt.DOMAIN: { + sensor.DOMAIN: { + "name": "test", + "state_class": "total", + "state_topic": "test-topic", + "unit_of_measurement": "fav unit", + "last_reset_topic": "last-reset-topic", + "last_reset_value_template": "{{ value_json.last_reset }}", + } } }, ) @@ -460,19 +474,20 @@ async def test_setting_sensor_last_reset_via_mqtt_json_message_2( """Test the setting of the value via MQTT with JSON payload.""" assert await async_setup_component( hass, - sensor.DOMAIN, + mqtt.DOMAIN, { - sensor.DOMAIN: { - **{ - "platform": "mqtt", - "name": "test", - "state_class": "total", - "state_topic": "test-topic", - "unit_of_measurement": "kWh", - "value_template": "{{ value_json.value | float / 60000 }}", - "last_reset_value_template": "{{ utcnow().fromtimestamp(value_json.time / 1000, tz=utcnow().tzinfo) }}", - }, - **extra, + mqtt.DOMAIN: { + sensor.DOMAIN: { + **{ + "name": "test", + "state_class": "total", + "state_topic": "test-topic", + "unit_of_measurement": "kWh", + "value_template": "{{ value_json.value | float / 60000 }}", + "last_reset_value_template": "{{ utcnow().fromtimestamp(value_json.time / 1000, tz=utcnow().tzinfo) }}", + }, + **extra, + } } }, ) @@ -498,13 +513,14 @@ async def test_force_update_disabled(hass, mqtt_mock_entry_with_yaml_config): """Test force update option.""" assert await async_setup_component( hass, - sensor.DOMAIN, + mqtt.DOMAIN, { - sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "test-topic", - "unit_of_measurement": "fav unit", + mqtt.DOMAIN: { + sensor.DOMAIN: { + "name": "test", + "state_topic": "test-topic", + "unit_of_measurement": "fav unit", + } } }, ) @@ -532,14 +548,15 @@ async def test_force_update_enabled(hass, mqtt_mock_entry_with_yaml_config): """Test force update option.""" assert await async_setup_component( hass, - sensor.DOMAIN, + mqtt.DOMAIN, { - sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "test-topic", - "unit_of_measurement": "fav unit", - "force_update": True, + mqtt.DOMAIN: { + sensor.DOMAIN: { + "name": "test", + "state_topic": "test-topic", + "unit_of_measurement": "fav unit", + "force_update": True, + } } }, ) @@ -568,21 +585,21 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -591,7 +608,7 @@ async def test_default_availability_list_payload( ): """Test availability by default payload with defined topic.""" await help_test_default_availability_list_payload( - hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -600,7 +617,7 @@ async def test_default_availability_list_payload_all( ): """Test availability by default payload with defined topic.""" await help_test_default_availability_list_payload_all( - hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -609,7 +626,7 @@ async def test_default_availability_list_payload_any( ): """Test availability by default payload with defined topic.""" await help_test_default_availability_list_payload_any( - hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -618,21 +635,25 @@ async def test_default_availability_list_single( ): """Test availability list and availability_topic are mutually exclusive.""" await help_test_default_availability_list_single( - hass, mqtt_mock_entry_no_yaml_config, caplog, sensor.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + sensor.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_discovery_update_availability(hass, mqtt_mock_entry_no_yaml_config): """Test availability discovery update.""" await help_test_discovery_update_availability( - hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -642,11 +663,12 @@ async def test_invalid_device_class(hass, mqtt_mock_entry_no_yaml_config): hass, sensor.DOMAIN, { - sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "test-topic", - "device_class": "foobarnotreal", + mqtt.DOMAIN: { + sensor.DOMAIN: { + "name": "test", + "state_topic": "test-topic", + "device_class": "foobarnotreal", + } } }, ) @@ -661,17 +683,18 @@ async def test_valid_device_class(hass, mqtt_mock_entry_with_yaml_config): """Test device_class option with valid values.""" assert await async_setup_component( hass, - "sensor", + mqtt.DOMAIN, { - "sensor": [ - { - "platform": "mqtt", - "name": "Test 1", - "state_topic": "test-topic", - "device_class": "temperature", - }, - {"platform": "mqtt", "name": "Test 2", "state_topic": "test-topic"}, - ] + mqtt.DOMAIN: { + sensor.DOMAIN: [ + { + "name": "Test 1", + "state_topic": "test-topic", + "device_class": "temperature", + }, + {"name": "Test 2", "state_topic": "test-topic"}, + ] + } }, ) await hass.async_block_till_done() @@ -689,11 +712,12 @@ async def test_invalid_state_class(hass, mqtt_mock_entry_no_yaml_config): hass, sensor.DOMAIN, { - sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "test-topic", - "state_class": "foobarnotreal", + mqtt.DOMAIN: { + sensor.DOMAIN: { + "name": "test", + "state_topic": "test-topic", + "state_class": "foobarnotreal", + } } }, ) @@ -708,17 +732,18 @@ async def test_valid_state_class(hass, mqtt_mock_entry_with_yaml_config): """Test state_class option with valid values.""" assert await async_setup_component( hass, - "sensor", + mqtt.DOMAIN, { - "sensor": [ - { - "platform": "mqtt", - "name": "Test 1", - "state_topic": "test-topic", - "state_class": "measurement", - }, - {"platform": "mqtt", "name": "Test 2", "state_topic": "test-topic"}, - ] + mqtt.DOMAIN: { + sensor.DOMAIN: [ + { + "name": "Test 1", + "state_topic": "test-topic", + "state_class": "measurement", + }, + {"name": "Test 2", "state_topic": "test-topic"}, + ] + } }, ) await hass.async_block_till_done() @@ -735,7 +760,7 @@ async def test_setting_attribute_via_mqtt_json_message( ): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -747,7 +772,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, MQTT_SENSOR_ATTRIBUTES_BLOCKED, ) @@ -755,7 +780,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -764,7 +789,11 @@ async def test_update_with_json_attrs_not_dict( ): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock_entry_with_yaml_config, caplog, sensor.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + sensor.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) @@ -773,14 +802,22 @@ async def test_update_with_json_attrs_bad_JSON( ): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock_entry_with_yaml_config, caplog, sensor.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + sensor.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock_entry_no_yaml_config, caplog, sensor.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + sensor.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) @@ -914,42 +951,42 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT sensor device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT sensor device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -967,7 +1004,6 @@ async def test_entity_device_info_with_hub(hass, mqtt_mock_entry_no_yaml_config) data = json.dumps( { - "platform": "mqtt", "name": "Test 1", "state_topic": "test-topic", "device": {"identifiers": ["helloworld"], "via_device": "hub-id"}, @@ -985,42 +1021,42 @@ async def test_entity_device_info_with_hub(hass, mqtt_mock_entry_no_yaml_config) async def test_entity_debug_info(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT sensor debug info.""" await help_test_entity_debug_info( - hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_debug_info_max_messages(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT sensor debug info.""" await help_test_entity_debug_info_max_messages( - hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" await help_test_entity_debug_info_message( - hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG, None + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY, None ) async def test_entity_debug_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT sensor debug info.""" await help_test_entity_debug_info_remove( - hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_debug_info_update_entity_id(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT sensor debug info.""" await help_test_entity_debug_info_update_entity_id( - hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_disabled_by_default(hass, mqtt_mock_entry_no_yaml_config): """Test entity disabled by default.""" await help_test_entity_disabled_by_default( - hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -1028,7 +1064,7 @@ async def test_entity_disabled_by_default(hass, mqtt_mock_entry_no_yaml_config): async def test_entity_category(hass, mqtt_mock_entry_no_yaml_config): """Test entity category.""" await help_test_entity_category( - hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -1036,19 +1072,20 @@ async def test_value_template_with_entity_id(hass, mqtt_mock_entry_with_yaml_con """Test the access to attributes in value_template via the entity_id.""" assert await async_setup_component( hass, - sensor.DOMAIN, + mqtt.DOMAIN, { - sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "test-topic", - "unit_of_measurement": "fav unit", - "value_template": '\ + mqtt.DOMAIN: { + sensor.DOMAIN: { + "name": "test", + "state_topic": "test-topic", + "unit_of_measurement": "fav unit", + "value_template": '\ {% if state_attr(entity_id, "friendly_name") == "test" %} \ {{ value | int + 1 }} \ {% else %} \ {{ value }} \ {% endif %}', + } } }, ) @@ -1064,7 +1101,7 @@ async def test_value_template_with_entity_id(hass, mqtt_mock_entry_with_yaml_con async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = sensor.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) @@ -1073,7 +1110,7 @@ async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_pa async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = sensor.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) @@ -1082,14 +1119,14 @@ async def test_cleanup_triggers_and_restoring_state( ): """Test cleanup old triggers at reloading and restoring the state.""" domain = sensor.DOMAIN - config1 = copy.deepcopy(DEFAULT_CONFIG[domain]) + config1 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][domain]) config1["name"] = "test1" config1["expire_after"] = 30 config1["state_topic"] = "test-topic1" config1["device_class"] = "temperature" config1["unit_of_measurement"] = TEMP_FAHRENHEIT - config2 = copy.deepcopy(DEFAULT_CONFIG[domain]) + config2 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][domain]) config2["name"] = "test2" config2["expire_after"] = 5 config2["state_topic"] = "test-topic2" @@ -1100,8 +1137,8 @@ async def test_cleanup_triggers_and_restoring_state( assert await async_setup_component( hass, - domain, - {domain: [config1, config2]}, + mqtt.DOMAIN, + {mqtt.DOMAIN: {domain: [config1, config2]}}, ) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -1116,7 +1153,7 @@ async def test_cleanup_triggers_and_restoring_state( freezer.move_to("2022-02-02 12:01:10+01:00") await help_test_reload_with_config( - hass, caplog, tmp_path, {domain: [config1, config2]} + hass, caplog, tmp_path, {mqtt.DOMAIN: {domain: [config1, config2]}} ) await hass.async_block_till_done() @@ -1150,7 +1187,7 @@ async def test_skip_restoring_state_with_over_due_expire_trigger( freezer.move_to("2022-02-02 12:02:00+01:00") domain = sensor.DOMAIN - config3 = copy.deepcopy(DEFAULT_CONFIG[domain]) + config3 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][domain]) config3["name"] = "test3" config3["expire_after"] = 10 config3["state_topic"] = "test-topic3" @@ -1163,10 +1200,11 @@ async def test_skip_restoring_state_with_over_due_expire_trigger( fake_extra_data = MagicMock() mock_restore_cache_with_extra_data(hass, ((fake_state, fake_extra_data),)) - with assert_setup_component(1, domain): - assert await async_setup_component(hass, domain, {domain: config3}) - await hass.async_block_till_done() - await mqtt_mock_entry_with_yaml_config() + assert await async_setup_component( + hass, mqtt.DOMAIN, {mqtt.DOMAIN: {domain: config3}} + ) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() assert "Skip state recovery after reload for sensor.test3" in caplog.text @@ -1192,7 +1230,7 @@ async def test_encoding_subscribable_topics( mqtt_mock_entry_with_yaml_config, caplog, sensor.DOMAIN, - DEFAULT_CONFIG[sensor.DOMAIN], + DEFAULT_CONFIG_LEGACY[sensor.DOMAIN], topic, value, attribute, @@ -1204,7 +1242,7 @@ async def test_encoding_subscribable_topics( async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = sensor.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG[platform]) + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) config["name"] = "test" del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) @@ -1214,7 +1252,20 @@ async def test_setup_manual_entity_from_yaml(hass): async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): """Test unloading the config entry.""" domain = sensor.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) + + +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +async def test_setup_with_legacy_schema(hass, mqtt_mock_entry_with_yaml_config): + """Test a setup with deprecated yaml platform schema.""" + domain = sensor.DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) + config["name"] = "test" + assert await async_setup_component(hass, domain, {domain: config}) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + assert hass.states.get(f"{domain}.test") is not None diff --git a/tests/components/mqtt/test_siren.py b/tests/components/mqtt/test_siren.py index 13648f1c486..fd91847f767 100644 --- a/tests/components/mqtt/test_siren.py +++ b/tests/components/mqtt/test_siren.py @@ -4,7 +4,7 @@ from unittest.mock import patch import pytest -from homeassistant.components import siren +from homeassistant.components import mqtt, siren from homeassistant.components.siren.const import ATTR_VOLUME_LEVEL from homeassistant.const import ( ATTR_ASSUMED_STATE, @@ -53,9 +53,14 @@ from .test_common import ( from tests.common import async_fire_mqtt_message DEFAULT_CONFIG = { - siren.DOMAIN: {"platform": "mqtt", "name": "test", "command_topic": "test-topic"} + mqtt.DOMAIN: {siren.DOMAIN: {"name": "test", "command_topic": "test-topic"}} } +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +DEFAULT_CONFIG_LEGACY = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) +DEFAULT_CONFIG_LEGACY[siren.DOMAIN]["platform"] = mqtt.DOMAIN + @pytest.fixture(autouse=True) def siren_platform_only(): @@ -83,15 +88,16 @@ async def test_controlling_state_via_topic(hass, mqtt_mock_entry_with_yaml_confi """Test the controlling state via topic.""" assert await async_setup_component( hass, - siren.DOMAIN, + mqtt.DOMAIN, { - siren.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "payload_on": 1, - "payload_off": 0, + mqtt.DOMAIN: { + siren.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "payload_on": 1, + "payload_off": 0, + } } }, ) @@ -119,15 +125,16 @@ async def test_sending_mqtt_commands_and_optimistic( """Test the sending MQTT commands in optimistic mode.""" assert await async_setup_component( hass, - siren.DOMAIN, + mqtt.DOMAIN, { - siren.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "command-topic", - "payload_on": "beer on", - "payload_off": "beer off", - "qos": "2", + mqtt.DOMAIN: { + siren.DOMAIN: { + "name": "test", + "command_topic": "command-topic", + "payload_on": "beer on", + "payload_off": "beer off", + "qos": "2", + } } }, ) @@ -162,16 +169,17 @@ async def test_controlling_state_via_topic_and_json_message( """Test the controlling state via topic and JSON message.""" assert await async_setup_component( hass, - siren.DOMAIN, + mqtt.DOMAIN, { - siren.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "payload_on": "beer on", - "payload_off": "beer off", - "state_value_template": "{{ value_json.val }}", + mqtt.DOMAIN: { + siren.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "payload_on": "beer on", + "payload_off": "beer off", + "state_value_template": "{{ value_json.val }}", + } } }, ) @@ -202,16 +210,17 @@ async def test_controlling_state_and_attributes_with_json_message_without_templa """Test the controlling state via topic and JSON message without a value template.""" assert await async_setup_component( hass, - siren.DOMAIN, + mqtt.DOMAIN, { - siren.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "payload_on": "beer on", - "payload_off": "beer off", - "available_tones": ["ping", "siren", "bell"], + mqtt.DOMAIN: { + siren.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "payload_on": "beer on", + "payload_off": "beer off", + "available_tones": ["ping", "siren", "bell"], + } } }, ) @@ -284,7 +293,6 @@ async def test_filtering_not_supported_attributes_optimistic( ): """Test setting attributes with support flags optimistic.""" config = { - "platform": "mqtt", "command_topic": "command-topic", "available_tones": ["ping", "siren", "bell"], } @@ -300,8 +308,8 @@ async def test_filtering_not_supported_attributes_optimistic( assert await async_setup_component( hass, - siren.DOMAIN, - {siren.DOMAIN: [config1, config2, config3]}, + mqtt.DOMAIN, + {mqtt.DOMAIN: {siren.DOMAIN: [config1, config2, config3]}}, ) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -370,7 +378,6 @@ async def test_filtering_not_supported_attributes_via_state( ): """Test setting attributes with support flags via state.""" config = { - "platform": "mqtt", "command_topic": "command-topic", "available_tones": ["ping", "siren", "bell"], } @@ -389,8 +396,8 @@ async def test_filtering_not_supported_attributes_via_state( assert await async_setup_component( hass, - siren.DOMAIN, - {siren.DOMAIN: [config1, config2, config3]}, + mqtt.DOMAIN, + {mqtt.DOMAIN: {siren.DOMAIN: [config1, config2, config3]}}, ) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -450,14 +457,14 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, siren.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, siren.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock_entry_with_yaml_config, siren.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, siren.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -513,17 +520,18 @@ async def test_custom_state_payload(hass, mqtt_mock_entry_with_yaml_config): """Test the state payload.""" assert await async_setup_component( hass, - siren.DOMAIN, + mqtt.DOMAIN, { - siren.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "payload_on": 1, - "payload_off": 0, - "state_on": "HIGH", - "state_off": "LOW", + mqtt.DOMAIN: { + siren.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "payload_on": 1, + "payload_off": 0, + "state_on": "HIGH", + "state_off": "LOW", + } } }, ) @@ -550,7 +558,7 @@ async def test_setting_attribute_via_mqtt_json_message( ): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock_entry_with_yaml_config, siren.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, siren.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -559,14 +567,14 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( ): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, mqtt_mock_entry_no_yaml_config, siren.DOMAIN, DEFAULT_CONFIG, {} + hass, mqtt_mock_entry_no_yaml_config, siren.DOMAIN, DEFAULT_CONFIG_LEGACY, {} ) async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock_entry_with_yaml_config, siren.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, siren.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -575,7 +583,11 @@ async def test_update_with_json_attrs_not_dict( ): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock_entry_with_yaml_config, caplog, siren.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + siren.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) @@ -584,14 +596,22 @@ async def test_update_with_json_attrs_bad_JSON( ): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock_entry_with_yaml_config, caplog, siren.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + siren.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock_entry_no_yaml_config, caplog, siren.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + siren.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) @@ -636,8 +656,8 @@ async def test_discovery_update_siren_topic_template( hass, mqtt_mock_entry_no_yaml_config, caplog ): """Test update of discovered siren.""" - config1 = copy.deepcopy(DEFAULT_CONFIG[siren.DOMAIN]) - config2 = copy.deepcopy(DEFAULT_CONFIG[siren.DOMAIN]) + config1 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[siren.DOMAIN]) + config2 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[siren.DOMAIN]) config1["name"] = "Beer" config2["name"] = "Milk" config1["state_topic"] = "siren/state1" @@ -673,8 +693,8 @@ async def test_discovery_update_siren_template( hass, mqtt_mock_entry_no_yaml_config, caplog ): """Test update of discovered siren.""" - config1 = copy.deepcopy(DEFAULT_CONFIG[siren.DOMAIN]) - config2 = copy.deepcopy(DEFAULT_CONFIG[siren.DOMAIN]) + config1 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[siren.DOMAIN]) + config2 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[siren.DOMAIN]) config1["name"] = "Beer" config2["name"] = "Milk" config1["state_topic"] = "siren/state1" @@ -706,7 +726,7 @@ async def test_discovery_update_siren_template( async def test_command_templates(hass, mqtt_mock_entry_with_yaml_config, caplog): """Test siren with command templates optimistic.""" - config1 = copy.deepcopy(DEFAULT_CONFIG[siren.DOMAIN]) + config1 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][siren.DOMAIN]) config1["name"] = "Beer" config1["available_tones"] = ["ping", "chimes"] config1[ @@ -719,8 +739,8 @@ async def test_command_templates(hass, mqtt_mock_entry_with_yaml_config, caplog) assert await async_setup_component( hass, - siren.DOMAIN, - {siren.DOMAIN: [config1, config2]}, + mqtt.DOMAIN, + {mqtt.DOMAIN: {siren.DOMAIN: [config1, config2]}}, ) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -824,42 +844,42 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT siren device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock_entry_no_yaml_config, siren.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, siren.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT siren device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock_entry_no_yaml_config, siren.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, siren.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock_entry_no_yaml_config, siren.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, siren.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock_entry_no_yaml_config, siren.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, siren.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock_entry_with_yaml_config, siren.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, siren.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock_entry_no_yaml_config, siren.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, siren.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -869,7 +889,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): hass, mqtt_mock_entry_no_yaml_config, siren.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, siren.SERVICE_TURN_ON, command_payload='{"state":"ON"}', ) @@ -906,7 +926,7 @@ async def test_publishing_with_custom_encoding( ): """Test publishing MQTT payload with command templates and different encoding.""" domain = siren.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG[domain]) + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) config[siren.ATTR_AVAILABLE_TONES] = ["siren", "xylophone"] await help_test_publishing_with_custom_encoding( @@ -926,7 +946,7 @@ async def test_publishing_with_custom_encoding( async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = siren.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) @@ -935,7 +955,7 @@ async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_pa async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = siren.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) @@ -960,7 +980,7 @@ async def test_encoding_subscribable_topics( mqtt_mock_entry_with_yaml_config, caplog, siren.DOMAIN, - DEFAULT_CONFIG[siren.DOMAIN], + DEFAULT_CONFIG_LEGACY[siren.DOMAIN], topic, value, attribute, @@ -971,7 +991,7 @@ async def test_encoding_subscribable_topics( async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = siren.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG[platform]) + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) config["name"] = "test" del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) @@ -981,7 +1001,20 @@ async def test_setup_manual_entity_from_yaml(hass): async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): """Test unloading the config entry.""" domain = siren.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) + + +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +async def test_setup_with_legacy_schema(hass, mqtt_mock_entry_with_yaml_config): + """Test a setup with deprecated yaml platform schema.""" + domain = siren.DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) + config["name"] = "test" + assert await async_setup_component(hass, domain, {domain: config}) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + assert hass.states.get(f"{domain}.test") is not None diff --git a/tests/components/mqtt/test_state_vacuum.py b/tests/components/mqtt/test_state_vacuum.py index b0b89c28646..4c2fbf3a596 100644 --- a/tests/components/mqtt/test_state_vacuum.py +++ b/tests/components/mqtt/test_state_vacuum.py @@ -5,7 +5,7 @@ from unittest.mock import patch import pytest -from homeassistant.components import vacuum +from homeassistant.components import mqtt, vacuum from homeassistant.components.mqtt.const import CONF_COMMAND_TOPIC, CONF_STATE_TOPIC from homeassistant.components.mqtt.vacuum import CONF_SCHEMA, schema_state as mqttvacuum from homeassistant.components.mqtt.vacuum.const import MQTT_VACUUM_ATTRIBUTES_BLOCKED @@ -73,19 +73,27 @@ SEND_COMMAND_TOPIC = "vacuum/send_command" STATE_TOPIC = "vacuum/state" DEFAULT_CONFIG = { - CONF_PLATFORM: "mqtt", - CONF_SCHEMA: "state", - CONF_NAME: "mqtttest", - CONF_COMMAND_TOPIC: COMMAND_TOPIC, - mqttvacuum.CONF_SEND_COMMAND_TOPIC: SEND_COMMAND_TOPIC, - CONF_STATE_TOPIC: STATE_TOPIC, - mqttvacuum.CONF_SET_FAN_SPEED_TOPIC: "vacuum/set_fan_speed", - mqttvacuum.CONF_FAN_SPEED_LIST: ["min", "medium", "high", "max"], + mqtt.DOMAIN: { + vacuum.DOMAIN: { + CONF_SCHEMA: "state", + CONF_NAME: "mqtttest", + CONF_COMMAND_TOPIC: COMMAND_TOPIC, + mqttvacuum.CONF_SEND_COMMAND_TOPIC: SEND_COMMAND_TOPIC, + CONF_STATE_TOPIC: STATE_TOPIC, + mqttvacuum.CONF_SET_FAN_SPEED_TOPIC: "vacuum/set_fan_speed", + mqttvacuum.CONF_FAN_SPEED_LIST: ["min", "medium", "high", "max"], + } + } } -DEFAULT_CONFIG_2 = { - vacuum.DOMAIN: {"platform": "mqtt", "schema": "state", "name": "test"} -} +DEFAULT_CONFIG_2 = {mqtt.DOMAIN: {vacuum.DOMAIN: {"schema": "state", "name": "test"}}} + +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +DEFAULT_CONFIG_LEGACY = deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) +DEFAULT_CONFIG_LEGACY[vacuum.DOMAIN][CONF_PLATFORM] = mqtt.DOMAIN +DEFAULT_CONFIG_2_LEGACY = deepcopy(DEFAULT_CONFIG_2[mqtt.DOMAIN]) +DEFAULT_CONFIG_2_LEGACY[vacuum.DOMAIN][CONF_PLATFORM] = mqtt.DOMAIN @pytest.fixture(autouse=True) @@ -97,9 +105,7 @@ def vacuum_platform_only(): async def test_default_supported_features(hass, mqtt_mock_entry_with_yaml_config): """Test that the correct supported features.""" - assert await async_setup_component( - hass, vacuum.DOMAIN, {vacuum.DOMAIN: DEFAULT_CONFIG} - ) + assert await async_setup_component(hass, mqtt.DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() entity = hass.states.get("vacuum.mqtttest") @@ -111,12 +117,14 @@ async def test_default_supported_features(hass, mqtt_mock_entry_with_yaml_config async def test_all_commands(hass, mqtt_mock_entry_with_yaml_config): """Test simple commands send to the vacuum.""" - config = deepcopy(DEFAULT_CONFIG) + config = deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][vacuum.DOMAIN]) config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( mqttvacuum.ALL_SERVICES, SERVICE_TO_STRING ) - assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) + assert await async_setup_component( + hass, mqtt.DOMAIN, {mqtt.DOMAIN: {vacuum.DOMAIN: config}} + ) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -185,13 +193,15 @@ async def test_commands_without_supported_features( hass, mqtt_mock_entry_with_yaml_config ): """Test commands which are not supported by the vacuum.""" - config = deepcopy(DEFAULT_CONFIG) + config = deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][vacuum.DOMAIN]) services = mqttvacuum.STRING_TO_SERVICE["status"] config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( services, SERVICE_TO_STRING ) - assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) + assert await async_setup_component( + hass, mqtt.DOMAIN, {mqtt.DOMAIN: {vacuum.DOMAIN: config}} + ) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -243,12 +253,14 @@ async def test_commands_without_supported_features( async def test_status(hass, mqtt_mock_entry_with_yaml_config): """Test status updates from the vacuum.""" - config = deepcopy(DEFAULT_CONFIG) + config = deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][vacuum.DOMAIN]) config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( mqttvacuum.ALL_SERVICES, SERVICE_TO_STRING ) - assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) + assert await async_setup_component( + hass, mqtt.DOMAIN, {mqtt.DOMAIN: {vacuum.DOMAIN: config}} + ) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() state = hass.states.get("vacuum.mqtttest") @@ -288,13 +300,15 @@ async def test_status(hass, mqtt_mock_entry_with_yaml_config): async def test_no_fan_vacuum(hass, mqtt_mock_entry_with_yaml_config): """Test status updates from the vacuum when fan is not supported.""" - config = deepcopy(DEFAULT_CONFIG) + config = deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][vacuum.DOMAIN]) del config[mqttvacuum.CONF_FAN_SPEED_LIST] config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( mqttvacuum.DEFAULT_SERVICES, SERVICE_TO_STRING ) - assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) + assert await async_setup_component( + hass, mqtt.DOMAIN, {mqtt.DOMAIN: {vacuum.DOMAIN: config}} + ) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -340,12 +354,14 @@ async def test_no_fan_vacuum(hass, mqtt_mock_entry_with_yaml_config): @pytest.mark.no_fail_on_log_exception async def test_status_invalid_json(hass, mqtt_mock_entry_with_yaml_config): """Test to make sure nothing breaks if the vacuum sends bad JSON.""" - config = deepcopy(DEFAULT_CONFIG) + config = deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][vacuum.DOMAIN]) config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( mqttvacuum.ALL_SERVICES, SERVICE_TO_STRING ) - assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) + assert await async_setup_component( + hass, mqtt.DOMAIN, {mqtt.DOMAIN: {vacuum.DOMAIN: config}} + ) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -359,28 +375,28 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY ) async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY ) async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY ) async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY ) @@ -389,7 +405,7 @@ async def test_setting_attribute_via_mqtt_json_message( ): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY ) @@ -401,7 +417,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, - DEFAULT_CONFIG_2, + DEFAULT_CONFIG_2_LEGACY, MQTT_VACUUM_ATTRIBUTES_BLOCKED, ) @@ -409,7 +425,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY ) @@ -418,7 +434,11 @@ async def test_update_with_json_attrs_not_dict( ): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock_entry_with_yaml_config, caplog, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + vacuum.DOMAIN, + DEFAULT_CONFIG_2_LEGACY, ) @@ -427,14 +447,22 @@ async def test_update_with_json_attrs_bad_json( ): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock_entry_with_yaml_config, caplog, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + vacuum.DOMAIN, + DEFAULT_CONFIG_2_LEGACY, ) async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock_entry_no_yaml_config, caplog, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + vacuum.DOMAIN, + DEFAULT_CONFIG_2_LEGACY, ) @@ -511,42 +539,42 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT vacuum device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY ) async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT vacuum device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY ) async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY ) async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY ) async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY ) async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY ) @@ -556,7 +584,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, - DEFAULT_CONFIG_2, + DEFAULT_CONFIG_2_LEGACY, vacuum.SERVICE_START, command_payload="start", state_payload="{}", @@ -615,7 +643,7 @@ async def test_publishing_with_custom_encoding( ): """Test publishing MQTT payload with different encoding.""" domain = vacuum.DOMAIN - config = deepcopy(DEFAULT_CONFIG) + config = deepcopy(DEFAULT_CONFIG_LEGACY[domain]) config["supported_features"] = [ "battery", "clean_spot", @@ -646,7 +674,7 @@ async def test_publishing_with_custom_encoding( async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = vacuum.DOMAIN - config = DEFAULT_CONFIG + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) @@ -655,7 +683,7 @@ async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_pa async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = vacuum.DOMAIN - config = DEFAULT_CONFIG + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) @@ -691,7 +719,7 @@ async def test_encoding_subscribable_topics( mqtt_mock_entry_with_yaml_config, caplog, vacuum.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY[vacuum.DOMAIN], topic, value, attribute, @@ -703,8 +731,21 @@ async def test_encoding_subscribable_topics( async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = vacuum.DOMAIN - config = deepcopy(DEFAULT_CONFIG) + config = deepcopy(DEFAULT_CONFIG_LEGACY[platform]) config["name"] = "test" del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None + + +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +async def test_setup_with_legacy_schema(hass, mqtt_mock_entry_with_yaml_config): + """Test a setup with deprecated yaml platform schema.""" + domain = vacuum.DOMAIN + config = deepcopy(DEFAULT_CONFIG_LEGACY[domain]) + config["name"] = "test" + assert await async_setup_component(hass, domain, {domain: config}) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + assert hass.states.get(f"{domain}.test") is not None diff --git a/tests/components/mqtt/test_switch.py b/tests/components/mqtt/test_switch.py index ac69b17e18e..87d919f41c5 100644 --- a/tests/components/mqtt/test_switch.py +++ b/tests/components/mqtt/test_switch.py @@ -4,7 +4,7 @@ from unittest.mock import patch import pytest -from homeassistant.components import switch +from homeassistant.components import mqtt, switch from homeassistant.const import ( ATTR_ASSUMED_STATE, ATTR_DEVICE_CLASS, @@ -51,9 +51,14 @@ from tests.common import async_fire_mqtt_message, mock_restore_cache from tests.components.switch import common DEFAULT_CONFIG = { - switch.DOMAIN: {"platform": "mqtt", "name": "test", "command_topic": "test-topic"} + mqtt.DOMAIN: {switch.DOMAIN: {"name": "test", "command_topic": "test-topic"}} } +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +DEFAULT_CONFIG_LEGACY = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) +DEFAULT_CONFIG_LEGACY[switch.DOMAIN]["platform"] = mqtt.DOMAIN + @pytest.fixture(autouse=True) def switch_platform_only(): @@ -66,16 +71,17 @@ async def test_controlling_state_via_topic(hass, mqtt_mock_entry_with_yaml_confi """Test the controlling state via topic.""" assert await async_setup_component( hass, - switch.DOMAIN, + mqtt.DOMAIN, { - switch.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "payload_on": 1, - "payload_off": 0, - "device_class": "switch", + mqtt.DOMAIN: { + switch.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "payload_on": 1, + "payload_off": 0, + "device_class": "switch", + } } }, ) @@ -112,15 +118,16 @@ async def test_sending_mqtt_commands_and_optimistic( assert await async_setup_component( hass, - switch.DOMAIN, + mqtt.DOMAIN, { - switch.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "command-topic", - "payload_on": "beer on", - "payload_off": "beer off", - "qos": "2", + mqtt.DOMAIN: { + switch.DOMAIN: { + "name": "test", + "command_topic": "command-topic", + "payload_on": "beer on", + "payload_off": "beer off", + "qos": "2", + } } }, ) @@ -155,12 +162,13 @@ async def test_sending_inital_state_and_optimistic( """Test the initial state in optimistic mode.""" assert await async_setup_component( hass, - switch.DOMAIN, + mqtt.DOMAIN, { - switch.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "command-topic", + mqtt.DOMAIN: { + switch.DOMAIN: { + "name": "test", + "command_topic": "command-topic", + } } }, ) @@ -178,16 +186,17 @@ async def test_controlling_state_via_topic_and_json_message( """Test the controlling state via topic and JSON message.""" assert await async_setup_component( hass, - switch.DOMAIN, + mqtt.DOMAIN, { - switch.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "payload_on": "beer on", - "payload_off": "beer off", - "value_template": "{{ value_json.val }}", + mqtt.DOMAIN: { + switch.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "payload_on": "beer on", + "payload_off": "beer off", + "value_template": "{{ value_json.val }}", + } } }, ) @@ -218,14 +227,14 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, switch.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, switch.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock_entry_with_yaml_config, switch.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, switch.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -281,17 +290,18 @@ async def test_custom_state_payload(hass, mqtt_mock_entry_with_yaml_config): """Test the state payload.""" assert await async_setup_component( hass, - switch.DOMAIN, + mqtt.DOMAIN, { - switch.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "payload_on": 1, - "payload_off": 0, - "state_on": "HIGH", - "state_off": "LOW", + mqtt.DOMAIN: { + switch.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "payload_on": 1, + "payload_off": 0, + "state_on": "HIGH", + "state_off": "LOW", + } } }, ) @@ -318,7 +328,7 @@ async def test_setting_attribute_via_mqtt_json_message( ): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock_entry_with_yaml_config, switch.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, switch.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -327,14 +337,14 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( ): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, mqtt_mock_entry_no_yaml_config, switch.DOMAIN, DEFAULT_CONFIG, {} + hass, mqtt_mock_entry_no_yaml_config, switch.DOMAIN, DEFAULT_CONFIG_LEGACY, {} ) async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock_entry_with_yaml_config, switch.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, switch.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -343,7 +353,11 @@ async def test_update_with_json_attrs_not_dict( ): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock_entry_with_yaml_config, caplog, switch.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + switch.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) @@ -352,14 +366,22 @@ async def test_update_with_json_attrs_bad_JSON( ): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock_entry_with_yaml_config, caplog, switch.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + switch.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock_entry_no_yaml_config, caplog, switch.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + switch.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) @@ -404,8 +426,8 @@ async def test_discovery_update_switch_topic_template( hass, mqtt_mock_entry_no_yaml_config, caplog ): """Test update of discovered switch.""" - config1 = copy.deepcopy(DEFAULT_CONFIG[switch.DOMAIN]) - config2 = copy.deepcopy(DEFAULT_CONFIG[switch.DOMAIN]) + config1 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[switch.DOMAIN]) + config2 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[switch.DOMAIN]) config1["name"] = "Beer" config2["name"] = "Milk" config1["state_topic"] = "switch/state1" @@ -441,8 +463,8 @@ async def test_discovery_update_switch_template( hass, mqtt_mock_entry_no_yaml_config, caplog ): """Test update of discovered switch.""" - config1 = copy.deepcopy(DEFAULT_CONFIG[switch.DOMAIN]) - config2 = copy.deepcopy(DEFAULT_CONFIG[switch.DOMAIN]) + config1 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[switch.DOMAIN]) + config2 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[switch.DOMAIN]) config1["name"] = "Beer" config2["name"] = "Milk" config1["state_topic"] = "switch/state1" @@ -512,42 +534,42 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT switch device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock_entry_no_yaml_config, switch.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, switch.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT switch device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock_entry_no_yaml_config, switch.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, switch.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock_entry_no_yaml_config, switch.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, switch.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock_entry_no_yaml_config, switch.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, switch.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock_entry_with_yaml_config, switch.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, switch.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock_entry_no_yaml_config, switch.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, switch.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -557,7 +579,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): hass, mqtt_mock_entry_no_yaml_config, switch.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, switch.SERVICE_TURN_ON, ) @@ -593,7 +615,7 @@ async def test_publishing_with_custom_encoding( ): """Test publishing MQTT payload with different encoding.""" domain = switch.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_publishing_with_custom_encoding( hass, @@ -612,7 +634,7 @@ async def test_publishing_with_custom_encoding( async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = switch.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) @@ -621,7 +643,7 @@ async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_pa async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = switch.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) @@ -646,7 +668,7 @@ async def test_encoding_subscribable_topics( mqtt_mock_entry_with_yaml_config, caplog, switch.DOMAIN, - DEFAULT_CONFIG[switch.DOMAIN], + DEFAULT_CONFIG_LEGACY[switch.DOMAIN], topic, value, attribute, @@ -657,7 +679,7 @@ async def test_encoding_subscribable_topics( async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = switch.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG[platform]) + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) config["name"] = "test" del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) @@ -667,7 +689,20 @@ async def test_setup_manual_entity_from_yaml(hass): async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): """Test unloading the config entry.""" domain = switch.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) + + +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +async def test_setup_with_legacy_schema(hass, mqtt_mock_entry_with_yaml_config): + """Test a setup with deprecated yaml platform schema.""" + domain = switch.DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) + config["name"] = "test" + assert await async_setup_component(hass, domain, {domain: config}) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + assert hass.states.get(f"{domain}.test") is not None From 57212a39e459eda0a7f6c73ea60b677d08f2e11f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20V=C3=B6lker?= Date: Tue, 6 Sep 2022 11:10:35 +0200 Subject: [PATCH 134/955] Adjust Renault default scan interval (#77823) raise DEFAULT_SCAN_INTERVAL to 7 minutes This PR is raising the default scan interval for the Renault API from 5 minutes to 7 minutes. Lower intervals fail sometimes, maybe due to quota limitations. This seems to be a working interval as described in home-assistant#73220 --- homeassistant/components/renault/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/renault/const.py b/homeassistant/components/renault/const.py index 89bf322c2bf..b29f4ad0701 100644 --- a/homeassistant/components/renault/const.py +++ b/homeassistant/components/renault/const.py @@ -6,7 +6,7 @@ DOMAIN = "renault" CONF_LOCALE = "locale" CONF_KAMEREON_ACCOUNT_ID = "kamereon_account_id" -DEFAULT_SCAN_INTERVAL = 300 # 5 minutes +DEFAULT_SCAN_INTERVAL = 420 # 7 minutes PLATFORMS = [ Platform.BINARY_SENSOR, From 3327493ab7688da93acf52643e7e898e2e27d400 Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Tue, 6 Sep 2022 11:24:08 +0200 Subject: [PATCH 135/955] Add state class total increasing to Tasmota energy today sensor (#77140) Add total increasing to tasmota energy today sensor --- homeassistant/components/tasmota/sensor.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tasmota/sensor.py b/homeassistant/components/tasmota/sensor.py index a9ab994b299..73b261ed78c 100644 --- a/homeassistant/components/tasmota/sensor.py +++ b/homeassistant/components/tasmota/sensor.py @@ -180,7 +180,10 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = { DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, STATE_CLASS: SensorStateClass.MEASUREMENT, }, - hc.SENSOR_TODAY: {DEVICE_CLASS: SensorDeviceClass.ENERGY}, + hc.SENSOR_TODAY: { + DEVICE_CLASS: SensorDeviceClass.ENERGY, + STATE_CLASS: SensorStateClass.TOTAL_INCREASING, + }, hc.SENSOR_TOTAL: { DEVICE_CLASS: SensorDeviceClass.ENERGY, STATE_CLASS: SensorStateClass.TOTAL, From 03b4d2556445514e0ea525f6f67d702012c40d6a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 6 Sep 2022 12:18:15 +0200 Subject: [PATCH 136/955] Add notify get_service to pylint checks (#77643) Add notify get_service to pylint checks (take 3) --- pylint/plugins/hass_enforce_type_hints.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 4eedca487ab..2a709c6debe 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -379,6 +379,18 @@ _FUNCTION_MATCH: dict[str, list[TypeHintMatch]] = { return_type=_Special.UNDEFINED, ), ], + "notify": [ + TypeHintMatch( + function_name="get_service", + arg_types={ + 0: "HomeAssistant", + 1: "ConfigType", + 2: "DiscoveryInfoType | None", + }, + return_type=_Special.UNDEFINED, + has_async_counterpart=True, + ), + ], } _CLASS_MATCH: dict[str, list[ClassTypeHintMatch]] = { From e5ac50fc57c3ab50f4a244969249b4d2e8c21cb5 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 6 Sep 2022 12:18:29 +0200 Subject: [PATCH 137/955] Add BaseNotificationService to pylint checks (#77663) * Add BaseNotificationService to pylint checks * Remove comment --- pylint/plugins/hass_enforce_type_hints.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 2a709c6debe..289f461c223 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -1873,6 +1873,20 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ], ), ], + "notify": [ + ClassTypeHintMatch( + base_class="BaseNotificationService", + matches=[ + TypeHintMatch( + function_name="send_message", + arg_types={1: "str"}, + kwargs_type="Any", + return_type=None, + has_async_counterpart=True, + ), + ], + ), + ], "remote": [ ClassTypeHintMatch( base_class="Entity", From 34da463df02c2c9c9ee574ca9e7ef115395e6420 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Tue, 6 Sep 2022 18:33:21 +0800 Subject: [PATCH 138/955] Cleanup camera after late PR review (#77880) Cleanup changes to camera from #77439 --- homeassistant/components/camera/__init__.py | 3 ++- homeassistant/components/camera/prefs.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index afc6be48144..e5ccb433975 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -893,10 +893,11 @@ async def websocket_update_prefs( entity_id = changes.pop("entity_id") try: entity_prefs = await prefs.async_update(entity_id, **changes) - connection.send_result(msg["id"], entity_prefs) except HomeAssistantError as ex: _LOGGER.error("Error setting camera preferences: %s", ex) connection.send_error(msg["id"], "update_failed", str(ex)) + else: + connection.send_result(msg["id"], entity_prefs) async def async_handle_snapshot_service( diff --git a/homeassistant/components/camera/prefs.py b/homeassistant/components/camera/prefs.py index effc2f619bd..1107da2ba38 100644 --- a/homeassistant/components/camera/prefs.py +++ b/homeassistant/components/camera/prefs.py @@ -68,7 +68,8 @@ class CameraPreferences: ) -> dict[str, bool | int]: """Update camera preferences. - Returns a dict with the preferences on success or a string on error. + Returns a dict with the preferences on success. + Raises HomeAssistantError on failure. """ if preload_stream is not UNDEFINED: # Prefs already initialized. From 0c767bd0d37a41af37728b1d8b4eae8dceb7e188 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 6 Sep 2022 13:35:14 +0200 Subject: [PATCH 139/955] Improve entity type hints [s] (part 1/2) (#77881) --- homeassistant/components/sabnzbd/sensor.py | 2 +- .../components/satel_integra/binary_sensor.py | 2 +- .../components/satel_integra/switch.py | 7 ++--- homeassistant/components/schluter/climate.py | 3 ++- .../components/screenlogic/climate.py | 7 ++--- homeassistant/components/scsgate/switch.py | 5 ++-- .../components/sense/binary_sensor.py | 2 +- homeassistant/components/sense/sensor.py | 6 ++--- homeassistant/components/serial/sensor.py | 2 +- homeassistant/components/serial_pm/sensor.py | 2 +- .../components/seventeentrack/sensor.py | 6 ++--- homeassistant/components/sigfox/sensor.py | 2 +- homeassistant/components/simulated/sensor.py | 2 +- .../components/sisyphus/media_player.py | 26 +++++++++---------- .../components/slimproto/media_player.py | 7 +++-- .../components/smappee/binary_sensor.py | 4 +-- homeassistant/components/smappee/sensor.py | 2 +- homeassistant/components/smappee/switch.py | 10 ++++--- .../components/smartthings/climate.py | 17 ++++++------ .../components/smartthings/switch.py | 5 ++-- homeassistant/components/smarttub/climate.py | 8 +++--- homeassistant/components/smarttub/switch.py | 8 +++--- .../components/snapcast/media_player.py | 12 ++++----- homeassistant/components/snmp/sensor.py | 2 +- homeassistant/components/snmp/switch.py | 7 ++--- 25 files changed, 86 insertions(+), 70 deletions(-) diff --git a/homeassistant/components/sabnzbd/sensor.py b/homeassistant/components/sabnzbd/sensor.py index b2828a30969..b4231a469ec 100644 --- a/homeassistant/components/sabnzbd/sensor.py +++ b/homeassistant/components/sabnzbd/sensor.py @@ -167,7 +167,7 @@ class SabnzbdSensor(SensorEntity): name=DEFAULT_NAME, ) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Call when entity about to be added to hass.""" self.async_on_remove( async_dispatcher_connect( diff --git a/homeassistant/components/satel_integra/binary_sensor.py b/homeassistant/components/satel_integra/binary_sensor.py index f55545239fb..389bde884ef 100644 --- a/homeassistant/components/satel_integra/binary_sensor.py +++ b/homeassistant/components/satel_integra/binary_sensor.py @@ -73,7 +73,7 @@ class SatelIntegraBinarySensor(BinarySensorEntity): self._react_to_signal = react_to_signal self._satel = controller - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" if self._react_to_signal == SIGNAL_OUTPUTS_UPDATED: if self._device_number in self._satel.violated_outputs: diff --git a/homeassistant/components/satel_integra/switch.py b/homeassistant/components/satel_integra/switch.py index 7c7b91c4ac6..469b2280290 100644 --- a/homeassistant/components/satel_integra/switch.py +++ b/homeassistant/components/satel_integra/switch.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from homeassistant.components.switch import SwitchEntity from homeassistant.core import HomeAssistant, callback @@ -61,7 +62,7 @@ class SatelIntegraSwitch(SwitchEntity): self._code = code self._satel = controller - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" async_dispatcher_connect( self.hass, SIGNAL_OUTPUTS_UPDATED, self._devices_updated @@ -78,13 +79,13 @@ class SatelIntegraSwitch(SwitchEntity): self._state = new_state self.async_write_ha_state() - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" _LOGGER.debug("Switch: %s status: %s, turning on", self._name, self._state) await self._satel.set_output(self._code, self._device_number, True) self.async_write_ha_state() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" _LOGGER.debug( "Switch name: %s status: %s, turning off", self._name, self._state diff --git a/homeassistant/components/schluter/climate.py b/homeassistant/components/schluter/climate.py index 3ccea458960..dd4f578cebb 100644 --- a/homeassistant/components/schluter/climate.py +++ b/homeassistant/components/schluter/climate.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from requests import RequestException import voluptuous as vol @@ -131,7 +132,7 @@ class SchluterThermostat(CoordinatorEntity, ClimateEntity): async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Mode is always heating, so do nothing.""" - def set_temperature(self, **kwargs): + def set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" target_temp = None target_temp = kwargs.get(ATTR_TEMPERATURE) diff --git a/homeassistant/components/screenlogic/climate.py b/homeassistant/components/screenlogic/climate.py index adc103fa684..b5e83ec827a 100644 --- a/homeassistant/components/screenlogic/climate.py +++ b/homeassistant/components/screenlogic/climate.py @@ -1,5 +1,6 @@ """Support for a ScreenLogic heating device.""" import logging +from typing import Any from screenlogicpy.const import DATA as SL_DATA, EQUIPMENT, HEAT_MODE @@ -130,7 +131,7 @@ class ScreenLogicClimate(ScreenlogicEntity, ClimateEntity, RestoreEntity): HEAT_MODE.NAME_FOR_NUM[mode_num] for mode_num in self._configured_heat_modes ] - async def async_set_temperature(self, **kwargs) -> None: + async def async_set_temperature(self, **kwargs: Any) -> None: """Change the setpoint of the heater.""" if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: raise ValueError(f"Expected attribute {ATTR_TEMPERATURE}") @@ -144,7 +145,7 @@ class ScreenLogicClimate(ScreenlogicEntity, ClimateEntity, RestoreEntity): f"Failed to set_temperature {temperature} on body {self.body['body_type']['value']}" ) - async def async_set_hvac_mode(self, hvac_mode) -> None: + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set the operation mode.""" if hvac_mode == HVACMode.OFF: mode = HEAT_MODE.OFF @@ -172,7 +173,7 @@ class ScreenLogicClimate(ScreenlogicEntity, ClimateEntity, RestoreEntity): f"Failed to set_preset_mode {mode} on body {self.body['body_type']['value']}" ) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Run when entity is about to be added.""" await super().async_added_to_hass() diff --git a/homeassistant/components/scsgate/switch.py b/homeassistant/components/scsgate/switch.py index b9c25207745..3f215130048 100644 --- a/homeassistant/components/scsgate/switch.py +++ b/homeassistant/components/scsgate/switch.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from scsgate.messages import ScenarioTriggeredMessage, StateMessage from scsgate.tasks import ToggleStatusTask @@ -116,7 +117,7 @@ class SCSGateSwitch(SwitchEntity): """Return true if switch is on.""" return self._toggled - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" self._scsgate.append_task(ToggleStatusTask(target=self._scs_id, toggled=True)) @@ -124,7 +125,7 @@ class SCSGateSwitch(SwitchEntity): self._toggled = True self.schedule_update_ha_state() - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" self._scsgate.append_task(ToggleStatusTask(target=self._scs_id, toggled=False)) diff --git a/homeassistant/components/sense/binary_sensor.py b/homeassistant/components/sense/binary_sensor.py index b9c1a3cb9eb..0d4fbcf1de2 100644 --- a/homeassistant/components/sense/binary_sensor.py +++ b/homeassistant/components/sense/binary_sensor.py @@ -126,7 +126,7 @@ class SenseDevice(BinarySensorEntity): """Return the device class of the binary sensor.""" return BinarySensorDeviceClass.POWER - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" self.async_on_remove( async_dispatcher_connect( diff --git a/homeassistant/components/sense/sensor.py b/homeassistant/components/sense/sensor.py index 8362f47f090..54f05e2dbb5 100644 --- a/homeassistant/components/sense/sensor.py +++ b/homeassistant/components/sense/sensor.py @@ -182,7 +182,7 @@ class SenseActiveSensor(SensorEntity): self._variant_id = variant_id self._variant_name = variant_name - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" self.async_on_remove( async_dispatcher_connect( @@ -230,7 +230,7 @@ class SenseVoltageSensor(SensorEntity): self._sense_monitor_id = sense_monitor_id self._voltage_index = index - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" self.async_on_remove( async_dispatcher_connect( @@ -323,7 +323,7 @@ class SenseEnergyDevice(SensorEntity): self._attr_icon = sense_to_mdi(device["icon"]) self._sense_devices_data = sense_devices_data - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" self.async_on_remove( async_dispatcher_connect( diff --git a/homeassistant/components/serial/sensor.py b/homeassistant/components/serial/sensor.py index 9f1e04f1373..63253375cc7 100644 --- a/homeassistant/components/serial/sensor.py +++ b/homeassistant/components/serial/sensor.py @@ -143,7 +143,7 @@ class SerialSensor(SensorEntity): self._template = value_template self._attributes = None - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Handle when an entity is about to be added to Home Assistant.""" self._serial_loop_task = self.hass.loop.create_task( self.serial_read( diff --git a/homeassistant/components/serial_pm/sensor.py b/homeassistant/components/serial_pm/sensor.py index 2f34b1e3aca..2c94b9bcf01 100644 --- a/homeassistant/components/serial_pm/sensor.py +++ b/homeassistant/components/serial_pm/sensor.py @@ -90,7 +90,7 @@ class ParticulateMatterSensor(SensorEntity): """Return the unit of measurement of this entity, if any.""" return CONCENTRATION_MICROGRAMS_PER_CUBIC_METER - def update(self): + def update(self) -> None: """Read from sensor and update the state.""" _LOGGER.debug("Reading data from PM sensor") try: diff --git a/homeassistant/components/seventeentrack/sensor.py b/homeassistant/components/seventeentrack/sensor.py index 12cdcc0680c..363eb507710 100644 --- a/homeassistant/components/seventeentrack/sensor.py +++ b/homeassistant/components/seventeentrack/sensor.py @@ -134,7 +134,7 @@ class SeventeenTrackSummarySensor(SensorEntity): """Return the state.""" return self._state - async def async_update(self): + async def async_update(self) -> None: """Update the sensor.""" await self._data.async_update() @@ -189,7 +189,7 @@ class SeventeenTrackPackageSensor(SensorEntity): ) @property - def available(self): + def available(self) -> bool: """Return whether the entity is available.""" return self._data.packages.get(self._tracking_number) is not None @@ -205,7 +205,7 @@ class SeventeenTrackPackageSensor(SensorEntity): """Return the state.""" return self._state - async def async_update(self): + async def async_update(self) -> None: """Update the sensor.""" await self._data.async_update() diff --git a/homeassistant/components/sigfox/sensor.py b/homeassistant/components/sigfox/sensor.py index 392bfeead89..1a1d7bb74b0 100644 --- a/homeassistant/components/sigfox/sensor.py +++ b/homeassistant/components/sigfox/sensor.py @@ -149,7 +149,7 @@ class SigfoxDevice(SensorEntity): "time": epoch_to_datetime(epoch_time), } - def update(self): + def update(self) -> None: """Fetch the latest device message.""" self._message_data = self.get_last_message() self._state = self._message_data["payload"] diff --git a/homeassistant/components/simulated/sensor.py b/homeassistant/components/simulated/sensor.py index ddd5d99ac97..f2e64655acc 100644 --- a/homeassistant/components/simulated/sensor.py +++ b/homeassistant/components/simulated/sensor.py @@ -121,7 +121,7 @@ class SimulatedSensor(SensorEntity): noise = self._random.gauss(mu=0, sigma=fwhm) return round(mean + periodic + noise, 3) - async def async_update(self): + async def async_update(self) -> None: """Update the sensor.""" self._state = self.signal_calc() diff --git a/homeassistant/components/sisyphus/media_player.py b/homeassistant/components/sisyphus/media_player.py index 208799361fe..ddd9da23e98 100644 --- a/homeassistant/components/sisyphus/media_player.py +++ b/homeassistant/components/sisyphus/media_player.py @@ -65,11 +65,11 @@ class SisyphusPlayer(MediaPlayerEntity): self._host = host self._table = table - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Add listeners after this object has been initialized.""" self._table.add_listener(self.async_write_ha_state) - async def async_update(self): + async def async_update(self) -> None: """Force update table state.""" await self._table.refresh() @@ -79,7 +79,7 @@ class SisyphusPlayer(MediaPlayerEntity): return self._table.id @property - def available(self): + def available(self) -> bool: """Return true if the table is responding to heartbeats.""" return self._table.is_connected @@ -113,7 +113,7 @@ class SisyphusPlayer(MediaPlayerEntity): """Return True if the current playlist is in shuffle mode.""" return self._table.is_shuffle - async def async_set_shuffle(self, shuffle): + async def async_set_shuffle(self, shuffle: bool) -> None: """Change the shuffle mode of the current playlist.""" await self._table.set_shuffle(shuffle) @@ -164,35 +164,35 @@ class SisyphusPlayer(MediaPlayerEntity): return super().media_image_url - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Wake up a sleeping table.""" await self._table.wakeup() - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Put the table to sleep.""" await self._table.sleep() - async def async_volume_down(self): + async def async_volume_down(self) -> None: """Slow down playback.""" await self._table.set_speed(max(0, self._table.speed - 0.1)) - async def async_volume_up(self): + async def async_volume_up(self) -> None: """Speed up playback.""" await self._table.set_speed(min(1.0, self._table.speed + 0.1)) - async def async_set_volume_level(self, volume): + async def async_set_volume_level(self, volume: float) -> None: """Set playback speed (0..1).""" await self._table.set_speed(volume) - async def async_media_play(self): + async def async_media_play(self) -> None: """Start playing.""" await self._table.play() - async def async_media_pause(self): + async def async_media_pause(self) -> None: """Pause.""" await self._table.pause() - async def async_media_next_track(self): + async def async_media_next_track(self) -> None: """Skip to next track.""" cur_track_index = self._get_current_track_index() @@ -200,7 +200,7 @@ class SisyphusPlayer(MediaPlayerEntity): self._table.active_playlist.tracks[cur_track_index + 1] ) - async def async_media_previous_track(self): + async def async_media_previous_track(self) -> None: """Skip to previous track.""" cur_track_index = self._get_current_track_index() diff --git a/homeassistant/components/slimproto/media_player.py b/homeassistant/components/slimproto/media_player.py index bcd538f57e8..a241cd2cd93 100644 --- a/homeassistant/components/slimproto/media_player.py +++ b/homeassistant/components/slimproto/media_player.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from typing import Any from aioslimproto.client import PlayerState, SlimClient from aioslimproto.const import EventType, SlimEvent @@ -175,7 +176,9 @@ class SlimProtoPlayer(MediaPlayerEntity): """Turn off device.""" await self.player.power(False) - async def async_play_media(self, media_type: str, media_id: str, **kwargs) -> None: + async def async_play_media( + self, media_type: str, media_id: str, **kwargs: Any + ) -> None: """Send the play_media command to the media player.""" to_send_media_type: str | None = media_type # Handle media_source @@ -193,7 +196,7 @@ class SlimProtoPlayer(MediaPlayerEntity): await self.player.play_url(media_id, mime_type=to_send_media_type) async def async_browse_media( - self, media_content_type=None, media_content_id=None + self, media_content_type: str | None = None, media_content_id: str | None = None ) -> BrowseMedia: """Implement the websocket media browsing helper.""" return await media_source.async_browse_media( diff --git a/homeassistant/components/smappee/binary_sensor.py b/homeassistant/components/smappee/binary_sensor.py index 1d7f00f5f4e..88d46e3689d 100644 --- a/homeassistant/components/smappee/binary_sensor.py +++ b/homeassistant/components/smappee/binary_sensor.py @@ -91,7 +91,7 @@ class SmappeePresence(BinarySensorEntity): sw_version=self._service_location.firmware_version, ) - async def async_update(self): + async def async_update(self) -> None: """Get the latest data from Smappee and update the state.""" await self._smappee_base.async_update() @@ -174,7 +174,7 @@ class SmappeeAppliance(BinarySensorEntity): sw_version=self._service_location.firmware_version, ) - async def async_update(self): + async def async_update(self) -> None: """Get the latest data from Smappee and update the state.""" await self._smappee_base.async_update() diff --git a/homeassistant/components/smappee/sensor.py b/homeassistant/components/smappee/sensor.py index b82d84f2c15..ff258677b3e 100644 --- a/homeassistant/components/smappee/sensor.py +++ b/homeassistant/components/smappee/sensor.py @@ -388,7 +388,7 @@ class SmappeeSensor(SensorEntity): sw_version=self._service_location.firmware_version, ) - async def async_update(self): + async def async_update(self) -> None: """Get the latest data from Smappee and update the state.""" await self._smappee_base.async_update() diff --git a/homeassistant/components/smappee/switch.py b/homeassistant/components/smappee/switch.py index e445b8e955b..b179daaf1a8 100644 --- a/homeassistant/components/smappee/switch.py +++ b/homeassistant/components/smappee/switch.py @@ -1,4 +1,6 @@ """Support for interacting with Smappee Comport Plugs, Switches and Output Modules.""" +from typing import Any + from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -108,7 +110,7 @@ class SmappeeActuator(SwitchEntity): """Icon to use in the frontend.""" return ICON - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn on Comport Plug.""" if self._actuator_type in ("SWITCH", "COMFORT_PLUG"): self._service_location.set_actuator_state(self._actuator_id, state="ON_ON") @@ -117,7 +119,7 @@ class SmappeeActuator(SwitchEntity): self._actuator_id, state=self._actuator_state_option ) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn off Comport Plug.""" if self._actuator_type in ("SWITCH", "COMFORT_PLUG"): self._service_location.set_actuator_state( @@ -129,7 +131,7 @@ class SmappeeActuator(SwitchEntity): ) @property - def available(self): + def available(self) -> bool: """Return True if entity is available. Unavailable for COMFORT_PLUGS.""" return ( self._connection_state == "CONNECTED" @@ -166,7 +168,7 @@ class SmappeeActuator(SwitchEntity): sw_version=self._service_location.firmware_version, ) - async def async_update(self): + async def async_update(self) -> None: """Get the latest data from Smappee and update the state.""" await self._smappee_base.async_update() diff --git a/homeassistant/components/smartthings/climate.py b/homeassistant/components/smartthings/climate.py index db13b5e6b37..7f628b8d8a0 100644 --- a/homeassistant/components/smartthings/climate.py +++ b/homeassistant/components/smartthings/climate.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio from collections.abc import Iterable, Sequence import logging +from typing import Any from pysmartthings import Attribute, Capability @@ -160,7 +161,7 @@ class SmartThingsThermostat(SmartThingsEntity, ClimateEntity): flags |= ClimateEntityFeature.FAN_MODE return flags - async def async_set_fan_mode(self, fan_mode): + async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new target fan mode.""" await self._device.set_thermostat_fan_mode(fan_mode, set_status=True) @@ -177,7 +178,7 @@ class SmartThingsThermostat(SmartThingsEntity, ClimateEntity): # the entity state ahead of receiving the confirming push updates self.async_schedule_update_ha_state(True) - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new operation mode and target temperatures.""" # Operation state if operation_state := kwargs.get(ATTR_HVAC_MODE): @@ -214,7 +215,7 @@ class SmartThingsThermostat(SmartThingsEntity, ClimateEntity): # the entity state ahead of receiving the confirming push updates self.async_schedule_update_ha_state(True) - async def async_update(self): + async def async_update(self) -> None: """Update the attributes of the climate device.""" thermostat_mode = self._device.status.thermostat_mode self._hvac_mode = MODE_TO_STATE.get(thermostat_mode) @@ -326,7 +327,7 @@ class SmartThingsAirConditioner(SmartThingsEntity, ClimateEntity): super().__init__(device) self._hvac_modes = None - async def async_set_fan_mode(self, fan_mode): + async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new target fan mode.""" await self._device.set_fan_mode(fan_mode, set_status=True) # State is set optimistically in the command above, therefore update @@ -352,7 +353,7 @@ class SmartThingsAirConditioner(SmartThingsEntity, ClimateEntity): # the entity state ahead of receiving the confirming push updates self.async_write_ha_state() - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" tasks = [] # operation mode @@ -372,21 +373,21 @@ class SmartThingsAirConditioner(SmartThingsEntity, ClimateEntity): # the entity state ahead of receiving the confirming push updates self.async_write_ha_state() - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Turn device on.""" await self._device.switch_on(set_status=True) # State is set optimistically in the command above, therefore update # the entity state ahead of receiving the confirming push updates self.async_write_ha_state() - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Turn device off.""" await self._device.switch_off(set_status=True) # State is set optimistically in the command above, therefore update # the entity state ahead of receiving the confirming push updates self.async_write_ha_state() - async def async_update(self): + async def async_update(self) -> None: """Update the calculated fields of the AC.""" modes = {HVACMode.OFF} for mode in self._device.status.supported_ac_modes: diff --git a/homeassistant/components/smartthings/switch.py b/homeassistant/components/smartthings/switch.py index f9df5dacdad..e6432dcb50c 100644 --- a/homeassistant/components/smartthings/switch.py +++ b/homeassistant/components/smartthings/switch.py @@ -2,6 +2,7 @@ from __future__ import annotations from collections.abc import Sequence +from typing import Any from pysmartthings import Capability @@ -41,14 +42,14 @@ def get_capabilities(capabilities: Sequence[str]) -> Sequence[str] | None: class SmartThingsSwitch(SmartThingsEntity, SwitchEntity): """Define a SmartThings switch.""" - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" await self._device.switch_off(set_status=True) # State is set optimistically in the command above, therefore update # the entity state ahead of receiving the confirming push updates self.async_write_ha_state() - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" await self._device.switch_on(set_status=True) # State is set optimistically in the command above, therefore update diff --git a/homeassistant/components/smarttub/climate.py b/homeassistant/components/smarttub/climate.py index 78d1d7b2495..a9ff8699008 100644 --- a/homeassistant/components/smarttub/climate.py +++ b/homeassistant/components/smarttub/climate.py @@ -1,6 +1,8 @@ """Platform for climate integration.""" from __future__ import annotations +from typing import Any + from smarttub import Spa from homeassistant.components.climate import ClimateEntity @@ -72,7 +74,7 @@ class SmartTubThermostat(SmartTubEntity, ClimateEntity): """Return the current running hvac operation.""" return HVAC_ACTIONS.get(self.spa_status.heater) - async def async_set_hvac_mode(self, hvac_mode: HVACMode): + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new target hvac mode. As with hvac_mode, we don't really have an option here. @@ -113,13 +115,13 @@ class SmartTubThermostat(SmartTubEntity, ClimateEntity): """Return the target water temperature.""" return self.spa_status.set_temperature - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" temperature = kwargs[ATTR_TEMPERATURE] await self.spa.set_temperature(temperature) await self.coordinator.async_refresh() - async def async_set_preset_mode(self, preset_mode: str): + async def async_set_preset_mode(self, preset_mode: str) -> None: """Activate the specified preset mode.""" heat_mode = HEAT_MODES[preset_mode] await self.spa.set_heat_mode(heat_mode) diff --git a/homeassistant/components/smarttub/switch.py b/homeassistant/components/smarttub/switch.py index 9657444d9ad..b9afe94c06b 100644 --- a/homeassistant/components/smarttub/switch.py +++ b/homeassistant/components/smarttub/switch.py @@ -1,4 +1,6 @@ """Platform for switch integration.""" +from typing import Any + import async_timeout from smarttub import SpaPump @@ -62,21 +64,21 @@ class SmartTubPump(SmartTubEntity, SwitchEntity): """Return True if the pump is on.""" return self.pump.state != SpaPump.PumpState.OFF - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the pump on.""" # the API only supports toggling if not self.is_on: await self.async_toggle() - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the pump off.""" # the API only supports toggling if self.is_on: await self.async_toggle() - async def async_toggle(self, **kwargs) -> None: + async def async_toggle(self, **kwargs: Any) -> None: """Toggle the pump on or off.""" async with async_timeout.timeout(API_TIMEOUT): await self.pump.toggle() diff --git a/homeassistant/components/snapcast/media_player.py b/homeassistant/components/snapcast/media_player.py index 0e6524c8504..e7c4c9d8443 100644 --- a/homeassistant/components/snapcast/media_player.py +++ b/homeassistant/components/snapcast/media_player.py @@ -181,19 +181,19 @@ class SnapcastGroupDevice(MediaPlayerEntity): name = f"{self._group.friendly_name} {GROUP_SUFFIX}" return {"friendly_name": name} - async def async_select_source(self, source): + async def async_select_source(self, source: str) -> None: """Set input source.""" streams = self._group.streams_by_name() if source in streams: await self._group.set_stream(streams[source].identifier) self.async_write_ha_state() - async def async_mute_volume(self, mute): + async def async_mute_volume(self, mute: bool) -> None: """Send the mute command.""" await self._group.set_muted(mute) self.async_write_ha_state() - async def async_set_volume_level(self, volume): + async def async_set_volume_level(self, volume: float) -> None: """Set the volume level.""" await self._group.set_volume(round(volume * 100)) self.async_write_ha_state() @@ -292,19 +292,19 @@ class SnapcastClientDevice(MediaPlayerEntity): """Latency for Client.""" return self._client.latency - async def async_select_source(self, source): + async def async_select_source(self, source: str) -> None: """Set input source.""" streams = self._client.group.streams_by_name() if source in streams: await self._client.group.set_stream(streams[source].identifier) self.async_write_ha_state() - async def async_mute_volume(self, mute): + async def async_mute_volume(self, mute: bool) -> None: """Send the mute command.""" await self._client.set_muted(mute) self.async_write_ha_state() - async def async_set_volume_level(self, volume): + async def async_set_volume_level(self, volume: float) -> None: """Set the volume level.""" await self._client.set_volume(round(volume * 100)) self.async_write_ha_state() diff --git a/homeassistant/components/snmp/sensor.py b/homeassistant/components/snmp/sensor.py index 11f8c7d2f64..a582eef01fa 100644 --- a/homeassistant/components/snmp/sensor.py +++ b/homeassistant/components/snmp/sensor.py @@ -166,7 +166,7 @@ class SnmpSensor(TemplateSensor): """Return the state of the sensor.""" return self._state - async def async_update(self): + async def async_update(self) -> None: """Get the latest data and updates the states.""" await self.data.async_update() diff --git a/homeassistant/components/snmp/switch.py b/homeassistant/components/snmp/switch.py index b8370c4bba1..feb87e7e253 100644 --- a/homeassistant/components/snmp/switch.py +++ b/homeassistant/components/snmp/switch.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any import pysnmp.hlapi.asyncio as hlapi from pysnmp.hlapi.asyncio import ( @@ -235,12 +236,12 @@ class SnmpSwitch(SwitchEntity): ContextData(), ] - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the switch.""" # If vartype set, use it - http://snmplabs.com/pysnmp/docs/api-reference.html#pysnmp.smi.rfc1902.ObjectType await self._execute_command(self._command_payload_on) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the switch.""" await self._execute_command(self._command_payload_off) @@ -256,7 +257,7 @@ class SnmpSwitch(SwitchEntity): else: await self._set(MAP_SNMP_VARTYPES.get(self._vartype, Integer)(command)) - async def async_update(self): + async def async_update(self) -> None: """Update the state.""" errindication, errstatus, errindex, restable = await getCmd( *self._request_args, ObjectType(ObjectIdentity(self._baseoid)) From 458001a06e7678273ea43c33e55b833adadced9e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 6 Sep 2022 13:35:52 +0200 Subject: [PATCH 140/955] Improve entity type hints [t] (#77883) --- homeassistant/components/tado/binary_sensor.py | 4 ++-- homeassistant/components/tado/climate.py | 13 +++++++------ homeassistant/components/tado/sensor.py | 4 ++-- homeassistant/components/tado/water_heater.py | 7 ++++--- homeassistant/components/tank_utility/sensor.py | 2 +- homeassistant/components/tapsaff/binary_sensor.py | 2 +- homeassistant/components/ted5000/sensor.py | 2 +- homeassistant/components/tellduslive/switch.py | 6 ++++-- homeassistant/components/tellstick/sensor.py | 2 +- homeassistant/components/telnet/switch.py | 4 ++-- homeassistant/components/temper/sensor.py | 2 +- homeassistant/components/tfiac/climate.py | 13 +++++++------ .../components/thermoworks_smoke/sensor.py | 2 +- homeassistant/components/thethingsnetwork/sensor.py | 2 +- homeassistant/components/thinkingcleaner/sensor.py | 2 +- homeassistant/components/thinkingcleaner/switch.py | 7 ++++--- homeassistant/components/tibber/sensor.py | 4 ++-- homeassistant/components/tmb/sensor.py | 2 +- homeassistant/components/tod/binary_sensor.py | 2 +- homeassistant/components/todoist/calendar.py | 2 +- homeassistant/components/toon/climate.py | 2 +- .../components/totalconnect/binary_sensor.py | 2 +- homeassistant/components/touchline/climate.py | 6 +++--- homeassistant/components/traccar/device_tracker.py | 4 ++-- homeassistant/components/transmission/sensor.py | 10 +++++----- homeassistant/components/transmission/switch.py | 11 ++++++----- homeassistant/components/transport_nsw/sensor.py | 2 +- homeassistant/components/travisci/sensor.py | 2 +- homeassistant/components/trend/binary_sensor.py | 4 ++-- 29 files changed, 67 insertions(+), 60 deletions(-) diff --git a/homeassistant/components/tado/binary_sensor.py b/homeassistant/components/tado/binary_sensor.py index ef880bee50b..0eff510051d 100644 --- a/homeassistant/components/tado/binary_sensor.py +++ b/homeassistant/components/tado/binary_sensor.py @@ -108,7 +108,7 @@ class TadoDeviceBinarySensor(TadoDeviceEntity, BinarySensorEntity): self._state = None - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register for sensor updates.""" self.async_on_remove( @@ -184,7 +184,7 @@ class TadoZoneBinarySensor(TadoZoneEntity, BinarySensorEntity): self._state_attributes = None self._tado_zone_data = None - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register for sensor updates.""" self.async_on_remove( diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py index ae9c2097f75..879b13310a4 100644 --- a/homeassistant/components/tado/climate.py +++ b/homeassistant/components/tado/climate.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any import voluptuous as vol @@ -273,7 +274,7 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): self._async_update_zone_data() - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register for sensor updates.""" self.async_on_remove( @@ -349,7 +350,7 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): """List of available fan modes.""" return self._supported_fan_modes - def set_fan_mode(self, fan_mode: str): + def set_fan_mode(self, fan_mode: str) -> None: """Turn fan on/off.""" self._control_hvac(fan_mode=HA_TO_TADO_FAN_MODE_MAP[fan_mode]) @@ -365,7 +366,7 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): """Return a list of available preset modes.""" return SUPPORT_PRESET - def set_preset_mode(self, preset_mode): + def set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" self._tado.set_presence(preset_mode) @@ -406,7 +407,7 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): self._tado.set_temperature_offset(self._device_id, offset) - def set_temperature(self, **kwargs): + def set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: return @@ -428,7 +429,7 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): self._control_hvac(hvac_mode=HA_TO_TADO_HVAC_MODE_MAP[hvac_mode]) @property - def available(self): + def available(self) -> bool: """Return if the device is available.""" return self._tado_zone_data.available @@ -482,7 +483,7 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): ] = self._tado_zone_data.default_overlay_termination_duration return state_attr - def set_swing_mode(self, swing_mode): + def set_swing_mode(self, swing_mode: str) -> None: """Set swing modes for the device.""" self._control_hvac(swing_mode=swing_mode) diff --git a/homeassistant/components/tado/sensor.py b/homeassistant/components/tado/sensor.py index 40dcce397c6..22f4608f670 100644 --- a/homeassistant/components/tado/sensor.py +++ b/homeassistant/components/tado/sensor.py @@ -102,7 +102,7 @@ class TadoHomeSensor(TadoHomeEntity, SensorEntity): self._state_attributes = None self._tado_weather_data = self._tado.data["weather"] - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register for sensor updates.""" self.async_on_remove( @@ -211,7 +211,7 @@ class TadoZoneSensor(TadoZoneEntity, SensorEntity): self._state_attributes = None self._tado_zone_data = None - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register for sensor updates.""" self.async_on_remove( diff --git a/homeassistant/components/tado/water_heater.py b/homeassistant/components/tado/water_heater.py index a009fdc3b92..3bd3ae48026 100644 --- a/homeassistant/components/tado/water_heater.py +++ b/homeassistant/components/tado/water_heater.py @@ -1,5 +1,6 @@ """Support for Tado hot water zones.""" import logging +from typing import Any import voluptuous as vol @@ -152,7 +153,7 @@ class TadoWaterHeater(TadoZoneEntity, WaterHeaterEntity): self._overlay_mode = CONST_MODE_SMART_SCHEDULE self._tado_zone_data = None - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register for sensor updates.""" self.async_on_remove( @@ -211,7 +212,7 @@ class TadoWaterHeater(TadoZoneEntity, WaterHeaterEntity): """Return the maximum temperature.""" return self._max_temperature - def set_operation_mode(self, operation_mode): + def set_operation_mode(self, operation_mode: str) -> None: """Set new operation mode.""" mode = None @@ -233,7 +234,7 @@ class TadoWaterHeater(TadoZoneEntity, WaterHeaterEntity): hvac_mode=CONST_MODE_HEAT, target_temp=temperature, duration=time_period ) - def set_temperature(self, **kwargs): + def set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" temperature = kwargs.get(ATTR_TEMPERATURE) if not self._supports_temperature_control or temperature is None: diff --git a/homeassistant/components/tank_utility/sensor.py b/homeassistant/components/tank_utility/sensor.py index 1e10eee6841..f902abc22e0 100644 --- a/homeassistant/components/tank_utility/sensor.py +++ b/homeassistant/components/tank_utility/sensor.py @@ -133,7 +133,7 @@ class TankUtilitySensor(SensorEntity): data.update(data.pop("lastReading", {})) return data - def update(self): + def update(self) -> None: """Set the device state and attributes.""" data = self.get_data() self._state = round(data[SENSOR_TYPE], SENSOR_ROUNDING_PRECISION) diff --git a/homeassistant/components/tapsaff/binary_sensor.py b/homeassistant/components/tapsaff/binary_sensor.py index 9608971e78a..5ffa8690f94 100644 --- a/homeassistant/components/tapsaff/binary_sensor.py +++ b/homeassistant/components/tapsaff/binary_sensor.py @@ -61,7 +61,7 @@ class TapsAffSensor(BinarySensorEntity): """Return true if taps aff.""" return self.data.is_taps_aff - def update(self): + def update(self) -> None: """Get the latest data.""" self.data.update() diff --git a/homeassistant/components/ted5000/sensor.py b/homeassistant/components/ted5000/sensor.py index b62efc01f0a..e579ac6c359 100644 --- a/homeassistant/components/ted5000/sensor.py +++ b/homeassistant/components/ted5000/sensor.py @@ -98,7 +98,7 @@ class Ted5000Sensor(SensorEntity): with suppress(KeyError): return self._gateway.data[self._mtu][self._unit] - def update(self): + def update(self) -> None: """Get the latest data from REST API.""" self._gateway.update() diff --git a/homeassistant/components/tellduslive/switch.py b/homeassistant/components/tellduslive/switch.py index a1bb2ffc9e1..fbecda2e775 100644 --- a/homeassistant/components/tellduslive/switch.py +++ b/homeassistant/components/tellduslive/switch.py @@ -1,4 +1,6 @@ """Support for Tellstick switches using Tellstick Net.""" +from typing import Any + from homeassistant.components import switch from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry @@ -37,12 +39,12 @@ class TelldusLiveSwitch(TelldusLiveEntity, SwitchEntity): """Return true if switch is on.""" return self.device.is_on - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" self.device.turn_on() self._update_callback() - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" self.device.turn_off() self._update_callback() diff --git a/homeassistant/components/tellstick/sensor.py b/homeassistant/components/tellstick/sensor.py index e820c46da45..bfd433e1674 100644 --- a/homeassistant/components/tellstick/sensor.py +++ b/homeassistant/components/tellstick/sensor.py @@ -160,6 +160,6 @@ class TellstickSensor(SensorEntity): self._attr_native_unit_of_measurement = sensor_info.unit or None self._attr_name = f"{name} {sensor_info.name}" - def update(self): + def update(self) -> None: """Update tellstick sensor.""" self._attr_native_value = self._tellcore_sensor.value(self._datatype).value diff --git a/homeassistant/components/telnet/switch.py b/homeassistant/components/telnet/switch.py index 9ad295d9ac5..f919834139b 100644 --- a/homeassistant/components/telnet/switch.py +++ b/homeassistant/components/telnet/switch.py @@ -146,14 +146,14 @@ class TelnetSwitch(SwitchEntity): return None self._attr_is_on = rendered == "True" - def turn_on(self, **kwargs) -> None: + def turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" self._telnet_command(self._command_on) if self.assumed_state: self._attr_is_on = True self.schedule_update_ha_state() - def turn_off(self, **kwargs) -> None: + def turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" self._telnet_command(self._command_off) if self.assumed_state: diff --git a/homeassistant/components/temper/sensor.py b/homeassistant/components/temper/sensor.py index b8d3cc38f69..606227ee7f5 100644 --- a/homeassistant/components/temper/sensor.py +++ b/homeassistant/components/temper/sensor.py @@ -91,7 +91,7 @@ class TemperSensor(SensorEntity): # set calibration data self.temper_device.set_calibration_data(scale=self.scale, offset=self.offset) - def update(self): + def update(self) -> None: """Retrieve latest state.""" try: sensor_value = self.temper_device.get_temperature("celsius") diff --git a/homeassistant/components/tfiac/climate.py b/homeassistant/components/tfiac/climate.py index 58f16d1cc99..46163056948 100644 --- a/homeassistant/components/tfiac/climate.py +++ b/homeassistant/components/tfiac/climate.py @@ -4,6 +4,7 @@ from __future__ import annotations from concurrent import futures from datetime import timedelta import logging +from typing import Any from pytfiac import Tfiac import voluptuous as vol @@ -94,7 +95,7 @@ class TfiacClimate(ClimateEntity): """Return if the device is available.""" return self._available - async def async_update(self): + async def async_update(self) -> None: """Update status via socket polling.""" try: await self._client.update() @@ -167,7 +168,7 @@ class TfiacClimate(ClimateEntity): """List of available swing modes.""" return SUPPORT_SWING - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" if (temp := kwargs.get(ATTR_TEMPERATURE)) is not None: await self._client.set_state(TARGET_TEMP, temp) @@ -179,18 +180,18 @@ class TfiacClimate(ClimateEntity): else: await self._client.set_state(OPERATION_MODE, HVAC_MAP[hvac_mode]) - async def async_set_fan_mode(self, fan_mode): + async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new fan mode.""" await self._client.set_state(FAN_MODE, fan_mode.capitalize()) - async def async_set_swing_mode(self, swing_mode): + async def async_set_swing_mode(self, swing_mode: str) -> None: """Set new swing mode.""" await self._client.set_swing(swing_mode.capitalize()) - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Turn device on.""" await self._client.set_state(OPERATION_MODE) - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Turn device off.""" await self._client.set_state(ON_MODE, "off") diff --git a/homeassistant/components/thermoworks_smoke/sensor.py b/homeassistant/components/thermoworks_smoke/sensor.py index 72be9a05519..2297883ec6d 100644 --- a/homeassistant/components/thermoworks_smoke/sensor.py +++ b/homeassistant/components/thermoworks_smoke/sensor.py @@ -127,7 +127,7 @@ class ThermoworksSmokeSensor(SensorEntity): else: self._attr_native_unit_of_measurement = self.mgr.units(self.serial, PROBE_1) - def update(self): + def update(self) -> None: """Get the monitored data from firebase.""" try: diff --git a/homeassistant/components/thethingsnetwork/sensor.py b/homeassistant/components/thethingsnetwork/sensor.py index d6a029a3604..e14bd944d36 100644 --- a/homeassistant/components/thethingsnetwork/sensor.py +++ b/homeassistant/components/thethingsnetwork/sensor.py @@ -109,7 +109,7 @@ class TtnDataSensor(SensorEntity): ATTR_TIME: self._state["time"], } - async def async_update(self): + async def async_update(self) -> None: """Get the current state.""" await self._ttn_data_storage.async_update() self._state = self._ttn_data_storage.data diff --git a/homeassistant/components/thinkingcleaner/sensor.py b/homeassistant/components/thinkingcleaner/sensor.py index 3eadffa0a10..8b0d30d13ed 100644 --- a/homeassistant/components/thinkingcleaner/sensor.py +++ b/homeassistant/components/thinkingcleaner/sensor.py @@ -107,7 +107,7 @@ class ThinkingCleanerSensor(SensorEntity): self._attr_name = f"{tc_object.name} {description.name}" - def update(self): + def update(self) -> None: """Update the sensor.""" self._update_devices() diff --git a/homeassistant/components/thinkingcleaner/switch.py b/homeassistant/components/thinkingcleaner/switch.py index 31fb9709e57..66d2ab02e93 100644 --- a/homeassistant/components/thinkingcleaner/switch.py +++ b/homeassistant/components/thinkingcleaner/switch.py @@ -3,6 +3,7 @@ from __future__ import annotations from datetime import timedelta import time +from typing import Any from pythinkingcleaner import Discovery, ThinkingCleaner import voluptuous as vol @@ -130,7 +131,7 @@ class ThinkingCleanerSwitch(SwitchEntity): return False - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" sensor_type = self.entity_description.key if sensor_type == "clean": @@ -141,13 +142,13 @@ class ThinkingCleanerSwitch(SwitchEntity): elif sensor_type == "find": self._tc_object.find_me() - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" if self.entity_description.key == "clean": self.set_graceful_lock(False) self._tc_object.stop_cleaning() - def update(self): + def update(self) -> None: """Update the switch state (Only for clean).""" if self.entity_description.key == "clean" and not self.is_update_locked(): self._tc_object.update() diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index 9cfc4ecbb5a..ca0c253590f 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -350,7 +350,7 @@ class TibberSensorElPrice(TibberSensor): self._device_name = self._home_name - async def async_update(self): + async def async_update(self) -> None: """Get the latest data and updates the states.""" now = dt_util.now() if ( @@ -448,7 +448,7 @@ class TibberSensorRT(TibberSensor, CoordinatorEntity["TibberRtDataCoordinator"]) self._attr_native_unit_of_measurement = tibber_home.currency @property - def available(self): + def available(self) -> bool: """Return True if entity is available.""" return self._tibber_home.rt_subscription_running diff --git a/homeassistant/components/tmb/sensor.py b/homeassistant/components/tmb/sensor.py index dafcf909399..8d6db00b2b9 100644 --- a/homeassistant/components/tmb/sensor.py +++ b/homeassistant/components/tmb/sensor.py @@ -119,7 +119,7 @@ class TMBSensor(SensorEntity): } @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): + def update(self) -> None: """Get the next bus information.""" try: self._state = self._ibus_client.get_stop_forecast(self._stop, self._line) diff --git a/homeassistant/components/tod/binary_sensor.py b/homeassistant/components/tod/binary_sensor.py index d287431d712..1c909388b60 100644 --- a/homeassistant/components/tod/binary_sensor.py +++ b/homeassistant/components/tod/binary_sensor.py @@ -225,7 +225,7 @@ class TodSensor(BinarySensorEntity): # Offset is already there self._time_before += timedelta(days=1) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Call when entity about to be added to Home Assistant.""" self._calculate_boundary_time() self._calculate_next_update() diff --git a/homeassistant/components/todoist/calendar.py b/homeassistant/components/todoist/calendar.py index f1240f33b1b..30391f90d03 100644 --- a/homeassistant/components/todoist/calendar.py +++ b/homeassistant/components/todoist/calendar.py @@ -308,7 +308,7 @@ class TodoistProjectEntity(CalendarEntity): """Return the name of the entity.""" return self._name - def update(self): + def update(self) -> None: """Update all Todoist Calendars.""" self.data.update() # Set Todoist-specific data that can't easily be grabbed diff --git a/homeassistant/components/toon/climate.py b/homeassistant/components/toon/climate.py index 07e0aed66ac..2d7cf4c04f6 100644 --- a/homeassistant/components/toon/climate.py +++ b/homeassistant/components/toon/climate.py @@ -103,7 +103,7 @@ class ToonThermostatDevice(ToonDisplayDeviceEntity, ClimateEntity): return {"heating_type": self.coordinator.data.agreement.heating_type} @toon_exception_handler - async def async_set_temperature(self, **kwargs) -> None: + async def async_set_temperature(self, **kwargs: Any) -> None: """Change the setpoint of the thermostat.""" temperature = kwargs.get(ATTR_TEMPERATURE) await self.coordinator.toon.set_current_setpoint(temperature) diff --git a/homeassistant/components/totalconnect/binary_sensor.py b/homeassistant/components/totalconnect/binary_sensor.py index e86b54272a0..32e0b3573f5 100644 --- a/homeassistant/components/totalconnect/binary_sensor.py +++ b/homeassistant/components/totalconnect/binary_sensor.py @@ -49,7 +49,7 @@ class TotalConnectBinarySensor(BinarySensorEntity): """Return the name of the device.""" return self._name - def update(self): + def update(self) -> None: """Return the state of the device.""" self._is_tampered = self._zone.is_tampered() self._is_low_battery = self._zone.is_low_battery() diff --git a/homeassistant/components/touchline/climate.py b/homeassistant/components/touchline/climate.py index 78fd00b734c..bd7bdce4efe 100644 --- a/homeassistant/components/touchline/climate.py +++ b/homeassistant/components/touchline/climate.py @@ -1,7 +1,7 @@ """Platform for Roth Touchline floor heating controller.""" from __future__ import annotations -from typing import NamedTuple +from typing import Any, NamedTuple from pytouchline import PyTouchline import voluptuous as vol @@ -75,7 +75,7 @@ class Touchline(ClimateEntity): self._current_operation_mode = None self._preset_mode = None - def update(self): + def update(self) -> None: """Update thermostat attributes.""" self.unit.update() self._name = self.unit.get_name() @@ -120,7 +120,7 @@ class Touchline(ClimateEntity): """Set new target hvac mode.""" self._current_operation_mode = HVACMode.HEAT - def set_temperature(self, **kwargs): + def set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" if kwargs.get(ATTR_TEMPERATURE) is not None: self._target_temperature = kwargs.get(ATTR_TEMPERATURE) diff --git a/homeassistant/components/traccar/device_tracker.py b/homeassistant/components/traccar/device_tracker.py index 784e3c70f33..b668d2fff33 100644 --- a/homeassistant/components/traccar/device_tracker.py +++ b/homeassistant/components/traccar/device_tracker.py @@ -411,7 +411,7 @@ class TraccarEntity(TrackerEntity, RestoreEntity): """Return the source type, eg gps or router, of the device.""" return SourceType.GPS - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register state update callback.""" await super().async_added_to_hass() self._unsub_dispatcher = async_dispatcher_connect( @@ -445,7 +445,7 @@ class TraccarEntity(TrackerEntity, RestoreEntity): } self._battery = attr.get(ATTR_BATTERY) - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Clean up after entity before removal.""" await super().async_will_remove_from_hass() self._unsub_dispatcher() diff --git a/homeassistant/components/transmission/sensor.py b/homeassistant/components/transmission/sensor.py index c3f160cb040..0472ce221fb 100644 --- a/homeassistant/components/transmission/sensor.py +++ b/homeassistant/components/transmission/sensor.py @@ -75,11 +75,11 @@ class TransmissionSensor(SensorEntity): return self._state @property - def available(self): + def available(self) -> bool: """Could the device be accessed during the last update call.""" return self._tm_client.api.available - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" @callback @@ -102,7 +102,7 @@ class TransmissionSpeedSensor(TransmissionSensor): """Return the unit of measurement of this entity, if any.""" return DATA_RATE_MEGABYTES_PER_SECOND - def update(self): + def update(self) -> None: """Get the latest data from Transmission and updates the state.""" if data := self._tm_client.api.data: mb_spd = ( @@ -117,7 +117,7 @@ class TransmissionSpeedSensor(TransmissionSensor): class TransmissionStatusSensor(TransmissionSensor): """Representation of a Transmission status sensor.""" - def update(self): + def update(self) -> None: """Get the latest data from Transmission and updates the state.""" if data := self._tm_client.api.data: upload = data.uploadSpeed @@ -163,7 +163,7 @@ class TransmissionTorrentsSensor(TransmissionSensor): STATE_ATTR_TORRENT_INFO: info, } - def update(self): + def update(self) -> None: """Get the latest data from Transmission and updates the state.""" torrents = _filter_torrents( self._tm_client.api.torrents, statuses=self.SUBTYPE_MODES[self._sub_type] diff --git a/homeassistant/components/transmission/switch.py b/homeassistant/components/transmission/switch.py index 63477eecf48..0fd9ffee51e 100644 --- a/homeassistant/components/transmission/switch.py +++ b/homeassistant/components/transmission/switch.py @@ -1,5 +1,6 @@ """Support for setting the Transmission BitTorrent client Turtle Mode.""" import logging +from typing import Any from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry @@ -61,11 +62,11 @@ class TransmissionSwitch(SwitchEntity): return self._state == STATE_ON @property - def available(self): + def available(self) -> bool: """Could the device be accessed during the last update call.""" return self._tm_client.api.available - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" if self.type == "on_off": _LOGGING.debug("Starting all torrents") @@ -75,7 +76,7 @@ class TransmissionSwitch(SwitchEntity): self._tm_client.api.set_alt_speed_enabled(True) self._tm_client.api.update() - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" if self.type == "on_off": _LOGGING.debug("Stopping all torrents") @@ -85,7 +86,7 @@ class TransmissionSwitch(SwitchEntity): self._tm_client.api.set_alt_speed_enabled(False) self._tm_client.api.update() - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" self.unsub_update = async_dispatcher_connect( self.hass, @@ -103,7 +104,7 @@ class TransmissionSwitch(SwitchEntity): self.unsub_update() self.unsub_update = None - def update(self): + def update(self) -> None: """Get the latest data from Transmission and updates the state.""" active = None if self.type == "on_off": diff --git a/homeassistant/components/transport_nsw/sensor.py b/homeassistant/components/transport_nsw/sensor.py index 54d555bacb7..27fd6fd67fd 100644 --- a/homeassistant/components/transport_nsw/sensor.py +++ b/homeassistant/components/transport_nsw/sensor.py @@ -120,7 +120,7 @@ class TransportNSWSensor(SensorEntity): """Icon to use in the frontend, if any.""" return self._icon - def update(self): + def update(self) -> None: """Get the latest data from Transport NSW and update the states.""" self.data.update() self._times = self.data.info diff --git a/homeassistant/components/travisci/sensor.py b/homeassistant/components/travisci/sensor.py index 7b15f5c0dc8..c35391d4573 100644 --- a/homeassistant/components/travisci/sensor.py +++ b/homeassistant/components/travisci/sensor.py @@ -174,7 +174,7 @@ class TravisCISensor(SensorEntity): return attrs - def update(self): + def update(self) -> None: """Get the latest data and updates the states.""" _LOGGER.debug("Updating sensor %s", self.name) diff --git a/homeassistant/components/trend/binary_sensor.py b/homeassistant/components/trend/binary_sensor.py index e3bd4816f44..b98d904cabd 100644 --- a/homeassistant/components/trend/binary_sensor.py +++ b/homeassistant/components/trend/binary_sensor.py @@ -169,7 +169,7 @@ class SensorTrend(BinarySensorEntity): ATTR_SAMPLE_DURATION: self._sample_duration, } - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Complete device setup after being added to hass.""" @callback @@ -195,7 +195,7 @@ class SensorTrend(BinarySensorEntity): ) ) - async def async_update(self): + async def async_update(self) -> None: """Get the latest data and update the states.""" # Remove outdated samples if self._sample_duration > 0: From 3ec231c911c66b58bc6e42e8a24ac44ad6b2c419 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 6 Sep 2022 13:36:44 +0200 Subject: [PATCH 141/955] Improve entity type hints [s] (part 2/2) (#77882) --- .../components/solaredge_local/sensor.py | 2 +- homeassistant/components/soma/sensor.py | 2 +- .../components/songpal/media_player.py | 20 ++++----- .../components/sonos/media_player.py | 3 +- .../components/sony_projector/switch.py | 7 +-- .../components/soundtouch/config_flow.py | 6 ++- .../components/soundtouch/media_player.py | 40 ++++++++++------- homeassistant/components/spider/climate.py | 10 +++-- homeassistant/components/spider/switch.py | 10 +++-- .../components/spotify/media_player.py | 7 +-- .../components/squeezebox/media_player.py | 45 ++++++++++--------- homeassistant/components/srp_energy/sensor.py | 4 +- homeassistant/components/starline/switch.py | 7 +-- .../components/starlingbank/sensor.py | 2 +- homeassistant/components/startca/sensor.py | 2 +- .../components/stiebel_eltron/climate.py | 7 +-- .../streamlabswater/binary_sensor.py | 2 +- .../components/streamlabswater/sensor.py | 2 +- homeassistant/components/subaru/sensor.py | 2 +- homeassistant/components/suez_water/sensor.py | 2 +- .../components/supervisord/sensor.py | 2 +- homeassistant/components/supla/switch.py | 5 ++- .../swiss_hydrological_data/sensor.py | 2 +- .../swiss_public_transport/sensor.py | 2 +- homeassistant/components/switchmate/switch.py | 5 ++- homeassistant/components/syncthing/sensor.py | 2 +- 26 files changed, 112 insertions(+), 88 deletions(-) diff --git a/homeassistant/components/solaredge_local/sensor.py b/homeassistant/components/solaredge_local/sensor.py index d07a95683eb..361ab8e5ce7 100644 --- a/homeassistant/components/solaredge_local/sensor.py +++ b/homeassistant/components/solaredge_local/sensor.py @@ -280,7 +280,7 @@ class SolarEdgeSensor(SensorEntity): pass return None - def update(self): + def update(self) -> None: """Get the latest data from the sensor and update the state.""" self._data.update() self._attr_native_value = self._data.data[self.entity_description.key] diff --git a/homeassistant/components/soma/sensor.py b/homeassistant/components/soma/sensor.py index 0494e5577b8..a53bcd26e83 100644 --- a/homeassistant/components/soma/sensor.py +++ b/homeassistant/components/soma/sensor.py @@ -45,7 +45,7 @@ class SomaSensor(SomaEntity, SensorEntity): return self.battery_state @Throttle(MIN_TIME_BETWEEN_UPDATES) - async def async_update(self): + async def async_update(self) -> None: """Update the sensor with the latest data.""" response = await self.get_battery_level_from_api() diff --git a/homeassistant/components/songpal/media_player.py b/homeassistant/components/songpal/media_player.py index 973c57adbbb..4414d590fc1 100644 --- a/homeassistant/components/songpal/media_player.py +++ b/homeassistant/components/songpal/media_player.py @@ -119,11 +119,11 @@ class SongpalEntity(MediaPlayerEntity): self._active_source = None self._sources = {} - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Run when entity is added to hass.""" await self.async_activate_websocket() - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Run when entity will be removed from hass.""" await self._dev.stop_listen_notifications() @@ -232,7 +232,7 @@ class SongpalEntity(MediaPlayerEntity): _LOGGER.debug("Calling set_sound_setting with %s: %s", name, value) await self._dev.set_sound_settings(name, value) - async def async_update(self): + async def async_update(self) -> None: """Fetch updates from the device.""" try: if self._sysinfo is None: @@ -281,7 +281,7 @@ class SongpalEntity(MediaPlayerEntity): _LOGGER.error("Unable to update: %s", ex) self._available = False - async def async_select_source(self, source): + async def async_select_source(self, source: str) -> None: """Select source.""" for out in self._sources.values(): if out.title == source: @@ -314,29 +314,29 @@ class SongpalEntity(MediaPlayerEntity): volume = self._volume / self._volume_max return volume - async def async_set_volume_level(self, volume): + async def async_set_volume_level(self, volume: float) -> None: """Set volume level.""" volume = int(volume * self._volume_max) _LOGGER.debug("Setting volume to %s", volume) return await self._volume_control.set_volume(volume) - async def async_volume_up(self): + async def async_volume_up(self) -> None: """Set volume up.""" return await self._volume_control.set_volume(self._volume + 1) - async def async_volume_down(self): + async def async_volume_down(self) -> None: """Set volume down.""" return await self._volume_control.set_volume(self._volume - 1) - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Turn the device on.""" return await self._dev.set_power(True) - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Turn the device off.""" return await self._dev.set_power(False) - async def async_mute_volume(self, mute): + async def async_mute_volume(self, mute: bool) -> None: """Mute or unmute the device.""" _LOGGER.debug("Set mute: %s", mute) return await self._volume_control.set_mute(mute) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 14e0693f55a..658fcf01ec0 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -23,6 +23,7 @@ from homeassistant.components.media_player import ( MediaPlayerEntityFeature, async_process_play_media_url, ) +from homeassistant.components.media_player.browse_media import BrowseMedia from homeassistant.components.media_player.const import ( ATTR_INPUT_SOURCE, ATTR_MEDIA_ENQUEUE, @@ -715,7 +716,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): async def async_browse_media( self, media_content_type: str | None = None, media_content_id: str | None = None - ) -> Any: + ) -> BrowseMedia: """Implement the websocket media browsing helper.""" return await media_browser.async_browse_media( self.hass, diff --git a/homeassistant/components/sony_projector/switch.py b/homeassistant/components/sony_projector/switch.py index a78af7f856c..259ab8b154c 100644 --- a/homeassistant/components/sony_projector/switch.py +++ b/homeassistant/components/sony_projector/switch.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any import pysdcp import voluptuous as vol @@ -78,7 +79,7 @@ class SonyProjector(SwitchEntity): """Return state attributes.""" return self._attributes - def update(self): + def update(self) -> None: """Get the latest state from the projector.""" try: self._state = self._sdcp.get_power() @@ -87,7 +88,7 @@ class SonyProjector(SwitchEntity): _LOGGER.error("Projector connection refused") self._available = False - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the projector on.""" _LOGGER.debug("Powering on projector '%s'", self.name) if self._sdcp.set_power(True): @@ -96,7 +97,7 @@ class SonyProjector(SwitchEntity): else: _LOGGER.error("Power on command was not successful") - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the projector off.""" _LOGGER.debug("Powering off projector '%s'", self.name) if self._sdcp.set_power(False): diff --git a/homeassistant/components/soundtouch/config_flow.py b/homeassistant/components/soundtouch/config_flow.py index 47b10912436..489dfff6feb 100644 --- a/homeassistant/components/soundtouch/config_flow.py +++ b/homeassistant/components/soundtouch/config_flow.py @@ -6,7 +6,9 @@ from requests import RequestException import voluptuous as vol from homeassistant import config_entries +from homeassistant.components.zeroconf import ZeroconfServiceInfo from homeassistant.const import CONF_HOST +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_validation as cv from .const import DOMAIN @@ -60,7 +62,9 @@ class SoundtouchConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_zeroconf(self, discovery_info): + async def async_step_zeroconf( + self, discovery_info: ZeroconfServiceInfo + ) -> FlowResult: """Handle a flow initiated by a zeroconf discovery.""" self.host = discovery_info.host diff --git a/homeassistant/components/soundtouch/media_player.py b/homeassistant/components/soundtouch/media_player.py index 6dff35df7bc..d1cf6a9fa94 100644 --- a/homeassistant/components/soundtouch/media_player.py +++ b/homeassistant/components/soundtouch/media_player.py @@ -4,6 +4,7 @@ from __future__ import annotations from functools import partial import logging import re +from typing import Any from libsoundtouch.device import SoundTouchDevice from libsoundtouch.utils import Source @@ -17,6 +18,7 @@ from homeassistant.components.media_player import ( MediaPlayerEntityFeature, ) from homeassistant.components.media_player.browse_media import ( + BrowseMedia, async_process_play_media_url, ) from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry @@ -154,7 +156,7 @@ class SoundTouchMediaPlayer(MediaPlayerEntity): """Return SoundTouch device.""" return self._device - def update(self): + def update(self) -> None: """Retrieve the latest data.""" self._status = self._device.status() self._volume = self._device.volume() @@ -194,47 +196,47 @@ class SoundTouchMediaPlayer(MediaPlayerEntity): """Boolean if volume is currently muted.""" return self._volume.muted - def turn_off(self): + def turn_off(self) -> None: """Turn off media player.""" self._device.power_off() - def turn_on(self): + def turn_on(self) -> None: """Turn on media player.""" self._device.power_on() - def volume_up(self): + def volume_up(self) -> None: """Volume up the media player.""" self._device.volume_up() - def volume_down(self): + def volume_down(self) -> None: """Volume down media player.""" self._device.volume_down() - def set_volume_level(self, volume): + def set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" self._device.set_volume(int(volume * 100)) - def mute_volume(self, mute): + def mute_volume(self, mute: bool) -> None: """Send mute command.""" self._device.mute() - def media_play_pause(self): + def media_play_pause(self) -> None: """Simulate play pause media player.""" self._device.play_pause() - def media_play(self): + def media_play(self) -> None: """Send play command.""" self._device.play() - def media_pause(self): + def media_pause(self) -> None: """Send media pause command to media player.""" self._device.pause() - def media_next_track(self): + def media_next_track(self) -> None: """Send next track command.""" self._device.next_track() - def media_previous_track(self): + def media_previous_track(self) -> None: """Send the previous track command.""" self._device.previous_track() @@ -273,7 +275,7 @@ class SoundTouchMediaPlayer(MediaPlayerEntity): """Album name of current playing media.""" return self._status.album - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Populate zone info which requires entity_id.""" @callback @@ -285,7 +287,9 @@ class SoundTouchMediaPlayer(MediaPlayerEntity): EVENT_HOMEASSISTANT_START, async_update_on_start ) - async def async_play_media(self, media_type, media_id, **kwargs): + async def async_play_media( + self, media_type: str, media_id: str, **kwargs: Any + ) -> None: """Play a piece of media.""" if media_source.is_media_source_id(media_id): play_item = await media_source.async_resolve_media( @@ -297,7 +301,7 @@ class SoundTouchMediaPlayer(MediaPlayerEntity): partial(self.play_media, media_type, media_id, **kwargs) ) - def play_media(self, media_type, media_id, **kwargs): + def play_media(self, media_type: str, media_id: str, **kwargs: Any) -> None: """Play a piece of media.""" _LOGGER.debug("Starting media with media_id: %s", media_id) if re.match(r"http?://", str(media_id)): @@ -319,7 +323,7 @@ class SoundTouchMediaPlayer(MediaPlayerEntity): else: _LOGGER.warning("Unable to find preset with id %s", media_id) - def select_source(self, source): + def select_source(self, source: str) -> None: """Select input source.""" if source == Source.AUX.value: _LOGGER.debug("Selecting source AUX") @@ -399,7 +403,9 @@ class SoundTouchMediaPlayer(MediaPlayerEntity): return attributes - async def async_browse_media(self, media_content_type=None, media_content_id=None): + async def async_browse_media( + self, media_content_type: str | None = None, media_content_id: str | None = None + ) -> BrowseMedia: """Implement the websocket media browsing helper.""" return await media_source.async_browse_media(self.hass, media_content_id) diff --git a/homeassistant/components/spider/climate.py b/homeassistant/components/spider/climate.py index fa04cbbe058..c372d64e095 100644 --- a/homeassistant/components/spider/climate.py +++ b/homeassistant/components/spider/climate.py @@ -1,4 +1,6 @@ """Support for Spider thermostats.""" +from typing import Any + from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ClimateEntityFeature, HVACMode from homeassistant.config_entries import ConfigEntry @@ -59,7 +61,7 @@ class SpiderThermostat(ClimateEntity): ) @property - def supported_features(self): + def supported_features(self) -> int: """Return the list of supported features.""" if self.thermostat.has_fan_mode: return ( @@ -112,7 +114,7 @@ class SpiderThermostat(ClimateEntity): """Return the list of available operation modes.""" return self.support_hvac - def set_temperature(self, **kwargs): + def set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: return @@ -128,7 +130,7 @@ class SpiderThermostat(ClimateEntity): """Return the fan setting.""" return self.thermostat.current_fan_speed - def set_fan_mode(self, fan_mode): + def set_fan_mode(self, fan_mode: str) -> None: """Set fan mode.""" self.thermostat.set_fan_speed(fan_mode) @@ -137,6 +139,6 @@ class SpiderThermostat(ClimateEntity): """List of available fan modes.""" return self.support_fan - def update(self): + def update(self) -> None: """Get the latest data.""" self.thermostat = self.api.get_thermostat(self.unique_id) diff --git a/homeassistant/components/spider/switch.py b/homeassistant/components/spider/switch.py index bbc37340db8..607e4c5b84a 100644 --- a/homeassistant/components/spider/switch.py +++ b/homeassistant/components/spider/switch.py @@ -1,4 +1,6 @@ """Support for Spider switches.""" +from typing import Any + from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -56,18 +58,18 @@ class SpiderPowerPlug(SwitchEntity): return self.power_plug.is_on @property - def available(self): + def available(self) -> bool: """Return true if switch is available.""" return self.power_plug.is_available - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn device on.""" self.power_plug.turn_on() - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn device off.""" self.power_plug.turn_off() - def update(self): + def update(self) -> None: """Get the latest data.""" self.power_plug = self.api.get_power_plug(self.power_plug.id) diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index 3db8ae7de08..263cf322cf1 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -5,6 +5,7 @@ from asyncio import run_coroutine_threadsafe import datetime as dt from datetime import timedelta import logging +from typing import Any import requests from spotipy import SpotifyException @@ -273,7 +274,7 @@ class SpotifyMediaPlayer(MediaPlayerEntity): return REPEAT_MODE_MAPPING_TO_HA.get(repeat_state) @spotify_exception_handler - def set_volume_level(self, volume: int) -> None: + def set_volume_level(self, volume: float) -> None: """Set the volume level.""" self.data.client.volume(int(volume * 100)) @@ -298,12 +299,12 @@ class SpotifyMediaPlayer(MediaPlayerEntity): self.data.client.next_track() @spotify_exception_handler - def media_seek(self, position): + def media_seek(self, position: float) -> None: """Send seek command.""" self.data.client.seek_track(int(position * 1000)) @spotify_exception_handler - def play_media(self, media_type: str, media_id: str, **kwargs) -> None: + def play_media(self, media_type: str, media_id: str, **kwargs: Any) -> None: """Play media.""" if media_type.startswith(MEDIA_PLAYER_PREFIX): media_type = media_type[len(MEDIA_PLAYER_PREFIX) :] diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index aae0ce638e7..bc6f7298a4f 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio import json import logging +from typing import Any from pysqueezebox import Server, async_discover import voluptuous as vol @@ -296,7 +297,7 @@ class SqueezeBoxEntity(MediaPlayerEntity): return SQUEEZEBOX_MODE.get(self._player.mode) return None - async def async_update(self): + async def async_update(self) -> None: """Update the Player() object.""" # only update available players, newly available players will be rediscovered and marked available if self._available: @@ -313,7 +314,7 @@ class SqueezeBoxEntity(MediaPlayerEntity): self.hass, SIGNAL_PLAYER_REDISCOVERED, self.rediscovered ) - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Remove from list of known players when removed from hass.""" self.hass.data[DOMAIN][KNOWN_PLAYERS].remove(self) @@ -419,60 +420,62 @@ class SqueezeBoxEntity(MediaPlayerEntity): """Return the result from the call_query service.""" return self._query_result - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Turn off media player.""" await self._player.async_set_power(False) - async def async_volume_up(self): + async def async_volume_up(self) -> None: """Volume up media player.""" await self._player.async_set_volume("+5") - async def async_volume_down(self): + async def async_volume_down(self) -> None: """Volume down media player.""" await self._player.async_set_volume("-5") - async def async_set_volume_level(self, volume): + async def async_set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" volume_percent = str(int(volume * 100)) await self._player.async_set_volume(volume_percent) - async def async_mute_volume(self, mute): + async def async_mute_volume(self, mute: bool) -> None: """Mute (true) or unmute (false) media player.""" await self._player.async_set_muting(mute) - async def async_media_stop(self): + async def async_media_stop(self) -> None: """Send stop command to media player.""" await self._player.async_stop() - async def async_media_play_pause(self): + async def async_media_play_pause(self) -> None: """Send pause command to media player.""" await self._player.async_toggle_pause() - async def async_media_play(self): + async def async_media_play(self) -> None: """Send play command to media player.""" await self._player.async_play() - async def async_media_pause(self): + async def async_media_pause(self) -> None: """Send pause command to media player.""" await self._player.async_pause() - async def async_media_next_track(self): + async def async_media_next_track(self) -> None: """Send next track command.""" await self._player.async_index("+1") - async def async_media_previous_track(self): + async def async_media_previous_track(self) -> None: """Send next track command.""" await self._player.async_index("-1") - async def async_media_seek(self, position): + async def async_media_seek(self, position: float) -> None: """Send seek command.""" await self._player.async_time(position) - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Turn the media player on.""" await self._player.async_set_power(True) - async def async_play_media(self, media_type, media_id, **kwargs): + async def async_play_media( + self, media_type: str, media_id: str, **kwargs: Any + ) -> None: """Send the play_media command to the media player.""" index = None @@ -528,7 +531,7 @@ class SqueezeBoxEntity(MediaPlayerEntity): if index is not None: await self._player.async_index(index) - async def async_set_repeat(self, repeat): + async def async_set_repeat(self, repeat: str) -> None: """Set the repeat mode.""" if repeat == REPEAT_MODE_ALL: repeat_mode = "playlist" @@ -539,12 +542,12 @@ class SqueezeBoxEntity(MediaPlayerEntity): await self._player.async_set_repeat(repeat_mode) - async def async_set_shuffle(self, shuffle): + async def async_set_shuffle(self, shuffle: bool) -> None: """Enable/disable shuffle mode.""" shuffle_mode = "song" if shuffle else "none" await self._player.async_set_shuffle(shuffle_mode) - async def async_clear_playlist(self): + async def async_clear_playlist(self) -> None: """Send the media player the command for clear playlist.""" await self._player.async_clear_playlist() @@ -575,7 +578,7 @@ class SqueezeBoxEntity(MediaPlayerEntity): self._query_result = await self._player.async_query(*all_params) _LOGGER.debug("call_query got result %s", self._query_result) - async def async_join_players(self, group_members): + async def async_join_players(self, group_members: list[str]) -> None: """ Add other Squeezebox players to this player's sync group. @@ -601,7 +604,7 @@ class SqueezeBoxEntity(MediaPlayerEntity): ) await self.async_join_players([other_player]) - async def async_unjoin_player(self): + async def async_unjoin_player(self) -> None: """Unsync this Squeezebox player.""" await self._player.async_unsync() diff --git a/homeassistant/components/srp_energy/sensor.py b/homeassistant/components/srp_energy/sensor.py index f1a97af9820..0e6c7cda577 100644 --- a/homeassistant/components/srp_energy/sensor.py +++ b/homeassistant/components/srp_energy/sensor.py @@ -153,7 +153,7 @@ class SrpEntity(SensorEntity): """Return the state class.""" return SensorStateClass.TOTAL_INCREASING - 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.async_write_ha_state) @@ -161,7 +161,7 @@ class SrpEntity(SensorEntity): if self.coordinator.data: self._state = self.coordinator.data - async def async_update(self): + async def async_update(self) -> None: """Update the entity. Only used by the generic entity update service. diff --git a/homeassistant/components/starline/switch.py b/homeassistant/components/starline/switch.py index e4b65455ecf..412c08b9ff7 100644 --- a/homeassistant/components/starline/switch.py +++ b/homeassistant/components/starline/switch.py @@ -2,6 +2,7 @@ from __future__ import annotations from dataclasses import dataclass +from typing import Any from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.config_entries import ConfigEntry @@ -88,7 +89,7 @@ class StarlineSwitch(StarlineEntity, SwitchEntity): self.entity_description = description @property - def available(self): + def available(self) -> bool: """Return True if entity is available.""" return super().available and self._device.online @@ -120,11 +121,11 @@ class StarlineSwitch(StarlineEntity, SwitchEntity): return False return self._device.car_state.get(self._key) - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" self._account.api.set_car_state(self._device.device_id, self._key, True) - def turn_off(self, **kwargs) -> None: + def turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" if self._key == "poke": return diff --git a/homeassistant/components/starlingbank/sensor.py b/homeassistant/components/starlingbank/sensor.py index 0069fe7a65f..350c420d5d6 100644 --- a/homeassistant/components/starlingbank/sensor.py +++ b/homeassistant/components/starlingbank/sensor.py @@ -105,7 +105,7 @@ class StarlingBalanceSensor(SensorEntity): """Return the entity icon.""" return ICON - def update(self): + def update(self) -> None: """Fetch new state data for the sensor.""" self._starling_account.update_balance_data() if self._balance_data_type == "cleared_balance": diff --git a/homeassistant/components/startca/sensor.py b/homeassistant/components/startca/sensor.py index 042fc33060a..28016a28d79 100644 --- a/homeassistant/components/startca/sensor.py +++ b/homeassistant/components/startca/sensor.py @@ -163,7 +163,7 @@ class StartcaSensor(SensorEntity): self._attr_name = f"{name} {description.name}" - async def async_update(self): + async def async_update(self) -> None: """Get the latest data from Start.ca and update the state.""" await self.startcadata.async_update() sensor_type = self.entity_description.key diff --git a/homeassistant/components/stiebel_eltron/climate.py b/homeassistant/components/stiebel_eltron/climate.py index 4d63820899e..3ce29d1f51a 100644 --- a/homeassistant/components/stiebel_eltron/climate.py +++ b/homeassistant/components/stiebel_eltron/climate.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( @@ -87,7 +88,7 @@ class StiebelEltron(ClimateEntity): self._force_update = False self._ste_data = ste_data - def update(self): + def update(self) -> None: """Update unit attributes.""" self._ste_data.update(no_throttle=self._force_update) self._force_update = False @@ -168,7 +169,7 @@ class StiebelEltron(ClimateEntity): self._ste_data.api.set_operation(new_mode) self._force_update = True - def set_temperature(self, **kwargs): + def set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" target_temperature = kwargs.get(ATTR_TEMPERATURE) if target_temperature is not None: @@ -176,7 +177,7 @@ class StiebelEltron(ClimateEntity): self._ste_data.api.set_target_temp(target_temperature) self._force_update = True - def set_preset_mode(self, preset_mode: str): + def set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" new_mode = HA_TO_STE_PRESET.get(preset_mode) _LOGGER.debug("set_hvac_mode: %s -> %s", self._operation, new_mode) diff --git a/homeassistant/components/streamlabswater/binary_sensor.py b/homeassistant/components/streamlabswater/binary_sensor.py index 35e788dee65..43465fb99ae 100644 --- a/homeassistant/components/streamlabswater/binary_sensor.py +++ b/homeassistant/components/streamlabswater/binary_sensor.py @@ -75,6 +75,6 @@ class StreamlabsAwayMode(BinarySensorEntity): """Return if away mode is on.""" return self._streamlabs_location_data.is_away() - def update(self): + def update(self) -> None: """Retrieve the latest location data and away mode state.""" self._streamlabs_location_data.update() diff --git a/homeassistant/components/streamlabswater/sensor.py b/homeassistant/components/streamlabswater/sensor.py index 6933fa788cb..afef8070fcb 100644 --- a/homeassistant/components/streamlabswater/sensor.py +++ b/homeassistant/components/streamlabswater/sensor.py @@ -106,7 +106,7 @@ class StreamLabsDailyUsage(SensorEntity): """Return gallons as the unit measurement for water.""" return VOLUME_GALLONS - def update(self): + def update(self) -> None: """Retrieve the latest daily usage.""" self._streamlabs_usage_data.update() diff --git a/homeassistant/components/subaru/sensor.py b/homeassistant/components/subaru/sensor.py index 3f2b98f8546..ba96902f6fb 100644 --- a/homeassistant/components/subaru/sensor.py +++ b/homeassistant/components/subaru/sensor.py @@ -265,7 +265,7 @@ class SubaruSensor(SubaruEntity, SensorEntity): return self.api_unit @property - def available(self): + def available(self) -> bool: """Return if entity is available.""" last_update_success = super().available if last_update_success and self.vin not in self.coordinator.data: diff --git a/homeassistant/components/suez_water/sensor.py b/homeassistant/components/suez_water/sensor.py index 447facffcf6..ac691829236 100644 --- a/homeassistant/components/suez_water/sensor.py +++ b/homeassistant/components/suez_water/sensor.py @@ -118,7 +118,7 @@ class SuezSensor(SensorEntity): self._available = False _LOGGER.warning("Unable to fetch data") - def update(self): + def update(self) -> None: """Return the latest collected data from Linky.""" self._fetch_data() _LOGGER.debug("Suez data state is: %s", self._state) diff --git a/homeassistant/components/supervisord/sensor.py b/homeassistant/components/supervisord/sensor.py index 2f443f0d25f..ce7efb11a3a 100644 --- a/homeassistant/components/supervisord/sensor.py +++ b/homeassistant/components/supervisord/sensor.py @@ -79,7 +79,7 @@ class SupervisorProcessSensor(SensorEntity): ATTR_GROUP: self._info.get("group"), } - def update(self): + def update(self) -> None: """Update device state.""" try: self._info = self._server.supervisor.getProcessInfo( diff --git a/homeassistant/components/supla/switch.py b/homeassistant/components/supla/switch.py index 02ccf035201..9c4c53c1e9f 100644 --- a/homeassistant/components/supla/switch.py +++ b/homeassistant/components/supla/switch.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging from pprint import pformat +from typing import Any from homeassistant.components.switch import SwitchEntity from homeassistant.core import HomeAssistant @@ -44,11 +45,11 @@ async def async_setup_platform( class SuplaSwitch(SuplaChannel, SwitchEntity): """Representation of a Supla Switch.""" - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the switch.""" await self.async_action("TURN_ON") - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the switch.""" await self.async_action("TURN_OFF") diff --git a/homeassistant/components/swiss_hydrological_data/sensor.py b/homeassistant/components/swiss_hydrological_data/sensor.py index a4bbf5992a2..7c14428c2fc 100644 --- a/homeassistant/components/swiss_hydrological_data/sensor.py +++ b/homeassistant/components/swiss_hydrological_data/sensor.py @@ -145,7 +145,7 @@ class SwissHydrologicalDataSensor(SensorEntity): """Icon to use in the frontend.""" return self._icon - def update(self): + def update(self) -> None: """Get the latest data and update the state.""" self.hydro_data.update() self._data = self.hydro_data.data diff --git a/homeassistant/components/swiss_public_transport/sensor.py b/homeassistant/components/swiss_public_transport/sensor.py index 5917f879445..e23b8cd3aeb 100644 --- a/homeassistant/components/swiss_public_transport/sensor.py +++ b/homeassistant/components/swiss_public_transport/sensor.py @@ -131,7 +131,7 @@ class SwissPublicTransportSensor(SensorEntity): """Icon to use in the frontend, if any.""" return ICON - async def async_update(self): + async def async_update(self) -> None: """Get the latest data from opendata.ch and update the states.""" try: diff --git a/homeassistant/components/switchmate/switch.py b/homeassistant/components/switchmate/switch.py index ecc5b9003b5..f88da064af6 100644 --- a/homeassistant/components/switchmate/switch.py +++ b/homeassistant/components/switchmate/switch.py @@ -2,6 +2,7 @@ from __future__ import annotations from datetime import timedelta +from typing import Any from switchmate import Switchmate import voluptuous as vol @@ -74,10 +75,10 @@ class SwitchmateEntity(SwitchEntity): """Return true if it is on.""" return self._device.state - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" await self._device.turn_on() - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" await self._device.turn_off() diff --git a/homeassistant/components/syncthing/sensor.py b/homeassistant/components/syncthing/sensor.py index f6e090075a9..33fcb93182e 100644 --- a/homeassistant/components/syncthing/sensor.py +++ b/homeassistant/components/syncthing/sensor.py @@ -173,7 +173,7 @@ class FolderSensor(SensorEntity): self._unsub_timer() self._unsub_timer = None - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" @callback From 3798d28bec6dc257da8387a6751949d47fb29a29 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 6 Sep 2022 13:37:00 +0200 Subject: [PATCH 142/955] Improve entity type hints [u] (#77884) --- .../components/ue_smart_radio/media_player.py | 20 ++++---- homeassistant/components/unifi/switch.py | 24 +++++----- .../components/universal/media_player.py | 47 ++++++++++--------- homeassistant/components/uvc/camera.py | 12 ++--- 4 files changed, 53 insertions(+), 50 deletions(-) diff --git a/homeassistant/components/ue_smart_radio/media_player.py b/homeassistant/components/ue_smart_radio/media_player.py index f7e0621b5e0..6c83f207762 100644 --- a/homeassistant/components/ue_smart_radio/media_player.py +++ b/homeassistant/components/ue_smart_radio/media_player.py @@ -115,7 +115,7 @@ class UERadioDevice(MediaPlayerEntity): self._session, ) - def update(self): + def update(self) -> None: """Get the latest details from the device.""" request = send_request( { @@ -192,35 +192,35 @@ class UERadioDevice(MediaPlayerEntity): """Title of current playing media.""" return self._media_title - def turn_on(self): + def turn_on(self) -> None: """Turn on specified media player or all.""" self.send_command(["power", 1]) - def turn_off(self): + def turn_off(self) -> None: """Turn off specified media player or all.""" self.send_command(["power", 0]) - def media_play(self): + def media_play(self) -> None: """Send the media player the command for play/pause.""" self.send_command(["play"]) - def media_pause(self): + def media_pause(self) -> None: """Send the media player the command for pause.""" self.send_command(["pause"]) - def media_stop(self): + def media_stop(self) -> None: """Send the media player the stop command.""" self.send_command(["stop"]) - def media_previous_track(self): + def media_previous_track(self) -> None: """Send the media player the command for prev track.""" self.send_command(["button", "rew"]) - def media_next_track(self): + def media_next_track(self) -> None: """Send the media player the command for next track.""" self.send_command(["button", "fwd"]) - def mute_volume(self, mute): + def mute_volume(self, mute: bool) -> None: """Send mute command.""" if mute: self._last_volume = self._volume @@ -228,6 +228,6 @@ class UERadioDevice(MediaPlayerEntity): else: self.send_command(["mixer", "volume", self._last_volume * 100]) - def set_volume_level(self, volume): + def set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" self.send_command(["mixer", "volume", volume * 100]) diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index 67c4d5c4544..b9f23c31392 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -232,7 +232,7 @@ class UniFiPOEClientSwitch(UniFiClient, SwitchEntity, RestoreEntity): if client.switch_port and self.port.poe_mode != "off": self.poe_mode = self.port.poe_mode - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Call when entity about to be added to Home Assistant.""" await super().async_added_to_hass() @@ -256,7 +256,7 @@ class UniFiPOEClientSwitch(UniFiClient, SwitchEntity, RestoreEntity): return self.port.poe_mode != "off" @property - def available(self): + def available(self) -> bool: """Return if switch is available. Poe_mode None means its POE state is unknown. @@ -270,11 +270,11 @@ class UniFiPOEClientSwitch(UniFiClient, SwitchEntity, RestoreEntity): and self.client.switch_mac in self.controller.api.devices ) - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Enable POE for client.""" await self.device.set_port_poe_mode(self.client.switch_port, self.poe_mode) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Disable POE for client.""" await self.device.set_port_poe_mode(self.client.switch_port, "off") @@ -335,16 +335,16 @@ class UniFiBlockClientSwitch(UniFiClient, SwitchEntity): """Return true if client is allowed to connect.""" return not self._is_blocked - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn on connectivity for client.""" await self.controller.api.clients.unblock(self.client.mac) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn off connectivity for client.""" await self.controller.api.clients.block(self.client.mac) @property - def icon(self): + def icon(self) -> str: """Return the icon to use in the frontend.""" if self._is_blocked: return "mdi:network-off" @@ -445,7 +445,7 @@ class UniFiDPIRestrictionSwitch(UniFiBase, SwitchEntity): """Return true if DPI group app restriction is enabled.""" return self._is_enabled - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Restrict access of apps related to DPI group.""" return await asyncio.gather( *[ @@ -454,7 +454,7 @@ class UniFiDPIRestrictionSwitch(UniFiBase, SwitchEntity): ] ) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Remove restriction of apps related to DPI group.""" return await asyncio.gather( *[ @@ -503,15 +503,15 @@ class UniFiOutletSwitch(UniFiBase, SwitchEntity): return self._item.outlets[self._outlet_index].relay_state @property - def available(self): + def available(self) -> bool: """Return if switch is available.""" return not self._item.disabled and self.controller.available - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Enable outlet relay.""" await self._item.set_outlet_relay_state(self._outlet_index, True) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Disable outlet relay.""" await self._item.set_outlet_relay_state(self._outlet_index, False) diff --git a/homeassistant/components/universal/media_player.py b/homeassistant/components/universal/media_player.py index ee2954aac28..75c2b5d0432 100644 --- a/homeassistant/components/universal/media_player.py +++ b/homeassistant/components/universal/media_player.py @@ -2,6 +2,7 @@ from __future__ import annotations from copy import copy +from typing import Any import voluptuous as vol @@ -183,7 +184,7 @@ class UniversalMediaPlayer(MediaPlayerEntity): self._state_template = state_template self._device_class = device_class - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Subscribe to children and template state changes.""" @callback @@ -529,95 +530,97 @@ class UniversalMediaPlayer(MediaPlayerEntity): """When was the position of the current playing media valid.""" return self._child_attr(ATTR_MEDIA_POSITION_UPDATED_AT) - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Turn the media player on.""" await self._async_call_service(SERVICE_TURN_ON, allow_override=True) - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Turn the media player off.""" await self._async_call_service(SERVICE_TURN_OFF, allow_override=True) - async def async_mute_volume(self, mute): + async def async_mute_volume(self, mute: bool) -> None: """Mute the volume.""" data = {ATTR_MEDIA_VOLUME_MUTED: mute} await self._async_call_service(SERVICE_VOLUME_MUTE, data, allow_override=True) - async def async_set_volume_level(self, volume): + async def async_set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" data = {ATTR_MEDIA_VOLUME_LEVEL: volume} await self._async_call_service(SERVICE_VOLUME_SET, data, allow_override=True) - async def async_media_play(self): + async def async_media_play(self) -> None: """Send play command.""" await self._async_call_service(SERVICE_MEDIA_PLAY, allow_override=True) - async def async_media_pause(self): + async def async_media_pause(self) -> None: """Send pause command.""" await self._async_call_service(SERVICE_MEDIA_PAUSE, allow_override=True) - async def async_media_stop(self): + async def async_media_stop(self) -> None: """Send stop command.""" await self._async_call_service(SERVICE_MEDIA_STOP, allow_override=True) - async def async_media_previous_track(self): + async def async_media_previous_track(self) -> None: """Send previous track command.""" await self._async_call_service( SERVICE_MEDIA_PREVIOUS_TRACK, allow_override=True ) - async def async_media_next_track(self): + async def async_media_next_track(self) -> None: """Send next track command.""" await self._async_call_service(SERVICE_MEDIA_NEXT_TRACK, allow_override=True) - async def async_media_seek(self, position): + async def async_media_seek(self, position: float) -> None: """Send seek command.""" data = {ATTR_MEDIA_SEEK_POSITION: position} await self._async_call_service(SERVICE_MEDIA_SEEK, data) - async def async_play_media(self, media_type, media_id, **kwargs): + async def async_play_media( + self, media_type: str, media_id: str, **kwargs: Any + ) -> None: """Play a piece of media.""" data = {ATTR_MEDIA_CONTENT_TYPE: media_type, ATTR_MEDIA_CONTENT_ID: media_id} await self._async_call_service(SERVICE_PLAY_MEDIA, data, allow_override=True) - async def async_volume_up(self): + async def async_volume_up(self) -> None: """Turn volume up for media player.""" await self._async_call_service(SERVICE_VOLUME_UP, allow_override=True) - async def async_volume_down(self): + async def async_volume_down(self) -> None: """Turn volume down for media player.""" await self._async_call_service(SERVICE_VOLUME_DOWN, allow_override=True) - async def async_media_play_pause(self): + async def async_media_play_pause(self) -> None: """Play or pause the media player.""" await self._async_call_service(SERVICE_MEDIA_PLAY_PAUSE, allow_override=True) - async def async_select_sound_mode(self, sound_mode): + async def async_select_sound_mode(self, sound_mode: str) -> None: """Select sound mode.""" data = {ATTR_SOUND_MODE: sound_mode} await self._async_call_service( SERVICE_SELECT_SOUND_MODE, data, allow_override=True ) - async def async_select_source(self, source): + async def async_select_source(self, source: str) -> None: """Set the input source.""" data = {ATTR_INPUT_SOURCE: source} await self._async_call_service(SERVICE_SELECT_SOURCE, data, allow_override=True) - async def async_clear_playlist(self): + async def async_clear_playlist(self) -> None: """Clear players playlist.""" await self._async_call_service(SERVICE_CLEAR_PLAYLIST, allow_override=True) - async def async_set_shuffle(self, shuffle): + async def async_set_shuffle(self, shuffle: bool) -> None: """Enable/disable shuffling.""" data = {ATTR_MEDIA_SHUFFLE: shuffle} await self._async_call_service(SERVICE_SHUFFLE_SET, data, allow_override=True) - async def async_set_repeat(self, repeat): + async def async_set_repeat(self, repeat: str) -> None: """Set repeat mode.""" data = {ATTR_MEDIA_REPEAT: repeat} await self._async_call_service(SERVICE_REPEAT_SET, data, allow_override=True) - async def async_toggle(self): + async def async_toggle(self) -> None: """Toggle the power on the media player.""" if SERVICE_TOGGLE in self._cmds: await self._async_call_service(SERVICE_TOGGLE, allow_override=True) @@ -625,7 +628,7 @@ class UniversalMediaPlayer(MediaPlayerEntity): # Delegate to turn_on or turn_off by default await super().async_toggle() - async def async_update(self): + async def async_update(self) -> None: """Update state in HA.""" self._child_state = None for child_name in self._children: diff --git a/homeassistant/components/uvc/camera.py b/homeassistant/components/uvc/camera.py index d6c3bd55d63..c64642077da 100644 --- a/homeassistant/components/uvc/camera.py +++ b/homeassistant/components/uvc/camera.py @@ -107,7 +107,7 @@ class UnifiVideoCamera(Camera): return self._name @property - def supported_features(self): + def supported_features(self) -> int: """Return supported features.""" channels = self._caminfo["channels"] for channel in channels: @@ -127,7 +127,7 @@ class UnifiVideoCamera(Camera): return attr @property - def is_recording(self): + def is_recording(self) -> bool: """Return true if the camera is recording.""" recording_state = "DISABLED" if "recordingIndicator" in self._caminfo: @@ -138,7 +138,7 @@ class UnifiVideoCamera(Camera): ] or recording_state in ("MOTION_INPROGRESS", "MOTION_FINISHED") @property - def motion_detection_enabled(self): + def motion_detection_enabled(self) -> bool: """Camera Motion Detection Status.""" return self._caminfo["recordingSettings"]["motionRecordEnabled"] @@ -230,11 +230,11 @@ class UnifiVideoCamera(Camera): _LOGGER.error("Unable to set recordmode to %s", set_mode) _LOGGER.debug(err) - def enable_motion_detection(self): + def enable_motion_detection(self) -> None: """Enable motion detection in camera.""" self.set_motion_detection(True) - def disable_motion_detection(self): + def disable_motion_detection(self) -> None: """Disable motion detection in camera.""" self.set_motion_detection(False) @@ -255,7 +255,7 @@ class UnifiVideoCamera(Camera): return None - def update(self): + def update(self) -> None: """Update the info.""" self._caminfo = self._nvr.get_camera(self._uuid) From 050cb275ffd51891fa58121643086dad304776a3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 6 Sep 2022 13:59:05 +0200 Subject: [PATCH 143/955] Improve entity type hints [v] (#77885) --- homeassistant/components/vasttrafik/sensor.py | 2 +- homeassistant/components/venstar/climate.py | 10 ++-- homeassistant/components/vera/climate.py | 2 +- homeassistant/components/verisure/switch.py | 5 +- homeassistant/components/versasense/sensor.py | 2 +- homeassistant/components/versasense/switch.py | 7 +-- homeassistant/components/vesync/switch.py | 5 +- .../components/viaggiatreno/sensor.py | 2 +- homeassistant/components/vicare/climate.py | 7 +-- .../components/vicare/water_heater.py | 5 +- homeassistant/components/vilfo/sensor.py | 4 +- homeassistant/components/vivotek/camera.py | 6 +-- homeassistant/components/vlc/media_player.py | 19 ++++---- .../components/volkszaehler/sensor.py | 4 +- .../components/volumio/media_player.py | 47 ++++++++++++------- .../components/volvooncall/switch.py | 6 ++- homeassistant/components/vulcan/calendar.py | 4 +- .../components/vultr/binary_sensor.py | 2 +- homeassistant/components/vultr/sensor.py | 2 +- homeassistant/components/vultr/switch.py | 7 +-- 20 files changed, 86 insertions(+), 62 deletions(-) diff --git a/homeassistant/components/vasttrafik/sensor.py b/homeassistant/components/vasttrafik/sensor.py index bec7959f9c8..d4229066eff 100644 --- a/homeassistant/components/vasttrafik/sensor.py +++ b/homeassistant/components/vasttrafik/sensor.py @@ -125,7 +125,7 @@ class VasttrafikDepartureSensor(SensorEntity): return self._state @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): + def update(self) -> None: """Get the departure board.""" try: self._departureboard = self._planner.departureboard( diff --git a/homeassistant/components/venstar/climate.py b/homeassistant/components/venstar/climate.py index 798ffe5b6d3..11d6ecc1783 100644 --- a/homeassistant/components/venstar/climate.py +++ b/homeassistant/components/venstar/climate.py @@ -124,7 +124,7 @@ class VenstarThermostat(VenstarEntity, ClimateEntity): self._attr_name = self._client.name @property - def supported_features(self): + def supported_features(self) -> int: """Return the list of supported features.""" features = ( ClimateEntityFeature.TARGET_TEMPERATURE @@ -141,7 +141,7 @@ class VenstarThermostat(VenstarEntity, ClimateEntity): return features @property - def temperature_unit(self): + def temperature_unit(self) -> str: """Return the unit of measurement, as defined by the API.""" if self._client.tempunits == self._client.TEMPUNITS_F: return TEMP_FAHRENHEIT @@ -300,7 +300,7 @@ class VenstarThermostat(VenstarEntity, ClimateEntity): _LOGGER.error("Failed to change the temperature") self.schedule_update_ha_state() - def set_fan_mode(self, fan_mode): + def set_fan_mode(self, fan_mode: str) -> None: """Set new target fan mode.""" if fan_mode == STATE_ON: success = self._client.set_fan(self._client.FAN_ON) @@ -316,7 +316,7 @@ class VenstarThermostat(VenstarEntity, ClimateEntity): self._set_operation_mode(hvac_mode) self.schedule_update_ha_state() - def set_humidity(self, humidity): + def set_humidity(self, humidity: int) -> None: """Set new target humidity.""" success = self._client.set_hum_setpoint(humidity) @@ -324,7 +324,7 @@ class VenstarThermostat(VenstarEntity, ClimateEntity): _LOGGER.error("Failed to change the target humidity level") self.schedule_update_ha_state() - def set_preset_mode(self, preset_mode): + def set_preset_mode(self, preset_mode: str) -> None: """Set the hold mode.""" if preset_mode == PRESET_AWAY: success = self._client.set_away(self._client.AWAY_AWAY) diff --git a/homeassistant/components/vera/climate.py b/homeassistant/components/vera/climate.py index b182e7635ab..8a3803db821 100644 --- a/homeassistant/components/vera/climate.py +++ b/homeassistant/components/vera/climate.py @@ -88,7 +88,7 @@ class VeraThermostat(VeraDevice[veraApi.VeraThermostat], ClimateEntity): """Return a list of available fan modes.""" return FAN_OPERATION_LIST - def set_fan_mode(self, fan_mode) -> None: + def set_fan_mode(self, fan_mode: str) -> None: """Set new target temperature.""" if fan_mode == FAN_ON: self.vera_device.fan_on() diff --git a/homeassistant/components/verisure/switch.py b/homeassistant/components/verisure/switch.py index 177beb4272b..ffb6e434fea 100644 --- a/homeassistant/components/verisure/switch.py +++ b/homeassistant/components/verisure/switch.py @@ -2,6 +2,7 @@ from __future__ import annotations from time import monotonic +from typing import Any from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry @@ -76,14 +77,14 @@ class VerisureSmartplug(CoordinatorEntity[VerisureDataUpdateCoordinator], Switch and self.serial_number in self.coordinator.data["smart_plugs"] ) - def turn_on(self, **kwargs) -> None: + def turn_on(self, **kwargs: Any) -> None: """Set smartplug status on.""" self.coordinator.verisure.set_smartplug_state(self.serial_number, True) self._state = True self._change_timestamp = monotonic() self.schedule_update_ha_state() - def turn_off(self, **kwargs) -> None: + def turn_off(self, **kwargs: Any) -> None: """Set smartplug status off.""" self.coordinator.verisure.set_smartplug_state(self.serial_number, False) self._state = False diff --git a/homeassistant/components/versasense/sensor.py b/homeassistant/components/versasense/sensor.py index 197ac9be250..349ed429b33 100644 --- a/homeassistant/components/versasense/sensor.py +++ b/homeassistant/components/versasense/sensor.py @@ -89,7 +89,7 @@ class VSensor(SensorEntity): """Return if the sensor is available.""" return self._available - async def async_update(self): + async def async_update(self) -> None: """Fetch new state data for the sensor.""" samples = await self.consumer.fetchPeripheralSample( None, self._identifier, self._parent_mac diff --git a/homeassistant/components/versasense/switch.py b/homeassistant/components/versasense/switch.py index 58181229ae8..b57013b539d 100644 --- a/homeassistant/components/versasense/switch.py +++ b/homeassistant/components/versasense/switch.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from homeassistant.components.switch import SwitchEntity from homeassistant.core import HomeAssistant @@ -86,11 +87,11 @@ class VActuator(SwitchEntity): """Return if the actuator is available.""" return self._available - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the actuator.""" await self.update_state(0) - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the actuator.""" await self.update_state(1) @@ -102,7 +103,7 @@ class VActuator(SwitchEntity): None, self._identifier, self._parent_mac, payload ) - async def async_update(self): + async def async_update(self) -> None: """Fetch state data from the actuator.""" samples = await self.consumer.fetchPeripheralSample( None, self._identifier, self._parent_mac diff --git a/homeassistant/components/vesync/switch.py b/homeassistant/components/vesync/switch.py index 68b10e40bcb..93cb5c67a5d 100644 --- a/homeassistant/components/vesync/switch.py +++ b/homeassistant/components/vesync/switch.py @@ -1,5 +1,6 @@ """Support for VeSync switches.""" import logging +from typing import Any from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry @@ -53,7 +54,7 @@ def _setup_entities(devices, async_add_entities): class VeSyncBaseSwitch(VeSyncDevice, SwitchEntity): """Base class for VeSync switch Device Representations.""" - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" self.device.turn_on() @@ -66,7 +67,7 @@ class VeSyncSwitchHA(VeSyncBaseSwitch, SwitchEntity): super().__init__(plug) self.smartplug = plug - def update(self): + def update(self) -> None: """Update outlet details and energy usage.""" self.smartplug.update() self.smartplug.update_energy() diff --git a/homeassistant/components/viaggiatreno/sensor.py b/homeassistant/components/viaggiatreno/sensor.py index 95eeb154f9c..acb6ffa647e 100644 --- a/homeassistant/components/viaggiatreno/sensor.py +++ b/homeassistant/components/viaggiatreno/sensor.py @@ -161,7 +161,7 @@ class ViaggiaTrenoSensor(SensorEntity): return True return False - async def async_update(self): + async def async_update(self) -> None: """Update state.""" uri = self.uri res = await async_http_request(self.hass, uri) diff --git a/homeassistant/components/vicare/climate.py b/homeassistant/components/vicare/climate.py index 53759b42243..44b73ddfbd0 100644 --- a/homeassistant/components/vicare/climate.py +++ b/homeassistant/components/vicare/climate.py @@ -3,6 +3,7 @@ from __future__ import annotations from contextlib import suppress import logging +from typing import Any from PyViCare.PyViCareUtils import ( PyViCareInvalidDataError, @@ -179,7 +180,7 @@ class ViCareClimate(ClimateEntity): configuration_url="https://developer.viessmann.com/", ) - def update(self): + def update(self) -> None: """Let HA know there has been an update from the ViCare API.""" try: _room_temperature = None @@ -326,7 +327,7 @@ class ViCareClimate(ClimateEntity): """Set target temperature step to wholes.""" return PRECISION_WHOLE - def set_temperature(self, **kwargs): + def set_temperature(self, **kwargs: Any) -> None: """Set new target temperatures.""" if (temp := kwargs.get(ATTR_TEMPERATURE)) is not None: self._circuit.setProgramTemperature(self._current_program, temp) @@ -342,7 +343,7 @@ class ViCareClimate(ClimateEntity): """Return the available preset mode.""" return list(HA_TO_VICARE_PRESET_HEATING) - def set_preset_mode(self, preset_mode): + def set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode and deactivate any existing programs.""" vicare_program = HA_TO_VICARE_PRESET_HEATING.get(preset_mode) if vicare_program is None: diff --git a/homeassistant/components/vicare/water_heater.py b/homeassistant/components/vicare/water_heater.py index 878f2ac47a5..a3bcc79a117 100644 --- a/homeassistant/components/vicare/water_heater.py +++ b/homeassistant/components/vicare/water_heater.py @@ -1,6 +1,7 @@ """Viessmann ViCare water_heater device.""" from contextlib import suppress import logging +from typing import Any from PyViCare.PyViCareUtils import ( PyViCareInvalidDataError, @@ -116,7 +117,7 @@ class ViCareWater(WaterHeaterEntity): self._current_mode = None self._heating_type = heating_type - def update(self): + def update(self) -> None: """Let HA know there has been an update from the ViCare API.""" try: with suppress(PyViCareNotSupportedFeatureError): @@ -177,7 +178,7 @@ class ViCareWater(WaterHeaterEntity): """Return the temperature we try to reach.""" return self._target_temperature - def set_temperature(self, **kwargs): + def set_temperature(self, **kwargs: Any) -> None: """Set new target temperatures.""" if (temp := kwargs.get(ATTR_TEMPERATURE)) is not None: self._api.setDomesticHotWaterTemperature(temp) diff --git a/homeassistant/components/vilfo/sensor.py b/homeassistant/components/vilfo/sensor.py index bb57270adc4..c41e61d099e 100644 --- a/homeassistant/components/vilfo/sensor.py +++ b/homeassistant/components/vilfo/sensor.py @@ -46,7 +46,7 @@ class VilfoRouterSensor(SensorEntity): self._attr_unique_id = f"{api.unique_id}_{description.key}" @property - def available(self): + def available(self) -> bool: """Return whether the sensor is available or not.""" return self.api.available @@ -61,7 +61,7 @@ class VilfoRouterSensor(SensorEntity): parent_device_name = self._device_info["name"] return f"{parent_device_name} {self.entity_description.name}" - async def async_update(self): + async def async_update(self) -> None: """Update the router data.""" await self.api.async_update() self._attr_native_value = self.api.data.get(self.entity_description.api_key) diff --git a/homeassistant/components/vivotek/camera.py b/homeassistant/components/vivotek/camera.py index c25e96f161e..46cf2586c34 100644 --- a/homeassistant/components/vivotek/camera.py +++ b/homeassistant/components/vivotek/camera.py @@ -114,12 +114,12 @@ class VivotekCam(Camera): """Return the camera motion detection status.""" return self._motion_detection_enabled - def disable_motion_detection(self): + def disable_motion_detection(self) -> None: """Disable motion detection in camera.""" response = self._cam.set_param(DEFAULT_EVENT_0_KEY, 0) self._motion_detection_enabled = int(response) == 1 - def enable_motion_detection(self): + def enable_motion_detection(self) -> None: """Enable motion detection in camera.""" response = self._cam.set_param(DEFAULT_EVENT_0_KEY, 1) self._motion_detection_enabled = int(response) == 1 @@ -134,6 +134,6 @@ class VivotekCam(Camera): """Return the camera model.""" return self._model_name - def update(self): + def update(self) -> None: """Update entity status.""" self._model_name = self._cam.model_name diff --git a/homeassistant/components/vlc/media_player.py b/homeassistant/components/vlc/media_player.py index 88b663e09c6..c8517adff91 100644 --- a/homeassistant/components/vlc/media_player.py +++ b/homeassistant/components/vlc/media_player.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any import vlc import voluptuous as vol @@ -134,37 +135,39 @@ class VlcDevice(MediaPlayerEntity): """When was the position of the current playing media valid.""" return self._media_position_updated_at - def media_seek(self, position): + def media_seek(self, position: float) -> None: """Seek the media to a specific location.""" track_length = self._vlc.get_length() / 1000 self._vlc.set_position(position / track_length) - def mute_volume(self, mute): + def mute_volume(self, mute: bool) -> None: """Mute the volume.""" self._vlc.audio_set_mute(mute) self._muted = mute - def set_volume_level(self, volume): + def set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" self._vlc.audio_set_volume(int(volume * 100)) self._volume = volume - def media_play(self): + def media_play(self) -> None: """Send play command.""" self._vlc.play() self._state = STATE_PLAYING - def media_pause(self): + def media_pause(self) -> None: """Send pause command.""" self._vlc.pause() self._state = STATE_PAUSED - def media_stop(self): + def media_stop(self) -> None: """Send stop command.""" self._vlc.stop() self._state = STATE_IDLE - async def async_play_media(self, media_type, media_id, **kwargs): + async def async_play_media( + self, media_type: str, media_id: str, **kwargs: Any + ) -> None: """Play media from a URL or file.""" # Handle media_source if media_source.is_media_source_id(media_id): @@ -191,7 +194,7 @@ class VlcDevice(MediaPlayerEntity): self._state = STATE_PLAYING async def async_browse_media( - self, media_content_type=None, media_content_id=None + self, media_content_type: str | None = None, media_content_id: str | None = None ) -> BrowseMedia: """Implement the websocket media browsing helper.""" return await media_source.async_browse_media( diff --git a/homeassistant/components/volkszaehler/sensor.py b/homeassistant/components/volkszaehler/sensor.py index 8694d8186fe..44d53018048 100644 --- a/homeassistant/components/volkszaehler/sensor.py +++ b/homeassistant/components/volkszaehler/sensor.py @@ -123,11 +123,11 @@ class VolkszaehlerSensor(SensorEntity): self._attr_name = f"{name} {description.name}" @property - def available(self): + def available(self) -> bool: """Could the device be accessed during the last update call.""" return self.vz_api.available - async def async_update(self): + async def async_update(self) -> None: """Get the latest data from REST API.""" await self.vz_api.async_update() diff --git a/homeassistant/components/volumio/media_player.py b/homeassistant/components/volumio/media_player.py index ec4931ce3bc..2b07c719f58 100644 --- a/homeassistant/components/volumio/media_player.py +++ b/homeassistant/components/volumio/media_player.py @@ -3,13 +3,17 @@ Volumio Platform. Volumio rest API: https://volumio.github.io/docs/API/REST_API.html """ +from __future__ import annotations + from datetime import timedelta import json +from typing import Any from homeassistant.components.media_player import ( MediaPlayerEntity, MediaPlayerEntityFeature, ) +from homeassistant.components.media_player.browse_media import BrowseMedia from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, REPEAT_MODE_ALL, @@ -83,7 +87,7 @@ class Volumio(MediaPlayerEntity): self._currentplaylist = None self.thumbnail_cache = {} - async def async_update(self): + async def async_update(self) -> None: """Update state.""" self._state = await self._volumio.get_state() await self._async_update_playlists() @@ -191,65 +195,65 @@ class Volumio(MediaPlayerEntity): """Name of the current input source.""" return self._currentplaylist - async def async_media_next_track(self): + async def async_media_next_track(self) -> None: """Send media_next command to media player.""" await self._volumio.next() - async def async_media_previous_track(self): + async def async_media_previous_track(self) -> None: """Send media_previous command to media player.""" await self._volumio.previous() - async def async_media_play(self): + async def async_media_play(self) -> None: """Send media_play command to media player.""" await self._volumio.play() - async def async_media_pause(self): + async def async_media_pause(self) -> None: """Send media_pause command to media player.""" if self._state.get("trackType") == "webradio": await self._volumio.stop() else: await self._volumio.pause() - async def async_media_stop(self): + async def async_media_stop(self) -> None: """Send media_stop command to media player.""" await self._volumio.stop() - async def async_set_volume_level(self, volume): + async def async_set_volume_level(self, volume: float) -> None: """Send volume_up command to media player.""" await self._volumio.set_volume_level(int(volume * 100)) - async def async_volume_up(self): + async def async_volume_up(self) -> None: """Service to send the Volumio the command for volume up.""" await self._volumio.volume_up() - async def async_volume_down(self): + async def async_volume_down(self) -> None: """Service to send the Volumio the command for volume down.""" await self._volumio.volume_down() - async def async_mute_volume(self, mute): + async def async_mute_volume(self, mute: bool) -> None: """Send mute command to media player.""" if mute: await self._volumio.mute() else: await self._volumio.unmute() - async def async_set_shuffle(self, shuffle): + async def async_set_shuffle(self, shuffle: bool) -> None: """Enable/disable shuffle mode.""" await self._volumio.set_shuffle(shuffle) - async def async_set_repeat(self, repeat): + async def async_set_repeat(self, repeat: str) -> None: """Set repeat mode.""" if repeat == REPEAT_MODE_OFF: await self._volumio.repeatAll("false") else: await self._volumio.repeatAll("true") - async def async_select_source(self, source): + async def async_select_source(self, source: str) -> None: """Choose an available playlist and play it.""" await self._volumio.play_playlist(source) self._currentplaylist = source - async def async_clear_playlist(self): + async def async_clear_playlist(self) -> None: """Clear players playlist.""" await self._volumio.clear_playlist() self._currentplaylist = None @@ -259,11 +263,15 @@ class Volumio(MediaPlayerEntity): """Update available Volumio playlists.""" self._playlists = await self._volumio.get_playlists() - async def async_play_media(self, media_type, media_id, **kwargs): + async def async_play_media( + self, media_type: str, media_id: str, **kwargs: Any + ) -> None: """Send the play_media command to the media player.""" await self._volumio.replace_and_play(json.loads(media_id)) - async def async_browse_media(self, media_content_type=None, media_content_id=None): + async def async_browse_media( + self, media_content_type: str | None = None, media_content_id: str | None = None + ) -> BrowseMedia: """Implement the websocket media browsing helper.""" self.thumbnail_cache = {} if media_content_type in (None, "library"): @@ -274,8 +282,11 @@ class Volumio(MediaPlayerEntity): ) async def async_get_browse_image( - self, media_content_type, media_content_id, media_image_id=None - ): + self, + media_content_type: str, + media_content_id: str, + media_image_id: str | None = None, + ) -> tuple[bytes | None, str | None]: """Get album art from Volumio.""" cached_url = self.thumbnail_cache.get(media_content_id) image_url = self._volumio.canonic_url(cached_url) diff --git a/homeassistant/components/volvooncall/switch.py b/homeassistant/components/volvooncall/switch.py index 4d7b65bce95..89300cd54e1 100644 --- a/homeassistant/components/volvooncall/switch.py +++ b/homeassistant/components/volvooncall/switch.py @@ -1,6 +1,8 @@ """Support for Volvo heater.""" from __future__ import annotations +from typing import Any + from volvooncall.dashboard import Instrument from homeassistant.components.switch import SwitchEntity @@ -67,12 +69,12 @@ class VolvoSwitch(VolvoEntity, SwitchEntity): """Determine if switch is on.""" return self.instrument.state - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" await self.instrument.turn_on() await self.coordinator.async_request_refresh() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" await self.instrument.turn_off() await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/vulcan/calendar.py b/homeassistant/components/vulcan/calendar.py index ac302940c72..cfa962e51a3 100644 --- a/homeassistant/components/vulcan/calendar.py +++ b/homeassistant/components/vulcan/calendar.py @@ -78,7 +78,9 @@ class VulcanCalendarEntity(CalendarEntity): """Return the next upcoming event.""" return self._event - async def async_get_events(self, hass, start_date, end_date) -> list[CalendarEvent]: + async def async_get_events( + self, hass: HomeAssistant, start_date: datetime, end_date: datetime + ) -> list[CalendarEvent]: """Get all events in a specific time frame.""" try: events = await get_lessons( diff --git a/homeassistant/components/vultr/binary_sensor.py b/homeassistant/components/vultr/binary_sensor.py index f3501b33146..4ef35d4f410 100644 --- a/homeassistant/components/vultr/binary_sensor.py +++ b/homeassistant/components/vultr/binary_sensor.py @@ -111,7 +111,7 @@ class VultrBinarySensor(BinarySensorEntity): ATTR_VCPUS: self.data.get("vcpu_count"), } - def update(self): + def update(self) -> None: """Update state of sensor.""" self._vultr.update() self.data = self._vultr.data[self.subscription] diff --git a/homeassistant/components/vultr/sensor.py b/homeassistant/components/vultr/sensor.py index 33b51e99138..c2eddfc3078 100644 --- a/homeassistant/components/vultr/sensor.py +++ b/homeassistant/components/vultr/sensor.py @@ -112,7 +112,7 @@ class VultrSensor(SensorEntity): except (TypeError, ValueError): return self.data.get(self.entity_description.key) - def update(self): + def update(self) -> None: """Update state of sensor.""" self._vultr.update() self.data = self._vultr.data[self.subscription] diff --git a/homeassistant/components/vultr/switch.py b/homeassistant/components/vultr/switch.py index 04709817713..a12cfab3e83 100644 --- a/homeassistant/components/vultr/switch.py +++ b/homeassistant/components/vultr/switch.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any import voluptuous as vol @@ -108,17 +109,17 @@ class VultrSwitch(SwitchEntity): ATTR_VCPUS: self.data.get("vcpu_count"), } - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Boot-up the subscription.""" if self.data["power_status"] != "running": self._vultr.start(self.subscription) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Halt the subscription.""" if self.data["power_status"] == "running": self._vultr.halt(self.subscription) - def update(self): + def update(self) -> None: """Get the latest data from the device and update the data.""" self._vultr.update() self.data = self._vultr.data[self.subscription] From a6b6949793e2571bf46cdca2e541ddf64cb1fc71 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 6 Sep 2022 13:59:37 +0200 Subject: [PATCH 144/955] Improve entity type hints [w] (#77886) --- homeassistant/components/w800rf32/binary_sensor.py | 2 +- homeassistant/components/wake_on_lan/switch.py | 7 ++++--- homeassistant/components/waqi/sensor.py | 2 +- homeassistant/components/waterfurnace/sensor.py | 2 +- .../components/waze_travel_time/sensor.py | 2 +- .../components/wirelesstag/binary_sensor.py | 2 +- homeassistant/components/wirelesstag/sensor.py | 2 +- homeassistant/components/wirelesstag/switch.py | 6 ++++-- homeassistant/components/worldtidesinfo/sensor.py | 2 +- homeassistant/components/worxlandroid/sensor.py | 2 +- homeassistant/components/ws66i/media_player.py | 14 +++++++------- homeassistant/components/wsdot/sensor.py | 2 +- 12 files changed, 24 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/w800rf32/binary_sensor.py b/homeassistant/components/w800rf32/binary_sensor.py index 2473d193197..ef8e02803f6 100644 --- a/homeassistant/components/w800rf32/binary_sensor.py +++ b/homeassistant/components/w800rf32/binary_sensor.py @@ -138,6 +138,6 @@ class W800rf32BinarySensor(BinarySensorEntity): self._state = state self.async_write_ha_state() - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register update callback.""" async_dispatcher_connect(self.hass, self._signal, self.binary_sensor_update) diff --git a/homeassistant/components/wake_on_lan/switch.py b/homeassistant/components/wake_on_lan/switch.py index 2f019ca6158..1b3904f63e6 100644 --- a/homeassistant/components/wake_on_lan/switch.py +++ b/homeassistant/components/wake_on_lan/switch.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging import subprocess as sp +from typing import Any import voluptuous as vol import wakeonlan @@ -125,7 +126,7 @@ class WolSwitch(SwitchEntity): """Return the unique id of this switch.""" return self._unique_id - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" service_kwargs = {} if self._broadcast_address is not None: @@ -146,7 +147,7 @@ class WolSwitch(SwitchEntity): self._state = True self.async_write_ha_state() - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the device off if an off action is present.""" if self._off_script is not None: self._off_script.run(context=self._context) @@ -155,7 +156,7 @@ class WolSwitch(SwitchEntity): self._state = False self.async_write_ha_state() - def update(self): + def update(self) -> None: """Check if device is on and update the state. Only called if assumed state is false.""" ping_cmd = [ "ping", diff --git a/homeassistant/components/waqi/sensor.py b/homeassistant/components/waqi/sensor.py index 499233717b6..c9cc527387a 100644 --- a/homeassistant/components/waqi/sensor.py +++ b/homeassistant/components/waqi/sensor.py @@ -183,7 +183,7 @@ class WaqiSensor(SensorEntity): except (IndexError, KeyError): return {ATTR_ATTRIBUTION: ATTRIBUTION} - async def async_update(self): + async def async_update(self) -> None: """Get the latest data and updates the states.""" if self.uid: result = await self._client.get_station_by_number(self.uid) diff --git a/homeassistant/components/waterfurnace/sensor.py b/homeassistant/components/waterfurnace/sensor.py index c6ef9610bca..454e73f5f7a 100644 --- a/homeassistant/components/waterfurnace/sensor.py +++ b/homeassistant/components/waterfurnace/sensor.py @@ -133,7 +133,7 @@ class WaterFurnaceSensor(SensorEntity): """Return the units of measurement.""" return self._unit_of_measurement - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" self.async_on_remove( async_dispatcher_connect( diff --git a/homeassistant/components/waze_travel_time/sensor.py b/homeassistant/components/waze_travel_time/sensor.py index 3471099b588..4992fc07db5 100644 --- a/homeassistant/components/waze_travel_time/sensor.py +++ b/homeassistant/components/waze_travel_time/sensor.py @@ -161,7 +161,7 @@ class WazeTravelTime(SensorEntity): await self.hass.async_add_executor_job(self.update) self.async_write_ha_state() - def update(self): + def update(self) -> None: """Fetch new state data for the sensor.""" _LOGGER.debug("Fetching Route for %s", self._attr_name) self._waze_data.origin = find_coordinates(self.hass, self._origin) diff --git a/homeassistant/components/wirelesstag/binary_sensor.py b/homeassistant/components/wirelesstag/binary_sensor.py index dde1a22f622..82c3a25590a 100644 --- a/homeassistant/components/wirelesstag/binary_sensor.py +++ b/homeassistant/components/wirelesstag/binary_sensor.py @@ -101,7 +101,7 @@ class WirelessTagBinarySensor(WirelessTagBaseSensor, BinarySensorEntity): self._sensor_type = sensor_type self._name = f"{self._tag.name} {self.event.human_readable_name}" - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" tag_id = self.tag_id event_type = self.device_class diff --git a/homeassistant/components/wirelesstag/sensor.py b/homeassistant/components/wirelesstag/sensor.py index 9cec276f26a..fb54c90cc1f 100644 --- a/homeassistant/components/wirelesstag/sensor.py +++ b/homeassistant/components/wirelesstag/sensor.py @@ -109,7 +109,7 @@ class WirelessTagSensor(WirelessTagBaseSensor, SensorEntity): f"sensor.{WIRELESSTAG_DOMAIN}_{self.underscored_name}_{self._sensor_type}" ) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" self.async_on_remove( async_dispatcher_connect( diff --git a/homeassistant/components/wirelesstag/switch.py b/homeassistant/components/wirelesstag/switch.py index a6e4a85559c..9829cffd2b5 100644 --- a/homeassistant/components/wirelesstag/switch.py +++ b/homeassistant/components/wirelesstag/switch.py @@ -1,6 +1,8 @@ """Switch implementation for Wireless Sensor Tags (wirelesstag.net).""" from __future__ import annotations +from typing import Any + import voluptuous as vol from homeassistant.components.switch import ( @@ -81,11 +83,11 @@ class WirelessTagSwitch(WirelessTagBaseSensor, SwitchEntity): self.entity_description = description self._name = f"{self._tag.name} {description.name}" - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn on the switch.""" self._api.arm(self) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn on the switch.""" self._api.disarm(self) diff --git a/homeassistant/components/worldtidesinfo/sensor.py b/homeassistant/components/worldtidesinfo/sensor.py index 2433a9f678e..776f6c6e20f 100644 --- a/homeassistant/components/worldtidesinfo/sensor.py +++ b/homeassistant/components/worldtidesinfo/sensor.py @@ -110,7 +110,7 @@ class WorldTidesInfoSensor(SensorEntity): return None return None - def update(self): + def update(self) -> None: """Get the latest data from WorldTidesInfo API.""" start = int(time.time()) resource = ( diff --git a/homeassistant/components/worxlandroid/sensor.py b/homeassistant/components/worxlandroid/sensor.py index 47617dc609e..d15a33e9e17 100644 --- a/homeassistant/components/worxlandroid/sensor.py +++ b/homeassistant/components/worxlandroid/sensor.py @@ -89,7 +89,7 @@ class WorxLandroidSensor(SensorEntity): return PERCENTAGE return None - async def async_update(self): + async def async_update(self) -> None: """Update the sensor data from the mower.""" connection_error = False diff --git a/homeassistant/components/ws66i/media_player.py b/homeassistant/components/ws66i/media_player.py index 7cd897e9c1a..05fd4133885 100644 --- a/homeassistant/components/ws66i/media_player.py +++ b/homeassistant/components/ws66i/media_player.py @@ -106,7 +106,7 @@ class Ws66iZone(CoordinatorEntity[Ws66iDataUpdateCoordinator], MediaPlayerEntity self._set_attrs_from_status() self.async_write_ha_state() - async def async_select_source(self, source): + async def async_select_source(self, source: str) -> None: """Set input source.""" idx = self._ws66i_data.sources.name_id[source] await self.hass.async_add_executor_job( @@ -115,7 +115,7 @@ class Ws66iZone(CoordinatorEntity[Ws66iDataUpdateCoordinator], MediaPlayerEntity self._status.source = idx self._async_update_attrs_write_ha_state() - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Turn the media player on.""" await self.hass.async_add_executor_job( self._ws66i.set_power, self._zone_id, True @@ -123,7 +123,7 @@ class Ws66iZone(CoordinatorEntity[Ws66iDataUpdateCoordinator], MediaPlayerEntity self._status.power = True self._async_update_attrs_write_ha_state() - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Turn the media player off.""" await self.hass.async_add_executor_job( self._ws66i.set_power, self._zone_id, False @@ -131,7 +131,7 @@ class Ws66iZone(CoordinatorEntity[Ws66iDataUpdateCoordinator], MediaPlayerEntity self._status.power = False self._async_update_attrs_write_ha_state() - async def async_mute_volume(self, mute): + async def async_mute_volume(self, mute: bool) -> None: """Mute (true) or unmute (false) media player.""" await self.hass.async_add_executor_job( self._ws66i.set_mute, self._zone_id, mute @@ -139,19 +139,19 @@ class Ws66iZone(CoordinatorEntity[Ws66iDataUpdateCoordinator], MediaPlayerEntity self._status.mute = bool(mute) self._async_update_attrs_write_ha_state() - async def async_set_volume_level(self, volume): + async def async_set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" await self.hass.async_add_executor_job(self._set_volume, int(volume * MAX_VOL)) self._async_update_attrs_write_ha_state() - async def async_volume_up(self): + async def async_volume_up(self) -> None: """Volume up the media player.""" await self.hass.async_add_executor_job( self._set_volume, min(self._status.volume + 1, MAX_VOL) ) self._async_update_attrs_write_ha_state() - async def async_volume_down(self): + async def async_volume_down(self) -> None: """Volume down media player.""" await self.hass.async_add_executor_job( self._set_volume, max(self._status.volume - 1, 0) diff --git a/homeassistant/components/wsdot/sensor.py b/homeassistant/components/wsdot/sensor.py index 76d1b92b476..b8c362e56a5 100644 --- a/homeassistant/components/wsdot/sensor.py +++ b/homeassistant/components/wsdot/sensor.py @@ -113,7 +113,7 @@ class WashingtonStateTravelTimeSensor(WashingtonStateTransportSensor): self._travel_time_id = travel_time_id WashingtonStateTransportSensor.__init__(self, name, access_code) - def update(self): + def update(self) -> None: """Get the latest data from WSDOT.""" params = { ATTR_ACCESS_CODE: self._access_code, From 856318b1371b312dfde3355548fab3525ac1ab07 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 6 Sep 2022 14:00:09 +0200 Subject: [PATCH 145/955] Improve entity type hints [x] (#77887) --- homeassistant/components/xbox/media_player.py | 23 +++++++------ homeassistant/components/xbox_live/sensor.py | 4 +-- .../components/xiaomi_aqara/binary_sensor.py | 2 +- .../components/xiaomi_aqara/switch.py | 7 ++-- homeassistant/components/xiaomi_miio/light.py | 2 +- .../components/xiaomi_miio/remote.py | 5 +-- .../components/xiaomi_miio/sensor.py | 4 +-- .../components/xiaomi_miio/switch.py | 33 ++++++++++--------- .../components/xiaomi_miio/vacuum.py | 5 ++- .../components/xiaomi_tv/media_player.py | 8 ++--- homeassistant/components/xs1/climate.py | 8 +++-- homeassistant/components/xs1/switch.py | 6 ++-- 12 files changed, 60 insertions(+), 47 deletions(-) diff --git a/homeassistant/components/xbox/media_player.py b/homeassistant/components/xbox/media_player.py index ae361d6806e..8209136fa23 100644 --- a/homeassistant/components/xbox/media_player.py +++ b/homeassistant/components/xbox/media_player.py @@ -2,6 +2,7 @@ from __future__ import annotations import re +from typing import Any from xbox.webapi.api.client import XboxLiveClient from xbox.webapi.api.provider.catalog.models import Image @@ -154,42 +155,42 @@ class XboxMediaPlayer(CoordinatorEntity[XboxUpdateCoordinator], MediaPlayerEntit """If the image url is remotely accessible.""" return True - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Turn the media player on.""" await self.client.smartglass.wake_up(self._console.id) - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Turn the media player off.""" await self.client.smartglass.turn_off(self._console.id) - async def async_mute_volume(self, mute): + async def async_mute_volume(self, mute: bool) -> None: """Mute the volume.""" if mute: await self.client.smartglass.mute(self._console.id) else: await self.client.smartglass.unmute(self._console.id) - async def async_volume_up(self): + async def async_volume_up(self) -> None: """Turn volume up for media player.""" await self.client.smartglass.volume(self._console.id, VolumeDirection.Up) - async def async_volume_down(self): + async def async_volume_down(self) -> None: """Turn volume down for media player.""" await self.client.smartglass.volume(self._console.id, VolumeDirection.Down) - async def async_media_play(self): + async def async_media_play(self) -> None: """Send play command.""" await self.client.smartglass.play(self._console.id) - async def async_media_pause(self): + async def async_media_pause(self) -> None: """Send pause command.""" await self.client.smartglass.pause(self._console.id) - async def async_media_previous_track(self): + async def async_media_previous_track(self) -> None: """Send previous track command.""" await self.client.smartglass.previous(self._console.id) - async def async_media_next_track(self): + async def async_media_next_track(self) -> None: """Send next track command.""" await self.client.smartglass.next(self._console.id) @@ -203,7 +204,9 @@ class XboxMediaPlayer(CoordinatorEntity[XboxUpdateCoordinator], MediaPlayerEntit media_content_id, ) - async def async_play_media(self, media_type, media_id, **kwargs): + async def async_play_media( + self, media_type: str, media_id: str, **kwargs: Any + ) -> None: """Launch an app on the Xbox.""" if media_id == "Home": await self.client.smartglass.go_home(self._console.id) diff --git a/homeassistant/components/xbox_live/sensor.py b/homeassistant/components/xbox_live/sensor.py index b1500e6cedc..3081f334821 100644 --- a/homeassistant/components/xbox_live/sensor.py +++ b/homeassistant/components/xbox_live/sensor.py @@ -128,7 +128,7 @@ class XboxSensor(SensorEntity): """Return the icon to use in the frontend.""" return ICON - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Start custom polling.""" @callback @@ -138,7 +138,7 @@ class XboxSensor(SensorEntity): async_track_time_interval(self.hass, async_update, self._interval) - def update(self): + def update(self) -> None: """Update state data from Xbox API.""" presence = self._api.gamer(gamertag="", xuid=self._xuid).get("presence") _LOGGER.debug("User presence: %s", presence) diff --git a/homeassistant/components/xiaomi_aqara/binary_sensor.py b/homeassistant/components/xiaomi_aqara/binary_sensor.py index 44c17b634cb..9ed79bc8250 100644 --- a/homeassistant/components/xiaomi_aqara/binary_sensor.py +++ b/homeassistant/components/xiaomi_aqara/binary_sensor.py @@ -153,7 +153,7 @@ class XiaomiBinarySensor(XiaomiDevice, BinarySensorEntity): """Return the class of binary sensor.""" return self._device_class - def update(self): + def update(self) -> None: """Update the sensor state.""" _LOGGER.debug("Updating xiaomi sensor (%s) by polling", self._sid) self._get_from_hub(self._sid) diff --git a/homeassistant/components/xiaomi_aqara/switch.py b/homeassistant/components/xiaomi_aqara/switch.py index 39bf637256f..0ea6a65f68e 100644 --- a/homeassistant/components/xiaomi_aqara/switch.py +++ b/homeassistant/components/xiaomi_aqara/switch.py @@ -1,5 +1,6 @@ """Support for Xiaomi Aqara binary sensors.""" import logging +from typing import Any from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry @@ -179,13 +180,13 @@ class XiaomiGenericSwitch(XiaomiDevice, SwitchEntity): attrs.update(super().extra_state_attributes) return attrs - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" if self._write_to_hub(self._sid, **{self._data_key: "on"}): self._state = True self.schedule_update_ha_state() - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" if self._write_to_hub(self._sid, **{self._data_key: "off"}): self._state = False @@ -216,7 +217,7 @@ class XiaomiGenericSwitch(XiaomiDevice, SwitchEntity): self._state = state return True - def update(self): + def update(self) -> None: """Get data from hub.""" _LOGGER.debug("Update data from hub: %s", self._name) self._get_from_hub(self._sid) diff --git a/homeassistant/components/xiaomi_miio/light.py b/homeassistant/components/xiaomi_miio/light.py index 2a7fce01442..49554311b91 100644 --- a/homeassistant/components/xiaomi_miio/light.py +++ b/homeassistant/components/xiaomi_miio/light.py @@ -1018,7 +1018,7 @@ class XiaomiGatewayLight(LightEntity): self._gateway.light.set_rgb(0, self._rgb) self.schedule_update_ha_state() - async def async_update(self): + async def async_update(self) -> None: """Fetch state from the device.""" try: state_dict = await self.hass.async_add_executor_job( diff --git a/homeassistant/components/xiaomi_miio/remote.py b/homeassistant/components/xiaomi_miio/remote.py index 39a9c984a8c..ffae02a2ee4 100644 --- a/homeassistant/components/xiaomi_miio/remote.py +++ b/homeassistant/components/xiaomi_miio/remote.py @@ -5,6 +5,7 @@ import asyncio from datetime import timedelta import logging import time +from typing import Any from miio import ChuangmiIr, DeviceException import voluptuous as vol @@ -227,14 +228,14 @@ class XiaomiMiioRemote(RemoteEntity): except DeviceException: return False - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" _LOGGER.error( "Device does not support turn_on, " "please use 'remote.send_command' to send commands" ) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" _LOGGER.error( "Device does not support turn_off, " diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index 819d95f208c..472339a4103 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -892,7 +892,7 @@ class XiaomiAirQualityMonitor(XiaomiMiioEntity, SensorEntity): """Return the state attributes of the device.""" return self._state_attrs - async def async_update(self): + async def async_update(self) -> None: """Fetch state from the miio device.""" try: state = await self.hass.async_add_executor_job(self._device.status) @@ -958,7 +958,7 @@ class XiaomiGatewayIlluminanceSensor(SensorEntity): """Return the state of the device.""" return self._state - async def async_update(self): + async def async_update(self) -> None: """Fetch state from the device.""" try: self._state = await self.hass.async_add_executor_job( diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index 577f9af837f..dbe783c6a87 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -5,6 +5,7 @@ import asyncio from dataclasses import dataclass from functools import partial import logging +from typing import Any from miio import AirConditioningCompanionV3, ChuangmiPlug, DeviceException, PowerStrip from miio.powerstrip import PowerMode @@ -527,7 +528,7 @@ class XiaomiGenericCoordinatedSwitch(XiaomiCoordinatedMiioEntity, SwitchEntity): self.async_write_ha_state() @property - def available(self): + def available(self) -> bool: """Return true when state is known.""" if ( super().available @@ -537,7 +538,7 @@ class XiaomiGenericCoordinatedSwitch(XiaomiCoordinatedMiioEntity, SwitchEntity): return False return super().available - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Turn on an option of the miio device.""" method = getattr(self, self.entity_description.method_on) if await method(): @@ -545,7 +546,7 @@ class XiaomiGenericCoordinatedSwitch(XiaomiCoordinatedMiioEntity, SwitchEntity): self._attr_is_on = True self.async_write_ha_state() - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn off an option of the miio device.""" method = getattr(self, self.entity_description.method_off) if await method(): @@ -748,15 +749,15 @@ class XiaomiGatewaySwitch(XiaomiGatewayDevice, SwitchEntity): """Return true if switch is on.""" return self._sub_device.status[self._data_key] == "on" - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" await self.hass.async_add_executor_job(self._sub_device.on, self._channel) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" await self.hass.async_add_executor_job(self._sub_device.off, self._channel) - async def async_toggle(self, **kwargs): + async def async_toggle(self, **kwargs: Any) -> None: """Toggle the switch.""" await self.hass.async_add_executor_job(self._sub_device.toggle, self._channel) @@ -816,7 +817,7 @@ class XiaomiPlugGenericSwitch(XiaomiMiioEntity, SwitchEntity): return False - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the plug on.""" result = await self._try_command("Turning the plug on failed", self._device.on) @@ -824,7 +825,7 @@ class XiaomiPlugGenericSwitch(XiaomiMiioEntity, SwitchEntity): self._state = True self._skip_update = True - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the plug off.""" result = await self._try_command( "Turning the plug off failed", self._device.off @@ -834,7 +835,7 @@ class XiaomiPlugGenericSwitch(XiaomiMiioEntity, SwitchEntity): self._state = False self._skip_update = True - async def async_update(self): + async def async_update(self) -> None: """Fetch state from the device.""" # On state change the device doesn't provide the new state immediately. if self._skip_update: @@ -907,7 +908,7 @@ class XiaomiPowerStripSwitch(XiaomiPlugGenericSwitch): if self._device_features & FEATURE_SET_POWER_PRICE == 1: self._state_attrs[ATTR_POWER_PRICE] = None - async def async_update(self): + async def async_update(self) -> None: """Fetch state from the device.""" # On state change the device doesn't provide the new state immediately. if self._skip_update: @@ -972,7 +973,7 @@ class ChuangMiPlugSwitch(XiaomiPlugGenericSwitch): if self._channel_usb is False: self._state_attrs[ATTR_LOAD_POWER] = None - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn a channel on.""" if self._channel_usb: result = await self._try_command( @@ -987,7 +988,7 @@ class ChuangMiPlugSwitch(XiaomiPlugGenericSwitch): self._state = True self._skip_update = True - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn a channel off.""" if self._channel_usb: result = await self._try_command( @@ -1002,7 +1003,7 @@ class ChuangMiPlugSwitch(XiaomiPlugGenericSwitch): self._state = False self._skip_update = True - async def async_update(self): + async def async_update(self) -> None: """Fetch state from the device.""" # On state change the device doesn't provide the new state immediately. if self._skip_update: @@ -1042,7 +1043,7 @@ class XiaomiAirConditioningCompanionSwitch(XiaomiPlugGenericSwitch): self._state_attrs.update({ATTR_TEMPERATURE: None, ATTR_LOAD_POWER: None}) - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the socket on.""" result = await self._try_command( "Turning the socket on failed", self._device.socket_on @@ -1052,7 +1053,7 @@ class XiaomiAirConditioningCompanionSwitch(XiaomiPlugGenericSwitch): self._state = True self._skip_update = True - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the socket off.""" result = await self._try_command( "Turning the socket off failed", self._device.socket_off @@ -1062,7 +1063,7 @@ class XiaomiAirConditioningCompanionSwitch(XiaomiPlugGenericSwitch): self._state = False self._skip_update = True - async def async_update(self): + async def async_update(self) -> None: """Fetch state from the device.""" # On state change the device doesn't provide the new state immediately. if self._skip_update: diff --git a/homeassistant/components/xiaomi_miio/vacuum.py b/homeassistant/components/xiaomi_miio/vacuum.py index 7df38109d16..19c38ac0a82 100644 --- a/homeassistant/components/xiaomi_miio/vacuum.py +++ b/homeassistant/components/xiaomi_miio/vacuum.py @@ -329,7 +329,10 @@ class MiroboVacuum( await self._try_command("Unable to locate the botvac: %s", self._device.find) async def async_send_command( - self, command: str, params: dict | list | None = None, **kwargs: Any + self, + command: str, + params: dict[str, Any] | list[Any] | None = None, + **kwargs: Any, ) -> None: """Send raw command.""" await self._try_command( diff --git a/homeassistant/components/xiaomi_tv/media_player.py b/homeassistant/components/xiaomi_tv/media_player.py index a808d1e3b8e..df174b6b0f0 100644 --- a/homeassistant/components/xiaomi_tv/media_player.py +++ b/homeassistant/components/xiaomi_tv/media_player.py @@ -87,7 +87,7 @@ class XiaomiTV(MediaPlayerEntity): """Indicate that state is assumed.""" return True - def turn_off(self): + def turn_off(self) -> None: """ Instruct the TV to turn sleep. @@ -100,17 +100,17 @@ class XiaomiTV(MediaPlayerEntity): self._state = STATE_OFF - def turn_on(self): + def turn_on(self) -> None: """Wake the TV back up from sleep.""" if self._state != STATE_ON: self._tv.wake() self._state = STATE_ON - def volume_up(self): + def volume_up(self) -> None: """Increase volume by one.""" self._tv.volume_up() - def volume_down(self): + def volume_down(self) -> None: """Decrease volume by one.""" self._tv.volume_down() diff --git a/homeassistant/components/xs1/climate.py b/homeassistant/components/xs1/climate.py index bf2ab898112..f74950437df 100644 --- a/homeassistant/components/xs1/climate.py +++ b/homeassistant/components/xs1/climate.py @@ -1,6 +1,8 @@ """Support for XS1 climate devices.""" from __future__ import annotations +from typing import Any + from xs1_api_client.api_constants import ActuatorType from homeassistant.components.climate import ClimateEntity @@ -69,7 +71,7 @@ class XS1ThermostatEntity(XS1DeviceEntity, ClimateEntity): return self.sensor.value() @property - def temperature_unit(self): + def temperature_unit(self) -> str: """Return the unit of measurement used by the platform.""" return self.device.unit() @@ -88,7 +90,7 @@ class XS1ThermostatEntity(XS1DeviceEntity, ClimateEntity): """Return the maximum temperature.""" return MAX_TEMP - def set_temperature(self, **kwargs): + def set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" temp = kwargs.get(ATTR_TEMPERATURE) @@ -100,7 +102,7 @@ class XS1ThermostatEntity(XS1DeviceEntity, ClimateEntity): def set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new target hvac mode.""" - async def async_update(self): + async def async_update(self) -> None: """Also update the sensor when available.""" await super().async_update() if self.sensor is not None: diff --git a/homeassistant/components/xs1/switch.py b/homeassistant/components/xs1/switch.py index 0f40678986b..0b231a0303c 100644 --- a/homeassistant/components/xs1/switch.py +++ b/homeassistant/components/xs1/switch.py @@ -1,6 +1,8 @@ """Support for XS1 switches.""" from __future__ import annotations +from typing import Any + from xs1_api_client.api_constants import ActuatorType from homeassistant.components.switch import SwitchEntity @@ -43,10 +45,10 @@ class XS1SwitchEntity(XS1DeviceEntity, SwitchEntity): """Return true if switch is on.""" return self.device.value() == 100 - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" self.device.turn_on() - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" self.device.turn_off() From 23052dc7b57de3193d56db9885b68e61d618bf87 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 6 Sep 2022 14:00:25 +0200 Subject: [PATCH 146/955] Improve entity type hints [y] (#77888) --- .../components/yamaha/media_player.py | 27 ++++++------ .../yamaha_musiccast/media_player.py | 43 ++++++++++--------- .../components/yeelight/binary_sensor.py | 2 +- homeassistant/components/yolink/climate.py | 2 +- 4 files changed, 39 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/yamaha/media_player.py b/homeassistant/components/yamaha/media_player.py index 1e48fceba07..9a2b1eafbc2 100644 --- a/homeassistant/components/yamaha/media_player.py +++ b/homeassistant/components/yamaha/media_player.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any import requests import rxv @@ -215,7 +216,7 @@ class YamahaDevice(MediaPlayerEntity): self._name = name self._zone = receiver.zone - def update(self): + def update(self) -> None: """Get the latest details from the device.""" try: self._play_status = self.receiver.play_status() @@ -335,42 +336,42 @@ class YamahaDevice(MediaPlayerEntity): supported_features |= feature return supported_features - def turn_off(self): + def turn_off(self) -> None: """Turn off media player.""" self.receiver.on = False - def set_volume_level(self, volume): + def set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" receiver_vol = 100 - (volume * 100) negative_receiver_vol = -receiver_vol self.receiver.volume = negative_receiver_vol - def mute_volume(self, mute): + def mute_volume(self, mute: bool) -> None: """Mute (true) or unmute (false) media player.""" self.receiver.mute = mute - def turn_on(self): + def turn_on(self) -> None: """Turn the media player on.""" self.receiver.on = True self._volume = (self.receiver.volume / 100) + 1 - def media_play(self): + def media_play(self) -> None: """Send play command.""" self._call_playback_function(self.receiver.play, "play") - def media_pause(self): + def media_pause(self) -> None: """Send pause command.""" self._call_playback_function(self.receiver.pause, "pause") - def media_stop(self): + def media_stop(self) -> None: """Send stop command.""" self._call_playback_function(self.receiver.stop, "stop") - def media_previous_track(self): + def media_previous_track(self) -> None: """Send previous track command.""" self._call_playback_function(self.receiver.previous, "previous track") - def media_next_track(self): + def media_next_track(self) -> None: """Send next track command.""" self._call_playback_function(self.receiver.next, "next track") @@ -380,11 +381,11 @@ class YamahaDevice(MediaPlayerEntity): except rxv.exceptions.ResponseException: _LOGGER.warning("Failed to execute %s on %s", function_text, self._name) - def select_source(self, source): + def select_source(self, source: str) -> None: """Select input source.""" self.receiver.input = self._reverse_mapping.get(source, source) - def play_media(self, media_type, media_id, **kwargs): + def play_media(self, media_type: str, media_id: str, **kwargs: Any) -> None: """Play media from an ID. This exposes a pass through for various input sources in the @@ -421,7 +422,7 @@ class YamahaDevice(MediaPlayerEntity): except AssertionError: _LOGGER.warning("Scene '%s' does not exist!", scene) - def select_sound_mode(self, sound_mode): + def select_sound_mode(self, sound_mode: str) -> None: """Set Sound Mode for Receiver..""" self.receiver.surround_program = sound_mode diff --git a/homeassistant/components/yamaha_musiccast/media_player.py b/homeassistant/components/yamaha_musiccast/media_player.py index 123e62ab3a2..9251728beda 100644 --- a/homeassistant/components/yamaha_musiccast/media_player.py +++ b/homeassistant/components/yamaha_musiccast/media_player.py @@ -3,6 +3,7 @@ from __future__ import annotations import contextlib import logging +from typing import Any from aiomusiccast import MusicCastGroupException, MusicCastMediaContent from aiomusiccast.features import ZoneFeature @@ -100,7 +101,7 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): self._cur_track = 0 self._repeat = REPEAT_MODE_OFF - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Run when this Entity has been added to HA.""" await super().async_added_to_hass() self.coordinator.entities.append(self) @@ -112,7 +113,7 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): self.coordinator.async_add_listener(self.async_schedule_check_client_list) ) - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Entity being removed from hass.""" await super().async_will_remove_from_hass() self.coordinator.entities.remove(self) @@ -205,36 +206,36 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): """Return the unique ID for this media_player.""" return f"{self.coordinator.data.device_id}_{self._zone_id}" - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Turn the media player on.""" await self.coordinator.musiccast.turn_on(self._zone_id) self.async_write_ha_state() - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Turn the media player off.""" await self.coordinator.musiccast.turn_off(self._zone_id) self.async_write_ha_state() - async def async_mute_volume(self, mute): + async def async_mute_volume(self, mute: bool) -> None: """Mute the volume.""" await self.coordinator.musiccast.mute_volume(self._zone_id, mute) self.async_write_ha_state() - async def async_set_volume_level(self, volume): + async def async_set_volume_level(self, volume: float) -> None: """Set the volume level, range 0..1.""" await self.coordinator.musiccast.set_volume_level(self._zone_id, volume) self.async_write_ha_state() - async def async_volume_up(self): + async def async_volume_up(self) -> None: """Turn volume up for media player.""" await self.coordinator.musiccast.volume_up(self._zone_id) - async def async_volume_down(self): + async def async_volume_down(self) -> None: """Turn volume down for media player.""" await self.coordinator.musiccast.volume_down(self._zone_id) - async def async_media_play(self): + async def async_media_play(self) -> None: """Send play command.""" if self._is_netusb: await self.coordinator.musiccast.netusb_play() @@ -243,7 +244,7 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): "Service play is not supported for non NetUSB sources." ) - async def async_media_pause(self): + async def async_media_pause(self) -> None: """Send pause command.""" if self._is_netusb: await self.coordinator.musiccast.netusb_pause() @@ -252,7 +253,7 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): "Service pause is not supported for non NetUSB sources." ) - async def async_media_stop(self): + async def async_media_stop(self) -> None: """Send stop command.""" if self._is_netusb: await self.coordinator.musiccast.netusb_stop() @@ -261,7 +262,7 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): "Service stop is not supported for non NetUSB sources." ) - async def async_set_shuffle(self, shuffle): + async def async_set_shuffle(self, shuffle: bool) -> None: """Enable/disable shuffle mode.""" if self._is_netusb: await self.coordinator.musiccast.netusb_shuffle(shuffle) @@ -270,7 +271,9 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): "Service shuffle is not supported for non NetUSB sources." ) - async def async_play_media(self, media_type: str, media_id: str, **kwargs) -> None: + async def async_play_media( + self, media_type: str, media_id: str, **kwargs: Any + ) -> None: """Play media.""" if media_source.is_media_source_id(media_id): play_item = await media_source.async_resolve_media( @@ -384,7 +387,7 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): return overview - async def async_select_sound_mode(self, sound_mode): + async def async_select_sound_mode(self, sound_mode: str) -> None: """Select sound mode.""" await self.coordinator.musiccast.select_sound_mode(self._zone_id, sound_mode) @@ -461,7 +464,7 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): return supported_features - async def async_media_previous_track(self): + async def async_media_previous_track(self) -> None: """Send previous track command.""" if self._is_netusb: await self.coordinator.musiccast.netusb_previous_track() @@ -472,7 +475,7 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): "Service previous track is not supported for non NetUSB or Tuner sources." ) - async def async_media_next_track(self): + async def async_media_next_track(self) -> None: """Send next track command.""" if self._is_netusb: await self.coordinator.musiccast.netusb_next_track() @@ -483,7 +486,7 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): "Service next track is not supported for non NetUSB or Tuner sources." ) - async def async_set_repeat(self, repeat): + async def async_set_repeat(self, repeat: str) -> None: """Enable/disable repeat mode.""" if self._is_netusb: await self.coordinator.musiccast.netusb_repeat( @@ -494,7 +497,7 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): "Service set repeat is not supported for non NetUSB sources." ) - async def async_select_source(self, source): + async def async_select_source(self, source: str) -> None: """Select input source.""" await self.coordinator.musiccast.select_source(self._zone_id, source) @@ -685,7 +688,7 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): # Services - async def async_join_players(self, group_members): + async def async_join_players(self, group_members: list[str]) -> None: """Add all clients given in entities to the group of the server. Creates a new group if necessary. Used for join service. @@ -752,7 +755,7 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): await self.update_all_mc_entities(True) - async def async_unjoin_player(self): + async def async_unjoin_player(self) -> None: """Leave the group. Stops the distribution if device is server. Used for unjoin service. diff --git a/homeassistant/components/yeelight/binary_sensor.py b/homeassistant/components/yeelight/binary_sensor.py index cd312715b9d..f78b4e1401d 100644 --- a/homeassistant/components/yeelight/binary_sensor.py +++ b/homeassistant/components/yeelight/binary_sensor.py @@ -28,7 +28,7 @@ async def async_setup_entry( class YeelightNightlightModeSensor(YeelightEntity, BinarySensorEntity): """Representation of a Yeelight nightlight mode sensor.""" - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" self.async_on_remove( async_dispatcher_connect( diff --git a/homeassistant/components/yolink/climate.py b/homeassistant/components/yolink/climate.py index 1f877571d94..84581c29a8d 100644 --- a/homeassistant/components/yolink/climate.py +++ b/homeassistant/components/yolink/climate.py @@ -115,7 +115,7 @@ class YoLinkClimateEntity(YoLinkEntity, ClimateEntity): self._attr_fan_mode = fan_mode self.async_write_ha_state() - async def async_set_temperature(self, **kwargs) -> None: + async def async_set_temperature(self, **kwargs: Any) -> None: """Set temperature.""" target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW) target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH) From 3a0eae3986317684ed111a6bafc416f931babd5b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 6 Sep 2022 14:01:09 +0200 Subject: [PATCH 147/955] Improve entity type hints [z] (#77890) --- homeassistant/components/zabbix/sensor.py | 2 +- homeassistant/components/zamg/sensor.py | 2 +- homeassistant/components/zamg/weather.py | 2 +- homeassistant/components/zha/binary_sensor.py | 6 +++--- homeassistant/components/zha/climate.py | 9 +++++---- homeassistant/components/zha/device_tracker.py | 4 ++-- homeassistant/components/zha/fan.py | 4 ++-- homeassistant/components/zha/light.py | 2 +- homeassistant/components/zhong_hong/climate.py | 11 ++++++----- .../components/ziggo_mediabox_xl/media_player.py | 16 ++++++++-------- .../components/zoneminder/binary_sensor.py | 2 +- homeassistant/components/zoneminder/camera.py | 2 +- homeassistant/components/zoneminder/sensor.py | 6 +++--- homeassistant/components/zoneminder/switch.py | 7 ++++--- homeassistant/components/zwave_me/climate.py | 4 +++- 15 files changed, 42 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/zabbix/sensor.py b/homeassistant/components/zabbix/sensor.py index 3e72e4d71f1..a5172d5ef95 100644 --- a/homeassistant/components/zabbix/sensor.py +++ b/homeassistant/components/zabbix/sensor.py @@ -117,7 +117,7 @@ class ZabbixTriggerCountSensor(SensorEntity): output="extend", only_true=1, monitored=1, filter={"value": 1} ) - def update(self): + def update(self) -> None: """Update the sensor.""" _LOGGER.debug("Updating ZabbixTriggerCountSensor: %s", str(self._name)) triggers = self._call_zabbix_api() diff --git a/homeassistant/components/zamg/sensor.py b/homeassistant/components/zamg/sensor.py index e8d5f745cce..73902b1982f 100644 --- a/homeassistant/components/zamg/sensor.py +++ b/homeassistant/components/zamg/sensor.py @@ -264,7 +264,7 @@ class ZamgSensor(SensorEntity): ATTR_UPDATED: self.probe.last_update.isoformat(), } - def update(self): + def update(self) -> None: """Delegate update to probe.""" self.probe.update() diff --git a/homeassistant/components/zamg/weather.py b/homeassistant/components/zamg/weather.py index 6910955fcf7..eb2992df64f 100644 --- a/homeassistant/components/zamg/weather.py +++ b/homeassistant/components/zamg/weather.py @@ -139,6 +139,6 @@ class ZamgWeather(WeatherEntity): """Return the wind bearing.""" return self.zamg_data.get_data(ATTR_WEATHER_WIND_BEARING) - def update(self): + def update(self) -> None: """Update current conditions.""" self.zamg_data.update() diff --git a/homeassistant/components/zha/binary_sensor.py b/homeassistant/components/zha/binary_sensor.py index 23d26e4b13f..abddfda3358 100644 --- a/homeassistant/components/zha/binary_sensor.py +++ b/homeassistant/components/zha/binary_sensor.py @@ -69,7 +69,7 @@ class BinarySensor(ZhaEntity, BinarySensorEntity): super().__init__(unique_id, zha_device, channels, **kwargs) self._channel = channels[0] - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Run when about to be added to hass.""" await super().async_added_to_hass() self.async_accept_signal( @@ -97,7 +97,7 @@ class BinarySensor(ZhaEntity, BinarySensorEntity): self._state = bool(value) self.async_write_ha_state() - async def async_update(self): + async def async_update(self) -> None: """Attempt to retrieve on off state from the binary sensor.""" await super().async_update() attribute = getattr(self._channel, "value_attribute", "on_off") @@ -167,7 +167,7 @@ class IASZone(BinarySensor): """Return device class from component DEVICE_CLASSES.""" return CLASS_MAPPING.get(self._channel.cluster.get("zone_type")) - async def async_update(self): + async def async_update(self) -> None: """Attempt to retrieve on off state from the binary sensor.""" await super().async_update() value = await self._channel.get_attribute_value("zone_status") diff --git a/homeassistant/components/zha/climate.py b/homeassistant/components/zha/climate.py index 573b3df44fa..4585d41d44d 100644 --- a/homeassistant/components/zha/climate.py +++ b/homeassistant/components/zha/climate.py @@ -9,6 +9,7 @@ from __future__ import annotations from datetime import datetime, timedelta import functools from random import randint +from typing import Any from zigpy.zcl.clusters.hvac import Fan as F, Thermostat as T @@ -276,7 +277,7 @@ class Thermostat(ZhaEntity, ClimateEntity): return self._presets @property - def supported_features(self): + def supported_features(self) -> int: """Return the list of supported features.""" features = self._supported_flags if HVACMode.HEAT_COOL in self.hvac_modes: @@ -358,7 +359,7 @@ class Thermostat(ZhaEntity, ClimateEntity): return self.DEFAULT_MIN_TEMP return round(min(temps) / ZCL_TEMP, 1) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Run when about to be added to hass.""" await super().async_added_to_hass() self.async_accept_signal( @@ -427,7 +428,7 @@ class Thermostat(ZhaEntity, ClimateEntity): self._preset = preset_mode self.async_write_ha_state() - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" low_temp = kwargs.get(ATTR_TARGET_TEMP_LOW) high_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH) @@ -533,7 +534,7 @@ class SinopeTechnologiesThermostat(Thermostat): ) ) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Run when about to be added to Hass.""" await super().async_added_to_hass() async_track_time_interval( diff --git a/homeassistant/components/zha/device_tracker.py b/homeassistant/components/zha/device_tracker.py index 5a20273d085..8ecc85a4e56 100644 --- a/homeassistant/components/zha/device_tracker.py +++ b/homeassistant/components/zha/device_tracker.py @@ -59,7 +59,7 @@ class ZHADeviceScannerEntity(ScannerEntity, ZhaEntity): self._keepalive_interval = 60 self._battery_level = None - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Run when about to be added to hass.""" await super().async_added_to_hass() if self._battery_channel: @@ -69,7 +69,7 @@ class ZHADeviceScannerEntity(ScannerEntity, ZhaEntity): self.async_battery_percentage_remaining_updated, ) - async def async_update(self): + async def async_update(self) -> None: """Handle polling.""" if self.zha_device.last_seen is None: self._connected = False diff --git a/homeassistant/components/zha/fan.py b/homeassistant/components/zha/fan.py index c38a71e3249..6c1d2bf2bee 100644 --- a/homeassistant/components/zha/fan.py +++ b/homeassistant/components/zha/fan.py @@ -260,7 +260,7 @@ class IkeaFan(BaseFan, ZhaEntity): super().__init__(unique_id, zha_device, channels, **kwargs) self._fan_channel = self.cluster_channels.get("ikea_airpurifier") - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Run when about to be added to hass.""" await super().async_added_to_hass() self.async_accept_signal( @@ -317,7 +317,7 @@ class IkeaFan(BaseFan, ZhaEntity): ] await self.async_set_percentage(percentage) - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" await self.async_set_percentage(0) diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index dbb250a5d33..ea46c0c49f1 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -168,7 +168,7 @@ class BaseLight(LogMixin, light.LightEntity): self._attr_brightness = value self.async_write_ha_state() - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" transition = kwargs.get(light.ATTR_TRANSITION) duration = ( diff --git a/homeassistant/components/zhong_hong/climate.py b/homeassistant/components/zhong_hong/climate.py index 0877e19834f..7566f5166e6 100644 --- a/homeassistant/components/zhong_hong/climate.py +++ b/homeassistant/components/zhong_hong/climate.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any import voluptuous as vol from zhong_hong_hvac.hub import ZhongHongGateway @@ -141,7 +142,7 @@ class ZhongHongClimate(ClimateEntity): self._current_fan_mode = None self.is_initialized = False - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" self._device.register_update_callback(self._after_update) self.is_initialized = True @@ -219,15 +220,15 @@ class ZhongHongClimate(ClimateEntity): """Return the maximum temperature.""" return self._device.max_temp - def turn_on(self): + def turn_on(self) -> None: """Turn on ac.""" return self._device.turn_on() - def turn_off(self): + def turn_off(self) -> None: """Turn off ac.""" return self._device.turn_off() - def set_temperature(self, **kwargs): + def set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" if (temperature := kwargs.get(ATTR_TEMPERATURE)) is not None: self._device.set_temperature(temperature) @@ -247,6 +248,6 @@ class ZhongHongClimate(ClimateEntity): self._device.set_operation_mode(hvac_mode.upper()) - def set_fan_mode(self, fan_mode): + def set_fan_mode(self, fan_mode: str) -> None: """Set new target fan mode.""" self._device.set_fan_mode(fan_mode) diff --git a/homeassistant/components/ziggo_mediabox_xl/media_player.py b/homeassistant/components/ziggo_mediabox_xl/media_player.py index 42c86020d2e..fd2ca59013a 100644 --- a/homeassistant/components/ziggo_mediabox_xl/media_player.py +++ b/homeassistant/components/ziggo_mediabox_xl/media_player.py @@ -107,7 +107,7 @@ class ZiggoMediaboxXLDevice(MediaPlayerEntity): self._available = available self._state = None - def update(self): + def update(self) -> None: """Retrieve the state of the device.""" try: if self._mediabox.test_connection(): @@ -153,25 +153,25 @@ class ZiggoMediaboxXLDevice(MediaPlayerEntity): for c in sorted(self._mediabox.channels().keys()) ] - def turn_on(self): + def turn_on(self) -> None: """Turn the media player on.""" self.send_keys(["POWER"]) - def turn_off(self): + def turn_off(self) -> None: """Turn off media player.""" self.send_keys(["POWER"]) - def media_play(self): + def media_play(self) -> None: """Send play command.""" self.send_keys(["PLAY"]) self._state = STATE_PLAYING - def media_pause(self): + def media_pause(self) -> None: """Send pause command.""" self.send_keys(["PAUSE"]) self._state = STATE_PAUSED - def media_play_pause(self): + def media_play_pause(self) -> None: """Simulate play pause media player.""" self.send_keys(["PAUSE"]) if self._state == STATE_PAUSED: @@ -179,12 +179,12 @@ class ZiggoMediaboxXLDevice(MediaPlayerEntity): else: self._state = STATE_PAUSED - def media_next_track(self): + def media_next_track(self) -> None: """Channel up.""" self.send_keys(["CHAN_UP"]) self._state = STATE_PLAYING - def media_previous_track(self): + def media_previous_track(self) -> None: """Channel down.""" self.send_keys(["CHAN_DOWN"]) self._state = STATE_PLAYING diff --git a/homeassistant/components/zoneminder/binary_sensor.py b/homeassistant/components/zoneminder/binary_sensor.py index 21f4588555c..a091c5fd308 100644 --- a/homeassistant/components/zoneminder/binary_sensor.py +++ b/homeassistant/components/zoneminder/binary_sensor.py @@ -49,6 +49,6 @@ class ZMAvailabilitySensor(BinarySensorEntity): """Return the class of this device, from component DEVICE_CLASSES.""" return BinarySensorDeviceClass.CONNECTIVITY - def update(self): + def update(self) -> None: """Update the state of this sensor (availability of ZoneMinder).""" self._state = self._client.is_available diff --git a/homeassistant/components/zoneminder/camera.py b/homeassistant/components/zoneminder/camera.py index a627f64d0bf..7c0c8b9d453 100644 --- a/homeassistant/components/zoneminder/camera.py +++ b/homeassistant/components/zoneminder/camera.py @@ -50,7 +50,7 @@ class ZoneMinderCamera(MjpegCamera): self._is_available = None self._monitor = monitor - def update(self): + def update(self) -> None: """Update our recording state from the ZM API.""" _LOGGER.debug("Updating camera state for monitor %i", self._monitor.id) self._is_recording = self._monitor.is_recording diff --git a/homeassistant/components/zoneminder/sensor.py b/homeassistant/components/zoneminder/sensor.py index 53ed16c037b..a7534604514 100644 --- a/homeassistant/components/zoneminder/sensor.py +++ b/homeassistant/components/zoneminder/sensor.py @@ -116,7 +116,7 @@ class ZMSensorMonitors(SensorEntity): """Return True if Monitor is available.""" return self._is_available - def update(self): + def update(self) -> None: """Update the sensor.""" if not (state := self._monitor.function): self._state = None @@ -143,7 +143,7 @@ class ZMSensorEvents(SensorEntity): """Return the name of the sensor.""" return f"{self._monitor.name} {self.time_period.title}" - def update(self): + def update(self) -> None: """Update the sensor.""" self._attr_native_value = self._monitor.get_events( self.time_period, self._include_archived @@ -174,7 +174,7 @@ class ZMSensorRunState(SensorEntity): """Return True if ZoneMinder is available.""" return self._is_available - def update(self): + def update(self) -> None: """Update the sensor.""" self._state = self._client.get_active_state() self._is_available = self._client.is_available diff --git a/homeassistant/components/zoneminder/switch.py b/homeassistant/components/zoneminder/switch.py index bd7f55915d1..fc153ca81d8 100644 --- a/homeassistant/components/zoneminder/switch.py +++ b/homeassistant/components/zoneminder/switch.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any import voluptuous as vol from zoneminder.monitor import MonitorState @@ -64,7 +65,7 @@ class ZMSwitchMonitors(SwitchEntity): """Return the name of the switch.""" return f"{self._monitor.name} State" - def update(self): + def update(self) -> None: """Update the switch value.""" self._state = self._monitor.function == self._on_state @@ -73,10 +74,10 @@ class ZMSwitchMonitors(SwitchEntity): """Return True if entity is on.""" return self._state - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" self._monitor.function = self._on_state - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" self._monitor.function = self._off_state diff --git a/homeassistant/components/zwave_me/climate.py b/homeassistant/components/zwave_me/climate.py index 373dbec2836..63903478b51 100644 --- a/homeassistant/components/zwave_me/climate.py +++ b/homeassistant/components/zwave_me/climate.py @@ -1,6 +1,8 @@ """Representation of a thermostat.""" from __future__ import annotations +from typing import Any + from zwave_me_ws import ZWaveMeData from homeassistant.components.climate import ClimateEntity @@ -52,7 +54,7 @@ class ZWaveMeClimate(ZWaveMeEntity, ClimateEntity): _attr_hvac_modes = [HVACMode.HEAT] _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE - def set_temperature(self, **kwargs) -> None: + def set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: return From b945327056a2b92dc2248d9a6c2d39a354d6ff3a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 6 Sep 2022 08:00:05 -0500 Subject: [PATCH 148/955] Bump zeroconf to 0.39.1 (#77859) --- 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 4438a22040d..ec670558e66 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.0"], + "requirements": ["zeroconf==0.39.1"], "dependencies": ["network", "api"], "codeowners": ["@bdraco"], "quality_scale": "internal", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3bf00427954..1299abcd61b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -40,7 +40,7 @@ typing-extensions>=3.10.0.2,<5.0 voluptuous-serialize==2.5.0 voluptuous==0.13.1 yarl==1.7.2 -zeroconf==0.39.0 +zeroconf==0.39.1 # 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 dafc3f798a0..bd6b6618852 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2569,7 +2569,7 @@ youtube_dl==2021.12.17 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.39.0 +zeroconf==0.39.1 # homeassistant.components.zha zha-quirks==0.0.79 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 399fa7427c9..8fa6eb8c8f0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1764,7 +1764,7 @@ yolink-api==0.0.9 youless-api==0.16 # homeassistant.components.zeroconf -zeroconf==0.39.0 +zeroconf==0.39.1 # homeassistant.components.zha zha-quirks==0.0.79 From 91fbff05db9aab3318b98be129fb686bbc342329 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 6 Sep 2022 09:40:20 -0400 Subject: [PATCH 149/955] Improve performance impact of zwave_js update entity and other tweaks (#77866) * Improve performance impact of zwave_js update entity and other tweaks * Reduce concurrent polls * we need to write state after setting in progress to false * Fix existing tests * Fix tests by fixing fixtures * remove redundant conditional * Add test for delayed startup * tweaks * outdent happy path * Add missing PROGRESS feature support * Update homeassistant/components/zwave_js/update.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/zwave_js/update.py Co-authored-by: Martin Hjelmare * Fix tests by reverting outdent, PR comments, mark callback * Remove redundant conditional * make more readable * Remove unused SCAN_INTERVAL * Catch FailedZWaveCommand * Add comment and remove poll unsub on update * Fix catching error and add test * readability * Fix tests * Add assertions * rely on built in progress indicator Co-authored-by: Martin Hjelmare --- homeassistant/components/zwave_js/__init__.py | 25 +- homeassistant/components/zwave_js/update.py | 98 +++--- tests/components/zwave_js/conftest.py | 3 +- .../aeotec_radiator_thermostat_state.json | 8 +- .../fixtures/aeotec_zw164_siren_state.json | 174 +++++------ .../fixtures/bulb_6_multi_color_state.json | 4 +- .../fixtures/chain_actuator_zws12_state.json | 9 +- .../fixtures/climate_adc_t3000_state.json | 294 +++++++++--------- .../fixtures/climate_danfoss_lc_13_state.json | 56 ---- .../climate_eurotronic_spirit_z_state.json | 3 +- .../climate_heatit_z_trm2fx_state.json | 186 +++++------ .../climate_heatit_z_trm3_no_value_state.json | 198 ++++++------ .../fixtures/climate_heatit_z_trm3_state.json | 4 +- ...setpoint_on_different_endpoints_state.json | 174 +++++------ ..._ct100_plus_different_endpoints_state.json | 116 ------- ...ostat_ct101_multiple_temp_units_state.json | 174 +++++------ .../cover_aeotec_nano_shutter_state.json | 174 +++++------ .../fixtures/cover_fibaro_fgr222_state.json | 138 ++++---- .../fixtures/cover_iblinds_v2_state.json | 4 +- .../fixtures/cover_qubino_shutter_state.json | 150 ++++----- .../zwave_js/fixtures/cover_zw062_state.json | 4 +- .../fixtures/eaton_rf9640_dimmer_state.json | 4 +- .../fixtures/ecolink_door_sensor_state.json | 4 +- .../express_controls_ezmultipli_state.json | 162 +++++----- .../zwave_js/fixtures/fan_ge_12730_state.json | 4 +- .../zwave_js/fixtures/fan_generic_state.json | 4 +- .../zwave_js/fixtures/fan_hs_fc200_state.json | 198 ++++++------ .../fixtures/fortrezz_ssa1_siren_state.json | 66 ++-- .../fixtures/fortrezz_ssa3_siren_state.json | 66 ++-- .../fixtures/inovelli_lzw36_state.json | 210 ++++++------- .../light_color_null_values_state.json | 138 ++++---- .../fixtures/lock_august_asl03_state.json | 4 +- .../fixtures/lock_id_lock_as_id150_state.json | 162 +++++----- ...pp_electric_strike_lock_control_state.json | 162 +++++----- .../fixtures/multisensor_6_state.json | 4 +- .../nortek_thermostat_added_event.json | 4 +- .../nortek_thermostat_removed_event.json | 4 +- .../fixtures/nortek_thermostat_state.json | 4 +- .../fixtures/null_name_check_state.json | 150 ++++----- .../fixtures/srt321_hrt4_zw_state.json | 54 ++-- .../vision_security_zl7432_state.json | 66 ++-- .../zwave_js/fixtures/zen_31_state.json | 246 +++++++-------- .../fixtures/zp3111-5_not_ready_state.json | 4 +- .../zwave_js/fixtures/zp3111-5_state.json | 162 +++++----- tests/components/zwave_js/test_init.py | 8 +- tests/components/zwave_js/test_update.py | 82 ++++- 46 files changed, 1941 insertions(+), 2027 deletions(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 03a8ee5fce2..98219520693 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -8,6 +8,7 @@ from typing import Any from async_timeout import timeout from zwave_js_server.client import Client as ZwaveClient +from zwave_js_server.const import CommandClass from zwave_js_server.exceptions import BaseZwaveJSServerError, InvalidServerVersion from zwave_js_server.model.driver import Driver from zwave_js_server.model.node import Node as ZwaveNode @@ -312,14 +313,6 @@ class ControllerEvents: node, ) - # Create a firmware update entity for each device - await self.driver_events.async_setup_platform(Platform.UPDATE) - async_dispatcher_send( - self.hass, - f"{DOMAIN}_{self.config_entry.entry_id}_add_firmware_update_entity", - node, - ) - # we only want to run discovery when the node has reached ready state, # otherwise we'll have all kinds of missing info issues. if node.ready: @@ -463,11 +456,27 @@ class NodeEvents: ), ) ) + # add listener for stateless node notification events self.config_entry.async_on_unload( node.on("notification", self.async_on_notification) ) + # Create a firmware update entity for each non-controller device that + # supports firmware updates + if not node.is_controller_node and any( + CommandClass.FIRMWARE_UPDATE_MD.value == cc.id + for cc in node.command_classes + ): + await self.controller_events.driver_events.async_setup_platform( + Platform.UPDATE + ) + async_dispatcher_send( + self.hass, + f"{DOMAIN}_{self.config_entry.entry_id}_add_firmware_update_entity", + node, + ) + async def async_handle_discovery_info( self, device: device_registry.DeviceEntry, diff --git a/homeassistant/components/zwave_js/update.py b/homeassistant/components/zwave_js/update.py index 7f25788e0be..97c14746dd9 100644 --- a/homeassistant/components/zwave_js/update.py +++ b/homeassistant/components/zwave_js/update.py @@ -1,14 +1,15 @@ """Representation of Z-Wave updates.""" from __future__ import annotations +import asyncio from collections.abc import Callable -from datetime import timedelta +from datetime import datetime, timedelta from typing import Any from awesomeversion import AwesomeVersion from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.const import NodeStatus -from zwave_js_server.exceptions import BaseZwaveJSServerError +from zwave_js_server.exceptions import BaseZwaveJSServerError, FailedZWaveCommand from zwave_js_server.model.driver import Driver from zwave_js_server.model.firmware import FirmwareUpdateInfo from zwave_js_server.model.node import Node as ZwaveNode @@ -21,12 +22,13 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.event import async_call_later +from homeassistant.helpers.start import async_at_start from .const import API_KEY_FIRMWARE_UPDATE_SERVICE, DATA_CLIENT, DOMAIN, LOGGER from .helpers import get_device_info, get_valueless_base_unique_id PARALLEL_UPDATES = 1 -SCAN_INTERVAL = timedelta(days=1) async def async_setup_entry( @@ -37,12 +39,14 @@ async def async_setup_entry( """Set up Z-Wave button from config entry.""" client: ZwaveClient = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT] + semaphore = asyncio.Semaphore(3) + @callback def async_add_firmware_update_entity(node: ZwaveNode) -> None: """Add firmware update entity.""" driver = client.driver assert driver is not None # Driver is ready before platforms are loaded. - async_add_entities([ZWaveNodeFirmwareUpdate(driver, node)], True) + async_add_entities([ZWaveNodeFirmwareUpdate(driver, node, semaphore)]) config_entry.async_on_unload( async_dispatcher_connect( @@ -62,30 +66,36 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): UpdateEntityFeature.INSTALL | UpdateEntityFeature.RELEASE_NOTES ) _attr_has_entity_name = True + _attr_should_poll = False - def __init__(self, driver: Driver, node: ZwaveNode) -> None: + def __init__( + self, driver: Driver, node: ZwaveNode, semaphore: asyncio.Semaphore + ) -> None: """Initialize a Z-Wave device firmware update entity.""" self.driver = driver self.node = node + self.semaphore = semaphore self._latest_version_firmware: FirmwareUpdateInfo | None = None self._status_unsub: Callable[[], None] | None = None + self._poll_unsub: Callable[[], None] | None = None # Entity class attributes self._attr_name = "Firmware" self._base_unique_id = get_valueless_base_unique_id(driver, node) self._attr_unique_id = f"{self._base_unique_id}.firmware_update" + self._attr_installed_version = self._attr_latest_version = node.firmware_version # device may not be precreated in main handler yet self._attr_device_info = get_device_info(driver, node) - self._attr_installed_version = self._attr_latest_version = node.firmware_version - + @callback def _update_on_status_change(self, _: dict[str, Any]) -> None: """Update the entity when node is awake.""" self._status_unsub = None - self.hass.async_create_task(self.async_update(True)) + self.hass.async_create_task(self._async_update()) - async def async_update(self, write_state: bool = False) -> None: + async def _async_update(self, _: HomeAssistant | datetime | None = None) -> None: """Update the entity.""" + self._poll_unsub = None for status, event_name in ( (NodeStatus.ASLEEP, "wake up"), (NodeStatus.DEAD, "alive"), @@ -97,34 +107,38 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): ) return - if available_firmware_updates := ( - await self.driver.controller.async_get_available_firmware_updates( - self.node, API_KEY_FIRMWARE_UPDATE_SERVICE + try: + async with self.semaphore: + available_firmware_updates = ( + await self.driver.controller.async_get_available_firmware_updates( + self.node, API_KEY_FIRMWARE_UPDATE_SERVICE + ) + ) + except FailedZWaveCommand as err: + LOGGER.debug( + "Failed to get firmware updates for node %s: %s", + self.node.node_id, + err, ) - ): - self._latest_version_firmware = max( - available_firmware_updates, - key=lambda x: AwesomeVersion(x.version), - ) - self._async_process_available_updates(write_state) - - @callback - def _async_process_available_updates(self, write_state: bool = True) -> None: - """ - Process available firmware updates. - - Sets latest version attribute and FirmwareUpdateInfo instance. - """ - # If we have an available firmware update that is a higher version than what's - # on the node, we should advertise it, otherwise we are on the latest version - if (firmware := self._latest_version_firmware) and AwesomeVersion( - firmware.version - ) > AwesomeVersion(self.node.firmware_version): - self._attr_latest_version = firmware.version else: - self._attr_latest_version = self._attr_installed_version - if write_state: - self.async_write_ha_state() + if available_firmware_updates: + self._latest_version_firmware = latest_firmware = max( + available_firmware_updates, + key=lambda x: AwesomeVersion(x.version), + ) + + # If we have an available firmware update that is a higher version than + # what's on the node, we should advertise it, otherwise there is + # nothing to do. + new_version = latest_firmware.version + current_version = self.node.firmware_version + if AwesomeVersion(new_version) > AwesomeVersion(current_version): + self._attr_latest_version = new_version + self.async_write_ha_state() + finally: + self._poll_unsub = async_call_later( + self.hass, timedelta(days=1), self._async_update + ) async def async_release_notes(self) -> str | None: """Get release notes.""" @@ -138,8 +152,6 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): """Install an update.""" firmware = self._latest_version_firmware assert firmware - self._attr_in_progress = True - self.async_write_ha_state() try: for file in firmware.files: await self.driver.controller.async_begin_ota_firmware_update( @@ -148,11 +160,9 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): except BaseZwaveJSServerError as err: raise HomeAssistantError(err) from err else: - self._attr_installed_version = firmware.version + self._attr_installed_version = self._attr_latest_version = firmware.version self._latest_version_firmware = None - self._async_process_available_updates() - finally: - self._attr_in_progress = False + self.async_write_ha_state() async def async_poll_value(self, _: bool) -> None: """Poll a value.""" @@ -179,8 +189,14 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): ) ) + self.async_on_remove(async_at_start(self.hass, self._async_update)) + async def async_will_remove_from_hass(self) -> None: """Call when entity will be removed.""" if self._status_unsub: self._status_unsub() self._status_unsub = None + + if self._poll_unsub: + self._poll_unsub() + self._poll_unsub = None diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 7131b1ade69..04a9c5671f9 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -584,7 +584,8 @@ def mock_client_fixture(controller_state, version_state, log_config_state): async def listen(driver_ready: asyncio.Event) -> None: driver_ready.set() - await asyncio.sleep(30) + listen_block = asyncio.Event() + await listen_block.wait() assert False, "Listen wasn't canceled!" async def disconnect(): diff --git a/tests/components/zwave_js/fixtures/aeotec_radiator_thermostat_state.json b/tests/components/zwave_js/fixtures/aeotec_radiator_thermostat_state.json index 789a72c98fa..111714560b5 100644 --- a/tests/components/zwave_js/fixtures/aeotec_radiator_thermostat_state.json +++ b/tests/components/zwave_js/fixtures/aeotec_radiator_thermostat_state.json @@ -39,7 +39,13 @@ "neighbors": [6, 7, 45, 67], "interviewAttempts": 1, "endpoints": [ - { "nodeId": 4, "index": 0, "installerIcon": 4608, "userIcon": 4608 } + { + "nodeId": 4, + "index": 0, + "installerIcon": 4608, + "userIcon": 4608, + "commandClasses": [] + } ], "values": [ { diff --git a/tests/components/zwave_js/fixtures/aeotec_zw164_siren_state.json b/tests/components/zwave_js/fixtures/aeotec_zw164_siren_state.json index 0f0dde61c83..f2b43878990 100644 --- a/tests/components/zwave_js/fixtures/aeotec_zw164_siren_state.json +++ b/tests/components/zwave_js/fixtures/aeotec_zw164_siren_state.json @@ -77,7 +77,93 @@ 32, 133, 89, 128, 121, 114, 115, 159, 108, 85, 134, 94 ], "mandatoryControlledCCs": [] - } + }, + "commandClasses": [ + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 1, + "isSecure": false + }, + { + "id": 121, + "name": "Sound Switch", + "version": 1, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 2, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + }, + { + "id": 96, + "name": "Multi Channel", + "version": 4, + "isSecure": false + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 152, + "name": "Security", + "version": 1, + "isSecure": true + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 4, + "isSecure": false + }, + { + "id": 113, + "name": "Notification", + "version": 8, + "isSecure": false + } + ] }, { "nodeId": 2, @@ -3665,92 +3751,6 @@ ], "mandatoryControlledCCs": [] }, - "commandClasses": [ - { - "id": 133, - "name": "Association", - "version": 2, - "isSecure": false - }, - { - "id": 89, - "name": "Association Group Information", - "version": 1, - "isSecure": false - }, - { - "id": 121, - "name": "Sound Switch", - "version": 1, - "isSecure": false - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 2, - "isSecure": false - }, - { - "id": 108, - "name": "Supervision", - "version": 1, - "isSecure": false - }, - { - "id": 134, - "name": "Version", - "version": 2, - "isSecure": false - }, - { - "id": 94, - "name": "Z-Wave Plus Info", - "version": 2, - "isSecure": false - }, - { - "id": 112, - "name": "Configuration", - "version": 1, - "isSecure": false - }, - { - "id": 142, - "name": "Multi Channel Association", - "version": 3, - "isSecure": false - }, - { - "id": 96, - "name": "Multi Channel", - "version": 4, - "isSecure": false - }, - { - "id": 90, - "name": "Device Reset Locally", - "version": 1, - "isSecure": false - }, - { - "id": 152, - "name": "Security", - "version": 1, - "isSecure": true - }, - { - "id": 122, - "name": "Firmware Update Meta Data", - "version": 4, - "isSecure": false - }, - { - "id": 113, - "name": "Notification", - "version": 8, - "isSecure": false - } - ], "interviewStage": "Complete", "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0371:0x0103:0x00a4:1.3", "isControllerNode": false diff --git a/tests/components/zwave_js/fixtures/bulb_6_multi_color_state.json b/tests/components/zwave_js/fixtures/bulb_6_multi_color_state.json index 7243cbe9383..172580f563e 100644 --- a/tests/components/zwave_js/fixtures/bulb_6_multi_color_state.json +++ b/tests/components/zwave_js/fixtures/bulb_6_multi_color_state.json @@ -57,10 +57,10 @@ "nodeId": 39, "index": 0, "installerIcon": 1536, - "userIcon": 1536 + "userIcon": 1536, + "commandClasses": [] } ], - "commandClasses": [], "values": [ { "commandClassName": "Multilevel Switch", diff --git a/tests/components/zwave_js/fixtures/chain_actuator_zws12_state.json b/tests/components/zwave_js/fixtures/chain_actuator_zws12_state.json index d17385f7d1e..f89fce5561e 100644 --- a/tests/components/zwave_js/fixtures/chain_actuator_zws12_state.json +++ b/tests/components/zwave_js/fixtures/chain_actuator_zws12_state.json @@ -44,9 +44,14 @@ "neighbors": [1, 2], "interviewAttempts": 1, "endpoints": [ - { "nodeId": 6, "index": 0, "installerIcon": 6656, "userIcon": 6656 } + { + "nodeId": 6, + "index": 0, + "installerIcon": 6656, + "userIcon": 6656, + "commandClasses": [] + } ], - "commandClasses": [], "values": [ { "commandClassName": "Multilevel Switch", diff --git a/tests/components/zwave_js/fixtures/climate_adc_t3000_state.json b/tests/components/zwave_js/fixtures/climate_adc_t3000_state.json index bc19c034099..ab80b46069c 100644 --- a/tests/components/zwave_js/fixtures/climate_adc_t3000_state.json +++ b/tests/components/zwave_js/fixtures/climate_adc_t3000_state.json @@ -58,7 +58,153 @@ }, "mandatorySupportedCCs": [32, 114, 64, 67, 134], "mandatoryControlledCCs": [] - } + }, + "commandClasses": [ + { + "id": 49, + "name": "Multilevel Sensor", + "version": 11, + "isSecure": true + }, + { + "id": 64, + "name": "Thermostat Mode", + "version": 2, + "isSecure": true + }, + { + "id": 66, + "name": "Thermostat Operating State", + "version": 2, + "isSecure": true + }, + { + "id": 67, + "name": "Thermostat Setpoint", + "version": 3, + "isSecure": true + }, + { + "id": 68, + "name": "Thermostat Fan Mode", + "version": 3, + "isSecure": true + }, + { + "id": 69, + "name": "Thermostat Fan State", + "version": 1, + "isSecure": true + }, + { + "id": 85, + "name": "Transport Service", + "version": 2, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 1, + "isSecure": true + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": true + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 100, + "name": "Humidity Control Setpoint", + "version": 1, + "isSecure": true + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 109, + "name": "Humidity Control Mode", + "version": 2, + "isSecure": true + }, + { + "id": 110, + "name": "Humidity Control Operating State", + "version": 1, + "isSecure": true + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": true + }, + { + "id": 113, + "name": "Notification", + "version": 7, + "isSecure": true + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": true + }, + { + "id": 115, + "name": "Powerlevel", + "version": 1, + "isSecure": true + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 3, + "isSecure": true + }, + { + "id": 128, + "name": "Battery", + "version": 1, + "isSecure": true + }, + { + "id": 129, + "name": "Clock", + "version": 1, + "isSecure": true + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": true + }, + { + "id": 134, + "name": "Version", + "version": 2, + "isSecure": true + }, + { + "id": 159, + "name": "Security 2", + "version": 1, + "isSecure": true + } + ] } ], "values": [ @@ -3940,152 +4086,6 @@ "mandatorySupportedCCs": [32, 114, 64, 67, 134], "mandatoryControlledCCs": [] }, - "commandClasses": [ - { - "id": 49, - "name": "Multilevel Sensor", - "version": 11, - "isSecure": true - }, - { - "id": 64, - "name": "Thermostat Mode", - "version": 2, - "isSecure": true - }, - { - "id": 66, - "name": "Thermostat Operating State", - "version": 2, - "isSecure": true - }, - { - "id": 67, - "name": "Thermostat Setpoint", - "version": 3, - "isSecure": true - }, - { - "id": 68, - "name": "Thermostat Fan Mode", - "version": 3, - "isSecure": true - }, - { - "id": 69, - "name": "Thermostat Fan State", - "version": 1, - "isSecure": true - }, - { - "id": 85, - "name": "Transport Service", - "version": 2, - "isSecure": false - }, - { - "id": 89, - "name": "Association Group Information", - "version": 1, - "isSecure": true - }, - { - "id": 90, - "name": "Device Reset Locally", - "version": 1, - "isSecure": true - }, - { - "id": 94, - "name": "Z-Wave Plus Info", - "version": 2, - "isSecure": false - }, - { - "id": 100, - "name": "Humidity Control Setpoint", - "version": 1, - "isSecure": true - }, - { - "id": 108, - "name": "Supervision", - "version": 1, - "isSecure": false - }, - { - "id": 109, - "name": "Humidity Control Mode", - "version": 2, - "isSecure": true - }, - { - "id": 110, - "name": "Humidity Control Operating State", - "version": 1, - "isSecure": true - }, - { - "id": 112, - "name": "Configuration", - "version": 1, - "isSecure": true - }, - { - "id": 113, - "name": "Notification", - "version": 7, - "isSecure": true - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 2, - "isSecure": true - }, - { - "id": 115, - "name": "Powerlevel", - "version": 1, - "isSecure": true - }, - { - "id": 122, - "name": "Firmware Update Meta Data", - "version": 3, - "isSecure": true - }, - { - "id": 128, - "name": "Battery", - "version": 1, - "isSecure": true - }, - { - "id": 129, - "name": "Clock", - "version": 1, - "isSecure": true - }, - { - "id": 133, - "name": "Association", - "version": 2, - "isSecure": true - }, - { - "id": 134, - "name": "Version", - "version": 2, - "isSecure": true - }, - { - "id": 159, - "name": "Security 2", - "version": 1, - "isSecure": true - } - ], "interviewStage": "Complete", "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0190:0x0006:0x0001:1.44", "statistics": { diff --git a/tests/components/zwave_js/fixtures/climate_danfoss_lc_13_state.json b/tests/components/zwave_js/fixtures/climate_danfoss_lc_13_state.json index cb8e78881df..8a88c1fc6e2 100644 --- a/tests/components/zwave_js/fixtures/climate_danfoss_lc_13_state.json +++ b/tests/components/zwave_js/fixtures/climate_danfoss_lc_13_state.json @@ -67,62 +67,6 @@ "neighbors": [1, 14], "interviewAttempts": 1, "interviewStage": 7, - "commandClasses": [ - { - "id": 67, - "name": "Thermostat Setpoint", - "version": 2, - "isSecure": false - }, - { - "id": 70, - "name": "Climate Control Schedule", - "version": 1, - "isSecure": false - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 1, - "isSecure": false - }, - { - "id": 117, - "name": "Protection", - "version": 2, - "isSecure": false - }, - { - "id": 128, - "name": "Battery", - "version": 1, - "isSecure": false - }, - { - "id": 129, - "name": "Clock", - "version": 1, - "isSecure": false - }, - { - "id": 132, - "name": "Wake Up", - "version": 1, - "isSecure": false - }, - { - "id": 134, - "name": "Version", - "version": 1, - "isSecure": false - }, - { - "id": 143, - "name": "Multi Command", - "version": 1, - "isSecure": false - } - ], "endpoints": [ { "nodeId": 5, diff --git a/tests/components/zwave_js/fixtures/climate_eurotronic_spirit_z_state.json b/tests/components/zwave_js/fixtures/climate_eurotronic_spirit_z_state.json index dfca647ae67..7025c27182f 100644 --- a/tests/components/zwave_js/fixtures/climate_eurotronic_spirit_z_state.json +++ b/tests/components/zwave_js/fixtures/climate_eurotronic_spirit_z_state.json @@ -72,7 +72,8 @@ "nodeId": 8, "index": 0, "installerIcon": 4608, - "userIcon": 4608 + "userIcon": 4608, + "commandClasses": [] } ], "values": [ diff --git a/tests/components/zwave_js/fixtures/climate_heatit_z_trm2fx_state.json b/tests/components/zwave_js/fixtures/climate_heatit_z_trm2fx_state.json index c99898fb595..d1a5fb8c1ee 100644 --- a/tests/components/zwave_js/fixtures/climate_heatit_z_trm2fx_state.json +++ b/tests/components/zwave_js/fixtures/climate_heatit_z_trm2fx_state.json @@ -66,7 +66,99 @@ }, "mandatorySupportedCCs": [32, 114, 64, 67, 134], "mandatoryControlledCCs": [] - } + }, + "commandClasses": [ + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 64, + "name": "Thermostat Mode", + "version": 3, + "isSecure": false + }, + { + "id": 67, + "name": "Thermostat Setpoint", + "version": 3, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 3, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 1, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + }, + { + "id": 96, + "name": "Multi Channel", + "version": 4, + "isSecure": false + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 152, + "name": "Security", + "version": 1, + "isSecure": true + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 3, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 4, + "isSecure": false + }, + { + "id": 50, + "name": "Meter", + "version": 3, + "isSecure": false + } + ] }, { "nodeId": 26, @@ -1348,98 +1440,6 @@ "mandatorySupportedCCs": [32, 114, 64, 67, 134], "mandatoryControlledCCs": [] }, - "commandClasses": [ - { - "id": 114, - "name": "Manufacturer Specific", - "version": 2, - "isSecure": false - }, - { - "id": 64, - "name": "Thermostat Mode", - "version": 3, - "isSecure": false - }, - { - "id": 67, - "name": "Thermostat Setpoint", - "version": 3, - "isSecure": false - }, - { - "id": 134, - "name": "Version", - "version": 3, - "isSecure": false - }, - { - "id": 94, - "name": "Z-Wave Plus Info", - "version": 2, - "isSecure": false - }, - { - "id": 133, - "name": "Association", - "version": 2, - "isSecure": false - }, - { - "id": 89, - "name": "Association Group Information", - "version": 1, - "isSecure": false - }, - { - "id": 142, - "name": "Multi Channel Association", - "version": 3, - "isSecure": false - }, - { - "id": 96, - "name": "Multi Channel", - "version": 4, - "isSecure": false - }, - { - "id": 90, - "name": "Device Reset Locally", - "version": 1, - "isSecure": false - }, - { - "id": 152, - "name": "Security", - "version": 1, - "isSecure": true - }, - { - "id": 108, - "name": "Supervision", - "version": 1, - "isSecure": false - }, - { - "id": 112, - "name": "Configuration", - "version": 3, - "isSecure": false - }, - { - "id": 122, - "name": "Firmware Update Meta Data", - "version": 4, - "isSecure": false - }, - { - "id": 50, - "name": "Meter", - "version": 3, - "isSecure": false - } - ], "interviewStage": "Complete", "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/climate_heatit_z_trm3_no_value_state.json b/tests/components/zwave_js/fixtures/climate_heatit_z_trm3_no_value_state.json index 61b138ebbe7..5c95fe1ffc0 100644 --- a/tests/components/zwave_js/fixtures/climate_heatit_z_trm3_no_value_state.json +++ b/tests/components/zwave_js/fixtures/climate_heatit_z_trm3_no_value_state.json @@ -68,7 +68,105 @@ }, "mandatorySupportedCCs": [32, 114, 64, 67, 134], "mandatoryControlledCCs": [] - } + }, + "commandClasses": [ + { + "id": 50, + "name": "Meter", + "version": 3, + "isSecure": false + }, + { + "id": 64, + "name": "Thermostat Mode", + "version": 3, + "isSecure": false + }, + { + "id": 66, + "name": "Thermostat Operating State", + "version": 1, + "isSecure": false + }, + { + "id": 67, + "name": "Thermostat Setpoint", + "version": 3, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 1, + "isSecure": false + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 96, + "name": "Multi Channel", + "version": 4, + "isSecure": false + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 3, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 1, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 4, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 3, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + }, + { + "id": 152, + "name": "Security", + "version": 1, + "isSecure": true + } + ] }, { "nodeId": 74, @@ -1148,104 +1246,6 @@ "mandatorySupportedCCs": [32, 114, 64, 67, 134], "mandatoryControlledCCs": [] }, - "commandClasses": [ - { - "id": 50, - "name": "Meter", - "version": 3, - "isSecure": false - }, - { - "id": 64, - "name": "Thermostat Mode", - "version": 3, - "isSecure": false - }, - { - "id": 66, - "name": "Thermostat Operating State", - "version": 1, - "isSecure": false - }, - { - "id": 67, - "name": "Thermostat Setpoint", - "version": 3, - "isSecure": false - }, - { - "id": 89, - "name": "Association Group Information", - "version": 1, - "isSecure": false - }, - { - "id": 90, - "name": "Device Reset Locally", - "version": 1, - "isSecure": false - }, - { - "id": 94, - "name": "Z-Wave Plus Info", - "version": 2, - "isSecure": false - }, - { - "id": 96, - "name": "Multi Channel", - "version": 4, - "isSecure": false - }, - { - "id": 108, - "name": "Supervision", - "version": 1, - "isSecure": false - }, - { - "id": 112, - "name": "Configuration", - "version": 3, - "isSecure": false - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 1, - "isSecure": false - }, - { - "id": 122, - "name": "Firmware Update Meta Data", - "version": 4, - "isSecure": false - }, - { - "id": 133, - "name": "Association", - "version": 2, - "isSecure": false - }, - { - "id": 134, - "name": "Version", - "version": 3, - "isSecure": false - }, - { - "id": 142, - "name": "Multi Channel Association", - "version": 3, - "isSecure": false - }, - { - "id": 152, - "name": "Security", - "version": 1, - "isSecure": true - } - ], "interviewStage": "Complete", "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/climate_heatit_z_trm3_state.json b/tests/components/zwave_js/fixtures/climate_heatit_z_trm3_state.json index da6876dceaa..bd3bf2d560e 100644 --- a/tests/components/zwave_js/fixtures/climate_heatit_z_trm3_state.json +++ b/tests/components/zwave_js/fixtures/climate_heatit_z_trm3_state.json @@ -66,7 +66,8 @@ "nodeId": 24, "index": 0, "installerIcon": 4608, - "userIcon": 4609 + "userIcon": 4609, + "commandClasses": [] }, { "nodeId": 24, @@ -93,7 +94,6 @@ "userIcon": 3329 } ], - "commandClasses": [], "values": [ { "endpoint": 0, diff --git a/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_mode_and_setpoint_on_different_endpoints_state.json b/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_mode_and_setpoint_on_different_endpoints_state.json index d5f540e8343..cbd25aa4ffd 100644 --- a/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_mode_and_setpoint_on_different_endpoints_state.json +++ b/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_mode_and_setpoint_on_different_endpoints_state.json @@ -62,7 +62,93 @@ "endpoints": [ { "nodeId": 8, - "index": 0 + "index": 0, + "commandClasses": [ + { + "id": 49, + "name": "Multilevel Sensor", + "version": 2, + "isSecure": false + }, + { + "id": 64, + "name": "Thermostat Mode", + "version": 2, + "isSecure": false + }, + { + "id": 66, + "name": "Thermostat Operating State", + "version": 2, + "isSecure": false + }, + { + "id": 67, + "name": "Thermostat Setpoint", + "version": 2, + "isSecure": false + }, + { + "id": 68, + "name": "Thermostat Fan Mode", + "version": 1, + "isSecure": false + }, + { + "id": 69, + "name": "Thermostat Fan State", + "version": 1, + "isSecure": false + }, + { + "id": 96, + "name": "Multi Channel", + "version": 3, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 1, + "isSecure": false + }, + { + "id": 128, + "name": "Battery", + "version": 1, + "isSecure": false + }, + { + "id": 129, + "name": "Clock", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 1, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 1, + "isSecure": false + }, + { + "id": 135, + "name": "Indicator", + "version": 1, + "isSecure": false + } + ] }, { "nodeId": 8, @@ -741,91 +827,5 @@ "mandatorySupportedCCs": [32, 114, 64, 67, 134], "mandatoryControlledCCs": [] }, - "commandClasses": [ - { - "id": 49, - "name": "Multilevel Sensor", - "version": 2, - "isSecure": false - }, - { - "id": 64, - "name": "Thermostat Mode", - "version": 2, - "isSecure": false - }, - { - "id": 66, - "name": "Thermostat Operating State", - "version": 2, - "isSecure": false - }, - { - "id": 67, - "name": "Thermostat Setpoint", - "version": 2, - "isSecure": false - }, - { - "id": 68, - "name": "Thermostat Fan Mode", - "version": 1, - "isSecure": false - }, - { - "id": 69, - "name": "Thermostat Fan State", - "version": 1, - "isSecure": false - }, - { - "id": 96, - "name": "Multi Channel", - "version": 3, - "isSecure": false - }, - { - "id": 112, - "name": "Configuration", - "version": 1, - "isSecure": false - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 1, - "isSecure": false - }, - { - "id": 128, - "name": "Battery", - "version": 1, - "isSecure": false - }, - { - "id": 129, - "name": "Clock", - "version": 1, - "isSecure": false - }, - { - "id": 133, - "name": "Association", - "version": 1, - "isSecure": false - }, - { - "id": 134, - "name": "Version", - "version": 1, - "isSecure": false - }, - { - "id": 135, - "name": "Indicator", - "version": 1, - "isSecure": false - } - ], "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_plus_different_endpoints_state.json b/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_plus_different_endpoints_state.json index ca0efb56711..21b8a7457eb 100644 --- a/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_plus_different_endpoints_state.json +++ b/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_plus_different_endpoints_state.json @@ -1316,121 +1316,5 @@ "mandatorySupportedCCs": [32, 114, 64, 67, 134], "mandatoryControlledCCs": [] }, - "commandClasses": [ - { - "id": 49, - "name": "Multilevel Sensor", - "version": 5, - "isSecure": false - }, - { - "id": 64, - "name": "Thermostat Mode", - "version": 2, - "isSecure": false - }, - { - "id": 66, - "name": "Thermostat Operating State", - "version": 2, - "isSecure": false - }, - { - "id": 67, - "name": "Thermostat Setpoint", - "version": 2, - "isSecure": false - }, - { - "id": 68, - "name": "Thermostat Fan Mode", - "version": 1, - "isSecure": false - }, - { - "id": 69, - "name": "Thermostat Fan State", - "version": 1, - "isSecure": false - }, - { - "id": 89, - "name": "Association Group Information", - "version": 1, - "isSecure": false - }, - { - "id": 90, - "name": "Device Reset Locally", - "version": 1, - "isSecure": false - }, - { - "id": 94, - "name": "Z-Wave Plus Info", - "version": 2, - "isSecure": false - }, - { - "id": 96, - "name": "Multi Channel", - "version": 4, - "isSecure": false - }, - { - "id": 112, - "name": "Configuration", - "version": 1, - "isSecure": false - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 2, - "isSecure": false - }, - { - "id": 122, - "name": "Firmware Update Meta Data", - "version": 3, - "isSecure": false - }, - { - "id": 128, - "name": "Battery", - "version": 1, - "isSecure": false - }, - { - "id": 129, - "name": "Clock", - "version": 1, - "isSecure": false - }, - { - "id": 133, - "name": "Association", - "version": 2, - "isSecure": false - }, - { - "id": 134, - "name": "Version", - "version": 2, - "isSecure": false - }, - { - "id": 135, - "name": "Indicator", - "version": 1, - "isSecure": false - }, - { - "id": 142, - "name": "Multi Channel Association", - "version": 3, - "isSecure": false - } - ], "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct101_multiple_temp_units_state.json b/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct101_multiple_temp_units_state.json index ba87b585b3c..98a0fab8dbb 100644 --- a/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct101_multiple_temp_units_state.json +++ b/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct101_multiple_temp_units_state.json @@ -54,7 +54,93 @@ "endpoints": [ { "nodeId": 4, - "index": 0 + "index": 0, + "commandClasses": [ + { + "id": 49, + "name": "Multilevel Sensor", + "version": 2, + "isSecure": false + }, + { + "id": 64, + "name": "Thermostat Mode", + "version": 2, + "isSecure": false + }, + { + "id": 66, + "name": "Thermostat Operating State", + "version": 2, + "isSecure": false + }, + { + "id": 67, + "name": "Thermostat Setpoint", + "version": 2, + "isSecure": false + }, + { + "id": 68, + "name": "Thermostat Fan Mode", + "version": 1, + "isSecure": false + }, + { + "id": 69, + "name": "Thermostat Fan State", + "version": 1, + "isSecure": false + }, + { + "id": 96, + "name": "Multi Channel", + "version": 3, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 1, + "isSecure": false + }, + { + "id": 128, + "name": "Battery", + "version": 1, + "isSecure": false + }, + { + "id": 129, + "name": "Clock", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 1, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 1, + "isSecure": false + }, + { + "id": 135, + "name": "Indicator", + "version": 1, + "isSecure": false + } + ] }, { "nodeId": 4, @@ -873,91 +959,5 @@ "mandatorySupportedCCs": [32, 114, 64, 67, 134], "mandatoryControlledCCs": [] }, - "commandClasses": [ - { - "id": 49, - "name": "Multilevel Sensor", - "version": 2, - "isSecure": false - }, - { - "id": 64, - "name": "Thermostat Mode", - "version": 2, - "isSecure": false - }, - { - "id": 66, - "name": "Thermostat Operating State", - "version": 2, - "isSecure": false - }, - { - "id": 67, - "name": "Thermostat Setpoint", - "version": 2, - "isSecure": false - }, - { - "id": 68, - "name": "Thermostat Fan Mode", - "version": 1, - "isSecure": false - }, - { - "id": 69, - "name": "Thermostat Fan State", - "version": 1, - "isSecure": false - }, - { - "id": 96, - "name": "Multi Channel", - "version": 3, - "isSecure": false - }, - { - "id": 112, - "name": "Configuration", - "version": 1, - "isSecure": false - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 1, - "isSecure": false - }, - { - "id": 128, - "name": "Battery", - "version": 1, - "isSecure": false - }, - { - "id": 129, - "name": "Clock", - "version": 1, - "isSecure": false - }, - { - "id": 133, - "name": "Association", - "version": 1, - "isSecure": false - }, - { - "id": 134, - "name": "Version", - "version": 1, - "isSecure": false - }, - { - "id": 135, - "name": "Indicator", - "version": 1, - "isSecure": false - } - ], "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/cover_aeotec_nano_shutter_state.json b/tests/components/zwave_js/fixtures/cover_aeotec_nano_shutter_state.json index cd6ceb2f192..48e692b2395 100644 --- a/tests/components/zwave_js/fixtures/cover_aeotec_nano_shutter_state.json +++ b/tests/components/zwave_js/fixtures/cover_aeotec_nano_shutter_state.json @@ -63,7 +63,93 @@ }, "mandatorySupportedCCs": [32, 38, 37, 114, 134], "mandatoryControlledCCs": [] - } + }, + "commandClasses": [ + { + "id": 38, + "name": "Multilevel Switch", + "version": 4, + "isSecure": true + }, + { + "id": 43, + "name": "Scene Activation", + "version": 1, + "isSecure": true + }, + { + "id": 44, + "name": "Scene Actuator Configuration", + "version": 1, + "isSecure": true + }, + { + "id": 89, + "name": "Association Group Information", + "version": 1, + "isSecure": true + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": true + }, + { + "id": 91, + "name": "Central Scene", + "version": 3, + "isSecure": true + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": true + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 4, + "isSecure": true + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": true + }, + { + "id": 134, + "name": "Version", + "version": 2, + "isSecure": true + }, + { + "id": 152, + "name": "Security", + "version": 1, + "isSecure": true + } + ] } ], "values": [ @@ -386,92 +472,6 @@ "mandatorySupportedCCs": [32, 38, 37, 114, 134], "mandatoryControlledCCs": [] }, - "commandClasses": [ - { - "id": 38, - "name": "Multilevel Switch", - "version": 4, - "isSecure": true - }, - { - "id": 43, - "name": "Scene Activation", - "version": 1, - "isSecure": true - }, - { - "id": 44, - "name": "Scene Actuator Configuration", - "version": 1, - "isSecure": true - }, - { - "id": 89, - "name": "Association Group Information", - "version": 1, - "isSecure": true - }, - { - "id": 90, - "name": "Device Reset Locally", - "version": 1, - "isSecure": true - }, - { - "id": 91, - "name": "Central Scene", - "version": 3, - "isSecure": true - }, - { - "id": 94, - "name": "Z-Wave Plus Info", - "version": 2, - "isSecure": false - }, - { - "id": 108, - "name": "Supervision", - "version": 1, - "isSecure": false - }, - { - "id": 112, - "name": "Configuration", - "version": 1, - "isSecure": true - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 2, - "isSecure": false - }, - { - "id": 122, - "name": "Firmware Update Meta Data", - "version": 4, - "isSecure": true - }, - { - "id": 133, - "name": "Association", - "version": 2, - "isSecure": true - }, - { - "id": 134, - "name": "Version", - "version": 2, - "isSecure": true - }, - { - "id": 152, - "name": "Security", - "version": 1, - "isSecure": true - } - ], "interviewStage": "Complete", "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0371:0x0003:0x008d:3.1", "isControllerNode": false diff --git a/tests/components/zwave_js/fixtures/cover_fibaro_fgr222_state.json b/tests/components/zwave_js/fixtures/cover_fibaro_fgr222_state.json index 4e50345195b..54b976a94a6 100644 --- a/tests/components/zwave_js/fixtures/cover_fibaro_fgr222_state.json +++ b/tests/components/zwave_js/fixtures/cover_fibaro_fgr222_state.json @@ -74,7 +74,75 @@ }, "mandatorySupportedCCs": [32, 38, 37, 114, 134], "mandatoryControlledCCs": [] - } + }, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 1, + "isSecure": false + }, + { + "id": 38, + "name": "Multilevel Switch", + "version": 3, + "isSecure": false + }, + { + "id": 49, + "name": "Multilevel Sensor", + "version": 2, + "isSecure": false + }, + { + "id": 50, + "name": "Meter", + "version": 2, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 1, + "isSecure": false + }, + { + "id": 117, + "name": "Protection", + "version": 2, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 1, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 2, + "isSecure": false + }, + { + "id": 145, + "name": "Manufacturer Proprietary", + "version": 1, + "isSecure": false + } + ] } ], "values": [ @@ -1029,74 +1097,6 @@ "mandatorySupportedCCs": [32, 38, 37, 114, 134], "mandatoryControlledCCs": [] }, - "commandClasses": [ - { - "id": 37, - "name": "Binary Switch", - "version": 1, - "isSecure": false - }, - { - "id": 38, - "name": "Multilevel Switch", - "version": 3, - "isSecure": false - }, - { - "id": 49, - "name": "Multilevel Sensor", - "version": 2, - "isSecure": false - }, - { - "id": 50, - "name": "Meter", - "version": 2, - "isSecure": false - }, - { - "id": 112, - "name": "Configuration", - "version": 1, - "isSecure": false - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 1, - "isSecure": false - }, - { - "id": 117, - "name": "Protection", - "version": 2, - "isSecure": false - }, - { - "id": 133, - "name": "Association", - "version": 2, - "isSecure": false - }, - { - "id": 134, - "name": "Version", - "version": 1, - "isSecure": false - }, - { - "id": 142, - "name": "Multi Channel Association", - "version": 2, - "isSecure": false - }, - { - "id": 145, - "name": "Manufacturer Proprietary", - "version": 1, - "isSecure": false - } - ], "interviewStage": "Complete", "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x010f:0x0302:0x1000:25.25", "statistics": { diff --git a/tests/components/zwave_js/fixtures/cover_iblinds_v2_state.json b/tests/components/zwave_js/fixtures/cover_iblinds_v2_state.json index f1e08bf7795..d71f719bc3e 100644 --- a/tests/components/zwave_js/fixtures/cover_iblinds_v2_state.json +++ b/tests/components/zwave_js/fixtures/cover_iblinds_v2_state.json @@ -54,10 +54,10 @@ "nodeId": 54, "index": 0, "installerIcon": 6400, - "userIcon": 6400 + "userIcon": 6400, + "commandClasses": [] } ], - "commandClasses": [], "values": [ { "endpoint": 0, diff --git a/tests/components/zwave_js/fixtures/cover_qubino_shutter_state.json b/tests/components/zwave_js/fixtures/cover_qubino_shutter_state.json index 4c9320085c3..015e4b91cd5 100644 --- a/tests/components/zwave_js/fixtures/cover_qubino_shutter_state.json +++ b/tests/components/zwave_js/fixtures/cover_qubino_shutter_state.json @@ -57,7 +57,81 @@ }, "mandatorySupportedCCs": [32, 38, 37, 114, 134], "mandatoryControlledCCs": [] - } + }, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 1, + "isSecure": false + }, + { + "id": 38, + "name": "Multilevel Switch", + "version": 3, + "isSecure": false + }, + { + "id": 50, + "name": "Meter", + "version": 4, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 2, + "isSecure": false + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": false + }, + { + "id": 113, + "name": "Notification", + "version": 5, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 2, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + } + ] } ], "values": [ @@ -792,80 +866,6 @@ "mandatorySupportedCCs": [32, 38, 37, 114, 134], "mandatoryControlledCCs": [] }, - "commandClasses": [ - { - "id": 37, - "name": "Binary Switch", - "version": 1, - "isSecure": false - }, - { - "id": 38, - "name": "Multilevel Switch", - "version": 3, - "isSecure": false - }, - { - "id": 50, - "name": "Meter", - "version": 4, - "isSecure": false - }, - { - "id": 89, - "name": "Association Group Information", - "version": 2, - "isSecure": false - }, - { - "id": 90, - "name": "Device Reset Locally", - "version": 1, - "isSecure": false - }, - { - "id": 94, - "name": "Z-Wave Plus Info", - "version": 2, - "isSecure": false - }, - { - "id": 112, - "name": "Configuration", - "version": 1, - "isSecure": false - }, - { - "id": 113, - "name": "Notification", - "version": 5, - "isSecure": false - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 2, - "isSecure": false - }, - { - "id": 133, - "name": "Association", - "version": 2, - "isSecure": false - }, - { - "id": 134, - "name": "Version", - "version": 2, - "isSecure": false - }, - { - "id": 142, - "name": "Multi Channel Association", - "version": 3, - "isSecure": false - } - ], "interviewStage": "Complete", "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0159:0x0003:0x0052:71.0", "statistics": { diff --git a/tests/components/zwave_js/fixtures/cover_zw062_state.json b/tests/components/zwave_js/fixtures/cover_zw062_state.json index a2033e30bd6..8e819faa347 100644 --- a/tests/components/zwave_js/fixtures/cover_zw062_state.json +++ b/tests/components/zwave_js/fixtures/cover_zw062_state.json @@ -62,10 +62,10 @@ "nodeId": 12, "index": 0, "installerIcon": 7680, - "userIcon": 7680 + "userIcon": 7680, + "commandClasses": [] } ], - "commandClasses": [], "values": [ { "endpoint": 0, diff --git a/tests/components/zwave_js/fixtures/eaton_rf9640_dimmer_state.json b/tests/components/zwave_js/fixtures/eaton_rf9640_dimmer_state.json index 23f8628b6d3..6885c1aa342 100644 --- a/tests/components/zwave_js/fixtures/eaton_rf9640_dimmer_state.json +++ b/tests/components/zwave_js/fixtures/eaton_rf9640_dimmer_state.json @@ -55,10 +55,10 @@ "nodeId": 19, "index": 0, "installerIcon": 1536, - "userIcon": 1536 + "userIcon": 1536, + "commandClasses": [] } ], - "commandClasses": [], "values": [ { "commandClassName": "Multilevel Switch", diff --git a/tests/components/zwave_js/fixtures/ecolink_door_sensor_state.json b/tests/components/zwave_js/fixtures/ecolink_door_sensor_state.json index 225f532dfb8..9633b84c394 100644 --- a/tests/components/zwave_js/fixtures/ecolink_door_sensor_state.json +++ b/tests/components/zwave_js/fixtures/ecolink_door_sensor_state.json @@ -47,10 +47,10 @@ "endpoints": [ { "nodeId": 2, - "index": 0 + "index": 0, + "commandClasses": [] } ], - "commandClasses": [], "values": [ { "commandClassName": "Basic", diff --git a/tests/components/zwave_js/fixtures/express_controls_ezmultipli_state.json b/tests/components/zwave_js/fixtures/express_controls_ezmultipli_state.json index ea267d86b8c..502dd573420 100644 --- a/tests/components/zwave_js/fixtures/express_controls_ezmultipli_state.json +++ b/tests/components/zwave_js/fixtures/express_controls_ezmultipli_state.json @@ -60,7 +60,87 @@ }, "mandatorySupportedCCs": [], "mandatoryControlledCCs": [] - } + }, + "commandClasses": [ + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 113, + "name": "Notification", + "version": 3, + "isSecure": false + }, + { + "id": 49, + "name": "Multilevel Sensor", + "version": 6, + "isSecure": false + }, + { + "id": 51, + "name": "Color Switch", + "version": 1, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 2, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 2, + "isSecure": false + }, + { + "id": 119, + "name": "Node Naming and Location", + "version": 1, + "isSecure": false + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 2, + "isSecure": false + }, + { + "id": 115, + "name": "Powerlevel", + "version": 1, + "isSecure": false + } + ] } ], "values": [ @@ -578,86 +658,6 @@ "mandatorySupportedCCs": [], "mandatoryControlledCCs": [] }, - "commandClasses": [ - { - "id": 94, - "name": "Z-Wave Plus Info", - "version": 2, - "isSecure": false - }, - { - "id": 113, - "name": "Notification", - "version": 3, - "isSecure": false - }, - { - "id": 49, - "name": "Multilevel Sensor", - "version": 6, - "isSecure": false - }, - { - "id": 51, - "name": "Color Switch", - "version": 1, - "isSecure": false - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 2, - "isSecure": false - }, - { - "id": 134, - "name": "Version", - "version": 2, - "isSecure": false - }, - { - "id": 89, - "name": "Association Group Information", - "version": 1, - "isSecure": false - }, - { - "id": 133, - "name": "Association", - "version": 2, - "isSecure": false - }, - { - "id": 112, - "name": "Configuration", - "version": 2, - "isSecure": false - }, - { - "id": 119, - "name": "Node Naming and Location", - "version": 1, - "isSecure": false - }, - { - "id": 90, - "name": "Device Reset Locally", - "version": 1, - "isSecure": false - }, - { - "id": 122, - "name": "Firmware Update Meta Data", - "version": 2, - "isSecure": false - }, - { - "id": 115, - "name": "Powerlevel", - "version": 1, - "isSecure": false - } - ], "interviewStage": "Complete", "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x001e:0x0004:0x0001:1.8", "statistics": { diff --git a/tests/components/zwave_js/fixtures/fan_ge_12730_state.json b/tests/components/zwave_js/fixtures/fan_ge_12730_state.json index a1fa0294fd5..59aff4035da 100644 --- a/tests/components/zwave_js/fixtures/fan_ge_12730_state.json +++ b/tests/components/zwave_js/fixtures/fan_ge_12730_state.json @@ -47,10 +47,10 @@ "endpoints": [ { "nodeId": 24, - "index": 0 + "index": 0, + "commandClasses": [] } ], - "commandClasses": [], "values": [ { "endpoint": 0, diff --git a/tests/components/zwave_js/fixtures/fan_generic_state.json b/tests/components/zwave_js/fixtures/fan_generic_state.json index a13b99d882f..29f49bb50dc 100644 --- a/tests/components/zwave_js/fixtures/fan_generic_state.json +++ b/tests/components/zwave_js/fixtures/fan_generic_state.json @@ -57,10 +57,10 @@ "nodeId": 17, "index": 0, "installerIcon": 1024, - "userIcon": 1024 + "userIcon": 1024, + "commandClasses": [] } ], - "commandClasses": [], "values": [ { "commandClassName": "Multilevel Switch", diff --git a/tests/components/zwave_js/fixtures/fan_hs_fc200_state.json b/tests/components/zwave_js/fixtures/fan_hs_fc200_state.json index a47904a6833..d8ae5fc899a 100644 --- a/tests/components/zwave_js/fixtures/fan_hs_fc200_state.json +++ b/tests/components/zwave_js/fixtures/fan_hs_fc200_state.json @@ -63,7 +63,105 @@ }, "mandatorySupportedCCs": [32, 38, 133, 89, 114, 115, 134, 94], "mandatoryControlledCCs": [] - } + }, + "commandClasses": [ + { + "id": 38, + "name": "Multilevel Switch", + "version": 4, + "isSecure": false + }, + { + "id": 43, + "name": "Scene Activation", + "version": 1, + "isSecure": false + }, + { + "id": 44, + "name": "Scene Actuator Configuration", + "version": 1, + "isSecure": false + }, + { + "id": 85, + "name": "Transport Service", + "version": 2, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 1, + "isSecure": false + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 91, + "name": "Central Scene", + "version": 3, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 3, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 115, + "name": "Powerlevel", + "version": 1, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 4, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 3, + "isSecure": false + }, + { + "id": 159, + "name": "Security 2", + "version": 1, + "isSecure": true + } + ] } ], "values": [ @@ -9859,104 +9957,6 @@ "mandatorySupportedCCs": [32, 38, 133, 89, 114, 115, 134, 94], "mandatoryControlledCCs": [] }, - "commandClasses": [ - { - "id": 38, - "name": "Multilevel Switch", - "version": 4, - "isSecure": false - }, - { - "id": 43, - "name": "Scene Activation", - "version": 1, - "isSecure": false - }, - { - "id": 44, - "name": "Scene Actuator Configuration", - "version": 1, - "isSecure": false - }, - { - "id": 85, - "name": "Transport Service", - "version": 2, - "isSecure": false - }, - { - "id": 89, - "name": "Association Group Information", - "version": 1, - "isSecure": false - }, - { - "id": 90, - "name": "Device Reset Locally", - "version": 1, - "isSecure": false - }, - { - "id": 91, - "name": "Central Scene", - "version": 3, - "isSecure": false - }, - { - "id": 94, - "name": "Z-Wave Plus Info", - "version": 2, - "isSecure": false - }, - { - "id": 108, - "name": "Supervision", - "version": 1, - "isSecure": false - }, - { - "id": 112, - "name": "Configuration", - "version": 3, - "isSecure": false - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 2, - "isSecure": false - }, - { - "id": 115, - "name": "Powerlevel", - "version": 1, - "isSecure": false - }, - { - "id": 122, - "name": "Firmware Update Meta Data", - "version": 4, - "isSecure": false - }, - { - "id": 133, - "name": "Association", - "version": 2, - "isSecure": false - }, - { - "id": 134, - "name": "Version", - "version": 3, - "isSecure": false - }, - { - "id": 159, - "name": "Security 2", - "version": 1, - "isSecure": true - } - ], "interviewStage": "Complete", "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x000c:0x0203:0x0001:50.5", "statistics": { diff --git a/tests/components/zwave_js/fixtures/fortrezz_ssa1_siren_state.json b/tests/components/zwave_js/fixtures/fortrezz_ssa1_siren_state.json index f24f611ebe9..98f26c9a669 100644 --- a/tests/components/zwave_js/fixtures/fortrezz_ssa1_siren_state.json +++ b/tests/components/zwave_js/fixtures/fortrezz_ssa1_siren_state.json @@ -62,7 +62,39 @@ }, "mandatorySupportedCCs": [32, 38], "mandatoryControlledCCs": [] - } + }, + "commandClasses": [ + { + "id": 38, + "name": "Multilevel Switch", + "version": 1, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 1, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 1, + "isSecure": false + }, + { + "id": 113, + "name": "Notification", + "version": 2, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": false + } + ] } ], "values": [ @@ -306,38 +338,6 @@ "mandatorySupportedCCs": [32, 38], "mandatoryControlledCCs": [] }, - "commandClasses": [ - { - "id": 38, - "name": "Multilevel Switch", - "version": 1, - "isSecure": false - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 1, - "isSecure": false - }, - { - "id": 134, - "name": "Version", - "version": 1, - "isSecure": false - }, - { - "id": 113, - "name": "Notification", - "version": 2, - "isSecure": false - }, - { - "id": 112, - "name": "Configuration", - "version": 1, - "isSecure": false - } - ], "interviewStage": "Complete", "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0084:0x0313:0x010b:1.11", "statistics": { diff --git a/tests/components/zwave_js/fixtures/fortrezz_ssa3_siren_state.json b/tests/components/zwave_js/fixtures/fortrezz_ssa3_siren_state.json index 5768510fb3d..86d04a3fa59 100644 --- a/tests/components/zwave_js/fixtures/fortrezz_ssa3_siren_state.json +++ b/tests/components/zwave_js/fixtures/fortrezz_ssa3_siren_state.json @@ -56,7 +56,39 @@ }, "mandatorySupportedCCs": [32, 38], "mandatoryControlledCCs": [] - } + }, + "commandClasses": [ + { + "id": 38, + "name": "Multilevel Switch", + "version": 1, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 1, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 1, + "isSecure": false + }, + { + "id": 113, + "name": "Notification", + "version": 2, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": false + } + ] } ], "values": [ @@ -298,38 +330,6 @@ "mandatorySupportedCCs": [32, 38], "mandatoryControlledCCs": [] }, - "commandClasses": [ - { - "id": 38, - "name": "Multilevel Switch", - "version": 1, - "isSecure": false - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 1, - "isSecure": false - }, - { - "id": 134, - "name": "Version", - "version": 1, - "isSecure": false - }, - { - "id": 113, - "name": "Notification", - "version": 2, - "isSecure": false - }, - { - "id": 112, - "name": "Configuration", - "version": 1, - "isSecure": false - } - ], "interviewStage": "Complete", "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0084:0x0331:0x010b:1.11", "statistics": { diff --git a/tests/components/zwave_js/fixtures/inovelli_lzw36_state.json b/tests/components/zwave_js/fixtures/inovelli_lzw36_state.json index b5986aaf35d..2dbbadcf138 100644 --- a/tests/components/zwave_js/fixtures/inovelli_lzw36_state.json +++ b/tests/components/zwave_js/fixtures/inovelli_lzw36_state.json @@ -65,116 +65,116 @@ "aggregatedEndpointCount": 0, "interviewAttempts": 1, "interviewStage": 7, - "commandClasses": [ - { - "id": 38, - "name": "Multilevel Switch", - "version": 4, - "isSecure": false - }, - { - "id": 94, - "name": "Z-Wave Plus Info", - "version": 2, - "isSecure": false - }, - { - "id": 152, - "name": "Security", - "version": 1, - "isSecure": true - }, - { - "id": 108, - "name": "Supervision", - "version": 1, - "isSecure": false - }, - { - "id": 112, - "name": "Configuration", - "version": 4, - "isSecure": false - }, - { - "id": 133, - "name": "Association", - "version": 2, - "isSecure": false - }, - { - "id": 89, - "name": "Association Group Information", - "version": 3, - "isSecure": false - }, - { - "id": 142, - "name": "Multi Channel Association", - "version": 3, - "isSecure": false - }, - { - "id": 134, - "name": "Version", - "version": 3, - "isSecure": false - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 2, - "isSecure": false - }, - { - "id": 90, - "name": "Device Reset Locally", - "version": 1, - "isSecure": false - }, - { - "id": 117, - "name": "Protection", - "version": 2, - "isSecure": false - }, - { - "id": 122, - "name": "Firmware Update Meta Data", - "version": 5, - "isSecure": false - }, - { - "id": 91, - "name": "Central Scene", - "version": 3, - "isSecure": false - }, - { - "id": 135, - "name": "Indicator", - "version": 3, - "isSecure": false - }, - { - "id": 96, - "name": "Multi Channel", - "version": 4, - "isSecure": false - }, - { - "id": 50, - "name": "Meter", - "version": 3, - "isSecure": false - } - ], "endpoints": [ { "nodeId": 19, "index": 0, "installerIcon": 7168, - "userIcon": 7168 + "userIcon": 7168, + "commandClasses": [ + { + "id": 38, + "name": "Multilevel Switch", + "version": 4, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 152, + "name": "Security", + "version": 1, + "isSecure": true + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 4, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 3, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 3, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 117, + "name": "Protection", + "version": 2, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 5, + "isSecure": false + }, + { + "id": 91, + "name": "Central Scene", + "version": 3, + "isSecure": false + }, + { + "id": 135, + "name": "Indicator", + "version": 3, + "isSecure": false + }, + { + "id": 96, + "name": "Multi Channel", + "version": 4, + "isSecure": false + }, + { + "id": 50, + "name": "Meter", + "version": 3, + "isSecure": false + } + ] }, { "nodeId": 19, diff --git a/tests/components/zwave_js/fixtures/light_color_null_values_state.json b/tests/components/zwave_js/fixtures/light_color_null_values_state.json index 6f4055a66fa..92e7e4ef30c 100644 --- a/tests/components/zwave_js/fixtures/light_color_null_values_state.json +++ b/tests/components/zwave_js/fixtures/light_color_null_values_state.json @@ -83,7 +83,75 @@ }, "mandatorySupportedCCs": [32], "mandatoryControlledCCs": [] - } + }, + "commandClasses": [ + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 38, + "name": "Multilevel Switch", + "version": 2, + "isSecure": false + }, + { + "id": 51, + "name": "Color Switch", + "version": 1, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 1, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 2, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 2, + "isSecure": false + }, + { + "id": 152, + "name": "Security", + "version": 1, + "isSecure": true + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + } + ] } ], "values": [ @@ -616,73 +684,5 @@ "mandatorySupportedCCs": [32], "mandatoryControlledCCs": [] }, - "commandClasses": [ - { - "id": 94, - "name": "Z-Wave Plus Info", - "version": 2, - "isSecure": false - }, - { - "id": 38, - "name": "Multilevel Switch", - "version": 2, - "isSecure": false - }, - { - "id": 51, - "name": "Color Switch", - "version": 1, - "isSecure": false - }, - { - "id": 112, - "name": "Configuration", - "version": 1, - "isSecure": false - }, - { - "id": 133, - "name": "Association", - "version": 2, - "isSecure": false - }, - { - "id": 89, - "name": "Association Group Information", - "version": 1, - "isSecure": false - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 2, - "isSecure": false - }, - { - "id": 134, - "name": "Version", - "version": 2, - "isSecure": false - }, - { - "id": 122, - "name": "Firmware Update Meta Data", - "version": 2, - "isSecure": false - }, - { - "id": 152, - "name": "Security", - "version": 1, - "isSecure": true - }, - { - "id": 90, - "name": "Device Reset Locally", - "version": 1, - "isSecure": false - } - ], "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/lock_august_asl03_state.json b/tests/components/zwave_js/fixtures/lock_august_asl03_state.json index 2b092b9d3b0..07c4a441d02 100644 --- a/tests/components/zwave_js/fixtures/lock_august_asl03_state.json +++ b/tests/components/zwave_js/fixtures/lock_august_asl03_state.json @@ -58,10 +58,10 @@ "nodeId": 6, "index": 0, "installerIcon": 768, - "userIcon": 768 + "userIcon": 768, + "commandClasses": [] } ], - "commandClasses": [], "values": [ { "commandClassName": "Door Lock", diff --git a/tests/components/zwave_js/fixtures/lock_id_lock_as_id150_state.json b/tests/components/zwave_js/fixtures/lock_id_lock_as_id150_state.json index 5e64724ba3b..3e3e9a7e0df 100644 --- a/tests/components/zwave_js/fixtures/lock_id_lock_as_id150_state.json +++ b/tests/components/zwave_js/fixtures/lock_id_lock_as_id150_state.json @@ -48,7 +48,87 @@ "nodeId": 60, "index": 0, "installerIcon": 768, - "userIcon": 768 + "userIcon": 768, + "commandClasses": [ + { + "id": 89, + "name": "Association Group Information", + "version": 1, + "isSecure": true + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 98, + "name": "Door Lock", + "version": 2, + "isSecure": true + }, + { + "id": 99, + "name": "User Code", + "version": 1, + "isSecure": true + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": false + }, + { + "id": 113, + "name": "Notification", + "version": 4, + "isSecure": true + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 2, + "isSecure": true + }, + { + "id": 128, + "name": "Battery", + "version": 1, + "isSecure": true + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": true + }, + { + "id": 134, + "name": "Version", + "version": 2, + "isSecure": true + }, + { + "id": 152, + "name": "Security", + "version": 1, + "isSecure": true + } + ] } ], "values": [ @@ -2836,85 +2916,5 @@ "mandatorySupportedCCs": [32, 98, 99, 114, 152, 134], "mandatoryControlledCCs": [] }, - "commandClasses": [ - { - "id": 89, - "name": "Association Group Information", - "version": 1, - "isSecure": true - }, - { - "id": 90, - "name": "Device Reset Locally", - "version": 1, - "isSecure": false - }, - { - "id": 94, - "name": "Z-Wave Plus Info", - "version": 2, - "isSecure": false - }, - { - "id": 98, - "name": "Door Lock", - "version": 2, - "isSecure": true - }, - { - "id": 99, - "name": "User Code", - "version": 1, - "isSecure": true - }, - { - "id": 112, - "name": "Configuration", - "version": 1, - "isSecure": false - }, - { - "id": 113, - "name": "Notification", - "version": 4, - "isSecure": true - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 2, - "isSecure": false - }, - { - "id": 122, - "name": "Firmware Update Meta Data", - "version": 2, - "isSecure": true - }, - { - "id": 128, - "name": "Battery", - "version": 1, - "isSecure": true - }, - { - "id": 133, - "name": "Association", - "version": 2, - "isSecure": true - }, - { - "id": 134, - "name": "Version", - "version": 2, - "isSecure": true - }, - { - "id": 152, - "name": "Security", - "version": 1, - "isSecure": true - } - ], "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/lock_popp_electric_strike_lock_control_state.json b/tests/components/zwave_js/fixtures/lock_popp_electric_strike_lock_control_state.json index 9ae86f1d581..e0d583764cf 100644 --- a/tests/components/zwave_js/fixtures/lock_popp_electric_strike_lock_control_state.json +++ b/tests/components/zwave_js/fixtures/lock_popp_electric_strike_lock_control_state.json @@ -35,7 +35,87 @@ }, "mandatorySupportedCCs": [113, 133, 98, 114, 152, 134], "mandatoryControlledCCs": [] - } + }, + "commandClasses": [ + { + "id": 48, + "name": "Binary Sensor", + "version": 2, + "isSecure": true + }, + { + "id": 89, + "name": "Association Group Information", + "version": 1, + "isSecure": true + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": true + }, + { + "id": 98, + "name": "Door Lock", + "version": 2, + "isSecure": true + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": true + }, + { + "id": 113, + "name": "Notification", + "version": 5, + "isSecure": true + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": true + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 3, + "isSecure": false + }, + { + "id": 128, + "name": "Battery", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": true + }, + { + "id": 134, + "name": "Version", + "version": 2, + "isSecure": true + }, + { + "id": 152, + "name": "Security", + "version": 1, + "isSecure": true + } + ] } ], "values": [ @@ -476,86 +556,6 @@ "mandatorySupportedCCs": [113, 133, 98, 114, 152, 134], "mandatoryControlledCCs": [] }, - "commandClasses": [ - { - "id": 48, - "name": "Binary Sensor", - "version": 2, - "isSecure": true - }, - { - "id": 89, - "name": "Association Group Information", - "version": 1, - "isSecure": true - }, - { - "id": 90, - "name": "Device Reset Locally", - "version": 1, - "isSecure": false - }, - { - "id": 94, - "name": "Z-Wave Plus Info", - "version": 2, - "isSecure": true - }, - { - "id": 98, - "name": "Door Lock", - "version": 2, - "isSecure": true - }, - { - "id": 112, - "name": "Configuration", - "version": 1, - "isSecure": true - }, - { - "id": 113, - "name": "Notification", - "version": 5, - "isSecure": true - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 2, - "isSecure": true - }, - { - "id": 122, - "name": "Firmware Update Meta Data", - "version": 3, - "isSecure": false - }, - { - "id": 128, - "name": "Battery", - "version": 1, - "isSecure": false - }, - { - "id": 133, - "name": "Association", - "version": 2, - "isSecure": true - }, - { - "id": 134, - "name": "Version", - "version": 2, - "isSecure": true - }, - { - "id": 152, - "name": "Security", - "version": 1, - "isSecure": true - } - ], "interviewStage": "Complete", "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0154:0x0005:0x0001:1.3", "statistics": { diff --git a/tests/components/zwave_js/fixtures/multisensor_6_state.json b/tests/components/zwave_js/fixtures/multisensor_6_state.json index 62535414b5b..580393ae6cd 100644 --- a/tests/components/zwave_js/fixtures/multisensor_6_state.json +++ b/tests/components/zwave_js/fixtures/multisensor_6_state.json @@ -61,10 +61,10 @@ "nodeId": 52, "index": 0, "installerIcon": 3079, - "userIcon": 3079 + "userIcon": 3079, + "commandClasses": [] } ], - "commandClasses": [], "values": [ { "commandClassName": "Basic", diff --git a/tests/components/zwave_js/fixtures/nortek_thermostat_added_event.json b/tests/components/zwave_js/fixtures/nortek_thermostat_added_event.json index c2a2802d273..73515b1c2ac 100644 --- a/tests/components/zwave_js/fixtures/nortek_thermostat_added_event.json +++ b/tests/components/zwave_js/fixtures/nortek_thermostat_added_event.json @@ -18,10 +18,10 @@ "endpoints": [ { "nodeId": 67, - "index": 0 + "index": 0, + "commandClasses": [] } ], - "commandClasses": [], "values": [ { "commandClassName": "Basic", diff --git a/tests/components/zwave_js/fixtures/nortek_thermostat_removed_event.json b/tests/components/zwave_js/fixtures/nortek_thermostat_removed_event.json index 48885802751..8491e65c037 100644 --- a/tests/components/zwave_js/fixtures/nortek_thermostat_removed_event.json +++ b/tests/components/zwave_js/fixtures/nortek_thermostat_removed_event.json @@ -53,10 +53,10 @@ "endpoints": [ { "nodeId": 67, - "index": 0 + "index": 0, + "commandClasses": [] } ], - "commandClasses": [], "values": [ { "commandClassName": "Manufacturer Specific", diff --git a/tests/components/zwave_js/fixtures/nortek_thermostat_state.json b/tests/components/zwave_js/fixtures/nortek_thermostat_state.json index 912cbe30574..a0cd7867b1a 100644 --- a/tests/components/zwave_js/fixtures/nortek_thermostat_state.json +++ b/tests/components/zwave_js/fixtures/nortek_thermostat_state.json @@ -61,10 +61,10 @@ "nodeId": 67, "index": 0, "installerIcon": 4608, - "userIcon": 4608 + "userIcon": 4608, + "commandClasses": [] } ], - "commandClasses": [], "values": [ { "commandClassName": "Manufacturer Specific", diff --git a/tests/components/zwave_js/fixtures/null_name_check_state.json b/tests/components/zwave_js/fixtures/null_name_check_state.json index b283041c3c6..b0ee80b146b 100644 --- a/tests/components/zwave_js/fixtures/null_name_check_state.json +++ b/tests/components/zwave_js/fixtures/null_name_check_state.json @@ -31,7 +31,81 @@ "nodeId": 10, "index": 0, "installerIcon": 3328, - "userIcon": 3328 + "userIcon": 3328, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 1, + "isSecure": false + }, + { + "id": 49, + "name": "Multilevel Sensor", + "version": 7, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 1, + "isSecure": false + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 96, + "name": "Multi Channel", + "version": 4, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 3, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 2, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + } + ] }, { "nodeId": 10, @@ -337,79 +411,5 @@ "mandatorySupportedCCs": [32, 49], "mandatoryControlledCCs": [] }, - "commandClasses": [ - { - "id": 37, - "name": "Binary Switch", - "version": 1, - "isSecure": false - }, - { - "id": 49, - "name": "Multilevel Sensor", - "version": 7, - "isSecure": false - }, - { - "id": 89, - "name": "Association Group Information", - "version": 1, - "isSecure": false - }, - { - "id": 90, - "name": "Device Reset Locally", - "version": 1, - "isSecure": false - }, - { - "id": 94, - "name": "Z-Wave Plus Info", - "version": 2, - "isSecure": false - }, - { - "id": 96, - "name": "Multi Channel", - "version": 4, - "isSecure": false - }, - { - "id": 112, - "name": "Configuration", - "version": 1, - "isSecure": false - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 2, - "isSecure": false - }, - { - "id": 122, - "name": "Firmware Update Meta Data", - "version": 3, - "isSecure": false - }, - { - "id": 133, - "name": "Association", - "version": 2, - "isSecure": false - }, - { - "id": 134, - "name": "Version", - "version": 2, - "isSecure": false - }, - { - "id": 142, - "name": "Multi Channel Association", - "version": 3, - "isSecure": false - } - ], "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/srt321_hrt4_zw_state.json b/tests/components/zwave_js/fixtures/srt321_hrt4_zw_state.json index 836cb20cf34..ac5232d55e0 100644 --- a/tests/components/zwave_js/fixtures/srt321_hrt4_zw_state.json +++ b/tests/components/zwave_js/fixtures/srt321_hrt4_zw_state.json @@ -53,36 +53,36 @@ "neighbors": [1, 5, 10, 12, 13, 14, 15, 18, 21], "interviewAttempts": 1, "interviewStage": 7, - "commandClasses": [ - { - "id": 37, - "name": "Binary Switch", - "version": 1, - "isSecure": false - }, - { - "id": 64, - "name": "Thermostat Mode", - "version": 1, - "isSecure": false - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 1, - "isSecure": false - }, - { - "id": 134, - "name": "Version", - "version": 1, - "isSecure": false - } - ], "endpoints": [ { "nodeId": 20, - "index": 0 + "index": 0, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 1, + "isSecure": false + }, + { + "id": 64, + "name": "Thermostat Mode", + "version": 1, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 1, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 1, + "isSecure": false + } + ] } ], "values": [ diff --git a/tests/components/zwave_js/fixtures/vision_security_zl7432_state.json b/tests/components/zwave_js/fixtures/vision_security_zl7432_state.json index c510c60a479..4878a26beab 100644 --- a/tests/components/zwave_js/fixtures/vision_security_zl7432_state.json +++ b/tests/components/zwave_js/fixtures/vision_security_zl7432_state.json @@ -60,7 +60,39 @@ }, "mandatorySupportedCCs": [32, 37, 39], "mandatoryControlledCCs": [] - } + }, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 1, + "isSecure": false + }, + { + "id": 96, + "name": "Multi Channel", + "version": 3, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 1, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 1, + "isSecure": false + } + ] }, { "nodeId": 7, @@ -363,37 +395,5 @@ "mandatorySupportedCCs": [32, 37, 39], "mandatoryControlledCCs": [] }, - "commandClasses": [ - { - "id": 37, - "name": "Binary Switch", - "version": 1, - "isSecure": false - }, - { - "id": 96, - "name": "Multi Channel", - "version": 3, - "isSecure": false - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 1, - "isSecure": false - }, - { - "id": 133, - "name": "Association", - "version": 1, - "isSecure": false - }, - { - "id": 134, - "name": "Version", - "version": 1, - "isSecure": false - } - ], "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/zen_31_state.json b/tests/components/zwave_js/fixtures/zen_31_state.json index 982e96d9adf..0d307154359 100644 --- a/tests/components/zwave_js/fixtures/zen_31_state.json +++ b/tests/components/zwave_js/fixtures/zen_31_state.json @@ -66,7 +66,129 @@ 32, 38, 133, 89, 51, 90, 114, 115, 159, 108, 85, 134, 94 ], "mandatoryControlledCCs": [] - } + }, + "commandClasses": [ + { + "id": 38, + "name": "Multilevel Switch", + "version": 4, + "isSecure": false + }, + { + "id": 49, + "name": "Multilevel Sensor", + "version": 11, + "isSecure": false + }, + { + "id": 50, + "name": "Meter", + "version": 3, + "isSecure": false + }, + { + "id": 51, + "name": "Color Switch", + "version": 3, + "isSecure": false + }, + { + "id": 86, + "name": "CRC-16 Encapsulation", + "version": 1, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 2, + "isSecure": false + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 91, + "name": "Central Scene", + "version": 3, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 96, + "name": "Multi Channel", + "version": 4, + "isSecure": false + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": false + }, + { + "id": 113, + "name": "Notification", + "version": 8, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 117, + "name": "Protection", + "version": 2, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 4, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 2, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + }, + { + "id": 152, + "name": "Security", + "version": 1, + "isSecure": true + } + ] }, { "nodeId": 94, @@ -2587,127 +2709,5 @@ ], "mandatoryControlledCCs": [] }, - "commandClasses": [ - { - "id": 38, - "name": "Multilevel Switch", - "version": 4, - "isSecure": false - }, - { - "id": 49, - "name": "Multilevel Sensor", - "version": 11, - "isSecure": false - }, - { - "id": 50, - "name": "Meter", - "version": 3, - "isSecure": false - }, - { - "id": 51, - "name": "Color Switch", - "version": 3, - "isSecure": false - }, - { - "id": 86, - "name": "CRC-16 Encapsulation", - "version": 1, - "isSecure": false - }, - { - "id": 89, - "name": "Association Group Information", - "version": 2, - "isSecure": false - }, - { - "id": 90, - "name": "Device Reset Locally", - "version": 1, - "isSecure": false - }, - { - "id": 91, - "name": "Central Scene", - "version": 3, - "isSecure": false - }, - { - "id": 94, - "name": "Z-Wave Plus Info", - "version": 2, - "isSecure": false - }, - { - "id": 96, - "name": "Multi Channel", - "version": 4, - "isSecure": false - }, - { - "id": 108, - "name": "Supervision", - "version": 1, - "isSecure": false - }, - { - "id": 112, - "name": "Configuration", - "version": 1, - "isSecure": false - }, - { - "id": 113, - "name": "Notification", - "version": 8, - "isSecure": false - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 2, - "isSecure": false - }, - { - "id": 117, - "name": "Protection", - "version": 2, - "isSecure": false - }, - { - "id": 122, - "name": "Firmware Update Meta Data", - "version": 4, - "isSecure": false - }, - { - "id": 133, - "name": "Association", - "version": 2, - "isSecure": false - }, - { - "id": 134, - "name": "Version", - "version": 2, - "isSecure": false - }, - { - "id": 142, - "name": "Multi Channel Association", - "version": 3, - "isSecure": false - }, - { - "id": 152, - "name": "Security", - "version": 1, - "isSecure": true - } - ], "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/zp3111-5_not_ready_state.json b/tests/components/zwave_js/fixtures/zp3111-5_not_ready_state.json index 1c4805b5c22..4e7d5f6a9dc 100644 --- a/tests/components/zwave_js/fixtures/zp3111-5_not_ready_state.json +++ b/tests/components/zwave_js/fixtures/zp3111-5_not_ready_state.json @@ -26,7 +26,8 @@ }, "mandatorySupportedCCs": [], "mandatoryControlledCCs": [] - } + }, + "commandClasses": [] } ], "values": [], @@ -53,7 +54,6 @@ "mandatorySupportedCCs": [], "mandatoryControlledCCs": [] }, - "commandClasses": [], "interviewStage": "ProtocolInfo", "statistics": { "commandsTX": 0, diff --git a/tests/components/zwave_js/fixtures/zp3111-5_state.json b/tests/components/zwave_js/fixtures/zp3111-5_state.json index c9d37b74c29..54f37d389dd 100644 --- a/tests/components/zwave_js/fixtures/zp3111-5_state.json +++ b/tests/components/zwave_js/fixtures/zp3111-5_state.json @@ -63,7 +63,87 @@ }, "mandatorySupportedCCs": [], "mandatoryControlledCCs": [] - } + }, + "commandClasses": [ + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 2, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 1, + "isSecure": false + }, + { + "id": 115, + "name": "Powerlevel", + "version": 1, + "isSecure": false + }, + { + "id": 128, + "name": "Battery", + "version": 1, + "isSecure": false + }, + { + "id": 113, + "name": "Notification", + "version": 4, + "isSecure": false + }, + { + "id": 49, + "name": "Multilevel Sensor", + "version": 7, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": false + }, + { + "id": 132, + "name": "Wake Up", + "version": 2, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 2, + "isSecure": false + } + ] } ], "values": [ @@ -607,86 +687,6 @@ "mandatorySupportedCCs": [], "mandatoryControlledCCs": [] }, - "commandClasses": [ - { - "id": 94, - "name": "Z-Wave Plus Info", - "version": 2, - "isSecure": false - }, - { - "id": 134, - "name": "Version", - "version": 2, - "isSecure": false - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 2, - "isSecure": false - }, - { - "id": 90, - "name": "Device Reset Locally", - "version": 1, - "isSecure": false - }, - { - "id": 133, - "name": "Association", - "version": 2, - "isSecure": false - }, - { - "id": 89, - "name": "Association Group Information", - "version": 1, - "isSecure": false - }, - { - "id": 115, - "name": "Powerlevel", - "version": 1, - "isSecure": false - }, - { - "id": 128, - "name": "Battery", - "version": 1, - "isSecure": false - }, - { - "id": 113, - "name": "Notification", - "version": 4, - "isSecure": false - }, - { - "id": 49, - "name": "Multilevel Sensor", - "version": 7, - "isSecure": false - }, - { - "id": 112, - "name": "Configuration", - "version": 1, - "isSecure": false - }, - { - "id": 132, - "name": "Wake Up", - "version": 2, - "isSecure": false - }, - { - "id": 122, - "name": "Firmware Update Meta Data", - "version": 2, - "isSecure": false - } - ], "interviewStage": "Complete", "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0109:0x2021:0x2101:5.1", "statistics": { diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index 57f552c9502..d038949d494 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -211,8 +211,8 @@ async def test_on_node_added_not_ready( client.driver.receive_event(event) await hass.async_block_till_done() - # the only entities are the node status sensor, ping button, and firmware update - assert len(hass.states.async_all()) == 3 + # the only entities are the node status sensor and ping button + assert len(hass.states.async_all()) == 2 device = dev_reg.async_get_device(identifiers={(DOMAIN, device_id)}) assert device @@ -254,8 +254,8 @@ async def test_existing_node_not_ready(hass, zp3111_not_ready, client, integrati assert not device.model assert not device.sw_version - # the only entities are the node status sensor, ping button, and firmware update - assert len(hass.states.async_all()) == 3 + # the only entities are the node status sensor and ping button + assert len(hass.states.async_all()) == 2 device = dev_reg.async_get_device(identifiers={(DOMAIN, device_id)}) assert device diff --git a/tests/components/zwave_js/test_update.py b/tests/components/zwave_js/test_update.py index c9ec8fa68c6..76fecfdee6d 100644 --- a/tests/components/zwave_js/test_update.py +++ b/tests/components/zwave_js/test_update.py @@ -19,9 +19,9 @@ from homeassistant.components.zwave_js.helpers import get_valueless_base_unique_ from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_registry import async_get -from homeassistant.util import datetime as dt_util +from homeassistant.util import dt as dt_util -from tests.common import async_fire_time_changed +from tests.common import MockConfigEntry, async_fire_time_changed UPDATE_ENTITY = "update.z_wave_thermostat_firmware" FIRMWARE_UPDATES = { @@ -162,14 +162,12 @@ async def test_update_entity_success( client.async_send_command.reset_mock() -async def test_update_entity_failure( +async def test_update_entity_install_failure( hass, client, climate_radio_thermostat_ct100_plus_different_endpoints, controller_node, integration, - caplog, - hass_ws_client, ): """Test update entity failed install.""" client.async_send_command.return_value = FIRMWARE_UPDATES @@ -194,15 +192,15 @@ async def test_update_entity_failure( async def test_update_entity_sleep( hass, client, - multisensor_6, + zen_31, integration, ): """Test update occurs when device is asleep after it wakes up.""" event = Event( "sleep", - data={"source": "node", "event": "sleep", "nodeId": multisensor_6.node_id}, + data={"source": "node", "event": "sleep", "nodeId": zen_31.node_id}, ) - multisensor_6.receive_event(event) + zen_31.receive_event(event) client.async_send_command.reset_mock() client.async_send_command.return_value = FIRMWARE_UPDATES @@ -215,9 +213,9 @@ async def test_update_entity_sleep( event = Event( "wake up", - data={"source": "node", "event": "wake up", "nodeId": multisensor_6.node_id}, + data={"source": "node", "event": "wake up", "nodeId": zen_31.node_id}, ) - multisensor_6.receive_event(event) + zen_31.receive_event(event) await hass.async_block_till_done() # Now that the node is up we can check for updates @@ -225,21 +223,21 @@ async def test_update_entity_sleep( args = client.async_send_command.call_args_list[0][0][0] assert args["command"] == "controller.get_available_firmware_updates" - assert args["nodeId"] == multisensor_6.node_id + assert args["nodeId"] == zen_31.node_id async def test_update_entity_dead( hass, client, - multisensor_6, + zen_31, integration, ): """Test update occurs when device is dead after it becomes alive.""" event = Event( "dead", - data={"source": "node", "event": "dead", "nodeId": multisensor_6.node_id}, + data={"source": "node", "event": "dead", "nodeId": zen_31.node_id}, ) - multisensor_6.receive_event(event) + zen_31.receive_event(event) client.async_send_command.reset_mock() client.async_send_command.return_value = FIRMWARE_UPDATES @@ -252,9 +250,9 @@ async def test_update_entity_dead( event = Event( "alive", - data={"source": "node", "event": "alive", "nodeId": multisensor_6.node_id}, + data={"source": "node", "event": "alive", "nodeId": zen_31.node_id}, ) - multisensor_6.receive_event(event) + zen_31.receive_event(event) await hass.async_block_till_done() # Now that the node is up we can check for updates @@ -262,4 +260,54 @@ async def test_update_entity_dead( args = client.async_send_command.call_args_list[0][0][0] assert args["command"] == "controller.get_available_firmware_updates" - assert args["nodeId"] == multisensor_6.node_id + assert args["nodeId"] == zen_31.node_id + + +async def test_update_entity_ha_not_running( + hass, + client, + zen_31, + hass_ws_client, +): + """Test update occurs after HA starts.""" + await hass.async_stop() + + entry = MockConfigEntry(domain="zwave_js", data={"url": "ws://test.org"}) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(client.async_send_command.call_args_list) == 0 + + await hass.async_start() + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args_list[0][0][0] + assert args["command"] == "controller.get_available_firmware_updates" + assert args["nodeId"] == zen_31.node_id + + +async def test_update_entity_failure( + hass, + client, + climate_radio_thermostat_ct100_plus_different_endpoints, + controller_node, + integration, +): + """Test update entity update failed.""" + assert len(client.async_send_command.call_args_list) == 0 + client.async_send_command.side_effect = FailedZWaveCommand("test", 260, "test") + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(days=1)) + await hass.async_block_till_done() + + state = hass.states.get(UPDATE_ENTITY) + assert state + assert state.state == STATE_OFF + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args_list[0][0][0] + assert args["command"] == "controller.get_available_firmware_updates" + assert ( + args["nodeId"] + == climate_radio_thermostat_ct100_plus_different_endpoints.node_id + ) From b2f86ddf76afc37f578f174db9fca148fefed6c3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 6 Sep 2022 08:48:39 -0500 Subject: [PATCH 150/955] Bump bluetooth-auto-recovery to 0.3.1 (#77898) --- 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 b98312040f0..ca6a76c55ae 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -7,7 +7,7 @@ "requirements": [ "bleak==0.16.0", "bluetooth-adapters==0.3.4", - "bluetooth-auto-recovery==0.3.0" + "bluetooth-auto-recovery==0.3.1" ], "codeowners": ["@bdraco"], "config_flow": true, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 1299abcd61b..09102494074 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,7 +12,7 @@ awesomeversion==22.8.0 bcrypt==3.1.7 bleak==0.16.0 bluetooth-adapters==0.3.4 -bluetooth-auto-recovery==0.3.0 +bluetooth-auto-recovery==0.3.1 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==37.0.4 diff --git a/requirements_all.txt b/requirements_all.txt index bd6b6618852..7e85b21b3e0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -433,7 +433,7 @@ bluemaestro-ble==0.2.0 bluetooth-adapters==0.3.4 # homeassistant.components.bluetooth -bluetooth-auto-recovery==0.3.0 +bluetooth-auto-recovery==0.3.1 # homeassistant.components.bond bond-async==0.1.22 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8fa6eb8c8f0..229beae8150 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -344,7 +344,7 @@ bluemaestro-ble==0.2.0 bluetooth-adapters==0.3.4 # homeassistant.components.bluetooth -bluetooth-auto-recovery==0.3.0 +bluetooth-auto-recovery==0.3.1 # homeassistant.components.bond bond-async==0.1.22 From d550b17bd921d5db0957cbec1c5271c5cf44eeeb Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Tue, 6 Sep 2022 17:33:16 +0200 Subject: [PATCH 151/955] Use identifiers host and serial number to match device (#75657) --- homeassistant/components/upnp/__init__.py | 29 ++++++++++------ homeassistant/components/upnp/config_flow.py | 8 +++-- homeassistant/components/upnp/const.py | 5 +-- homeassistant/components/upnp/device.py | 8 ++++- tests/components/upnp/conftest.py | 7 ++-- tests/components/upnp/test_config_flow.py | 36 +++++++++++++++++++- 6 files changed, 75 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index a45e58f28bc..95531450e5a 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -25,15 +25,18 @@ from homeassistant.helpers.update_coordinator import ( ) from .const import ( + CONFIG_ENTRY_HOST, CONFIG_ENTRY_MAC_ADDRESS, CONFIG_ENTRY_ORIGINAL_UDN, CONFIG_ENTRY_ST, CONFIG_ENTRY_UDN, DEFAULT_SCAN_INTERVAL, DOMAIN, + IDENTIFIER_HOST, + IDENTIFIER_SERIAL_NUMBER, LOGGER, ) -from .device import Device, async_create_device, async_get_mac_address_from_host +from .device import Device, async_create_device NOTIFICATION_ID = "upnp_notification" NOTIFICATION_TITLE = "UPnP/IGD Setup" @@ -106,24 +109,30 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: device.original_udn = entry.data[CONFIG_ENTRY_ORIGINAL_UDN] # Store mac address for changed UDN matching. - if device.host: - device.mac_address = await async_get_mac_address_from_host(hass, device.host) - if device.mac_address and not entry.data.get("CONFIG_ENTRY_MAC_ADDRESS"): + device_mac_address = await device.async_get_mac_address() + if device_mac_address and not entry.data.get(CONFIG_ENTRY_MAC_ADDRESS): hass.config_entries.async_update_entry( entry=entry, data={ **entry.data, - CONFIG_ENTRY_MAC_ADDRESS: device.mac_address, + CONFIG_ENTRY_MAC_ADDRESS: device_mac_address, + CONFIG_ENTRY_HOST: device.host, }, ) + identifiers = {(DOMAIN, device.usn)} + if device.host: + identifiers.add((IDENTIFIER_HOST, device.host)) + if device.serial_number: + identifiers.add((IDENTIFIER_SERIAL_NUMBER, device.serial_number)) + connections = {(dr.CONNECTION_UPNP, device.udn)} - if device.mac_address: - connections.add((dr.CONNECTION_NETWORK_MAC, device.mac_address)) + if device_mac_address: + connections.add((dr.CONNECTION_NETWORK_MAC, device_mac_address)) device_registry = dr.async_get(hass) device_entry = device_registry.async_get_device( - identifiers=set(), connections=connections + identifiers=identifiers, connections=connections ) if device_entry: LOGGER.debug( @@ -136,7 +145,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: device_entry = device_registry.async_get_or_create( config_entry_id=entry.entry_id, connections=connections, - identifiers={(DOMAIN, device.usn)}, + identifiers=identifiers, name=device.name, manufacturer=device.manufacturer, model=device.model_name, @@ -148,7 +157,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Update identifier. device_entry = device_registry.async_update_device( device_entry.id, - new_identifiers={(DOMAIN, device.usn)}, + new_identifiers=identifiers, ) assert device_entry diff --git a/homeassistant/components/upnp/config_flow.py b/homeassistant/components/upnp/config_flow.py index 7d4e768e855..3386cf40711 100644 --- a/homeassistant/components/upnp/config_flow.py +++ b/homeassistant/components/upnp/config_flow.py @@ -13,6 +13,7 @@ from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult from .const import ( + CONFIG_ENTRY_HOST, CONFIG_ENTRY_LOCATION, CONFIG_ENTRY_MAC_ADDRESS, CONFIG_ENTRY_ORIGINAL_UDN, @@ -161,22 +162,25 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): unique_id = discovery_info.ssdp_usn await self.async_set_unique_id(unique_id) 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. # 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, }, ) # Handle devices changing their UDN, only allow a single host. for entry in self._async_current_entries(include_ignore=True): entry_mac_address = entry.data.get(CONFIG_ENTRY_MAC_ADDRESS) - entry_st = entry.data.get(CONFIG_ENTRY_ST) - if entry_mac_address != mac_address: + entry_host = entry.data.get(CONFIG_ENTRY_HOST) + if entry_mac_address != mac_address and entry_host != host: continue + entry_st = entry.data.get(CONFIG_ENTRY_ST) if discovery_info.ssdp_st != entry_st: # Check ssdp_st to prevent swapping between IGDv1 and IGDv2. continue diff --git a/homeassistant/components/upnp/const.py b/homeassistant/components/upnp/const.py index e673922d1c2..023ec82a487 100644 --- a/homeassistant/components/upnp/const.py +++ b/homeassistant/components/upnp/const.py @@ -6,7 +6,6 @@ from homeassistant.const import TIME_SECONDS LOGGER = logging.getLogger(__package__) -CONF_LOCAL_IP = "local_ip" DOMAIN = "upnp" BYTES_RECEIVED = "bytes_received" BYTES_SENT = "bytes_sent" @@ -24,7 +23,9 @@ CONFIG_ENTRY_UDN = "udn" CONFIG_ENTRY_ORIGINAL_UDN = "original_udn" CONFIG_ENTRY_MAC_ADDRESS = "mac_address" CONFIG_ENTRY_LOCATION = "location" +CONFIG_ENTRY_HOST = "host" +IDENTIFIER_HOST = "upnp_host" +IDENTIFIER_SERIAL_NUMBER = "upnp_serial_number" DEFAULT_SCAN_INTERVAL = timedelta(seconds=30).total_seconds() ST_IGD_V1 = "urn:schemas-upnp-org:device:InternetGatewayDevice:1" ST_IGD_V2 = "urn:schemas-upnp-org:device:InternetGatewayDevice:2" -SSDP_SEARCH_TIMEOUT = 4 diff --git a/homeassistant/components/upnp/device.py b/homeassistant/components/upnp/device.py index 3a688b8571d..e06ada02b77 100644 --- a/homeassistant/components/upnp/device.py +++ b/homeassistant/components/upnp/device.py @@ -69,9 +69,15 @@ class Device: self.hass = hass self._igd_device = igd_device self.coordinator: DataUpdateCoordinator | None = None - self.mac_address: str | None = None self.original_udn: str | None = None + async def async_get_mac_address(self) -> str | None: + """Get mac address.""" + if not self.host: + return None + + return await async_get_mac_address_from_host(self.hass, self.host) + @property def udn(self) -> str: """Get the UDN.""" diff --git a/tests/components/upnp/conftest.py b/tests/components/upnp/conftest.py index e7cd24d0c7c..b159a371d9a 100644 --- a/tests/components/upnp/conftest.py +++ b/tests/components/upnp/conftest.py @@ -25,7 +25,7 @@ TEST_UDN = "uuid:device" TEST_ST = "urn:schemas-upnp-org:device:InternetGatewayDevice:1" TEST_USN = f"{TEST_UDN}::{TEST_ST}" TEST_LOCATION = "http://192.168.1.1/desc.xml" -TEST_HOSTNAME = urlparse(TEST_LOCATION).hostname +TEST_HOST = urlparse(TEST_LOCATION).hostname TEST_FRIENDLY_NAME = "mock-name" TEST_MAC_ADDRESS = "00:11:22:33:44:55" TEST_DISCOVERY = ssdp.SsdpServiceInfo( @@ -41,10 +41,11 @@ TEST_DISCOVERY = ssdp.SsdpServiceInfo( ssdp.ATTR_UPNP_FRIENDLY_NAME: TEST_FRIENDLY_NAME, ssdp.ATTR_UPNP_MANUFACTURER: "mock-manufacturer", ssdp.ATTR_UPNP_MODEL_NAME: "mock-model-name", + ssdp.ATTR_UPNP_SERIAL: "mock-serial", ssdp.ATTR_UPNP_UDN: TEST_UDN, }, ssdp_headers={ - "_host": TEST_HOSTNAME, + "_host": TEST_HOST, }, ) @@ -54,8 +55,10 @@ def mock_igd_device() -> IgdDevice: """Mock async_upnp_client device.""" mock_upnp_device = create_autospec(UpnpDevice, instance=True) mock_upnp_device.device_url = TEST_DISCOVERY.ssdp_location + mock_upnp_device.serial_number = TEST_DISCOVERY.upnp[ssdp.ATTR_UPNP_SERIAL] mock_igd_device = create_autospec(IgdDevice) + mock_igd_device.device_type = TEST_DISCOVERY.ssdp_st mock_igd_device.name = TEST_DISCOVERY.upnp[ssdp.ATTR_UPNP_FRIENDLY_NAME] mock_igd_device.manufacturer = TEST_DISCOVERY.upnp[ssdp.ATTR_UPNP_MANUFACTURER] mock_igd_device.model_name = TEST_DISCOVERY.upnp[ssdp.ATTR_UPNP_MODEL_NAME] diff --git a/tests/components/upnp/test_config_flow.py b/tests/components/upnp/test_config_flow.py index e89b8274c18..f0a1de1ce37 100644 --- a/tests/components/upnp/test_config_flow.py +++ b/tests/components/upnp/test_config_flow.py @@ -8,6 +8,7 @@ import pytest from homeassistant import config_entries, data_entry_flow from homeassistant.components import ssdp from homeassistant.components.upnp.const import ( + CONFIG_ENTRY_HOST, CONFIG_ENTRY_LOCATION, CONFIG_ENTRY_MAC_ADDRESS, CONFIG_ENTRY_ORIGINAL_UDN, @@ -21,6 +22,7 @@ from homeassistant.core import HomeAssistant from .conftest import ( TEST_DISCOVERY, TEST_FRIENDLY_NAME, + TEST_HOST, TEST_LOCATION, TEST_MAC_ADDRESS, TEST_ST, @@ -140,7 +142,7 @@ async def test_flow_ssdp_no_mac_address(hass: HomeAssistant): @pytest.mark.usefixtures("mock_mac_address_from_host") -async def test_flow_ssdp_discovery_changed_udn(hass: HomeAssistant): +async def test_flow_ssdp_discovery_changed_udn_match_mac(hass: HomeAssistant): """Test config flow: discovery through ssdp, same device, but new UDN, matched on mac address.""" entry = MockConfigEntry( domain=DOMAIN, @@ -171,6 +173,38 @@ async def test_flow_ssdp_discovery_changed_udn(hass: HomeAssistant): assert result["reason"] == "config_entry_updated" +@pytest.mark.usefixtures("mock_mac_address_from_host") +async def test_flow_ssdp_discovery_changed_udn_match_host(hass: HomeAssistant): + """Test config flow: discovery through ssdp, same device, but new UDN, matched on mac address.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=TEST_USN, + data={ + CONFIG_ENTRY_ST: TEST_ST, + CONFIG_ENTRY_UDN: TEST_UDN, + CONFIG_ENTRY_ORIGINAL_UDN: TEST_UDN, + CONFIG_ENTRY_LOCATION: TEST_LOCATION, + CONFIG_ENTRY_HOST: TEST_HOST, + }, + source=config_entries.SOURCE_SSDP, + state=config_entries.ConfigEntryState.LOADED, + ) + entry.add_to_hass(hass) + + # New discovery via step ssdp. + new_udn = TEST_UDN + "2" + new_discovery = deepcopy(TEST_DISCOVERY) + new_discovery.ssdp_usn = f"{new_udn}::{TEST_ST}" + new_discovery.upnp["_udn"] = new_udn + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=new_discovery, + ) + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "config_entry_updated" + + @pytest.mark.usefixtures( "ssdp_instant_discovery", "mock_setup_entry", From 5459b5fdfe6ddae76ef1bf4f89a901e930d380d3 Mon Sep 17 00:00:00 2001 From: Yevhenii Vaskivskyi Date: Tue, 6 Sep 2022 17:34:11 +0200 Subject: [PATCH 152/955] Handle exception on projector being unavailable (#77802) --- homeassistant/components/epson/manifest.json | 2 +- homeassistant/components/epson/media_player.py | 14 +++++++++----- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/epson/manifest.json b/homeassistant/components/epson/manifest.json index 82b74486377..0ba8351fd15 100644 --- a/homeassistant/components/epson/manifest.json +++ b/homeassistant/components/epson/manifest.json @@ -3,7 +3,7 @@ "name": "Epson", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/epson", - "requirements": ["epson-projector==0.4.6"], + "requirements": ["epson-projector==0.5.0"], "codeowners": ["@pszafer"], "iot_class": "local_polling", "loggers": ["epson_projector"] diff --git a/homeassistant/components/epson/media_player.py b/homeassistant/components/epson/media_player.py index 57bb0165f6a..0e70984ac31 100644 --- a/homeassistant/components/epson/media_player.py +++ b/homeassistant/components/epson/media_player.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging -from epson_projector import Projector +from epson_projector import Projector, ProjectorUnavailableError from epson_projector.const import ( BACK, BUSY, @@ -20,7 +20,6 @@ from epson_projector.const import ( POWER, SOURCE, SOURCE_LIST, - STATE_UNAVAILABLE as EPSON_STATE_UNAVAILABLE, TURN_OFF, TURN_ON, VOL_DOWN, @@ -123,11 +122,16 @@ class EpsonProjectorMediaPlayer(MediaPlayerEntity): async def async_update(self) -> None: """Update state of device.""" - power_state = await self._projector.get_power() - _LOGGER.debug("Projector status: %s", power_state) - if not power_state or power_state == EPSON_STATE_UNAVAILABLE: + try: + power_state = await self._projector.get_power() + except ProjectorUnavailableError as ex: + _LOGGER.debug("Projector is unavailable: %s", ex) self._attr_available = False return + if not power_state: + self._attr_available = False + return + _LOGGER.debug("Projector status: %s", power_state) self._attr_available = True if power_state == EPSON_CODES[POWER]: self._attr_state = STATE_ON diff --git a/requirements_all.txt b/requirements_all.txt index 7e85b21b3e0..379538ea883 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -630,7 +630,7 @@ envoy_reader==0.20.1 ephem==4.1.2 # homeassistant.components.epson -epson-projector==0.4.6 +epson-projector==0.5.0 # homeassistant.components.epsonworkforce epsonprinter==0.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 229beae8150..1b833e2d7b0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -477,7 +477,7 @@ envoy_reader==0.20.1 ephem==4.1.2 # homeassistant.components.epson -epson-projector==0.4.6 +epson-projector==0.5.0 # homeassistant.components.faa_delays faadelays==0.0.7 From e3fb04e1166d15f576d4b6fdec962f13871aaafe Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 6 Sep 2022 17:34:59 +0200 Subject: [PATCH 153/955] Add comment to life360 device tracker (#77879) --- homeassistant/components/life360/device_tracker.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/life360/device_tracker.py b/homeassistant/components/life360/device_tracker.py index 1fa63a7659a..f4047574f6a 100644 --- a/homeassistant/components/life360/device_tracker.py +++ b/homeassistant/components/life360/device_tracker.py @@ -166,7 +166,11 @@ class Life360DeviceTracker( @property def force_update(self) -> bool: - """Return True if state updates should be forced.""" + """Return True if state updates should be forced. + + Overridden because CoordinatorEntity sets `should_poll` to False, + which causes TrackerEntity to set `force_update` to True. + """ return False @property From bd84981ae0b1a88beb00c200488fc9a8eb980332 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 6 Sep 2022 18:03:53 +0200 Subject: [PATCH 154/955] Use _attr_force_update in tellstick (#77899) --- homeassistant/components/tellstick/switch.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/tellstick/switch.py b/homeassistant/components/tellstick/switch.py index 5f88bab420b..966a3defd5e 100644 --- a/homeassistant/components/tellstick/switch.py +++ b/homeassistant/components/tellstick/switch.py @@ -42,6 +42,8 @@ def setup_platform( class TellstickSwitch(TellstickDevice, SwitchEntity): """Representation of a Tellstick switch.""" + _attr_force_update = True + def _parse_ha_data(self, kwargs): """Turn the value from HA into something useful.""" @@ -58,8 +60,3 @@ class TellstickSwitch(TellstickDevice, SwitchEntity): self._tellcore_device.turn_on() else: self._tellcore_device.turn_off() - - @property - def force_update(self) -> bool: - """Will trigger anytime the state property is updated.""" - return True From 85beceb53391775d99b9fb89107fc6aa8aca6b4b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 6 Sep 2022 18:17:28 +0200 Subject: [PATCH 155/955] Use attributes in rflink binary sensor (#77901) Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> --- .../components/rflink/binary_sensor.py | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/rflink/binary_sensor.py b/homeassistant/components/rflink/binary_sensor.py index 7b095555376..e307d9de382 100644 --- a/homeassistant/components/rflink/binary_sensor.py +++ b/homeassistant/components/rflink/binary_sensor.py @@ -1,11 +1,14 @@ """Support for Rflink binary sensors.""" from __future__ import annotations +from typing import Any + import voluptuous as vol from homeassistant.components.binary_sensor import ( DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.const import ( @@ -73,12 +76,17 @@ class RflinkBinarySensor(RflinkDevice, BinarySensorEntity, RestoreEntity): """Representation of an Rflink binary sensor.""" def __init__( - self, device_id, device_class=None, force_update=False, off_delay=None, **kwargs - ): + self, + device_id: str, + device_class: BinarySensorDeviceClass | None = None, + force_update: bool = False, + off_delay: int | None = None, + **kwargs: Any, + ) -> None: """Handle sensor specific args and super init.""" self._state = None - self._device_class = device_class - self._force_update = force_update + self._attr_device_class = device_class + self._attr_force_update = force_update self._off_delay = off_delay self._delay_listener = None super().__init__(device_id, **kwargs) @@ -119,13 +127,3 @@ class RflinkBinarySensor(RflinkDevice, BinarySensorEntity, RestoreEntity): def is_on(self): """Return true if the binary sensor is on.""" return self._state - - @property - def device_class(self): - """Return the class of this sensor.""" - return self._device_class - - @property - def force_update(self): - """Force update.""" - return self._force_update From 5de95663a9317ee3779b9a02b70c254720691fff Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 6 Sep 2022 18:47:34 +0200 Subject: [PATCH 156/955] Introduce new StrEnums in media player (#77872) * Introduce RepeatMode enum in media player * Add MediaClass and MediaType --- .../components/media_player/__init__.py | 7 +- .../components/media_player/browse_media.py | 12 ++-- .../components/media_player/const.py | 70 +++++++++++++++++++ pylint/plugins/hass_enforce_type_hints.py | 2 +- 4 files changed, 81 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 29c75a4fc22..c27a81c6dc2 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -128,6 +128,7 @@ from .const import ( # noqa: F401 SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, MediaPlayerEntityFeature, + RepeatMode, ) from .errors import BrowseError @@ -410,7 +411,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: component.async_register_entity_service( SERVICE_REPEAT_SET, - {vol.Required(ATTR_MEDIA_REPEAT): vol.In(REPEAT_MODES)}, + {vol.Required(ATTR_MEDIA_REPEAT): vol.Coerce(RepeatMode)}, "async_set_repeat", [MediaPlayerEntityFeature.REPEAT_SET], ) @@ -801,11 +802,11 @@ class MediaPlayerEntity(Entity): """Enable/disable shuffle mode.""" await self.hass.async_add_executor_job(self.set_shuffle, shuffle) - def set_repeat(self, repeat: str) -> None: + def set_repeat(self, repeat: RepeatMode) -> None: """Set repeat mode.""" raise NotImplementedError() - async def async_set_repeat(self, repeat: str) -> None: + async def async_set_repeat(self, repeat: RepeatMode) -> None: """Set repeat mode.""" await self.hass.async_add_executor_job(self.set_repeat, repeat) diff --git a/homeassistant/components/media_player/browse_media.py b/homeassistant/components/media_player/browse_media.py index e3474eeb58e..81ded203e75 100644 --- a/homeassistant/components/media_player/browse_media.py +++ b/homeassistant/components/media_player/browse_media.py @@ -19,7 +19,7 @@ from homeassistant.helpers.network import ( is_hass_url, ) -from .const import CONTENT_AUTH_EXPIRY_TIME, MEDIA_CLASS_DIRECTORY +from .const import CONTENT_AUTH_EXPIRY_TIME, MediaClass, MediaType # Paths that we don't need to sign PATHS_WITHOUT_AUTH = ("/api/tts_proxy/",) @@ -92,14 +92,14 @@ class BrowseMedia: def __init__( self, *, - media_class: str, + media_class: MediaClass | str, media_content_id: str, - media_content_type: str, + media_content_type: MediaType | str, title: str, can_play: bool, can_expand: bool, children: Sequence[BrowseMedia] | None = None, - children_media_class: str | None = None, + children_media_class: MediaClass | str | None = None, thumbnail: str | None = None, not_shown: int = 0, ) -> None: @@ -115,7 +115,7 @@ class BrowseMedia: self.thumbnail = thumbnail self.not_shown = not_shown - def as_dict(self, *, parent: bool = True) -> dict: + def as_dict(self, *, parent: bool = True) -> dict[str, Any]: """Convert Media class to browse media dictionary.""" if self.children_media_class is None and self.children: self.calculate_children_class() @@ -147,7 +147,7 @@ class BrowseMedia: def calculate_children_class(self) -> None: """Count the children media classes and calculate the correct class.""" - self.children_media_class = MEDIA_CLASS_DIRECTORY + self.children_media_class = MediaClass.DIRECTORY assert self.children is not None proposed_class = self.children[0].media_class if all(child.media_class == proposed_class for child in self.children): diff --git a/homeassistant/components/media_player/const.py b/homeassistant/components/media_player/const.py index 4d534467ad6..2d3fa9c9b3e 100644 --- a/homeassistant/components/media_player/const.py +++ b/homeassistant/components/media_player/const.py @@ -1,6 +1,8 @@ """Provides the constants needed for component.""" from enum import IntEnum +from homeassistant.backports.enum import StrEnum + # How long our auth signature on the content should be valid for CONTENT_AUTH_EXPIRY_TIME = 3600 * 24 @@ -38,6 +40,34 @@ ATTR_SOUND_MODE_LIST = "sound_mode_list" DOMAIN = "media_player" + +class MediaClass(StrEnum): + """Media class for media player entities.""" + + ALBUM = "album" + APP = "app" + ARTIST = "artist" + CHANNEL = "channel" + COMPOSER = "composer" + CONTRIBUTING_ARTIST = "contributing_artist" + DIRECTORY = "directory" + EPISODE = "episode" + GAME = "game" + GENRE = "genre" + IMAGE = "image" + MOVIE = "movie" + MUSIC = "music" + PLAYLIST = "playlist" + PODCAST = "podcast" + SEASON = "season" + TRACK = "track" + TV_SHOW = "tv_show" + URL = "url" + VIDEO = "video" + + +# These MEDIA_CLASS_* constants are deprecated as of Home Assistant 2022.10. +# Please use the MediaClass enum instead. MEDIA_CLASS_ALBUM = "album" MEDIA_CLASS_APP = "app" MEDIA_CLASS_ARTIST = "artist" @@ -59,6 +89,35 @@ MEDIA_CLASS_TV_SHOW = "tv_show" MEDIA_CLASS_URL = "url" MEDIA_CLASS_VIDEO = "video" + +class MediaType(StrEnum): + """Media type for media player entities.""" + + ALBUM = "album" + APP = "app" + APPS = "apps" + ARTIST = "artist" + CHANNEL = "channel" + CHANNELS = "channels" + COMPOSER = "composer" + CONTRIBUTING_ARTIST = "contributing_artist" + EPISODE = "episode" + GAME = "game" + GENRE = "genre" + IMAGE = "image" + MOVIE = "movie" + MUSIC = "music" + PLAYLIST = "playlist" + PODCAST = "podcast" + SEASON = "season" + TRACK = "track" + TVSHOW = "tvshow" + URL = "url" + VIDEO = "video" + + +# These MEDIA_TYPE_* constants are deprecated as of Home Assistant 2022.10. +# Please use the MediaType enum instead. MEDIA_TYPE_ALBUM = "album" MEDIA_TYPE_APP = "app" MEDIA_TYPE_APPS = "apps" @@ -88,6 +147,17 @@ SERVICE_SELECT_SOUND_MODE = "select_sound_mode" SERVICE_SELECT_SOURCE = "select_source" SERVICE_UNJOIN = "unjoin" + +class RepeatMode(StrEnum): + """Repeat mode for media player entities.""" + + ALL = "all" + OFF = "off" + ONE = "one" + + +# These REPEAT_MODE_* constants are deprecated as of Home Assistant 2022.10. +# Please use the RepeatMode enum instead. REPEAT_MODE_ALL = "all" REPEAT_MODE_OFF = "off" REPEAT_MODE_ONE = "one" diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 289f461c223..2418b97c198 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -1741,7 +1741,7 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { TypeHintMatch( function_name="set_repeat", arg_types={ - 1: "str", + 1: "RepeatMode", }, return_type=None, has_async_counterpart=True, From 97d63e5c36fb30cdd9887190df310002bb4bed85 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 6 Sep 2022 18:54:53 +0200 Subject: [PATCH 157/955] Update frontend to 20220906.0 (#77910) --- 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 416634053d6..8abc8fd4e32 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==20220905.0"], + "requirements": ["home-assistant-frontend==20220906.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 09102494074..58a000fbc25 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -19,7 +19,7 @@ cryptography==37.0.4 fnvhash==0.1.0 hass-nabucasa==0.55.0 home-assistant-bluetooth==1.3.0 -home-assistant-frontend==20220905.0 +home-assistant-frontend==20220906.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index 379538ea883..d3436ab3973 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -851,7 +851,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220905.0 +home-assistant-frontend==20220906.0 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1b833e2d7b0..ace37759f9d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -628,7 +628,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220905.0 +home-assistant-frontend==20220906.0 # homeassistant.components.home_connect homeconnect==0.7.2 From 759f12bcdace36e7521de431025293df2c1a58e3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 6 Sep 2022 19:58:27 +0200 Subject: [PATCH 158/955] Use attributes in hvv_departures (#77588) --- .../components/hvv_departures/sensor.py | 73 ++++++------------- 1 file changed, 21 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/hvv_departures/sensor.py b/homeassistant/components/hvv_departures/sensor.py index 93e1002edf4..0a516529386 100644 --- a/homeassistant/components/hvv_departures/sensor.py +++ b/homeassistant/components/hvv_departures/sensor.py @@ -8,7 +8,7 @@ from pygti.exceptions import InvalidAuth from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ATTRIBUTION, ATTR_ID +from homeassistant.const import ATTR_ID from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client from homeassistant.helpers.entity import DeviceInfo @@ -54,18 +54,25 @@ async def async_setup_entry( class HVVDepartureSensor(SensorEntity): """HVVDepartureSensor class.""" + _attr_attribution = ATTRIBUTION + _attr_device_class = SensorDeviceClass.TIMESTAMP + _attr_icon = ICON + def __init__(self, hass, config_entry, session, hub): """Initialize.""" self.config_entry = config_entry self.station_name = self.config_entry.data[CONF_STATION]["name"] - self.attr = {ATTR_ATTRIBUTION: ATTRIBUTION} - self._available = False - self._state = None - self._name = f"Departures at {self.station_name}" + self._attr_extra_state_attributes = {} + self._attr_available = False + self._attr_name = f"Departures at {self.station_name}" self._last_error = None self.gti = hub.gti + station_id = config_entry.data[CONF_STATION]["id"] + station_type = config_entry.data[CONF_STATION]["type"] + self._attr_unique_id = f"{config_entry.entry_id}-{station_id}-{station_type}" + @Throttle(MIN_TIME_BETWEEN_UPDATES) async def async_update(self, **kwargs: Any) -> None: """Update the sensor.""" @@ -95,20 +102,20 @@ class HVVDepartureSensor(SensorEntity): if self._last_error != InvalidAuth: _LOGGER.error("Authentication failed: %r", error) self._last_error = InvalidAuth - self._available = False + self._attr_available = False except ClientConnectorError as error: if self._last_error != ClientConnectorError: _LOGGER.warning("Network unavailable: %r", error) self._last_error = ClientConnectorError - self._available = False + self._attr_available = False except Exception as error: # pylint: disable=broad-except if self._last_error != error: _LOGGER.error("Error occurred while fetching data: %r", error) self._last_error = error - self._available = False + self._attr_available = False if not (data["returnCode"] == "OK" and data.get("departures")): - self._available = False + self._attr_available = False return if self._last_error == ClientConnectorError: @@ -119,14 +126,14 @@ class HVVDepartureSensor(SensorEntity): departure = data["departures"][0] line = departure["line"] delay = departure.get("delay", 0) - self._available = True - self._state = ( + self._attr_available = True + self._attr_native_value = ( departure_time + timedelta(minutes=departure["timeOffset"]) + timedelta(seconds=delay) ) - self.attr.update( + self._attr_extra_state_attributes.update( { ATTR_LINE: line["name"], ATTR_ORIGIN: line["origin"], @@ -154,15 +161,7 @@ class HVVDepartureSensor(SensorEntity): ATTR_DELAY: delay, } ) - self.attr[ATTR_NEXT] = departures - - @property - def unique_id(self): - """Return a unique ID to use for this sensor.""" - station_id = self.config_entry.data[CONF_STATION]["id"] - station_type = self.config_entry.data[CONF_STATION]["type"] - - return f"{self.config_entry.entry_id}-{station_id}-{station_type}" + self._attr_extra_state_attributes[ATTR_NEXT] = departures @property def device_info(self): @@ -177,35 +176,5 @@ class HVVDepartureSensor(SensorEntity): ) }, manufacturer=MANUFACTURER, - name=self._name, + name=self.name, ) - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def native_value(self): - """Return the state of the sensor.""" - return self._state - - @property - def icon(self): - """Return the icon of the sensor.""" - return ICON - - @property - def available(self): - """Return True if entity is available.""" - return self._available - - @property - def device_class(self): - """Return the class of this device, from component DEVICE_CLASSES.""" - return SensorDeviceClass.TIMESTAMP - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - return self.attr From 5632e334268a0a5a63c803b62136b84411c300e5 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 6 Sep 2022 20:00:51 +0200 Subject: [PATCH 159/955] Improve type hints in lw12wifi light (#77656) --- homeassistant/components/lw12wifi/light.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lw12wifi/light.py b/homeassistant/components/lw12wifi/light.py index 7814475b41f..9f520a79ae1 100644 --- a/homeassistant/components/lw12wifi/light.py +++ b/homeassistant/components/lw12wifi/light.py @@ -116,7 +116,7 @@ class LW12WiFi(LightEntity): """Return True if unable to access real state of the entity.""" return True - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Instruct the light to turn on.""" self._light.light_on() if ATTR_HS_COLOR in kwargs: @@ -124,7 +124,7 @@ class LW12WiFi(LightEntity): self._light.set_color(*self._rgb_color) self._effect = None if ATTR_BRIGHTNESS in kwargs: - self._brightness = kwargs.get(ATTR_BRIGHTNESS) + self._brightness = kwargs[ATTR_BRIGHTNESS] brightness = int(self._brightness / 255 * 100) self._light.set_light_option(lw12.LW12_LIGHT.BRIGHTNESS, brightness) if ATTR_EFFECT in kwargs: From dbb556a8122ef1081f0155e30e99a61db264e5ca Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 6 Sep 2022 20:13:01 +0200 Subject: [PATCH 160/955] Revert "Add ability to ignore devices for UniFi Protect" (#77916) --- .../components/unifiprotect/__init__.py | 30 ++----- .../components/unifiprotect/config_flow.py | 78 +++++++------------ .../components/unifiprotect/const.py | 1 - homeassistant/components/unifiprotect/data.py | 65 +--------------- .../components/unifiprotect/services.py | 6 +- .../components/unifiprotect/strings.json | 6 +- .../unifiprotect/translations/en.json | 4 - .../components/unifiprotect/utils.py | 46 ++++------- tests/components/unifiprotect/conftest.py | 1 - .../unifiprotect/test_config_flow.py | 45 +---------- tests/components/unifiprotect/test_init.py | 39 +++------- tests/components/unifiprotect/utils.py | 12 +-- 12 files changed, 75 insertions(+), 258 deletions(-) diff --git a/homeassistant/components/unifiprotect/__init__.py b/homeassistant/components/unifiprotect/__init__.py index 60829223e2f..30b1d1ad56d 100644 --- a/homeassistant/components/unifiprotect/__init__.py +++ b/homeassistant/components/unifiprotect/__init__.py @@ -26,7 +26,6 @@ from homeassistant.helpers.aiohttp_client import async_create_clientsession from .const import ( CONF_ALL_UPDATES, - CONF_IGNORED, CONF_OVERRIDE_CHOST, DEFAULT_SCAN_INTERVAL, DEVICES_FOR_SUBSCRIBE, @@ -36,11 +35,11 @@ from .const import ( OUTDATED_LOG_MESSAGE, PLATFORMS, ) -from .data import ProtectData +from .data import ProtectData, async_ufp_instance_for_config_entry_ids from .discovery import async_start_discovery from .migrate import async_migrate_data from .services import async_cleanup_services, async_setup_services -from .utils import async_unifi_mac, convert_mac_list +from .utils import _async_unifi_mac_from_hass, async_get_devices from .views import ThumbnailProxyView, VideoProxyView _LOGGER = logging.getLogger(__name__) @@ -107,19 +106,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def _async_options_updated(hass: HomeAssistant, entry: ConfigEntry) -> None: """Update options.""" - - data: ProtectData = hass.data[DOMAIN][entry.entry_id] - changed = data.async_get_changed_options(entry) - - if len(changed) == 1 and CONF_IGNORED in changed: - new_macs = convert_mac_list(entry.options.get(CONF_IGNORED, "")) - added_macs = new_macs - data.ignored_macs - removed_macs = data.ignored_macs - new_macs - # if only ignored macs are added, we can handle without reloading - if not removed_macs and added_macs: - data.async_add_new_ignored_macs(added_macs) - return - await hass.config_entries.async_reload(entry.entry_id) @@ -139,15 +125,15 @@ async def async_remove_config_entry_device( ) -> bool: """Remove ufp config entry from a device.""" unifi_macs = { - async_unifi_mac(connection[1]) + _async_unifi_mac_from_hass(connection[1]) for connection in device_entry.connections if connection[0] == dr.CONNECTION_NETWORK_MAC } - data: ProtectData = hass.data[DOMAIN][config_entry.entry_id] - if data.api.bootstrap.nvr.mac in unifi_macs: + api = async_ufp_instance_for_config_entry_ids(hass, {config_entry.entry_id}) + assert api is not None + if api.bootstrap.nvr.mac in unifi_macs: return False - for device in data.get_by_types(DEVICES_THAT_ADOPT): + for device in async_get_devices(api.bootstrap, DEVICES_THAT_ADOPT): if device.is_adopted_by_us and device.mac in unifi_macs: - data.async_ignore_mac(device.mac) - break + return False return True diff --git a/homeassistant/components/unifiprotect/config_flow.py b/homeassistant/components/unifiprotect/config_flow.py index 1907a201c8d..f07ca923a53 100644 --- a/homeassistant/components/unifiprotect/config_flow.py +++ b/homeassistant/components/unifiprotect/config_flow.py @@ -35,7 +35,6 @@ from homeassistant.util.network import is_ip_address from .const import ( CONF_ALL_UPDATES, CONF_DISABLE_RTSP, - CONF_IGNORED, CONF_MAX_MEDIA, CONF_OVERRIDE_CHOST, DEFAULT_MAX_MEDIA, @@ -47,7 +46,7 @@ from .const import ( ) from .data import async_last_update_was_successful from .discovery import async_start_discovery -from .utils import _async_resolve, async_short_mac, async_unifi_mac, convert_mac_list +from .utils import _async_resolve, _async_short_mac, _async_unifi_mac_from_hass _LOGGER = logging.getLogger(__name__) @@ -121,7 +120,7 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Handle integration discovery.""" self._discovered_device = discovery_info - mac = async_unifi_mac(discovery_info["hw_addr"]) + mac = _async_unifi_mac_from_hass(discovery_info["hw_addr"]) await self.async_set_unique_id(mac) source_ip = discovery_info["source_ip"] direct_connect_domain = discovery_info["direct_connect_domain"] @@ -183,7 +182,7 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): placeholders = { "name": discovery_info["hostname"] or discovery_info["platform"] - or f"NVR {async_short_mac(discovery_info['hw_addr'])}", + or f"NVR {_async_short_mac(discovery_info['hw_addr'])}", "ip_address": discovery_info["source_ip"], } self.context["title_placeholders"] = placeholders @@ -225,7 +224,6 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): CONF_ALL_UPDATES: False, CONF_OVERRIDE_CHOST: False, CONF_MAX_MEDIA: DEFAULT_MAX_MEDIA, - CONF_IGNORED: "", }, ) @@ -367,53 +365,33 @@ class OptionsFlowHandler(config_entries.OptionsFlow): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Manage the options.""" - - values = user_input or self.config_entry.options - schema = vol.Schema( - { - vol.Optional( - CONF_DISABLE_RTSP, - description={ - "suggested_value": values.get(CONF_DISABLE_RTSP, False) - }, - ): bool, - vol.Optional( - CONF_ALL_UPDATES, - description={ - "suggested_value": values.get(CONF_ALL_UPDATES, False) - }, - ): bool, - vol.Optional( - CONF_OVERRIDE_CHOST, - description={ - "suggested_value": values.get(CONF_OVERRIDE_CHOST, False) - }, - ): bool, - vol.Optional( - CONF_MAX_MEDIA, - description={ - "suggested_value": values.get(CONF_MAX_MEDIA, DEFAULT_MAX_MEDIA) - }, - ): vol.All(vol.Coerce(int), vol.Range(min=100, max=10000)), - vol.Optional( - CONF_IGNORED, - description={"suggested_value": values.get(CONF_IGNORED, "")}, - ): str, - } - ) - errors: dict[str, str] = {} - if user_input is not None: - try: - convert_mac_list(user_input.get(CONF_IGNORED, ""), raise_exception=True) - except vol.Invalid: - errors[CONF_IGNORED] = "invalid_mac_list" - - if not errors: - return self.async_create_entry(title="", data=user_input) + return self.async_create_entry(title="", data=user_input) return self.async_show_form( step_id="init", - data_schema=schema, - errors=errors, + data_schema=vol.Schema( + { + vol.Optional( + CONF_DISABLE_RTSP, + default=self.config_entry.options.get(CONF_DISABLE_RTSP, False), + ): bool, + vol.Optional( + CONF_ALL_UPDATES, + default=self.config_entry.options.get(CONF_ALL_UPDATES, False), + ): bool, + vol.Optional( + CONF_OVERRIDE_CHOST, + default=self.config_entry.options.get( + CONF_OVERRIDE_CHOST, False + ), + ): bool, + vol.Optional( + CONF_MAX_MEDIA, + default=self.config_entry.options.get( + CONF_MAX_MEDIA, DEFAULT_MAX_MEDIA + ), + ): vol.All(vol.Coerce(int), vol.Range(min=100, max=10000)), + } + ), ) diff --git a/homeassistant/components/unifiprotect/const.py b/homeassistant/components/unifiprotect/const.py index 080dc41f358..93a0fa5ff74 100644 --- a/homeassistant/components/unifiprotect/const.py +++ b/homeassistant/components/unifiprotect/const.py @@ -20,7 +20,6 @@ CONF_DISABLE_RTSP = "disable_rtsp" CONF_ALL_UPDATES = "all_updates" CONF_OVERRIDE_CHOST = "override_connection_host" CONF_MAX_MEDIA = "max_media" -CONF_IGNORED = "ignored_devices" CONFIG_OPTIONS = [ CONF_ALL_UPDATES, diff --git a/homeassistant/components/unifiprotect/data.py b/homeassistant/components/unifiprotect/data.py index c17b99d639f..20b5747a342 100644 --- a/homeassistant/components/unifiprotect/data.py +++ b/homeassistant/components/unifiprotect/data.py @@ -28,7 +28,6 @@ from homeassistant.helpers.event import async_track_time_interval from .const import ( CONF_DISABLE_RTSP, - CONF_IGNORED, CONF_MAX_MEDIA, DEFAULT_MAX_MEDIA, DEVICES_THAT_ADOPT, @@ -37,11 +36,7 @@ from .const import ( DISPATCH_CHANNELS, DOMAIN, ) -from .utils import ( - async_dispatch_id as _ufpd, - async_get_devices_by_type, - convert_mac_list, -) +from .utils import async_dispatch_id as _ufpd, async_get_devices_by_type _LOGGER = logging.getLogger(__name__) ProtectDeviceType = Union[ProtectAdoptableDeviceModel, NVR] @@ -72,7 +67,6 @@ class ProtectData: self._hass = hass self._entry = entry - self._existing_options = dict(entry.options) self._hass = hass self._update_interval = update_interval self._subscriptions: dict[str, list[Callable[[ProtectDeviceType], None]]] = {} @@ -80,8 +74,6 @@ class ProtectData: self._unsub_interval: CALLBACK_TYPE | None = None self._unsub_websocket: CALLBACK_TYPE | None = None self._auth_failures = 0 - self._ignored_macs: set[str] | None = None - self._ignore_update_cancel: Callable[[], None] | None = None self.last_update_success = False self.api = protect @@ -96,47 +88,6 @@ class ProtectData: """Max number of events to load at once.""" return self._entry.options.get(CONF_MAX_MEDIA, DEFAULT_MAX_MEDIA) - @property - def ignored_macs(self) -> set[str]: - """Set of ignored MAC addresses.""" - - if self._ignored_macs is None: - self._ignored_macs = convert_mac_list( - self._entry.options.get(CONF_IGNORED, "") - ) - - return self._ignored_macs - - @callback - def async_get_changed_options(self, entry: ConfigEntry) -> dict[str, Any]: - """Get changed options for when entry is updated.""" - - return dict( - set(self._entry.options.items()) - set(self._existing_options.items()) - ) - - @callback - def async_ignore_mac(self, mac: str) -> None: - """Ignores a MAC address for a UniFi Protect device.""" - - new_macs = (self._ignored_macs or set()).copy() - new_macs.add(mac) - _LOGGER.debug("Updating ignored_devices option: %s", self.ignored_macs) - options = dict(self._entry.options) - options[CONF_IGNORED] = ",".join(new_macs) - self._hass.config_entries.async_update_entry(self._entry, options=options) - - @callback - def async_add_new_ignored_macs(self, new_macs: set[str]) -> None: - """Add new ignored MAC addresses and ensures the devices are removed.""" - - for mac in new_macs: - device = self.api.bootstrap.get_device_from_mac(mac) - if device is not None: - self._async_remove_device(device) - self._ignored_macs = None - self._existing_options = dict(self._entry.options) - def get_by_types( self, device_types: Iterable[ModelType], ignore_unadopted: bool = True ) -> Generator[ProtectAdoptableDeviceModel, None, None]: @@ -148,8 +99,6 @@ class ProtectData: for device in devices: if ignore_unadopted and not device.is_adopted_by_us: continue - if device.mac in self.ignored_macs: - continue yield device async def async_setup(self) -> None: @@ -159,11 +108,6 @@ class ProtectData: ) await self.async_refresh() - for mac in self.ignored_macs: - device = self.api.bootstrap.get_device_from_mac(mac) - if device is not None: - self._async_remove_device(device) - async def async_stop(self, *args: Any) -> None: """Stop processing data.""" if self._unsub_websocket: @@ -228,7 +172,6 @@ class ProtectData: @callback def _async_remove_device(self, device: ProtectAdoptableDeviceModel) -> None: - registry = dr.async_get(self._hass) device_entry = registry.async_get_device( identifiers=set(), connections={(dr.CONNECTION_NETWORK_MAC, device.mac)} @@ -353,13 +296,13 @@ class ProtectData: @callback -def async_ufp_data_for_config_entry_ids( +def async_ufp_instance_for_config_entry_ids( hass: HomeAssistant, config_entry_ids: set[str] -) -> ProtectData | None: +) -> ProtectApiClient | None: """Find the UFP instance for the config entry ids.""" domain_data = hass.data[DOMAIN] for config_entry_id in config_entry_ids: if config_entry_id in domain_data: protect_data: ProtectData = domain_data[config_entry_id] - return protect_data + return protect_data.api return None diff --git a/homeassistant/components/unifiprotect/services.py b/homeassistant/components/unifiprotect/services.py index 914803e9c45..915c51b6c0a 100644 --- a/homeassistant/components/unifiprotect/services.py +++ b/homeassistant/components/unifiprotect/services.py @@ -25,7 +25,7 @@ from homeassistant.helpers.service import async_extract_referenced_entity_ids from homeassistant.util.read_only_dict import ReadOnlyDict from .const import ATTR_MESSAGE, DOMAIN -from .data import async_ufp_data_for_config_entry_ids +from .data import async_ufp_instance_for_config_entry_ids SERVICE_ADD_DOORBELL_TEXT = "add_doorbell_text" SERVICE_REMOVE_DOORBELL_TEXT = "remove_doorbell_text" @@ -70,8 +70,8 @@ def _async_get_ufp_instance(hass: HomeAssistant, device_id: str) -> ProtectApiCl return _async_get_ufp_instance(hass, device_entry.via_device_id) config_entry_ids = device_entry.config_entries - if ufp_data := async_ufp_data_for_config_entry_ids(hass, config_entry_ids): - return ufp_data.api + if ufp_instance := async_ufp_instance_for_config_entry_ids(hass, config_entry_ids): + return ufp_instance raise HomeAssistantError(f"No device found for device id: {device_id}") diff --git a/homeassistant/components/unifiprotect/strings.json b/homeassistant/components/unifiprotect/strings.json index d9750d31ae1..d3cfe24abd2 100644 --- a/homeassistant/components/unifiprotect/strings.json +++ b/homeassistant/components/unifiprotect/strings.json @@ -50,13 +50,9 @@ "disable_rtsp": "Disable the RTSP stream", "all_updates": "Realtime metrics (WARNING: Greatly increases CPU usage)", "override_connection_host": "Override Connection Host", - "max_media": "Max number of event to load for Media Browser (increases RAM usage)", - "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)" } } - }, - "error": { - "invalid_mac_list": "Must be a list of MAC addresses seperated by commas" } } } diff --git a/homeassistant/components/unifiprotect/translations/en.json b/homeassistant/components/unifiprotect/translations/en.json index c6050d05284..5d690e3fd3e 100644 --- a/homeassistant/components/unifiprotect/translations/en.json +++ b/homeassistant/components/unifiprotect/translations/en.json @@ -42,15 +42,11 @@ } }, "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/utils.py b/homeassistant/components/unifiprotect/utils.py index 8c368da1c40..808117aac9e 100644 --- a/homeassistant/components/unifiprotect/utils.py +++ b/homeassistant/components/unifiprotect/utils.py @@ -1,9 +1,9 @@ """UniFi Protect Integration utils.""" from __future__ import annotations +from collections.abc import Generator, Iterable import contextlib from enum import Enum -import re import socket from typing import Any @@ -14,16 +14,12 @@ from pyunifiprotect.data import ( LightModeType, ProtectAdoptableDeviceModel, ) -import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import config_validation as cv from .const import DOMAIN, ModelType -MAC_RE = re.compile(r"[0-9A-F]{12}") - def get_nested_attr(obj: Any, attr: str) -> Any: """Fetch a nested attribute.""" @@ -42,16 +38,15 @@ def get_nested_attr(obj: Any, attr: str) -> Any: @callback -def async_unifi_mac(mac: str) -> str: - """Convert MAC address to format from UniFi Protect.""" +def _async_unifi_mac_from_hass(mac: str) -> str: # MAC addresses in UFP are always caps - return mac.replace(":", "").replace("-", "").replace("_", "").upper() + return mac.replace(":", "").upper() @callback -def async_short_mac(mac: str) -> str: +def _async_short_mac(mac: str) -> str: """Get the short mac address from the full mac.""" - return async_unifi_mac(mac)[-6:] + return _async_unifi_mac_from_hass(mac)[-6:] async def _async_resolve(hass: HomeAssistant, host: str) -> str | None: @@ -82,6 +77,18 @@ def async_get_devices_by_type( return devices +@callback +def async_get_devices( + bootstrap: Bootstrap, model_type: Iterable[ModelType] +) -> Generator[ProtectAdoptableDeviceModel, None, None]: + """Return all device by type.""" + return ( + device + for device_type in model_type + for device in async_get_devices_by_type(bootstrap, device_type).values() + ) + + @callback def async_get_light_motion_current(obj: Light) -> str: """Get light motion mode for Flood Light.""" @@ -99,22 +106,3 @@ def async_dispatch_id(entry: ConfigEntry, dispatch: str) -> str: """Generate entry specific dispatch ID.""" return f"{DOMAIN}.{entry.entry_id}.{dispatch}" - - -@callback -def convert_mac_list(option: str, raise_exception: bool = False) -> set[str]: - """Convert csv list of MAC addresses.""" - - macs = set() - values = cv.ensure_list_csv(option) - for value in values: - if value == "": - continue - value = async_unifi_mac(value) - if not MAC_RE.match(value): - if raise_exception: - raise vol.Invalid("invalid_mac_list") - continue - macs.add(value) - - return macs diff --git a/tests/components/unifiprotect/conftest.py b/tests/components/unifiprotect/conftest.py index fa245e8b1cc..b006dfbd004 100644 --- a/tests/components/unifiprotect/conftest.py +++ b/tests/components/unifiprotect/conftest.py @@ -68,7 +68,6 @@ def mock_ufp_config_entry(): "port": 443, "verify_ssl": False, }, - options={"ignored_devices": "FFFFFFFFFFFF,test"}, version=2, ) diff --git a/tests/components/unifiprotect/test_config_flow.py b/tests/components/unifiprotect/test_config_flow.py index 26a9dd73ee8..d0fb0dba9f2 100644 --- a/tests/components/unifiprotect/test_config_flow.py +++ b/tests/components/unifiprotect/test_config_flow.py @@ -14,7 +14,6 @@ from homeassistant.components import dhcp, ssdp from homeassistant.components.unifiprotect.const import ( CONF_ALL_UPDATES, CONF_DISABLE_RTSP, - CONF_IGNORED, CONF_OVERRIDE_CHOST, DOMAIN, ) @@ -270,52 +269,10 @@ async def test_form_options(hass: HomeAssistant, ufp_client: ProtectApiClient) - "all_updates": True, "disable_rtsp": True, "override_connection_host": True, + "max_media": 1000, } -async def test_form_options_invalid_mac( - hass: HomeAssistant, ufp_client: ProtectApiClient -) -> None: - """Test we handle options flows.""" - mock_config = MockConfigEntry( - domain=DOMAIN, - data={ - "host": "1.1.1.1", - "username": "test-username", - "password": "test-password", - "id": "UnifiProtect", - "port": 443, - "verify_ssl": False, - "max_media": 1000, - }, - version=2, - unique_id=dr.format_mac(MAC_ADDR), - ) - mock_config.add_to_hass(hass) - - with _patch_discovery(), patch( - "homeassistant.components.unifiprotect.ProtectApiClient" - ) as mock_api: - mock_api.return_value = ufp_client - - await hass.config_entries.async_setup(mock_config.entry_id) - 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" - - result2 = await hass.config_entries.options.async_configure( - result["flow_id"], - {CONF_IGNORED: "test,test2"}, - ) - - assert result2["type"] == FlowResultType.FORM - assert result2["errors"] == {CONF_IGNORED: "invalid_mac_list"} - - @pytest.mark.parametrize( "source, data", [ diff --git a/tests/components/unifiprotect/test_init.py b/tests/components/unifiprotect/test_init.py index 7a1e590b87d..9392caa30ac 100644 --- a/tests/components/unifiprotect/test_init.py +++ b/tests/components/unifiprotect/test_init.py @@ -7,21 +7,20 @@ from unittest.mock import AsyncMock, patch import aiohttp from pyunifiprotect import NotAuthorized, NvrError, ProtectApiClient -from pyunifiprotect.data import NVR, Bootstrap, Doorlock, Light, Sensor +from pyunifiprotect.data import NVR, Bootstrap, Light from homeassistant.components.unifiprotect.const import ( CONF_DISABLE_RTSP, - CONF_IGNORED, DEFAULT_SCAN_INTERVAL, DOMAIN, ) from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component from . import _patch_discovery -from .utils import MockUFPFixture, get_device_from_ufp_device, init_entry, time_changed +from .utils import MockUFPFixture, init_entry, time_changed from tests.common import MockConfigEntry @@ -212,38 +211,28 @@ async def test_device_remove_devices( hass: HomeAssistant, ufp: MockUFPFixture, light: Light, - doorlock: Doorlock, - sensor: Sensor, hass_ws_client: Callable[ [HomeAssistant], Awaitable[aiohttp.ClientWebSocketResponse] ], ) -> None: """Test we can only remove a device that no longer exists.""" - sensor.mac = "FFFFFFFFFFFF" - - await init_entry(hass, ufp, [light, doorlock, sensor], regenerate_ids=False) + await init_entry(hass, ufp, [light]) assert await async_setup_component(hass, "config", {}) - + entity_id = "light.test_light" entry_id = ufp.entry.entry_id + + registry: er.EntityRegistry = er.async_get(hass) + entity = registry.async_get(entity_id) + assert entity is not None device_registry = dr.async_get(hass) - light_device = get_device_from_ufp_device(hass, light) - assert light_device is not None + live_device_entry = device_registry.async_get(entity.device_id) assert ( - await remove_device(await hass_ws_client(hass), light_device.id, entry_id) - is True + await remove_device(await hass_ws_client(hass), live_device_entry.id, entry_id) + is False ) - doorlock_device = get_device_from_ufp_device(hass, doorlock) - assert ( - await remove_device(await hass_ws_client(hass), doorlock_device.id, entry_id) - is True - ) - - sensor_device = get_device_from_ufp_device(hass, sensor) - assert sensor_device is None - dead_device_entry = device_registry.async_get_or_create( config_entry_id=entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "e9:88:e7:b8:b4:40")}, @@ -253,10 +242,6 @@ async def test_device_remove_devices( is True ) - await time_changed(hass, 60) - entry = hass.config_entries.async_get_entry(entry_id) - entry.options[CONF_IGNORED] == f"{light.mac},{doorlock.mac}" - async def test_device_remove_devices_nvr( hass: HomeAssistant, diff --git a/tests/components/unifiprotect/utils.py b/tests/components/unifiprotect/utils.py index 3376db4ec51..bee479b8e2b 100644 --- a/tests/components/unifiprotect/utils.py +++ b/tests/components/unifiprotect/utils.py @@ -23,7 +23,7 @@ from pyunifiprotect.test_util.anonymize import random_hex from homeassistant.const import Platform from homeassistant.core import HomeAssistant, split_entity_id -from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity import EntityDescription import homeassistant.util.dt as dt_util @@ -229,13 +229,3 @@ async def adopt_devices( ufp.ws_msg(mock_msg) await hass.async_block_till_done() - - -def get_device_from_ufp_device( - hass: HomeAssistant, device: ProtectAdoptableDeviceModel -) -> dr.DeviceEntry | None: - """Return all device by type.""" - registry = dr.async_get(hass) - return registry.async_get_device( - identifiers=set(), connections={(dr.CONNECTION_NETWORK_MAC, device.mac)} - ) From 87ab14d7584cd2a6d87eb71ab7de427137cc1baa Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 6 Sep 2022 20:18:52 +0200 Subject: [PATCH 161/955] Add protocol type for legacy notify platforms (#77894) --- homeassistant/components/notify/__init__.py | 2 +- homeassistant/components/notify/legacy.py | 27 ++++++++++++++++++--- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/notify/__init__.py b/homeassistant/components/notify/__init__.py index 60d24578593..52864dd001d 100644 --- a/homeassistant/components/notify/__init__.py +++ b/homeassistant/components/notify/__init__.py @@ -57,7 +57,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: await asyncio.wait([asyncio.create_task(setup) for setup in platform_setups]) async def persistent_notification(service: ServiceCall) -> None: - """Send notification via the built-in persistsent_notify integration.""" + """Send notification via the built-in persistent_notify integration.""" message = service.data[ATTR_MESSAGE] message.hass = hass check_templates_warn(hass, message) diff --git a/homeassistant/components/notify/legacy.py b/homeassistant/components/notify/legacy.py index f9066b7dff9..3d6d1582848 100644 --- a/homeassistant/components/notify/legacy.py +++ b/homeassistant/components/notify/legacy.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio from collections.abc import Coroutine from functools import partial -from typing import Any, cast +from typing import Any, Optional, Protocol, cast from homeassistant.const import CONF_DESCRIPTION, CONF_NAME from homeassistant.core import HomeAssistant, ServiceCall, callback @@ -33,6 +33,26 @@ NOTIFY_SERVICES = "notify_services" NOTIFY_DISCOVERY_DISPATCHER = "notify_discovery_dispatcher" +class LegacyNotifyPlatform(Protocol): + """Define the format of legacy notify platforms.""" + + async def async_get_service( + self, + hass: HomeAssistant, + config: ConfigType, + discovery_info: DiscoveryInfoType | None = ..., + ) -> BaseNotificationService: + """Set up notification service.""" + + def get_service( + self, + hass: HomeAssistant, + config: ConfigType, + discovery_info: DiscoveryInfoType | None = ..., + ) -> BaseNotificationService: + """Set up notification service.""" + + @callback def async_setup_legacy( hass: HomeAssistant, config: ConfigType @@ -50,8 +70,9 @@ def async_setup_legacy( if p_config is None: p_config = {} - platform = await async_prepare_setup_platform( - hass, config, DOMAIN, integration_name + platform = cast( + Optional[LegacyNotifyPlatform], + await async_prepare_setup_platform(hass, config, DOMAIN, integration_name), ) if platform is None: From 6864f4398698b4e9e83315b913af0c3d8db59d56 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 6 Sep 2022 20:19:33 +0200 Subject: [PATCH 162/955] Drop unused property from zha (#77897) --- homeassistant/components/zha/entity.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index 61dbe5339d0..ae4bc7e5ea4 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -54,7 +54,6 @@ class BaseZhaEntity(LogMixin, entity.Entity): def __init__(self, unique_id: str, zha_device: ZHADevice, **kwargs: Any) -> None: """Init ZHA entity.""" self._name: str = "" - self._force_update: bool = False self._unique_id: str = unique_id if self.unique_id_suffix: self._unique_id += f"-{self.unique_id_suffix}" @@ -84,11 +83,6 @@ class BaseZhaEntity(LogMixin, entity.Entity): """Return device specific state attributes.""" return self._extra_state_attributes - @property - def force_update(self) -> bool: - """Force update this entity.""" - return self._force_update - @property def device_info(self) -> entity.DeviceInfo: """Return a device description for device registry.""" From 19b85851a19d73073939325c2cbd85b518a7cdca Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 6 Sep 2022 20:20:07 +0200 Subject: [PATCH 163/955] Use _attr_force_update in tasmota (#77900) --- homeassistant/components/tasmota/binary_sensor.py | 6 +----- homeassistant/components/tasmota/light.py | 5 ----- homeassistant/components/tasmota/sensor.py | 6 +----- 3 files changed, 2 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/tasmota/binary_sensor.py b/homeassistant/components/tasmota/binary_sensor.py index a0f4dfff5ac..2bc23655a20 100644 --- a/homeassistant/components/tasmota/binary_sensor.py +++ b/homeassistant/components/tasmota/binary_sensor.py @@ -58,6 +58,7 @@ class TasmotaBinarySensor( ): """Representation a Tasmota binary sensor.""" + _attr_force_update = True _tasmota_entity: tasmota_switch.TasmotaSwitch def __init__(self, **kwds: Any) -> None: @@ -98,11 +99,6 @@ class TasmotaBinarySensor( self.async_write_ha_state() - @property - def force_update(self) -> bool: - """Force update.""" - return True - @property def is_on(self) -> bool | None: """Return true if the binary sensor is on.""" diff --git a/homeassistant/components/tasmota/light.py b/homeassistant/components/tasmota/light.py index 970d006e79d..2ff51752ea5 100644 --- a/homeassistant/components/tasmota/light.py +++ b/homeassistant/components/tasmota/light.py @@ -229,11 +229,6 @@ class TasmotaLight( hs_color = self._hs return (hs_color[0], hs_color[1]) - @property - def force_update(self) -> bool: - """Force update.""" - return False - @property def supported_color_modes(self) -> set[str] | None: """Flag supported color modes.""" diff --git a/homeassistant/components/tasmota/sensor.py b/homeassistant/components/tasmota/sensor.py index 73b261ed78c..1878e2794f3 100644 --- a/homeassistant/components/tasmota/sensor.py +++ b/homeassistant/components/tasmota/sensor.py @@ -256,6 +256,7 @@ async def async_setup_entry( class TasmotaSensor(TasmotaAvailability, TasmotaDiscoveryUpdate, SensorEntity): """Representation of a Tasmota sensor.""" + _attr_force_update = True _tasmota_entity: tasmota_sensor.TasmotaSensor def __init__(self, **kwds: Any) -> None: @@ -332,11 +333,6 @@ class TasmotaSensor(TasmotaAvailability, TasmotaDiscoveryUpdate, SensorEntity): return self._state_timestamp return self._state - @property - def force_update(self) -> bool: - """Force update.""" - return True - @property def native_unit_of_measurement(self) -> str | None: """Return the unit this state is expressed in.""" From 769084058d47128b88e82b5241180a346369339d Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Tue, 6 Sep 2022 21:22:04 +0300 Subject: [PATCH 164/955] Add sensors for Tuya "tdq" category switches (#77581) --- homeassistant/components/tuya/sensor.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index 266ee951530..e19bcd20ba5 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -340,6 +340,31 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { entity_registry_enabled_default=False, ), ), + # IoT Switch + # Note: Undocumented + "tdq": ( + TuyaSensorEntityDescription( + key=DPCode.CUR_CURRENT, + name="Current", + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), + TuyaSensorEntityDescription( + key=DPCode.CUR_POWER, + name="Power", + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), + TuyaSensorEntityDescription( + key=DPCode.CUR_VOLTAGE, + name="Voltage", + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), + ), # Luminance Sensor # https://developer.tuya.com/en/docs/iot/categoryldcg?id=Kaiuz3n7u69l8 "ldcg": ( From 4e625f03602d91cc07587c0189b9416eb516037d Mon Sep 17 00:00:00 2001 From: Matthew Simpson Date: Tue, 6 Sep 2022 20:50:03 +0100 Subject: [PATCH 165/955] Bump btsmarthub_devicelist to 0.2.2 (#77609) --- homeassistant/components/bt_smarthub/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/bt_smarthub/manifest.json b/homeassistant/components/bt_smarthub/manifest.json index 6a0453752e9..fb34117eb6b 100644 --- a/homeassistant/components/bt_smarthub/manifest.json +++ b/homeassistant/components/bt_smarthub/manifest.json @@ -2,7 +2,7 @@ "domain": "bt_smarthub", "name": "BT Smart Hub", "documentation": "https://www.home-assistant.io/integrations/bt_smarthub", - "requirements": ["btsmarthub_devicelist==0.2.0"], + "requirements": ["btsmarthub_devicelist==0.2.2"], "codeowners": ["@jxwolstenholme"], "iot_class": "local_polling", "loggers": ["btsmarthub_devicelist"] diff --git a/requirements_all.txt b/requirements_all.txt index d3436ab3973..d1250664db6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -470,7 +470,7 @@ bthome-ble==1.0.0 bthomehub5-devicelist==0.1.1 # homeassistant.components.bt_smarthub -btsmarthub_devicelist==0.2.0 +btsmarthub_devicelist==0.2.2 # homeassistant.components.buienradar buienradar==1.0.5 From 9ba1bb7c73f735bdc3e219634d88c46518be0ecf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 6 Sep 2022 16:43:18 -0500 Subject: [PATCH 166/955] Bump aiohomekit to 1.5.2 (#77927) --- 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 e3526dd870a..08eac050c98 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==1.5.1"], + "requirements": ["aiohomekit==1.5.2"], "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 d1250664db6..ce54414af84 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==1.5.1 +aiohomekit==1.5.2 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ace37759f9d..67949a21a2d 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==1.5.1 +aiohomekit==1.5.2 # homeassistant.components.emulated_hue # homeassistant.components.http From dfef6c3d281e0e9b80e82f1c23fdebe209fac8b0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 6 Sep 2022 18:12:32 -0500 Subject: [PATCH 167/955] Small tweaks to improve performance of bluetooth matching (#77934) * Small tweaks to improve performance of bluetooth matching * Small tweaks to improve performance of bluetooth matching * cleanup --- homeassistant/components/bluetooth/match.py | 68 +++++++++++---------- 1 file changed, 36 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/bluetooth/match.py b/homeassistant/components/bluetooth/match.py index 813acfc8cda..dd1c9c1fa3c 100644 --- a/homeassistant/components/bluetooth/match.py +++ b/homeassistant/components/bluetooth/match.py @@ -180,12 +180,20 @@ class BluetoothMatcherIndexBase(Generic[_T]): We put them in the bucket that they are most likely to match. """ + # Local name is the cheapest to match since its just a dict lookup if LOCAL_NAME in matcher: self.local_name.setdefault( _local_name_to_index_key(matcher[LOCAL_NAME]), [] ).append(matcher) return + # Manufacturer data is 2nd cheapest since its all ints + if MANUFACTURER_ID in matcher: + self.manufacturer_id.setdefault(matcher[MANUFACTURER_ID], []).append( + matcher + ) + return + if SERVICE_UUID in matcher: self.service_uuid.setdefault(matcher[SERVICE_UUID], []).append(matcher) return @@ -196,12 +204,6 @@ class BluetoothMatcherIndexBase(Generic[_T]): ) return - if MANUFACTURER_ID in matcher: - self.manufacturer_id.setdefault(matcher[MANUFACTURER_ID], []).append( - matcher - ) - return - def remove(self, matcher: _T) -> None: """Remove a matcher from the index. @@ -214,6 +216,10 @@ class BluetoothMatcherIndexBase(Generic[_T]): ) return + if MANUFACTURER_ID in matcher: + self.manufacturer_id[matcher[MANUFACTURER_ID]].remove(matcher) + return + if SERVICE_UUID in matcher: self.service_uuid[matcher[SERVICE_UUID]].remove(matcher) return @@ -222,10 +228,6 @@ class BluetoothMatcherIndexBase(Generic[_T]): self.service_data_uuid[matcher[SERVICE_DATA_UUID]].remove(matcher) return - if MANUFACTURER_ID in matcher: - self.manufacturer_id[matcher[MANUFACTURER_ID]].remove(matcher) - return - def build(self) -> None: """Rebuild the index sets.""" self.service_uuid_set = set(self.service_uuid) @@ -235,33 +237,36 @@ class BluetoothMatcherIndexBase(Generic[_T]): def match(self, service_info: BluetoothServiceInfoBleak) -> list[_T]: """Check for a match.""" matches = [] - if len(service_info.name) >= LOCAL_NAME_MIN_MATCH_LENGTH: + if service_info.name and len(service_info.name) >= LOCAL_NAME_MIN_MATCH_LENGTH: for matcher in self.local_name.get( service_info.name[:LOCAL_NAME_MIN_MATCH_LENGTH], [] ): if ble_device_matches(matcher, service_info): matches.append(matcher) - for service_data_uuid in self.service_data_uuid_set.intersection( - service_info.service_data - ): - for matcher in self.service_data_uuid[service_data_uuid]: - if ble_device_matches(matcher, service_info): - matches.append(matcher) + if self.service_data_uuid_set and service_info.service_data: + for service_data_uuid in self.service_data_uuid_set.intersection( + service_info.service_data + ): + for matcher in self.service_data_uuid[service_data_uuid]: + if ble_device_matches(matcher, service_info): + matches.append(matcher) - for manufacturer_id in self.manufacturer_id_set.intersection( - service_info.manufacturer_data - ): - for matcher in self.manufacturer_id[manufacturer_id]: - if ble_device_matches(matcher, service_info): - matches.append(matcher) + if self.manufacturer_id_set and service_info.manufacturer_data: + for manufacturer_id in self.manufacturer_id_set.intersection( + service_info.manufacturer_data + ): + for matcher in self.manufacturer_id[manufacturer_id]: + if ble_device_matches(matcher, service_info): + matches.append(matcher) - for service_uuid in self.service_uuid_set.intersection( - service_info.service_uuids - ): - for matcher in self.service_uuid[service_uuid]: - if ble_device_matches(matcher, service_info): - matches.append(matcher) + if self.service_uuid_set and service_info.service_uuids: + for service_uuid in self.service_uuid_set.intersection( + service_info.service_uuids + ): + for matcher in self.service_uuid[service_uuid]: + if ble_device_matches(matcher, service_info): + matches.append(matcher) return matches @@ -347,8 +352,6 @@ def ble_device_matches( service_info: BluetoothServiceInfoBleak, ) -> bool: """Check if a ble device and advertisement_data matches the matcher.""" - device = service_info.device - # Don't check address here since all callers already # check the address and we don't want to double check # since it would result in an unreachable reject case. @@ -379,7 +382,8 @@ def ble_device_matches( return False if (local_name := matcher.get(LOCAL_NAME)) and ( - (device_name := advertisement_data.local_name or device.name) is None + (device_name := advertisement_data.local_name or service_info.device.name) + is None or not _memorized_fnmatch( device_name, local_name, From 4f7ad27b659ab1363557689a3a4fcb512e1c6fe1 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 7 Sep 2022 00:27:56 +0000 Subject: [PATCH 168/955] [ci skip] Translation update --- .../components/aemet/translations/ja.json | 2 +- .../alarm_control_panel/translations/ja.json | 8 +++---- .../components/androidtv/translations/ja.json | 10 ++++----- .../components/asuswrt/translations/ja.json | 4 ++-- .../automation/translations/ja.json | 1 + .../components/awair/translations/ja.json | 2 +- .../azure_event_hub/translations/ja.json | 4 ++-- .../bluemaestro/translations/id.json | 22 +++++++++++++++++++ .../bluemaestro/translations/ja.json | 7 ++++++ .../components/bluetooth/translations/ja.json | 2 +- .../components/climacell/translations/ja.json | 4 ++-- .../components/demo/translations/ja.json | 2 +- .../derivative/translations/ja.json | 12 +++++----- .../components/ecowitt/translations/ca.json | 5 ++++- .../components/ecowitt/translations/ja.json | 3 +++ .../components/fibaro/translations/id.json | 10 ++++++++- .../components/fibaro/translations/ja.json | 3 +++ .../components/fibaro/translations/no.json | 10 ++++++++- .../components/fibaro/translations/ru.json | 10 ++++++++- .../components/github/translations/ja.json | 2 +- .../components/group/translations/ja.json | 2 +- .../components/guardian/translations/ja.json | 1 + .../here_travel_time/translations/ja.json | 2 +- .../translations/sensor.id.json | 10 ++++++++- .../homewizard/translations/ja.json | 2 +- .../components/hue/translations/hu.json | 6 ++--- .../components/hue/translations/ja.json | 6 ++--- .../integration/translations/ja.json | 6 ++--- .../intellifire/translations/ja.json | 2 +- .../components/iss/translations/ja.json | 4 ++-- .../components/isy994/translations/ja.json | 2 +- .../components/knx/translations/ja.json | 10 ++++----- .../components/lametric/translations/ja.json | 1 + .../components/lcn/translations/ja.json | 2 +- .../components/min_max/translations/ja.json | 2 +- .../components/mqtt/translations/ja.json | 1 + .../components/nest/translations/ja.json | 7 ++++++ .../components/netatmo/translations/ja.json | 2 +- .../components/netgear/translations/ja.json | 2 +- .../nfandroidtv/translations/ja.json | 2 +- .../nmap_tracker/translations/ja.json | 8 +++---- .../components/nobo_hub/translations/ca.json | 13 +++++++++-- .../components/nobo_hub/translations/id.json | 4 +++- .../components/nobo_hub/translations/ja.json | 3 ++- .../components/overkiz/translations/ca.json | 3 ++- .../components/overkiz/translations/ja.json | 2 +- .../components/powerwall/translations/ja.json | 4 ++-- .../components/samsungtv/translations/ja.json | 2 +- .../components/sensibo/translations/id.json | 6 +++++ .../components/sensibo/translations/ja.json | 6 +++++ .../components/sensibo/translations/ru.json | 6 +++++ .../components/sensor/translations/ca.json | 2 ++ .../components/sensor/translations/ja.json | 8 +++---- .../simplisafe/translations/ja.json | 2 +- .../speedtestdotnet/translations/ja.json | 1 + .../switch_as_x/translations/ja.json | 4 ++-- .../components/switchbot/translations/ja.json | 2 +- .../tankerkoenig/translations/ja.json | 2 +- .../components/threshold/translations/ja.json | 4 ++-- .../components/tod/translations/ja.json | 4 ++-- .../tomorrowio/translations/ja.json | 2 +- .../components/toon/translations/ja.json | 2 +- .../tuya/translations/select.ja.json | 12 +++++----- .../components/twinkly/translations/ja.json | 2 +- .../components/unifi/translations/ja.json | 2 +- .../unifiprotect/translations/en.json | 4 ++++ .../unifiprotect/translations/ja.json | 4 ++-- .../uptimerobot/translations/ja.json | 4 ++-- .../utility_meter/translations/ja.json | 2 +- .../components/vulcan/translations/ja.json | 2 +- .../components/webostv/translations/ja.json | 2 +- .../components/wiz/translations/ja.json | 4 ++-- .../wolflink/translations/sensor.ja.json | 10 ++++----- .../xiaomi_ble/translations/ja.json | 6 +++++ .../yalexs_ble/translations/ja.json | 2 +- .../components/zha/translations/ca.json | 6 +++-- .../components/zha/translations/ja.json | 8 ++++--- .../components/zwave_js/translations/ja.json | 2 +- .../components/zwave_me/translations/ja.json | 2 +- 79 files changed, 244 insertions(+), 115 deletions(-) create mode 100644 homeassistant/components/bluemaestro/translations/id.json create mode 100644 homeassistant/components/bluemaestro/translations/ja.json diff --git a/homeassistant/components/aemet/translations/ja.json b/homeassistant/components/aemet/translations/ja.json index 1279f90beaa..343284ac6d8 100644 --- a/homeassistant/components/aemet/translations/ja.json +++ b/homeassistant/components/aemet/translations/ja.json @@ -14,7 +14,7 @@ "longitude": "\u7d4c\u5ea6", "name": "\u7d71\u5408\u306e\u540d\u524d" }, - "description": "AEMET OpenData\u7d71\u5408\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002 API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001https://opendata.aemet.es/centrodedescargas/altaUsuario \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044" + "description": "API \u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001https://opendata.aemet.es/centrodedescargas/altaUsuario \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044" } } }, diff --git a/homeassistant/components/alarm_control_panel/translations/ja.json b/homeassistant/components/alarm_control_panel/translations/ja.json index 875d24fd3fe..dbcf46d76da 100644 --- a/homeassistant/components/alarm_control_panel/translations/ja.json +++ b/homeassistant/components/alarm_control_panel/translations/ja.json @@ -4,7 +4,7 @@ "arm_away": "\u8b66\u6212 {entity_name} \u96e2\u5e2d(away)", "arm_home": "\u8b66\u6212 {entity_name} \u5728\u5b85", "arm_night": "\u8b66\u6212 {entity_name} \u591c", - "arm_vacation": "\u8b66\u6212 {entity_name} \u4f11\u6687", + "arm_vacation": "\u30a2\u30fc\u30e0{entity_name}\u4f11\u6687", "disarm": "\u89e3\u9664 {entity_name}", "trigger": "\u30c8\u30ea\u30ac\u30fc {entity_name}" }, @@ -12,7 +12,7 @@ "is_armed_away": "{entity_name} \u306f\u8b66\u6212 \u96e2\u5e2d(away)", "is_armed_home": "{entity_name} \u306f\u8b66\u6212 \u5728\u5b85", "is_armed_night": "{entity_name} \u306f\u8b66\u6212 \u591c", - "is_armed_vacation": "{entity_name} \u306f\u8b66\u6212 \u4f11\u6687", + "is_armed_vacation": "{entity_name}\u306f\u6b66\u88c5\u4f11\u6687\u4e2d\u3067\u3059", "is_disarmed": "{entity_name} \u306f\u89e3\u9664", "is_triggered": "{entity_name} \u304c\u30c8\u30ea\u30ac\u30fc\u3055\u308c\u307e\u3059" }, @@ -20,7 +20,7 @@ "armed_away": "{entity_name} \u8b66\u6212 \u96e2\u5e2d(away)", "armed_home": "{entity_name} \u8b66\u6212 \u5728\u5b85", "armed_night": "{entity_name} \u8b66\u6212 \u591c", - "armed_vacation": "{entity_name} \u8b66\u6212 \u4f11\u6687", + "armed_vacation": "{entity_name}\u6b66\u88c5\u4f11\u6687", "disarmed": "{entity_name} \u89e3\u9664", "triggered": "{entity_name} \u304c\u30c8\u30ea\u30ac\u30fc\u3055\u308c\u307e\u3057\u305f" } @@ -32,7 +32,7 @@ "armed_custom_bypass": "\u8b66\u6212 \u30ab\u30b9\u30bf\u30e0 \u30d0\u30a4\u30d1\u30b9", "armed_home": "\u8b66\u6212 \u5728\u5b85", "armed_night": "\u8b66\u6212 \u591c", - "armed_vacation": "\u8b66\u6212 \u4f11\u6687", + "armed_vacation": "\u6b66\u88c5\u4f11\u6687", "arming": "\u8b66\u6212\u4e2d", "disarmed": "\u89e3\u9664", "disarming": "\u89e3\u9664", diff --git a/homeassistant/components/androidtv/translations/ja.json b/homeassistant/components/androidtv/translations/ja.json index 26b83a2643a..c9cabbc538b 100644 --- a/homeassistant/components/androidtv/translations/ja.json +++ b/homeassistant/components/androidtv/translations/ja.json @@ -41,12 +41,12 @@ "init": { "data": { "apps": "\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u30ea\u30b9\u30c8\u306e\u8a2d\u5b9a", - "exclude_unnamed_apps": "\u540d\u524d\u304c\u4e0d\u660e\u306a\u30a2\u30d7\u30ea\u3092\u9664\u5916\u3059\u308b", - "get_sources": "\u5b9f\u884c\u4e2d\u306e\u30a2\u30d7\u30ea\u3092\u30bd\u30fc\u30b9\u306e\u30ea\u30b9\u30c8\u3068\u3057\u3066\u53d6\u5f97\u3059\u308b\u304b\u3069\u3046\u304b", - "screencap": "\u753b\u9762\u306b\u8868\u793a\u4e2d\u306e\u3082\u306e\u304b\u3089\u3001\u30a2\u30eb\u30d0\u30e0\u30a2\u30fc\u30c8\u3092\u62bd\u51fa\u3059\u308b\u304b\u3069\u3046\u304b\u3092\u6c7a\u5b9a\u3057\u307e\u3059", + "exclude_unnamed_apps": "\u30bd\u30fc\u30b9 \u30ea\u30b9\u30c8\u304b\u3089\u4e0d\u660e\u306a\u540d\u524d\u306e\u30a2\u30d7\u30ea\u3092\u9664\u5916\u3059\u308b", + "get_sources": "\u5b9f\u884c\u4e2d\u306e\u30a2\u30d7\u30ea\u3092\u30bd\u30fc\u30b9\u306e\u30ea\u30b9\u30c8\u3068\u3057\u3066\u53d6\u5f97\u3059\u308b", + "screencap": "\u30a2\u30eb\u30d0\u30e0 \u30a2\u30fc\u30c8\u306b\u30b9\u30af\u30ea\u30fc\u30f3 \u30ad\u30e3\u30d7\u30c1\u30e3\u3092\u4f7f\u7528\u3059\u308b", "state_detection_rules": "\u72b6\u614b\u691c\u51fa\u30eb\u30fc\u30eb\u3092\u8a2d\u5b9a", - "turn_off_command": "\u30c7\u30d5\u30a9\u30eb\u30c8\u306eturn_off\u30b3\u30de\u30f3\u30c9\u3092\u4e0a\u66f8\u304d\u3059\u308bADB\u30b7\u30a7\u30eb\u30b3\u30de\u30f3\u30c9", - "turn_on_command": "\u30c7\u30d5\u30a9\u30eb\u30c8\u306eturn_on\u30b3\u30de\u30f3\u30c9\u3092\u4e0a\u66f8\u304d\u3059\u308bADB\u30b7\u30a7\u30eb\u30b3\u30de\u30f3\u30c9" + "turn_off_command": "ADB \u30b7\u30a7\u30eb\u306f\u3001\u30b3\u30de\u30f3\u30c9\u3092\u30aa\u30d5\u306b\u3057\u307e\u3059 (\u30c7\u30d5\u30a9\u30eb\u30c8\u3067\u306f\u7a7a\u306e\u307e\u307e\u306b\u3057\u3066\u304a\u304d\u307e\u3059)", + "turn_on_command": "ADB shell turn on \u30b3\u30de\u30f3\u30c9 (\u30c7\u30d5\u30a9\u30eb\u30c8\u3067\u306f\u7a7a\u306e\u307e\u307e)" } }, "rules": { diff --git a/homeassistant/components/asuswrt/translations/ja.json b/homeassistant/components/asuswrt/translations/ja.json index 6bbe87fbf7e..411d7ac68a5 100644 --- a/homeassistant/components/asuswrt/translations/ja.json +++ b/homeassistant/components/asuswrt/translations/ja.json @@ -19,7 +19,7 @@ "mode": "\u30e2\u30fc\u30c9", "name": "\u540d\u524d", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", - "port": "\u30dd\u30fc\u30c8", + "port": "\u30dd\u30fc\u30c8 (\u30d7\u30ed\u30c8\u30b3\u30eb\u306e\u30c7\u30d5\u30a9\u30eb\u30c8\u306e\u5834\u5408\u306f\u7a7a\u306e\u307e\u307e\u306b\u3057\u3066\u304a\u304d\u307e\u3059)", "protocol": "\u4f7f\u7528\u3059\u308b\u901a\u4fe1\u30d7\u30ed\u30c8\u30b3\u30eb", "ssh_key": "SSH\u30ad\u30fc \u30d5\u30a1\u30a4\u30eb\u3078\u306e\u30d1\u30b9 (\u30d1\u30b9\u30ef\u30fc\u30c9\u306e\u4ee3\u308f\u308a)", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" @@ -37,7 +37,7 @@ "dnsmasq": "dnsmasq.leases\u30d5\u30a1\u30a4\u30eb\u306e\u30eb\u30fc\u30bf\u30fc\u5185\u306e\u5834\u6240", "interface": "\u7d71\u8a08\u3092\u53d6\u5f97\u3057\u305f\u3044\u30a4\u30f3\u30bf\u30d5\u30a7\u30fc\u30b9(\u4f8b: eth0\u3001eth1\u306a\u3069)", "require_ip": "\u30c7\u30d0\u30a4\u30b9\u306b\u306fIP\u304c\u5fc5\u8981\u3067\u3059(\u30a2\u30af\u30bb\u30b9\u30dd\u30a4\u30f3\u30c8\u30e2\u30fc\u30c9\u306e\u5834\u5408)", - "track_unknown": "\u8ffd\u8de1\u4e0d\u660e/\u540d\u524d\u306e\u306a\u3044\u30c7\u30d0\u30a4\u30b9" + "track_unknown": "\u4e0d\u660e/\u540d\u524d\u306e\u306a\u3044\u30c7\u30d0\u30a4\u30b9\u3092\u8ffd\u8de1\u3059\u308b" }, "title": "AsusWRT\u306e\u30aa\u30d7\u30b7\u30e7\u30f3" } diff --git a/homeassistant/components/automation/translations/ja.json b/homeassistant/components/automation/translations/ja.json index 4392cebdbd0..150ef345e68 100644 --- a/homeassistant/components/automation/translations/ja.json +++ b/homeassistant/components/automation/translations/ja.json @@ -4,6 +4,7 @@ "fix_flow": { "step": { "confirm": { + "description": "\u81ea\u52d5\u5316 \" {name} \" (` {entity_id} `) \u306b\u306f\u3001\u4e0d\u660e\u306a\u30b5\u30fc\u30d3\u30b9 ` {service} ` \u3092\u547c\u3073\u51fa\u3059\u30a2\u30af\u30b7\u30e7\u30f3\u304c\u3042\u308a\u307e\u3059\u3002 \n\n\u3053\u306e\u30a8\u30e9\u30fc\u306b\u3088\u308a\u3001\u81ea\u52d5\u5316\u304c\u6b63\u3057\u304f\u5b9f\u884c\u3055\u308c\u306a\u304f\u306a\u308a\u307e\u3059\u3002\u3053\u306e\u30b5\u30fc\u30d3\u30b9\u304c\u5229\u7528\u3067\u304d\u306a\u304f\u306a\u3063\u305f\u304b\u3001\u30bf\u30a4\u30d7\u30df\u30b9\u304c\u539f\u56e0\u3067\u3042\u308b\u53ef\u80fd\u6027\u304c\u3042\u308a\u307e\u3059\u3002 \n\n\u3053\u306e\u30a8\u30e9\u30fc\u3092\u4fee\u6b63\u3059\u308b\u306b\u306f\u3001[\u30aa\u30fc\u30c8\u30e1\u30fc\u30b7\u30e7\u30f3\u3092\u7de8\u96c6]( {edit} ) \u3057\u3001\u3053\u306e\u30b5\u30fc\u30d3\u30b9\u3092\u547c\u3073\u51fa\u3059\u30a2\u30af\u30b7\u30e7\u30f3\u3092\u524a\u9664\u3057\u307e\u3059\u3002 \n\n\u4e0b\u306e\u9001\u4fe1\u3092\u30af\u30ea\u30c3\u30af\u3057\u3066\u3001\u3053\u306e\u81ea\u52d5\u5316\u3092\u4fee\u6b63\u3057\u305f\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "title": "{name} \u306f\u3001\u4e0d\u660e\u306a\u30b5\u30fc\u30d3\u30b9\u3092\u4f7f\u7528\u3057\u3066\u3044\u307e\u3059" } } diff --git a/homeassistant/components/awair/translations/ja.json b/homeassistant/components/awair/translations/ja.json index a6bbe56c838..11c151082b9 100644 --- a/homeassistant/components/awair/translations/ja.json +++ b/homeassistant/components/awair/translations/ja.json @@ -29,7 +29,7 @@ "data": { "host": "IP\u30a2\u30c9\u30ec\u30b9" }, - "description": "\u6b21\u306e\u624b\u9806\u306b\u5f93\u3063\u3066\u3001Awair Local API\u3092\u6709\u52b9\u306b\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059: {url}" + "description": "[\u3053\u308c\u3089\u306e\u624b\u9806]( {url} ) \u306b\u5f93\u3063\u3066\u3001Awair Local API \u3092\u6709\u52b9\u306b\u3059\u308b\u65b9\u6cd5\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002 \n\n\u5b8c\u4e86\u3057\u305f\u3089\u9001\u4fe1\u3092\u30af\u30ea\u30c3\u30af\u3057\u307e\u3059\u3002" }, "local_pick": { "data": { diff --git a/homeassistant/components/azure_event_hub/translations/ja.json b/homeassistant/components/azure_event_hub/translations/ja.json index 720e57d8066..717ce8f203b 100644 --- a/homeassistant/components/azure_event_hub/translations/ja.json +++ b/homeassistant/components/azure_event_hub/translations/ja.json @@ -2,9 +2,9 @@ "config": { "abort": { "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "cannot_connect": "configuration.yaml\u3092\u4f7f\u7528\u3057\u305f\u8a8d\u8a3c\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002yaml\u3092\u524a\u9664\u3057\u3066\u69cb\u6210\u30d5\u30ed\u30fc(config flow)\u3092\u4f7f\u7528\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "cannot_connect": "configuration.yaml \u304b\u3089\u306e\u8cc7\u683c\u60c5\u5831\u3067\u306e\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002yaml \u304b\u3089\u524a\u9664\u3057\u3066\u3001\u69cb\u6210\u30d5\u30ed\u30fc\u3092\u4f7f\u7528\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002", - "unknown": "configuration.yaml\u3092\u4f7f\u7528\u3057\u305f\u8a8d\u8a3c\u63a5\u7d9a\u304c\u4e0d\u660e\u306a\u30a8\u30e9\u30fc\u3067\u5931\u6557\u3057\u307e\u3057\u305f\u3002yaml\u3092\u524a\u9664\u3057\u3066\u69cb\u6210\u30d5\u30ed\u30fc(config flow)\u3092\u4f7f\u7528\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + "unknown": "configuration.yaml \u304b\u3089\u306e\u8cc7\u683c\u60c5\u5831\u3092\u4f7f\u7528\u3057\u305f\u63a5\u7d9a\u304c\u4e0d\u660e\u306a\u30a8\u30e9\u30fc\u3067\u5931\u6557\u3057\u307e\u3057\u305f\u3002yaml \u304b\u3089\u524a\u9664\u3057\u3066\u69cb\u6210\u30d5\u30ed\u30fc\u3092\u4f7f\u7528\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", diff --git a/homeassistant/components/bluemaestro/translations/id.json b/homeassistant/components/bluemaestro/translations/id.json new file mode 100644 index 00000000000..573eb39ed15 --- /dev/null +++ b/homeassistant/components/bluemaestro/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/bluemaestro/translations/ja.json b/homeassistant/components/bluemaestro/translations/ja.json new file mode 100644 index 00000000000..bab0033cc32 --- /dev/null +++ b/homeassistant/components/bluemaestro/translations/ja.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "not_supported": "\u30c7\u30d0\u30a4\u30b9\u304c\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/ja.json b/homeassistant/components/bluetooth/translations/ja.json index ea90f827e41..e19ee5b7dd2 100644 --- a/homeassistant/components/bluetooth/translations/ja.json +++ b/homeassistant/components/bluetooth/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "no_adapters": "Bluetooth\u30a2\u30c0\u30d7\u30bf\u30fc\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" + "no_adapters": "\u672a\u69cb\u6210\u306e Bluetooth \u30a2\u30c0\u30d7\u30bf\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/climacell/translations/ja.json b/homeassistant/components/climacell/translations/ja.json index 5c78820c853..e2742d11435 100644 --- a/homeassistant/components/climacell/translations/ja.json +++ b/homeassistant/components/climacell/translations/ja.json @@ -3,10 +3,10 @@ "step": { "init": { "data": { - "timestep": "\u6700\u5c0f: NowCast Forecasts\u306e\u9593" + "timestep": "\u6700\u5c0f\u3002 NowCast \u4e88\u6e2c\u306e\u9593" }, "description": "`nowcast` forecast(\u4e88\u6e2c) \u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u3092\u6709\u52b9\u306b\u3059\u308b\u3053\u3068\u3092\u9078\u629e\u3057\u305f\u5834\u5408\u3001\u5404\u4e88\u6e2c\u9593\u306e\u5206\u6570\u3092\u8a2d\u5b9a\u3059\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u3059\u3002\u63d0\u4f9b\u3055\u308c\u308bforecast(\u4e88\u6e2c)\u306e\u6570\u306f\u3001forecast(\u4e88\u6e2c)\u306e\u9593\u306b\u9078\u629e\u3057\u305f\u5206\u6570\u306b\u4f9d\u5b58\u3057\u307e\u3059\u3002", - "title": "[%key:component::climacell::title%]\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u66f4\u65b0\u3057\u307e\u3059" + "title": "ClimaCell \u30aa\u30d7\u30b7\u30e7\u30f3\u306e\u66f4\u65b0" } } } diff --git a/homeassistant/components/demo/translations/ja.json b/homeassistant/components/demo/translations/ja.json index bd4d650de1c..1130653c893 100644 --- a/homeassistant/components/demo/translations/ja.json +++ b/homeassistant/components/demo/translations/ja.json @@ -15,7 +15,7 @@ "fix_flow": { "step": { "confirm": { - "description": "\u30d6\u30ea\u30f3\u30ab\u30fc\u6db2\u306e\u88dc\u5145\u304c\u5b8c\u4e86\u3057\u305f\u3089\u3001OK\u3092\u62bc\u3057\u3066\u304f\u3060\u3055\u3044", + "description": "\u30a6\u30a4\u30f3\u30ab\u30fc\u6db2\u304c\u88dc\u5145\u3055\u308c\u305f\u3089\u3001SUBMIT \u3092\u62bc\u3057\u307e\u3059", "title": "\u30d6\u30ea\u30f3\u30ab\u30fc\u6db2\u3092\u88dc\u5145\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059" } } diff --git a/homeassistant/components/derivative/translations/ja.json b/homeassistant/components/derivative/translations/ja.json index c5939e95deb..e89ba315ccd 100644 --- a/homeassistant/components/derivative/translations/ja.json +++ b/homeassistant/components/derivative/translations/ja.json @@ -11,12 +11,12 @@ "unit_time": "\u6642\u9593\u5358\u4f4d" }, "data_description": { - "round": "\u51fa\u529b\u5024\u306e\u5c0f\u6570\u70b9\u4ee5\u4e0b\u306e\u6841\u6570\u3002", + "round": "\u51fa\u529b\u306e 10 \u9032\u6570\u306e\u6841\u6570\u3092\u5236\u5fa1\u3057\u307e\u3059\u3002", "time_window": "\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u308b\u5834\u5408\u3001\u30bb\u30f3\u30b5\u30fc\u306e\u5024\u306f\u3053\u306e\u30a6\u30a3\u30f3\u30c9\u30a6\u5185\u306e\u5fae\u5206\u306e\u6642\u9593\u52a0\u91cd\u79fb\u52d5\u5e73\u5747\u3068\u306a\u308a\u307e\u3059\u3002", - "unit_prefix": "\u5fae\u5206\u306f\u3001\u9078\u629e\u3055\u308c\u305f\u5358\u4f4d\u306e\u30d7\u30ec\u30d5\u30a3\u30c3\u30af\u30b9\u3068\u5fae\u5206\u306e\u6642\u9593\u5358\u4f4d\u306b\u5f93\u3063\u3066\u30b9\u30b1\u30fc\u30ea\u30f3\u30b0\u3055\u308c\u307e\u3059\u3002" + "unit_prefix": "\u51fa\u529b\u306f\u3001\u9078\u629e\u3057\u305f\u30e1\u30c8\u30ea\u30c3\u30af \u30d7\u30ec\u30d5\u30a3\u30c3\u30af\u30b9\u3068\u5c0e\u95a2\u6570\u306e\u6642\u9593\u5358\u4f4d\u306b\u5f93\u3063\u3066\u30b9\u30b1\u30fc\u30ea\u30f3\u30b0\u3055\u308c\u307e\u3059\u3002" }, - "description": "\u7cbe\u5ea6\u306f\u3001\u51fa\u529b\u306e\u5c0f\u6570\u70b9\u4ee5\u4e0b\u306e\u6841\u6570\u3092\u5236\u5fa1\u3057\u307e\u3059\u3002\n\u6642\u9593\u7a93\u304c0\u3067\u306a\u3044\u5834\u5408\u3001\u30bb\u30f3\u30b5\u30fc\u306e\u5024\u306f\u7a93\u5185\u306e\u5fae\u5206\u306e\u6642\u9593\u52a0\u91cd\u79fb\u52d5\u5e73\u5747\u306b\u306a\u308a\u307e\u3059\u3002\n\u5fae\u5206\u306f\u3001\u9078\u629e\u3055\u308c\u305f\u5358\u4f4d\u306e\u30d7\u30ea\u30d5\u30a3\u30c3\u30af\u30b9\u3068\u5fae\u5206\u306e\u6642\u9593\u5358\u4f4d\u306b\u5f93\u3063\u3066\u30b9\u30b1\u30fc\u30ea\u30f3\u30b0\u3055\u308c\u307e\u3059\u3002", - "title": "\u65b0\u3057\u3044\u6d3e\u751f(Derivative)\u30bb\u30f3\u30b5\u30fc" + "description": "\u30bb\u30f3\u30b5\u30fc\u306e\u5c0e\u95a2\u6570\u3092\u63a8\u5b9a\u3059\u308b\u30bb\u30f3\u30b5\u30fc\u3092\u4f5c\u6210\u3057\u307e\u3059\u3002", + "title": "\u5fae\u5206\u30bb\u30f3\u30b5\u30fc\u3092\u8ffd\u52a0" } } }, @@ -32,9 +32,9 @@ "unit_time": "\u6642\u9593\u5358\u4f4d" }, "data_description": { - "round": "\u51fa\u529b\u5024\u306e\u5c0f\u6570\u70b9\u4ee5\u4e0b\u306e\u6841\u6570\u3002", + "round": "\u51fa\u529b\u306e 10 \u9032\u6570\u306e\u6841\u6570\u3092\u5236\u5fa1\u3057\u307e\u3059\u3002", "time_window": "\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u308b\u5834\u5408\u3001\u30bb\u30f3\u30b5\u30fc\u306e\u5024\u306f\u3053\u306e\u30a6\u30a3\u30f3\u30c9\u30a6\u5185\u306e\u5fae\u5206\u306e\u6642\u9593\u52a0\u91cd\u79fb\u52d5\u5e73\u5747\u3068\u306a\u308a\u307e\u3059\u3002", - "unit_prefix": "\u5fae\u5206\u306f\u3001\u9078\u629e\u3055\u308c\u305f\u5358\u4f4d\u306e\u30d7\u30ec\u30d5\u30a3\u30c3\u30af\u30b9\u3068\u5fae\u5206\u306e\u6642\u9593\u5358\u4f4d\u306b\u5f93\u3063\u3066\u30b9\u30b1\u30fc\u30ea\u30f3\u30b0\u3055\u308c\u307e\u3059\u3002." + "unit_prefix": "\u51fa\u529b\u306f\u3001\u9078\u629e\u3057\u305f\u30e1\u30c8\u30ea\u30c3\u30af \u30d7\u30ec\u30d5\u30a3\u30c3\u30af\u30b9\u3068\u5c0e\u95a2\u6570\u306e\u6642\u9593\u5358\u4f4d\u306b\u5f93\u3063\u3066\u30b9\u30b1\u30fc\u30ea\u30f3\u30b0\u3055\u308c\u307e\u3059\u3002." } } } diff --git a/homeassistant/components/ecowitt/translations/ca.json b/homeassistant/components/ecowitt/translations/ca.json index c2c0bfcfeab..beac6285b0a 100644 --- a/homeassistant/components/ecowitt/translations/ca.json +++ b/homeassistant/components/ecowitt/translations/ca.json @@ -1,5 +1,8 @@ { "config": { + "create_entry": { + "default": "Per acabar de configurar la integraci\u00f3, utilitza l'aplicaci\u00f3 Ecowitt (al m\u00f2bil) o v\u00e9s a Ecowitt WebUI a trav\u00e9s d'un navegador anant a l'adre\u00e7a IP de l'estaci\u00f3.\n\nTria la teva estaci\u00f3 -> Men\u00fa Altres -> Servidors de pujada DIY. Prem seg\u00fcent (next) i selecciona 'Personalitzat' ('Customized')\n\n- IP del servidor: `{servidor}`\n- Ruta: `{path}`\n- Port: `{port}`\n\nFes clic a 'Desa' ('Save')." + }, "error": { "invalid_port": "Aquest port ja est\u00e0 en \u00fas.", "unknown": "Error inesperat" @@ -10,7 +13,7 @@ "path": "Cam\u00ed amb testimoni de seguretat", "port": "Escoltant el port" }, - "description": "S'han de completar els seg\u00fcents passos per configurar aquesta integraci\u00f3.\n\nUtilitza l'aplicaci\u00f3 Ecowitt (al teu tel\u00e8fon) o accedeix a la Web Ecowitt des d'un navegador a l'adre\u00e7a IP de l'estaci\u00f3.\nEscull la teva estaci\u00f3 -> Menu Others -> DIY Upload Servers.\nFes clic a Seg\u00fcent i selecciona \"Personalitzat\"\n\nEscull el protocol Ecowitt i estableix la IP o l'adre\u00e7a del teu servidor Home Assistant.\nLa ruta ha de conincidir, la pots copiar amb el testimoni de seguretat /.\nDesa la configuraci\u00f3. L'Ecowitt hauria de comen\u00e7ar a enviar dades al teu servidor." + "description": "Est\u00e0s segur que vols configurar Ecowitt?" } } } diff --git a/homeassistant/components/ecowitt/translations/ja.json b/homeassistant/components/ecowitt/translations/ja.json index 821eb535e7b..0853f5068b1 100644 --- a/homeassistant/components/ecowitt/translations/ja.json +++ b/homeassistant/components/ecowitt/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "create_entry": { + "default": "\u7d71\u5408\u306e\u8a2d\u5b9a\u3092\u5b8c\u4e86\u3059\u308b\u306b\u306f\u3001Ecowitt \u30a2\u30d7\u30ea (\u96fb\u8a71\u3067) \u3092\u4f7f\u7528\u3059\u308b\u304b\u3001\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3\u306e IP \u30a2\u30c9\u30ec\u30b9\u3067\u30d6\u30e9\u30a6\u30b6\u30fc\u3067 Ecowitt WebUI \u306b\u30a2\u30af\u30bb\u30b9\u3057\u307e\u3059\u3002 \n\n\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3\u3092\u9078\u629e - >\u30e1\u30cb\u30e5\u30fc\u306e [\u305d\u306e\u4ed6] - > DIY \u30a2\u30c3\u30d7\u30ed\u30fc\u30c9 \u30b5\u30fc\u30d0\u30fc] \u3092\u9078\u629e\u3057\u307e\u3059\u3002\u6b21\u306b\u30d2\u30c3\u30c8\u3057\u3001\u300c\u30ab\u30b9\u30bf\u30de\u30a4\u30ba\u300d\u3092\u9078\u629e\u3057\u307e\u3059\n\n - \u30b5\u30fc\u30d0\u30fc IP: ` {server} `\n - \u30d1\u30b9: ` {path} `\n - \u30dd\u30fc\u30c8: ` {port} ` \n\n \u300c\u4fdd\u5b58\u300d\u3092\u30af\u30ea\u30c3\u30af\u3057\u307e\u3059\u3002" + }, "error": { "invalid_port": "\u30dd\u30fc\u30c8\u306f\u3059\u3067\u306b\u4f7f\u7528\u3055\u308c\u3066\u3044\u307e\u3059\u3002", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" diff --git a/homeassistant/components/fibaro/translations/id.json b/homeassistant/components/fibaro/translations/id.json index 715ad91c275..b54dd126aed 100644 --- a/homeassistant/components/fibaro/translations/id.json +++ b/homeassistant/components/fibaro/translations/id.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Perangkat sudah dikonfigurasi" + "already_configured": "Perangkat sudah dikonfigurasi", + "reauth_successful": "Autentikasi ulang berhasil" }, "error": { "cannot_connect": "Gagal terhubung", @@ -9,6 +10,13 @@ "unknown": "Kesalahan yang tidak diharapkan" }, "step": { + "reauth_confirm": { + "data": { + "password": "Kata Sandi" + }, + "description": "Perbarui kata sandi Anda untuk {username}", + "title": "Autentikasi Ulang Integrasi" + }, "user": { "data": { "import_plugins": "Impor entitas dari plugin fibaro?", diff --git a/homeassistant/components/fibaro/translations/ja.json b/homeassistant/components/fibaro/translations/ja.json index 8fc6562ff3b..a2b94a4bb17 100644 --- a/homeassistant/components/fibaro/translations/ja.json +++ b/homeassistant/components/fibaro/translations/ja.json @@ -9,6 +9,9 @@ "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { + "reauth_confirm": { + "description": "{username}\u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u66f4\u65b0\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + }, "user": { "data": { "import_plugins": "fibaro\u30d7\u30e9\u30b0\u30a4\u30f3\u304b\u3089\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u3092\u30a4\u30f3\u30dd\u30fc\u30c8\u3057\u307e\u3059\u304b\uff1f", diff --git a/homeassistant/components/fibaro/translations/no.json b/homeassistant/components/fibaro/translations/no.json index 8c868bb1ad8..f98835533f5 100644 --- a/homeassistant/components/fibaro/translations/no.json +++ b/homeassistant/components/fibaro/translations/no.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Enheten er allerede konfigurert" + "already_configured": "Enheten er allerede konfigurert", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" }, "error": { "cannot_connect": "Tilkobling mislyktes", @@ -9,6 +10,13 @@ "unknown": "Uventet feil" }, "step": { + "reauth_confirm": { + "data": { + "password": "Passord" + }, + "description": "Oppdater passordet ditt for {username}", + "title": "Godkjenne integrering p\u00e5 nytt" + }, "user": { "data": { "import_plugins": "Importere enheter fra fibaro plugins?", diff --git a/homeassistant/components/fibaro/translations/ru.json b/homeassistant/components/fibaro/translations/ru.json index 56e75b5fa34..ce065a9d1b5 100644 --- a/homeassistant/components/fibaro/translations/ru.json +++ b/homeassistant/components/fibaro/translations/ru.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", @@ -9,6 +10,13 @@ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u041e\u0431\u043d\u043e\u0432\u0438\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 {username}", + "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": { "import_plugins": "\u0418\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043e\u0431\u044a\u0435\u043a\u0442\u044b \u0438\u0437 \u043f\u043b\u0430\u0433\u0438\u043d\u043e\u0432 fibaro", diff --git a/homeassistant/components/github/translations/ja.json b/homeassistant/components/github/translations/ja.json index f7dfc37f64d..614b37f714e 100644 --- a/homeassistant/components/github/translations/ja.json +++ b/homeassistant/components/github/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "could_not_register": "\u7d71\u5408\u3068GitHub\u3068\u306e\u767b\u9332\u304c\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f" + "could_not_register": "GitHub \u3068\u306e\u7d71\u5408\u3092\u767b\u9332\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f" }, "progress": { "wait_for_device": "1. {url} \u3092\u958b\u304f\n2. \u6b21\u306e\u30ad\u30fc\u3092\u8cbc\u308a\u4ed8\u3051\u3066\u3001\u7d71\u5408\u3092\u8a8d\u8a3c\u3057\u307e\u3059\u3002\n```\n{code}\n```\n" diff --git a/homeassistant/components/group/translations/ja.json b/homeassistant/components/group/translations/ja.json index 46fbbc7b22f..c3403eafe03 100644 --- a/homeassistant/components/group/translations/ja.json +++ b/homeassistant/components/group/translations/ja.json @@ -60,7 +60,7 @@ "title": "\u65b0\u3057\u3044\u30b0\u30eb\u30fc\u30d7" }, "user": { - "description": "\u30b0\u30eb\u30fc\u30d7\u306e\u7a2e\u985e\u3092\u9078\u629e", + "description": "\u30b0\u30eb\u30fc\u30d7\u3092\u4f7f\u7528\u3059\u308b\u3068\u3001\u540c\u3058\u30bf\u30a4\u30d7\u306e\u8907\u6570\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u3092\u8868\u3059\u65b0\u3057\u3044\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u3092\u4f5c\u6210\u3067\u304d\u307e\u3059\u3002", "menu_options": { "binary_sensor": "\u30d0\u30a4\u30ca\u30ea\u30fc\u30bb\u30f3\u30b5\u30fc\u30b0\u30eb\u30fc\u30d7", "cover": "\u30ab\u30d0\u30fc\u30b0\u30eb\u30fc\u30d7", diff --git a/homeassistant/components/guardian/translations/ja.json b/homeassistant/components/guardian/translations/ja.json index 7e95869e071..96e66212b12 100644 --- a/homeassistant/components/guardian/translations/ja.json +++ b/homeassistant/components/guardian/translations/ja.json @@ -23,6 +23,7 @@ "fix_flow": { "step": { "confirm": { + "description": "\u3053\u306e\u30b5\u30fc\u30d3\u30b9\u3092\u4f7f\u7528\u3059\u308b\u3059\u3079\u3066\u306e\u81ea\u52d5\u5316\u307e\u305f\u306f\u30b9\u30af\u30ea\u30d7\u30c8\u3092\u66f4\u65b0\u3057\u3066\u3001\u4ee3\u308f\u308a\u306b ` {alternate_service} ` \u30b5\u30fc\u30d3\u30b9\u3092\u4f7f\u7528\u3057\u3001\u30bf\u30fc\u30b2\u30c3\u30c8 \u30a8\u30f3\u30c6\u30a3\u30c6\u30a3 ID \u3092 ` {alternate_target} ` \u306b\u3057\u307e\u3059\u3002\u6b21\u306b\u3001\u4e0b\u306e [\u9001\u4fe1] \u3092\u30af\u30ea\u30c3\u30af\u3057\u3066\u3001\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u6e08\u307f\u3068\u3057\u3066\u30de\u30fc\u30af\u3057\u307e\u3059\u3002", "title": "{deprecated_service} \u30b5\u30fc\u30d3\u30b9\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" } } diff --git a/homeassistant/components/here_travel_time/translations/ja.json b/homeassistant/components/here_travel_time/translations/ja.json index ae3eb1d4f73..5d83a13fbe0 100644 --- a/homeassistant/components/here_travel_time/translations/ja.json +++ b/homeassistant/components/here_travel_time/translations/ja.json @@ -10,7 +10,7 @@ "step": { "destination_coordinates": { "data": { - "destination": "GPS\u5ea7\u6a19\u3068\u3057\u3066\u306e\u76ee\u7684\u5730" + "destination": "GPS \u5ea7\u6a19\u3068\u3057\u3066\u306e\u76ee\u7684\u5730" }, "title": "\u76ee\u7684\u5730\u3092\u9078\u629e" }, diff --git a/homeassistant/components/homekit_controller/translations/sensor.id.json b/homeassistant/components/homekit_controller/translations/sensor.id.json index feb6a4f869b..2ba9de75815 100644 --- a/homeassistant/components/homekit_controller/translations/sensor.id.json +++ b/homeassistant/components/homekit_controller/translations/sensor.id.json @@ -1,12 +1,20 @@ { "state": { "homekit_controller__thread_node_capabilities": { + "border_router_capable": "Kemampuan Router Perbatasan", "full": "Perangkat Akhir Lengkap", "minimal": "Perangkat Akhir Minimal", - "none": "Tidak Ada" + "none": "Tidak Ada", + "router_eligible": "Perangkat Akhir yang Memenuhi Syarat Router", + "sleepy": "Perangkat Akhir yang Jenak" }, "homekit_controller__thread_status": { + "border_router": "Router Perbatasan", + "child": "Anakan", + "detached": "Terpisah", "disabled": "Dinonaktifkan", + "joining": "Bergabung", + "leader": "Kepala", "router": "Router" } } diff --git a/homeassistant/components/homewizard/translations/ja.json b/homeassistant/components/homewizard/translations/ja.json index 51b11300936..f7e1e33ee5f 100644 --- a/homeassistant/components/homewizard/translations/ja.json +++ b/homeassistant/components/homewizard/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "api_not_enabled": "API\u304c\u6709\u52b9\u306b\u306a\u3063\u3066\u3044\u307e\u305b\u3093\u3002HomeWizard Energy App\u306esettings\u3067API\u3092\u6709\u52b9\u306b\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "device_not_supported": "\u3053\u306e\u30c7\u30d0\u30a4\u30b9\u306f\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093", - "invalid_discovery_parameters": "\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u306a\u3044API\u30d0\u30fc\u30b8\u30e7\u30f3", + "invalid_discovery_parameters": "\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u306a\u3044 API \u30d0\u30fc\u30b8\u30e7\u30f3\u304c\u691c\u51fa\u3055\u308c\u307e\u3057\u305f", "unknown_error": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { diff --git a/homeassistant/components/hue/translations/hu.json b/homeassistant/components/hue/translations/hu.json index af8f616190a..32f52c47a7b 100644 --- a/homeassistant/components/hue/translations/hu.json +++ b/homeassistant/components/hue/translations/hu.json @@ -1,19 +1,19 @@ { "config": { "abort": { - "all_configured": "M\u00e1r minden Philips Hue bridge konfigur\u00e1lt", + "all_configured": "M\u00e1r minden Philips Hue bridge konfigur\u00e1lva van", "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", "cannot_connect": "Sikertelen csatlakoz\u00e1s", "discover_timeout": "Nem tal\u00e1lhat\u00f3 a Hue bridge", "invalid_host": "\u00c9rv\u00e9nytelen c\u00edm", - "no_bridges": "Nem tal\u00e1lhat\u00f3 Philips Hue bridget", + "no_bridges": "Nem tal\u00e1lhat\u00f3 Philips Hue bridge", "not_hue_bridge": "Nem egy Hue Bridge", "unknown": "Ismeretlen hiba t\u00f6rt\u00e9nt" }, "error": { "linking": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt", - "register_failed": "Regisztr\u00e1ci\u00f3 nem siker\u00fclt, k\u00e9rem pr\u00f3b\u00e1lja \u00fajra" + "register_failed": "A regisztr\u00e1ci\u00f3 nem siker\u00fclt, k\u00e9rem pr\u00f3b\u00e1lja \u00fajra" }, "step": { "init": { diff --git a/homeassistant/components/hue/translations/ja.json b/homeassistant/components/hue/translations/ja.json index dcf38b0e48c..c017e094abd 100644 --- a/homeassistant/components/hue/translations/ja.json +++ b/homeassistant/components/hue/translations/ja.json @@ -55,14 +55,14 @@ }, "trigger_type": { "double_short_release": "\u4e21\u65b9\u306e \"{subtype}\" \u3092\u96e2\u3059", - "initial_press": "\u30dc\u30bf\u30f3 \"{subtype}\" \u6700\u521d\u306b\u62bc\u3055\u308c\u305f", - "long_release": "\u30dc\u30bf\u30f3 \"{subtype}\" \u96e2\u3057\u305f\u5f8c\u306b\u9577\u62bc\u3057", + "initial_press": "\u300c {subtype} \u300d\u304c\u6700\u521d\u306b\u62bc\u3055\u308c\u307e\u3057\u305f", + "long_release": "\u300c {subtype} \u300d\u3092\u9577\u62bc\u3057\u3057\u3066\u96e2\u3059", "remote_button_long_release": "\u9577\u62bc\u3057\u3059\u308b\u3068 \"{subtype}\" \u30dc\u30bf\u30f3\u304c\u30ea\u30ea\u30fc\u30b9\u3055\u308c\u308b", "remote_button_short_press": "\"{subtype}\" \u30dc\u30bf\u30f3\u304c\u62bc\u3055\u308c\u307e\u3057\u305f\u3002", "remote_button_short_release": "\"{subtype}\" \u30dc\u30bf\u30f3\u304c\u30ea\u30ea\u30fc\u30b9\u3055\u308c\u307e\u3057\u305f", "remote_double_button_long_press": "\u4e21\u65b9\u306e \"{subtype}\" \u306f\u9577\u62bc\u3057\u5f8c\u306b\u30ea\u30ea\u30fc\u30b9\u3055\u308c\u307e\u3057\u305f", "remote_double_button_short_press": "\u4e21\u65b9\u306e \"{subtype}\" \u3092\u96e2\u3059", - "repeat": "\u30dc\u30bf\u30f3 \"{subtype}\" \u3092\u62bc\u3057\u305f\u307e\u307e", + "repeat": "\u300c {subtype} \u300d\u304c\u62bc\u3055\u308c\u305f", "short_release": "\u30dc\u30bf\u30f3 \"{subtype}\" \u77ed\u62bc\u3057\u306e\u5f8c\u306b\u96e2\u3059", "start": "\"{subtype}\" \u304c\u6700\u521d\u306b\u62bc\u3055\u308c\u307e\u3057\u305f" } diff --git a/homeassistant/components/integration/translations/ja.json b/homeassistant/components/integration/translations/ja.json index 237d7eef353..011183a820a 100644 --- a/homeassistant/components/integration/translations/ja.json +++ b/homeassistant/components/integration/translations/ja.json @@ -8,15 +8,15 @@ "round": "\u7cbe\u5ea6", "source": "\u5165\u529b\u30bb\u30f3\u30b5\u30fc", "unit_prefix": "\u30e1\u30c8\u30ea\u30c3\u30af\u63a5\u982d\u8f9e", - "unit_time": "\u7a4d\u7b97\u6642\u9593" + "unit_time": "\u6642\u9593\u5358\u4f4d" }, "data_description": { "round": "\u51fa\u529b\u5024\u306e\u5c0f\u6570\u70b9\u4ee5\u4e0b\u306e\u6841\u6570\u3002", "unit_prefix": "\u51fa\u529b\u306f\u3001\u9078\u629e\u3055\u308c\u305f\u5358\u4f4d\u306e\u30d7\u30ec\u30d5\u30a3\u30c3\u30af\u30b9\u306b\u5f93\u3063\u3066\u30b9\u30b1\u30fc\u30ea\u30f3\u30b0\u3055\u308c\u307e\u3059\u3002", "unit_time": "\u51fa\u529b\u306f\u3001\u9078\u629e\u3055\u308c\u305f\u6642\u9593\u5358\u4f4d\u306b\u5f93\u3063\u3066\u30b9\u30b1\u30fc\u30ea\u30f3\u30b0\u3055\u308c\u307e\u3059\u3002" }, - "description": "\u7cbe\u5ea6\u306f\u3001\u51fa\u529b\u306e\u5c0f\u6570\u70b9\u4ee5\u4e0b\u306e\u6841\u6570\u3092\u5236\u5fa1\u3057\u307e\u3059\u3002\n\u5408\u8a08\u306f\u3001\u9078\u629e\u3055\u308c\u305f\u5358\u4f4d\u306e\u30d7\u30ea\u30d5\u30a3\u30c3\u30af\u30b9\u3068\u7a4d\u5206\u6642\u9593\u306b\u5f93\u3063\u3066\u30b9\u30b1\u30fc\u30ea\u30f3\u30b0\u3055\u308c\u307e\u3059\u3002", - "title": "\u65b0\u3057\u3044\u7d71\u5408\u30bb\u30f3\u30b5\u30fc" + "description": "\u30ea\u30fc\u30de\u30f3\u548c\u3092\u8a08\u7b97\u3057\u3066\u30bb\u30f3\u30b5\u30fc\u306e\u7a4d\u5206\u3092\u63a8\u5b9a\u3059\u308b\u30bb\u30f3\u30b5\u30fc\u3092\u4f5c\u6210\u3057\u307e\u3059\u3002", + "title": "\u30ea\u30fc\u30de\u30f3\u548c\u7a4d\u5206\u30bb\u30f3\u30b5\u30fc\u3092\u8ffd\u52a0" } } }, diff --git a/homeassistant/components/intellifire/translations/ja.json b/homeassistant/components/intellifire/translations/ja.json index 6c74e4743ed..0b09ffc61fe 100644 --- a/homeassistant/components/intellifire/translations/ja.json +++ b/homeassistant/components/intellifire/translations/ja.json @@ -23,7 +23,7 @@ }, "manual_device_entry": { "data": { - "host": "\u30db\u30b9\u30c8" + "host": "\u30db\u30b9\u30c8 (IP \u30a2\u30c9\u30ec\u30b9)" }, "description": "\u30ed\u30fc\u30ab\u30eb\u8a2d\u5b9a" }, diff --git a/homeassistant/components/iss/translations/ja.json b/homeassistant/components/iss/translations/ja.json index d53b9f8fecb..0568f747ed9 100644 --- a/homeassistant/components/iss/translations/ja.json +++ b/homeassistant/components/iss/translations/ja.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "latitude_longitude_not_defined": "Home Assistant\u3067\u7def\u5ea6\u3068\u7d4c\u5ea6\u304c\u5b9a\u7fa9\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", + "latitude_longitude_not_defined": "Home Assistant \u3067\u306f\u3001\u7def\u5ea6\u3068\u7d4c\u5ea6\u306f\u5b9a\u7fa9\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "user": { - "description": "\u56fd\u969b\u5b87\u5b99\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3\u306e\u8a2d\u5b9a\u3092\u3057\u307e\u3059\u304b\uff1f" + "description": "\u56fd\u969b\u5b87\u5b99\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3 (ISS) \u3092\u69cb\u6210\u3057\u307e\u3059\u304b?" } } }, diff --git a/homeassistant/components/isy994/translations/ja.json b/homeassistant/components/isy994/translations/ja.json index 60b69d07363..2ec57251d65 100644 --- a/homeassistant/components/isy994/translations/ja.json +++ b/homeassistant/components/isy994/translations/ja.json @@ -17,7 +17,7 @@ "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, - "description": "{host} \u306e\u8cc7\u683c\u60c5\u5831\u304c\u7121\u52b9\u306b\u306a\u308a\u307e\u3057\u305f\u3002", + "description": "{host}\u306e\u8a8d\u8a3c\u60c5\u5831\u304c\u7121\u52b9\u306b\u306a\u308a\u307e\u3057\u305f\u3002", "title": "ISY\u306e\u518d\u8a8d\u8a3c" }, "user": { diff --git a/homeassistant/components/knx/translations/ja.json b/homeassistant/components/knx/translations/ja.json index bbac3566bca..3272508a525 100644 --- a/homeassistant/components/knx/translations/ja.json +++ b/homeassistant/components/knx/translations/ja.json @@ -61,13 +61,13 @@ "user_id": "\u591a\u304f\u306e\u5834\u5408\u3001\u3053\u308c\u306f\u30c8\u30f3\u30cd\u30eb\u756a\u53f7+1\u3067\u3059\u3002\u3057\u305f\u304c\u3063\u3066\u3001 '\u30c8\u30f3\u30cd\u30eb2' \u306e\u30e6\u30fc\u30b6\u30fcID\u306f\u3001'3 '\u306b\u306a\u308a\u307e\u3059\u3002", "user_password": "ETS\u306e\u30c8\u30f3\u30cd\u30eb\u306e\u3001'\u30d7\u30ed\u30d1\u30c6\u30a3' \u30d1\u30cd\u30eb\u3067\u8a2d\u5b9a\u3055\u308c\u305f\u7279\u5b9a\u306e\u30c8\u30f3\u30cd\u30eb\u63a5\u7d9a\u7528\u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u3002" }, - "description": "IP\u30bb\u30ad\u30e5\u30a2\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + "description": "IP \u30bb\u30ad\u30e5\u30a2\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "secure_tunneling": { - "description": "IP\u30bb\u30ad\u30e5\u30a2\u306e\u8a2d\u5b9a\u65b9\u6cd5\u3092\u9078\u629e\u3057\u307e\u3059\u3002", + "description": "KNX/IP \u30bb\u30ad\u30e5\u30a2\u3092\u69cb\u6210\u3059\u308b\u65b9\u6cd5\u3092\u9078\u629e\u3057\u307e\u3059\u3002", "menu_options": { - "secure_knxkeys": "IP\u30bb\u30ad\u30e5\u30a2\u60c5\u5831\u3092\u542b\u3080knxkeys\u30d5\u30a1\u30a4\u30eb\u3092\u8a2d\u5b9a\u3057\u307e\u3059", - "secure_manual": "IP\u30bb\u30ad\u30e5\u30a2\u3092\u624b\u52d5\u3067\u8a2d\u5b9a\u3059\u308b" + "secure_knxkeys": "IP \u30bb\u30ad\u30e5\u30a2 \u30ad\u30fc\u3092\u542b\u3080\u300c.knxkeys\u300d\u30d5\u30a1\u30a4\u30eb\u3092\u4f7f\u7528\u3059\u308b", + "secure_manual": "IP \u30bb\u30ad\u30e5\u30a2 \u30ad\u30fc\u3092\u624b\u52d5\u3067\u69cb\u6210\u3059\u308b" } }, "tunnel": { @@ -102,7 +102,7 @@ "multicast_group": "\u30eb\u30fc\u30c6\u30a3\u30f3\u30b0\u3068\u691c\u51fa\u306b\u4f7f\u7528\u3055\u308c\u307e\u3059\u3002\u30c7\u30d5\u30a9\u30eb\u30c8t: `224.0.23.12`", "multicast_port": "\u30eb\u30fc\u30c6\u30a3\u30f3\u30b0\u3068\u691c\u51fa\u306b\u4f7f\u7528\u3055\u308c\u307e\u3059\u3002\u30c7\u30d5\u30a9\u30eb\u30c8: `3671`", "rate_limit": "1\u79d2\u3042\u305f\u308a\u306e\u6700\u5927\u9001\u4fe1\u30c6\u30ec\u30b0\u30e9\u30e0\u3002\n\u63a8\u5968: 20\uff5e40", - "state_updater": "KNX Bus\u304b\u3089\u306e\u72b6\u614b\u306e\u8aad\u307f\u53d6\u308a\u3092\u30b0\u30ed\u30fc\u30d0\u30eb\u306b\u6709\u52b9\u307e\u305f\u306f\u7121\u52b9\u306b\u3057\u307e\u3059\u3002\u7121\u52b9\u306b\u3059\u308b\u3068\u3001Home Assistant\u306f\u3001KNX Bus\u304b\u3089\u30a2\u30af\u30c6\u30a3\u30d6\u306b\u72b6\u614b\u3092\u53d6\u5f97\u3057\u306a\u304f\u306a\u308b\u306e\u3067\u3001`sync_state`\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u30aa\u30d7\u30b7\u30e7\u30f3\u306f\u610f\u5473\u3092\u6301\u305f\u306a\u304f\u306a\u308a\u307e\u3059\u3002" + "state_updater": "KNX \u30d0\u30b9\u304b\u3089\u72b6\u614b\u3092\u8aad\u307f\u53d6\u308b\u305f\u3081\u306e\u30c7\u30d5\u30a9\u30eb\u30c8\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u3002\u7121\u52b9\u306b\u3059\u308b\u3068\u3001Home Assistant \u306f KNX \u30d0\u30b9\u304b\u3089\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u306e\u72b6\u614b\u3092\u7a4d\u6975\u7684\u306b\u53d6\u5f97\u3057\u307e\u305b\u3093\u3002 \u300csync_state\u300d\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3 \u30aa\u30d7\u30b7\u30e7\u30f3\u3067\u30aa\u30fc\u30d0\u30fc\u30e9\u30a4\u30c9\u3067\u304d\u307e\u3059\u3002" } }, "tunnel": { diff --git a/homeassistant/components/lametric/translations/ja.json b/homeassistant/components/lametric/translations/ja.json index 4f6768ca80b..5fc913a12b7 100644 --- a/homeassistant/components/lametric/translations/ja.json +++ b/homeassistant/components/lametric/translations/ja.json @@ -43,6 +43,7 @@ }, "issues": { "manual_migration": { + "description": "LaMetric \u7d71\u5408\u306f\u6700\u65b0\u5316\u3055\u308c\u307e\u3057\u305f\u3002\u30e6\u30fc\u30b6\u30fc \u30a4\u30f3\u30bf\u30fc\u30d5\u30a7\u30a4\u30b9\u3092\u4ecb\u3057\u3066\u69cb\u6210\u304a\u3088\u3073\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3055\u308c\u3001\u901a\u4fe1\u306f\u30ed\u30fc\u30ab\u30eb\u306b\u306a\u308a\u307e\u3057\u305f\u3002 \n\n\u6b8b\u5ff5\u306a\u304c\u3089\u3001\u53ef\u80fd\u306a\u81ea\u52d5\u79fb\u884c\u30d1\u30b9\u304c\u306a\u3044\u305f\u3081\u3001Home Assistant \u3067 LaMetric \u3092\u518d\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u65b9\u6cd5\u306b\u3064\u3044\u3066\u306f\u3001Home Assistant LaMetric \u7d71\u5408\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002 \n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001\u53e4\u3044 LaMetric YAML \u69cb\u6210\u3092 configuration.yaml \u30d5\u30a1\u30a4\u30eb\u304b\u3089\u524a\u9664\u3057\u3001Home Assistant \u3092\u518d\u8d77\u52d5\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "title": "LaMetric\u306b\u5fc5\u8981\u306a\u624b\u52d5\u3067\u306e\u79fb\u884c" } } diff --git a/homeassistant/components/lcn/translations/ja.json b/homeassistant/components/lcn/translations/ja.json index 30849a56dc5..17b7ab9b0f1 100644 --- a/homeassistant/components/lcn/translations/ja.json +++ b/homeassistant/components/lcn/translations/ja.json @@ -5,7 +5,7 @@ "fingerprint": "\u6307\u7d0b\u30b3\u30fc\u30c9\u3092\u53d7\u4fe1\u3057\u307e\u3057\u305f(fingerprint code received)", "send_keys": "\u9001\u4fe1\u30ad\u30fc\u3092\u53d7\u4fe1\u3057\u307e\u3057\u305f(send keys received)", "transmitter": "\u9001\u4fe1\u6a5f\u30b3\u30fc\u30c9\u53d7\u4fe1\u3057\u307e\u3057\u305f(transmitter code received)", - "transponder": "\u30c8\u30e9\u30f3\u30b9\u30dd\u30fc\u30c0\u30fc\u30b3\u30fc\u30c9\u3092\u53d7\u4fe1\u3057\u307e\u3057\u305f(transpoder code received)" + "transponder": "\u30c8\u30e9\u30f3\u30b9\u30dd\u30f3\u30c0\u30fc\u30b3\u30fc\u30c9\u3092\u53d7\u4fe1" } } } \ No newline at end of file diff --git a/homeassistant/components/min_max/translations/ja.json b/homeassistant/components/min_max/translations/ja.json index 072e7a9b487..c4d33b616b8 100644 --- a/homeassistant/components/min_max/translations/ja.json +++ b/homeassistant/components/min_max/translations/ja.json @@ -11,7 +11,7 @@ "data_description": { "round_digits": "\u7d71\u8a08\u7684\u7279\u6027\u304c\u5e73\u5747\u5024\u307e\u305f\u306f\u4e2d\u592e\u5024\u306e\u5834\u5408\u306b\u3001\u51fa\u529b\u306b\u542b\u307e\u308c\u308b\u5c0f\u6570\u70b9\u4ee5\u4e0b\u306e\u6841\u6570\u3092\u5236\u5fa1\u3057\u307e\u3059\u3002" }, - "description": "\u7d71\u8a08\u7684\u7279\u6027\u304c\u5e73\u5747\u307e\u305f\u306f\u4e2d\u592e\u5024\u306a\u5834\u5408\u306e\u7cbe\u5ea6\u3067\u3001\u5c0f\u6570\u70b9\u4ee5\u4e0b\u306e\u6841\u6570\u3092\u5236\u5fa1\u3057\u307e\u3059\u3002", + "description": "\u5165\u529b\u30bb\u30f3\u30b5\u30fc\u306e\u30ea\u30b9\u30c8\u304b\u3089\u6700\u5c0f\u5024\u3001\u6700\u5927\u5024\u3001\u5e73\u5747\u5024\u3001\u307e\u305f\u306f\u4e2d\u592e\u5024\u3092\u8a08\u7b97\u3059\u308b\u30bb\u30f3\u30b5\u30fc\u3092\u4f5c\u6210\u3057\u307e\u3059\u3002", "title": "\u6700\u5c0f/\u6700\u5927/\u5e73\u5747/\u4e2d\u592e\u5024\u30bb\u30f3\u30b5\u30fc\u3092\u8ffd\u52a0" } } diff --git a/homeassistant/components/mqtt/translations/ja.json b/homeassistant/components/mqtt/translations/ja.json index dd344b59374..274e7666a30 100644 --- a/homeassistant/components/mqtt/translations/ja.json +++ b/homeassistant/components/mqtt/translations/ja.json @@ -51,6 +51,7 @@ }, "issues": { "deprecated_yaml": { + "description": "\u624b\u52d5\u3067\u69cb\u6210\u3055\u308c\u305f MQTT {platform} (s) \u306f\u3001\u30d7\u30e9\u30c3\u30c8\u30d5\u30a9\u30fc\u30e0 \u30ad\u30fc ` {platform} ` \u306e\u4e0b\u306b\u3042\u308a\u307e\u3059\u3002 \n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001\u69cb\u6210\u3092\u300cmqtt\u300d\u7d71\u5408\u30ad\u30fc\u306b\u79fb\u52d5\u3057\u3001Home Assistant \u3092\u518d\u8d77\u52d5\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8]( {more_info_url} ) \u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "title": "\u624b\u52d5\u3067\u8a2d\u5b9a\u3057\u305f\u3001MQTT {platform} \u306b\u306f\u6ce8\u610f\u304c\u5fc5\u8981\u3067\u3059" } }, diff --git a/homeassistant/components/nest/translations/ja.json b/homeassistant/components/nest/translations/ja.json index f7c3afd128a..68085a7e30a 100644 --- a/homeassistant/components/nest/translations/ja.json +++ b/homeassistant/components/nest/translations/ja.json @@ -1,4 +1,7 @@ { + "application_credentials": { + "description": "[\u624b\u9806]( {more_info_url} ) \u306b\u5f93\u3063\u3066\u3001Cloud Console \u3092\u69cb\u6210\u3057\u307e\u3059\u3002 \n\n 1. [OAuth\u540c\u610f\u753b\u9762]( {oauth_consent_url} )\u306b\u79fb\u52d5\u3057\u3066\u8a2d\u5b9a\n1. [Credentials]( {oauth_creds_url} ) \u306b\u79fb\u52d5\u3057\u3001**Create Credentials** \u3092\u30af\u30ea\u30c3\u30af\u3057\u307e\u3059\u3002\n 1. \u30c9\u30ed\u30c3\u30d7\u30c0\u30a6\u30f3 \u30ea\u30b9\u30c8\u304b\u3089 **OAuth \u30af\u30e9\u30a4\u30a2\u30f3\u30c8 ID** \u3092\u9078\u629e\u3057\u307e\u3059\u3002\n 1. [\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u306e\u7a2e\u985e] \u3067 [**Web \u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3**] \u3092\u9078\u629e\u3057\u307e\u3059\u3002\n 1. *Authorized redirect URI* \u306e\u4e0b\u306b ` {redirect_url} ` \u3092\u8ffd\u52a0\u3057\u307e\u3059\u3002" + }, "config": { "abort": { "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", @@ -42,15 +45,18 @@ "title": "\u30cd\u30b9\u30c8: \u30af\u30e9\u30a6\u30c9\u30d7\u30ed\u30b8\u30a7\u30af\u30c8ID\u3092\u5165\u529b" }, "create_cloud_project": { + "description": "Nest \u7d71\u5408\u306b\u3088\u308a\u3001Smart Device Management API \u3092\u4f7f\u7528\u3057\u3066\u3001Nest \u30b5\u30fc\u30e2\u30b9\u30bf\u30c3\u30c8\u3001\u30ab\u30e1\u30e9\u3001\u304a\u3088\u3073\u30c9\u30a2\u30d9\u30eb\u3092\u7d71\u5408\u3067\u304d\u307e\u3059\u3002 SDM API \u306b\u306f **5 \u7c73\u30c9\u30eb**\u306e 1 \u56de\u9650\u308a\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u6599\u91d1\u304c\u5fc5\u8981\u3067\u3059\u3002 [\u8a73\u7d30]( {more_info_url} ) \u306e\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002 \n\n 1. [Google Cloud Console]( {cloud_console_url} ) \u306b\u79fb\u52d5\u3057\u307e\u3059\u3002\n 1. \u521d\u3081\u3066\u306e\u30d7\u30ed\u30b8\u30a7\u30af\u30c8\u306e\u5834\u5408\u306f\u3001**Create Project** \u3092\u30af\u30ea\u30c3\u30af\u3057\u3066\u304b\u3089 **New Project** \u3092\u30af\u30ea\u30c3\u30af\u3057\u307e\u3059\u3002\n 1. \u30af\u30e9\u30a6\u30c9 \u30d7\u30ed\u30b8\u30a7\u30af\u30c8\u306b\u540d\u524d\u3092\u4ed8\u3051\u3066\u3001[**\u4f5c\u6210**] \u3092\u30af\u30ea\u30c3\u30af\u3057\u307e\u3059\u3002\n 1. \u5f8c\u3067\u5fc5\u8981\u306b\u306a\u308b\u305f\u3081\u3001\u30af\u30e9\u30a6\u30c9 \u30d7\u30ed\u30b8\u30a7\u30af\u30c8 ID \u3092\u4fdd\u5b58\u3057\u307e\u3059 (\u4f8b: *example-project-12345*)\u3002\n 1. [Smart Device Management API]( {sdm_api_url} ) \u306e API \u30e9\u30a4\u30d6\u30e9\u30ea\u306b\u79fb\u52d5\u3057\u3001[**\u6709\u52b9\u306b\u3059\u308b**] \u3092\u30af\u30ea\u30c3\u30af\u3057\u307e\u3059\u3002\n 1. [Cloud Pub/Sub API]( {pubsub_api_url} ) \u306e API \u30e9\u30a4\u30d6\u30e9\u30ea\u306b\u79fb\u52d5\u3057\u3001[**\u6709\u52b9\u306b\u3059\u308b**] \u3092\u30af\u30ea\u30c3\u30af\u3057\u307e\u3059\u3002 \n\n\u30af\u30e9\u30a6\u30c9 \u30d7\u30ed\u30b8\u30a7\u30af\u30c8\u304c\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3055\u308c\u305f\u3089\u3001\u6b21\u306b\u9032\u307f\u307e\u3059\u3002", "title": "\u30cd\u30b9\u30c8: \u30af\u30e9\u30a6\u30c9\u30d7\u30ed\u30b8\u30a7\u30af\u30c8\u306e\u4f5c\u6210\u3068\u8a2d\u5b9a" }, "device_project": { "data": { "project_id": "\u30c7\u30d0\u30a4\u30b9\u30a2\u30af\u30bb\u30b9\u30d7\u30ed\u30b8\u30a7\u30af\u30c8ID" }, + "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u306b **5 \u7c73\u30c9\u30eb\u306e\u624b\u6570\u6599\u304c\u5fc5\u8981**\u306a Nest Device Access \u30d7\u30ed\u30b8\u30a7\u30af\u30c8\u3092\u4f5c\u6210\u3057\u307e\u3059\u3002\n 1. [\u30c7\u30d0\u30a4\u30b9 \u30a2\u30af\u30bb\u30b9 \u30b3\u30f3\u30bd\u30fc\u30eb]( {device_access_console_url} ) \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3001\u652f\u6255\u3044\u30d5\u30ed\u30fc\u3092\u5b9f\u884c\u3057\u307e\u3059\u3002\n 1. **\u30d7\u30ed\u30b8\u30a7\u30af\u30c8\u306e\u4f5c\u6210**\u3092\u30af\u30ea\u30c3\u30af\u3057\u307e\u3059\n1. \u30c7\u30d0\u30a4\u30b9 \u30a2\u30af\u30bb\u30b9 \u30d7\u30ed\u30b8\u30a7\u30af\u30c8\u306b\u540d\u524d\u3092\u4ed8\u3051\u3066\u3001**\u6b21\u3078**\u3092\u30af\u30ea\u30c3\u30af\u3057\u307e\u3059\u3002\n 1. OAuth \u30af\u30e9\u30a4\u30a2\u30f3\u30c8 ID \u3092\u5165\u529b\u3057\u307e\u3059\n1. **\u6709\u52b9\u5316**\u304a\u3088\u3073**\u30d7\u30ed\u30b8\u30a7\u30af\u30c8\u306e\u4f5c\u6210**\u3092\u30af\u30ea\u30c3\u30af\u3057\u3066\u3001\u30a4\u30d9\u30f3\u30c8\u3092\u6709\u52b9\u306b\u3057\u307e\u3059\u3002 \n\n\u4ee5\u4e0b\u306b\u30c7\u30d0\u30a4\u30b9 \u30a2\u30af\u30bb\u30b9 \u30d7\u30ed\u30b8\u30a7\u30af\u30c8 ID \u3092\u5165\u529b\u3057\u307e\u3059 ([\u8a73\u7d30]( {more_info_url} ))\u3002\n", "title": "\u30cd\u30b9\u30c8: \u30c7\u30d0\u30a4\u30b9\u30a2\u30af\u30bb\u30b9\u30d7\u30ed\u30b8\u30a7\u30af\u30c8\u306e\u4f5c\u6210" }, "device_project_upgrade": { + "description": "Nest Device Access Project \u3092\u65b0\u3057\u3044 OAuth \u30af\u30e9\u30a4\u30a2\u30f3\u30c8 ID \u3067\u66f4\u65b0\u3057\u307e\u3059 ([\u8a73\u7d30]( {more_info_url} ))\n 1. [\u30c7\u30d0\u30a4\u30b9 \u30a2\u30af\u30bb\u30b9 \u30b3\u30f3\u30bd\u30fc\u30eb]( {device_access_console_url} ) \u306b\u79fb\u52d5\u3057\u307e\u3059\u3002\n 1. *OAuth \u30af\u30e9\u30a4\u30a2\u30f3\u30c8 ID* \u306e\u6a2a\u306b\u3042\u308b\u3054\u307f\u7bb1\u30a2\u30a4\u30b3\u30f3\u3092\u30af\u30ea\u30c3\u30af\u3057\u307e\u3059\u3002\n 1. [...] \u30aa\u30fc\u30d0\u30fc\u30d5\u30ed\u30fc \u30e1\u30cb\u30e5\u30fc\u3092\u30af\u30ea\u30c3\u30af\u3057\u3001[\u30af\u30e9\u30a4\u30a2\u30f3\u30c8 ID \u3092\u8ffd\u52a0] \u3092\u30af\u30ea\u30c3\u30af\u3057\u307e\u3059\u3002\n 1. \u65b0\u3057\u3044 OAuth \u30af\u30e9\u30a4\u30a2\u30f3\u30c8 ID \u3092\u5165\u529b\u3057\u3001[**\u8ffd\u52a0**] \u3092\u30af\u30ea\u30c3\u30af\u3057\u307e\u3059\u3002 \n\n\u3042\u306a\u305f\u306e OAuth \u30af\u30e9\u30a4\u30a2\u30f3\u30c8 ID \u306f\u6b21\u306e\u3068\u304a\u308a\u3067\u3059: ` {client_id} `", "title": "\u30cd\u30b9\u30c8: \u30c7\u30d0\u30a4\u30b9\u30a2\u30af\u30bb\u30b9\u30d7\u30ed\u30b8\u30a7\u30af\u30c8\u306e\u66f4\u65b0" }, "init": { @@ -97,6 +103,7 @@ "title": "Nest YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" }, "removed_app_auth": { + "description": "\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u3092\u5411\u4e0a\u3055\u305b\u3001\u30d5\u30a3\u30c3\u30b7\u30f3\u30b0\u306e\u30ea\u30b9\u30af\u3092\u8efd\u6e1b\u3059\u308b\u305f\u3081\u306b\u3001Google \u306f Home Assistant \u3067\u4f7f\u7528\u3055\u308c\u308b\u8a8d\u8a3c\u65b9\u6cd5\u3092\u5ec3\u6b62\u3057\u307e\u3057\u305f\u3002 \n\n **\u3053\u308c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001\u304a\u5ba2\u69d8\u306b\u3088\u308b\u30a2\u30af\u30b7\u30e7\u30f3\u304c\u5fc5\u8981\u3067\u3059** ([\u8a73\u7d30]( {more_info_url} )) \n\n 1.\u7d71\u5408\u30da\u30fc\u30b8\u306b\u30a2\u30af\u30bb\u30b9\u3057\u307e\u3059\n1. Nest \u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067 [\u518d\u69cb\u6210] \u3092\u30af\u30ea\u30c3\u30af\u3057\u307e\u3059\u3002\n 1. Home Assistant \u304c Web \u8a8d\u8a3c\u3078\u306e\u30a2\u30c3\u30d7\u30b0\u30ec\u30fc\u30c9\u624b\u9806\u3092\u6848\u5185\u3057\u307e\u3059\u3002 \n\n\u30c8\u30e9\u30d6\u30eb\u30b7\u30e5\u30fc\u30c6\u30a3\u30f3\u30b0\u60c5\u5831\u306b\u3064\u3044\u3066\u306f\u3001Nest \u306e [\u7d71\u5408\u624b\u9806]( {documentation_url} ) \u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "title": "Nest\u8a8d\u8a3c\u8cc7\u683c\u60c5\u5831\u3092\u66f4\u65b0\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059" } } diff --git a/homeassistant/components/netatmo/translations/ja.json b/homeassistant/components/netatmo/translations/ja.json index c60eb3dcd5e..671cc4da751 100644 --- a/homeassistant/components/netatmo/translations/ja.json +++ b/homeassistant/components/netatmo/translations/ja.json @@ -22,7 +22,7 @@ }, "device_automation": { "trigger_subtype": { - "away": "\u96e2\u5e2d(away)", + "away": "\u3042\u3061\u3089\u3078", "hg": "\u30d5\u30ed\u30b9\u30c8(frost)\u30ac\u30fc\u30c9", "schedule": "\u30b9\u30b1\u30b8\u30e5\u30fc\u30eb" }, diff --git a/homeassistant/components/netgear/translations/ja.json b/homeassistant/components/netgear/translations/ja.json index 4402afcb92d..8cfa0de9e7e 100644 --- a/homeassistant/components/netgear/translations/ja.json +++ b/homeassistant/components/netgear/translations/ja.json @@ -13,7 +13,7 @@ "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "\u30e6\u30fc\u30b6\u30fc\u540d(\u30aa\u30d7\u30b7\u30e7\u30f3)" }, - "description": "\u30c7\u30d5\u30a9\u30eb\u30c8\u306e\u30db\u30b9\u30c8: {host}\n\u30c7\u30d5\u30a9\u30eb\u30c8\u306e\u30dd\u30fc\u30c8: {port}\n\u30c7\u30d5\u30a9\u30eb\u30c8\u306e\u30e6\u30fc\u30b6\u30fc\u540d: {username}" + "description": "\u30c7\u30d5\u30a9\u30eb\u30c8\u306e\u30db\u30b9\u30c8: {host}\n\u30c7\u30d5\u30a9\u30eb\u30c8\u306e\u30e6\u30fc\u30b6\u30fc\u540d: {username}" } } }, diff --git a/homeassistant/components/nfandroidtv/translations/ja.json b/homeassistant/components/nfandroidtv/translations/ja.json index 3db6768efcb..e096c6219f4 100644 --- a/homeassistant/components/nfandroidtv/translations/ja.json +++ b/homeassistant/components/nfandroidtv/translations/ja.json @@ -13,7 +13,7 @@ "host": "\u30db\u30b9\u30c8", "name": "\u540d\u524d" }, - "description": "\u3053\u306e\u7d71\u5408\u306b\u306f\u3001AndroidTV\u30a2\u30d7\u30ea\u306e\u901a\u77e5\u304c\u5fc5\u8981\u3067\u3059\u3002 \n\nAndroid TV\u306e\u5834\u5408: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\nFire TV\u306e\u5834\u5408: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK\n\n\u30eb\u30fc\u30bf\u30fc\u306eDHCP\u4e88\u7d04((DHCP reservation)\u30eb\u30fc\u30bf\u30fc\u306e\u30e6\u30fc\u30b6\u30fc\u30de\u30cb\u30e5\u30a2\u30eb\u3092\u53c2\u7167))\u307e\u305f\u306f\u3001\u30c7\u30d0\u30a4\u30b9\u306b\u9759\u7684IP\u30a2\u30c9\u30ec\u30b9\u3092\u8a2d\u5b9a\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\u305d\u3046\u3067\u306a\u3044\u5834\u5408\u3001\u30c7\u30d0\u30a4\u30b9\u306f\u6700\u7d42\u7684\u306b\u4f7f\u7528\u3067\u304d\u306a\u304f\u306a\u308a\u307e\u3059\u3002" + "description": "\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u3092\u53c2\u7167\u3057\u3066\u3001\u3059\u3079\u3066\u306e\u8981\u4ef6\u304c\u6e80\u305f\u3055\u308c\u3066\u3044\u308b\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002" } } } diff --git a/homeassistant/components/nmap_tracker/translations/ja.json b/homeassistant/components/nmap_tracker/translations/ja.json index 60de8477859..75788b4b908 100644 --- a/homeassistant/components/nmap_tracker/translations/ja.json +++ b/homeassistant/components/nmap_tracker/translations/ja.json @@ -9,9 +9,9 @@ "step": { "user": { "data": { - "exclude": "\u30b9\u30ad\u30e3\u30f3\u5bfe\u8c61\u304b\u3089\u9664\u5916\u3059\u308b\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30a2\u30c9\u30ec\u30b9(\u30b3\u30f3\u30de\u533a\u5207\u308a)", + "exclude": "\u30b9\u30ad\u30e3\u30f3\u304b\u3089\u9664\u5916\u3059\u308b\u30cd\u30c3\u30c8\u30ef\u30fc\u30af \u30a2\u30c9\u30ec\u30b9 (\u30ab\u30f3\u30de\u533a\u5207\u308a)", "home_interval": "\u30a2\u30af\u30c6\u30a3\u30d6\u306a\u30c7\u30d0\u30a4\u30b9\u306e\u30b9\u30ad\u30e3\u30f3\u9593\u9694(\u5206)\u306e\u6700\u5c0f\u6642\u9593(\u30d0\u30c3\u30c6\u30ea\u30fc\u3092\u7bc0\u7d04)", - "hosts": "\u30b9\u30ad\u30e3\u30f3\u3059\u308b\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30a2\u30c9\u30ec\u30b9(\u30b3\u30f3\u30de\u533a\u5207\u308a)", + "hosts": "\u30b9\u30ad\u30e3\u30f3\u3059\u308b\u30cd\u30c3\u30c8\u30ef\u30fc\u30af \u30a2\u30c9\u30ec\u30b9 (\u30ab\u30f3\u30de\u533a\u5207\u308a)", "scan_options": "Nmap\u306b\u672a\u52a0\u5de5\u3067\u305d\u306e\u307e\u307e\u6e21\u3055\u308c\u308b\u30b9\u30ad\u30e3\u30f3\u8a2d\u5b9a\u306e\u30aa\u30d7\u30b7\u30e7\u30f3" }, "description": "Nmap\u3067\u30b9\u30ad\u30e3\u30f3\u3055\u308c\u308b\u30db\u30b9\u30c8\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u3002\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30a2\u30c9\u30ec\u30b9\u304a\u3088\u3073\u9664\u5916\u5bfe\u8c61\u306f\u3001IP\u30a2\u30c9\u30ec\u30b9(192.168.1.1)\u3001IP\u30cd\u30c3\u30c8\u30ef\u30fc\u30af(192.168.0.0/24)\u3001\u307e\u305f\u306f\u3001IP\u7bc4\u56f2(192.168.1.0-32)\u3067\u3059\u3002" @@ -26,9 +26,9 @@ "init": { "data": { "consider_home": "\u898b\u3048\u306a\u304f\u306a\u3063\u305f\u5f8c\u3001\u30c7\u30d0\u30a4\u30b9\u30c8\u30e9\u30c3\u30ab\u30fc\u3092\u30db\u30fc\u30e0\u3067\u306a\u3044\u3082\u306e\u3068\u3057\u3066\u898b\u306a\u3057\u3066\u3001\u30de\u30fc\u30af\u3059\u308b\u307e\u3067\u5f85\u6a5f\u3059\u308b\u307e\u3067\u306e\u79d2\u6570\u3002", - "exclude": "\u30b9\u30ad\u30e3\u30f3\u5bfe\u8c61\u304b\u3089\u9664\u5916\u3059\u308b\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30a2\u30c9\u30ec\u30b9(\u30b3\u30f3\u30de\u533a\u5207\u308a)", + "exclude": "\u30b9\u30ad\u30e3\u30f3\u304b\u3089\u9664\u5916\u3059\u308b\u30cd\u30c3\u30c8\u30ef\u30fc\u30af \u30a2\u30c9\u30ec\u30b9 (\u30ab\u30f3\u30de\u533a\u5207\u308a)", "home_interval": "\u30a2\u30af\u30c6\u30a3\u30d6\u306a\u30c7\u30d0\u30a4\u30b9\u306e\u30b9\u30ad\u30e3\u30f3\u9593\u9694(\u5206)\u306e\u6700\u5c0f\u6642\u9593(\u30d0\u30c3\u30c6\u30ea\u30fc\u3092\u7bc0\u7d04)", - "hosts": "\u30b9\u30ad\u30e3\u30f3\u3059\u308b\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30a2\u30c9\u30ec\u30b9(\u30b3\u30f3\u30de\u533a\u5207\u308a)", + "hosts": "\u30b9\u30ad\u30e3\u30f3\u3059\u308b\u30cd\u30c3\u30c8\u30ef\u30fc\u30af \u30a2\u30c9\u30ec\u30b9 (\u30ab\u30f3\u30de\u533a\u5207\u308a)", "interval_seconds": "\u30b9\u30ad\u30e3\u30f3\u9593\u9694", "scan_options": "Nmap\u306b\u672a\u52a0\u5de5\u3067\u305d\u306e\u307e\u307e\u6e21\u3055\u308c\u308b\u30b9\u30ad\u30e3\u30f3\u8a2d\u5b9a\u306e\u30aa\u30d7\u30b7\u30e7\u30f3" }, diff --git a/homeassistant/components/nobo_hub/translations/ca.json b/homeassistant/components/nobo_hub/translations/ca.json index 25fd161b93a..58f53e2e5af 100644 --- a/homeassistant/components/nobo_hub/translations/ca.json +++ b/homeassistant/components/nobo_hub/translations/ca.json @@ -4,6 +4,7 @@ "already_configured": "El dispositiu ja est\u00e0 configurat" }, "error": { + "cannot_connect": "No s'ha pogut connectar - comprova el n\u00famero de s\u00e8rie", "invalid_ip": "Adre\u00e7a IP inv\u00e0lida", "invalid_serial": "N\u00famero de s\u00e8rie inv\u00e0lid", "unknown": "Error inesperat" @@ -13,7 +14,14 @@ "data": { "ip_address": "Adre\u00e7a IP", "serial": "N\u00famero de s\u00e8rie (12 d\u00edgits)" - } + }, + "description": "Configura un Nob\u00f8 Ecohub que no ha estat descobert a la teva xarxa local. Si el teu concentrador es troba en una altra xarxa, pots connectar-lo introduint el n\u00famero de s\u00e8rie complet (12 d\u00edgits) i la seva adre\u00e7a IP." + }, + "selected": { + "data": { + "serial_suffix": "Sufix del n\u00famero de s\u00e8rie (3 d\u00edgits)" + }, + "description": "Configurant {hub}.\n\nPer connectar-te al hub, has d'introduir els darrers 3 d\u00edgits del n\u00famero de s\u00e8rie del hub." }, "user": { "data": { @@ -28,7 +36,8 @@ "init": { "data": { "override_type": "Tipus de substituci\u00f3" - } + }, + "description": "Selecciona substitueix el tipus \"Ara\" (\"Now\") per finalitzar la substituci\u00f3 del canvi de perfil de la setmana vinent." } } } diff --git a/homeassistant/components/nobo_hub/translations/id.json b/homeassistant/components/nobo_hub/translations/id.json index 28cfbbe8a55..19a2f01ebc5 100644 --- a/homeassistant/components/nobo_hub/translations/id.json +++ b/homeassistant/components/nobo_hub/translations/id.json @@ -4,6 +4,7 @@ "already_configured": "Perangkat sudah dikonfigurasi" }, "error": { + "cannot_connect": "Gagal terhubung - periksa nomor seri", "invalid_ip": "Alamat IP tidak valid", "invalid_serial": "Nomor seri tidak valid", "unknown": "Kesalahan yang tidak diharapkan" @@ -13,7 +14,8 @@ "data": { "ip_address": "Alamat IP", "serial": "Nomor seri (12 digit)" - } + }, + "description": "Konfigurasikan Nob\u00f8 Ecohub yang tidak ditemukan di jaringan lokal Anda. Jika hub Anda berada di jaringan lain, Anda masih dapat menyambungkannya dengan memasukkan nomor seri lengkap (12 digit) dan alamat IP-nya." }, "selected": { "data": { diff --git a/homeassistant/components/nobo_hub/translations/ja.json b/homeassistant/components/nobo_hub/translations/ja.json index cea3f2fb8df..3b99fe98872 100644 --- a/homeassistant/components/nobo_hub/translations/ja.json +++ b/homeassistant/components/nobo_hub/translations/ja.json @@ -14,7 +14,8 @@ "data": { "ip_address": "IP\u30a2\u30c9\u30ec\u30b9", "serial": "\u30b7\u30ea\u30a2\u30eb\u30ca\u30f3\u30d0\u30fc(12\u6841)" - } + }, + "description": "\u30ed\u30fc\u30ab\u30eb \u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u3067\u691c\u51fa\u3055\u308c\u306a\u3044 Nob\u00f8 Ecohub \u3092\u69cb\u6210\u3057\u307e\u3059\u3002\u30cf\u30d6\u304c\u5225\u306e\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u3042\u308b\u5834\u5408\u3067\u3082\u3001\u5b8c\u5168\u306a\u30b7\u30ea\u30a2\u30eb\u756a\u53f7 (12 \u6841) \u3068\u305d\u306e IP \u30a2\u30c9\u30ec\u30b9\u3092\u5165\u529b\u3059\u308b\u3053\u3068\u3067\u63a5\u7d9a\u3067\u304d\u307e\u3059\u3002" }, "selected": { "data": { diff --git a/homeassistant/components/overkiz/translations/ca.json b/homeassistant/components/overkiz/translations/ca.json index d3a5a38edc3..ca55a8468b3 100644 --- a/homeassistant/components/overkiz/translations/ca.json +++ b/homeassistant/components/overkiz/translations/ca.json @@ -11,7 +11,8 @@ "server_in_maintenance": "El servidor est\u00e0 inoperatiu per manteniment", "too_many_attempts": "Massa intents amb un 'token' inv\u00e0lid, bloquejat temporalment", "too_many_requests": "Massa sol\u00b7licituds, torna-ho a provar m\u00e9s tard", - "unknown": "Error inesperat" + "unknown": "Error inesperat", + "unknown_user": "Usuari desconegut. Els comptes de Somfy Protect no s\u00f3n compatibles amb aquesta integraci\u00f3." }, "flow_title": "Passarel\u00b7la: {gateway_id}", "step": { diff --git a/homeassistant/components/overkiz/translations/ja.json b/homeassistant/components/overkiz/translations/ja.json index df49ea40332..d2f72355dfd 100644 --- a/homeassistant/components/overkiz/translations/ja.json +++ b/homeassistant/components/overkiz/translations/ja.json @@ -10,7 +10,7 @@ "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "server_in_maintenance": "\u30e1\u30f3\u30c6\u30ca\u30f3\u30b9\u306e\u305f\u3081\u30b5\u30fc\u30d0\u30fc\u304c\u30c0\u30a6\u30f3\u3057\u3066\u3044\u307e\u3059", "too_many_attempts": "\u7121\u52b9\u306a\u30c8\u30fc\u30af\u30f3\u306b\u3088\u308b\u8a66\u884c\u56de\u6570\u304c\u591a\u3059\u304e\u305f\u305f\u3081\u3001\u4e00\u6642\u7684\u306b\u7981\u6b62\u3055\u308c\u307e\u3057\u305f\u3002", - "too_many_requests": "\u30ea\u30af\u30a8\u30b9\u30c8\u304c\u591a\u3059\u304e\u307e\u3059\u3002\u3057\u3070\u3089\u304f\u3057\u3066\u304b\u3089\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044\u3002", + "too_many_requests": "\u30ea\u30af\u30a8\u30b9\u30c8\u304c\u591a\u3059\u304e\u307e\u3059\u3002\u3057\u3070\u3089\u304f\u3057\u3066\u304b\u3089\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc", "unknown_user": "\u4e0d\u660e\u306a\u30e6\u30fc\u30b6\u30fc\u3067\u3059\u3002Somfy Protect\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3001\u3053\u306e\u7d71\u5408\u3067\u306f\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002" }, diff --git a/homeassistant/components/powerwall/translations/ja.json b/homeassistant/components/powerwall/translations/ja.json index be7078143b0..51508f6e3ed 100644 --- a/homeassistant/components/powerwall/translations/ja.json +++ b/homeassistant/components/powerwall/translations/ja.json @@ -21,7 +21,7 @@ "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, - "description": "\u30d1\u30b9\u30ef\u30fc\u30c9\u306f\u901a\u5e38\u3001Backup Gateway\u306e\u30b7\u30ea\u30a2\u30eb\u756a\u53f7\u306e\u6700\u5f8c\u306e5\u6587\u5b57\u3067\u3042\u308a\u3001Tesla\u30a2\u30d7\u30ea\u3067\u898b\u3064\u3051\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u3059\u3002\u307e\u305f\u306f\u3001Backup Gateway2\u306e\u30c9\u30a2\u306e\u5185\u5074\u306b\u3042\u308b\u30d1\u30b9\u30ef\u30fc\u30c9\u306e\u6700\u5f8c\u306e5\u6587\u5b57\u3067\u3059\u3002", + "description": "\u30d1\u30b9\u30ef\u30fc\u30c9\u306f\u901a\u5e38\u3001\u30d0\u30c3\u30af\u30a2\u30c3\u30d7 \u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u306e\u30b7\u30ea\u30a2\u30eb\u756a\u53f7\u306e\u6700\u5f8c\u306e 5 \u6587\u5b57\u3067\u3042\u308a\u3001Tesla \u30a2\u30d7\u30ea\u3067\u78ba\u8a8d\u3059\u308b\u304b\u3001\u30d0\u30c3\u30af\u30a2\u30c3\u30d7 \u30b2\u30fc\u30c8\u30a6\u30a7\u30a4 2 \u306e\u30c9\u30a2\u306e\u5185\u5074\u306b\u3042\u308b\u30d1\u30b9\u30ef\u30fc\u30c9\u306e\u6700\u5f8c\u306e 5 \u6587\u5b57\u3092\u78ba\u8a8d\u3067\u304d\u307e\u3059\u3002", "title": "powerwall\u306e\u518d\u8a8d\u8a3c" }, "user": { @@ -29,7 +29,7 @@ "ip_address": "IP\u30a2\u30c9\u30ec\u30b9", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, - "description": "\u30d1\u30b9\u30ef\u30fc\u30c9\u306f\u901a\u5e38\u3001Backup Gateway\u306e\u30b7\u30ea\u30a2\u30eb\u756a\u53f7\u306e\u6700\u5f8c\u306e5\u6587\u5b57\u3067\u3042\u308a\u3001Tesla\u30a2\u30d7\u30ea\u3067\u898b\u3064\u3051\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u3059\u3002\u307e\u305f\u306f\u3001Backup Gateway2\u306e\u30c9\u30a2\u306e\u5185\u5074\u306b\u3042\u308b\u30d1\u30b9\u30ef\u30fc\u30c9\u306e\u6700\u5f8c\u306e5\u6587\u5b57\u3067\u3059\u3002", + "description": "\u30d1\u30b9\u30ef\u30fc\u30c9\u306f\u901a\u5e38\u3001\u30d0\u30c3\u30af\u30a2\u30c3\u30d7 \u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u306e\u30b7\u30ea\u30a2\u30eb\u756a\u53f7\u306e\u6700\u5f8c\u306e 5 \u6587\u5b57\u3067\u3042\u308a\u3001Tesla \u30a2\u30d7\u30ea\u3067\u78ba\u8a8d\u3059\u308b\u304b\u3001\u30d0\u30c3\u30af\u30a2\u30c3\u30d7 \u30b2\u30fc\u30c8\u30a6\u30a7\u30a4 2 \u306e\u30c9\u30a2\u306e\u5185\u5074\u306b\u3042\u308b\u30d1\u30b9\u30ef\u30fc\u30c9\u306e\u6700\u5f8c\u306e 5 \u6587\u5b57\u3092\u78ba\u8a8d\u3067\u304d\u307e\u3059\u3002", "title": "Powerwall\u306b\u63a5\u7d9a" } } diff --git a/homeassistant/components/samsungtv/translations/ja.json b/homeassistant/components/samsungtv/translations/ja.json index 6b0cae1ba4d..547bad1c594 100644 --- a/homeassistant/components/samsungtv/translations/ja.json +++ b/homeassistant/components/samsungtv/translations/ja.json @@ -26,7 +26,7 @@ "description": "{device} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f\u3053\u308c\u307e\u3067\u306bHome Assistant\u3092\u4e00\u5ea6\u3082\u63a5\u7d9a\u3057\u305f\u3053\u3068\u304c\u306a\u3044\u5834\u5408\u306f\u3001\u30c6\u30ec\u30d3\u306b\u8a8d\u8a3c\u3092\u6c42\u3081\u308b\u30dd\u30c3\u30d7\u30a2\u30c3\u30d7\u304c\u8868\u793a\u3055\u308c\u307e\u3059\u3002" }, "reauth_confirm": { - "description": "\u9001\u4fe1(submit)\u3001\u8a8d\u8a3c\u3092\u8981\u6c42\u3059\u308b {device} \u306e\u30dd\u30c3\u30d7\u30a2\u30c3\u30d7\u3092\u300130\u79d2\u4ee5\u5185\u306b\u53d7\u3051\u5165\u308c\u307e\u3059\u3002" + "description": "\u9001\u4fe1\u5f8c\u300130 \u79d2\u4ee5\u5185\u306b\u627f\u8a8d\u3092\u8981\u6c42\u3059\u308b{device}\u306e\u30dd\u30c3\u30d7\u30a2\u30c3\u30d7\u3092\u53d7\u3051\u5165\u308c\u308b\u304b\u3001PIN \u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "reauth_confirm_encrypted": { "description": "{device} \u306b\u8868\u793a\u3055\u308c\u3066\u3044\u308bPIN\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" diff --git a/homeassistant/components/sensibo/translations/id.json b/homeassistant/components/sensibo/translations/id.json index dfaf05cca33..fc33b6e7698 100644 --- a/homeassistant/components/sensibo/translations/id.json +++ b/homeassistant/components/sensibo/translations/id.json @@ -15,11 +15,17 @@ "reauth_confirm": { "data": { "api_key": "Kunci API" + }, + "data_description": { + "api_key": "Ikuti petunjuk dalam dokumentasi untuk mendapatkan kunci API baru." } }, "user": { "data": { "api_key": "Kunci API" + }, + "data_description": { + "api_key": "Ikuti petunjuk dalam dokumentasi untuk mendapatkan kunci API." } } } diff --git a/homeassistant/components/sensibo/translations/ja.json b/homeassistant/components/sensibo/translations/ja.json index 4c74b6412ac..d961099628a 100644 --- a/homeassistant/components/sensibo/translations/ja.json +++ b/homeassistant/components/sensibo/translations/ja.json @@ -15,11 +15,17 @@ "reauth_confirm": { "data": { "api_key": "API\u30ad\u30fc" + }, + "data_description": { + "api_key": "\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u3001\u65b0\u3057\u3044 API \u30ad\u30fc\u3092\u53d6\u5f97\u3057\u307e\u3059\u3002" } }, "user": { "data": { "api_key": "API\u30ad\u30fc" + }, + "data_description": { + "api_key": "\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066 API \u30ad\u30fc\u3092\u53d6\u5f97\u3057\u307e\u3059\u3002" } } } diff --git a/homeassistant/components/sensibo/translations/ru.json b/homeassistant/components/sensibo/translations/ru.json index aafef088706..a374be60719 100644 --- a/homeassistant/components/sensibo/translations/ru.json +++ b/homeassistant/components/sensibo/translations/ru.json @@ -15,11 +15,17 @@ "reauth_confirm": { "data": { "api_key": "\u041a\u043b\u044e\u0447 API" + }, + "data_description": { + "api_key": "\u0421\u043b\u0435\u0434\u0443\u0439\u0442\u0435 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0438, \u0447\u0442\u043e\u0431\u044b \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u043d\u043e\u0432\u044b\u0439 \u043a\u043b\u044e\u0447 API." } }, "user": { "data": { "api_key": "\u041a\u043b\u044e\u0447 API" + }, + "data_description": { + "api_key": "\u0421\u043b\u0435\u0434\u0443\u0439\u0442\u0435 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0438, \u0447\u0442\u043e\u0431\u044b \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u043a\u043b\u044e\u0447 API." } } } diff --git a/homeassistant/components/sensor/translations/ca.json b/homeassistant/components/sensor/translations/ca.json index fc8db4c2791..df4bcff1c53 100644 --- a/homeassistant/components/sensor/translations/ca.json +++ b/homeassistant/components/sensor/translations/ca.json @@ -11,6 +11,7 @@ "is_gas": "Gas actual de {entity_name}", "is_humidity": "Humitat actual de {entity_name}", "is_illuminance": "Il\u00b7luminaci\u00f3 actual de {entity_name}", + "is_moisture": "Humitat actual de {entity_name}", "is_nitrogen_dioxide": "Concentraci\u00f3 actual de di\u00f2xid de nitrogen de {entity_name}", "is_nitrogen_monoxide": "Concentraci\u00f3 actual de mon\u00f2xid de nitrogen de {entity_name}", "is_nitrous_oxide": "Concentraci\u00f3 actual d'\u00f2xid nitr\u00f3s de {entity_name}", @@ -40,6 +41,7 @@ "gas": "Canvia el gas de {entity_name}", "humidity": "Canvia la humitat de {entity_name}", "illuminance": "Canvia la il\u00b7luminaci\u00f3 de {entity_name}", + "moisture": "Canvia la humitat de {entity_name}", "nitrogen_dioxide": "Canvia la concentraci\u00f3 de di\u00f2xid de nitrogen de {entity_name}", "nitrogen_monoxide": "Canvia la concentraci\u00f3 de mon\u00f2xid de nitrogen de {entity_name}", "nitrous_oxide": "Canvia la concentraci\u00f3 d'\u00f2xid nitr\u00f3s de {entity_name}", diff --git a/homeassistant/components/sensor/translations/ja.json b/homeassistant/components/sensor/translations/ja.json index 037b6d70dbe..b7153e4b5de 100644 --- a/homeassistant/components/sensor/translations/ja.json +++ b/homeassistant/components/sensor/translations/ja.json @@ -1,7 +1,7 @@ { "device_automation": { "condition_type": { - "is_apparent_power": "\u73fe\u5728\u306e {entity_name} \u898b\u304b\u3051\u306e\u96fb\u529b(apparent power)", + "is_apparent_power": "\u73fe\u5728\u306e{entity_name}\u306e\u76ae\u76f8\u96fb\u529b", "is_battery_level": "\u73fe\u5728\u306e {entity_name} \u96fb\u6c60\u6b8b\u91cf", "is_carbon_dioxide": "\u73fe\u5728\u306e {entity_name} \u4e8c\u9178\u5316\u70ad\u7d20\u6fc3\u5ea6\u30ec\u30d9\u30eb", "is_carbon_monoxide": "\u73fe\u5728\u306e {entity_name} \u4e00\u9178\u5316\u70ad\u7d20\u6fc3\u5ea6\u30ec\u30d9\u30eb", @@ -22,7 +22,7 @@ "is_power": "\u73fe\u5728\u306e {entity_name} \u96fb\u6e90", "is_power_factor": "\u73fe\u5728\u306e {entity_name} \u529b\u7387", "is_pressure": "\u73fe\u5728\u306e {entity_name} \u5727\u529b", - "is_reactive_power": "\u73fe\u5728\u306e {entity_name} \u30ea\u30a2\u30af\u30c6\u30a3\u30d6\u96fb\u6e90(reactive power)", + "is_reactive_power": "\u73fe\u5728\u306e{entity_name}\u7121\u52b9\u96fb\u529b", "is_signal_strength": "\u73fe\u5728\u306e {entity_name} \u4fe1\u53f7\u5f37\u5ea6", "is_sulphur_dioxide": "\u73fe\u5728\u306e {entity_name} \u4e8c\u9178\u5316\u786b\u9ec4\u6fc3\u5ea6\u30ec\u30d9\u30eb", "is_temperature": "\u73fe\u5728\u306e {entity_name} \u6e29\u5ea6", @@ -31,7 +31,7 @@ "is_voltage": "\u73fe\u5728\u306e {entity_name} \u96fb\u5727" }, "trigger_type": { - "apparent_power": "{entity_name} \u898b\u304b\u3051\u306e\u96fb\u529b(apparent power)\u306e\u5909\u5316", + "apparent_power": "{entity_name}\u76ae\u76f8\u96fb\u529b\u306e\u5909\u5316", "battery_level": "{entity_name} \u96fb\u6c60\u6b8b\u91cf\u306e\u5909\u5316", "carbon_dioxide": "{entity_name} \u4e8c\u9178\u5316\u70ad\u7d20\u6fc3\u5ea6\u306e\u5909\u5316", "carbon_monoxide": "{entity_name} \u4e00\u9178\u5316\u70ad\u7d20\u6fc3\u5ea6\u306e\u5909\u5316", @@ -52,7 +52,7 @@ "power": "{entity_name} \u96fb\u6e90(power)\u306e\u5909\u5316", "power_factor": "{entity_name} \u529b\u7387\u304c\u5909\u5316", "pressure": "{entity_name} \u5727\u529b\u306e\u5909\u5316", - "reactive_power": "{entity_name} \u30ea\u30a2\u30af\u30c6\u30a3\u30d6\u96fb\u6e90\u306e\u5909\u66f4(reactive power)", + "reactive_power": "{entity_name}\u7121\u52b9\u96fb\u529b\u306e\u5909\u66f4", "signal_strength": "{entity_name} \u4fe1\u53f7\u5f37\u5ea6\u306e\u5909\u5316", "sulphur_dioxide": "{entity_name} \u4e8c\u9178\u5316\u786b\u9ec4\u6fc3\u5ea6\u306e\u5909\u5316", "temperature": "{entity_name} \u6e29\u5ea6\u5909\u5316", diff --git a/homeassistant/components/simplisafe/translations/ja.json b/homeassistant/components/simplisafe/translations/ja.json index 38e383a75af..c448222f286 100644 --- a/homeassistant/components/simplisafe/translations/ja.json +++ b/homeassistant/components/simplisafe/translations/ja.json @@ -35,7 +35,7 @@ "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "E\u30e1\u30fc\u30eb" }, - "description": "2021\u5e74\u3088\u308a\u3001SimpliSafe\u306fWeb\u30a2\u30d7\u30ea\u306b\u3088\u308b\u65b0\u3057\u3044\u8a8d\u8a3c\u6a5f\u69cb\u306b\u79fb\u884c\u3057\u307e\u3057\u305f\u3002\u6280\u8853\u7684\u306a\u5236\u9650\u306e\u305f\u3081\u3001\u3053\u306e\u30d7\u30ed\u30bb\u30b9\u306e\u6700\u5f8c\u306b\u624b\u52d5\u3067\u306e\u624b\u9806\u304c\u3042\u308a\u307e\u3059\u3002\u958b\u59cb\u3059\u308b\u524d\u306b\u3001[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code)\u3092\u5fc5\u305a\u304a\u8aad\u307f\u304f\u3060\u3055\u3044\u3002\n\n\u6e96\u5099\u304c\u3067\u304d\u305f\u3089\u3001[\u3053\u3053]({url}) \u3092\u30af\u30ea\u30c3\u30af\u3057\u3066SimpliSafe\u306eWeb\u30a2\u30d7\u30ea\u3092\u958b\u304d\u3001\u8a8d\u8a3c\u60c5\u5831\u3092\u5165\u529b\u3057\u307e\u3059\u3002\u51e6\u7406\u304c\u5b8c\u4e86\u3057\u305f\u3089\u3001\u3053\u3053\u306b\u623b\u3063\u3066\u304d\u3066\u9001\u4fe1(submit) \u3092\u30af\u30ea\u30c3\u30af\u3057\u307e\u3059\u3002" + "description": "SimpliSafe \u306f\u3001Web \u30a2\u30d7\u30ea\u3092\u4ecb\u3057\u3066\u30e6\u30fc\u30b6\u30fc\u3092\u8a8d\u8a3c\u3057\u307e\u3059\u3002\u6280\u8853\u7684\u306a\u5236\u9650\u306b\u3088\u308a\u3001\u3053\u306e\u30d7\u30ed\u30bb\u30b9\u306e\u6700\u5f8c\u306b\u624b\u52d5\u306e\u624b\u9806\u304c\u3042\u308a\u307e\u3059\u3002\u958b\u59cb\u3059\u308b\u524d\u306b\u3001[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code)\u3092\u5fc5\u305a\u304a\u8aad\u307f\u304f\u3060\u3055\u3044\u3002 \n\n\u6e96\u5099\u304c\u3067\u304d\u305f\u3089\u3001[\u3053\u3053]( {url} ) \u3092\u30af\u30ea\u30c3\u30af\u3057\u3066 SimpliSafe Web \u30a2\u30d7\u30ea\u3092\u958b\u304d\u3001\u8cc7\u683c\u60c5\u5831\u3092\u5165\u529b\u3057\u307e\u3059\u3002\u30d6\u30e9\u30a6\u30b6\u3067\u65e2\u306b SimpliSafe \u306b\u30ed\u30b0\u30a4\u30f3\u3057\u3066\u3044\u308b\u5834\u5408\u306f\u3001\u65b0\u3057\u3044\u30bf\u30d6\u3092\u958b\u304d\u3001\u4e0a\u8a18\u306e URL \u3092\u30b3\u30d4\u30fc\u3057\u3066\u305d\u306e\u30bf\u30d6\u306b\u8cbc\u308a\u4ed8\u3051\u307e\u3059\u3002 \n\n\u51e6\u7406\u304c\u5b8c\u4e86\u3057\u305f\u3089\u3001\u3053\u3053\u306b\u623b\u308a\u3001\u300ccom.simplisafe.mobile\u300d\u306e URL \u304b\u3089\u8a8d\u8a3c\u30b3\u30fc\u30c9\u3092\u5165\u529b\u3057\u307e\u3059\u3002" } } }, diff --git a/homeassistant/components/speedtestdotnet/translations/ja.json b/homeassistant/components/speedtestdotnet/translations/ja.json index a2f196a5359..c09a2a6032a 100644 --- a/homeassistant/components/speedtestdotnet/translations/ja.json +++ b/homeassistant/components/speedtestdotnet/translations/ja.json @@ -14,6 +14,7 @@ "fix_flow": { "step": { "confirm": { + "description": "\u3053\u306e\u30b5\u30fc\u30d3\u30b9\u3092\u4f7f\u7528\u3059\u308b\u3059\u3079\u3066\u306e\u81ea\u52d5\u5316\u307e\u305f\u306f\u30b9\u30af\u30ea\u30d7\u30c8\u3092\u66f4\u65b0\u3057\u3066\u3001\u4ee3\u308f\u308a\u306b\u30bf\u30fc\u30b2\u30c3\u30c8 Speedtest entity_id \u3067\u300chomeassistant.update_entity\u300d\u30b5\u30fc\u30d3\u30b9\u3092\u4f7f\u7528\u3057\u307e\u3059\u3002\u6b21\u306b\u3001\u4e0b\u306e [\u9001\u4fe1] \u3092\u30af\u30ea\u30c3\u30af\u3057\u3066\u3001\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u6e08\u307f\u3068\u3057\u3066\u30de\u30fc\u30af\u3057\u307e\u3059\u3002", "title": "speedtest\u30b5\u30fc\u30d3\u30b9\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" } } diff --git a/homeassistant/components/switch_as_x/translations/ja.json b/homeassistant/components/switch_as_x/translations/ja.json index 41af80fa2b8..40903c4d36d 100644 --- a/homeassistant/components/switch_as_x/translations/ja.json +++ b/homeassistant/components/switch_as_x/translations/ja.json @@ -4,11 +4,11 @@ "user": { "data": { "entity_id": "\u30b9\u30a4\u30c3\u30c1", - "target_domain": "\u30bf\u30a4\u30d7" + "target_domain": "\u65b0\u3057\u3044\u30bf\u30a4\u30d7" }, "description": "Home Assistant\u306b\u3001\u30e9\u30a4\u30c8\u3084\u30ab\u30d0\u30fc\u306a\u3069\u306e\u8868\u793a\u3055\u305b\u305f\u3044\u30b9\u30a4\u30c3\u30c1\u3092\u9078\u3073\u307e\u3059\u3002\u5143\u306e\u30b9\u30a4\u30c3\u30c1\u306f\u975e\u8868\u793a\u306b\u306a\u308a\u307e\u3059\u3002" } } }, - "title": "X\u3068\u3057\u3066\u5207\u308a\u66ff\u3048\u308b" + "title": "\u30b9\u30a4\u30c3\u30c1\u306e\u30c7\u30d0\u30a4\u30b9 \u30bf\u30a4\u30d7\u3092\u5909\u66f4\u3059\u308b" } \ No newline at end of file diff --git a/homeassistant/components/switchbot/translations/ja.json b/homeassistant/components/switchbot/translations/ja.json index 8954b2397ab..133c9f44d86 100644 --- a/homeassistant/components/switchbot/translations/ja.json +++ b/homeassistant/components/switchbot/translations/ja.json @@ -10,7 +10,7 @@ "error": { "other": "\u7a7a" }, - "flow_title": "{name}", + "flow_title": "{name} ( {address} )", "step": { "confirm": { "description": "{name} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" diff --git a/homeassistant/components/tankerkoenig/translations/ja.json b/homeassistant/components/tankerkoenig/translations/ja.json index 8ee4aae3b88..45e3233f2fd 100644 --- a/homeassistant/components/tankerkoenig/translations/ja.json +++ b/homeassistant/components/tankerkoenig/translations/ja.json @@ -18,7 +18,7 @@ "data": { "stations": "\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3" }, - "description": "\u534a\u5f84\u5185\u306b {stations_count} \u500b\u306e\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3\u304c\u898b\u3064\u304b\u308a\u307e\u3057\u305f", + "description": "\u534a\u5f84\u5185\u306b{stations_count}\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3\u304c\u898b\u3064\u304b\u308a\u307e\u3057\u305f", "title": "\u8ffd\u52a0\u3059\u308b\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3\u3092\u9078\u629e\u3057\u307e\u3059" }, "user": { diff --git a/homeassistant/components/threshold/translations/ja.json b/homeassistant/components/threshold/translations/ja.json index 821de593499..4f6b8e6ea31 100644 --- a/homeassistant/components/threshold/translations/ja.json +++ b/homeassistant/components/threshold/translations/ja.json @@ -13,7 +13,7 @@ "upper": "\u4e0a\u9650\u5024" }, "description": "\u30bb\u30f3\u30b5\u30fc\u306e\u30aa\u30f3\u3068\u30aa\u30d5\u3092\u5207\u308a\u66ff\u3048\u308b\u30bf\u30a4\u30df\u30f3\u30b0\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u3002\n\n\u4e0b\u9650\u306e\u307f\u8a2d\u5b9a - \u5165\u529b\u30bb\u30f3\u30b5\u30fc\u306e\u5024\u304c\u4e0b\u9650\u5024\u3088\u308a\u5c0f\u3055\u3044\u3068\u304d\u306b\u30aa\u30f3\u306b\u3057\u307e\u3059\u3002\n\u4e0a\u9650\u306e\u307f\u8a2d\u5b9a - \u5165\u529b\u30bb\u30f3\u30b5\u30fc\u306e\u5024\u304c\u4e0a\u9650\u5024\u3088\u308a\u5927\u304d\u3044\u3068\u304d\u306b\u30aa\u30f3\u306b\u3057\u307e\u3059\u3002\n\u4e0b\u9650\u3068\u4e0a\u9650\u306e\u4e21\u65b9\u3092\u8a2d\u5b9a - \u5165\u529b\u30bb\u30f3\u30b5\u30fc\u306e\u5024\u304c[\u4e0b\u9650..\u4e0a\u9650]\u306e\u7bc4\u56f2\u5185\u306b\u3042\u308b\u3068\u304d\u306b\u30aa\u30f3\u306b\u3057\u307e\u3059\u3002", - "title": "\u65b0\u3057\u3044\u3057\u304d\u3044\u5024\u30bb\u30f3\u30b5\u30fc" + "title": "\u3057\u304d\u3044\u5024\u30bb\u30f3\u30b5\u30fc\u306e\u8ffd\u52a0" } } }, @@ -30,7 +30,7 @@ "name": "\u540d\u524d", "upper": "\u4e0a\u9650\u5024" }, - "description": "\u30bb\u30f3\u30b5\u30fc\u306e\u30aa\u30f3\u3068\u30aa\u30d5\u3092\u5207\u308a\u66ff\u3048\u308b\u30bf\u30a4\u30df\u30f3\u30b0\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u3002\n\n\u4e0b\u9650\u306e\u307f\u8a2d\u5b9a - \u5165\u529b\u30bb\u30f3\u30b5\u30fc\u306e\u5024\u304c\u4e0b\u9650\u5024\u3088\u308a\u5c0f\u3055\u3044\u3068\u304d\u306b\u30aa\u30f3\u306b\u3057\u307e\u3059\u3002\n\u4e0a\u9650\u306e\u307f\u8a2d\u5b9a - \u5165\u529b\u30bb\u30f3\u30b5\u30fc\u306e\u5024\u304c\u4e0a\u9650\u5024\u3088\u308a\u5927\u304d\u3044\u3068\u304d\u306b\u30aa\u30f3\u306b\u3057\u307e\u3059\u3002\n\u4e0b\u9650\u3068\u4e0a\u9650\u306e\u4e21\u65b9\u3092\u8a2d\u5b9a - \u5165\u529b\u30bb\u30f3\u30b5\u30fc\u306e\u5024\u304c[\u4e0b\u9650..\u4e0a\u9650]\u306e\u7bc4\u56f2\u5185\u306b\u3042\u308b\u3068\u304d\u306b\u30aa\u30f3\u306b\u3057\u307e\u3059\u3002" + "description": "\u8a2d\u5b9a\u3055\u308c\u305f\u4e0b\u9650\u306e\u307f - \u5165\u529b\u30bb\u30f3\u30b5\u30fc\u306e\u5024\u304c\u4e0b\u9650\u3088\u308a\u3082\u5c0f\u3055\u3044\u5834\u5408\u306b\u30aa\u30f3\u306b\u3057\u307e\u3059\u3002\n\u4e0a\u9650\u306e\u307f\u8a2d\u5b9a - \u5165\u529b\u30bb\u30f3\u30b5\u30fc\u306e\u5024\u304c\u4e0a\u9650\u3088\u308a\u3082\u5927\u304d\u3044\u5834\u5408\u306b\u30aa\u30f3\u306b\u3057\u307e\u3059\u3002\n\u4e0b\u9650\u3068\u4e0a\u9650\u306e\u4e21\u65b9\u304c\u69cb\u6210\u3055\u308c\u3066\u3044\u308b - \u5165\u529b\u30bb\u30f3\u30b5\u30fc\u306e\u5024\u304c [\u4e0b\u9650..\u4e0a\u9650] \u306e\u7bc4\u56f2\u5185\u306b\u3042\u308b\u5834\u5408\u306b\u30aa\u30f3\u306b\u3057\u307e\u3059\u3002" } } }, diff --git a/homeassistant/components/tod/translations/ja.json b/homeassistant/components/tod/translations/ja.json index 10bad2407d0..396fbed7020 100644 --- a/homeassistant/components/tod/translations/ja.json +++ b/homeassistant/components/tod/translations/ja.json @@ -7,8 +7,8 @@ "before_time": "\u30aa\u30d5\u30bf\u30a4\u30e0(Off time)", "name": "\u540d\u524d" }, - "description": "\u30bb\u30f3\u30b5\u30fc\u306e\u30aa\u30f3\u3068\u30aa\u30d5\u3092\u5207\u308a\u66ff\u3048\u308b\u30bf\u30a4\u30df\u30f3\u30b0\u3092\u69cb\u6210\u3057\u307e\u3059\u3002", - "title": "\u65b0\u3057\u3044\u6642\u523b\u30bb\u30f3\u30b5\u30fc" + "description": "\u6642\u9593\u306b\u5fdc\u3058\u3066\u30aa\u30f3\u307e\u305f\u306f\u30aa\u30d5\u306b\u306a\u308b\u30d0\u30a4\u30ca\u30ea \u30bb\u30f3\u30b5\u30fc\u3092\u4f5c\u6210\u3057\u307e\u3059\u3002", + "title": "\u6642\u523b\u30bb\u30f3\u30b5\u30fc\u306e\u8ffd\u52a0" } } }, diff --git a/homeassistant/components/tomorrowio/translations/ja.json b/homeassistant/components/tomorrowio/translations/ja.json index c935416c89b..bf2fe04afe7 100644 --- a/homeassistant/components/tomorrowio/translations/ja.json +++ b/homeassistant/components/tomorrowio/translations/ja.json @@ -21,7 +21,7 @@ "step": { "init": { "data": { - "timestep": "\u6700\u5c0f: Between NowCast Forecasts" + "timestep": "\u6700\u5c0f\u3002 NowCast \u4e88\u6e2c\u306e\u9593" }, "description": "`nowcast` forecast(\u4e88\u6e2c) \u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u3092\u6709\u52b9\u306b\u3059\u308b\u3053\u3068\u3092\u9078\u629e\u3057\u305f\u5834\u5408\u3001\u5404\u4e88\u6e2c\u9593\u306e\u5206\u6570\u3092\u8a2d\u5b9a\u3059\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u3059\u3002\u63d0\u4f9b\u3055\u308c\u308bforecast(\u4e88\u6e2c)\u306e\u6570\u306f\u3001forecast(\u4e88\u6e2c)\u306e\u9593\u306b\u9078\u629e\u3057\u305f\u5206\u6570\u306b\u4f9d\u5b58\u3057\u307e\u3059\u3002", "title": "Tomorrow.io\u30aa\u30d7\u30b7\u30e7\u30f3\u306e\u66f4\u65b0" diff --git a/homeassistant/components/toon/translations/ja.json b/homeassistant/components/toon/translations/ja.json index 746b717ae08..159b971c5cf 100644 --- a/homeassistant/components/toon/translations/ja.json +++ b/homeassistant/components/toon/translations/ja.json @@ -14,7 +14,7 @@ "agreement": "\u5408\u610f(Agreement)" }, "description": "\u8ffd\u52a0\u3057\u305f\u3044\u5951\u7d04(Agreement)\u30a2\u30c9\u30ec\u30b9\u3092\u9078\u629e\u3057\u307e\u3059\u3002", - "title": "\u5951\u7d04(Agreement)\u306e\u9078\u629e" + "title": "\u5951\u7d04\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044" }, "pick_implementation": { "title": "\u8a8d\u8a3c\u3059\u308b\u30c6\u30ca\u30f3\u30c8\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044" diff --git a/homeassistant/components/tuya/translations/select.ja.json b/homeassistant/components/tuya/translations/select.ja.json index 5d8c83175b2..cab1c980a7a 100644 --- a/homeassistant/components/tuya/translations/select.ja.json +++ b/homeassistant/components/tuya/translations/select.ja.json @@ -109,24 +109,24 @@ "small": "\u5c0f" }, "tuya__vacuum_mode": { - "bow": "\u66f2\u304c\u308b", + "bow": "\u5f13", "chargego": "\u30c9\u30c3\u30af\u306b\u623b\u308b", - "left_bow": "\u5de6\u306b\u66f2\u304c\u308b", + "left_bow": "\u5f13\u5de6", "left_spiral": "\u30b9\u30d1\u30a4\u30e9\u30eb\u30ec\u30d5\u30c8", "mop": "\u30e2\u30c3\u30d7", - "part": "\u90e8\u5206", - "partial_bow": "\u90e8\u5206\u7684\u306b\u66f2\u304c\u308b", + "part": "\u90e8", + "partial_bow": "\u90e8\u5206\u7684\u306b\u5f13", "pick_zone": "\u30d4\u30c3\u30af\u30be\u30fc\u30f3", "point": "\u30dd\u30a4\u30f3\u30c8", "pose": "\u30dd\u30fc\u30ba", "random": "\u30e9\u30f3\u30c0\u30e0", - "right_bow": "\u53f3\u306b\u66f2\u304c\u308b", + "right_bow": "\u5f13\u53f3", "right_spiral": "\u30b9\u30d1\u30a4\u30e9\u30eb\u30e9\u30a4\u30c8", "single": "\u30b7\u30f3\u30b0\u30eb", "smart": "\u30b9\u30de\u30fc\u30c8", "spiral": "\u30b9\u30d1\u30a4\u30e9\u30eb(Spiral)", "standby": "\u30b9\u30bf\u30f3\u30d0\u30a4", - "wall_follow": "\u58c1\u3092\u305f\u3069\u308b", + "wall_follow": "\u30d5\u30a9\u30ed\u30fc\u30a6\u30a9\u30fc\u30eb", "zone": "\u30be\u30fc\u30f3" } } diff --git a/homeassistant/components/twinkly/translations/ja.json b/homeassistant/components/twinkly/translations/ja.json index 2ec0d28e0bf..03cc727696e 100644 --- a/homeassistant/components/twinkly/translations/ja.json +++ b/homeassistant/components/twinkly/translations/ja.json @@ -8,7 +8,7 @@ }, "step": { "discovery_confirm": { - "description": "{name} - {model} ({host}) \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" + "description": "{name} - {model} ( {host} ) \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b?" }, "user": { "data": { diff --git a/homeassistant/components/unifi/translations/ja.json b/homeassistant/components/unifi/translations/ja.json index cd810e12d6a..1039f10dfaf 100644 --- a/homeassistant/components/unifi/translations/ja.json +++ b/homeassistant/components/unifi/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u30b3\u30f3\u30c8\u30ed\u30fc\u30e9\u30fc\u30b5\u30a4\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "configuration_updated": "\u8a2d\u5b9a\u304c\u66f4\u65b0\u3055\u308c\u307e\u3057\u305f\u3002", + "configuration_updated": "\u69cb\u6210\u304c\u66f4\u65b0\u3055\u308c\u307e\u3057\u305f", "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, "error": { diff --git a/homeassistant/components/unifiprotect/translations/en.json b/homeassistant/components/unifiprotect/translations/en.json index 5d690e3fd3e..c6050d05284 100644 --- a/homeassistant/components/unifiprotect/translations/en.json +++ b/homeassistant/components/unifiprotect/translations/en.json @@ -42,11 +42,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/ja.json b/homeassistant/components/unifiprotect/translations/ja.json index 51193ed3123..b55c9c53e84 100644 --- a/homeassistant/components/unifiprotect/translations/ja.json +++ b/homeassistant/components/unifiprotect/translations/ja.json @@ -16,7 +16,7 @@ "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, - "description": "{name} ({ip_address}) \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f", + "description": "{name} ( {ip_address} ) \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b? UniFi OS\u30b3\u30f3\u30bd\u30fc\u30eb\u3067\u4f5c\u6210\u3057\u305f\u30ed\u30fc\u30ab\u30eb\u30e6\u30fc\u30b6\u30fc\u3067\u30ed\u30b0\u30a4\u30f3\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002Ubiquiti Cloud Users\u3067\u306f\u52d5\u4f5c\u3057\u307e\u305b\u3093\u3002\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001{local_user_documentation_url} \u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "title": "UniFi Protect\u304c\u691c\u51fa\u3055\u308c\u307e\u3057\u305f" }, "reauth_confirm": { @@ -54,7 +54,7 @@ "max_media": "\u30e1\u30c7\u30a3\u30a2\u30d6\u30e9\u30a6\u30b6\u306b\u30ed\u30fc\u30c9\u3059\u308b\u30a4\u30d9\u30f3\u30c8\u306e\u6700\u5927\u6570(RAM\u4f7f\u7528\u91cf\u304c\u5897\u52a0)", "override_connection_host": "\u63a5\u7d9a\u30db\u30b9\u30c8\u3092\u4e0a\u66f8\u304d" }, - "description": "\u30ea\u30a2\u30eb\u30bf\u30a4\u30e0\u30e1\u30c8\u30ea\u30c3\u30af \u30aa\u30d7\u30b7\u30e7\u30f3(Realtime metrics option)\u306f\u3001\u8a3a\u65ad\u30bb\u30f3\u30b5\u30fc(diagnostics sensors)\u3092\u6709\u52b9\u306b\u3057\u3066\u3044\u3066\u3001\u30ea\u30a2\u30eb\u30bf\u30a4\u30e0\u3067\u66f4\u65b0\u3057\u305f\u3044\u5834\u5408\u306b\u306e\u307f\u6709\u52b9\u306b\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u6709\u52b9\u306b\u3055\u308c\u3066\u3044\u306a\u3044\u5834\u5408\u306b\u306f\u300115\u5206\u3054\u3068\u306b1\u56de\u3060\u3051\u66f4\u65b0\u3055\u308c\u307e\u3059\u3002", + "description": "\u30ea\u30a2\u30eb\u30bf\u30a4\u30e0 \u30e1\u30c8\u30ea\u30c3\u30af \u30aa\u30d7\u30b7\u30e7\u30f3\u306f\u3001\u8a3a\u65ad\u30bb\u30f3\u30b5\u30fc\u3092\u6709\u52b9\u306b\u3057\u3066\u30ea\u30a2\u30eb\u30bf\u30a4\u30e0\u3067\u66f4\u65b0\u3059\u308b\u5834\u5408\u306b\u306e\u307f\u6709\u52b9\u306b\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\u6709\u52b9\u306b\u3057\u306a\u3044\u5834\u5408\u300115 \u5206\u3054\u3068\u306b 1 \u56de\u3060\u3051\u66f4\u65b0\u3055\u308c\u307e\u3059\u3002", "title": "UniFi Protect\u30aa\u30d7\u30b7\u30e7\u30f3" } } diff --git a/homeassistant/components/uptimerobot/translations/ja.json b/homeassistant/components/uptimerobot/translations/ja.json index 55ff808725f..56e69cada10 100644 --- a/homeassistant/components/uptimerobot/translations/ja.json +++ b/homeassistant/components/uptimerobot/translations/ja.json @@ -18,14 +18,14 @@ "data": { "api_key": "API\u30ad\u30fc" }, - "description": "UptimeRobot\u304b\u3089\u65b0\u898f\u306e\u8aad\u307f\u53d6\u308a\u5c02\u7528\u306eAPI\u30ad\u30fc\u3092\u5f97\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", + "description": "UptimeRobot \u304b\u3089\u65b0\u3057\u3044\u300c\u30e1\u30a4\u30f3\u300dAPI \u30ad\u30fc\u3092\u63d0\u4f9b\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "user": { "data": { "api_key": "API\u30ad\u30fc" }, - "description": "UptimeRobot\u304b\u3089\u8aad\u307f\u53d6\u308a\u5c02\u7528\u306eAPI\u30ad\u30fc\u3092\u5f97\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059" + "description": "UptimeRobot \u304b\u3089\u300c\u30e1\u30a4\u30f3\u300dAPI \u30ad\u30fc\u3092\u63d0\u4f9b\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059" } } } diff --git a/homeassistant/components/utility_meter/translations/ja.json b/homeassistant/components/utility_meter/translations/ja.json index ec12f2310be..02fcaa988fd 100644 --- a/homeassistant/components/utility_meter/translations/ja.json +++ b/homeassistant/components/utility_meter/translations/ja.json @@ -18,7 +18,7 @@ "tariffs": "\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u308b\u6599\u91d1(Tariff)\u306e\u30ea\u30b9\u30c8\u3002\u5358\u4e00\u306e\u6599\u91d1\u8868\u306e\u307f\u304c\u5fc5\u8981\u306a\u5834\u5408\u306f\u7a7a\u767d\u306e\u307e\u307e\u306b\u3057\u307e\u3059\u3002" }, "description": "\u8a2d\u5b9a\u3055\u308c\u305f\u671f\u9593\u3001\u901a\u5e38\u306f\u6bce\u6708\u3001\u69d8\u3005\u306a\u30e6\u30fc\u30c6\u30a3\u30ea\u30c6\u30a3(\u30a8\u30cd\u30eb\u30ae\u30fc\u3001\u30ac\u30b9\u3001\u6c34\u3001\u6696\u623f\u306a\u3069)\u306e\u6d88\u8cbb\u91cf\u3092\u8ffd\u8de1\u3059\u308b\u30bb\u30f3\u30b5\u30fc\u3092\u4f5c\u6210\u3057\u307e\u3059\u3002\u30e6\u30fc\u30c6\u30a3\u30ea\u30c6\u30a3\u30bb\u30f3\u30b5\u30fc\u306f\u3001\u30aa\u30d7\u30b7\u30e7\u30f3\u3068\u3057\u3066\u3001\u6599\u91d1\u8868\u306b\u3088\u308b\u6d88\u8cbb\u91cf\u306e\u5206\u5272\u3092\u30b5\u30dd\u30fc\u30c8\u3057\u307e\u3059\u3002\u3053\u306e\u5834\u5408\u3001\u5404\u6599\u91d1\u8868\u306e\u305f\u3081\u306e1\u3064\u306e\u30bb\u30f3\u30b5\u30fc\u3068\u3001\u73fe\u5728\u306e\u6599\u91d1(Tariff)\u3092\u9078\u629e\u3059\u308b\u305f\u3081\u306e\u9078\u629e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u304c\u4f5c\u6210\u3055\u308c\u307e\u3059\u3002", - "title": "\u65b0\u3057\u3044\u30e6\u30fc\u30c6\u30a3\u30ea\u30c6\u30a3\u30e1\u30fc\u30bf\u30fc" + "title": "\u30e6\u30fc\u30c6\u30a3\u30ea\u30c6\u30a3\u30e1\u30fc\u30bf\u30fc\u3092\u8ffd\u52a0" } } }, diff --git a/homeassistant/components/vulcan/translations/ja.json b/homeassistant/components/vulcan/translations/ja.json index 5d6f5bbcca7..2f2569b45df 100644 --- a/homeassistant/components/vulcan/translations/ja.json +++ b/homeassistant/components/vulcan/translations/ja.json @@ -3,7 +3,7 @@ "abort": { "all_student_already_configured": "\u3059\u3079\u3066\u306e\u751f\u5f92\u306f\u3059\u3067\u306b\u8ffd\u52a0\u3055\u308c\u3066\u3044\u307e\u3059\u3002", "already_configured": "\u305d\u306e\u5b66\u751f\u306f\u3059\u3067\u306b\u8ffd\u52a0\u3055\u308c\u3066\u3044\u307e\u3059\u3002", - "no_matching_entries": "\u4e00\u81f4\u3059\u308b\u30a8\u30f3\u30c8\u30ea\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3002\u5225\u306e\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u4f7f\u7528\u3059\u308b\u304b\u3001outdated student\u7d71\u5408\u3092\u524a\u9664\u3057\u3066\u304f\u3060\u3055\u3044..", + "no_matching_entries": "\u4e00\u81f4\u3059\u308b\u30a8\u30f3\u30c8\u30ea\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u5225\u306e\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u4f7f\u7528\u3059\u308b\u304b\u3001\u53e4\u3044\u5b66\u751f\u3068\u306e\u7d71\u5408\u3092\u524a\u9664\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f" }, "error": { diff --git a/homeassistant/components/webostv/translations/ja.json b/homeassistant/components/webostv/translations/ja.json index 75d085fc98d..614c4188498 100644 --- a/homeassistant/components/webostv/translations/ja.json +++ b/homeassistant/components/webostv/translations/ja.json @@ -11,7 +11,7 @@ "flow_title": "WebOS\u30b9\u30de\u30fc\u30c8TV", "step": { "pairing": { - "description": "\u9001\u4fe1(submit)\u3092\u30af\u30ea\u30c3\u30af\u3057\u3066\u3001TV\u3067\u30da\u30a2\u30ea\u30f3\u30b0\u306e\u30ea\u30af\u30a8\u30b9\u30c8\u3092\u53d7\u3051\u5165\u308c\u307e\u3059\u3002 \n\n![Image](/static/images/config_webos.png)", + "description": "[\u9001\u4fe1] \u3092\u30af\u30ea\u30c3\u30af\u3057\u3066\u3001\u30c6\u30ec\u30d3\u3067\u30da\u30a2\u30ea\u30f3\u30b0 \u30ea\u30af\u30a8\u30b9\u30c8\u3092\u53d7\u3051\u5165\u308c\u307e\u3059\u3002 \n\n ![\u753b\u50cf](/static/images/config_webos.png)", "title": "webOS TV\u30da\u30a2\u30ea\u30f3\u30b0" }, "user": { diff --git a/homeassistant/components/wiz/translations/ja.json b/homeassistant/components/wiz/translations/ja.json index b8e1536654b..b2737b8540b 100644 --- a/homeassistant/components/wiz/translations/ja.json +++ b/homeassistant/components/wiz/translations/ja.json @@ -6,7 +6,7 @@ "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" }, "error": { - "bulb_time_out": "\u96fb\u7403\u306b\u63a5\u7d9a\u3067\u304d\u307e\u305b\u3093\u3002\u96fb\u7403\u304c\u30aa\u30d5\u30e9\u30a4\u30f3\u306b\u306a\u3063\u3066\u3044\u308b\u304b\u3001\u9593\u9055\u3063\u305fIP/\u30db\u30b9\u30c8\u304c\u5165\u529b\u3055\u308c\u3066\u3044\u308b\u53ef\u80fd\u6027\u304c\u3042\u308a\u307e\u3059\u3002\u96fb\u7403\u306e\u96fb\u6e90\u3092\u5165\u308c\u3066\u518d\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044\u3002", + "bulb_time_out": "\u96fb\u7403\u306b\u63a5\u7d9a\u3067\u304d\u307e\u305b\u3093\u3002\u96fb\u7403\u304c\u30aa\u30d5\u30e9\u30a4\u30f3\u3067\u3042\u308b\u304b\u3001\u9593\u9055\u3063\u305f IP \u304c\u5165\u529b\u3055\u308c\u305f\u53ef\u80fd\u6027\u304c\u3042\u308a\u307e\u3059\u3002\u30e9\u30a4\u30c8\u3092\u30aa\u30f3\u306b\u3057\u3066\u3001\u3082\u3046\u4e00\u5ea6\u8a66\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "no_ip": "\u6709\u52b9\u306aIP\u30a2\u30c9\u30ec\u30b9\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002", "no_wiz_light": "\u3053\u306e\u96fb\u7403\u306f\u3001WiZ Platform\u7d71\u5408\u3092\u4ecb\u3057\u3066\u63a5\u7d9a\u3059\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002", @@ -26,7 +26,7 @@ "data": { "host": "\u30db\u30b9\u30c8" }, - "description": "\u65b0\u3057\u3044\u96fb\u7403(bulb)\u3092\u8ffd\u52a0\u3059\u308b\u306b\u306f\u3001\u30db\u30b9\u30c8\u540d\u307e\u305f\u306fIP\u30a2\u30c9\u30ec\u30b9\u3068\u540d\u524d\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044:" + "description": "IP \u30a2\u30c9\u30ec\u30b9\u3092\u7a7a\u306e\u307e\u307e\u306b\u3059\u308b\u3068\u3001\u30c7\u30d0\u30a4\u30b9\u306e\u691c\u51fa\u306b\u691c\u51fa\u304c\u4f7f\u7528\u3055\u308c\u307e\u3059\u3002" } } } diff --git a/homeassistant/components/wolflink/translations/sensor.ja.json b/homeassistant/components/wolflink/translations/sensor.ja.json index d2ba79d4b19..3a6ccdb9d16 100644 --- a/homeassistant/components/wolflink/translations/sensor.ja.json +++ b/homeassistant/components/wolflink/translations/sensor.ja.json @@ -22,7 +22,7 @@ "dhw_prior": "DHWPrior", "eco": "\u30a8\u30b3", "ein": "\u6709\u52b9", - "estrichtrocknung": "Screed drying", + "estrichtrocknung": "\u30b9\u30af\u30ea\u30fc\u30c9\u4e7e\u71e5", "externe_deaktivierung": "\u5916\u90e8\u306e\u975e\u30a2\u30af\u30c6\u30a3\u30d6\u5316", "fernschalter_ein": "\u30ea\u30e2\u30fc\u30c8\u5236\u5fa1\u304c\u6709\u52b9", "frost_heizkreis": "\u6696\u623f(\u52a0\u71b1)\u56de\u8def\u306e\u971c", @@ -39,11 +39,11 @@ "kalibration_heizbetrieb": "\u6696\u623f(\u52a0\u71b1)\u306e\u30ad\u30e3\u30ea\u30d6\u30ec\u30fc\u30b7\u30e7\u30f3", "kalibration_kombibetrieb": "\u30b3\u30f3\u30d3\u30e2\u30fc\u30c9 \u30ad\u30e3\u30ea\u30d6\u30ec\u30fc\u30b7\u30e7\u30f3", "kalibration_warmwasserbetrieb": "DHW\u30ad\u30e3\u30ea\u30d6\u30ec\u30fc\u30b7\u30e7\u30f3", - "kaskadenbetrieb": "Cascade\u64cd\u4f5c", + "kaskadenbetrieb": "\u30ab\u30b9\u30b1\u30fc\u30c9\u64cd\u4f5c", "kombibetrieb": "\u30b3\u30f3\u30d3\u30e2\u30fc\u30c9", "kombigerat": "\u30b3\u30f3\u30d3 \u30dc\u30a4\u30e9\u30fc", - "kombigerat_mit_solareinbindung": "\u592a\u967d\u71b1\u5229\u7528\u306e\u30b3\u30f3\u30d3\u30dc\u30a4\u30e9\u30fc", - "mindest_kombizeit": "\u6700\u5c0fcombi time", + "kombigerat_mit_solareinbindung": "\u30bd\u30fc\u30e9\u30fc\u7d71\u5408\u578b\u30b3\u30f3\u30d3\u30dc\u30a4\u30e9\u30fc", + "mindest_kombizeit": "\u6700\u77ed\u30b3\u30f3\u30d3\u6642\u9593", "nachlauf_heizkreispumpe": "\u6696\u623f(\u52a0\u71b1)\u56de\u8def\u30dd\u30f3\u30d7\u306e\u4f5c\u52d5", "nachspulen": "\u30d5\u30e9\u30c3\u30b7\u30e5\u5f8c(Post-flush)", "nur_heizgerat": "\u30dc\u30a4\u30e9\u30fc\u306e\u307f", @@ -65,7 +65,7 @@ "sparen": "\u30a8\u30b3\u30ce\u30df\u30fc", "spreizung_hoch": "dT\u304c\u5e83\u3059\u304e\u308b(dT too wide)", "spreizung_kf": "\u5e83\u3052\u308b(Spread) KF", - "stabilisierung": "\u5b89\u5b9a\u5316", + "stabilisierung": "\u5b89\u5b9a", "standby": "\u30b9\u30bf\u30f3\u30d0\u30a4", "start": "\u8d77\u52d5", "storung": "\u30d5\u30a9\u30fc\u30eb\u30c8", diff --git a/homeassistant/components/xiaomi_ble/translations/ja.json b/homeassistant/components/xiaomi_ble/translations/ja.json index 597c0ecc3f2..3d84421b1b3 100644 --- a/homeassistant/components/xiaomi_ble/translations/ja.json +++ b/homeassistant/components/xiaomi_ble/translations/ja.json @@ -19,6 +19,9 @@ "bluetooth_confirm": { "description": "{name} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" }, + "confirm_slow": { + "description": "\u76f4\u524d\u306b\u3053\u306e\u30c7\u30d0\u30a4\u30b9\u304b\u3089\u306e\u30d6\u30ed\u30fc\u30c9\u30ad\u30e3\u30b9\u30c8\u304c\u306a\u304b\u3063\u305f\u305f\u3081\u3001\u3053\u306e\u30c7\u30d0\u30a4\u30b9\u304c\u6697\u53f7\u5316\u3092\u4f7f\u7528\u3057\u3066\u3044\u308b\u304b\u3069\u3046\u304b\u306f\u308f\u304b\u308a\u307e\u305b\u3093\u3002\u3053\u308c\u306f\u3001\u30c7\u30d0\u30a4\u30b9\u304c\u9045\u3044\u30d6\u30ed\u30fc\u30c9\u30ad\u30e3\u30b9\u30c8\u9593\u9694\u3092\u4f7f\u7528\u3057\u3066\u3044\u308b\u3053\u3068\u304c\u539f\u56e0\u3067\u3042\u308b\u53ef\u80fd\u6027\u304c\u3042\u308a\u307e\u3059\u3002\u3068\u306b\u304b\u304f\u3053\u306e\u30c7\u30d0\u30a4\u30b9\u3092\u8ffd\u52a0\u3059\u308b\u3053\u3068\u3092\u78ba\u8a8d\u3059\u308b\u3068\u3001\u6b21\u306b\u30d6\u30ed\u30fc\u30c9\u30ad\u30e3\u30b9\u30c8\u3092\u53d7\u4fe1\u3057\u305f\u3068\u304d\u306b\u3001\u5fc5\u8981\u306b\u5fdc\u3058\u3066\u30d0\u30a4\u30f3\u30c9\u30ad\u30fc\u3092\u5165\u529b\u3059\u308b\u3088\u3046\u6c42\u3081\u3089\u308c\u307e\u3059\u3002" + }, "get_encryption_key_4_5": { "data": { "bindkey": "\u30d0\u30a4\u30f3\u30c9\u30ad\u30fc" @@ -31,6 +34,9 @@ }, "description": "\u30bb\u30f3\u30b5\u30fc\u304b\u3089\u30d6\u30ed\u30fc\u30c9\u30ad\u30e3\u30b9\u30c8\u3055\u308c\u308b\u30bb\u30f3\u30b5\u30fc\u30c7\u30fc\u30bf\u306f\u6697\u53f7\u5316\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5fa9\u53f7\u5316\u3059\u308b\u306b\u306f\u300116\u9032\u6570\u306724\u6587\u5b57\u306a\u30d0\u30a4\u30f3\u30c9\u30ad\u30fc\u304c\u5fc5\u8981\u3067\u3059\u3002" }, + "slow_confirm": { + "description": "\u76f4\u524d\u306b\u3053\u306e\u30c7\u30d0\u30a4\u30b9\u304b\u3089\u306e\u30d6\u30ed\u30fc\u30c9\u30ad\u30e3\u30b9\u30c8\u304c\u306a\u304b\u3063\u305f\u305f\u3081\u3001\u3053\u306e\u30c7\u30d0\u30a4\u30b9\u304c\u6697\u53f7\u5316\u3092\u4f7f\u7528\u3057\u3066\u3044\u308b\u304b\u3069\u3046\u304b\u306f\u308f\u304b\u308a\u307e\u305b\u3093\u3002\u3053\u308c\u306f\u3001\u30c7\u30d0\u30a4\u30b9\u304c\u9045\u3044\u30d6\u30ed\u30fc\u30c9\u30ad\u30e3\u30b9\u30c8\u9593\u9694\u3092\u4f7f\u7528\u3057\u3066\u3044\u308b\u3053\u3068\u304c\u539f\u56e0\u3067\u3042\u308b\u53ef\u80fd\u6027\u304c\u3042\u308a\u307e\u3059\u3002\u3068\u306b\u304b\u304f\u3053\u306e\u30c7\u30d0\u30a4\u30b9\u3092\u8ffd\u52a0\u3059\u308b\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u6b21\u306b\u30d6\u30ed\u30fc\u30c9\u30ad\u30e3\u30b9\u30c8\u3092\u53d7\u4fe1\u3057\u305f\u3068\u304d\u306b\u3001\u5fc5\u8981\u306b\u5fdc\u3058\u3066\u30d0\u30a4\u30f3\u30c9\u30ad\u30fc\u3092\u5165\u529b\u3059\u308b\u3088\u3046\u6c42\u3081\u3089\u308c\u307e\u3059\u3002" + }, "user": { "data": { "address": "\u30c7\u30d0\u30a4\u30b9" diff --git a/homeassistant/components/yalexs_ble/translations/ja.json b/homeassistant/components/yalexs_ble/translations/ja.json index 5e752cb1cfa..20d2b73959e 100644 --- a/homeassistant/components/yalexs_ble/translations/ja.json +++ b/homeassistant/components/yalexs_ble/translations/ja.json @@ -16,7 +16,7 @@ "flow_title": "{name}", "step": { "integration_discovery_confirm": { - "description": "\u30a2\u30c9\u30ec\u30b9 {address} \u3092\u3001Bluetooth\u7d4c\u7531\u3067 {name} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" + "description": "\u30a2\u30c9\u30ec\u30b9{address}\u3067 Bluetooth \u7d4c\u7531\u3067{name}\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b?" }, "user": { "data": { diff --git a/homeassistant/components/zha/translations/ca.json b/homeassistant/components/zha/translations/ca.json index 1ee6d49cfc3..073e72b80a3 100644 --- a/homeassistant/components/zha/translations/ca.json +++ b/homeassistant/components/zha/translations/ca.json @@ -6,7 +6,8 @@ "usb_probe_failed": "No s'ha pogut provar el dispositiu USB" }, "error": { - "cannot_connect": "Ha fallat la connexi\u00f3" + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_backup_json": "JSON de c\u00f2pia de seguretat inv\u00e0lid" }, "flow_title": "{name}", "step": { @@ -176,7 +177,8 @@ "usb_probe_failed": "No s'ha pogut provar el dispositiu USB" }, "error": { - "cannot_connect": "Ha fallat la connexi\u00f3" + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_backup_json": "JSON de c\u00f2pia de seguretat inv\u00e0lid" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/zha/translations/ja.json b/homeassistant/components/zha/translations/ja.json index 9cfb8421c68..9c92cc50398 100644 --- a/homeassistant/components/zha/translations/ja.json +++ b/homeassistant/components/zha/translations/ja.json @@ -61,6 +61,7 @@ "data": { "overwrite_coordinator_ieee": "\u7121\u7ddaIEEE \u30c9\u30ec\u30b9\u3092\u5b8c\u5168\u306b\u7f6e\u304d\u63db\u3048\u308b" }, + "description": "\u30d0\u30c3\u30af\u30a2\u30c3\u30d7\u306b\u306f\u3001\u7121\u7dda\u3068\u306f\u7570\u306a\u308b IEEE \u30a2\u30c9\u30ec\u30b9\u304c\u3042\u308a\u307e\u3059\u3002\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u304c\u6b63\u3057\u304f\u6a5f\u80fd\u3059\u308b\u306b\u306f\u3001\u7121\u7dda\u306e IEEE \u30a2\u30c9\u30ec\u30b9\u3082\u5909\u66f4\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002 \n\n\u3053\u308c\u306f\u6052\u4e45\u7684\u306a\u64cd\u4f5c\u3067\u3059\u3002", "title": "\u7121\u7ddaIEEE\u30a2\u30c9\u30ec\u30b9\u306e\u4e0a\u66f8\u304d" }, "pick_radio": { @@ -83,6 +84,7 @@ "data": { "uploaded_backup_file": "\u30d5\u30a1\u30a4\u30eb\u3092\u30a2\u30c3\u30d7\u30ed\u30fc\u30c9\u3059\u308b" }, + "description": "\u30a2\u30c3\u30d7\u30ed\u30fc\u30c9\u3055\u308c\u305f\u30d0\u30c3\u30af\u30a2\u30c3\u30d7 JSON \u30d5\u30a1\u30a4\u30eb\u304b\u3089\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u8a2d\u5b9a\u3092\u5fa9\u5143\u3057\u307e\u3059\u3002 **Network Settings** \u304b\u3089\u5225\u306e ZHA \u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u304b\u3089\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u3059\u308b\u304b\u3001Zigbee2MQTT `coordinator_backup.json` \u30d5\u30a1\u30a4\u30eb\u3092\u4f7f\u7528\u3067\u304d\u307e\u3059\u3002", "title": "\u624b\u52d5\u30d0\u30c3\u30af\u30a2\u30c3\u30d7\u3092\u30a2\u30c3\u30d7\u30ed\u30fc\u30c9\u3059\u308b" }, "user": { @@ -96,15 +98,15 @@ }, "config_panel": { "zha_alarm_options": { - "alarm_arm_requires_code": "\u8b66\u6212\u30a2\u30af\u30b7\u30e7\u30f3\u306b\u5fc5\u8981\u306a\u30b3\u30fc\u30c9", + "alarm_arm_requires_code": "\u30a2\u30fc\u30df\u30f3\u30b0\u30a2\u30af\u30b7\u30e7\u30f3\u306b\u5fc5\u8981\u306a\u30b3\u30fc\u30c9", "alarm_failed_tries": "\u30a2\u30e9\u30fc\u30e0\u3092\u30c8\u30ea\u30ac\u30fc\u3055\u305b\u308b\u305f\u3081\u306b\u9023\u7d9a\u3057\u3066\u5931\u6557\u3057\u305f\u30b3\u30fc\u30c9 \u30a8\u30f3\u30c8\u30ea\u306e\u6570", "alarm_master_code": "\u30a2\u30e9\u30fc\u30e0 \u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u30d1\u30cd\u30eb\u306e\u30de\u30b9\u30bf\u30fc\u30b3\u30fc\u30c9", "title": "\u30a2\u30e9\u30fc\u30e0 \u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u30d1\u30cd\u30eb\u306e\u30aa\u30d7\u30b7\u30e7\u30f3" }, "zha_options": { "always_prefer_xy_color_mode": "\u5e38\u306bXY\u30ab\u30e9\u30fc\u30e2\u30fc\u30c9\u3092\u512a\u5148", - "consider_unavailable_battery": "(\u79d2)\u5f8c\u306b\u30d0\u30c3\u30c6\u30ea\u30fc\u99c6\u52d5\u306e\u30c7\u30d0\u30a4\u30b9\u304c\u4f7f\u7528\u3067\u304d\u306a\u304f\u306a\u308b\u3068\u898b\u306a\u3059", - "consider_unavailable_mains": "(\u79d2)\u5f8c\u306b\u4e3b\u96fb\u6e90\u304c\u30c7\u30d0\u30a4\u30b9\u304b\u3089\u4f7f\u7528\u3067\u304d\u306a\u304f\u306a\u308b\u3068\u898b\u306a\u3059", + "consider_unavailable_battery": "\u30d0\u30c3\u30c6\u30ea\u99c6\u52d5\u306e\u30c7\u30d0\u30a4\u30b9\u306f (\u79d2) \u5f8c\u306b\u5229\u7528\u3067\u304d\u306a\u3044\u3068\u898b\u306a\u3059", + "consider_unavailable_mains": "(\u79d2) \u5f8c\u306b\u4e3b\u96fb\u6e90\u304c\u4f9b\u7d66\u3055\u308c\u3066\u3044\u308b\u30c7\u30d0\u30a4\u30b9\u3092\u4f7f\u7528\u3067\u304d\u306a\u3044\u3068\u898b\u306a\u3059", "default_light_transition": "\u30c7\u30d5\u30a9\u30eb\u30c8\u306e\u30e9\u30a4\u30c8\u9077\u79fb\u6642\u9593(\u79d2)", "enable_identify_on_join": "\u30c7\u30d0\u30a4\u30b9\u304c\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u306b\u53c2\u52a0\u3059\u308b\u969b\u306b\u3001\u8b58\u5225\u52b9\u679c\u3092\u6709\u52b9\u306b\u3059\u308b", "enhanced_light_transition": "\u30aa\u30d5\u72b6\u614b\u304b\u3089\u3001\u30a8\u30f3\u30cf\u30f3\u30b9\u30c9\u30e9\u30a4\u30c8\u30ab\u30e9\u30fc/\u8272\u6e29\u5ea6\u3078\u306e\u9077\u79fb\u3092\u6709\u52b9\u306b\u3057\u307e\u3059", diff --git a/homeassistant/components/zwave_js/translations/ja.json b/homeassistant/components/zwave_js/translations/ja.json index 94645e302f4..41568815193 100644 --- a/homeassistant/components/zwave_js/translations/ja.json +++ b/homeassistant/components/zwave_js/translations/ja.json @@ -83,7 +83,7 @@ "trigger_type": { "event.notification.entry_control": "\u30a8\u30f3\u30c8\u30ea\u30fc\u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u901a\u77e5\u3092\u9001\u4fe1\u3057\u307e\u3057\u305f", "event.notification.notification": "\u901a\u77e5\u3092\u9001\u4fe1\u3057\u307e\u3057\u305f", - "event.value_notification.basic": "{subtype} \u306a\u30d9\u30fc\u30b7\u30c3\u30af CC \u30a4\u30d9\u30f3\u30c8", + "event.value_notification.basic": "{subtype}\u306e\u57fa\u672c CC \u30a4\u30d9\u30f3\u30c8", "event.value_notification.central_scene": "{subtype} \u306e\u30bb\u30f3\u30c8\u30e9\u30eb \u30b7\u30fc\u30f3 \u30a2\u30af\u30b7\u30e7\u30f3", "event.value_notification.scene_activation": "{subtype} \u3067\u306e\u30b7\u30fc\u30f3\u306e\u30a2\u30af\u30c6\u30a3\u30d6\u5316", "state.node_status": "\u30ce\u30fc\u30c9\u30b9\u30c6\u30fc\u30bf\u30b9\u304c\u5909\u5316\u3057\u307e\u3057\u305f", diff --git a/homeassistant/components/zwave_me/translations/ja.json b/homeassistant/components/zwave_me/translations/ja.json index 2816ea011e4..cc0c52ad93a 100644 --- a/homeassistant/components/zwave_me/translations/ja.json +++ b/homeassistant/components/zwave_me/translations/ja.json @@ -13,7 +13,7 @@ "token": "\u30c8\u30fc\u30af\u30f3", "url": "URL" }, - "description": "Z-Way\u30b5\u30fc\u30d0\u30fc\u306eIP\u30a2\u30c9\u30ec\u30b9\u3068Z-Way\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3\u3092\u5165\u529b\u3057\u307e\u3059\u3002HTTP\u306e\u4ee3\u308f\u308a\u306bHTTPS\u3092\u4f7f\u7528\u3059\u308b\u5fc5\u8981\u304c\u3042\u308b\u5834\u5408\u306f\u3001IP\u30a2\u30c9\u30ec\u30b9\u306e\u524d\u306b\u3001wss://\u3092\u4ed8\u3051\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u3059\u3002\u30c8\u30fc\u30af\u30f3\u3092\u53d6\u5f97\u3059\u308b\u306b\u306f\u3001Z-Way user interface > Menu > Settings > User > API token \u306b\u79fb\u52d5\u3057\u307e\u3059\u3002Home Assistant\u306e\u65b0\u3057\u3044\u30e6\u30fc\u30b6\u30fc\u3092\u4f5c\u6210\u3057\u3001Home Assistant\u304b\u3089\u5236\u5fa1\u3059\u308b\u5fc5\u8981\u306e\u3042\u308b\u30c7\u30d0\u30a4\u30b9\u3078\u306e\u30a2\u30af\u30bb\u30b9\u3092\u8a31\u53ef\u3059\u308b\u3053\u3068\u3092\u304a\u52e7\u3081\u3057\u307e\u3059\u3002find.z-wave.me\u3092\u4ecb\u3057\u305f\u30ea\u30e2\u30fc\u30c8\u30a2\u30af\u30bb\u30b9\u3092\u4f7f\u7528\u3057\u3066\u3001\u30ea\u30e2\u30fc\u30c8Z-Way\u3092\u63a5\u7d9a\u3059\u308b\u3053\u3068\u3082\u3067\u304d\u307e\u3059\u3002IP\u30d5\u30a3\u30fc\u30eb\u30c9\u306b\u3001wss://find.z-wave.me \u3092\u5165\u529b\u3057\u3001\u30b0\u30ed\u30fc\u30d0\u30eb\u30b9\u30b3\u30fc\u30d7\u3067\u30c8\u30fc\u30af\u30f3\u3092\u30b3\u30d4\u30fc\u3057\u307e\u3059\uff08\u3053\u308c\u306b\u3064\u3044\u3066\u306f\u3001find.z-wave.me\u3092\u4ecb\u3057\u3066Z-Way\u306b\u30ed\u30b0\u30a4\u30f3\u3057\u307e\u3059\uff09\u3002" + "description": "Z-Way\u30b5\u30fc\u30d0\u30fc\u306e\u30dd\u30fc\u30c8\u3068\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3\u3067IP\u30a2\u30c9\u30ec\u30b9\u3092\u5165\u529b\u3057\u307e\u3059\u3002\u30c8\u30fc\u30af\u30f3\u3092\u53d6\u5f97\u3059\u308b\u306b\u306f\u3001Z-Way \u30e6\u30fc\u30b6\u30fc \u30a4\u30f3\u30bf\u30fc\u30d5\u30a7\u30a4\u30b9\u306e [\u30b9\u30de\u30fc\u30c8 \u30db\u30fc\u30e0 UI] > [\u30e1\u30cb\u30e5\u30fc] > [\u8a2d\u5b9a] >\u30e6\u30fc\u30b6\u30fc] >\u7ba1\u7406\u8005] > API \u30c8\u30fc\u30af\u30f3] \u306b\u79fb\u52d5\u3057\u307e\u3059\u3002 \n\n\u30ed\u30fc\u30ab\u30eb \u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u3067 Z-Way \u306b\u63a5\u7d9a\u3059\u308b\u4f8b:\n URL: {local_url}\n\u30c8\u30fc\u30af\u30f3: {local_token} \n\n\u30ea\u30e2\u30fc\u30c8 \u30a2\u30af\u30bb\u30b9 find.z-wave.me \u7d4c\u7531\u3067 Z-Way \u306b\u63a5\u7d9a\u3059\u308b\u4f8b:\n URL: {find_url}\n\u30c8\u30fc\u30af\u30f3: {find_token} \n\n\u9759\u7684\u30d1\u30d6\u30ea\u30c3\u30af IP \u30a2\u30c9\u30ec\u30b9\u3092\u4f7f\u7528\u3057\u3066 Z-Way \u306b\u63a5\u7d9a\u3059\u308b\u4f8b:\n URL: {remote_url}\n\u30c8\u30fc\u30af\u30f3: {local_token} \n\n find.z-wave.me \u7d4c\u7531\u3067\u63a5\u7d9a\u3059\u308b\u5834\u5408\u3001\u30b0\u30ed\u30fc\u30d0\u30eb \u30b9\u30b3\u30fc\u30d7\u306e\u30c8\u30fc\u30af\u30f3\u3092\u4f7f\u7528\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059 (\u3053\u308c\u306b\u306f find.z-wave.me \u7d4c\u7531\u3067 Z-Way \u306b\u30ed\u30b0\u30a4\u30f3\u3057\u307e\u3059)\u3002" } } } From 35cdad943b1cdcba513661fc91bc11740dd49230 Mon Sep 17 00:00:00 2001 From: Lennard Scheibel <44374653+lscheibel@users.noreply.github.com> Date: Wed, 7 Sep 2022 05:18:27 +0200 Subject: [PATCH 169/955] Fix shopping_list service calls not notifying event bus (#77794) --- .../components/shopping_list/__init__.py | 58 ++++++++++++------- .../components/shopping_list/const.py | 1 + .../components/shopping_list/intent.py | 4 +- .../components/websocket_api/permissions.py | 2 +- tests/components/shopping_list/test_init.py | 49 ++++++++++++++++ 5 files changed, 89 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/shopping_list/__init__.py b/homeassistant/components/shopping_list/__init__.py index 2af54722739..7344b729539 100644 --- a/homeassistant/components/shopping_list/__init__.py +++ b/homeassistant/components/shopping_list/__init__.py @@ -17,6 +17,7 @@ from homeassistant.util.json import load_json, save_json from .const import ( DOMAIN, + EVENT_SHOPPING_LIST_UPDATED, SERVICE_ADD_ITEM, SERVICE_CLEAR_COMPLETED_ITEMS, SERVICE_COMPLETE_ALL, @@ -29,7 +30,6 @@ ATTR_COMPLETE = "complete" _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = vol.Schema({DOMAIN: {}}, extra=vol.ALLOW_EXTRA) -EVENT = "shopping_list_updated" ITEM_UPDATE_SCHEMA = vol.Schema({ATTR_COMPLETE: bool, ATTR_NAME: str}) PERSISTENCE = ".shopping_list.json" @@ -204,14 +204,19 @@ class ShoppingData: self.hass = hass self.items = [] - async def async_add(self, name): + async def async_add(self, name, context=None): """Add a shopping list item.""" item = {"name": name, "id": uuid.uuid4().hex, "complete": False} self.items.append(item) await self.hass.async_add_executor_job(self.save) + self.hass.bus.async_fire( + EVENT_SHOPPING_LIST_UPDATED, + {"action": "add", "item": item}, + context=context, + ) return item - async def async_update(self, item_id, info): + async def async_update(self, item_id, info, context=None): """Update a shopping list item.""" item = next((itm for itm in self.items if itm["id"] == item_id), None) @@ -221,22 +226,37 @@ class ShoppingData: info = ITEM_UPDATE_SCHEMA(info) item.update(info) await self.hass.async_add_executor_job(self.save) + self.hass.bus.async_fire( + EVENT_SHOPPING_LIST_UPDATED, + {"action": "update", "item": item}, + context=context, + ) return item - async def async_clear_completed(self): + async def async_clear_completed(self, context=None): """Clear completed items.""" self.items = [itm for itm in self.items if not itm["complete"]] await self.hass.async_add_executor_job(self.save) + self.hass.bus.async_fire( + EVENT_SHOPPING_LIST_UPDATED, + {"action": "clear"}, + context=context, + ) - async def async_update_list(self, info): + async def async_update_list(self, info, context=None): """Update all items in the list.""" for item in self.items: item.update(info) await self.hass.async_add_executor_job(self.save) + self.hass.bus.async_fire( + EVENT_SHOPPING_LIST_UPDATED, + {"action": "update_list"}, + context=context, + ) return self.items @callback - def async_reorder(self, item_ids): + def async_reorder(self, item_ids, context=None): """Reorder items.""" # The array for sorted items. new_items = [] @@ -259,6 +279,11 @@ class ShoppingData: new_items.append(all_items_mapping[key]) self.items = new_items self.hass.async_add_executor_job(self.save) + self.hass.bus.async_fire( + EVENT_SHOPPING_LIST_UPDATED, + {"action": "reorder"}, + context=context, + ) async def async_load(self): """Load items.""" @@ -298,7 +323,6 @@ class UpdateShoppingListItemView(http.HomeAssistantView): try: item = await request.app["hass"].data[DOMAIN].async_update(item_id, data) - request.app["hass"].bus.async_fire(EVENT) return self.json(item) except KeyError: return self.json_message("Item not found", HTTPStatus.NOT_FOUND) @@ -316,7 +340,6 @@ class CreateShoppingListItemView(http.HomeAssistantView): async def post(self, request, data): """Create a new shopping list item.""" item = await request.app["hass"].data[DOMAIN].async_add(data["name"]) - request.app["hass"].bus.async_fire(EVENT) return self.json(item) @@ -330,7 +353,6 @@ class ClearCompletedItemsView(http.HomeAssistantView): """Retrieve if API is running.""" hass = request.app["hass"] await hass.data[DOMAIN].async_clear_completed() - hass.bus.async_fire(EVENT) return self.json_message("Cleared completed items.") @@ -353,10 +375,7 @@ async def websocket_handle_add( msg: dict, ) -> None: """Handle add item to shopping_list.""" - item = await hass.data[DOMAIN].async_add(msg["name"]) - hass.bus.async_fire( - EVENT, {"action": "add", "item": item}, context=connection.context(msg) - ) + item = await hass.data[DOMAIN].async_add(msg["name"], connection.context(msg)) connection.send_message(websocket_api.result_message(msg["id"], item)) @@ -373,9 +392,8 @@ async def websocket_handle_update( data = msg try: - item = await hass.data[DOMAIN].async_update(item_id, data) - hass.bus.async_fire( - EVENT, {"action": "update", "item": item}, context=connection.context(msg) + item = await hass.data[DOMAIN].async_update( + item_id, data, connection.context(msg) ) connection.send_message(websocket_api.result_message(msg_id, item)) except KeyError: @@ -391,8 +409,7 @@ async def websocket_handle_clear( msg: dict, ) -> None: """Handle clearing shopping_list items.""" - await hass.data[DOMAIN].async_clear_completed() - hass.bus.async_fire(EVENT, {"action": "clear"}, context=connection.context(msg)) + await hass.data[DOMAIN].async_clear_completed(connection.context(msg)) connection.send_message(websocket_api.result_message(msg["id"])) @@ -410,10 +427,7 @@ def websocket_handle_reorder( """Handle reordering shopping_list items.""" msg_id = msg.pop("id") try: - hass.data[DOMAIN].async_reorder(msg.pop("item_ids")) - hass.bus.async_fire( - EVENT, {"action": "reorder"}, context=connection.context(msg) - ) + hass.data[DOMAIN].async_reorder(msg.pop("item_ids"), connection.context(msg)) connection.send_result(msg_id) except KeyError: connection.send_error( diff --git a/homeassistant/components/shopping_list/const.py b/homeassistant/components/shopping_list/const.py index 2969fc8f86d..fffc1064226 100644 --- a/homeassistant/components/shopping_list/const.py +++ b/homeassistant/components/shopping_list/const.py @@ -1,5 +1,6 @@ """All constants related to the shopping list component.""" DOMAIN = "shopping_list" +EVENT_SHOPPING_LIST_UPDATED = "shopping_list_updated" SERVICE_ADD_ITEM = "add_item" SERVICE_COMPLETE_ITEM = "complete_item" diff --git a/homeassistant/components/shopping_list/intent.py b/homeassistant/components/shopping_list/intent.py index a6808b99328..4f5a39171b8 100644 --- a/homeassistant/components/shopping_list/intent.py +++ b/homeassistant/components/shopping_list/intent.py @@ -2,7 +2,7 @@ from homeassistant.helpers import intent import homeassistant.helpers.config_validation as cv -from . import DOMAIN, EVENT +from . import DOMAIN, EVENT_SHOPPING_LIST_UPDATED INTENT_ADD_ITEM = "HassShoppingListAddItem" INTENT_LAST_ITEMS = "HassShoppingListLastItems" @@ -28,7 +28,7 @@ class AddItemIntent(intent.IntentHandler): response = intent_obj.create_response() response.async_set_speech(f"I've added {item} to your shopping list") - intent_obj.hass.bus.async_fire(EVENT) + intent_obj.hass.bus.async_fire(EVENT_SHOPPING_LIST_UPDATED) return response diff --git a/homeassistant/components/websocket_api/permissions.py b/homeassistant/components/websocket_api/permissions.py index 5dade8eeb2a..280594580e8 100644 --- a/homeassistant/components/websocket_api/permissions.py +++ b/homeassistant/components/websocket_api/permissions.py @@ -11,7 +11,7 @@ from homeassistant.components.lovelace.const import EVENT_LOVELACE_UPDATED from homeassistant.components.persistent_notification import ( EVENT_PERSISTENT_NOTIFICATIONS_UPDATED, ) -from homeassistant.components.shopping_list import EVENT as EVENT_SHOPPING_LIST_UPDATED +from homeassistant.components.shopping_list import EVENT_SHOPPING_LIST_UPDATED from homeassistant.const import ( EVENT_COMPONENT_LOADED, EVENT_CORE_CONFIG_UPDATE, diff --git a/tests/components/shopping_list/test_init.py b/tests/components/shopping_list/test_init.py index 3c18929f6a1..515d0818460 100644 --- a/tests/components/shopping_list/test_init.py +++ b/tests/components/shopping_list/test_init.py @@ -3,6 +3,7 @@ from http import HTTPStatus from homeassistant.components.shopping_list.const import ( DOMAIN, + EVENT_SHOPPING_LIST_UPDATED, SERVICE_ADD_ITEM, SERVICE_CLEAR_COMPLETED_ITEMS, SERVICE_COMPLETE_ITEM, @@ -15,6 +16,8 @@ from homeassistant.components.websocket_api.const import ( from homeassistant.const import ATTR_NAME from homeassistant.helpers import intent +from tests.common import async_capture_events + async def test_add_item(hass, sl_setup): """Test adding an item intent.""" @@ -136,10 +139,12 @@ async def test_ws_get_items(hass, hass_ws_client, sl_setup): ) client = await hass_ws_client(hass) + events = async_capture_events(hass, EVENT_SHOPPING_LIST_UPDATED) await client.send_json({"id": 5, "type": "shopping_list/items"}) msg = await client.receive_json() assert msg["success"] is True + assert len(events) == 0 assert msg["id"] == 5 assert msg["type"] == TYPE_RESULT @@ -166,11 +171,13 @@ async def test_deprecated_api_update(hass, hass_client, sl_setup): wine_id = hass.data["shopping_list"].items[1]["id"] client = await hass_client() + events = async_capture_events(hass, EVENT_SHOPPING_LIST_UPDATED) resp = await client.post( f"/api/shopping_list/item/{beer_id}", json={"name": "soda"} ) assert resp.status == HTTPStatus.OK + assert len(events) == 1 data = await resp.json() assert data == {"id": beer_id, "name": "soda", "complete": False} @@ -179,6 +186,7 @@ async def test_deprecated_api_update(hass, hass_client, sl_setup): ) assert resp.status == HTTPStatus.OK + assert len(events) == 2 data = await resp.json() assert data == {"id": wine_id, "name": "wine", "complete": True} @@ -199,6 +207,7 @@ async def test_ws_update_item(hass, hass_ws_client, sl_setup): beer_id = hass.data["shopping_list"].items[0]["id"] wine_id = hass.data["shopping_list"].items[1]["id"] client = await hass_ws_client(hass) + events = async_capture_events(hass, EVENT_SHOPPING_LIST_UPDATED) await client.send_json( { "id": 5, @@ -211,6 +220,8 @@ async def test_ws_update_item(hass, hass_ws_client, sl_setup): assert msg["success"] is True data = msg["result"] assert data == {"id": beer_id, "name": "soda", "complete": False} + assert len(events) == 1 + await client.send_json( { "id": 6, @@ -223,6 +234,7 @@ async def test_ws_update_item(hass, hass_ws_client, sl_setup): assert msg["success"] is True data = msg["result"] assert data == {"id": wine_id, "name": "wine", "complete": True} + assert len(events) == 2 beer, wine = hass.data["shopping_list"].items assert beer == {"id": beer_id, "name": "soda", "complete": False} @@ -237,9 +249,11 @@ async def test_api_update_fails(hass, hass_client, sl_setup): ) client = await hass_client() + events = async_capture_events(hass, EVENT_SHOPPING_LIST_UPDATED) resp = await client.post("/api/shopping_list/non_existing", json={"name": "soda"}) assert resp.status == HTTPStatus.NOT_FOUND + assert len(events) == 0 beer_id = hass.data["shopping_list"].items[0]["id"] resp = await client.post(f"/api/shopping_list/item/{beer_id}", json={"name": 123}) @@ -253,6 +267,7 @@ async def test_ws_update_item_fail(hass, hass_ws_client, sl_setup): hass, "test", "HassShoppingListAddItem", {"item": {"value": "beer"}} ) client = await hass_ws_client(hass) + events = async_capture_events(hass, EVENT_SHOPPING_LIST_UPDATED) await client.send_json( { "id": 5, @@ -265,9 +280,12 @@ async def test_ws_update_item_fail(hass, hass_ws_client, sl_setup): assert msg["success"] is False data = msg["error"] assert data == {"code": "item_not_found", "message": "Item not found"} + assert len(events) == 0 + await client.send_json({"id": 6, "type": "shopping_list/items/update", "name": 123}) msg = await client.receive_json() assert msg["success"] is False + assert len(events) == 0 async def test_deprecated_api_clear_completed(hass, hass_client, sl_setup): @@ -284,15 +302,18 @@ async def test_deprecated_api_clear_completed(hass, hass_client, sl_setup): wine_id = hass.data["shopping_list"].items[1]["id"] client = await hass_client() + events = async_capture_events(hass, EVENT_SHOPPING_LIST_UPDATED) # Mark beer as completed resp = await client.post( f"/api/shopping_list/item/{beer_id}", json={"complete": True} ) assert resp.status == HTTPStatus.OK + assert len(events) == 1 resp = await client.post("/api/shopping_list/clear_completed") assert resp.status == HTTPStatus.OK + assert len(events) == 2 items = hass.data["shopping_list"].items assert len(items) == 1 @@ -311,6 +332,7 @@ async def test_ws_clear_items(hass, hass_ws_client, sl_setup): beer_id = hass.data["shopping_list"].items[0]["id"] wine_id = hass.data["shopping_list"].items[1]["id"] client = await hass_ws_client(hass) + events = async_capture_events(hass, EVENT_SHOPPING_LIST_UPDATED) await client.send_json( { "id": 5, @@ -321,24 +343,29 @@ async def test_ws_clear_items(hass, hass_ws_client, sl_setup): ) msg = await client.receive_json() assert msg["success"] is True + assert len(events) == 1 + await client.send_json({"id": 6, "type": "shopping_list/items/clear"}) msg = await client.receive_json() assert msg["success"] is True items = hass.data["shopping_list"].items assert len(items) == 1 assert items[0] == {"id": wine_id, "name": "wine", "complete": False} + assert len(events) == 2 async def test_deprecated_api_create(hass, hass_client, sl_setup): """Test the API.""" client = await hass_client() + events = async_capture_events(hass, EVENT_SHOPPING_LIST_UPDATED) resp = await client.post("/api/shopping_list/item", json={"name": "soda"}) assert resp.status == HTTPStatus.OK data = await resp.json() assert data["name"] == "soda" assert data["complete"] is False + assert len(events) == 1 items = hass.data["shopping_list"].items assert len(items) == 1 @@ -350,21 +377,26 @@ async def test_deprecated_api_create_fail(hass, hass_client, sl_setup): """Test the API.""" client = await hass_client() + events = async_capture_events(hass, EVENT_SHOPPING_LIST_UPDATED) resp = await client.post("/api/shopping_list/item", json={"name": 1234}) assert resp.status == HTTPStatus.BAD_REQUEST assert len(hass.data["shopping_list"].items) == 0 + assert len(events) == 0 async def test_ws_add_item(hass, hass_ws_client, sl_setup): """Test adding shopping_list item websocket command.""" client = await hass_ws_client(hass) + events = async_capture_events(hass, EVENT_SHOPPING_LIST_UPDATED) await client.send_json({"id": 5, "type": "shopping_list/items/add", "name": "soda"}) msg = await client.receive_json() assert msg["success"] is True data = msg["result"] assert data["name"] == "soda" assert data["complete"] is False + assert len(events) == 1 + items = hass.data["shopping_list"].items assert len(items) == 1 assert items[0]["name"] == "soda" @@ -374,9 +406,11 @@ async def test_ws_add_item(hass, hass_ws_client, sl_setup): async def test_ws_add_item_fail(hass, hass_ws_client, sl_setup): """Test adding shopping_list item failure websocket command.""" client = await hass_ws_client(hass) + events = async_capture_events(hass, EVENT_SHOPPING_LIST_UPDATED) await client.send_json({"id": 5, "type": "shopping_list/items/add", "name": 123}) msg = await client.receive_json() assert msg["success"] is False + assert len(events) == 0 assert len(hass.data["shopping_list"].items) == 0 @@ -397,6 +431,7 @@ async def test_ws_reorder_items(hass, hass_ws_client, sl_setup): apple_id = hass.data["shopping_list"].items[2]["id"] client = await hass_ws_client(hass) + events = async_capture_events(hass, EVENT_SHOPPING_LIST_UPDATED) await client.send_json( { "id": 6, @@ -406,6 +441,7 @@ async def test_ws_reorder_items(hass, hass_ws_client, sl_setup): ) msg = await client.receive_json() assert msg["success"] is True + assert len(events) == 1 assert hass.data["shopping_list"].items[0] == { "id": wine_id, "name": "wine", @@ -432,6 +468,7 @@ async def test_ws_reorder_items(hass, hass_ws_client, sl_setup): } ) _ = await client.receive_json() + assert len(events) == 2 await client.send_json( { @@ -442,6 +479,7 @@ async def test_ws_reorder_items(hass, hass_ws_client, sl_setup): ) msg = await client.receive_json() assert msg["success"] is True + assert len(events) == 3 assert hass.data["shopping_list"].items[0] == { "id": apple_id, "name": "apple", @@ -476,6 +514,7 @@ async def test_ws_reorder_items_failure(hass, hass_ws_client, sl_setup): apple_id = hass.data["shopping_list"].items[2]["id"] client = await hass_ws_client(hass) + events = async_capture_events(hass, EVENT_SHOPPING_LIST_UPDATED) # Testing sending bad item id. await client.send_json( @@ -488,6 +527,7 @@ async def test_ws_reorder_items_failure(hass, hass_ws_client, sl_setup): msg = await client.receive_json() assert msg["success"] is False assert msg["error"]["code"] == ERR_NOT_FOUND + assert len(events) == 0 # Testing not sending all unchecked item ids. await client.send_json( @@ -500,10 +540,12 @@ async def test_ws_reorder_items_failure(hass, hass_ws_client, sl_setup): msg = await client.receive_json() assert msg["success"] is False assert msg["error"]["code"] == ERR_INVALID_FORMAT + assert len(events) == 0 async def test_add_item_service(hass, sl_setup): """Test adding shopping_list item service.""" + events = async_capture_events(hass, EVENT_SHOPPING_LIST_UPDATED) await hass.services.async_call( DOMAIN, SERVICE_ADD_ITEM, @@ -513,10 +555,12 @@ async def test_add_item_service(hass, sl_setup): await hass.async_block_till_done() assert len(hass.data[DOMAIN].items) == 1 + assert len(events) == 1 async def test_clear_completed_items_service(hass, sl_setup): """Test clearing completed shopping_list items service.""" + events = async_capture_events(hass, EVENT_SHOPPING_LIST_UPDATED) await hass.services.async_call( DOMAIN, SERVICE_ADD_ITEM, @@ -525,7 +569,9 @@ async def test_clear_completed_items_service(hass, sl_setup): ) await hass.async_block_till_done() assert len(hass.data[DOMAIN].items) == 1 + assert len(events) == 1 + events = async_capture_events(hass, EVENT_SHOPPING_LIST_UPDATED) await hass.services.async_call( DOMAIN, SERVICE_COMPLETE_ITEM, @@ -534,7 +580,9 @@ async def test_clear_completed_items_service(hass, sl_setup): ) await hass.async_block_till_done() assert len(hass.data[DOMAIN].items) == 1 + assert len(events) == 1 + events = async_capture_events(hass, EVENT_SHOPPING_LIST_UPDATED) await hass.services.async_call( DOMAIN, SERVICE_CLEAR_COMPLETED_ITEMS, @@ -543,3 +591,4 @@ async def test_clear_completed_items_service(hass, sl_setup): ) await hass.async_block_till_done() assert len(hass.data[DOMAIN].items) == 0 + assert len(events) == 1 From 79d2c878440e2b737307bfb2d39f10e7241d86ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A5le=20Stor=C3=B8=20Hauknes?= Date: Wed, 7 Sep 2022 07:25:23 +0200 Subject: [PATCH 170/955] Mill gen 3 cloud, support precision halves for gen 3 heaters (#77932) * Update pymill to latest version * Use float with PRECISION_HALVES for Mill Gen 3 cloud connected heaters --- homeassistant/components/mill/climate.py | 5 +++-- homeassistant/components/mill/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/mill/climate.py b/homeassistant/components/mill/climate.py index fbe4ad44710..a18f9b3bafb 100644 --- a/homeassistant/components/mill/climate.py +++ b/homeassistant/components/mill/climate.py @@ -92,7 +92,6 @@ class MillHeater(CoordinatorEntity, ClimateEntity): _attr_fan_modes = [FAN_ON, FAN_OFF] _attr_max_temp = MAX_TEMP _attr_min_temp = MIN_TEMP - _attr_target_temperature_step = PRECISION_WHOLE _attr_temperature_unit = TEMP_CELSIUS def __init__(self, coordinator, heater): @@ -120,8 +119,10 @@ class MillHeater(CoordinatorEntity, ClimateEntity): self._attr_supported_features = ( ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE ) + self._attr_target_temperature_step = PRECISION_WHOLE else: self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE + self._attr_target_temperature_step = PRECISION_HALVES self._update_attr(heater) @@ -130,7 +131,7 @@ class MillHeater(CoordinatorEntity, ClimateEntity): if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: return await self.coordinator.mill_data_connection.set_heater_temp( - self._id, int(temperature) + self._id, float(temperature) ) await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/mill/manifest.json b/homeassistant/components/mill/manifest.json index c2adebae594..9a1026e4dfd 100644 --- a/homeassistant/components/mill/manifest.json +++ b/homeassistant/components/mill/manifest.json @@ -2,7 +2,7 @@ "domain": "mill", "name": "Mill", "documentation": "https://www.home-assistant.io/integrations/mill", - "requirements": ["millheater==0.9.0", "mill-local==0.1.1"], + "requirements": ["millheater==0.10.0", "mill-local==0.1.1"], "codeowners": ["@danielhiversen"], "config_flow": true, "iot_class": "local_polling", diff --git a/requirements_all.txt b/requirements_all.txt index ce54414af84..3556fc845af 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1061,7 +1061,7 @@ micloud==0.5 mill-local==0.1.1 # homeassistant.components.mill -millheater==0.9.0 +millheater==0.10.0 # homeassistant.components.minio minio==5.0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 67949a21a2d..678316b2a6c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -760,7 +760,7 @@ micloud==0.5 mill-local==0.1.1 # homeassistant.components.mill -millheater==0.9.0 +millheater==0.10.0 # homeassistant.components.minio minio==5.0.10 From fce28d4848d577d7bbb1e172757e2bb0d6a9d297 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 7 Sep 2022 01:28:47 -0400 Subject: [PATCH 171/955] Bump zwave-js-server-python to 0.41.1 (#77915) * Bump zwave-js-server-python to 0.41.1 * Fix fixture --- .../components/zwave_js/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../fixtures/climate_adc_t3000_state.json | 112 +++++------------- 4 files changed, 31 insertions(+), 87 deletions(-) diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index b906efec96c..7c569301831 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -3,7 +3,7 @@ "name": "Z-Wave", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zwave_js", - "requirements": ["pyserial==3.5", "zwave-js-server-python==0.41.0"], + "requirements": ["pyserial==3.5", "zwave-js-server-python==0.41.1"], "codeowners": ["@home-assistant/z-wave"], "dependencies": ["usb", "http", "websocket_api"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 3556fc845af..20004b123d6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2599,7 +2599,7 @@ zigpy==0.50.2 zm-py==0.5.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.41.0 +zwave-js-server-python==0.41.1 # homeassistant.components.zwave_me zwave_me_ws==0.2.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 678316b2a6c..f6da5ca40c3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1785,7 +1785,7 @@ zigpy-znp==0.8.2 zigpy==0.50.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.41.0 +zwave-js-server-python==0.41.1 # homeassistant.components.zwave_me zwave_me_ws==0.2.4 diff --git a/tests/components/zwave_js/fixtures/climate_adc_t3000_state.json b/tests/components/zwave_js/fixtures/climate_adc_t3000_state.json index ab80b46069c..b6a235ad45e 100644 --- a/tests/components/zwave_js/fixtures/climate_adc_t3000_state.json +++ b/tests/components/zwave_js/fixtures/climate_adc_t3000_state.json @@ -227,9 +227,7 @@ "unit": "\u00b0F" }, "value": 72, - "nodeId": 68, - "newValue": 73, - "prevValue": 72.5 + "nodeId": 68 }, { "endpoint": 0, @@ -250,9 +248,7 @@ "unit": "%" }, "value": 34, - "nodeId": 68, - "newValue": 34, - "prevValue": 34 + "nodeId": 68 }, { "endpoint": 0, @@ -448,9 +444,7 @@ "8": "Quiet circulation mode" } }, - "value": 0, - "newValue": 1, - "prevValue": 0 + "value": 0 }, { "endpoint": 0, @@ -581,9 +575,7 @@ "2": "De-humidifying" } }, - "value": 0, - "newValue": 1, - "prevValue": 0 + "value": 0 }, { "endpoint": 0, @@ -1295,9 +1287,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 1, - "newValue": 1, - "prevValue": 1 + "value": 1 }, { "endpoint": 0, @@ -1323,9 +1313,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 0, - "newValue": 0, - "prevValue": 0 + "value": 0 }, { "endpoint": 0, @@ -1351,9 +1339,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 1, - "newValue": 1, - "prevValue": 1 + "value": 1 }, { "endpoint": 0, @@ -1379,9 +1365,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 0, - "newValue": 0, - "prevValue": 0 + "value": 0 }, { "endpoint": 0, @@ -1407,9 +1391,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 1, - "newValue": 1, - "prevValue": 0 + "value": 1 }, { "endpoint": 0, @@ -1435,9 +1417,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 1, - "newValue": 1, - "prevValue": 1 + "value": 1 }, { "endpoint": 0, @@ -1463,9 +1443,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 0, - "newValue": 0, - "prevValue": 0 + "value": 0 }, { "endpoint": 0, @@ -1491,9 +1469,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 0, - "newValue": 0, - "prevValue": 0 + "value": 0 }, { "endpoint": 0, @@ -1519,9 +1495,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 1, - "newValue": 1, - "prevValue": 1 + "value": 1 }, { "endpoint": 0, @@ -1547,9 +1521,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 0, - "newValue": 0, - "prevValue": 0 + "value": 0 }, { "endpoint": 0, @@ -1575,9 +1547,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 1, - "newValue": 1, - "prevValue": 1 + "value": 1 }, { "endpoint": 0, @@ -1603,9 +1573,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 0, - "newValue": 0, - "prevValue": 0 + "value": 0 }, { "endpoint": 0, @@ -1631,9 +1599,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 0, - "newValue": 0, - "prevValue": 0 + "value": 0 }, { "endpoint": 0, @@ -1659,9 +1625,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 0, - "newValue": 0, - "prevValue": 0 + "value": 0 }, { "endpoint": 0, @@ -1687,9 +1651,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 0, - "newValue": 0, - "prevValue": 0 + "value": 0 }, { "endpoint": 0, @@ -1715,9 +1677,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 0, - "newValue": 0, - "prevValue": 0 + "value": 0 }, { "endpoint": 0, @@ -1743,9 +1703,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 0, - "newValue": 0, - "prevValue": 0 + "value": 0 }, { "endpoint": 0, @@ -1771,9 +1729,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 0, - "newValue": 0, - "prevValue": 0 + "value": 0 }, { "endpoint": 0, @@ -1799,9 +1755,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 0, - "newValue": 0, - "prevValue": 0 + "value": 0 }, { "endpoint": 0, @@ -1827,9 +1781,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 0, - "newValue": 0, - "prevValue": 0 + "value": 0 }, { "endpoint": 0, @@ -1855,9 +1807,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 1, - "newValue": 1, - "prevValue": 1 + "value": 1 }, { "endpoint": 0, @@ -1883,9 +1833,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 1, - "newValue": 1, - "prevValue": 1 + "value": 1 }, { "endpoint": 0, @@ -1911,9 +1859,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 0, - "newValue": 0, - "prevValue": 0 + "value": 0 }, { "endpoint": 0, @@ -1939,9 +1885,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 0, - "newValue": 0, - "prevValue": 0 + "value": 0 }, { "endpoint": 0, From a82484d7a7bb10fb1e6c635f755fc80a76ab53df Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 7 Sep 2022 09:40:52 +0200 Subject: [PATCH 172/955] Use attributes in rest base entity (#77903) --- homeassistant/components/rest/entity.py | 39 ++++++++++--------------- 1 file changed, 16 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/rest/entity.py b/homeassistant/components/rest/entity.py index 5d7a65b3d48..f0cccc8b762 100644 --- a/homeassistant/components/rest/entity.py +++ b/homeassistant/components/rest/entity.py @@ -1,10 +1,12 @@ """The base entity for the rest component.""" +from __future__ import annotations from abc import abstractmethod from typing import Any from homeassistant.core import callback from homeassistant.helpers.entity import Entity +from homeassistant.helpers.template import Template from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .data import RestData @@ -15,31 +17,22 @@ class RestEntity(Entity): def __init__( self, - coordinator: DataUpdateCoordinator[Any], + coordinator: DataUpdateCoordinator[Any] | None, rest: RestData, - resource_template, - force_update, + resource_template: Template | None, + force_update: bool, ) -> None: """Create the entity that may have a coordinator.""" - self.coordinator = coordinator + self._coordinator = coordinator self.rest = rest self._resource_template = resource_template - self._force_update = force_update + self._attr_should_poll = not coordinator + self._attr_force_update = force_update @property - def force_update(self): - """Force update.""" - return self._force_update - - @property - def should_poll(self) -> bool: - """Poll only if we do not have a coordinator.""" - return not self.coordinator - - @property - def available(self): + def available(self) -> bool: """Return the availability of this sensor.""" - if self.coordinator and not self.coordinator.last_update_success: + if self._coordinator and not self._coordinator.last_update_success: return False return self.rest.data is not None @@ -47,9 +40,9 @@ class RestEntity(Entity): """When entity is added to hass.""" await super().async_added_to_hass() self._update_from_rest_data() - if self.coordinator: + if self._coordinator: self.async_on_remove( - self.coordinator.async_add_listener(self._handle_coordinator_update) + self._coordinator.async_add_listener(self._handle_coordinator_update) ) @callback @@ -58,10 +51,10 @@ class RestEntity(Entity): self._update_from_rest_data() self.async_write_ha_state() - async def async_update(self): + async def async_update(self) -> None: """Get the latest data from REST API and update the state.""" - if self.coordinator: - await self.coordinator.async_request_refresh() + if self._coordinator: + await self._coordinator.async_request_refresh() return if self._resource_template is not None: @@ -70,5 +63,5 @@ class RestEntity(Entity): self._update_from_rest_data() @abstractmethod - def _update_from_rest_data(self): + def _update_from_rest_data(self) -> None: """Update state from the rest data.""" From 9fb0b3995cc3bf7f3fea386ec0cb5e90f28735e2 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 7 Sep 2022 09:44:15 +0200 Subject: [PATCH 173/955] Adjust pylint checks for notify get_service (#77606) Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> --- pylint/plugins/hass_enforce_type_hints.py | 42 ++++++++++++++++++----- tests/pylint/test_enforce_type_hints.py | 29 ++++++++++++++++ 2 files changed, 63 insertions(+), 8 deletions(-) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 2418b97c198..717534626af 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -4,13 +4,20 @@ from __future__ import annotations from dataclasses import dataclass from enum import Enum import re +from typing import TYPE_CHECKING from astroid import nodes +from astroid.exceptions import NameInferenceError from pylint.checkers import BaseChecker from pylint.lint import PyLinter from homeassistant.const import Platform +if TYPE_CHECKING: + # InferenceResult is available only from astroid >= 2.12.0 + # pre-commit should still work on out of date environments + from astroid.typing import InferenceResult + class _Special(Enum): """Sentinel values""" @@ -387,7 +394,8 @@ _FUNCTION_MATCH: dict[str, list[TypeHintMatch]] = { 1: "ConfigType", 2: "DiscoveryInfoType | None", }, - return_type=_Special.UNDEFINED, + return_type=["BaseNotificationService", None], + check_return_type_inheritance=True, has_async_counterpart=True, ), ], @@ -2534,21 +2542,39 @@ def _is_valid_return_type(match: TypeHintMatch, node: nodes.NodeNG) -> bool: if ( match.check_return_type_inheritance - and isinstance(match.return_type, str) + and isinstance(match.return_type, (str, list)) and isinstance(node, nodes.Name) ): - ancestor: nodes.ClassDef - for infer_node in node.infer(): - if isinstance(infer_node, nodes.ClassDef): - if infer_node.name == match.return_type: + if isinstance(match.return_type, str): + valid_types = {match.return_type} + else: + valid_types = {el for el in match.return_type if isinstance(el, str)} + + try: + for infer_node in node.infer(): + if _check_ancestry(infer_node, valid_types): return True - for ancestor in infer_node.ancestors(): - if ancestor.name == match.return_type: + except NameInferenceError: + for class_node in node.root().nodes_of_class(nodes.ClassDef): + if class_node.name != node.name: + continue + for infer_node in class_node.infer(): + if _check_ancestry(infer_node, valid_types): return True return False +def _check_ancestry(infer_node: InferenceResult, valid_types: set[str]) -> bool: + if isinstance(infer_node, nodes.ClassDef): + if infer_node.name in valid_types: + return True + for ancestor in infer_node.ancestors(): + if ancestor.name in valid_types: + return True + return False + + def _get_all_annotations(node: nodes.FunctionDef) -> list[nodes.NodeNG | None]: args = node.args annotations: list[nodes.NodeNG | None] = ( diff --git a/tests/pylint/test_enforce_type_hints.py b/tests/pylint/test_enforce_type_hints.py index ebea738edc4..9abd2c89a74 100644 --- a/tests/pylint/test_enforce_type_hints.py +++ b/tests/pylint/test_enforce_type_hints.py @@ -1011,3 +1011,32 @@ def test_vacuum_entity(linter: UnittestLinter, type_hint_checker: BaseChecker) - with assert_no_messages(linter): type_hint_checker.visit_classdef(class_node) + + +def test_notify_get_service( + linter: UnittestLinter, type_hint_checker: BaseChecker +) -> None: + """Ensure valid hints are accepted for async_get_service.""" + func_node = astroid.extract_node( + """ + class BaseNotificationService(): + pass + + async def async_get_service( #@ + hass: HomeAssistant, + config: ConfigType, + discovery_info: DiscoveryInfoType | None = None, + ) -> CustomNotificationService: + pass + + class CustomNotificationService(BaseNotificationService): + pass + """, + "homeassistant.components.pylint_test.notify", + ) + type_hint_checker.visit_module(func_node.parent) + + with assert_no_messages( + linter, + ): + type_hint_checker.visit_asyncfunctiondef(func_node) From 79b46956e9e98ce61c2a44302b24bdd493084d51 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 7 Sep 2022 03:22:19 -0500 Subject: [PATCH 174/955] Bump led-ble to 0.7.1 (#77931) --- homeassistant/components/led_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/led_ble/manifest.json b/homeassistant/components/led_ble/manifest.json index 273fbfedc04..1dd289daa4d 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/ble_ble", - "requirements": ["led-ble==0.7.0"], + "requirements": ["led-ble==0.7.1"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "bluetooth": [ diff --git a/requirements_all.txt b/requirements_all.txt index 20004b123d6..5b7ce29d9b7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -968,7 +968,7 @@ lakeside==0.12 laundrify_aio==1.1.2 # homeassistant.components.led_ble -led-ble==0.7.0 +led-ble==0.7.1 # homeassistant.components.foscam libpyfoscam==1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f6da5ca40c3..cb7d19c1af5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -706,7 +706,7 @@ lacrosse-view==0.0.9 laundrify_aio==1.1.2 # homeassistant.components.led_ble -led-ble==0.7.0 +led-ble==0.7.1 # homeassistant.components.foscam libpyfoscam==1.0 From 9fa30af8deb464bfdafabcb6f454c378579cc215 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Wed, 7 Sep 2022 10:26:07 +0200 Subject: [PATCH 175/955] Bump pyTibber to 0.25.2 (#77919) --- homeassistant/components/tibber/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tibber/manifest.json b/homeassistant/components/tibber/manifest.json index 0ac93c86668..d885bc8fbfd 100644 --- a/homeassistant/components/tibber/manifest.json +++ b/homeassistant/components/tibber/manifest.json @@ -3,7 +3,7 @@ "domain": "tibber", "name": "Tibber", "documentation": "https://www.home-assistant.io/integrations/tibber", - "requirements": ["pyTibber==0.24.0"], + "requirements": ["pyTibber==0.25.2"], "codeowners": ["@danielhiversen"], "quality_scale": "silver", "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index 5b7ce29d9b7..a03e500b7b3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1383,7 +1383,7 @@ pyRFXtrx==0.30.0 pySwitchmate==0.5.1 # homeassistant.components.tibber -pyTibber==0.24.0 +pyTibber==0.25.2 # homeassistant.components.dlink pyW215==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cb7d19c1af5..58393835133 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -980,7 +980,7 @@ pyMetno==0.9.0 pyRFXtrx==0.30.0 # homeassistant.components.tibber -pyTibber==0.24.0 +pyTibber==0.25.2 # homeassistant.components.nextbus py_nextbusnext==0.1.5 From f71313ee1e7fd1a34360a8f5d6e847b15fde0489 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 7 Sep 2022 10:58:54 +0200 Subject: [PATCH 176/955] Adjust get_scanner pylint checks (#77944) --- homeassistant/components/actiontec/device_tracker.py | 4 +++- homeassistant/components/arris_tg2492lg/device_tracker.py | 2 +- homeassistant/components/aruba/device_tracker.py | 2 +- homeassistant/components/bbox/device_tracker.py | 2 +- homeassistant/components/bt_home_hub_5/device_tracker.py | 4 +++- homeassistant/components/bt_smarthub/device_tracker.py | 2 +- homeassistant/components/cisco_ios/device_tracker.py | 2 +- .../components/cisco_mobility_express/device_tracker.py | 2 +- homeassistant/components/cppm_tracker/device_tracker.py | 2 +- homeassistant/components/ddwrt/device_tracker.py | 2 +- homeassistant/components/fortios/device_tracker.py | 2 +- homeassistant/components/hitron_coda/device_tracker.py | 4 +++- homeassistant/components/linksys_smart/device_tracker.py | 4 +++- homeassistant/components/luci/device_tracker.py | 2 +- homeassistant/components/opnsense/device_tracker.py | 6 +++++- homeassistant/components/quantum_gateway/device_tracker.py | 4 +++- homeassistant/components/sky_hub/device_tracker.py | 5 ++--- homeassistant/components/snmp/device_tracker.py | 2 +- homeassistant/components/swisscom/device_tracker.py | 4 +++- homeassistant/components/synology_srm/device_tracker.py | 4 +++- homeassistant/components/tado/device_tracker.py | 2 +- homeassistant/components/thomson/device_tracker.py | 2 +- homeassistant/components/tomato/device_tracker.py | 2 +- homeassistant/components/unifi_direct/device_tracker.py | 2 +- homeassistant/components/upc_connect/device_tracker.py | 2 +- homeassistant/components/xiaomi/device_tracker.py | 2 +- homeassistant/components/xiaomi_miio/device_tracker.py | 4 +++- pylint/plugins/hass_enforce_type_hints.py | 3 ++- 28 files changed, 50 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/actiontec/device_tracker.py b/homeassistant/components/actiontec/device_tracker.py index cc26c191c8c..9c18e2ba907 100644 --- a/homeassistant/components/actiontec/device_tracker.py +++ b/homeassistant/components/actiontec/device_tracker.py @@ -31,7 +31,9 @@ PLATFORM_SCHEMA: Final = BASE_PLATFORM_SCHEMA.extend( ) -def get_scanner(hass: HomeAssistant, config: ConfigType) -> DeviceScanner | None: +def get_scanner( + hass: HomeAssistant, config: ConfigType +) -> ActiontecDeviceScanner | None: """Validate the configuration and return an Actiontec scanner.""" scanner = ActiontecDeviceScanner(config[DOMAIN]) return scanner if scanner.success_init else None diff --git a/homeassistant/components/arris_tg2492lg/device_tracker.py b/homeassistant/components/arris_tg2492lg/device_tracker.py index 5bc157cbb6b..b456aa3f703 100644 --- a/homeassistant/components/arris_tg2492lg/device_tracker.py +++ b/homeassistant/components/arris_tg2492lg/device_tracker.py @@ -24,7 +24,7 @@ PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( ) -def get_scanner(hass: HomeAssistant, config: ConfigType) -> DeviceScanner: +def get_scanner(hass: HomeAssistant, config: ConfigType) -> ArrisDeviceScanner: """Return the Arris device scanner.""" conf = config[DOMAIN] url = f"http://{conf[CONF_HOST]}" diff --git a/homeassistant/components/aruba/device_tracker.py b/homeassistant/components/aruba/device_tracker.py index dc2d2fee8e9..d0794553b42 100644 --- a/homeassistant/components/aruba/device_tracker.py +++ b/homeassistant/components/aruba/device_tracker.py @@ -34,7 +34,7 @@ PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( ) -def get_scanner(hass: HomeAssistant, config: ConfigType) -> DeviceScanner | None: +def get_scanner(hass: HomeAssistant, config: ConfigType) -> ArubaDeviceScanner | None: """Validate the configuration and return a Aruba scanner.""" scanner = ArubaDeviceScanner(config[DOMAIN]) diff --git a/homeassistant/components/bbox/device_tracker.py b/homeassistant/components/bbox/device_tracker.py index ab8e7a72441..a9b0312673b 100644 --- a/homeassistant/components/bbox/device_tracker.py +++ b/homeassistant/components/bbox/device_tracker.py @@ -31,7 +31,7 @@ PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( ) -def get_scanner(hass: HomeAssistant, config: ConfigType) -> DeviceScanner | None: +def get_scanner(hass: HomeAssistant, config: ConfigType) -> BboxDeviceScanner | None: """Validate the configuration and return a Bbox scanner.""" scanner = BboxDeviceScanner(config[DOMAIN]) diff --git a/homeassistant/components/bt_home_hub_5/device_tracker.py b/homeassistant/components/bt_home_hub_5/device_tracker.py index 2cef6e9ba41..4d89c851245 100644 --- a/homeassistant/components/bt_home_hub_5/device_tracker.py +++ b/homeassistant/components/bt_home_hub_5/device_tracker.py @@ -25,7 +25,9 @@ PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( ) -def get_scanner(hass: HomeAssistant, config: ConfigType) -> DeviceScanner | None: +def get_scanner( + hass: HomeAssistant, config: ConfigType +) -> BTHomeHub5DeviceScanner | None: """Return a BT Home Hub 5 scanner if successful.""" scanner = BTHomeHub5DeviceScanner(config[DOMAIN]) diff --git a/homeassistant/components/bt_smarthub/device_tracker.py b/homeassistant/components/bt_smarthub/device_tracker.py index 0d50b09affa..48475bbeac9 100644 --- a/homeassistant/components/bt_smarthub/device_tracker.py +++ b/homeassistant/components/bt_smarthub/device_tracker.py @@ -30,7 +30,7 @@ PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( ) -def get_scanner(hass: HomeAssistant, config: ConfigType) -> DeviceScanner | None: +def get_scanner(hass: HomeAssistant, config: ConfigType) -> BTSmartHubScanner | None: """Return a BT Smart Hub scanner if successful.""" info = config[DOMAIN] smarthub_client = BTSmartHub( diff --git a/homeassistant/components/cisco_ios/device_tracker.py b/homeassistant/components/cisco_ios/device_tracker.py index dc9fe07aa53..b8a4d4cd53d 100644 --- a/homeassistant/components/cisco_ios/device_tracker.py +++ b/homeassistant/components/cisco_ios/device_tracker.py @@ -31,7 +31,7 @@ PLATFORM_SCHEMA = vol.All( ) -def get_scanner(hass: HomeAssistant, config: ConfigType) -> DeviceScanner | None: +def get_scanner(hass: HomeAssistant, config: ConfigType) -> CiscoDeviceScanner | None: """Validate the configuration and return a Cisco scanner.""" scanner = CiscoDeviceScanner(config[DOMAIN]) diff --git a/homeassistant/components/cisco_mobility_express/device_tracker.py b/homeassistant/components/cisco_mobility_express/device_tracker.py index d207922f6a5..a4dff37705b 100644 --- a/homeassistant/components/cisco_mobility_express/device_tracker.py +++ b/homeassistant/components/cisco_mobility_express/device_tracker.py @@ -38,7 +38,7 @@ PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( ) -def get_scanner(hass: HomeAssistant, config: ConfigType) -> DeviceScanner | None: +def get_scanner(hass: HomeAssistant, config: ConfigType) -> CiscoMEDeviceScanner | None: """Validate the configuration and return a Cisco ME scanner.""" config = config[DOMAIN] diff --git a/homeassistant/components/cppm_tracker/device_tracker.py b/homeassistant/components/cppm_tracker/device_tracker.py index 8984da6beed..0c7acd33f23 100644 --- a/homeassistant/components/cppm_tracker/device_tracker.py +++ b/homeassistant/components/cppm_tracker/device_tracker.py @@ -32,7 +32,7 @@ PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( _LOGGER = logging.getLogger(__name__) -def get_scanner(hass: HomeAssistant, config: ConfigType) -> DeviceScanner | None: +def get_scanner(hass: HomeAssistant, config: ConfigType) -> CPPMDeviceScanner | None: """Initialize Scanner.""" data = { diff --git a/homeassistant/components/ddwrt/device_tracker.py b/homeassistant/components/ddwrt/device_tracker.py index 0947b755470..ba34ec48e0f 100644 --- a/homeassistant/components/ddwrt/device_tracker.py +++ b/homeassistant/components/ddwrt/device_tracker.py @@ -46,7 +46,7 @@ PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( ) -def get_scanner(hass: HomeAssistant, config: ConfigType) -> DeviceScanner | None: +def get_scanner(hass: HomeAssistant, config: ConfigType) -> DdWrtDeviceScanner | None: """Validate the configuration and return a DD-WRT scanner.""" try: return DdWrtDeviceScanner(config[DOMAIN]) diff --git a/homeassistant/components/fortios/device_tracker.py b/homeassistant/components/fortios/device_tracker.py index d6992d8c045..d0c08a06441 100644 --- a/homeassistant/components/fortios/device_tracker.py +++ b/homeassistant/components/fortios/device_tracker.py @@ -35,7 +35,7 @@ PLATFORM_SCHEMA = BASE_PLATFORM_SCHEMA.extend( ) -def get_scanner(hass: HomeAssistant, config: ConfigType) -> DeviceScanner | None: +def get_scanner(hass: HomeAssistant, config: ConfigType) -> FortiOSDeviceScanner | None: """Validate the configuration and return a FortiOSDeviceScanner.""" host = config[DOMAIN][CONF_HOST] verify_ssl = config[DOMAIN][CONF_VERIFY_SSL] diff --git a/homeassistant/components/hitron_coda/device_tracker.py b/homeassistant/components/hitron_coda/device_tracker.py index 749347e82fe..c9ee93634b2 100644 --- a/homeassistant/components/hitron_coda/device_tracker.py +++ b/homeassistant/components/hitron_coda/device_tracker.py @@ -32,7 +32,9 @@ PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( ) -def get_scanner(_hass: HomeAssistant, config: ConfigType) -> DeviceScanner | None: +def get_scanner( + _hass: HomeAssistant, config: ConfigType +) -> HitronCODADeviceScanner | None: """Validate the configuration and return a Hitron CODA-4582U scanner.""" scanner = HitronCODADeviceScanner(config[DOMAIN]) diff --git a/homeassistant/components/linksys_smart/device_tracker.py b/homeassistant/components/linksys_smart/device_tracker.py index 8b296404532..3b0aeffaa6d 100644 --- a/homeassistant/components/linksys_smart/device_tracker.py +++ b/homeassistant/components/linksys_smart/device_tracker.py @@ -24,7 +24,9 @@ _LOGGER = logging.getLogger(__name__) PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend({vol.Required(CONF_HOST): cv.string}) -def get_scanner(hass: HomeAssistant, config: ConfigType) -> DeviceScanner | None: +def get_scanner( + hass: HomeAssistant, config: ConfigType +) -> LinksysSmartWifiDeviceScanner | None: """Validate the configuration and return a Linksys AP scanner.""" try: return LinksysSmartWifiDeviceScanner(config[DOMAIN]) diff --git a/homeassistant/components/luci/device_tracker.py b/homeassistant/components/luci/device_tracker.py index c57e235a8d2..82fc51ba5cd 100644 --- a/homeassistant/components/luci/device_tracker.py +++ b/homeassistant/components/luci/device_tracker.py @@ -38,7 +38,7 @@ PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( ) -def get_scanner(hass: HomeAssistant, config: ConfigType) -> DeviceScanner | None: +def get_scanner(hass: HomeAssistant, config: ConfigType) -> LuciDeviceScanner | None: """Validate the configuration and return a Luci scanner.""" scanner = LuciDeviceScanner(config[DOMAIN]) diff --git a/homeassistant/components/opnsense/device_tracker.py b/homeassistant/components/opnsense/device_tracker.py index e726a0484f9..b5c75f1cc21 100644 --- a/homeassistant/components/opnsense/device_tracker.py +++ b/homeassistant/components/opnsense/device_tracker.py @@ -1,4 +1,6 @@ """Device tracker support for OPNSense routers.""" +from __future__ import annotations + from homeassistant.components.device_tracker import DeviceScanner from homeassistant.core import HomeAssistant from homeassistant.helpers.typing import ConfigType @@ -6,7 +8,9 @@ from homeassistant.helpers.typing import ConfigType from . import CONF_TRACKER_INTERFACE, OPNSENSE_DATA -async def async_get_scanner(hass: HomeAssistant, config: ConfigType) -> DeviceScanner: +async def async_get_scanner( + hass: HomeAssistant, config: ConfigType +) -> OPNSenseDeviceScanner: """Configure the OPNSense device_tracker.""" interface_client = hass.data[OPNSENSE_DATA]["interfaces"] scanner = OPNSenseDeviceScanner( diff --git a/homeassistant/components/quantum_gateway/device_tracker.py b/homeassistant/components/quantum_gateway/device_tracker.py index b9be96ecaea..076c1d2722b 100644 --- a/homeassistant/components/quantum_gateway/device_tracker.py +++ b/homeassistant/components/quantum_gateway/device_tracker.py @@ -30,7 +30,9 @@ PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( ) -def get_scanner(hass: HomeAssistant, config: ConfigType) -> DeviceScanner | None: +def get_scanner( + hass: HomeAssistant, config: ConfigType +) -> QuantumGatewayDeviceScanner | None: """Validate the configuration and return a Quantum Gateway scanner.""" scanner = QuantumGatewayDeviceScanner(config[DOMAIN]) diff --git a/homeassistant/components/sky_hub/device_tracker.py b/homeassistant/components/sky_hub/device_tracker.py index 731baddcdb4..65d806a9bca 100644 --- a/homeassistant/components/sky_hub/device_tracker.py +++ b/homeassistant/components/sky_hub/device_tracker.py @@ -24,7 +24,7 @@ PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend({vol.Optional(CONF_HOST): cv.str async def async_get_scanner( hass: HomeAssistant, config: ConfigType -) -> DeviceScanner | None: +) -> SkyHubDeviceScanner | None: """Return a Sky Hub scanner if successful.""" host = config[DOMAIN].get(CONF_HOST, "192.168.1.254") websession = async_get_clientsession(hass) @@ -33,8 +33,7 @@ async def async_get_scanner( _LOGGER.debug("Initialising Sky Hub") await hub.async_connect() if hub.success_init: - scanner = SkyHubDeviceScanner(hub) - return scanner + return SkyHubDeviceScanner(hub) return None diff --git a/homeassistant/components/snmp/device_tracker.py b/homeassistant/components/snmp/device_tracker.py index c20dd58a752..696b079fd5e 100644 --- a/homeassistant/components/snmp/device_tracker.py +++ b/homeassistant/components/snmp/device_tracker.py @@ -39,7 +39,7 @@ PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( ) -def get_scanner(hass: HomeAssistant, config: ConfigType) -> DeviceScanner | None: +def get_scanner(hass: HomeAssistant, config: ConfigType) -> SnmpScanner | None: """Validate the configuration and return an SNMP scanner.""" scanner = SnmpScanner(config[DOMAIN]) diff --git a/homeassistant/components/swisscom/device_tracker.py b/homeassistant/components/swisscom/device_tracker.py index d95067e6b33..29da03b262e 100644 --- a/homeassistant/components/swisscom/device_tracker.py +++ b/homeassistant/components/swisscom/device_tracker.py @@ -26,7 +26,9 @@ PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( ) -def get_scanner(hass: HomeAssistant, config: ConfigType) -> DeviceScanner | None: +def get_scanner( + hass: HomeAssistant, config: ConfigType +) -> SwisscomDeviceScanner | None: """Return the Swisscom device scanner.""" scanner = SwisscomDeviceScanner(config[DOMAIN]) diff --git a/homeassistant/components/synology_srm/device_tracker.py b/homeassistant/components/synology_srm/device_tracker.py index 49369741218..15c61ff0a3c 100644 --- a/homeassistant/components/synology_srm/device_tracker.py +++ b/homeassistant/components/synology_srm/device_tracker.py @@ -70,7 +70,9 @@ ATTRIBUTE_ALIAS = { } -def get_scanner(hass: HomeAssistant, config: ConfigType) -> DeviceScanner | None: +def get_scanner( + hass: HomeAssistant, config: ConfigType +) -> SynologySrmDeviceScanner | None: """Validate the configuration and return Synology SRM scanner.""" scanner = SynologySrmDeviceScanner(config[DOMAIN]) diff --git a/homeassistant/components/tado/device_tracker.py b/homeassistant/components/tado/device_tracker.py index ec917877042..6d6a17b2b16 100644 --- a/homeassistant/components/tado/device_tracker.py +++ b/homeassistant/components/tado/device_tracker.py @@ -38,7 +38,7 @@ PLATFORM_SCHEMA = BASE_PLATFORM_SCHEMA.extend( ) -def get_scanner(hass: HomeAssistant, config: ConfigType) -> DeviceScanner | None: +def get_scanner(hass: HomeAssistant, config: ConfigType) -> TadoDeviceScanner | None: """Return a Tado scanner.""" scanner = TadoDeviceScanner(hass, config[DOMAIN]) return scanner if scanner.success_init else None diff --git a/homeassistant/components/thomson/device_tracker.py b/homeassistant/components/thomson/device_tracker.py index c04636e622a..4af21ec8e16 100644 --- a/homeassistant/components/thomson/device_tracker.py +++ b/homeassistant/components/thomson/device_tracker.py @@ -38,7 +38,7 @@ PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( ) -def get_scanner(hass: HomeAssistant, config: ConfigType) -> DeviceScanner | None: +def get_scanner(hass: HomeAssistant, config: ConfigType) -> ThomsonDeviceScanner | None: """Validate the configuration and return a THOMSON scanner.""" scanner = ThomsonDeviceScanner(config[DOMAIN]) diff --git a/homeassistant/components/tomato/device_tracker.py b/homeassistant/components/tomato/device_tracker.py index 2d51bb51b53..c99a44120e5 100644 --- a/homeassistant/components/tomato/device_tracker.py +++ b/homeassistant/components/tomato/device_tracker.py @@ -43,7 +43,7 @@ PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( ) -def get_scanner(hass: HomeAssistant, config: ConfigType) -> DeviceScanner: +def get_scanner(hass: HomeAssistant, config: ConfigType) -> TomatoDeviceScanner: """Validate the configuration and returns a Tomato scanner.""" return TomatoDeviceScanner(config[DOMAIN]) diff --git a/homeassistant/components/unifi_direct/device_tracker.py b/homeassistant/components/unifi_direct/device_tracker.py index 7a81975c0ba..d0ac37f312c 100644 --- a/homeassistant/components/unifi_direct/device_tracker.py +++ b/homeassistant/components/unifi_direct/device_tracker.py @@ -34,7 +34,7 @@ PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( ) -def get_scanner(hass: HomeAssistant, config: ConfigType) -> DeviceScanner | None: +def get_scanner(hass: HomeAssistant, config: ConfigType) -> UnifiDeviceScanner | None: """Validate the configuration and return a Unifi direct scanner.""" scanner = UnifiDeviceScanner(config[DOMAIN]) if not scanner.connected: diff --git a/homeassistant/components/upc_connect/device_tracker.py b/homeassistant/components/upc_connect/device_tracker.py index 35960dcf706..3025ea746d0 100644 --- a/homeassistant/components/upc_connect/device_tracker.py +++ b/homeassistant/components/upc_connect/device_tracker.py @@ -32,7 +32,7 @@ PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( async def async_get_scanner( hass: HomeAssistant, config: ConfigType -) -> DeviceScanner | None: +) -> UPCDeviceScanner | None: """Return the UPC device scanner.""" conf = config[DOMAIN] session = async_get_clientsession(hass) diff --git a/homeassistant/components/xiaomi/device_tracker.py b/homeassistant/components/xiaomi/device_tracker.py index f941dba2d01..b8cf5f005c4 100644 --- a/homeassistant/components/xiaomi/device_tracker.py +++ b/homeassistant/components/xiaomi/device_tracker.py @@ -28,7 +28,7 @@ PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( ) -def get_scanner(hass: HomeAssistant, config: ConfigType) -> DeviceScanner | None: +def get_scanner(hass: HomeAssistant, config: ConfigType) -> XiaomiDeviceScanner | None: """Validate the configuration and return a Xiaomi Device Scanner.""" scanner = XiaomiDeviceScanner(config[DOMAIN]) diff --git a/homeassistant/components/xiaomi_miio/device_tracker.py b/homeassistant/components/xiaomi_miio/device_tracker.py index 1ec914fc647..e4bebdd0e62 100644 --- a/homeassistant/components/xiaomi_miio/device_tracker.py +++ b/homeassistant/components/xiaomi_miio/device_tracker.py @@ -26,7 +26,9 @@ PLATFORM_SCHEMA = BASE_PLATFORM_SCHEMA.extend( ) -def get_scanner(hass: HomeAssistant, config: ConfigType) -> DeviceScanner | None: +def get_scanner( + hass: HomeAssistant, config: ConfigType +) -> XiaomiMiioDeviceScanner | None: """Return a Xiaomi MiIO device scanner.""" scanner = None host = config[DOMAIN][CONF_HOST] diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 717534626af..23578630b7e 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -327,7 +327,8 @@ _FUNCTION_MATCH: dict[str, list[TypeHintMatch]] = { 0: "HomeAssistant", 1: "ConfigType", }, - return_type=["DeviceScanner", "DeviceScanner | None"], + return_type=["DeviceScanner", None], + check_return_type_inheritance=True, has_async_counterpart=True, ), ], From e42c48ebca2dceddf827a1ced013043aa14d3461 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 7 Sep 2022 06:12:17 -0500 Subject: [PATCH 177/955] Bump PySwitchbot to 0.18.25 (#77935) --- homeassistant/components/switchbot/__init__.py | 2 ++ homeassistant/components/switchbot/const.py | 2 ++ homeassistant/components/switchbot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switchbot/__init__.py b/homeassistant/components/switchbot/__init__.py index 59ed071f325..345190d8933 100644 --- a/homeassistant/components/switchbot/__init__.py +++ b/homeassistant/components/switchbot/__init__.py @@ -31,6 +31,7 @@ from .coordinator import SwitchbotDataUpdateCoordinator PLATFORMS_BY_TYPE = { SupportedModels.BULB.value: [Platform.SENSOR, Platform.LIGHT], SupportedModels.LIGHT_STRIP.value: [Platform.SENSOR, Platform.LIGHT], + SupportedModels.CEILING_LIGHT.value: [Platform.SENSOR, Platform.LIGHT], SupportedModels.BOT.value: [Platform.SWITCH, Platform.SENSOR], SupportedModels.PLUG.value: [Platform.SWITCH, Platform.SENSOR], SupportedModels.CURTAIN.value: [ @@ -43,6 +44,7 @@ PLATFORMS_BY_TYPE = { SupportedModels.MOTION.value: [Platform.BINARY_SENSOR, Platform.SENSOR], } CLASS_BY_DEVICE = { + SupportedModels.CEILING_LIGHT.value: switchbot.SwitchbotCeilingLight, SupportedModels.CURTAIN.value: switchbot.SwitchbotCurtain, SupportedModels.BOT.value: switchbot.Switchbot, SupportedModels.PLUG.value: switchbot.SwitchbotPlugMini, diff --git a/homeassistant/components/switchbot/const.py b/homeassistant/components/switchbot/const.py index aa334120b85..ecd86e1bef5 100644 --- a/homeassistant/components/switchbot/const.py +++ b/homeassistant/components/switchbot/const.py @@ -16,6 +16,7 @@ class SupportedModels(StrEnum): BOT = "bot" BULB = "bulb" + CEILING_LIGHT = "ceiling_light" CURTAIN = "curtain" HYGROMETER = "hygrometer" LIGHT_STRIP = "light_strip" @@ -30,6 +31,7 @@ CONNECTABLE_SUPPORTED_MODEL_TYPES = { SwitchbotModel.PLUG_MINI: SupportedModels.PLUG, SwitchbotModel.COLOR_BULB: SupportedModels.BULB, SwitchbotModel.LIGHT_STRIP: SupportedModels.LIGHT_STRIP, + SwitchbotModel.CEILING_LIGHT: SupportedModels.CEILING_LIGHT, } NON_CONNECTABLE_SUPPORTED_MODEL_TYPES = { diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index 9fb73a62dd6..040e76391bd 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.18.22"], + "requirements": ["PySwitchbot==0.18.25"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index a03e500b7b3..83baf69949c 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.18.22 +PySwitchbot==0.18.25 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 58393835133..33a3f57bd70 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.18.22 +PySwitchbot==0.18.25 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From a0071e7877a3b70ba1297a40d41f2a7612b3e67b Mon Sep 17 00:00:00 2001 From: kingy444 Date: Wed, 7 Sep 2022 21:17:45 +1000 Subject: [PATCH 178/955] Bump aiopvapi to 2.0.1 (#77949) --- homeassistant/components/hunterdouglas_powerview/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hunterdouglas_powerview/manifest.json b/homeassistant/components/hunterdouglas_powerview/manifest.json index d3711313c1d..2dbec2aba1c 100644 --- a/homeassistant/components/hunterdouglas_powerview/manifest.json +++ b/homeassistant/components/hunterdouglas_powerview/manifest.json @@ -2,7 +2,7 @@ "domain": "hunterdouglas_powerview", "name": "Hunter Douglas PowerView", "documentation": "https://www.home-assistant.io/integrations/hunterdouglas_powerview", - "requirements": ["aiopvapi==2.0.0"], + "requirements": ["aiopvapi==2.0.1"], "codeowners": ["@bdraco", "@kingy444", "@trullock"], "config_flow": true, "homekit": { diff --git a/requirements_all.txt b/requirements_all.txt index 83baf69949c..2fe8c9b8a53 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -226,7 +226,7 @@ aioopenexchangerates==0.4.0 aiopulse==0.4.3 # homeassistant.components.hunterdouglas_powerview -aiopvapi==2.0.0 +aiopvapi==2.0.1 # homeassistant.components.pvpc_hourly_pricing aiopvpc==3.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 33a3f57bd70..d575459abce 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -201,7 +201,7 @@ aioopenexchangerates==0.4.0 aiopulse==0.4.3 # homeassistant.components.hunterdouglas_powerview -aiopvapi==2.0.0 +aiopvapi==2.0.1 # homeassistant.components.pvpc_hourly_pricing aiopvpc==3.0.0 From 01f1629ac0763a1634a3dc001dd84a5ed92a461f Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 7 Sep 2022 15:13:51 +0200 Subject: [PATCH 179/955] Update surepy to 0.8.0 (#77948) --- homeassistant/components/surepetcare/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- script/pip_check | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/surepetcare/manifest.json b/homeassistant/components/surepetcare/manifest.json index 6368bab898e..c6629cb7d24 100644 --- a/homeassistant/components/surepetcare/manifest.json +++ b/homeassistant/components/surepetcare/manifest.json @@ -3,7 +3,7 @@ "name": "Sure Petcare", "documentation": "https://www.home-assistant.io/integrations/surepetcare", "codeowners": ["@benleb", "@danielhiversen"], - "requirements": ["surepy==0.7.2"], + "requirements": ["surepy==0.8.0"], "iot_class": "cloud_polling", "config_flow": true, "loggers": ["rich", "surepy"] diff --git a/requirements_all.txt b/requirements_all.txt index 2fe8c9b8a53..4e49ffccfb9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2318,7 +2318,7 @@ subarulink==0.5.0 sunwatcher==0.2.1 # homeassistant.components.surepetcare -surepy==0.7.2 +surepy==0.8.0 # homeassistant.components.swiss_hydrological_data swisshydrodata==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d575459abce..214b4d0988b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1594,7 +1594,7 @@ subarulink==0.5.0 sunwatcher==0.2.1 # homeassistant.components.surepetcare -surepy==0.7.2 +surepy==0.8.0 # homeassistant.components.system_bridge systembridgeconnector==3.4.4 diff --git a/script/pip_check b/script/pip_check index f29ea5a5dd0..5b69e1569c6 100755 --- a/script/pip_check +++ b/script/pip_check @@ -3,7 +3,7 @@ PIP_CACHE=$1 # Number of existing dependency conflicts # Update if a PR resolve one! -DEPENDENCY_CONFLICTS=6 +DEPENDENCY_CONFLICTS=5 PIP_CHECK=$(pip check --cache-dir=$PIP_CACHE) LINE_COUNT=$(echo "$PIP_CHECK" | wc -l) From 9490771a8737892a7a86afd866a3520b836779fd Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 7 Sep 2022 16:18:00 +0200 Subject: [PATCH 180/955] Refactor distance, speed and volume utils (#77952) * Refactor distance util * Fix bmw connected drive tests * Adjust here travel time tests * Adjust waze travel time tests * Adjust test_distance * Adjust rounding values * Adjust more tests * Adjust volume conversions * Add tests --- homeassistant/util/distance.py | 43 ++-- homeassistant/util/speed.py | 19 +- homeassistant/util/volume.py | 41 ++-- .../bmw_connected_drive/test_sensor.py | 2 +- .../here_travel_time/test_sensor.py | 14 +- .../waze_travel_time/test_sensor.py | 4 +- tests/util/test_distance.py | 185 +++++++++++------- tests/util/test_unit_system.py | 8 +- tests/util/test_volume.py | 63 +++++- 9 files changed, 248 insertions(+), 131 deletions(-) diff --git a/homeassistant/util/distance.py b/homeassistant/util/distance.py index 38b9253ffbf..4a7445c46ed 100644 --- a/homeassistant/util/distance.py +++ b/homeassistant/util/distance.py @@ -1,7 +1,6 @@ """Distance util functions.""" from __future__ import annotations -from collections.abc import Callable from numbers import Number from homeassistant.const import ( @@ -28,26 +27,26 @@ VALID_UNITS: tuple[str, ...] = ( LENGTH_YARD, ) -TO_METERS: dict[str, Callable[[float], float]] = { - LENGTH_METERS: lambda meters: meters, - LENGTH_MILES: lambda miles: miles * 1609.344, - LENGTH_YARD: lambda yards: yards * 0.9144, - LENGTH_FEET: lambda feet: feet * 0.3048, - LENGTH_INCHES: lambda inches: inches * 0.0254, - LENGTH_KILOMETERS: lambda kilometers: kilometers * 1000, - LENGTH_CENTIMETERS: lambda centimeters: centimeters * 0.01, - LENGTH_MILLIMETERS: lambda millimeters: millimeters * 0.001, -} +MM_TO_M = 0.001 # 1 mm = 0.001 m +CM_TO_M = 0.01 # 1 cm = 0.01 m +KM_TO_M = 1000 # 1 km = 1000 m -METERS_TO: dict[str, Callable[[float], float]] = { - LENGTH_METERS: lambda meters: meters, - LENGTH_MILES: lambda meters: meters * 0.000621371, - LENGTH_YARD: lambda meters: meters * 1.09361, - LENGTH_FEET: lambda meters: meters * 3.28084, - LENGTH_INCHES: lambda meters: meters * 39.3701, - LENGTH_KILOMETERS: lambda meters: meters * 0.001, - LENGTH_CENTIMETERS: lambda meters: meters * 100, - LENGTH_MILLIMETERS: lambda meters: meters * 1000, +IN_TO_M = 0.0254 # 1 inch = 0.0254 m +FOOT_TO_M = IN_TO_M * 12 # 12 inches = 1 foot (0.3048 m) +YARD_TO_M = FOOT_TO_M * 3 # 3 feet = 1 yard (0.9144 m) +MILE_TO_M = YARD_TO_M * 1760 # 1760 yard = 1 mile (1609.344 m) + +NAUTICAL_MILE_TO_M = 1852 # 1 nautical mile = 1852 m + +UNIT_CONVERSION: dict[str, float] = { + LENGTH_METERS: 1, + LENGTH_MILLIMETERS: 1 / MM_TO_M, + LENGTH_CENTIMETERS: 1 / CM_TO_M, + LENGTH_KILOMETERS: 1 / KM_TO_M, + LENGTH_INCHES: 1 / IN_TO_M, + LENGTH_FEET: 1 / FOOT_TO_M, + LENGTH_YARD: 1 / YARD_TO_M, + LENGTH_MILES: 1 / MILE_TO_M, } @@ -64,6 +63,6 @@ def convert(value: float, unit_1: str, unit_2: str) -> float: if unit_1 == unit_2 or unit_1 not in VALID_UNITS: return value - meters: float = TO_METERS[unit_1](value) + meters: float = value / UNIT_CONVERSION[unit_1] - return METERS_TO[unit_2](meters) + return meters * UNIT_CONVERSION[unit_2] diff --git a/homeassistant/util/speed.py b/homeassistant/util/speed.py index 12618e020f8..823c65b59b0 100644 --- a/homeassistant/util/speed.py +++ b/homeassistant/util/speed.py @@ -16,6 +16,15 @@ from homeassistant.const import ( UNIT_NOT_RECOGNIZED_TEMPLATE, ) +from .distance import ( + FOOT_TO_M, + IN_TO_M, + KM_TO_M, + MILE_TO_M, + MM_TO_M, + NAUTICAL_MILE_TO_M, +) + VALID_UNITS: tuple[str, ...] = ( SPEED_FEET_PER_SECOND, SPEED_INCHES_PER_DAY, @@ -27,23 +36,19 @@ VALID_UNITS: tuple[str, ...] = ( SPEED_MILLIMETERS_PER_DAY, ) -FOOT_TO_M = 0.3048 HRS_TO_SECS = 60 * 60 # 1 hr = 3600 seconds -IN_TO_M = 0.0254 -KM_TO_M = 1000 # 1 km = 1000 m -MILE_TO_M = 1609.344 -NAUTICAL_MILE_TO_M = 1852 # 1 nautical mile = 1852 m +DAYS_TO_SECS = 24 * HRS_TO_SECS # 1 day = 24 hours = 86400 seconds # Units in terms of m/s UNIT_CONVERSION: dict[str, float] = { SPEED_FEET_PER_SECOND: 1 / FOOT_TO_M, - SPEED_INCHES_PER_DAY: (24 * HRS_TO_SECS) / IN_TO_M, + SPEED_INCHES_PER_DAY: DAYS_TO_SECS / IN_TO_M, SPEED_INCHES_PER_HOUR: HRS_TO_SECS / IN_TO_M, SPEED_KILOMETERS_PER_HOUR: HRS_TO_SECS / KM_TO_M, SPEED_KNOTS: HRS_TO_SECS / NAUTICAL_MILE_TO_M, SPEED_METERS_PER_SECOND: 1, SPEED_MILES_PER_HOUR: HRS_TO_SECS / MILE_TO_M, - SPEED_MILLIMETERS_PER_DAY: (24 * HRS_TO_SECS) * 1000, + SPEED_MILLIMETERS_PER_DAY: DAYS_TO_SECS / MM_TO_M, } diff --git a/homeassistant/util/volume.py b/homeassistant/util/volume.py index 84a3faa0951..73e89a064a2 100644 --- a/homeassistant/util/volume.py +++ b/homeassistant/util/volume.py @@ -14,6 +14,8 @@ from homeassistant.const import ( VOLUME_MILLILITERS, ) +from .distance import FOOT_TO_M, IN_TO_M + VALID_UNITS: tuple[str, ...] = ( VOLUME_LITERS, VOLUME_MILLILITERS, @@ -23,25 +25,41 @@ VALID_UNITS: tuple[str, ...] = ( VOLUME_CUBIC_FEET, ) +ML_TO_L = 0.001 # 1 mL = 0.001 L +CUBIC_METER_TO_L = 1000 # 1 m3 = 1000 L +GALLON_TO_L = 231 * pow(IN_TO_M, 3) * CUBIC_METER_TO_L # US gallon is 231 cubic inches +FLUID_OUNCE_TO_L = GALLON_TO_L / 128 # 128 fluid ounces in a US gallon +CUBIC_FOOT_TO_L = CUBIC_METER_TO_L * pow(FOOT_TO_M, 3) + +# Units in terms of L +UNIT_CONVERSION: dict[str, float] = { + VOLUME_LITERS: 1, + VOLUME_MILLILITERS: 1 / ML_TO_L, + VOLUME_GALLONS: 1 / GALLON_TO_L, + VOLUME_FLUID_OUNCE: 1 / FLUID_OUNCE_TO_L, + VOLUME_CUBIC_METERS: 1 / CUBIC_METER_TO_L, + VOLUME_CUBIC_FEET: 1 / CUBIC_FOOT_TO_L, +} + def liter_to_gallon(liter: float) -> float: """Convert a volume measurement in Liter to Gallon.""" - return liter * 0.2642 + return _convert(liter, VOLUME_LITERS, VOLUME_GALLONS) def gallon_to_liter(gallon: float) -> float: """Convert a volume measurement in Gallon to Liter.""" - return gallon * 3.785 + return _convert(gallon, VOLUME_GALLONS, VOLUME_LITERS) def cubic_meter_to_cubic_feet(cubic_meter: float) -> float: """Convert a volume measurement in cubic meter to cubic feet.""" - return cubic_meter * 35.3146667 + return _convert(cubic_meter, VOLUME_CUBIC_METERS, VOLUME_CUBIC_FEET) def cubic_feet_to_cubic_meter(cubic_feet: float) -> float: """Convert a volume measurement in cubic feet to cubic meter.""" - return cubic_feet * 0.0283168466 + return _convert(cubic_feet, VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS) def convert(volume: float, from_unit: str, to_unit: str) -> float: @@ -56,15 +74,10 @@ def convert(volume: float, from_unit: str, to_unit: str) -> float: if from_unit == to_unit: return volume + return _convert(volume, from_unit, to_unit) - result: float = volume - if from_unit == VOLUME_LITERS and to_unit == VOLUME_GALLONS: - result = liter_to_gallon(volume) - elif from_unit == VOLUME_GALLONS and to_unit == VOLUME_LITERS: - result = gallon_to_liter(volume) - elif from_unit == VOLUME_CUBIC_METERS and to_unit == VOLUME_CUBIC_FEET: - result = cubic_meter_to_cubic_feet(volume) - elif from_unit == VOLUME_CUBIC_FEET and to_unit == VOLUME_CUBIC_METERS: - result = cubic_feet_to_cubic_meter(volume) - return result +def _convert(volume: float, from_unit: str, to_unit: str) -> float: + """Convert a temperature from one unit to another, bypassing checks.""" + liters = volume / UNIT_CONVERSION[from_unit] + return liters * UNIT_CONVERSION[to_unit] diff --git a/tests/components/bmw_connected_drive/test_sensor.py b/tests/components/bmw_connected_drive/test_sensor.py index cb1299a274b..90b3f725a6d 100644 --- a/tests/components/bmw_connected_drive/test_sensor.py +++ b/tests/components/bmw_connected_drive/test_sensor.py @@ -17,7 +17,7 @@ from . import setup_mocked_integration ("sensor.i3_rex_remaining_range_total", METRIC, "279", "km"), ("sensor.i3_rex_remaining_range_total", IMPERIAL, "173.36", "mi"), ("sensor.i3_rex_mileage", METRIC, "137009", "km"), - ("sensor.i3_rex_mileage", IMPERIAL, "85133.42", "mi"), + ("sensor.i3_rex_mileage", IMPERIAL, "85133.45", "mi"), ("sensor.i3_rex_remaining_battery_percent", METRIC, "82", "%"), ("sensor.i3_rex_remaining_battery_percent", IMPERIAL, "82", "%"), ("sensor.i3_rex_remaining_range_electric", METRIC, "174", "km"), diff --git a/tests/components/here_travel_time/test_sensor.py b/tests/components/here_travel_time/test_sensor.py index 93fbd1bd204..70669a63da5 100644 --- a/tests/components/here_travel_time/test_sensor.py +++ b/tests/components/here_travel_time/test_sensor.py @@ -67,7 +67,7 @@ from tests.common import MockConfigEntry None, None, "30", - "23.903", + 23.903, "31", ), ( @@ -77,7 +77,7 @@ from tests.common import MockConfigEntry None, None, "30", - "23.903", + 23.903, "30", ), ( @@ -87,7 +87,7 @@ from tests.common import MockConfigEntry None, None, "30", - "14.852631013", + 14.85263, "30", ), ( @@ -97,7 +97,7 @@ from tests.common import MockConfigEntry "08:00:00", None, "30", - "14.852631013", + 14.85263, "30", ), ( @@ -107,7 +107,7 @@ from tests.common import MockConfigEntry None, "08:00:00", "30", - "23.903", + 23.903, "31", ), ], @@ -163,7 +163,9 @@ async def test_sensor( hass.states.get("sensor.test_duration_in_traffic").state == expected_duration_in_traffic ) - assert hass.states.get("sensor.test_distance").state == expected_distance + assert float(hass.states.get("sensor.test_distance").state) == pytest.approx( + expected_distance + ) assert hass.states.get("sensor.test_route").state == ( "US-29 - K St NW; US-29 - Whitehurst Fwy; " "I-495 N - Capital Beltway; MD-187 S - Old Georgetown Rd" diff --git a/tests/components/waze_travel_time/test_sensor.py b/tests/components/waze_travel_time/test_sensor.py index 2b28190e430..67ba7c6e311 100644 --- a/tests/components/waze_travel_time/test_sensor.py +++ b/tests/components/waze_travel_time/test_sensor.py @@ -97,7 +97,9 @@ async def test_sensor(hass): @pytest.mark.usefixtures("mock_update", "mock_config") async def test_imperial(hass): """Test that the imperial option works.""" - assert hass.states.get("sensor.waze_travel_time").attributes["distance"] == 186.4113 + assert hass.states.get("sensor.waze_travel_time").attributes[ + "distance" + ] == pytest.approx(186.4113) @pytest.mark.usefixtures("mock_update_wrcerror") diff --git a/tests/util/test_distance.py b/tests/util/test_distance.py index e1cf7ae0f6a..eb001add114 100644 --- a/tests/util/test_distance.py +++ b/tests/util/test_distance.py @@ -45,102 +45,149 @@ def test_convert_nonnumeric_value(): distance_util.convert("a", LENGTH_KILOMETERS, LENGTH_METERS) -def test_convert_from_miles(): +@pytest.mark.parametrize( + "unit,expected", + [ + (LENGTH_KILOMETERS, 8.04672), + (LENGTH_METERS, 8046.72), + (LENGTH_CENTIMETERS, 804672.0), + (LENGTH_MILLIMETERS, 8046720.0), + (LENGTH_YARD, 8800.0), + (LENGTH_FEET, 26400.0008448), + (LENGTH_INCHES, 316800.171072), + ], +) +def test_convert_from_miles(unit, expected): """Test conversion from miles to other units.""" miles = 5 - assert distance_util.convert(miles, LENGTH_MILES, LENGTH_KILOMETERS) == 8.04672 - assert distance_util.convert(miles, LENGTH_MILES, LENGTH_METERS) == 8046.72 - assert distance_util.convert(miles, LENGTH_MILES, LENGTH_CENTIMETERS) == 804672.0 - assert distance_util.convert(miles, LENGTH_MILES, LENGTH_MILLIMETERS) == 8046720.0 - assert distance_util.convert(miles, LENGTH_MILES, LENGTH_YARD) == 8799.9734592 - assert distance_util.convert(miles, LENGTH_MILES, LENGTH_FEET) == 26400.0008448 - assert distance_util.convert(miles, LENGTH_MILES, LENGTH_INCHES) == 316800.171072 + assert distance_util.convert(miles, LENGTH_MILES, unit) == pytest.approx(expected) -def test_convert_from_yards(): +@pytest.mark.parametrize( + "unit,expected", + [ + (LENGTH_KILOMETERS, 0.0045720000000000005), + (LENGTH_METERS, 4.572), + (LENGTH_CENTIMETERS, 457.2), + (LENGTH_MILLIMETERS, 4572), + (LENGTH_MILES, 0.002840908212), + (LENGTH_FEET, 15.00000048), + (LENGTH_INCHES, 180.0000972), + ], +) +def test_convert_from_yards(unit, expected): """Test conversion from yards to other units.""" yards = 5 - assert ( - distance_util.convert(yards, LENGTH_YARD, LENGTH_KILOMETERS) - == 0.0045720000000000005 - ) - assert distance_util.convert(yards, LENGTH_YARD, LENGTH_METERS) == 4.572 - assert distance_util.convert(yards, LENGTH_YARD, LENGTH_CENTIMETERS) == 457.2 - assert distance_util.convert(yards, LENGTH_YARD, LENGTH_MILLIMETERS) == 4572.0 - assert distance_util.convert(yards, LENGTH_YARD, LENGTH_MILES) == 0.002840908212 - assert distance_util.convert(yards, LENGTH_YARD, LENGTH_FEET) == 15.00000048 - assert distance_util.convert(yards, LENGTH_YARD, LENGTH_INCHES) == 180.0000972 + assert distance_util.convert(yards, LENGTH_YARD, unit) == pytest.approx(expected) -def test_convert_from_feet(): +@pytest.mark.parametrize( + "unit,expected", + [ + (LENGTH_KILOMETERS, 1.524), + (LENGTH_METERS, 1524), + (LENGTH_CENTIMETERS, 152400.0), + (LENGTH_MILLIMETERS, 1524000.0), + (LENGTH_MILES, 0.9469694040000001), + (LENGTH_YARD, 1666.66667), + (LENGTH_INCHES, 60000.032400000004), + ], +) +def test_convert_from_feet(unit, expected): """Test conversion from feet to other units.""" feet = 5000 - assert distance_util.convert(feet, LENGTH_FEET, LENGTH_KILOMETERS) == 1.524 - assert distance_util.convert(feet, LENGTH_FEET, LENGTH_METERS) == 1524 - assert distance_util.convert(feet, LENGTH_FEET, LENGTH_CENTIMETERS) == 152400.0 - assert distance_util.convert(feet, LENGTH_FEET, LENGTH_MILLIMETERS) == 1524000.0 - assert distance_util.convert(feet, LENGTH_FEET, LENGTH_MILES) == 0.9469694040000001 - assert distance_util.convert(feet, LENGTH_FEET, LENGTH_YARD) == 1666.66164 - assert distance_util.convert(feet, LENGTH_FEET, LENGTH_INCHES) == 60000.032400000004 + assert distance_util.convert(feet, LENGTH_FEET, unit) == pytest.approx(expected) -def test_convert_from_inches(): +@pytest.mark.parametrize( + "unit,expected", + [ + (LENGTH_KILOMETERS, 0.127), + (LENGTH_METERS, 127.0), + (LENGTH_CENTIMETERS, 12700.0), + (LENGTH_MILLIMETERS, 127000.0), + (LENGTH_MILES, 0.078914117), + (LENGTH_YARD, 138.88889), + (LENGTH_FEET, 416.66668), + ], +) +def test_convert_from_inches(unit, expected): """Test conversion from inches to other units.""" inches = 5000 - assert distance_util.convert(inches, LENGTH_INCHES, LENGTH_KILOMETERS) == 0.127 - assert distance_util.convert(inches, LENGTH_INCHES, LENGTH_METERS) == 127.0 - assert distance_util.convert(inches, LENGTH_INCHES, LENGTH_CENTIMETERS) == 12700.0 - assert distance_util.convert(inches, LENGTH_INCHES, LENGTH_MILLIMETERS) == 127000.0 - assert distance_util.convert(inches, LENGTH_INCHES, LENGTH_MILES) == 0.078914117 - assert ( - distance_util.convert(inches, LENGTH_INCHES, LENGTH_YARD) == 138.88846999999998 - ) - assert distance_util.convert(inches, LENGTH_INCHES, LENGTH_FEET) == 416.66668 + assert distance_util.convert(inches, LENGTH_INCHES, unit) == pytest.approx(expected) -def test_convert_from_kilometers(): +@pytest.mark.parametrize( + "unit,expected", + [ + (LENGTH_METERS, 5000), + (LENGTH_CENTIMETERS, 500000), + (LENGTH_MILLIMETERS, 5000000), + (LENGTH_MILES, 3.106855), + (LENGTH_YARD, 5468.066), + (LENGTH_FEET, 16404.2), + (LENGTH_INCHES, 196850.5), + ], +) +def test_convert_from_kilometers(unit, expected): """Test conversion from kilometers to other units.""" km = 5 - assert distance_util.convert(km, LENGTH_KILOMETERS, LENGTH_METERS) == 5000 - assert distance_util.convert(km, LENGTH_KILOMETERS, LENGTH_CENTIMETERS) == 500000 - assert distance_util.convert(km, LENGTH_KILOMETERS, LENGTH_MILLIMETERS) == 5000000 - assert distance_util.convert(km, LENGTH_KILOMETERS, LENGTH_MILES) == 3.106855 - assert distance_util.convert(km, LENGTH_KILOMETERS, LENGTH_YARD) == 5468.05 - assert distance_util.convert(km, LENGTH_KILOMETERS, LENGTH_FEET) == 16404.2 - assert distance_util.convert(km, LENGTH_KILOMETERS, LENGTH_INCHES) == 196850.5 + assert distance_util.convert(km, LENGTH_KILOMETERS, unit) == pytest.approx(expected) -def test_convert_from_meters(): +@pytest.mark.parametrize( + "unit,expected", + [ + (LENGTH_KILOMETERS, 5), + (LENGTH_CENTIMETERS, 500000), + (LENGTH_MILLIMETERS, 5000000), + (LENGTH_MILES, 3.106855), + (LENGTH_YARD, 5468.066), + (LENGTH_FEET, 16404.2), + (LENGTH_INCHES, 196850.5), + ], +) +def test_convert_from_meters(unit, expected): """Test conversion from meters to other units.""" m = 5000 - assert distance_util.convert(m, LENGTH_METERS, LENGTH_KILOMETERS) == 5 - assert distance_util.convert(m, LENGTH_METERS, LENGTH_CENTIMETERS) == 500000 - assert distance_util.convert(m, LENGTH_METERS, LENGTH_MILLIMETERS) == 5000000 - assert distance_util.convert(m, LENGTH_METERS, LENGTH_MILES) == 3.106855 - assert distance_util.convert(m, LENGTH_METERS, LENGTH_YARD) == 5468.05 - assert distance_util.convert(m, LENGTH_METERS, LENGTH_FEET) == 16404.2 - assert distance_util.convert(m, LENGTH_METERS, LENGTH_INCHES) == 196850.5 + assert distance_util.convert(m, LENGTH_METERS, unit) == pytest.approx(expected) -def test_convert_from_centimeters(): +@pytest.mark.parametrize( + "unit,expected", + [ + (LENGTH_KILOMETERS, 5), + (LENGTH_METERS, 5000), + (LENGTH_MILLIMETERS, 5000000), + (LENGTH_MILES, 3.106855), + (LENGTH_YARD, 5468.066), + (LENGTH_FEET, 16404.2), + (LENGTH_INCHES, 196850.5), + ], +) +def test_convert_from_centimeters(unit, expected): """Test conversion from centimeters to other units.""" cm = 500000 - assert distance_util.convert(cm, LENGTH_CENTIMETERS, LENGTH_KILOMETERS) == 5 - assert distance_util.convert(cm, LENGTH_CENTIMETERS, LENGTH_METERS) == 5000 - assert distance_util.convert(cm, LENGTH_CENTIMETERS, LENGTH_MILLIMETERS) == 5000000 - assert distance_util.convert(cm, LENGTH_CENTIMETERS, LENGTH_MILES) == 3.106855 - assert distance_util.convert(cm, LENGTH_CENTIMETERS, LENGTH_YARD) == 5468.05 - assert distance_util.convert(cm, LENGTH_CENTIMETERS, LENGTH_FEET) == 16404.2 - assert distance_util.convert(cm, LENGTH_CENTIMETERS, LENGTH_INCHES) == 196850.5 + assert distance_util.convert(cm, LENGTH_CENTIMETERS, unit) == pytest.approx( + expected + ) -def test_convert_from_millimeters(): +@pytest.mark.parametrize( + "unit,expected", + [ + (LENGTH_KILOMETERS, 5), + (LENGTH_METERS, 5000), + (LENGTH_CENTIMETERS, 500000), + (LENGTH_MILES, 3.106855), + (LENGTH_YARD, 5468.066), + (LENGTH_FEET, 16404.2), + (LENGTH_INCHES, 196850.5), + ], +) +def test_convert_from_millimeters(unit, expected): """Test conversion from millimeters to other units.""" mm = 5000000 - assert distance_util.convert(mm, LENGTH_MILLIMETERS, LENGTH_KILOMETERS) == 5 - assert distance_util.convert(mm, LENGTH_MILLIMETERS, LENGTH_METERS) == 5000 - assert distance_util.convert(mm, LENGTH_MILLIMETERS, LENGTH_CENTIMETERS) == 500000 - assert distance_util.convert(mm, LENGTH_MILLIMETERS, LENGTH_MILES) == 3.106855 - assert distance_util.convert(mm, LENGTH_MILLIMETERS, LENGTH_YARD) == 5468.05 - assert distance_util.convert(mm, LENGTH_MILLIMETERS, LENGTH_FEET) == 16404.2 - assert distance_util.convert(mm, LENGTH_MILLIMETERS, LENGTH_INCHES) == 196850.5 + assert distance_util.convert(mm, LENGTH_MILLIMETERS, unit) == pytest.approx( + expected + ) diff --git a/tests/util/test_unit_system.py b/tests/util/test_unit_system.py index 954df07fc9d..c4cc94636b1 100644 --- a/tests/util/test_unit_system.py +++ b/tests/util/test_unit_system.py @@ -177,13 +177,17 @@ def test_length_unknown_unit(): def test_length_to_metric(): """Test length conversion to metric system.""" assert METRIC_SYSTEM.length(100, METRIC_SYSTEM.length_unit) == 100 - assert METRIC_SYSTEM.length(5, IMPERIAL_SYSTEM.length_unit) == 8.04672 + assert METRIC_SYSTEM.length(5, IMPERIAL_SYSTEM.length_unit) == pytest.approx( + 8.04672 + ) def test_length_to_imperial(): """Test length conversion to imperial system.""" assert IMPERIAL_SYSTEM.length(100, IMPERIAL_SYSTEM.length_unit) == 100 - assert IMPERIAL_SYSTEM.length(5, METRIC_SYSTEM.length_unit) == 3.106855 + assert IMPERIAL_SYSTEM.length(5, METRIC_SYSTEM.length_unit) == pytest.approx( + 3.106855 + ) def test_wind_speed_unknown_unit(): diff --git a/tests/util/test_volume.py b/tests/util/test_volume.py index 3cbf5b72130..f78d6c4ed18 100644 --- a/tests/util/test_volume.py +++ b/tests/util/test_volume.py @@ -42,28 +42,73 @@ def test_convert_nonnumeric_value(): def test_convert_from_liters(): """Test conversion from liters to other units.""" liters = 5 - assert volume_util.convert(liters, VOLUME_LITERS, VOLUME_GALLONS) == 1.321 + assert volume_util.convert(liters, VOLUME_LITERS, VOLUME_GALLONS) == pytest.approx( + 1.32086 + ) def test_convert_from_gallons(): """Test conversion from gallons to other units.""" gallons = 5 - assert volume_util.convert(gallons, VOLUME_GALLONS, VOLUME_LITERS) == 18.925 + assert volume_util.convert(gallons, VOLUME_GALLONS, VOLUME_LITERS) == pytest.approx( + 18.92706 + ) def test_convert_from_cubic_meters(): """Test conversion from cubic meter to other units.""" cubic_meters = 5 - assert ( - volume_util.convert(cubic_meters, VOLUME_CUBIC_METERS, VOLUME_CUBIC_FEET) - == 176.5733335 - ) + assert volume_util.convert( + cubic_meters, VOLUME_CUBIC_METERS, VOLUME_CUBIC_FEET + ) == pytest.approx(176.5733335) def test_convert_from_cubic_feet(): """Test conversion from cubic feet to cubic meters to other units.""" cubic_feets = 500 - assert ( - volume_util.convert(cubic_feets, VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS) - == 14.1584233 + assert volume_util.convert( + cubic_feets, VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS + ) == pytest.approx(14.1584233) + + +@pytest.mark.parametrize( + "source_unit,target_unit,expected", + [ + (VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS, 14.1584233), + (VOLUME_CUBIC_FEET, VOLUME_FLUID_OUNCE, 478753.2467), + (VOLUME_CUBIC_FEET, VOLUME_GALLONS, 3740.25974), + (VOLUME_CUBIC_FEET, VOLUME_LITERS, 14158.42329599), + (VOLUME_CUBIC_FEET, VOLUME_MILLILITERS, 14158423.29599), + (VOLUME_CUBIC_METERS, VOLUME_CUBIC_METERS, 500), + (VOLUME_CUBIC_METERS, VOLUME_FLUID_OUNCE, 16907011.35), + (VOLUME_CUBIC_METERS, VOLUME_GALLONS, 132086.02617), + (VOLUME_CUBIC_METERS, VOLUME_LITERS, 500000), + (VOLUME_CUBIC_METERS, VOLUME_MILLILITERS, 500000000), + (VOLUME_FLUID_OUNCE, VOLUME_CUBIC_FEET, 0.52218967), + (VOLUME_FLUID_OUNCE, VOLUME_CUBIC_METERS, 0.014786764), + (VOLUME_FLUID_OUNCE, VOLUME_GALLONS, 3.90625), + (VOLUME_FLUID_OUNCE, VOLUME_LITERS, 14.786764), + (VOLUME_FLUID_OUNCE, VOLUME_MILLILITERS, 14786.764), + (VOLUME_GALLONS, VOLUME_CUBIC_FEET, 66.84027), + (VOLUME_GALLONS, VOLUME_CUBIC_METERS, 1.892706), + (VOLUME_GALLONS, VOLUME_FLUID_OUNCE, 64000), + (VOLUME_GALLONS, VOLUME_LITERS, 1892.70589), + (VOLUME_GALLONS, VOLUME_MILLILITERS, 1892705.89), + (VOLUME_LITERS, VOLUME_CUBIC_FEET, 17.65733), + (VOLUME_LITERS, VOLUME_CUBIC_METERS, 0.5), + (VOLUME_LITERS, VOLUME_FLUID_OUNCE, 16907.011), + (VOLUME_LITERS, VOLUME_GALLONS, 132.086), + (VOLUME_LITERS, VOLUME_MILLILITERS, 500000), + (VOLUME_MILLILITERS, VOLUME_CUBIC_FEET, 0.01765733), + (VOLUME_MILLILITERS, VOLUME_CUBIC_METERS, 0.0005), + (VOLUME_MILLILITERS, VOLUME_FLUID_OUNCE, 16.907), + (VOLUME_MILLILITERS, VOLUME_GALLONS, 0.132086), + (VOLUME_MILLILITERS, VOLUME_LITERS, 0.5), + ], +) +def test_convert(source_unit, target_unit, expected): + """Test conversion between units.""" + value = 500 + assert volume_util.convert(value, source_unit, target_unit) == pytest.approx( + expected ) From 951047d94e9b426e1626edeb9d425cdd2a578656 Mon Sep 17 00:00:00 2001 From: Alex Thompson Date: Wed, 7 Sep 2022 10:24:21 -0400 Subject: [PATCH 181/955] Add initial implementation of tilt_ble integration (#77633) Co-authored-by: J. Nick Koston --- .strict-typing | 1 + CODEOWNERS | 2 + homeassistant/components/tilt_ble/__init__.py | 49 +++++ .../components/tilt_ble/config_flow.py | 94 ++++++++ homeassistant/components/tilt_ble/const.py | 3 + .../components/tilt_ble/manifest.json | 15 ++ homeassistant/components/tilt_ble/sensor.py | 136 ++++++++++++ .../components/tilt_ble/strings.json | 21 ++ .../components/tilt_ble/translations/en.json | 21 ++ homeassistant/generated/bluetooth.py | 4 + homeassistant/generated/config_flows.py | 1 + mypy.ini | 10 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/tilt_ble/__init__.py | 25 +++ tests/components/tilt_ble/conftest.py | 8 + tests/components/tilt_ble/test_config_flow.py | 200 ++++++++++++++++++ tests/components/tilt_ble/test_sensor.py | 64 ++++++ 18 files changed, 660 insertions(+) create mode 100644 homeassistant/components/tilt_ble/__init__.py create mode 100644 homeassistant/components/tilt_ble/config_flow.py create mode 100644 homeassistant/components/tilt_ble/const.py create mode 100644 homeassistant/components/tilt_ble/manifest.json create mode 100644 homeassistant/components/tilt_ble/sensor.py create mode 100644 homeassistant/components/tilt_ble/strings.json create mode 100644 homeassistant/components/tilt_ble/translations/en.json create mode 100644 tests/components/tilt_ble/__init__.py create mode 100644 tests/components/tilt_ble/conftest.py create mode 100644 tests/components/tilt_ble/test_config_flow.py create mode 100644 tests/components/tilt_ble/test_sensor.py diff --git a/.strict-typing b/.strict-typing index f36439a66d3..76190758e5d 100644 --- a/.strict-typing +++ b/.strict-typing @@ -247,6 +247,7 @@ homeassistant.components.tailscale.* homeassistant.components.tautulli.* homeassistant.components.tcp.* homeassistant.components.tile.* +homeassistant.components.tilt_ble.* homeassistant.components.tplink.* homeassistant.components.tolo.* homeassistant.components.tractive.* diff --git a/CODEOWNERS b/CODEOWNERS index 61620875f39..7203e4705f1 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1132,6 +1132,8 @@ build.json @home-assistant/supervisor /tests/components/tibber/ @danielhiversen /homeassistant/components/tile/ @bachya /tests/components/tile/ @bachya +/homeassistant/components/tilt_ble/ @apt-itude +/tests/components/tilt_ble/ @apt-itude /homeassistant/components/time_date/ @fabaff /tests/components/time_date/ @fabaff /homeassistant/components/tmb/ @alemuro diff --git a/homeassistant/components/tilt_ble/__init__.py b/homeassistant/components/tilt_ble/__init__.py new file mode 100644 index 00000000000..eebb3660368 --- /dev/null +++ b/homeassistant/components/tilt_ble/__init__.py @@ -0,0 +1,49 @@ +"""The tilt_ble integration.""" +from __future__ import annotations + +import logging + +from tilt_ble import TiltBluetoothDeviceData + +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 Tilt BLE device from a config entry.""" + address = entry.unique_id + assert address is not None + data = TiltBluetoothDeviceData() + 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/tilt_ble/config_flow.py b/homeassistant/components/tilt_ble/config_flow.py new file mode 100644 index 00000000000..d534622eb7b --- /dev/null +++ b/homeassistant/components/tilt_ble/config_flow.py @@ -0,0 +1,94 @@ +"""Config flow for tilt_ble.""" +from __future__ import annotations + +from typing import Any + +from tilt_ble import TiltBluetoothDeviceData as DeviceData +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 TiltConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for tilt_ble.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize the config flow.""" + self._discovery_info: BluetoothServiceInfoBleak | None = None + self._discovered_device: DeviceData | 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 = DeviceData() + 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 = DeviceData() + 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/tilt_ble/const.py b/homeassistant/components/tilt_ble/const.py new file mode 100644 index 00000000000..aa50bad4aae --- /dev/null +++ b/homeassistant/components/tilt_ble/const.py @@ -0,0 +1,3 @@ +"""Constants for the tilt_ble integration.""" + +DOMAIN = "tilt_ble" diff --git a/homeassistant/components/tilt_ble/manifest.json b/homeassistant/components/tilt_ble/manifest.json new file mode 100644 index 00000000000..898807e1bec --- /dev/null +++ b/homeassistant/components/tilt_ble/manifest.json @@ -0,0 +1,15 @@ +{ + "domain": "tilt_ble", + "name": "Tilt Hydrometer BLE", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/tilt_ble", + "bluetooth": [ + { + "manufacturer_id": 76 + } + ], + "requirements": ["tilt-ble==0.2.3"], + "dependencies": ["bluetooth"], + "codeowners": ["@apt-itude"], + "iot_class": "local_push" +} diff --git a/homeassistant/components/tilt_ble/sensor.py b/homeassistant/components/tilt_ble/sensor.py new file mode 100644 index 00000000000..54d05b3c900 --- /dev/null +++ b/homeassistant/components/tilt_ble/sensor.py @@ -0,0 +1,136 @@ +"""Support for Tilt Hydrometers.""" +from __future__ import annotations + +from typing import Optional, Union + +from tilt_ble import DeviceClass, DeviceKey, SensorDeviceInfo, SensorUpdate, Units + +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_MANUFACTURER, + ATTR_MODEL, + ATTR_NAME, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + TEMP_FAHRENHEIT, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN + +SENSOR_DESCRIPTIONS = { + (DeviceClass.TEMPERATURE, Units.TEMP_FAHRENHEIT): SensorEntityDescription( + key=f"{DeviceClass.TEMPERATURE}_{Units.TEMP_FAHRENHEIT}", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=TEMP_FAHRENHEIT, + state_class=SensorStateClass.MEASUREMENT, + ), + (DeviceClass.SPECIFIC_GRAVITY, Units.SPECIFIC_GRAVITY): SensorEntityDescription( + key=f"{DeviceClass.SPECIFIC_GRAVITY}_{Units.SPECIFIC_GRAVITY}", + state_class=SensorStateClass.MEASUREMENT, + ), + ( + DeviceClass.SIGNAL_STRENGTH, + Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + ): SensorEntityDescription( + key=f"{DeviceClass.SIGNAL_STRENGTH}_{Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT}", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + 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[ATTR_NAME] = sensor_device_info.name + if sensor_device_info.manufacturer is not None: + hass_device_info[ATTR_MANUFACTURER] = sensor_device_info.manufacturer + if sensor_device_info.model is not None: + hass_device_info[ATTR_MODEL] = sensor_device_info.model + return hass_device_info + + +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[ + (description.device_class, description.native_unit_of_measurement) + ] + for device_key, description in sensor_update.entity_descriptions.items() + if description.device_class and description.native_unit_of_measurement + }, + 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 Tilt Hydrometer 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( + TiltBluetoothSensorEntity, async_add_entities + ) + ) + entry.async_on_unload(coordinator.async_register_processor(processor)) + + +class TiltBluetoothSensorEntity( + PassiveBluetoothProcessorEntity[ + PassiveBluetoothDataProcessor[Optional[Union[float, int]]] + ], + SensorEntity, +): + """Representation of a Tilt Hydrometer 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/components/tilt_ble/strings.json b/homeassistant/components/tilt_ble/strings.json new file mode 100644 index 00000000000..7111626cca1 --- /dev/null +++ b/homeassistant/components/tilt_ble/strings.json @@ -0,0 +1,21 @@ +{ + "config": { + "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "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%]" + } + }, + "abort": { + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/tilt_ble/translations/en.json b/homeassistant/components/tilt_ble/translations/en.json new file mode 100644 index 00000000000..d24df64f135 --- /dev/null +++ b/homeassistant/components/tilt_ble/translations/en.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "already_in_progress": "Configuration flow is already in progress", + "no_devices_found": "No devices found on the network" + }, + "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 b2400f733da..5368d9a745d 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -274,6 +274,10 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ "local_name": "TP39*", "connectable": False }, + { + "domain": "tilt_ble", + "manufacturer_id": 76 + }, { "domain": "xiaomi_ble", "connectable": False, diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 7e12845cf74..2b78c6888d4 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -389,6 +389,7 @@ FLOWS = { "thermopro", "tibber", "tile", + "tilt_ble", "tolo", "tomorrowio", "toon", diff --git a/mypy.ini b/mypy.ini index 957da7254eb..466f61814ba 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2230,6 +2230,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.tilt_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.tplink.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index 4e49ffccfb9..34aecea532e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2380,6 +2380,9 @@ thingspeak==1.0.0 # homeassistant.components.tikteck tikteck==0.4 +# homeassistant.components.tilt_ble +tilt-ble==0.2.3 + # homeassistant.components.tmb tmb==0.0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 214b4d0988b..5e90f44f381 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1620,6 +1620,9 @@ thermobeacon-ble==0.3.1 # homeassistant.components.thermopro thermopro-ble==0.4.3 +# homeassistant.components.tilt_ble +tilt-ble==0.2.3 + # homeassistant.components.todoist todoist-python==8.0.0 diff --git a/tests/components/tilt_ble/__init__.py b/tests/components/tilt_ble/__init__.py new file mode 100644 index 00000000000..5942efff908 --- /dev/null +++ b/tests/components/tilt_ble/__init__.py @@ -0,0 +1,25 @@ +"""Tests for the tilt_ble integration.""" + +from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo + +NOT_TILT_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", +) + +TILT_GREEN_SERVICE_INFO = BluetoothServiceInfo( + name="F6-0F-28-F2-1F-CB", + address="F6:0F:28:F2:1F:CB", + rssi=-70, + manufacturer_data={ + 76: b"\x02\x15\xa4\x95\xbb \xc5\xb1KD\xb5\x12\x13p\xf0-t\xde\x00F\x03\xebR" + }, + service_data={}, + service_uuids=[], + source="local", +) diff --git a/tests/components/tilt_ble/conftest.py b/tests/components/tilt_ble/conftest.py new file mode 100644 index 00000000000..552b41d10da --- /dev/null +++ b/tests/components/tilt_ble/conftest.py @@ -0,0 +1,8 @@ +"""Tilt session fixtures.""" + +import pytest + + +@pytest.fixture(autouse=True) +def mock_bluetooth(enable_bluetooth): + """Auto mock bluetooth.""" diff --git a/tests/components/tilt_ble/test_config_flow.py b/tests/components/tilt_ble/test_config_flow.py new file mode 100644 index 00000000000..c2bb20e0dd6 --- /dev/null +++ b/tests/components/tilt_ble/test_config_flow.py @@ -0,0 +1,200 @@ +"""Test the Tilt config flow.""" + +from unittest.mock import patch + +from homeassistant import config_entries +from homeassistant.components.tilt_ble.const import DOMAIN +from homeassistant.data_entry_flow import FlowResultType + +from . import NOT_TILT_SERVICE_INFO, TILT_GREEN_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=TILT_GREEN_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + with patch( + "homeassistant.components.govee_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"] == "Tilt Green" + assert result2["data"] == {} + assert result2["result"].unique_id == "F6:0F:28:F2:1F:CB" + + +async def test_async_step_bluetooth_not_tilt(hass): + """Test discovery via bluetooth not tilt.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=NOT_TILT_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.tilt_ble.config_flow.async_discovered_service_info", + return_value=[TILT_GREEN_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.tilt_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "F6:0F:28:F2:1F:CB"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Tilt Green" + assert result2["data"] == {} + assert result2["result"].unique_id == "F6:0F:28:F2:1F:CB" + + +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.tilt_ble.config_flow.async_discovered_service_info", + return_value=[TILT_GREEN_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="F6:0F:28:F2:1F:CB", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.tilt_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "F6:0F:28:F2:1F:CB"}, + ) + 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="F6:0F:28:F2:1F:CB", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.tilt_ble.config_flow.async_discovered_service_info", + return_value=[TILT_GREEN_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="F6:0F:28:F2:1F:CB", + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=TILT_GREEN_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=TILT_GREEN_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=TILT_GREEN_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=TILT_GREEN_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + with patch( + "homeassistant.components.tilt_ble.config_flow.async_discovered_service_info", + return_value=[TILT_GREEN_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.tilt_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "F6:0F:28:F2:1F:CB"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Tilt Green" + assert result2["data"] == {} + assert result2["result"].unique_id == "F6:0F:28:F2:1F:CB" + + # Verify the original one was aborted + assert not hass.config_entries.flow.async_progress(DOMAIN) diff --git a/tests/components/tilt_ble/test_sensor.py b/tests/components/tilt_ble/test_sensor.py new file mode 100644 index 00000000000..cfd83cb6573 --- /dev/null +++ b/tests/components/tilt_ble/test_sensor.py @@ -0,0 +1,64 @@ +"""Test the Tilt Hydrometer BLE sensors.""" + +from __future__ import annotations + +from unittest.mock import patch + +from homeassistant.components.bluetooth import BluetoothCallback, BluetoothChange +from homeassistant.components.sensor import ATTR_STATE_CLASS +from homeassistant.components.tilt_ble.const import DOMAIN +from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT +from homeassistant.core import HomeAssistant + +from . import TILT_GREEN_SERVICE_INFO + +from tests.common import MockConfigEntry + + +async def test_sensors(hass: HomeAssistant): + """Test setting up creates the sensors.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="61DE521B-F0BF-9F44-64D4-75BBE1738105", + ) + entry.add_to_hass(hass) + + saved_callback: BluetoothCallback | None = None + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + assert saved_callback is not None + saved_callback(TILT_GREEN_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 2 + + temp_sensor = hass.states.get("sensor.tilt_green_temperature") + assert temp_sensor is not None + + temp_sensor_attribtes = temp_sensor.attributes + assert temp_sensor.state == "21" + assert temp_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Tilt Green Temperature" + assert temp_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "°C" + assert temp_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" + + temp_sensor = hass.states.get("sensor.tilt_green_specific_gravity") + assert temp_sensor is not None + + temp_sensor_attribtes = temp_sensor.attributes + assert temp_sensor.state == "1.003" + assert temp_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Tilt Green Specific Gravity" + assert temp_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() From 645f5e5ac1518b4f921bedf521464e0a54111f4a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 7 Sep 2022 16:30:22 +0200 Subject: [PATCH 182/955] Introduce new MediaPlayerState StrEnum (#77941) * Adjust media-player checks in pylint plugin * Adjust media-player definitions * Adjust cast signatures * Adjust play_media signature * Introduce MediaPlayerState * Fix cast implementations * Revert cast changes * Update hass_enforce_type_hints.py * Use set Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> * Fix tests * Keep unused constants * Fix test * Revert tests Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> --- .../components/media_player/__init__.py | 37 ++++++++++++------- .../components/media_player/const.py | 12 ++++++ pylint/plugins/hass_enforce_type_hints.py | 8 ++-- 3 files changed, 39 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index c27a81c6dc2..e607f312df6 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -30,7 +30,7 @@ from homeassistant.components.websocket_api.const import ( ERR_UNKNOWN_ERROR, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( +from homeassistant.const import ( # noqa: F401 SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, @@ -128,6 +128,8 @@ from .const import ( # noqa: F401 SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, MediaPlayerEntityFeature, + MediaPlayerState, + MediaType, RepeatMode, ) from .errors import BrowseError @@ -226,7 +228,8 @@ def is_on(hass, entity_id=None): """ entity_ids = [entity_id] if entity_id else hass.states.entity_ids(DOMAIN) return any( - not hass.states.is_state(entity_id, STATE_OFF) for entity_id in entity_ids + not hass.states.is_state(entity_id, MediaPlayerState.OFF) + for entity_id in entity_ids ) @@ -454,7 +457,7 @@ class MediaPlayerEntity(Entity): _attr_media_artist: str | None = None _attr_media_channel: str | None = None _attr_media_content_id: str | None = None - _attr_media_content_type: str | None = None + _attr_media_content_type: MediaType | str | None = None _attr_media_duration: int | None = None _attr_media_episode: str | None = None _attr_media_image_hash: str | None @@ -467,13 +470,13 @@ class MediaPlayerEntity(Entity): _attr_media_series_title: str | None = None _attr_media_title: str | None = None _attr_media_track: int | None = None - _attr_repeat: str | None = None + _attr_repeat: RepeatMode | str | None = None _attr_shuffle: bool | None = None _attr_sound_mode_list: list[str] | None = None _attr_sound_mode: str | None = None _attr_source_list: list[str] | None = None _attr_source: str | None = None - _attr_state: str | None = None + _attr_state: MediaPlayerState | str | None = None _attr_supported_features: int = 0 _attr_volume_level: float | None = None @@ -488,7 +491,7 @@ class MediaPlayerEntity(Entity): return None @property - def state(self) -> str | None: + def state(self) -> MediaPlayerState | str | None: """State of the player.""" return self._attr_state @@ -515,7 +518,7 @@ class MediaPlayerEntity(Entity): return self._attr_media_content_id @property - def media_content_type(self) -> str | None: + def media_content_type(self) -> MediaType | str | None: """Content type of current playing media.""" return self._attr_media_content_type @@ -664,7 +667,7 @@ class MediaPlayerEntity(Entity): return self._attr_shuffle @property - def repeat(self) -> str | None: + def repeat(self) -> RepeatMode | str | None: """Return current repeat mode.""" return self._attr_repeat @@ -758,12 +761,14 @@ class MediaPlayerEntity(Entity): """Send seek command.""" await self.hass.async_add_executor_job(self.media_seek, position) - def play_media(self, media_type: str, media_id: str, **kwargs: Any) -> None: + def play_media( + self, media_type: MediaType | str, media_id: str, **kwargs: Any + ) -> None: """Play a piece of media.""" raise NotImplementedError() async def async_play_media( - self, media_type: str, media_id: str, **kwargs: Any + self, media_type: MediaType | str, media_id: str, **kwargs: Any ) -> None: """Play a piece of media.""" await self.hass.async_add_executor_job( @@ -905,7 +910,11 @@ class MediaPlayerEntity(Entity): ) return - if self.state in (STATE_OFF, STATE_IDLE, STATE_STANDBY): + if self.state in { + MediaPlayerState.OFF, + MediaPlayerState.IDLE, + MediaPlayerState.STANDBY, + }: await self.async_turn_on() else: await self.async_turn_off() @@ -954,7 +963,7 @@ class MediaPlayerEntity(Entity): ) return - if self.state == STATE_PLAYING: + if self.state == MediaPlayerState.PLAYING: await self.async_media_pause() else: await self.async_media_play() @@ -962,7 +971,7 @@ class MediaPlayerEntity(Entity): @property def entity_picture(self) -> str | None: """Return image of the media playing.""" - if self.state == STATE_OFF: + if self.state == MediaPlayerState.OFF: return None if self.media_image_remotely_accessible: @@ -1008,7 +1017,7 @@ class MediaPlayerEntity(Entity): if self.support_grouping: state_attr[ATTR_GROUP_MEMBERS] = self.group_members - if self.state == STATE_OFF: + if self.state == MediaPlayerState.OFF: return state_attr for attr in ATTR_TO_PROPERTY: diff --git a/homeassistant/components/media_player/const.py b/homeassistant/components/media_player/const.py index 2d3fa9c9b3e..ea8069cc7e4 100644 --- a/homeassistant/components/media_player/const.py +++ b/homeassistant/components/media_player/const.py @@ -41,6 +41,18 @@ ATTR_SOUND_MODE_LIST = "sound_mode_list" DOMAIN = "media_player" +class MediaPlayerState(StrEnum): + """State of media player entities.""" + + OFF = "off" + ON = "on" + IDLE = "idle" + PLAYING = "playing" + PAUSED = "paused" + STANDBY = "standby" + BUFFERING = "buffering" + + class MediaClass(StrEnum): """Media class for media player entities.""" diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 23578630b7e..379c19b044d 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -1514,7 +1514,7 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ), TypeHintMatch( function_name="state", - return_type=["str", None], + return_type=["MediaPlayerState", None], ), TypeHintMatch( function_name="access_token", @@ -1534,7 +1534,7 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ), TypeHintMatch( function_name="media_content_type", - return_type=["str", None], + return_type=["MediaType", "str", None], ), TypeHintMatch( function_name="media_duration", @@ -1643,7 +1643,7 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ), TypeHintMatch( function_name="repeat", - return_type=["str", None], + return_type=["RepeatMode", None], ), TypeHintMatch( function_name="group_members", @@ -1711,7 +1711,7 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { TypeHintMatch( function_name="play_media", arg_types={ - 1: "str", + 1: "MediaType | str", 2: "str", }, kwargs_type="Any", From 2cfdc15c380772be23f4af6c04518b9c527f1f07 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 7 Sep 2022 09:54:21 -0500 Subject: [PATCH 183/955] Handle stale switchbot advertisement data while connected (#77956) --- homeassistant/components/switchbot/cover.py | 3 +++ homeassistant/components/switchbot/manifest.json | 2 +- homeassistant/components/switchbot/switch.py | 13 +++++++------ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 13 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/switchbot/cover.py b/homeassistant/components/switchbot/cover.py index c2b6cb1a4c7..df716be6ff3 100644 --- a/homeassistant/components/switchbot/cover.py +++ b/homeassistant/components/switchbot/cover.py @@ -4,6 +4,8 @@ from __future__ import annotations import logging from typing import Any +import switchbot + from homeassistant.components.cover import ( ATTR_CURRENT_POSITION, ATTR_POSITION, @@ -36,6 +38,7 @@ async def async_setup_entry( class SwitchBotCurtainEntity(SwitchbotEntity, CoverEntity, RestoreEntity): """Representation of a Switchbot.""" + _device: switchbot.SwitchbotCurtain _attr_device_class = CoverDeviceClass.CURTAIN _attr_supported_features = ( CoverEntityFeature.OPEN diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index 040e76391bd..f322734ba54 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.18.25"], + "requirements": ["PySwitchbot==0.18.27"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/homeassistant/components/switchbot/switch.py b/homeassistant/components/switchbot/switch.py index 17235135cfa..d524a7100f0 100644 --- a/homeassistant/components/switchbot/switch.py +++ b/homeassistant/components/switchbot/switch.py @@ -4,6 +4,8 @@ from __future__ import annotations import logging from typing import Any +import switchbot + from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_ON @@ -34,6 +36,7 @@ class SwitchBotSwitch(SwitchbotEntity, SwitchEntity, RestoreEntity): """Representation of a Switchbot switch.""" _attr_device_class = SwitchDeviceClass.SWITCH + _device: switchbot.Switchbot def __init__(self, coordinator: SwitchbotDataUpdateCoordinator) -> None: """Initialize the Switchbot.""" @@ -69,21 +72,19 @@ class SwitchBotSwitch(SwitchbotEntity, SwitchEntity, RestoreEntity): @property def assumed_state(self) -> bool: """Return true if unable to access real state of entity.""" - if not self.data["data"]["switchMode"]: - return True - return False + return not self._device.switch_mode() @property def is_on(self) -> bool | None: """Return true if device is on.""" - if not self.data["data"]["switchMode"]: + if not self._device.switch_mode(): return self._attr_is_on - return self.data["data"]["isOn"] + return self._device.is_on() @property def extra_state_attributes(self) -> dict: """Return the state attributes.""" return { **super().extra_state_attributes, - "switch_mode": self.data["data"]["switchMode"], + "switch_mode": self._device.switch_mode(), } diff --git a/requirements_all.txt b/requirements_all.txt index 34aecea532e..f96fbc6d141 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.18.25 +PySwitchbot==0.18.27 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5e90f44f381..2f295442c9e 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.18.25 +PySwitchbot==0.18.27 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From 4076f8b94e742da743b52d40e443e3a5be11dfa2 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 7 Sep 2022 11:10:24 -0400 Subject: [PATCH 184/955] Fix ZHA lighting initial hue/saturation attribute read (#77727) * Handle the case of `current_hue` being `None` * WIP unit tests --- homeassistant/components/zha/light.py | 18 +++--- tests/components/zha/common.py | 21 +++++- tests/components/zha/test_light.py | 93 ++++++++++++++++++++++++--- 3 files changed, 114 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index ea46c0c49f1..528833608b3 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -612,16 +612,18 @@ class Light(BaseLight, ZhaEntity): and self._color_channel.enhanced_current_hue is not None ): curr_hue = self._color_channel.enhanced_current_hue * 65535 / 360 - else: + elif self._color_channel.current_hue is not None: curr_hue = self._color_channel.current_hue * 254 / 360 - curr_saturation = self._color_channel.current_saturation - if curr_hue is not None and curr_saturation is not None: - self._attr_hs_color = ( - int(curr_hue), - int(curr_saturation * 2.54), - ) else: - self._attr_hs_color = (0, 0) + curr_hue = 0 + + if (curr_saturation := self._color_channel.current_saturation) is None: + curr_saturation = 0 + + self._attr_hs_color = ( + int(curr_hue), + int(curr_saturation * 2.54), + ) if self._color_channel.color_loop_supported: self._attr_supported_features |= light.LightEntityFeature.EFFECT diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index cad8020267f..56197fa39ec 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -2,12 +2,14 @@ import asyncio from datetime import timedelta import math -from unittest.mock import AsyncMock, Mock +from typing import Any +from unittest.mock import AsyncMock, Mock, patch import zigpy.zcl import zigpy.zcl.foundation as zcl_f import homeassistant.components.zha.core.const as zha_const +from homeassistant.components.zha.core.helpers import async_get_zha_config_value from homeassistant.helpers import entity_registry import homeassistant.util.dt as dt_util @@ -243,3 +245,20 @@ async def async_shift_time(hass): next_update = dt_util.utcnow() + timedelta(seconds=11) async_fire_time_changed(hass, next_update) await hass.async_block_till_done() + + +def patch_zha_config(component: str, overrides: dict[tuple[str, str], Any]): + """Patch the ZHA custom configuration defaults.""" + + def new_get_config(config_entry, section, config_key, default): + if (section, config_key) in overrides: + return overrides[section, config_key] + else: + return async_get_zha_config_value( + config_entry, section, config_key, default + ) + + return patch( + f"homeassistant.components.zha.{component}.async_get_zha_config_value", + side_effect=new_get_config, + ) diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index 5f5e7ab2e38..018ac68a25f 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -14,6 +14,10 @@ from homeassistant.components.light import ( FLASH_SHORT, ColorMode, ) +from homeassistant.components.zha.core.const import ( + CONF_ALWAYS_PREFER_XY_COLOR_MODE, + ZHA_OPTIONS, +) from homeassistant.components.zha.core.group import GroupMember from homeassistant.components.zha.light import FLASH_EFFECTS from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, Platform @@ -26,6 +30,7 @@ from .common import ( async_test_rejoin, find_entity_id, get_zha_gateway, + patch_zha_config, send_attributes_report, ) from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE @@ -340,7 +345,11 @@ async def test_light( if cluster_identify: await async_test_flash_from_hass(hass, cluster_identify, entity_id, FLASH_SHORT) - # test turning the lights on and off from the HA + # test long flashing the lights from the HA + if cluster_identify: + await async_test_flash_from_hass(hass, cluster_identify, entity_id, FLASH_LONG) + + # test dimming the lights on and off from the HA if cluster_level: await async_test_level_on_off_from_hass( hass, cluster_on_off, cluster_level, entity_id @@ -355,16 +364,82 @@ async def test_light( # test rejoin await async_test_off_from_hass(hass, cluster_on_off, entity_id) - clusters = [cluster_on_off] - if cluster_level: - clusters.append(cluster_level) - if cluster_color: - clusters.append(cluster_color) + clusters = [c for c in (cluster_on_off, cluster_level, cluster_color) if c] await async_test_rejoin(hass, zigpy_device, clusters, reporting) - # test long flashing the lights from the HA - if cluster_identify: - await async_test_flash_from_hass(hass, cluster_identify, entity_id, FLASH_LONG) + +@pytest.mark.parametrize( + "plugged_attr_reads, config_override, expected_state", + [ + # HS light without cached hue or saturation + ( + { + "color_capabilities": ( + lighting.Color.ColorCapabilities.Hue_and_saturation + ), + }, + {(ZHA_OPTIONS, CONF_ALWAYS_PREFER_XY_COLOR_MODE): False}, + {}, + ), + # HS light with cached hue + ( + { + "color_capabilities": ( + lighting.Color.ColorCapabilities.Hue_and_saturation + ), + "current_hue": 100, + }, + {(ZHA_OPTIONS, CONF_ALWAYS_PREFER_XY_COLOR_MODE): False}, + {}, + ), + # HS light with cached saturation + ( + { + "color_capabilities": ( + lighting.Color.ColorCapabilities.Hue_and_saturation + ), + "current_saturation": 100, + }, + {(ZHA_OPTIONS, CONF_ALWAYS_PREFER_XY_COLOR_MODE): False}, + {}, + ), + # HS light with both + ( + { + "color_capabilities": ( + lighting.Color.ColorCapabilities.Hue_and_saturation + ), + "current_hue": 100, + "current_saturation": 100, + }, + {(ZHA_OPTIONS, CONF_ALWAYS_PREFER_XY_COLOR_MODE): False}, + {}, + ), + ], +) +async def test_light_initialization( + hass, + zigpy_device_mock, + zha_device_joined_restored, + plugged_attr_reads, + config_override, + expected_state, +): + """Test zha light initialization with cached attributes and color modes.""" + + # create zigpy devices + zigpy_device = zigpy_device_mock(LIGHT_COLOR) + + # mock attribute reads + zigpy_device.endpoints[1].light_color.PLUGGED_ATTR_READS = plugged_attr_reads + + with patch_zha_config("light", config_override): + zha_device = await zha_device_joined_restored(zigpy_device) + entity_id = await find_entity_id(Platform.LIGHT, zha_device, hass) + + assert entity_id is not None + + # TODO ensure hue and saturation are properly set on startup @patch( From cd5967c4c698fa43dcdce316190a8337eed0e8c1 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 7 Sep 2022 17:31:53 +0200 Subject: [PATCH 185/955] Update frontend to 20220907.0 (#77963) --- 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 8abc8fd4e32..07822979683 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==20220906.0"], + "requirements": ["home-assistant-frontend==20220907.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 58a000fbc25..7c11725e460 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -19,7 +19,7 @@ cryptography==37.0.4 fnvhash==0.1.0 hass-nabucasa==0.55.0 home-assistant-bluetooth==1.3.0 -home-assistant-frontend==20220906.0 +home-assistant-frontend==20220907.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index f96fbc6d141..59949dfffd1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -851,7 +851,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220906.0 +home-assistant-frontend==20220907.0 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2f295442c9e..76b39c62cbd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -628,7 +628,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220906.0 +home-assistant-frontend==20220907.0 # homeassistant.components.home_connect homeconnect==0.7.2 From 52c8c80f91283dd2665cd242d33131d98dfbcb17 Mon Sep 17 00:00:00 2001 From: Chris McCurdy Date: Wed, 7 Sep 2022 11:43:05 -0400 Subject: [PATCH 186/955] Add additional method of retrieving UUID for LG soundbar configuration (#77856) --- .../components/lg_soundbar/config_flow.py | 19 +- .../lg_soundbar/test_config_flow.py | 168 +++++++++++++++++- 2 files changed, 182 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/lg_soundbar/config_flow.py b/homeassistant/components/lg_soundbar/config_flow.py index bd9a727d1f4..0606bad2d67 100644 --- a/homeassistant/components/lg_soundbar/config_flow.py +++ b/homeassistant/components/lg_soundbar/config_flow.py @@ -1,5 +1,5 @@ """Config flow to configure the LG Soundbar integration.""" -from queue import Queue +from queue import Full, Queue import socket import temescal @@ -20,18 +20,29 @@ def test_connect(host, port): uuid_q = Queue(maxsize=1) name_q = Queue(maxsize=1) + def queue_add(attr_q, data): + try: + attr_q.put_nowait(data) + except Full: + pass + def msg_callback(response): - if response["msg"] == "MAC_INFO_DEV" and "s_uuid" in response["data"]: - uuid_q.put_nowait(response["data"]["s_uuid"]) + if ( + response["msg"] in ["MAC_INFO_DEV", "PRODUCT_INFO"] + and "s_uuid" in response["data"] + ): + queue_add(uuid_q, response["data"]["s_uuid"]) if ( response["msg"] == "SPK_LIST_VIEW_INFO" and "s_user_name" in response["data"] ): - name_q.put_nowait(response["data"]["s_user_name"]) + queue_add(name_q, response["data"]["s_user_name"]) try: connection = temescal.temescal(host, port=port, callback=msg_callback) connection.get_mac_info() + if uuid_q.empty(): + connection.get_product_info() connection.get_info() details = {"name": name_q.get(timeout=10), "uuid": uuid_q.get(timeout=10)} return details diff --git a/tests/components/lg_soundbar/test_config_flow.py b/tests/components/lg_soundbar/test_config_flow.py index 3fafc2c7628..8bcf817cbba 100644 --- a/tests/components/lg_soundbar/test_config_flow.py +++ b/tests/components/lg_soundbar/test_config_flow.py @@ -1,5 +1,5 @@ """Test the lg_soundbar config flow.""" -from unittest.mock import MagicMock, patch +from unittest.mock import DEFAULT, MagicMock, Mock, call, patch from homeassistant import config_entries from homeassistant.components.lg_soundbar.const import DEFAULT_PORT, DOMAIN @@ -43,6 +43,172 @@ async def test_form(hass): assert len(mock_setup_entry.mock_calls) == 1 +async def test_form_uuid_missing_from_mac_info(hass): + """Test we get the form, but uuid is missing from the initial get_mac_info function call.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch( + "homeassistant.components.lg_soundbar.config_flow.temescal", return_value=Mock() + ) as mock_temescal, patch( + "homeassistant.components.lg_soundbar.async_setup_entry", return_value=True + ) as mock_setup_entry: + tmock = mock_temescal.temescal + tmock.return_value = Mock() + instance = tmock.return_value + + def temescal_side_effect(addr, port, callback): + product_info = {"msg": "PRODUCT_INFO", "data": {"s_uuid": "uuid"}} + instance.get_product_info.side_effect = lambda: callback(product_info) + info = {"msg": "SPK_LIST_VIEW_INFO", "data": {"s_user_name": "name"}} + instance.get_info.side_effect = lambda: callback(info) + return DEFAULT + + tmock.side_effect = temescal_side_effect + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "1.1.1.1", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "name" + assert result2["data"] == { + CONF_HOST: "1.1.1.1", + CONF_PORT: DEFAULT_PORT, + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_uuid_present_in_both_functions_uuid_q_empty(hass): + """Get the form, uuid present in both get_mac_info and get_product_info calls. + + Value from get_mac_info is not added to uuid_q before get_product_info is run. + """ + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + mock_uuid_q = MagicMock() + mock_name_q = MagicMock() + + with patch( + "homeassistant.components.lg_soundbar.config_flow.temescal", return_value=Mock() + ) as mock_temescal, patch( + "homeassistant.components.lg_soundbar.config_flow.Queue", + return_value=MagicMock(), + ) as mock_q, patch( + "homeassistant.components.lg_soundbar.async_setup_entry", return_value=True + ) as mock_setup_entry: + mock_q.side_effect = [mock_uuid_q, mock_name_q] + mock_uuid_q.empty.return_value = True + mock_uuid_q.get.return_value = "uuid" + mock_name_q.get.return_value = "name" + tmock = mock_temescal.temescal + tmock.return_value = Mock() + instance = tmock.return_value + + def temescal_side_effect(addr, port, callback): + mac_info = {"msg": "MAC_INFO_DEV", "data": {"s_uuid": "uuid"}} + instance.get_mac_info.side_effect = lambda: callback(mac_info) + product_info = {"msg": "PRODUCT_INFO", "data": {"s_uuid": "uuid"}} + instance.get_product_info.side_effect = lambda: callback(product_info) + info = {"msg": "SPK_LIST_VIEW_INFO", "data": {"s_user_name": "name"}} + instance.get_info.side_effect = lambda: callback(info) + return DEFAULT + + tmock.side_effect = temescal_side_effect + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "1.1.1.1", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "name" + assert result2["data"] == { + CONF_HOST: "1.1.1.1", + CONF_PORT: DEFAULT_PORT, + } + assert len(mock_setup_entry.mock_calls) == 1 + mock_uuid_q.empty.assert_called_once() + mock_uuid_q.put_nowait.has_calls([call("uuid"), call("uuid")]) + mock_uuid_q.get.assert_called_once() + + +async def test_form_uuid_present_in_both_functions_uuid_q_not_empty(hass): + """Get the form, uuid present in both get_mac_info and get_product_info calls. + + Value from get_mac_info is added to uuid_q before get_product_info is run. + """ + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + mock_uuid_q = MagicMock() + mock_name_q = MagicMock() + + with patch( + "homeassistant.components.lg_soundbar.config_flow.temescal", return_value=Mock() + ) as mock_temescal, patch( + "homeassistant.components.lg_soundbar.config_flow.Queue", + return_value=MagicMock(), + ) as mock_q, patch( + "homeassistant.components.lg_soundbar.async_setup_entry", return_value=True + ) as mock_setup_entry: + mock_q.side_effect = [mock_uuid_q, mock_name_q] + mock_uuid_q.empty.return_value = False + mock_uuid_q.get.return_value = "uuid" + mock_name_q.get.return_value = "name" + tmock = mock_temescal.temescal + tmock.return_value = Mock() + instance = tmock.return_value + + def temescal_side_effect(addr, port, callback): + mac_info = {"msg": "MAC_INFO_DEV", "data": {"s_uuid": "uuid"}} + instance.get_mac_info.side_effect = lambda: callback(mac_info) + info = {"msg": "SPK_LIST_VIEW_INFO", "data": {"s_user_name": "name"}} + instance.get_info.side_effect = lambda: callback(info) + return DEFAULT + + tmock.side_effect = temescal_side_effect + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "1.1.1.1", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "name" + assert result2["data"] == { + CONF_HOST: "1.1.1.1", + CONF_PORT: DEFAULT_PORT, + } + assert len(mock_setup_entry.mock_calls) == 1 + mock_uuid_q.empty.assert_called_once() + mock_uuid_q.put_nowait.assert_called_once() + mock_uuid_q.get.assert_called_once() + + async def test_form_cannot_connect(hass): """Test we handle cannot connect error.""" result = await hass.config_entries.flow.async_init( From f62fbbe5242409896dd80f19f6f4360b58a38cfb Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 7 Sep 2022 23:42:16 +0200 Subject: [PATCH 187/955] Use _attr_force_update in mqtt (#77902) --- homeassistant/components/mqtt/binary_sensor.py | 6 +----- homeassistant/components/mqtt/sensor.py | 6 +----- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index cffb2fd8300..adb4c12daf9 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -184,6 +184,7 @@ class MqttBinarySensor(MqttEntity, BinarySensorEntity, RestoreEntity): return DISCOVERY_SCHEMA def _setup_from_config(self, config): + self._attr_force_update = config[CONF_FORCE_UPDATE] self._value_template = MqttValueTemplate( self._config.get(CONF_VALUE_TEMPLATE), entity=self, @@ -300,11 +301,6 @@ class MqttBinarySensor(MqttEntity, BinarySensorEntity, RestoreEntity): """Return the class of this sensor.""" return self._config.get(CONF_DEVICE_CLASS) - @property - def force_update(self) -> bool: - """Force update.""" - return self._config[CONF_FORCE_UPDATE] - @property def available(self) -> bool: """Return true if the device is available and value has not expired.""" diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 4c04d6176f1..b361ce06a81 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -233,6 +233,7 @@ class MqttSensor(MqttEntity, RestoreSensor): def _setup_from_config(self, config): """(Re)Setup the entity.""" + self._attr_force_update = config[CONF_FORCE_UPDATE] self._template = MqttValueTemplate( self._config.get(CONF_VALUE_TEMPLATE), entity=self ).async_render_with_possible_json_value @@ -350,11 +351,6 @@ class MqttSensor(MqttEntity, RestoreSensor): """Return the unit this state is expressed in.""" return self._config.get(CONF_UNIT_OF_MEASUREMENT) - @property - def force_update(self) -> bool: - """Force update.""" - return self._config[CONF_FORCE_UPDATE] - @property def native_value(self): """Return the state of the entity.""" From ea26c0bf77af246a4e3a33e42a96490ceb1aa481 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 8 Sep 2022 00:27:38 +0000 Subject: [PATCH 188/955] [ci skip] Translation update --- .../automation/translations/ru.json | 13 +++++++++++ .../bluemaestro/translations/he.json | 21 ++++++++++++++++++ .../bluemaestro/translations/it.json | 22 +++++++++++++++++++ .../components/fibaro/translations/he.json | 9 +++++++- .../components/fibaro/translations/it.json | 10 ++++++++- .../components/group/translations/hu.json | 2 +- .../integration/translations/it.json | 2 +- .../components/nobo_hub/translations/he.json | 17 ++++++++++++++ .../components/nobo_hub/translations/it.json | 21 ++++++++++++++---- .../components/schedule/translations/it.json | 2 +- .../components/sensibo/translations/it.json | 6 +++++ .../speedtestdotnet/translations/ru.json | 13 +++++++++++ .../switch_as_x/translations/it.json | 4 ++-- .../components/tilt_ble/translations/ca.json | 21 ++++++++++++++++++ .../components/tilt_ble/translations/de.json | 21 ++++++++++++++++++ .../components/tilt_ble/translations/es.json | 21 ++++++++++++++++++ .../components/tilt_ble/translations/fr.json | 21 ++++++++++++++++++ .../components/tilt_ble/translations/he.json | 21 ++++++++++++++++++ .../components/tilt_ble/translations/it.json | 21 ++++++++++++++++++ .../components/tilt_ble/translations/no.json | 12 ++++++++++ .../tilt_ble/translations/pt-BR.json | 21 ++++++++++++++++++ 21 files changed, 290 insertions(+), 11 deletions(-) create mode 100644 homeassistant/components/bluemaestro/translations/he.json create mode 100644 homeassistant/components/bluemaestro/translations/it.json create mode 100644 homeassistant/components/nobo_hub/translations/he.json create mode 100644 homeassistant/components/tilt_ble/translations/ca.json create mode 100644 homeassistant/components/tilt_ble/translations/de.json create mode 100644 homeassistant/components/tilt_ble/translations/es.json create mode 100644 homeassistant/components/tilt_ble/translations/fr.json create mode 100644 homeassistant/components/tilt_ble/translations/he.json create mode 100644 homeassistant/components/tilt_ble/translations/it.json create mode 100644 homeassistant/components/tilt_ble/translations/no.json create mode 100644 homeassistant/components/tilt_ble/translations/pt-BR.json diff --git a/homeassistant/components/automation/translations/ru.json b/homeassistant/components/automation/translations/ru.json index d98f55a898e..222d67922f8 100644 --- a/homeassistant/components/automation/translations/ru.json +++ b/homeassistant/components/automation/translations/ru.json @@ -1,4 +1,17 @@ { + "issues": { + "service_not_found": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u0412 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0438 \"{name}\" (`{entity_id}`) \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0435, \u043a\u043e\u0442\u043e\u0440\u043e\u0435 \u0432\u044b\u0437\u044b\u0432\u0430\u0435\u0442 \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0443\u044e \u0441\u043b\u0443\u0436\u0431\u0443: `{service}`.\n\n\u042d\u0442\u043e \u043c\u043e\u0436\u0435\u0442 \u043f\u0440\u0435\u043f\u044f\u0442\u0441\u0442\u0432\u043e\u0432\u0430\u0442\u044c \u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u043e\u043c\u0443 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044e \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0438. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e, \u044d\u0442\u0430 \u0441\u043b\u0443\u0436\u0431\u0430 \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430, \u0430 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e, \u043f\u0440\u0438\u0447\u0438\u043d\u043e\u0439 \u0441\u0442\u0430\u043b\u0430 \u043e\u043f\u0435\u0447\u0430\u0442\u043a\u0430.\n\n\u0427\u0442\u043e\u0431\u044b \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c \u044d\u0442\u0443 \u043e\u0448\u0438\u0431\u043a\u0443, [\u043e\u0442\u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u0443\u0439\u0442\u0435 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u044e]({edit}) \u0438\u043b\u0438 \u0443\u0434\u0430\u043b\u0438\u0442\u0435 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0435, \u0432\u044b\u0437\u044b\u0432\u0430\u044e\u0449\u0435\u0435 \u044d\u0442\u0443 \u0441\u043b\u0443\u0436\u0431\u0443.\n\n\u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 \u041f\u041e\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u042c, \u0447\u0442\u043e\u0431\u044b \u043e\u0442\u043c\u0435\u0442\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443 \u043a\u0430\u043a \u0443\u0441\u0442\u0440\u0430\u043d\u0451\u043d\u043d\u0443\u044e.", + "title": "{name} \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0443\u044e \u0441\u043b\u0443\u0436\u0431\u0443" + } + } + }, + "title": "{name} \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0443\u044e \u0441\u043b\u0443\u0436\u0431\u0443" + } + }, "state": { "_": { "off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", diff --git a/homeassistant/components/bluemaestro/translations/he.json b/homeassistant/components/bluemaestro/translations/he.json new file mode 100644 index 00000000000..de780eb221a --- /dev/null +++ b/homeassistant/components/bluemaestro/translations/he.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + }, + "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/bluemaestro/translations/it.json b/homeassistant/components/bluemaestro/translations/it.json new file mode 100644 index 00000000000..7784ed3a240 --- /dev/null +++ b/homeassistant/components/bluemaestro/translations/it.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "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": "{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/fibaro/translations/he.json b/homeassistant/components/fibaro/translations/he.json index c479d8488f2..1173a604472 100644 --- a/homeassistant/components/fibaro/translations/he.json +++ b/homeassistant/components/fibaro/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": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", @@ -9,6 +10,12 @@ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + }, + "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1" + }, "user": { "data": { "password": "\u05e1\u05d9\u05e1\u05de\u05d4", diff --git a/homeassistant/components/fibaro/translations/it.json b/homeassistant/components/fibaro/translations/it.json index 641ed94e49f..82c549faba1 100644 --- a/homeassistant/components/fibaro/translations/it.json +++ b/homeassistant/components/fibaro/translations/it.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" }, "error": { "cannot_connect": "Impossibile connettersi", @@ -9,6 +10,13 @@ "unknown": "Errore imprevisto" }, "step": { + "reauth_confirm": { + "data": { + "password": "Password" + }, + "description": "Aggiorna la tua password per {username}", + "title": "Autentica nuovamente l'integrazione" + }, "user": { "data": { "import_plugins": "Vuoi importare le entit\u00e0 dai plugin fibaro?", diff --git a/homeassistant/components/group/translations/hu.json b/homeassistant/components/group/translations/hu.json index ae455bafebe..dfe4e0dcfee 100644 --- a/homeassistant/components/group/translations/hu.json +++ b/homeassistant/components/group/translations/hu.json @@ -66,7 +66,7 @@ "cover": "Red\u0151ny csoport", "fan": "Ventil\u00e1tor csoport", "light": "L\u00e1mpa csoport", - "lock": "Csoport z\u00e1rol\u00e1sa", + "lock": "Z\u00e1r csoport", "media_player": "M\u00e9dialej\u00e1tsz\u00f3 csoport", "switch": "Kapcsol\u00f3csoport" }, diff --git a/homeassistant/components/integration/translations/it.json b/homeassistant/components/integration/translations/it.json index 92e4077aa90..ae7e110280d 100644 --- a/homeassistant/components/integration/translations/it.json +++ b/homeassistant/components/integration/translations/it.json @@ -32,5 +32,5 @@ } } }, - "title": "Integrazione - Sensore integrale di somma Riemann" + "title": "Integrazione - Sensore integrale a somma di Riemann" } \ No newline at end of file diff --git a/homeassistant/components/nobo_hub/translations/he.json b/homeassistant/components/nobo_hub/translations/he.json new file mode 100644 index 00000000000..93c0754ba16 --- /dev/null +++ b/homeassistant/components/nobo_hub/translations/he.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "manual": { + "data": { + "ip_address": "\u05db\u05ea\u05d5\u05d1\u05ea IP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nobo_hub/translations/it.json b/homeassistant/components/nobo_hub/translations/it.json index ed7ce1f7cd6..2b8100c49e2 100644 --- a/homeassistant/components/nobo_hub/translations/it.json +++ b/homeassistant/components/nobo_hub/translations/it.json @@ -4,7 +4,7 @@ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" }, "error": { - "cannot_connect": "Impossibile connettersi - controllare il numero di serie", + "cannot_connect": "Impossibile connettersi - controlla il numero di serie", "invalid_ip": "Indirizzo IP non valido", "invalid_serial": "Numero di serie non valido", "unknown": "Errore imprevisto" @@ -14,17 +14,30 @@ "data": { "ip_address": "Indirizzo IP", "serial": "Numero di serie (12 cifre)" - } + }, + "description": "Configura un Nob\u00f8 Ecohub non rilevato sulla tua rete locale. Se il tuo hub si trova su un'altra rete, puoi comunque connetterti inserendo il numero di serie completo (12 cifre) e il suo indirizzo IP." }, "selected": { "data": { "serial_suffix": "Suffisso del numero di serie (3 cifre)" - } + }, + "description": "Configurazione {hub}.\n\nPer connettersi all'hub, \u00e8 necessario inserire le ultime 3 cifre del numero di serie dell'hub." }, "user": { "data": { "device": "Hub scoperti" - } + }, + "description": "Seleziona Nob\u00f8 Ecohub da configurare." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "override_type": "Tipo di sostituzione" + }, + "description": "Seleziona il tipo di sostituzione \"Ora\" per terminare la sostituzione nella modifica del profilo della prossima settimana." } } } diff --git a/homeassistant/components/schedule/translations/it.json b/homeassistant/components/schedule/translations/it.json index c50d8a66d1c..f75cfe395c6 100644 --- a/homeassistant/components/schedule/translations/it.json +++ b/homeassistant/components/schedule/translations/it.json @@ -5,5 +5,5 @@ "on": "Acceso" } }, - "title": "Programma" + "title": "Calendarizzazione" } \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/it.json b/homeassistant/components/sensibo/translations/it.json index 912bfee5114..f2c45f9b5ae 100644 --- a/homeassistant/components/sensibo/translations/it.json +++ b/homeassistant/components/sensibo/translations/it.json @@ -15,11 +15,17 @@ "reauth_confirm": { "data": { "api_key": "Chiave API" + }, + "data_description": { + "api_key": "Segui la documentazione per ottenere una nuova chiave API." } }, "user": { "data": { "api_key": "Chiave API" + }, + "data_description": { + "api_key": "Segui la documentazione per ottenere la tua chiave API." } } } diff --git a/homeassistant/components/speedtestdotnet/translations/ru.json b/homeassistant/components/speedtestdotnet/translations/ru.json index 3ffcd10bf31..45b3a7da55a 100644 --- a/homeassistant/components/speedtestdotnet/translations/ru.json +++ b/homeassistant/components/speedtestdotnet/translations/ru.json @@ -9,6 +9,19 @@ } } }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u0412\u043c\u0435\u0441\u0442\u043e \u044d\u0442\u043e\u0439 \u0441\u043b\u0443\u0436\u0431\u044b \u0442\u0435\u043f\u0435\u0440\u044c \u0441\u043b\u0435\u0434\u0443\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0441\u043b\u0443\u0436\u0431\u0443 `homeassistant.update_entity` \u0441 \u0446\u0435\u043b\u0435\u0432\u044b\u043c \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u043c Speedtest. \u041e\u0442\u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u0443\u0439\u0442\u0435 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0438 \u0438 \u0441\u043a\u0440\u0438\u043f\u0442\u044b \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 \u041f\u041e\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u042c, \u0447\u0442\u043e\u0431\u044b \u043e\u0442\u043c\u0435\u0442\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443 \u043a\u0430\u043a \u0443\u0441\u0442\u0440\u0430\u043d\u0451\u043d\u043d\u0443\u044e.", + "title": "\u0421\u043b\u0443\u0436\u0431\u0430 speedtest \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + } + } + }, + "title": "\u0421\u043b\u0443\u0436\u0431\u0430 speedtest \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/switch_as_x/translations/it.json b/homeassistant/components/switch_as_x/translations/it.json index 9d7f3c798bf..f2f0be88fc7 100644 --- a/homeassistant/components/switch_as_x/translations/it.json +++ b/homeassistant/components/switch_as_x/translations/it.json @@ -6,9 +6,9 @@ "entity_id": "Interruttore", "target_domain": "Nuovo tipo" }, - "description": "Scegli un interruttore che vuoi mostrare in Home Assistant come luce, copertura o qualsiasi altra cosa. L'interruttore originale sar\u00e0 nascosto." + "description": "Scegli un interruttore che vuoi mostrare in Home Assistant come luce, serranda o qualsiasi altra cosa. L'interruttore originale sar\u00e0 nascosto." } } }, - "title": "Cambia il tipo di dispositivo di un interruttore" + "title": "Cambia tipo di dispositivo di un interruttore" } \ No newline at end of file diff --git a/homeassistant/components/tilt_ble/translations/ca.json b/homeassistant/components/tilt_ble/translations/ca.json new file mode 100644 index 00000000000..0cd4571dc9d --- /dev/null +++ b/homeassistant/components/tilt_ble/translations/ca.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", + "no_devices_found": "No s'han trobat dispositius a la xarxa" + }, + "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/tilt_ble/translations/de.json b/homeassistant/components/tilt_ble/translations/de.json new file mode 100644 index 00000000000..6b3976336d2 --- /dev/null +++ b/homeassistant/components/tilt_ble/translations/de.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Willst 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/tilt_ble/translations/es.json b/homeassistant/components/tilt_ble/translations/es.json new file mode 100644 index 00000000000..76fb203eacd --- /dev/null +++ b/homeassistant/components/tilt_ble/translations/es.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", + "no_devices_found": "No se encontraron dispositivos en la red" + }, + "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/tilt_ble/translations/fr.json b/homeassistant/components/tilt_ble/translations/fr.json new file mode 100644 index 00000000000..c8a1af034cf --- /dev/null +++ b/homeassistant/components/tilt_ble/translations/fr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", + "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Voulez-vous configurer {name}\u00a0?" + }, + "user": { + "data": { + "address": "Appareil" + }, + "description": "S\u00e9lectionnez l'appareil \u00e0 configurer" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tilt_ble/translations/he.json b/homeassistant/components/tilt_ble/translations/he.json new file mode 100644 index 00000000000..de780eb221a --- /dev/null +++ b/homeassistant/components/tilt_ble/translations/he.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + }, + "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/tilt_ble/translations/it.json b/homeassistant/components/tilt_ble/translations/it.json new file mode 100644 index 00000000000..501b5095826 --- /dev/null +++ b/homeassistant/components/tilt_ble/translations/it.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", + "no_devices_found": "Nessun dispositivo trovato sulla rete" + }, + "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/tilt_ble/translations/no.json b/homeassistant/components/tilt_ble/translations/no.json new file mode 100644 index 00000000000..9e85034b506 --- /dev/null +++ b/homeassistant/components/tilt_ble/translations/no.json @@ -0,0 +1,12 @@ +{ + "config": { + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "Enhet" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tilt_ble/translations/pt-BR.json b/homeassistant/components/tilt_ble/translations/pt-BR.json new file mode 100644 index 00000000000..2067d7f9312 --- /dev/null +++ b/homeassistant/components/tilt_ble/translations/pt-BR.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "A configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "no_devices_found": "Nenhum dispositivo encontrado na rede" + }, + "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 From 1802ecfc245f402993b3e81e3dd3a3f7365371c1 Mon Sep 17 00:00:00 2001 From: Yevhenii Vaskivskyi Date: Thu, 8 Sep 2022 08:49:36 +0200 Subject: [PATCH 189/955] Fix `len` method typo for Osram light (#78008) * Fix `len` method typo for Osram light * Fix typo in line 395 --- homeassistant/components/osramlightify/light.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/osramlightify/light.py b/homeassistant/components/osramlightify/light.py index 9fc54d0352c..e1b204115e6 100644 --- a/homeassistant/components/osramlightify/light.py +++ b/homeassistant/components/osramlightify/light.py @@ -373,7 +373,7 @@ class Luminary(LightEntity): self._max_mireds = color_util.color_temperature_kelvin_to_mired( self._luminary.min_temp() or DEFAULT_KELVIN ) - if len(self._attr_supported_color_modes == 1): + if len(self._attr_supported_color_modes) == 1: # The light supports only a single color mode self._attr_color_mode = list(self._attr_supported_color_modes)[0] @@ -392,7 +392,7 @@ class Luminary(LightEntity): if ColorMode.HS in self._attr_supported_color_modes: self._rgb_color = self._luminary.rgb() - if len(self._attr_supported_color_modes > 1): + if len(self._attr_supported_color_modes) > 1: # The light supports hs + color temp, determine which one it is if self._rgb_color == (0, 0, 0): self._attr_color_mode = ColorMode.COLOR_TEMP From 37631d20179f808facbef2d6a30b16187ede2f26 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 8 Sep 2022 03:13:01 -0400 Subject: [PATCH 190/955] Add value ID to zwave_js device diagnostics (#78015) --- homeassistant/components/zwave_js/diagnostics.py | 1 + tests/components/zwave_js/test_diagnostics.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/homeassistant/components/zwave_js/diagnostics.py b/homeassistant/components/zwave_js/diagnostics.py index 078bd761b71..33d32e96fe0 100644 --- a/homeassistant/components/zwave_js/diagnostics.py +++ b/homeassistant/components/zwave_js/diagnostics.py @@ -123,6 +123,7 @@ def get_device_entities( "entity_category": entry.entity_category, "supported_features": entry.supported_features, "unit_of_measurement": entry.unit_of_measurement, + "value_id": value_id, "primary_value": primary_value_data, } entities.append(entity) diff --git a/tests/components/zwave_js/test_diagnostics.py b/tests/components/zwave_js/test_diagnostics.py index 9f3a7b0884c..41505364111 100644 --- a/tests/components/zwave_js/test_diagnostics.py +++ b/tests/components/zwave_js/test_diagnostics.py @@ -152,6 +152,7 @@ async def test_device_diagnostics_missing_primary_value( x for x in diagnostics_data["entities"] if x["entity_id"] == entity_id ) + assert air_entity["value_id"] == value.value_id assert air_entity["primary_value"] == { "command_class": value.command_class, "command_class_name": value.command_class_name, @@ -189,4 +190,5 @@ async def test_device_diagnostics_missing_primary_value( x for x in diagnostics_data["entities"] if x["entity_id"] == entity_id ) + assert air_entity["value_id"] == value.value_id assert air_entity["primary_value"] is None From 23168434d5f034cc85b585be4ab47ea5779ce3a8 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 8 Sep 2022 09:14:58 +0200 Subject: [PATCH 191/955] Add pylint directory to black pre-commit (#78011) Add pylint to black CI --- .pre-commit-config.yaml | 2 +- pylint/plugins/hass_imports.py | 13 ++++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cb9a5ed04dc..56faefc1a85 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ repos: args: - --safe - --quiet - files: ^((homeassistant|script|tests)/.+)?[^/]+\.py$ + files: ^((homeassistant|pylint|script|tests)/.+)?[^/]+\.py$ - repo: https://github.com/codespell-project/codespell rev: v2.1.0 hooks: diff --git a/pylint/plugins/hass_imports.py b/pylint/plugins/hass_imports.py index 6068e1c2baf..a45e2c91996 100644 --- a/pylint/plugins/hass_imports.py +++ b/pylint/plugins/hass_imports.py @@ -303,9 +303,13 @@ class HassImportsFormatChecker(BaseChecker): # type: ignore[misc] if module.startswith(f"{self.current_package}."): self.add_message("hass-relative-import", node=node) - def _visit_importfrom_relative(self, current_package: str, node: nodes.ImportFrom) -> None: + def _visit_importfrom_relative( + self, current_package: str, node: nodes.ImportFrom + ) -> None: """Called when a ImportFrom node is visited.""" - if node.level <= 1 or not current_package.startswith("homeassistant.components"): + if node.level <= 1 or not current_package.startswith( + "homeassistant.components" + ): return split_package = current_package.split(".") if not node.modname and len(split_package) == node.level + 1: @@ -330,7 +334,10 @@ class HassImportsFormatChecker(BaseChecker): # type: ignore[misc] ): self.add_message("hass-relative-import", node=node) return - if self.current_package.startswith("homeassistant.components") and node.modname == "homeassistant.components": + if ( + self.current_package.startswith("homeassistant.components") + and node.modname == "homeassistant.components" + ): for name in node.names: if name[0] == self.current_package.split(".")[2]: self.add_message("hass-relative-import", node=node) From f87e873275305184585634f11e6256a72d6f40e1 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 8 Sep 2022 09:45:22 +0200 Subject: [PATCH 192/955] Adjust alexa imports (#78013) --- homeassistant/components/alexa/capabilities.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index dfac5c89ffd..15870c7bbfa 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -5,12 +5,14 @@ import logging from homeassistant.components import ( button, + climate, cover, fan, image_processing, input_button, input_number, light, + media_player, timer, vacuum, ) @@ -18,8 +20,6 @@ from homeassistant.components.alarm_control_panel import ( AlarmControlPanelEntityFeature, CodeFormat, ) -import homeassistant.components.climate.const as climate -import homeassistant.components.media_player.const as media_player from homeassistant.const import ( ATTR_CODE_FORMAT, ATTR_SUPPORTED_FEATURES, From e2568d83758cfc7e282f1aa9fecee18dea9c2331 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 8 Sep 2022 10:34:44 +0200 Subject: [PATCH 193/955] Import climate constants from root [m-z] (#78020) --- homeassistant/components/melcloud/climate.py | 4 ++-- homeassistant/components/modbus/climate.py | 7 +++++-- homeassistant/components/moehlenhoff_alpha2/climate.py | 4 ++-- homeassistant/components/mysensors/climate.py | 4 ++-- homeassistant/components/nexia/climate.py | 4 ++-- homeassistant/components/nuheat/climate.py | 4 ++-- homeassistant/components/oem/climate.py | 5 +++-- .../climate_entities/atlantic_pass_apc_zone_control.py | 3 +-- homeassistant/components/plugwise/climate.py | 4 ++-- homeassistant/components/proliphix/climate.py | 5 +++-- homeassistant/components/prometheus/__init__.py | 2 +- homeassistant/components/schluter/climate.py | 2 -- homeassistant/components/screenlogic/climate.py | 4 ++-- homeassistant/components/sensibo/climate.py | 7 +++++-- homeassistant/components/senz/climate.py | 4 ++-- homeassistant/components/smartthings/climate.py | 5 +++-- homeassistant/components/spider/climate.py | 7 +++++-- homeassistant/components/touchline/climate.py | 8 ++++++-- homeassistant/components/velbus/climate.py | 7 +++++-- homeassistant/components/xs1/climate.py | 7 +++++-- homeassistant/components/zha/sensor.py | 2 +- homeassistant/components/zhong_hong/climate.py | 5 +++-- homeassistant/components/zwave_me/climate.py | 7 +++++-- 23 files changed, 67 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/melcloud/climate.py b/homeassistant/components/melcloud/climate.py index 7f2e2e5c6ca..9fc455893f6 100644 --- a/homeassistant/components/melcloud/climate.py +++ b/homeassistant/components/melcloud/climate.py @@ -14,11 +14,11 @@ from pymelcloud.atw_device import ( ) import voluptuous as vol -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_HVAC_MODE, DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, + ClimateEntity, ClimateEntityFeature, HVACMode, ) diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index eb2706c6334..7d6376a5a42 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -5,8 +5,11 @@ from datetime import datetime import struct from typing import Any -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ClimateEntityFeature, HVACMode +from homeassistant.components.climate import ( + ClimateEntity, + ClimateEntityFeature, + HVACMode, +) from homeassistant.const import ( ATTR_TEMPERATURE, CONF_NAME, diff --git a/homeassistant/components/moehlenhoff_alpha2/climate.py b/homeassistant/components/moehlenhoff_alpha2/climate.py index b68e48f83c7..5eac2095706 100644 --- a/homeassistant/components/moehlenhoff_alpha2/climate.py +++ b/homeassistant/components/moehlenhoff_alpha2/climate.py @@ -2,8 +2,8 @@ import logging from typing import Any -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, diff --git a/homeassistant/components/mysensors/climate.py b/homeassistant/components/mysensors/climate.py index 3e540bd5714..435bf2ffddb 100644 --- a/homeassistant/components/mysensors/climate.py +++ b/homeassistant/components/mysensors/climate.py @@ -3,10 +3,10 @@ from __future__ import annotations from typing import Any -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, + ClimateEntity, ClimateEntityFeature, HVACMode, ) diff --git a/homeassistant/components/nexia/climate.py b/homeassistant/components/nexia/climate.py index 7a487d0b975..81e6158a872 100644 --- a/homeassistant/components/nexia/climate.py +++ b/homeassistant/components/nexia/climate.py @@ -20,12 +20,12 @@ from nexia.util import find_humidity_setpoint from nexia.zone import NexiaThermostatZone import voluptuous as vol -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_HUMIDITY, ATTR_HVAC_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, diff --git a/homeassistant/components/nuheat/climate.py b/homeassistant/components/nuheat/climate.py index 78e93ad5cea..c731e3472d6 100644 --- a/homeassistant/components/nuheat/climate.py +++ b/homeassistant/components/nuheat/climate.py @@ -12,9 +12,9 @@ from nuheat.util import ( nuheat_to_fahrenheit, ) -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_HVAC_MODE, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, diff --git a/homeassistant/components/oem/climate.py b/homeassistant/components/oem/climate.py index 3f0110699eb..d6337807fc2 100644 --- a/homeassistant/components/oem/climate.py +++ b/homeassistant/components/oem/climate.py @@ -7,8 +7,9 @@ from oemthermostat import Thermostat import requests import voluptuous as vol -from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( + PLATFORM_SCHEMA, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, diff --git a/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_zone_control.py b/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_zone_control.py index fc1d909390b..bdb204d4ba7 100644 --- a/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_zone_control.py +++ b/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_zone_control.py @@ -3,8 +3,7 @@ from typing import cast from pyoverkiz.enums import OverkizCommand, OverkizCommandParam, OverkizState -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import HVACMode +from homeassistant.components.climate import ClimateEntity, HVACMode from homeassistant.components.overkiz.entity import OverkizEntity from homeassistant.const import TEMP_CELSIUS diff --git a/homeassistant/components/plugwise/climate.py b/homeassistant/components/plugwise/climate.py index 9729ad745fd..155c4f73bb6 100644 --- a/homeassistant/components/plugwise/climate.py +++ b/homeassistant/components/plugwise/climate.py @@ -4,8 +4,8 @@ from __future__ import annotations from collections.abc import Mapping from typing import Any -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, diff --git a/homeassistant/components/proliphix/climate.py b/homeassistant/components/proliphix/climate.py index e58ddda0605..4ff1ff0906c 100644 --- a/homeassistant/components/proliphix/climate.py +++ b/homeassistant/components/proliphix/climate.py @@ -6,8 +6,9 @@ from typing import Any import proliphix import voluptuous as vol -from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( + PLATFORM_SCHEMA, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, diff --git a/homeassistant/components/prometheus/__init__.py b/homeassistant/components/prometheus/__init__.py index 949895cb76a..d54074088b0 100644 --- a/homeassistant/components/prometheus/__init__.py +++ b/homeassistant/components/prometheus/__init__.py @@ -8,7 +8,7 @@ import prometheus_client import voluptuous as vol from homeassistant import core as hacore -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_CURRENT_TEMPERATURE, ATTR_HVAC_ACTION, ATTR_HVAC_MODES, diff --git a/homeassistant/components/schluter/climate.py b/homeassistant/components/schluter/climate.py index dd4f578cebb..24a09437d4b 100644 --- a/homeassistant/components/schluter/climate.py +++ b/homeassistant/components/schluter/climate.py @@ -12,8 +12,6 @@ from homeassistant.components.climate import ( SCAN_INTERVAL, TEMP_CELSIUS, ClimateEntity, -) -from homeassistant.components.climate.const import ( ClimateEntityFeature, HVACAction, HVACMode, diff --git a/homeassistant/components/screenlogic/climate.py b/homeassistant/components/screenlogic/climate.py index b5e83ec827a..4b28eea7208 100644 --- a/homeassistant/components/screenlogic/climate.py +++ b/homeassistant/components/screenlogic/climate.py @@ -4,9 +4,9 @@ from typing import Any from screenlogicpy.const import DATA as SL_DATA, EQUIPMENT, HEAT_MODE -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_PRESET_MODE, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, diff --git a/homeassistant/components/sensibo/climate.py b/homeassistant/components/sensibo/climate.py index eda60458d4f..8bd225f2e4d 100644 --- a/homeassistant/components/sensibo/climate.py +++ b/homeassistant/components/sensibo/climate.py @@ -7,8 +7,11 @@ from typing import TYPE_CHECKING, Any from pysensibo.model import SensiboDevice import voluptuous as vol -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ClimateEntityFeature, HVACMode +from homeassistant.components.climate import ( + ClimateEntity, + ClimateEntityFeature, + HVACMode, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_STATE, diff --git a/homeassistant/components/senz/climate.py b/homeassistant/components/senz/climate.py index d47ae7a4a85..2e023e13491 100644 --- a/homeassistant/components/senz/climate.py +++ b/homeassistant/components/senz/climate.py @@ -5,8 +5,8 @@ from typing import Any from aiosenz import MODE_AUTO, Thermostat -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, diff --git a/homeassistant/components/smartthings/climate.py b/homeassistant/components/smartthings/climate.py index 7f628b8d8a0..8a516dfc356 100644 --- a/homeassistant/components/smartthings/climate.py +++ b/homeassistant/components/smartthings/climate.py @@ -8,11 +8,12 @@ from typing import Any from pysmartthings import Attribute, Capability -from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN, ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_HVAC_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, + DOMAIN as CLIMATE_DOMAIN, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, diff --git a/homeassistant/components/spider/climate.py b/homeassistant/components/spider/climate.py index c372d64e095..6c02e8485c4 100644 --- a/homeassistant/components/spider/climate.py +++ b/homeassistant/components/spider/climate.py @@ -1,8 +1,11 @@ """Support for Spider thermostats.""" from typing import Any -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ClimateEntityFeature, HVACMode +from homeassistant.components.climate import ( + ClimateEntity, + ClimateEntityFeature, + HVACMode, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/touchline/climate.py b/homeassistant/components/touchline/climate.py index bd7bdce4efe..4ce1c4553cb 100644 --- a/homeassistant/components/touchline/climate.py +++ b/homeassistant/components/touchline/climate.py @@ -6,8 +6,12 @@ from typing import Any, NamedTuple from pytouchline import PyTouchline import voluptuous as vol -from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity -from homeassistant.components.climate.const import ClimateEntityFeature, HVACMode +from homeassistant.components.climate import ( + PLATFORM_SCHEMA, + ClimateEntity, + ClimateEntityFeature, + HVACMode, +) from homeassistant.const import ATTR_TEMPERATURE, CONF_HOST, TEMP_CELSIUS from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/velbus/climate.py b/homeassistant/components/velbus/climate.py index 18e01d3ae87..a6549f0262c 100644 --- a/homeassistant/components/velbus/climate.py +++ b/homeassistant/components/velbus/climate.py @@ -5,8 +5,11 @@ from typing import Any from velbusaio.channels import Temperature as VelbusTemp -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ClimateEntityFeature, HVACMode +from homeassistant.components.climate import ( + ClimateEntity, + ClimateEntityFeature, + HVACMode, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/xs1/climate.py b/homeassistant/components/xs1/climate.py index f74950437df..4c4f6682ffa 100644 --- a/homeassistant/components/xs1/climate.py +++ b/homeassistant/components/xs1/climate.py @@ -5,8 +5,11 @@ from typing import Any from xs1_api_client.api_constants import ActuatorType -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ClimateEntityFeature, HVACMode +from homeassistant.components.climate import ( + ClimateEntity, + ClimateEntityFeature, + HVACMode, +) from homeassistant.const import ATTR_TEMPERATURE from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 61fad097fc8..74ec924af78 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -5,7 +5,7 @@ import functools import numbers from typing import TYPE_CHECKING, Any, TypeVar -from homeassistant.components.climate.const import HVACAction +from homeassistant.components.climate import HVACAction from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, diff --git a/homeassistant/components/zhong_hong/climate.py b/homeassistant/components/zhong_hong/climate.py index 7566f5166e6..265d4d15506 100644 --- a/homeassistant/components/zhong_hong/climate.py +++ b/homeassistant/components/zhong_hong/climate.py @@ -8,9 +8,10 @@ import voluptuous as vol from zhong_hong_hvac.hub import ZhongHongGateway from zhong_hong_hvac.hvac import HVAC as ZhongHongHVAC -from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_HVAC_MODE, + PLATFORM_SCHEMA, + ClimateEntity, ClimateEntityFeature, HVACMode, ) diff --git a/homeassistant/components/zwave_me/climate.py b/homeassistant/components/zwave_me/climate.py index 63903478b51..7d654311213 100644 --- a/homeassistant/components/zwave_me/climate.py +++ b/homeassistant/components/zwave_me/climate.py @@ -5,8 +5,11 @@ from typing import Any from zwave_me_ws import ZWaveMeData -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ClimateEntityFeature, HVACMode +from homeassistant.components.climate import ( + ClimateEntity, + ClimateEntityFeature, + HVACMode, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE from homeassistant.core import HomeAssistant, callback From 5276d849ec497ccd0cecf3cb6a8dacae4fa6f845 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 8 Sep 2022 10:46:23 +0200 Subject: [PATCH 194/955] Improve type hints in apple_tv media player (#77940) --- .coveragerc | 1 + .../components/apple_tv/browse_media.py | 29 ++-- .../components/apple_tv/media_player.py | 155 ++++++++---------- .../components/media_player/__init__.py | 1 + 4 files changed, 86 insertions(+), 100 deletions(-) diff --git a/.coveragerc b/.coveragerc index 7fded7dd8b2..23531c159fd 100644 --- a/.coveragerc +++ b/.coveragerc @@ -64,6 +64,7 @@ omit = homeassistant/components/anthemav/media_player.py homeassistant/components/apcupsd/* homeassistant/components/apple_tv/__init__.py + homeassistant/components/apple_tv/browse_media.py homeassistant/components/apple_tv/media_player.py homeassistant/components/apple_tv/remote.py homeassistant/components/aqualogic/* diff --git a/homeassistant/components/apple_tv/browse_media.py b/homeassistant/components/apple_tv/browse_media.py index 0673c9923fb..9944c49a823 100644 --- a/homeassistant/components/apple_tv/browse_media.py +++ b/homeassistant/components/apple_tv/browse_media.py @@ -1,34 +1,29 @@ """Support for media browsing.""" +from typing import Any -from homeassistant.components.media_player import BrowseMedia -from homeassistant.components.media_player.const import ( - MEDIA_CLASS_APP, - MEDIA_CLASS_DIRECTORY, - MEDIA_TYPE_APP, - MEDIA_TYPE_APPS, -) +from homeassistant.components.media_player import BrowseMedia, MediaClass, MediaType -def build_app_list(app_list): +def build_app_list(app_list: dict[str, str]) -> BrowseMedia: """Create response payload for app list.""" - app_list = [ - {"app_id": app_id, "title": app_name, "type": MEDIA_TYPE_APP} + media_list = [ + {"app_id": app_id, "title": app_name, "type": MediaType.APP} for app_name, app_id in app_list.items() ] return BrowseMedia( - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_id="apps", - media_content_type=MEDIA_TYPE_APPS, + media_content_type=MediaType.APPS, title="Apps", can_play=False, can_expand=True, - children=[item_payload(item) for item in app_list], - children_media_class=MEDIA_CLASS_APP, + children=[item_payload(item) for item in media_list], + children_media_class=MediaClass.APP, ) -def item_payload(item): +def item_payload(item: dict[str, Any]) -> BrowseMedia: """ Create response payload for a single media item. @@ -36,8 +31,8 @@ def item_payload(item): """ return BrowseMedia( title=item["title"], - media_class=MEDIA_CLASS_APP, - media_content_type=MEDIA_TYPE_APP, + media_class=MediaClass.APP, + media_content_type=MediaType.APP, media_content_id=item["app_id"], can_play=False, can_expand=False, diff --git a/homeassistant/components/apple_tv/media_player.py b/homeassistant/components/apple_tv/media_player.py index 771b27a6dc3..06618e4f2a3 100644 --- a/homeassistant/components/apple_tv/media_player.py +++ b/homeassistant/components/apple_tv/media_player.py @@ -1,6 +1,7 @@ """Support for Apple TV media player.""" from __future__ import annotations +from datetime import datetime import logging from typing import Any @@ -9,45 +10,31 @@ from pyatv.const import ( DeviceState, FeatureName, FeatureState, - MediaType, + MediaType as AppleMediaType, PowerState, RepeatState, ShuffleState, ) from pyatv.helpers import is_streamable +from pyatv.interface import AppleTV, Playing from homeassistant.components import media_source from homeassistant.components.media_player import ( BrowseMedia, MediaPlayerEntity, MediaPlayerEntityFeature, -) -from homeassistant.components.media_player.browse_media import ( + MediaPlayerState, + MediaType, + RepeatMode, async_process_play_media_url, ) -from homeassistant.components.media_player.const import ( - MEDIA_TYPE_APP, - MEDIA_TYPE_MUSIC, - MEDIA_TYPE_TVSHOW, - MEDIA_TYPE_VIDEO, - REPEAT_MODE_ALL, - REPEAT_MODE_OFF, - REPEAT_MODE_ONE, -) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - CONF_NAME, - STATE_IDLE, - STATE_OFF, - STATE_PAUSED, - STATE_PLAYING, - STATE_STANDBY, -) +from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.dt as dt_util -from . import AppleTVEntity +from . import AppleTVEntity, AppleTVManager from .browse_media import build_app_list from .const import DOMAIN @@ -108,8 +95,9 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Load Apple TV media player based on a config entry.""" - name = config_entry.data[CONF_NAME] - manager = hass.data[DOMAIN][config_entry.unique_id] + name: str = config_entry.data[CONF_NAME] + assert config_entry.unique_id is not None + manager: AppleTVManager = hass.data[DOMAIN][config_entry.unique_id] async_add_entities([AppleTvMediaPlayer(name, config_entry.unique_id, manager)]) @@ -118,14 +106,14 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): _attr_supported_features = SUPPORT_APPLE_TV - def __init__(self, name, identifier, manager, **kwargs): + def __init__(self, name: str, identifier: str, manager: AppleTVManager) -> None: """Initialize the Apple TV media player.""" - super().__init__(name, identifier, manager, **kwargs) - self._playing = None - self._app_list = {} + super().__init__(name, identifier, manager) + self._playing: Playing | None = None + self._app_list: dict[str, str] = {} @callback - def async_device_connected(self, atv): + def async_device_connected(self, atv: AppleTV) -> None: """Handle when connection is made to device.""" # NB: Do not use _is_feature_available here as it only works when playing if self.atv.features.in_state(FeatureState.Available, FeatureName.PushUpdates): @@ -153,7 +141,7 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): if self.atv.features.in_state(FeatureState.Available, FeatureName.AppList): self.hass.create_task(self._update_app_list()) - async def _update_app_list(self): + async def _update_app_list(self) -> None: _LOGGER.debug("Updating app list") try: apps = await self.atv.apps.app_list() @@ -165,127 +153,128 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): self._app_list = { app.name: app.identifier for app in sorted(apps, key=lambda app: app.name.lower()) + if app.name is not None } self.async_write_ha_state() @callback - def async_device_disconnected(self): + def async_device_disconnected(self) -> None: """Handle when connection was lost to device.""" self._attr_supported_features = SUPPORT_APPLE_TV @property - def state(self): + def state(self) -> MediaPlayerState | None: """Return the state of the device.""" if self.manager.is_connecting: return None if self.atv is None: - return STATE_OFF + return MediaPlayerState.OFF if ( self._is_feature_available(FeatureName.PowerState) and self.atv.power.power_state == PowerState.Off ): - return STATE_STANDBY + return MediaPlayerState.STANDBY if self._playing: state = self._playing.device_state if state in (DeviceState.Idle, DeviceState.Loading): - return STATE_IDLE + return MediaPlayerState.IDLE if state == DeviceState.Playing: - return STATE_PLAYING + return MediaPlayerState.PLAYING if state in (DeviceState.Paused, DeviceState.Seeking, DeviceState.Stopped): - return STATE_PAUSED - return STATE_STANDBY # Bad or unknown state? + return MediaPlayerState.PAUSED + return MediaPlayerState.STANDBY # Bad or unknown state? return None @callback - def playstatus_update(self, _, playing): + def playstatus_update(self, _, playing: Playing) -> None: """Print what is currently playing when it changes.""" self._playing = playing self.async_write_ha_state() @callback - def playstatus_error(self, _, exception): + def playstatus_error(self, _, exception: Exception) -> None: """Inform about an error and restart push updates.""" _LOGGER.warning("A %s error occurred: %s", exception.__class__, exception) self._playing = None self.async_write_ha_state() @callback - def powerstate_update(self, old_state: PowerState, new_state: PowerState): + def powerstate_update(self, old_state: PowerState, new_state: PowerState) -> None: """Update power state when it changes.""" self.async_write_ha_state() @property - def app_id(self): + def app_id(self) -> str | None: """ID of the current running app.""" if self._is_feature_available(FeatureName.App): return self.atv.metadata.app.identifier return None @property - def app_name(self): + def app_name(self) -> str | None: """Name of the current running app.""" if self._is_feature_available(FeatureName.App): return self.atv.metadata.app.name return None @property - def source_list(self): + def source_list(self) -> list[str]: """List of available input sources.""" return list(self._app_list.keys()) @property - def media_content_type(self): + def media_content_type(self) -> MediaType | None: """Content type of current playing media.""" if self._playing: return { - MediaType.Video: MEDIA_TYPE_VIDEO, - MediaType.Music: MEDIA_TYPE_MUSIC, - MediaType.TV: MEDIA_TYPE_TVSHOW, + AppleMediaType.Video: MediaType.VIDEO, + AppleMediaType.Music: MediaType.MUSIC, + AppleMediaType.TV: MediaType.TVSHOW, }.get(self._playing.media_type) return None @property - def media_content_id(self): + def media_content_id(self) -> str | None: """Content ID of current playing media.""" if self._playing: return self._playing.content_identifier return None @property - def volume_level(self): + def volume_level(self) -> float | None: """Volume level of the media player (0..1).""" if self._is_feature_available(FeatureName.Volume): return self.atv.audio.volume / 100.0 # from percent return None @property - def media_duration(self): + def media_duration(self) -> int | None: """Duration of current playing media in seconds.""" if self._playing: return self._playing.total_time return None @property - def media_position(self): + def media_position(self) -> int | None: """Position of current playing media in seconds.""" if self._playing: return self._playing.position return None @property - def media_position_updated_at(self): + def media_position_updated_at(self) -> datetime | None: """Last valid time of media position.""" - if self.state in (STATE_PLAYING, STATE_PAUSED): + if self.state in {MediaPlayerState.PLAYING, MediaPlayerState.PAUSED}: return dt_util.utcnow() return None async def async_play_media( - self, media_type: str, media_id: str, **kwargs: Any + self, media_type: MediaType | str, media_id: str, **kwargs: Any ) -> None: """Send the play_media command to the media player.""" # If input (file) has a file format supported by pyatv, then stream it with # RAOP. Otherwise try to play it with regular AirPlay. - if media_type == MEDIA_TYPE_APP: + if media_type == MediaType.APP: await self.atv.apps.launch_app(media_id) return @@ -294,10 +283,10 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): self.hass, media_id, self.entity_id ) media_id = async_process_play_media_url(self.hass, play_item.url) - media_type = MEDIA_TYPE_MUSIC + media_type = MediaType.MUSIC if self._is_feature_available(FeatureName.StreamFile) and ( - media_type == MEDIA_TYPE_MUSIC or await is_streamable(media_id) + media_type == MediaType.MUSIC or await is_streamable(media_id) ): _LOGGER.debug("Streaming %s via RAOP", media_id) await self.atv.stream.stream_file(media_id) @@ -308,13 +297,13 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): _LOGGER.error("Media streaming is not possible with current configuration") @property - def media_image_hash(self): + def media_image_hash(self) -> str | None: """Hash value for media image.""" state = self.state if ( self._playing and self._is_feature_available(FeatureName.Artwork) - and state not in [None, STATE_OFF, STATE_IDLE] + and state not in {None, MediaPlayerState.OFF, MediaPlayerState.IDLE} ): return self.atv.metadata.artwork_id return None @@ -322,7 +311,7 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): async def async_get_media_image(self) -> tuple[bytes | None, str | None]: """Fetch media image of current playing image.""" state = self.state - if self._playing and state not in [STATE_OFF, STATE_IDLE]: + if self._playing and state not in {MediaPlayerState.OFF, MediaPlayerState.IDLE}: artwork = await self.atv.metadata.artwork() if artwork: return artwork.bytes, artwork.mimetype @@ -330,65 +319,65 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): return None, None @property - def media_title(self): + def media_title(self) -> str | None: """Title of current playing media.""" if self._playing: return self._playing.title return None @property - def media_artist(self): + def media_artist(self) -> str | None: """Artist of current playing media, music track only.""" - if self._is_feature_available(FeatureName.Artist): + if self._playing and self._is_feature_available(FeatureName.Artist): return self._playing.artist return None @property - def media_album_name(self): + def media_album_name(self) -> str | None: """Album name of current playing media, music track only.""" - if self._is_feature_available(FeatureName.Album): + if self._playing and self._is_feature_available(FeatureName.Album): return self._playing.album return None @property - def media_series_title(self): + def media_series_title(self) -> str | None: """Title of series of current playing media, TV show only.""" - if self._is_feature_available(FeatureName.SeriesName): + if self._playing and self._is_feature_available(FeatureName.SeriesName): return self._playing.series_name return None @property - def media_season(self): + def media_season(self) -> str | None: """Season of current playing media, TV show only.""" - if self._is_feature_available(FeatureName.SeasonNumber): + if self._playing and self._is_feature_available(FeatureName.SeasonNumber): return str(self._playing.season_number) return None @property - def media_episode(self): + def media_episode(self) -> str | None: """Episode of current playing media, TV show only.""" - if self._is_feature_available(FeatureName.EpisodeNumber): + if self._playing and self._is_feature_available(FeatureName.EpisodeNumber): return str(self._playing.episode_number) return None @property - def repeat(self): + def repeat(self) -> RepeatMode | None: """Return current repeat mode.""" - if self._is_feature_available(FeatureName.Repeat): + if self._playing and self._is_feature_available(FeatureName.Repeat): return { - RepeatState.Track: REPEAT_MODE_ONE, - RepeatState.All: REPEAT_MODE_ALL, - }.get(self._playing.repeat, REPEAT_MODE_OFF) + RepeatState.Track: RepeatMode.ONE, + RepeatState.All: RepeatMode.ALL, + }.get(self._playing.repeat, RepeatMode.OFF) return None @property - def shuffle(self): + def shuffle(self) -> bool | None: """Boolean if shuffle is enabled.""" - if self._is_feature_available(FeatureName.Shuffle): + if self._playing and self._is_feature_available(FeatureName.Shuffle): return self._playing.shuffle != ShuffleState.Off return None - def _is_feature_available(self, feature): + def _is_feature_available(self, feature: FeatureName) -> bool: """Return if a feature is available.""" if self.atv and self._playing: return self.atv.features.in_state(FeatureState.Available, feature) @@ -396,7 +385,7 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): async def async_browse_media( self, - media_content_type: str | None = None, + media_content_type: MediaType | str | None = None, media_content_id: str | None = None, ) -> BrowseMedia: """Implement the websocket media browsing helper.""" @@ -496,12 +485,12 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): # pyatv expects volume in percent await self.atv.audio.set_volume(volume * 100.0) - async def async_set_repeat(self, repeat: str) -> None: + async def async_set_repeat(self, repeat: RepeatMode) -> None: """Set repeat mode.""" if self.atv: mode = { - REPEAT_MODE_ONE: RepeatState.Track, - REPEAT_MODE_ALL: RepeatState.All, + RepeatMode.ONE: RepeatState.Track, + RepeatMode.ALL: RepeatState.All, }.get(repeat, RepeatState.Off) await self.atv.remote_control.set_repeat(mode) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index e607f312df6..2c24a43ffc3 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -127,6 +127,7 @@ from .const import ( # noqa: F401 SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, + MediaClass, MediaPlayerEntityFeature, MediaPlayerState, MediaType, From 11e897a5e84ccbf5ac9ed7da81761648ece4c808 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Thu, 8 Sep 2022 10:50:14 +0200 Subject: [PATCH 195/955] Extract lametric device from coordinator in notify (#78027) --- homeassistant/components/lametric/notify.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lametric/notify.py b/homeassistant/components/lametric/notify.py index 4b404840388..c9ae376c496 100644 --- a/homeassistant/components/lametric/notify.py +++ b/homeassistant/components/lametric/notify.py @@ -21,6 +21,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import CONF_CYCLES, CONF_ICON_TYPE, CONF_PRIORITY, CONF_SOUND, DOMAIN +from .coordinator import LaMetricDataUpdateCoordinator async def async_get_service( @@ -31,8 +32,10 @@ async def async_get_service( """Get the LaMetric notification service.""" if discovery_info is None: return None - lametric: LaMetricDevice = hass.data[DOMAIN][discovery_info["entry_id"]] - return LaMetricNotificationService(lametric) + coordinator: LaMetricDataUpdateCoordinator = hass.data[DOMAIN][ + discovery_info["entry_id"] + ] + return LaMetricNotificationService(coordinator.lametric) class LaMetricNotificationService(BaseNotificationService): From bfe245cc3fad09d1761ab7290dab683f341b5d89 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 8 Sep 2022 10:59:40 +0200 Subject: [PATCH 196/955] Fix zwave_js default emulate hardware in options flow (#78024) --- .../components/zwave_js/config_flow.py | 4 ++- tests/components/zwave_js/test_config_flow.py | 30 +++++++++++++++++-- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index 3da785cdcf2..c114662888f 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -792,7 +792,9 @@ class OptionsFlowHandler(BaseZwaveJSFlow, config_entries.OptionsFlow): CONF_ADDON_S2_AUTHENTICATED_KEY: self.s2_authenticated_key, CONF_ADDON_S2_UNAUTHENTICATED_KEY: self.s2_unauthenticated_key, CONF_ADDON_LOG_LEVEL: user_input[CONF_LOG_LEVEL], - CONF_ADDON_EMULATE_HARDWARE: user_input[CONF_EMULATE_HARDWARE], + CONF_ADDON_EMULATE_HARDWARE: user_input.get( + CONF_EMULATE_HARDWARE, False + ), } if new_addon_config != addon_config: diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index 6c4b18e8dc3..d4f159f2510 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -1951,6 +1951,30 @@ async def different_device_server_version(*args): 0, different_device_server_version, ), + ( + {"config": ADDON_DISCOVERY_INFO}, + {}, + { + "device": "/test", + "network_key": "old123", + "s0_legacy_key": "old123", + "s2_access_control_key": "old456", + "s2_authenticated_key": "old789", + "s2_unauthenticated_key": "old987", + "log_level": "info", + }, + { + "usb_path": "/new", + "s0_legacy_key": "new123", + "s2_access_control_key": "new456", + "s2_authenticated_key": "new789", + "s2_unauthenticated_key": "new987", + "log_level": "info", + "emulate_hardware": False, + }, + 0, + different_device_server_version, + ), ], ) async def test_options_different_device( @@ -2018,14 +2042,16 @@ async def test_options_different_device( result = await hass.config_entries.options.async_configure(result["flow_id"]) await hass.async_block_till_done() + # Default emulate_hardware is False. + addon_options = {"emulate_hardware": False} | old_addon_options # Legacy network key is not reset. - old_addon_options.pop("network_key") + addon_options.pop("network_key") assert set_addon_options.call_count == 2 assert set_addon_options.call_args == call( hass, "core_zwave_js", - {"options": old_addon_options}, + {"options": addon_options}, ) assert result["type"] == "progress" assert result["step_id"] == "start_addon" From 9a5fe950a4895b3bc20664f9d41605998049a1fe Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 8 Sep 2022 11:03:10 +0200 Subject: [PATCH 197/955] Use new media player enums [a-d] (#77939) --- .../components/androidtv/media_player.py | 22 +++++----- .../components/anthemav/media_player.py | 14 +++---- .../components/aquostv/media_player.py | 9 ++--- .../components/arcam_fmj/media_player.py | 34 ++++++++-------- .../components/blackbird/media_player.py | 5 +-- .../components/bluesound/media_player.py | 15 ++++--- .../components/braviatv/coordinator.py | 11 ++--- .../components/braviatv/media_player.py | 15 ++++--- .../components/channels/media_player.py | 40 ++++++------------- .../components/clementine/media_player.py | 33 ++++++--------- homeassistant/components/cmus/media_player.py | 34 ++++++---------- .../components/denon/media_player.py | 9 +++-- .../components/denonavr/media_player.py | 22 ++++------ .../components/directv/media_player.py | 31 ++++++-------- .../components/dunehd/media_player.py | 14 +++---- 15 files changed, 131 insertions(+), 177 deletions(-) diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index 696fab5788f..f9004de1c52 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -23,6 +23,7 @@ from homeassistant.components import persistent_notification from homeassistant.components.media_player import ( MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -33,11 +34,6 @@ from homeassistant.const import ( ATTR_SW_VERSION, CONF_HOST, CONF_NAME, - STATE_IDLE, - STATE_OFF, - STATE_PAUSED, - STATE_PLAYING, - STATE_STANDBY, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform @@ -85,11 +81,11 @@ PREFIX_FIRETV = "Fire TV" # Translate from `AndroidTV` / `FireTV` reported state to HA state. ANDROIDTV_STATES = { - "off": STATE_OFF, - "idle": STATE_IDLE, - "standby": STATE_STANDBY, - "playing": STATE_PLAYING, - "paused": STATE_PAUSED, + "off": MediaPlayerState.OFF, + "idle": MediaPlayerState.IDLE, + "standby": MediaPlayerState.STANDBY, + "playing": MediaPlayerState.PLAYING, + "paused": MediaPlayerState.PAUSED, } @@ -323,7 +319,11 @@ class ADBDevice(MediaPlayerEntity): async def async_get_media_image(self) -> tuple[bytes | None, str | None]: """Fetch current playing image.""" - if not self._screencap or self.state in (STATE_OFF, None) or not self.available: + if ( + not self._screencap + or self.state in {MediaPlayerState.OFF, None} + or not self.available + ): return None, None media_data = await self._adb_screencap() diff --git a/homeassistant/components/anthemav/media_player.py b/homeassistant/components/anthemav/media_player.py index a854ea0653e..0c5837e154e 100644 --- a/homeassistant/components/anthemav/media_player.py +++ b/homeassistant/components/anthemav/media_player.py @@ -12,16 +12,10 @@ from homeassistant.components.media_player import ( MediaPlayerDeviceClass, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, ) from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import ( - CONF_HOST, - CONF_MAC, - CONF_NAME, - CONF_PORT, - STATE_OFF, - STATE_ON, -) +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_PORT from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -163,7 +157,9 @@ class AnthemAVR(MediaPlayerEntity): def set_states(self) -> None: """Set all the states from the device to the entity.""" - self._attr_state = STATE_ON if self._zone.power is True else STATE_OFF + self._attr_state = ( + MediaPlayerState.ON if self._zone.power else MediaPlayerState.OFF + ) self._attr_is_volume_muted = self._zone.mute self._attr_volume_level = self._zone.volume_as_percentage self._attr_media_title = self._zone.input_name diff --git a/homeassistant/components/aquostv/media_player.py b/homeassistant/components/aquostv/media_player.py index b0ff674e2de..34d5e4161fb 100644 --- a/homeassistant/components/aquostv/media_player.py +++ b/homeassistant/components/aquostv/media_player.py @@ -10,6 +10,7 @@ from homeassistant.components.media_player import ( PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, ) from homeassistant.const import ( CONF_HOST, @@ -18,8 +19,6 @@ from homeassistant.const import ( CONF_PORT, CONF_TIMEOUT, CONF_USERNAME, - STATE_OFF, - STATE_ON, ) from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -93,7 +92,7 @@ def _retry(func): except (OSError, TypeError, ValueError): update_retries -= 1 if update_retries == 0: - obj.set_state(STATE_OFF) + obj.set_state(MediaPlayerState.OFF) return wrapper @@ -134,9 +133,9 @@ class SharpAquosTVDevice(MediaPlayerEntity): def update(self) -> None: """Retrieve the latest data.""" if self._remote.power() == 1: - self._attr_state = STATE_ON + self._attr_state = MediaPlayerState.ON else: - self._attr_state = STATE_OFF + self._attr_state = MediaPlayerState.OFF # Set TV to be able to remotely power on if self._power_on_enabled: self._remote.power_on_command_settings(2) diff --git a/homeassistant/components/arcam_fmj/media_player.py b/homeassistant/components/arcam_fmj/media_player.py index f995b79df04..65a5d8c3580 100644 --- a/homeassistant/components/arcam_fmj/media_player.py +++ b/homeassistant/components/arcam_fmj/media_player.py @@ -9,17 +9,15 @@ from arcam.fmj.state import State from homeassistant.components.media_player import ( BrowseMedia, + MediaClass, MediaPlayerEntity, MediaPlayerEntityFeature, -) -from homeassistant.components.media_player.const import ( - MEDIA_CLASS_DIRECTORY, - MEDIA_CLASS_MUSIC, - MEDIA_TYPE_MUSIC, + MediaPlayerState, + MediaType, ) from homeassistant.components.media_player.errors import BrowseError from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON +from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo @@ -91,11 +89,11 @@ class ArcamFmj(MediaPlayerEntity): self._attr_entity_registry_enabled_default = state.zn == 1 @property - def state(self): + def state(self) -> MediaPlayerState: """Return the state of the device.""" if self._state.get_power(): - return STATE_ON - return STATE_OFF + return MediaPlayerState.ON + return MediaPlayerState.OFF @property def device_info(self): @@ -202,7 +200,9 @@ class ArcamFmj(MediaPlayerEntity): await self._state.set_power(False) async def async_browse_media( - self, media_content_type: str | None = None, media_content_id: str | None = None + self, + media_content_type: MediaType | str | None = None, + media_content_id: str | None = None, ) -> BrowseMedia: """Implement the websocket media browsing helper.""" if media_content_id not in (None, "root"): @@ -215,9 +215,9 @@ class ArcamFmj(MediaPlayerEntity): radio = [ BrowseMedia( title=preset.name, - media_class=MEDIA_CLASS_MUSIC, + media_class=MediaClass.MUSIC, media_content_id=f"preset:{preset.index}", - media_content_type=MEDIA_TYPE_MUSIC, + media_content_type=MediaType.MUSIC, can_play=True, can_expand=False, ) @@ -226,7 +226,7 @@ class ArcamFmj(MediaPlayerEntity): root = BrowseMedia( title="Arcam FMJ Receiver", - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_id="root", media_content_type="library", can_play=False, @@ -237,7 +237,7 @@ class ArcamFmj(MediaPlayerEntity): return root async def async_play_media( - self, media_type: str, media_id: str, **kwargs: Any + self, media_type: MediaType | str, media_id: str, **kwargs: Any ) -> None: """Play media.""" @@ -289,13 +289,13 @@ class ArcamFmj(MediaPlayerEntity): return value / 99.0 @property - def media_content_type(self): + def media_content_type(self) -> MediaType | None: """Content type of current playing media.""" source = self._state.get_source() if source == SourceCodes.DAB: - value = MEDIA_TYPE_MUSIC + value = MediaType.MUSIC elif source == SourceCodes.FM: - value = MEDIA_TYPE_MUSIC + value = MediaType.MUSIC else: value = None return value diff --git a/homeassistant/components/blackbird/media_player.py b/homeassistant/components/blackbird/media_player.py index b0fda2de0f4..5e6e996c7dd 100644 --- a/homeassistant/components/blackbird/media_player.py +++ b/homeassistant/components/blackbird/media_player.py @@ -12,6 +12,7 @@ from homeassistant.components.media_player import ( PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -19,8 +20,6 @@ from homeassistant.const import ( CONF_NAME, CONF_PORT, CONF_TYPE, - STATE_OFF, - STATE_ON, ) from homeassistant.core import HomeAssistant, ServiceCall import homeassistant.helpers.config_validation as cv @@ -163,7 +162,7 @@ class BlackbirdZone(MediaPlayerEntity): state = self._blackbird.zone_status(self._zone_id) if not state: return - self._attr_state = STATE_ON if state.power else STATE_OFF + self._attr_state = MediaPlayerState.ON if state.power else MediaPlayerState.OFF idx = state.av if idx in self._source_id_name: self._attr_source = self._source_id_name[idx] diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index 53f6de14b3c..36af7d46489 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -21,12 +21,12 @@ from homeassistant.components.media_player import ( PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaType, ) from homeassistant.components.media_player.browse_media import ( BrowseMedia, async_process_play_media_url, ) -from homeassistant.components.media_player.const import MEDIA_TYPE_MUSIC from homeassistant.const import ( ATTR_ENTITY_ID, CONF_HOST, @@ -205,6 +205,8 @@ async def async_setup_platform( class BluesoundPlayer(MediaPlayerEntity): """Representation of a Bluesound Player.""" + _attr_media_content_type = MediaType.MUSIC + def __init__(self, hass, host, port=None, name=None, init_callback=None): """Initialize the media player.""" self.host = host @@ -551,11 +553,6 @@ class BluesoundPlayer(MediaPlayerEntity): return self._services_items - @property - def media_content_type(self): - """Content type of current playing media.""" - return MEDIA_TYPE_MUSIC - @property def state(self): """Return the state of the device.""" @@ -1022,7 +1019,7 @@ class BluesoundPlayer(MediaPlayerEntity): return await self.send_bluesound_command(f"Play?seek={float(position)}") async def async_play_media( - self, media_type: str, media_id: str, **kwargs: Any + self, media_type: MediaType | str, media_id: str, **kwargs: Any ) -> None: """Send the play_media command to the media player.""" if self.is_grouped and not self.is_master: @@ -1069,7 +1066,9 @@ class BluesoundPlayer(MediaPlayerEntity): return await self.send_bluesound_command("Volume?mute=0") async def async_browse_media( - self, media_content_type: str | None = None, media_content_id: str | None = None + self, + media_content_type: MediaType | str | None = None, + media_content_id: str | None = None, ) -> BrowseMedia: """Implement the websocket media browsing helper.""" return await media_source.async_browse_media( diff --git a/homeassistant/components/braviatv/coordinator.py b/homeassistant/components/braviatv/coordinator.py index 49c902e0d44..d190c00b1c0 100644 --- a/homeassistant/components/braviatv/coordinator.py +++ b/homeassistant/components/braviatv/coordinator.py @@ -16,10 +16,7 @@ from pybravia import ( ) from typing_extensions import Concatenate, ParamSpec -from homeassistant.components.media_player.const import ( - MEDIA_TYPE_APP, - MEDIA_TYPE_CHANNEL, -) +from homeassistant.components.media_player import MediaType from homeassistant.core import HomeAssistant from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -74,7 +71,7 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): self.source_map: dict[str, dict] = {} self.media_title: str | None = None self.media_content_id: str | None = None - self.media_content_type: str | None = None + self.media_content_type: MediaType | None = None self.media_uri: str | None = None self.media_duration: int | None = None self.volume_level: float | None = None @@ -182,7 +179,7 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): self.is_channel = self.media_uri[:2] == "tv" if self.is_channel: self.media_content_id = playing_info.get("dispNum") - self.media_content_type = MEDIA_TYPE_CHANNEL + self.media_content_type = MediaType.CHANNEL else: self.media_content_id = self.media_uri self.media_content_type = None @@ -193,7 +190,7 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): self.media_content_type = None if not playing_info: self.media_title = "Smart TV" - self.media_content_type = MEDIA_TYPE_APP + self.media_content_type = MediaType.APP @catch_braviatv_errors async def async_turn_on(self) -> None: diff --git a/homeassistant/components/braviatv/media_player.py b/homeassistant/components/braviatv/media_player.py index 525e265d415..65a8e46946e 100644 --- a/homeassistant/components/braviatv/media_player.py +++ b/homeassistant/components/braviatv/media_player.py @@ -5,9 +5,10 @@ from homeassistant.components.media_player import ( MediaPlayerDeviceClass, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, + MediaType, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import STATE_OFF, STATE_PAUSED, STATE_PLAYING from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -50,11 +51,15 @@ class BraviaTVMediaPlayer(BraviaTVEntity, MediaPlayerEntity): ) @property - def state(self) -> str | None: + def state(self) -> MediaPlayerState: """Return the state of the device.""" if self.coordinator.is_on: - return STATE_PLAYING if self.coordinator.playing else STATE_PAUSED - return STATE_OFF + return ( + MediaPlayerState.PLAYING + if self.coordinator.playing + else MediaPlayerState.PAUSED + ) + return MediaPlayerState.OFF @property def source(self) -> str | None: @@ -87,7 +92,7 @@ class BraviaTVMediaPlayer(BraviaTVEntity, MediaPlayerEntity): return self.coordinator.media_content_id @property - def media_content_type(self) -> str | None: + def media_content_type(self) -> MediaType | None: """Content type of current playing media.""" return self.coordinator.media_content_type diff --git a/homeassistant/components/channels/media_player.py b/homeassistant/components/channels/media_player.py index acb9f7ae680..a834e9010ce 100644 --- a/homeassistant/components/channels/media_player.py +++ b/homeassistant/components/channels/media_player.py @@ -10,22 +10,10 @@ from homeassistant.components.media_player import ( PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, + MediaType, ) -from homeassistant.components.media_player.const import ( - MEDIA_TYPE_CHANNEL, - MEDIA_TYPE_EPISODE, - MEDIA_TYPE_MOVIE, - MEDIA_TYPE_TVSHOW, -) -from homeassistant.const import ( - ATTR_SECONDS, - CONF_HOST, - CONF_NAME, - CONF_PORT, - STATE_IDLE, - STATE_PAUSED, - STATE_PLAYING, -) +from homeassistant.const import ATTR_SECONDS, CONF_HOST, CONF_NAME, CONF_PORT from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -78,6 +66,7 @@ async def async_setup_platform( class ChannelsPlayer(MediaPlayerEntity): """Representation of a Channels instance.""" + _attr_media_content_type = MediaType.CHANNEL _attr_supported_features = ( MediaPlayerEntityFeature.PLAY | MediaPlayerEntityFeature.PAUSE @@ -156,16 +145,16 @@ class ChannelsPlayer(MediaPlayerEntity): return self._name @property - def state(self): + def state(self) -> MediaPlayerState | None: """Return the state of the player.""" if self.status == "stopped": - return STATE_IDLE + return MediaPlayerState.IDLE if self.status == "paused": - return STATE_PAUSED + return MediaPlayerState.PAUSED if self.status == "playing": - return STATE_PLAYING + return MediaPlayerState.PLAYING return None @@ -190,11 +179,6 @@ class ChannelsPlayer(MediaPlayerEntity): """Content ID of current playing channel.""" return self.channel_number - @property - def media_content_type(self): - """Content type of current playing media.""" - return MEDIA_TYPE_CHANNEL - @property def media_image_url(self): """Image url of current playing media.""" @@ -253,12 +237,14 @@ class ChannelsPlayer(MediaPlayerEntity): self.update_state(response) break - def play_media(self, media_type: str, media_id: str, **kwargs: Any) -> None: + def play_media( + self, media_type: MediaType | str, media_id: str, **kwargs: Any + ) -> None: """Send the play_media command to the player.""" - if media_type == MEDIA_TYPE_CHANNEL: + if media_type == MediaType.CHANNEL: response = self.client.play_channel(media_id) self.update_state(response) - elif media_type in (MEDIA_TYPE_MOVIE, MEDIA_TYPE_EPISODE, MEDIA_TYPE_TVSHOW): + elif media_type in {MediaType.MOVIE, MediaType.EPISODE, MediaType.TVSHOW}: response = self.client.play_recording(media_id) self.update_state(response) diff --git a/homeassistant/components/clementine/media_player.py b/homeassistant/components/clementine/media_player.py index 06bfb654ea1..770f19e9970 100644 --- a/homeassistant/components/clementine/media_player.py +++ b/homeassistant/components/clementine/media_player.py @@ -11,17 +11,10 @@ from homeassistant.components.media_player import ( PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, + MediaType, ) -from homeassistant.components.media_player.const import MEDIA_TYPE_MUSIC -from homeassistant.const import ( - CONF_ACCESS_TOKEN, - CONF_HOST, - CONF_NAME, - CONF_PORT, - STATE_OFF, - STATE_PAUSED, - STATE_PLAYING, -) +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST, CONF_NAME, CONF_PORT from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -62,7 +55,7 @@ def setup_platform( class ClementineDevice(MediaPlayerEntity): """Representation of Clementine Player.""" - _attr_media_content_type = MEDIA_TYPE_MUSIC + _attr_media_content_type = MediaType.MUSIC _attr_supported_features = ( MediaPlayerEntityFeature.PAUSE | MediaPlayerEntityFeature.VOLUME_STEP @@ -84,16 +77,16 @@ class ClementineDevice(MediaPlayerEntity): client = self._client if client.state == "Playing": - self._attr_state = STATE_PLAYING + self._attr_state = MediaPlayerState.PLAYING elif client.state == "Paused": - self._attr_state = STATE_PAUSED + self._attr_state = MediaPlayerState.PAUSED elif client.state == "Disconnected": - self._attr_state = STATE_OFF + self._attr_state = MediaPlayerState.OFF else: - self._attr_state = STATE_PAUSED + self._attr_state = MediaPlayerState.PAUSED if client.last_update and (time.time() - client.last_update > 40): - self._attr_state = STATE_OFF + self._attr_state = MediaPlayerState.OFF volume = float(client.volume) if client.volume else 0.0 self._attr_volume_level = volume / 100.0 @@ -112,7 +105,7 @@ class ClementineDevice(MediaPlayerEntity): self._attr_media_image_hash = None except Exception: - self._attr_state = STATE_OFF + self._attr_state = MediaPlayerState.OFF raise def select_source(self, source: str) -> None: @@ -150,19 +143,19 @@ class ClementineDevice(MediaPlayerEntity): def media_play_pause(self) -> None: """Simulate play pause media player.""" - if self.state == STATE_PLAYING: + if self.state == MediaPlayerState.PLAYING: self.media_pause() else: self.media_play() def media_play(self) -> None: """Send play command.""" - self._attr_state = STATE_PLAYING + self._attr_state = MediaPlayerState.PLAYING self._client.play() def media_pause(self) -> None: """Send media pause command to media player.""" - self._attr_state = STATE_PAUSED + self._attr_state = MediaPlayerState.PAUSED self._client.pause() def media_next_track(self) -> None: diff --git a/homeassistant/components/cmus/media_player.py b/homeassistant/components/cmus/media_player.py index 09fec24b543..65bfef3a0cb 100644 --- a/homeassistant/components/cmus/media_player.py +++ b/homeassistant/components/cmus/media_player.py @@ -11,20 +11,10 @@ from homeassistant.components.media_player import ( PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, + MediaType, ) -from homeassistant.components.media_player.const import ( - MEDIA_TYPE_MUSIC, - MEDIA_TYPE_PLAYLIST, -) -from homeassistant.const import ( - CONF_HOST, - CONF_NAME, - CONF_PASSWORD, - CONF_PORT, - STATE_OFF, - STATE_PAUSED, - STATE_PLAYING, -) +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -92,7 +82,7 @@ class CmusRemote: class CmusDevice(MediaPlayerEntity): """Representation of a running cmus.""" - _attr_media_content_type = MEDIA_TYPE_MUSIC + _attr_media_content_type = MediaType.MUSIC _attr_supported_features = ( MediaPlayerEntityFeature.PAUSE | MediaPlayerEntityFeature.VOLUME_SET @@ -128,11 +118,11 @@ class CmusDevice(MediaPlayerEntity): else: self.status = status if self.status.get("status") == "playing": - self._attr_state = STATE_PLAYING + self._attr_state = MediaPlayerState.PLAYING elif self.status.get("status") == "paused": - self._attr_state = STATE_PAUSED + self._attr_state = MediaPlayerState.PAUSED else: - self._attr_state = STATE_OFF + self._attr_state = MediaPlayerState.OFF self._attr_media_content_id = self.status.get("file") self._attr_media_duration = self.status.get("duration") self._attr_media_title = self.status["tag"].get("title") @@ -187,16 +177,18 @@ class CmusDevice(MediaPlayerEntity): if current_volume <= 100: self._remote.cmus.set_volume(int(current_volume) - 5) - def play_media(self, media_type: str, media_id: str, **kwargs: Any) -> None: + def play_media( + self, media_type: MediaType | str, media_id: str, **kwargs: Any + ) -> None: """Send the play command.""" - if media_type in [MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST]: + if media_type in {MediaType.MUSIC, MediaType.PLAYLIST}: self._remote.cmus.player_play_file(media_id) else: _LOGGER.error( "Invalid media type %s. Only %s and %s are supported", media_type, - MEDIA_TYPE_MUSIC, - MEDIA_TYPE_PLAYLIST, + MediaType.MUSIC, + MediaType.PLAYLIST, ) def media_pause(self) -> None: diff --git a/homeassistant/components/denon/media_player.py b/homeassistant/components/denon/media_player.py index 2dd7a29e17e..8c71dd46c3e 100644 --- a/homeassistant/components/denon/media_player.py +++ b/homeassistant/components/denon/media_player.py @@ -10,8 +10,9 @@ from homeassistant.components.media_player import ( PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, ) -from homeassistant.const import CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON +from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -214,12 +215,12 @@ class DenonDevice(MediaPlayerEntity): return self._name @property - def state(self): + def state(self) -> MediaPlayerState | None: """Return the state of the device.""" if self._pwstate == "PWSTANDBY": - return STATE_OFF + return MediaPlayerState.OFF if self._pwstate == "PWON": - return STATE_ON + return MediaPlayerState.ON return None diff --git a/homeassistant/components/denonavr/media_player.py b/homeassistant/components/denonavr/media_player.py index c06d5a939a3..cc0e0c06656 100644 --- a/homeassistant/components/denonavr/media_player.py +++ b/homeassistant/components/denonavr/media_player.py @@ -22,19 +22,11 @@ import voluptuous as vol from homeassistant.components.media_player import ( MediaPlayerEntity, MediaPlayerEntityFeature, -) -from homeassistant.components.media_player.const import ( - MEDIA_TYPE_CHANNEL, - MEDIA_TYPE_MUSIC, + MediaPlayerState, + MediaType, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_COMMAND, - CONF_HOST, - CONF_MODEL, - STATE_PAUSED, - STATE_PLAYING, -) +from homeassistant.const import ATTR_COMMAND, CONF_HOST, CONF_MODEL from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity import DeviceInfo @@ -297,11 +289,11 @@ class DenonDevice(MediaPlayerEntity): return None @property - def media_content_type(self): + def media_content_type(self) -> MediaType: """Content type of current playing media.""" - if self._receiver.state in (STATE_PLAYING, STATE_PAUSED): - return MEDIA_TYPE_MUSIC - return MEDIA_TYPE_CHANNEL + if self._receiver.state in {MediaPlayerState.PLAYING, MediaPlayerState.PAUSED}: + return MediaType.MUSIC + return MediaType.CHANNEL @property def media_duration(self): diff --git a/homeassistant/components/directv/media_player.py b/homeassistant/components/directv/media_player.py index 7d1434e9909..bc838757854 100644 --- a/homeassistant/components/directv/media_player.py +++ b/homeassistant/components/directv/media_player.py @@ -10,15 +10,10 @@ from homeassistant.components.media_player import ( MediaPlayerDeviceClass, MediaPlayerEntity, MediaPlayerEntityFeature, -) -from homeassistant.components.media_player.const import ( - MEDIA_TYPE_CHANNEL, - MEDIA_TYPE_MOVIE, - MEDIA_TYPE_MUSIC, - MEDIA_TYPE_TVSHOW, + MediaPlayerState, + MediaType, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import STATE_OFF, STATE_PAUSED, STATE_PLAYING from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import dt as dt_util @@ -34,7 +29,7 @@ from .entity import DIRECTVEntity _LOGGER = logging.getLogger(__name__) -KNOWN_MEDIA_TYPES = [MEDIA_TYPE_MOVIE, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW] +KNOWN_MEDIA_TYPES = {MediaType.MOVIE, MediaType.MUSIC, MediaType.TVSHOW} SUPPORT_DTV = ( MediaPlayerEntityFeature.PAUSE @@ -134,18 +129,18 @@ class DIRECTVMediaPlayer(DIRECTVEntity, MediaPlayerEntity): # MediaPlayerEntity properties and methods @property - def state(self): + def state(self) -> MediaPlayerState: """Return the state of the device.""" if self._is_standby: - return STATE_OFF + return MediaPlayerState.OFF # For recorded media we can determine if it is paused or not. # For live media we're unable to determine and will always return # playing instead. if self._paused: - return STATE_PAUSED + return MediaPlayerState.PAUSED - return STATE_PLAYING + return MediaPlayerState.PLAYING @property def media_content_id(self): @@ -156,7 +151,7 @@ class DIRECTVMediaPlayer(DIRECTVEntity, MediaPlayerEntity): return self._program.program_id @property - def media_content_type(self): + def media_content_type(self) -> MediaType | None: """Return the content type of current playing media.""" if self._is_standby or self._program is None: return None @@ -164,7 +159,7 @@ class DIRECTVMediaPlayer(DIRECTVEntity, MediaPlayerEntity): if self._program.program_type in KNOWN_MEDIA_TYPES: return self._program.program_type - return MEDIA_TYPE_MOVIE + return MediaType.MOVIE @property def media_duration(self): @@ -196,7 +191,7 @@ class DIRECTVMediaPlayer(DIRECTVEntity, MediaPlayerEntity): if self._is_standby or self._program is None: return None - if self.media_content_type == MEDIA_TYPE_MUSIC: + if self.media_content_type == MediaType.MUSIC: return self._program.music_title return self._program.title @@ -320,14 +315,14 @@ class DIRECTVMediaPlayer(DIRECTVEntity, MediaPlayerEntity): await self.dtv.remote("ffwd", self._address) async def async_play_media( - self, media_type: str, media_id: str, **kwargs: Any + self, media_type: MediaType | str, media_id: str, **kwargs: Any ) -> None: """Select input source.""" - if media_type != MEDIA_TYPE_CHANNEL: + if media_type != MediaType.CHANNEL: _LOGGER.error( "Invalid media type %s. Only %s is supported", media_type, - MEDIA_TYPE_CHANNEL, + MediaType.CHANNEL, ) return diff --git a/homeassistant/components/dunehd/media_player.py b/homeassistant/components/dunehd/media_player.py index f6d90b7b1d9..4770492876d 100644 --- a/homeassistant/components/dunehd/media_player.py +++ b/homeassistant/components/dunehd/media_player.py @@ -8,9 +8,9 @@ from pdunehd import DuneHDPlayer from homeassistant.components.media_player import ( MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import STATE_OFF, STATE_ON, STATE_PAUSED, STATE_PLAYING from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -57,17 +57,17 @@ class DuneHDPlayerEntity(MediaPlayerEntity): self.__update_title() @property - def state(self) -> str | None: + def state(self) -> MediaPlayerState: """Return player state.""" - state = STATE_OFF + state = MediaPlayerState.OFF if "playback_position" in self._state: - state = STATE_PLAYING + state = MediaPlayerState.PLAYING if self._state.get("player_state") in ("playing", "buffering", "photo_viewer"): - state = STATE_PLAYING + state = MediaPlayerState.PLAYING if int(self._state.get("playback_speed", 1234)) == 0: - state = STATE_PAUSED + state = MediaPlayerState.PAUSED if self._state.get("player_state") == "navigator": - state = STATE_ON + state = MediaPlayerState.ON return state @property From b21f8c9ea813fbe3e721497d93608977d4f43d1e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 8 Sep 2022 11:21:46 +0200 Subject: [PATCH 198/955] Remove use of deprecated SUPPORT_* constants from MQTT light (#77828) * Remove use of deprecated SUPPORT_* constants from MQTT light * Refactor --- homeassistant/components/light/__init__.py | 17 -- .../components/mqtt/light/schema_json.py | 67 ++++-- .../components/mqtt/light/schema_template.py | 88 ++++---- tests/components/mqtt/test_light_json.py | 190 ++++++++++-------- tests/components/mqtt/test_light_template.py | 52 +++-- 5 files changed, 244 insertions(+), 170 deletions(-) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 33ec3119b95..5089f454ac8 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -977,20 +977,3 @@ class LightEntity(ToggleEntity): def supported_features(self) -> int: """Flag supported features.""" return self._attr_supported_features - - -def legacy_supported_features( - supported_features: int, supported_color_modes: list[str] | None -) -> int: - """Calculate supported features with backwards compatibility.""" - # Backwards compatibility for supported_color_modes added in 2021.4 - if supported_color_modes is None: - return supported_features - if any(mode in supported_color_modes for mode in COLOR_MODES_COLOR): - supported_features |= SUPPORT_COLOR - if any(mode in supported_color_modes for mode in COLOR_MODES_BRIGHTNESS): - supported_features |= SUPPORT_BRIGHTNESS - if ColorMode.COLOR_TEMP in supported_color_modes: - supported_features |= SUPPORT_COLOR_TEMP - - return supported_features diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index 295b43120d4..f7703b0f1a4 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -20,14 +20,13 @@ from homeassistant.components.light import ( ENTITY_ID_FORMAT, FLASH_LONG, FLASH_SHORT, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - SUPPORT_COLOR_TEMP, VALID_COLOR_MODES, ColorMode, LightEntity, LightEntityFeature, - legacy_supported_features, + brightness_supported, + color_supported, + filter_supported_color_modes, valid_supported_color_modes, ) from homeassistant.const import ( @@ -198,6 +197,7 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): 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 @@ -230,13 +230,20 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): ) self._supported_features |= config[CONF_EFFECT] and LightEntityFeature.EFFECT if not self._config[CONF_COLOR_MODE]: - self._supported_features |= config[CONF_BRIGHTNESS] and SUPPORT_BRIGHTNESS - self._supported_features |= config[CONF_COLOR_TEMP] and SUPPORT_COLOR_TEMP - self._supported_features |= config[CONF_HS] and SUPPORT_COLOR - self._supported_features |= config[CONF_RGB] and ( - SUPPORT_COLOR | SUPPORT_BRIGHTNESS - ) - self._supported_features |= config[CONF_XY] and SUPPORT_COLOR + color_modes = {ColorMode.ONOFF} + if config[CONF_BRIGHTNESS]: + color_modes.add(ColorMode.BRIGHTNESS) + if config[CONF_COLOR_TEMP]: + 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)) + 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)) def _update_color(self, values): if not self._config[CONF_COLOR_MODE]: @@ -332,7 +339,12 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): elif values["state"] is None: self._state = None - if self._supported_features and SUPPORT_COLOR and "color" in values: + if ( + not self._config[CONF_COLOR_MODE] + and color_supported(self._supported_color_modes) + and "color" in values + ): + # Deprecated color handling if values["color"] is None: self._hs = None else: @@ -341,7 +353,7 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): if self._config[CONF_COLOR_MODE] and "color_mode" in values: self._update_color(values) - if self._supported_features and SUPPORT_BRIGHTNESS: + if brightness_supported(self._supported_color_modes): try: self._brightness = int( values["brightness"] @@ -354,10 +366,10 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): _LOGGER.warning("Invalid brightness value received") if ( - self._supported_features - and SUPPORT_COLOR_TEMP + 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 @@ -474,19 +486,25 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): @property def color_mode(self): """Return current color mode.""" - return self._color_mode + if self._config[CONF_COLOR_MODE]: + return self._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: + return ColorMode.HS + return ColorMode.COLOR_TEMP @property def supported_color_modes(self): """Flag supported color modes.""" - return self._config.get(CONF_SUPPORTED_COLOR_MODES) + return self._supported_color_modes @property def supported_features(self): """Flag supported features.""" - return legacy_supported_features( - self._supported_features, self._config.get(CONF_SUPPORTED_COLOR_MODES) - ) + return self._supported_features def _set_flash_and_transition(self, message, **kwargs): if ATTR_TRANSITION in kwargs: @@ -510,7 +528,10 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): return tuple(round(i / 255 * brightness) for i in rgbxx) def _supports_color_mode(self, color_mode): - return self.supported_color_modes and color_mode in self.supported_color_modes + """Return True if the light natively supports a color mode.""" + return ( + self._config[CONF_COLOR_MODE] and color_mode in self.supported_color_modes + ) async def async_turn_on(self, **kwargs): # noqa: C901 """Turn the device on. @@ -524,6 +545,7 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): if ATTR_HS_COLOR in kwargs and ( self._config[CONF_HS] or self._config[CONF_RGB] or self._config[CONF_XY] ): + # Legacy color handling hs_color = kwargs[ATTR_HS_COLOR] message["color"] = {} if self._config[CONF_RGB]: @@ -548,6 +570,7 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): message["color"]["s"] = hs_color[1] if self._optimistic: + self._color_temp = None self._hs = kwargs[ATTR_HS_COLOR] should_update = True @@ -617,7 +640,9 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): 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 should_update = True if ATTR_EFFECT in kwargs: diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py index 73f2786ad12..dacc977a036 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -11,11 +11,10 @@ from homeassistant.components.light import ( ATTR_HS_COLOR, ATTR_TRANSITION, ENTITY_ID_FORMAT, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - SUPPORT_COLOR_TEMP, + ColorMode, LightEntity, LightEntityFeature, + filter_supported_color_modes, ) from homeassistant.const import ( CONF_NAME, @@ -129,6 +128,7 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity): # features self._brightness = None + self._fixed_color_mode = None self._color_temp = None self._hs = None self._effect = None @@ -166,6 +166,21 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity): or self._templates[CONF_STATE_TEMPLATE] is None ) + color_modes = {ColorMode.ONOFF} + if self._templates[CONF_BRIGHTNESS_TEMPLATE] is not None: + color_modes.add(ColorMode.BRIGHTNESS) + if self._templates[CONF_COLOR_TEMP_TEMPLATE] is not None: + color_modes.add(ColorMode.COLOR_TEMP) + if ( + self._templates[CONF_RED_TEMPLATE] is not None + and self._templates[CONF_GREEN_TEMPLATE] is not None + 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)) + def _prepare_subscribe_topics(self): """(Re)Subscribe to topics.""" for tpl in self._templates.values(): @@ -200,11 +215,10 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity): if self._templates[CONF_COLOR_TEMP_TEMPLATE] is not None: try: - self._color_temp = int( - self._templates[ - CONF_COLOR_TEMP_TEMPLATE - ].async_render_with_possible_json_value(msg.payload) - ) + 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 except ValueError: _LOGGER.warning("Invalid color temperature value received") @@ -214,22 +228,21 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity): and self._templates[CONF_BLUE_TEMPLATE] is not None ): try: - red = int( - self._templates[ - CONF_RED_TEMPLATE - ].async_render_with_possible_json_value(msg.payload) - ) - green = int( - self._templates[ - CONF_GREEN_TEMPLATE - ].async_render_with_possible_json_value(msg.payload) - ) - blue = int( - self._templates[ - CONF_BLUE_TEMPLATE - ].async_render_with_possible_json_value(msg.payload) - ) - self._hs = color_util.color_RGB_to_hs(red, green, blue) + red = self._templates[ + CONF_RED_TEMPLATE + ].async_render_with_possible_json_value(msg.payload) + green = self._templates[ + CONF_GREEN_TEMPLATE + ].async_render_with_possible_json_value(msg.payload) + blue = self._templates[ + CONF_BLUE_TEMPLATE + ].async_render_with_possible_json_value(msg.payload) + if red == "None" and green == "None" and blue == "None": + self._hs = None + else: + self._hs = color_util.color_RGB_to_hs( + int(red), int(green), int(blue) + ) except ValueError: _LOGGER.warning("Invalid color value received") @@ -340,6 +353,7 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity): if self._optimistic: self._color_temp = kwargs[ATTR_COLOR_TEMP] + self._hs = None if ATTR_HS_COLOR in kwargs: hs_color = kwargs[ATTR_HS_COLOR] @@ -363,6 +377,7 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity): values["sat"] = hs_color[1] if self._optimistic: + self._color_temp = None self._hs = kwargs[ATTR_HS_COLOR] if ATTR_EFFECT in kwargs: @@ -415,21 +430,26 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity): 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._templates[CONF_BRIGHTNESS_TEMPLATE] is not None: - features = features | SUPPORT_BRIGHTNESS - if ( - self._templates[CONF_RED_TEMPLATE] is not None - and self._templates[CONF_GREEN_TEMPLATE] is not None - and self._templates[CONF_BLUE_TEMPLATE] is not None - ): - features = features | SUPPORT_COLOR | SUPPORT_BRIGHTNESS if self._config.get(CONF_EFFECT_LIST) is not None: features = features | LightEntityFeature.EFFECT - if self._templates[CONF_COLOR_TEMP_TEMPLATE] is not None: - features = features | SUPPORT_COLOR_TEMP return features diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index e61d4e77286..af6daf6b7e4 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -237,8 +237,8 @@ async def test_fail_setup_if_color_modes_invalid( assert error in caplog.text -async def test_rgb_light(hass, mqtt_mock_entry_with_yaml_config): - """Test RGB light flags brightness support.""" +async def test_legacy_rgb_light(hass, mqtt_mock_entry_with_yaml_config): + """Test legacy RGB light flags expected features and color modes.""" assert await async_setup_component( hass, mqtt.DOMAIN, @@ -257,12 +257,9 @@ async def test_rgb_light(hass, mqtt_mock_entry_with_yaml_config): await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") - expected_features = ( - light.SUPPORT_BRIGHTNESS - | light.SUPPORT_COLOR - | light.SUPPORT_FLASH - | light.SUPPORT_TRANSITION - ) + color_modes = [light.ColorMode.HS] + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + expected_features = light.SUPPORT_FLASH | light.SUPPORT_TRANSITION assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features @@ -348,13 +345,10 @@ async def test_controlling_state_via_topic(hass, mqtt_mock_entry_with_yaml_confi state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN + color_modes = [light.ColorMode.COLOR_TEMP, light.ColorMode.HS] + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes expected_features = ( - light.SUPPORT_BRIGHTNESS - | light.SUPPORT_COLOR - | light.SUPPORT_COLOR_TEMP - | light.SUPPORT_EFFECT - | light.SUPPORT_FLASH - | light.SUPPORT_TRANSITION + light.SUPPORT_EFFECT | light.SUPPORT_FLASH | light.SUPPORT_TRANSITION ) assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features assert state.attributes.get("rgb_color") is None @@ -380,11 +374,35 @@ async def test_controlling_state_via_topic(hass, mqtt_mock_entry_with_yaml_confi assert state.state == STATE_ON assert state.attributes.get("rgb_color") == (255, 255, 255) assert state.attributes.get("brightness") == 255 - assert state.attributes.get("color_temp") == 155 + assert state.attributes.get("color_temp") is None # rgb color has priority assert state.attributes.get("effect") == "colorloop" assert state.attributes.get("xy_color") == (0.323, 0.329) assert state.attributes.get("hs_color") == (0.0, 0.0) + # Turn on the light + async_fire_mqtt_message( + hass, + "test_light_rgb", + '{"state":"ON",' + '"brightness":255,' + '"color":null,' + '"color_temp":155,' + '"effect":"colorloop"}', + ) + + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("rgb_color") == ( + 255, + 253, + 248, + ) # temp converted to color + assert state.attributes.get("brightness") == 255 + assert state.attributes.get("color_temp") == 155 + assert state.attributes.get("effect") == "colorloop" + assert state.attributes.get("xy_color") == (0.328, 0.334) # temp converted to color + assert state.attributes.get("hs_color") == (44.098, 2.43) # temp converted to color + # Turn the light off async_fire_mqtt_message(hass, "test_light_rgb", '{"state":"OFF"}') @@ -421,7 +439,7 @@ async def test_controlling_state_via_topic(hass, mqtt_mock_entry_with_yaml_confi async_fire_mqtt_message(hass, "test_light_rgb", '{"state":"ON", "color":null}') light_state = hass.states.get("light.test") - assert "hs_color" in light_state.attributes + assert "hs_color" in light_state.attributes # Color temp approximation async_fire_mqtt_message(hass, "test_light_rgb", '{"state":"ON", "color_temp":155}') @@ -472,12 +490,7 @@ async def test_controlling_state_via_topic2( state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN expected_features = ( - light.SUPPORT_BRIGHTNESS - | light.SUPPORT_COLOR - | light.SUPPORT_COLOR_TEMP - | light.SUPPORT_EFFECT - | light.SUPPORT_FLASH - | light.SUPPORT_TRANSITION + light.SUPPORT_EFFECT | light.SUPPORT_FLASH | light.SUPPORT_TRANSITION ) assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features assert state.attributes.get("brightness") is None @@ -660,14 +673,11 @@ async def test_sending_mqtt_commands_and_optimistic( assert state.attributes.get("brightness") == 95 assert state.attributes.get("hs_color") == (100, 100) assert state.attributes.get("effect") == "random" - assert state.attributes.get("color_temp") == 100 + assert state.attributes.get("color_temp") is None # hs_color has priority + color_modes = [light.ColorMode.COLOR_TEMP, light.ColorMode.HS] + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes expected_features = ( - light.SUPPORT_BRIGHTNESS - | light.SUPPORT_COLOR - | light.SUPPORT_COLOR_TEMP - | light.SUPPORT_EFFECT - | light.SUPPORT_FLASH - | light.SUPPORT_TRANSITION + light.SUPPORT_EFFECT | light.SUPPORT_FLASH | light.SUPPORT_TRANSITION ) assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features assert state.attributes.get(ATTR_ASSUMED_STATE) @@ -692,6 +702,8 @@ async def test_sending_mqtt_commands_and_optimistic( mqtt_mock.async_publish.reset_mock() state = hass.states.get("light.test") assert state.state == STATE_ON + assert state.attributes.get("color_mode") == light.ColorMode.COLOR_TEMP + assert state.attributes.get("color_temp") == 90 await common.async_turn_off(hass, "light.test") @@ -706,49 +718,61 @@ async def test_sending_mqtt_commands_and_optimistic( await common.async_turn_on( hass, "light.test", brightness=50, xy_color=[0.123, 0.123] ) - await common.async_turn_on(hass, "light.test", brightness=50, hs_color=[359, 78]) - await common.async_turn_on(hass, "light.test", rgb_color=[255, 128, 0]) - - mqtt_mock.async_publish.assert_has_calls( - [ - call( - "test_light_rgb/set", - JsonValidator( - '{"state": "ON", "color": {"r": 0, "g": 123, "b": 255,' - ' "x": 0.14, "y": 0.131, "h": 210.824, "s": 100.0},' - ' "brightness": 50}' - ), - 2, - False, - ), - call( - "test_light_rgb/set", - JsonValidator( - '{"state": "ON", "color": {"r": 255, "g": 56, "b": 59,' - ' "x": 0.654, "y": 0.301, "h": 359.0, "s": 78.0},' - ' "brightness": 50}' - ), - 2, - False, - ), - call( - "test_light_rgb/set", - JsonValidator( - '{"state": "ON", "color": {"r": 255, "g": 128, "b": 0,' - ' "x": 0.611, "y": 0.375, "h": 30.118, "s": 100.0}}' - ), - 2, - False, - ), - ], - any_order=True, + mqtt_mock.async_publish.assert_called_once_with( + "test_light_rgb/set", + JsonValidator( + '{"state": "ON", "color": {"r": 0, "g": 123, "b": 255,' + ' "x": 0.14, "y": 0.131, "h": 210.824, "s": 100.0},' + ' "brightness": 50}' + ), + 2, + False, ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("light.test") + assert state.attributes.get("color_mode") == light.ColorMode.HS + assert state.attributes["brightness"] == 50 + assert state.attributes["hs_color"] == (210.824, 100.0) + assert state.attributes["rgb_color"] == (0, 123, 255) + assert state.attributes["xy_color"] == (0.14, 0.131) + await common.async_turn_on(hass, "light.test", brightness=50, hs_color=[359, 78]) + mqtt_mock.async_publish.assert_called_once_with( + "test_light_rgb/set", + JsonValidator( + '{"state": "ON", "color": {"r": 255, "g": 56, "b": 59,' + ' "x": 0.654, "y": 0.301, "h": 359.0, "s": 78.0},' + ' "brightness": 50}' + ), + 2, + False, + ) + mqtt_mock.async_publish.reset_mock() state = hass.states.get("light.test") assert state.state == STATE_ON - assert state.attributes["rgb_color"] == (255, 128, 0) + assert state.attributes.get("color_mode") == light.ColorMode.HS + assert state.attributes["brightness"] == 50 + assert state.attributes["hs_color"] == (359.0, 78.0) + assert state.attributes["rgb_color"] == (255, 56, 59) + assert state.attributes["xy_color"] == (0.654, 0.301) + + await common.async_turn_on(hass, "light.test", rgb_color=[255, 128, 0]) + mqtt_mock.async_publish.assert_called_once_with( + "test_light_rgb/set", + JsonValidator( + '{"state": "ON", "color": {"r": 255, "g": 128, "b": 0,' + ' "x": 0.611, "y": 0.375, "h": 30.118, "s": 100.0}}' + ), + 2, + False, + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("color_mode") == light.ColorMode.HS assert state.attributes["brightness"] == 50 assert state.attributes["hs_color"] == (30.118, 100) + assert state.attributes["rgb_color"] == (255, 128, 0) assert state.attributes["xy_color"] == (0.611, 0.375) @@ -794,12 +818,7 @@ async def test_sending_mqtt_commands_and_optimistic2( state = hass.states.get("light.test") assert state.state == STATE_ON expected_features = ( - light.SUPPORT_BRIGHTNESS - | light.SUPPORT_COLOR - | light.SUPPORT_COLOR_TEMP - | light.SUPPORT_EFFECT - | light.SUPPORT_FLASH - | light.SUPPORT_TRANSITION + light.SUPPORT_EFFECT | light.SUPPORT_FLASH | light.SUPPORT_TRANSITION ) assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features assert state.attributes.get("brightness") == 95 @@ -1682,7 +1701,9 @@ async def test_white_scale(hass, mqtt_mock_entry_with_yaml_config): # Turn on the light with brightness async_fire_mqtt_message( - hass, "test_light_bright_scale", '{"state":"ON", "brightness": 99}' + hass, + "test_light_bright_scale", + '{"state":"ON", "brightness": 99, "color_mode":"hs", "color":{"h":180,"s":50}}', ) state = hass.states.get("light.test") @@ -1726,13 +1747,9 @@ async def test_invalid_values(hass, mqtt_mock_entry_with_yaml_config): state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN - expected_features = ( - light.SUPPORT_BRIGHTNESS - | light.SUPPORT_COLOR - | light.SUPPORT_COLOR_TEMP - | light.SUPPORT_FLASH - | light.SUPPORT_TRANSITION - ) + color_modes = [light.ColorMode.COLOR_TEMP, light.ColorMode.HS] + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + expected_features = light.SUPPORT_FLASH | light.SUPPORT_TRANSITION assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features assert state.attributes.get("rgb_color") is None assert state.attributes.get("brightness") is None @@ -1754,8 +1771,7 @@ async def test_invalid_values(hass, mqtt_mock_entry_with_yaml_config): assert state.state == STATE_ON assert state.attributes.get("rgb_color") == (255, 255, 255) assert state.attributes.get("brightness") == 255 - assert state.attributes.get("color_temp") == 100 - + assert state.attributes.get("color_temp") is None # Empty color value async_fire_mqtt_message( hass, @@ -1814,6 +1830,14 @@ async def test_invalid_values(hass, mqtt_mock_entry_with_yaml_config): assert state.state == STATE_ON assert state.attributes.get("brightness") == 255 + # Unset color and set a valid color temperature + async_fire_mqtt_message( + hass, "test_light_rgb", '{"state":"ON", "color": null, "color_temp": 100}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("color_temp") == 100 + # Bad color temperature async_fire_mqtt_message( hass, "test_light_rgb", '{"state":"ON", "color_temp": "badValue"}' @@ -2199,7 +2223,7 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): [ ( "state_topic", - '{ "state": "ON", "brightness": 200 }', + '{ "state": "ON", "brightness": 200, "color_mode":"hs", "color":{"h":180,"s":50} }', "brightness", 200, None, diff --git a/tests/components/mqtt/test_light_template.py b/tests/components/mqtt/test_light_template.py index d88480f9f5a..8ecdcc2c872 100644 --- a/tests/components/mqtt/test_light_template.py +++ b/tests/components/mqtt/test_light_template.py @@ -167,12 +167,9 @@ async def test_rgb_light(hass, mqtt_mock_entry_with_yaml_config): state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN - expected_features = ( - light.SUPPORT_TRANSITION - | light.SUPPORT_COLOR - | light.SUPPORT_FLASH - | light.SUPPORT_BRIGHTNESS - ) + color_modes = [light.ColorMode.HS] + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + expected_features = light.SUPPORT_FLASH | light.SUPPORT_TRANSITION assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features @@ -281,8 +278,27 @@ async def test_state_brightness_color_effect_temp_change_via_topic( assert state.state == STATE_ON assert state.attributes.get("rgb_color") == (255, 128, 63) assert state.attributes.get("brightness") == 255 + assert state.attributes.get("color_temp") is None # rgb color has priority + assert state.attributes.get("effect") is None + + # turn on the light + async_fire_mqtt_message(hass, "test_light_rgb", "on,255,145,None-None-None,") + + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("rgb_color") == ( + 246, + 244, + 255, + ) # temp converted to color + assert state.attributes.get("brightness") == 255 assert state.attributes.get("color_temp") == 145 assert state.attributes.get("effect") is None + assert state.attributes.get("xy_color") == (0.317, 0.317) # temp converted to color + assert state.attributes.get("hs_color") == ( + 251.249, + 4.253, + ) # temp converted to color # make the light state unknown async_fire_mqtt_message(hass, "test_light_rgb", "None") @@ -375,7 +391,7 @@ async def test_sending_mqtt_commands_and_optimistic( assert state.state == STATE_ON assert state.attributes.get("hs_color") == (100, 100) assert state.attributes.get("effect") == "random" - assert state.attributes.get("color_temp") == 100 + assert state.attributes.get("color_temp") is None # hs_color has priority assert state.attributes.get(ATTR_ASSUMED_STATE) await common.async_turn_off(hass, "light.test") @@ -779,7 +795,7 @@ async def test_invalid_values(hass, mqtt_mock_entry_with_yaml_config): state = hass.states.get("light.test") assert state.state == STATE_ON assert state.attributes.get("brightness") == 255 - assert state.attributes.get("color_temp") == 215 + assert state.attributes.get("color_temp") is None # hs_color has priority assert state.attributes.get("rgb_color") == (255, 255, 255) assert state.attributes.get("effect") == "rainbow" @@ -797,13 +813,6 @@ async def test_invalid_values(hass, mqtt_mock_entry_with_yaml_config): state = hass.states.get("light.test") assert state.attributes.get("brightness") == 255 - # bad color temp values - async_fire_mqtt_message(hass, "test_light_rgb", "on,,off,255-255-255") - - # color temp should not have changed - state = hass.states.get("light.test") - assert state.attributes.get("color_temp") == 215 - # bad color values async_fire_mqtt_message(hass, "test_light_rgb", "on,255,a-b-c") @@ -811,6 +820,19 @@ async def test_invalid_values(hass, mqtt_mock_entry_with_yaml_config): state = hass.states.get("light.test") assert state.attributes.get("rgb_color") == (255, 255, 255) + # Unset color and set a valid color temperature + async_fire_mqtt_message(hass, "test_light_rgb", "on,,215,None-None-None") + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("color_temp") == 215 + + # bad color temp values + async_fire_mqtt_message(hass, "test_light_rgb", "on,,off,") + + # color temp should not have changed + state = hass.states.get("light.test") + assert state.attributes.get("color_temp") == 215 + # bad effect value async_fire_mqtt_message(hass, "test_light_rgb", "on,255,a-b-c,white") From c134bcc536a7f5e75a4d5ddca8297d8774fa3cd4 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 8 Sep 2022 11:22:38 +0200 Subject: [PATCH 199/955] Remove use of deprecated SUPPORT_* constants from Template light (#77836) --- homeassistant/components/template/light.py | 38 ++- tests/components/template/test_light.py | 283 +++++++-------------- 2 files changed, 128 insertions(+), 193 deletions(-) diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index f10e6c2ea09..db1c89921d1 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -13,11 +13,10 @@ from homeassistant.components.light import ( ATTR_HS_COLOR, ATTR_TRANSITION, ENTITY_ID_FORMAT, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - SUPPORT_COLOR_TEMP, + ColorMode, LightEntity, LightEntityFeature, + filter_supported_color_modes, ) from homeassistant.const import ( CONF_ENTITY_ID, @@ -182,10 +181,22 @@ class LightTemplate(TemplateEntity, LightEntity): self._color = None self._effect = None self._effect_list = None + self._fixed_color_mode = None self._max_mireds = None self._min_mireds = None self._supports_transition = False + color_modes = {ColorMode.ONOFF} + if self._level_script is not None: + color_modes.add(ColorMode.BRIGHTNESS) + if self._temperature_script is not None: + color_modes.add(ColorMode.COLOR_TEMP) + if self._color_script 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)) + @property def brightness(self) -> int | None: """Return the brightness of the light.""" @@ -227,16 +238,25 @@ class LightTemplate(TemplateEntity, LightEntity): """Return the effect list.""" return self._effect_list + @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._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) -> int: """Flag supported features.""" supported_features = 0 - if self._level_script is not None: - supported_features |= SUPPORT_BRIGHTNESS - if self._temperature_script is not None: - supported_features |= SUPPORT_COLOR_TEMP - if self._color_script is not None: - supported_features |= SUPPORT_COLOR if self._effect_script is not None: supported_features |= LightEntityFeature.EFFECT if self._supports_transition is True: diff --git a/tests/components/template/test_light.py b/tests/components/template/test_light.py index 1199a318626..89fcc97e2f7 100644 --- a/tests/components/template/test_light.py +++ b/tests/components/template/test_light.py @@ -9,9 +9,6 @@ from homeassistant.components.light import ( ATTR_EFFECT, ATTR_HS_COLOR, ATTR_TRANSITION, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - SUPPORT_COLOR_TEMP, SUPPORT_TRANSITION, ColorMode, LightEntityFeature, @@ -115,7 +112,7 @@ async def setup_light(hass, count, light_config): @pytest.mark.parametrize("count", [1]) @pytest.mark.parametrize( "supported_features,supported_color_modes", - [(SUPPORT_BRIGHTNESS, [ColorMode.BRIGHTNESS])], + [(0, [ColorMode.BRIGHTNESS])], ) @pytest.mark.parametrize( "light_config", @@ -140,10 +137,6 @@ async def test_template_state_invalid( @pytest.mark.parametrize("count", [1]) -@pytest.mark.parametrize( - "supported_features,supported_color_modes", - [(SUPPORT_BRIGHTNESS, [ColorMode.BRIGHTNESS])], -) @pytest.mark.parametrize( "light_config", [ @@ -155,18 +148,16 @@ async def test_template_state_invalid( }, ], ) -async def test_template_state_text( - hass, supported_features, supported_color_modes, setup_light -): +async def test_template_state_text(hass, setup_light): """Test the state text of a template.""" set_state = STATE_ON hass.states.async_set("light.test_state", set_state) await hass.async_block_till_done() state = hass.states.get("light.test_template_light") assert state.state == set_state - assert state.attributes["color_mode"] == ColorMode.UNKNOWN # Brightness is None - assert state.attributes["supported_color_modes"] == supported_color_modes - assert state.attributes["supported_features"] == supported_features + assert state.attributes["color_mode"] == ColorMode.BRIGHTNESS + assert state.attributes["supported_color_modes"] == [ColorMode.BRIGHTNESS] + assert state.attributes["supported_features"] == 0 set_state = STATE_OFF hass.states.async_set("light.test_state", set_state) @@ -174,22 +165,18 @@ async def test_template_state_text( state = hass.states.get("light.test_template_light") assert state.state == set_state assert "color_mode" not in state.attributes - assert state.attributes["supported_color_modes"] == supported_color_modes - assert state.attributes["supported_features"] == supported_features + assert state.attributes["supported_color_modes"] == [ColorMode.BRIGHTNESS] + assert state.attributes["supported_features"] == 0 @pytest.mark.parametrize("count", [1]) -@pytest.mark.parametrize( - "supported_features,supported_color_modes", - [(SUPPORT_BRIGHTNESS, [ColorMode.BRIGHTNESS])], -) @pytest.mark.parametrize( "value_template,expected_state,expected_color_mode", [ ( "{{ 1 == 1 }}", STATE_ON, - ColorMode.UNKNOWN, + ColorMode.BRIGHTNESS, ), ( "{{ 1 == 2 }}", @@ -202,8 +189,6 @@ async def test_templatex_state_boolean( hass, expected_color_mode, expected_state, - supported_features, - supported_color_modes, count, value_template, ): @@ -218,8 +203,8 @@ async def test_templatex_state_boolean( state = hass.states.get("light.test_template_light") assert state.state == expected_state assert state.attributes.get("color_mode") == expected_color_mode - assert state.attributes["supported_color_modes"] == supported_color_modes - assert state.attributes["supported_features"] == supported_features + assert state.attributes["supported_color_modes"] == [ColorMode.BRIGHTNESS] + assert state.attributes["supported_features"] == 0 @pytest.mark.parametrize("count", [0]) @@ -279,10 +264,6 @@ async def test_missing_key(hass, count, setup_light): @pytest.mark.parametrize("count", [1]) -@pytest.mark.parametrize( - "supported_features,supported_color_modes", - [(SUPPORT_BRIGHTNESS, [ColorMode.BRIGHTNESS])], -) @pytest.mark.parametrize( "light_config", [ @@ -294,9 +275,7 @@ async def test_missing_key(hass, count, setup_light): }, ], ) -async def test_on_action( - hass, setup_light, calls, supported_features, supported_color_modes -): +async def test_on_action(hass, setup_light, calls): """Test on action.""" hass.states.async_set("light.test_state", STATE_OFF) await hass.async_block_till_done() @@ -304,8 +283,8 @@ async def test_on_action( state = hass.states.get("light.test_template_light") assert state.state == STATE_OFF assert "color_mode" not in state.attributes - assert state.attributes["supported_color_modes"] == supported_color_modes - assert state.attributes["supported_features"] == supported_features + assert state.attributes["supported_color_modes"] == [ColorMode.BRIGHTNESS] + assert state.attributes["supported_features"] == 0 await hass.services.async_call( light.DOMAIN, @@ -320,15 +299,11 @@ async def test_on_action( assert state.state == STATE_OFF assert "color_mode" not in state.attributes - assert state.attributes["supported_color_modes"] == supported_color_modes - assert state.attributes["supported_features"] == supported_features + assert state.attributes["supported_color_modes"] == [ColorMode.BRIGHTNESS] + assert state.attributes["supported_features"] == 0 @pytest.mark.parametrize("count", [1]) -@pytest.mark.parametrize( - "supported_features,supported_color_modes", - [(SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION, [ColorMode.BRIGHTNESS])], -) @pytest.mark.parametrize( "light_config", [ @@ -358,9 +333,7 @@ async def test_on_action( }, ], ) -async def test_on_action_with_transition( - hass, setup_light, calls, supported_features, supported_color_modes -): +async def test_on_action_with_transition(hass, setup_light, calls): """Test on action with transition.""" hass.states.async_set("light.test_state", STATE_OFF) await hass.async_block_till_done() @@ -368,8 +341,8 @@ async def test_on_action_with_transition( state = hass.states.get("light.test_template_light") assert state.state == STATE_OFF assert "color_mode" not in state.attributes - assert state.attributes["supported_color_modes"] == supported_color_modes - assert state.attributes["supported_features"] == supported_features + assert state.attributes["supported_color_modes"] == [ColorMode.BRIGHTNESS] + assert state.attributes["supported_features"] == SUPPORT_TRANSITION await hass.services.async_call( light.DOMAIN, @@ -383,15 +356,11 @@ async def test_on_action_with_transition( assert state.state == STATE_OFF assert "color_mode" not in state.attributes - assert state.attributes["supported_color_modes"] == supported_color_modes - assert state.attributes["supported_features"] == supported_features + assert state.attributes["supported_color_modes"] == [ColorMode.BRIGHTNESS] + assert state.attributes["supported_features"] == SUPPORT_TRANSITION @pytest.mark.parametrize("count", [1]) -@pytest.mark.parametrize( - "supported_features,supported_color_modes,expected_color_mode", - [(SUPPORT_BRIGHTNESS, [ColorMode.BRIGHTNESS], ColorMode.BRIGHTNESS)], -) @pytest.mark.parametrize( "light_config", [ @@ -406,9 +375,6 @@ async def test_on_action_optimistic( hass, setup_light, calls, - supported_features, - supported_color_modes, - expected_color_mode, ): """Test on action with optimistic state.""" hass.states.async_set("light.test_state", STATE_OFF) @@ -417,8 +383,8 @@ async def test_on_action_optimistic( state = hass.states.get("light.test_template_light") assert state.state == STATE_OFF assert "color_mode" not in state.attributes - assert state.attributes["supported_color_modes"] == supported_color_modes - assert state.attributes["supported_features"] == supported_features + assert state.attributes["supported_color_modes"] == [ColorMode.BRIGHTNESS] + assert state.attributes["supported_features"] == 0 await hass.services.async_call( light.DOMAIN, @@ -432,9 +398,9 @@ async def test_on_action_optimistic( assert calls[-1].data["action"] == "turn_on" assert calls[-1].data["caller"] == "light.test_template_light" assert state.state == STATE_ON - assert state.attributes["color_mode"] == ColorMode.UNKNOWN # Brightness is None - assert state.attributes["supported_color_modes"] == supported_color_modes - assert state.attributes["supported_features"] == supported_features + assert state.attributes["color_mode"] == ColorMode.BRIGHTNESS + assert state.attributes["supported_color_modes"] == [ColorMode.BRIGHTNESS] + assert state.attributes["supported_features"] == 0 await hass.services.async_call( light.DOMAIN, @@ -449,16 +415,12 @@ async def test_on_action_optimistic( assert calls[-1].data["brightness"] == 100 assert calls[-1].data["caller"] == "light.test_template_light" assert state.state == STATE_ON - assert state.attributes["color_mode"] == expected_color_mode - assert state.attributes["supported_color_modes"] == supported_color_modes - assert state.attributes["supported_features"] == supported_features + assert state.attributes["color_mode"] == ColorMode.BRIGHTNESS + assert state.attributes["supported_color_modes"] == [ColorMode.BRIGHTNESS] + assert state.attributes["supported_features"] == 0 @pytest.mark.parametrize("count", [1]) -@pytest.mark.parametrize( - "supported_features,supported_color_modes", - [(SUPPORT_BRIGHTNESS, [ColorMode.BRIGHTNESS])], -) @pytest.mark.parametrize( "light_config", [ @@ -470,18 +432,16 @@ async def test_on_action_optimistic( }, ], ) -async def test_off_action( - hass, setup_light, calls, supported_features, supported_color_modes -): +async def test_off_action(hass, setup_light, calls): """Test off action.""" hass.states.async_set("light.test_state", STATE_ON) await hass.async_block_till_done() state = hass.states.get("light.test_template_light") assert state.state == STATE_ON - assert state.attributes["color_mode"] == ColorMode.UNKNOWN # Brightness is None - assert state.attributes["supported_color_modes"] == supported_color_modes - assert state.attributes["supported_features"] == supported_features + assert state.attributes["color_mode"] == ColorMode.BRIGHTNESS + assert state.attributes["supported_color_modes"] == [ColorMode.BRIGHTNESS] + assert state.attributes["supported_features"] == 0 await hass.services.async_call( light.DOMAIN, @@ -494,16 +454,12 @@ async def test_off_action( assert calls[-1].data["action"] == "turn_off" assert calls[-1].data["caller"] == "light.test_template_light" assert state.state == STATE_ON - assert state.attributes["color_mode"] == ColorMode.UNKNOWN # Brightness is None - assert state.attributes["supported_color_modes"] == supported_color_modes - assert state.attributes["supported_features"] == supported_features + assert state.attributes["color_mode"] == ColorMode.BRIGHTNESS + assert state.attributes["supported_color_modes"] == [ColorMode.BRIGHTNESS] + assert state.attributes["supported_features"] == 0 @pytest.mark.parametrize("count", [(1)]) -@pytest.mark.parametrize( - "supported_features,supported_color_modes", - [(SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION, [ColorMode.BRIGHTNESS])], -) @pytest.mark.parametrize( "light_config", [ @@ -533,18 +489,16 @@ async def test_off_action( }, ], ) -async def test_off_action_with_transition( - hass, setup_light, calls, supported_features, supported_color_modes -): +async def test_off_action_with_transition(hass, setup_light, calls): """Test off action with transition.""" hass.states.async_set("light.test_state", STATE_ON) await hass.async_block_till_done() state = hass.states.get("light.test_template_light") assert state.state == STATE_ON - assert state.attributes["color_mode"] == ColorMode.UNKNOWN # Brightness is None - assert state.attributes["supported_color_modes"] == supported_color_modes - assert state.attributes["supported_features"] == supported_features + assert state.attributes["color_mode"] == ColorMode.BRIGHTNESS + assert state.attributes["supported_color_modes"] == [ColorMode.BRIGHTNESS] + assert state.attributes["supported_features"] == SUPPORT_TRANSITION await hass.services.async_call( light.DOMAIN, @@ -556,16 +510,12 @@ async def test_off_action_with_transition( assert len(calls) == 1 assert calls[0].data["transition"] == 2 assert state.state == STATE_ON - assert state.attributes["color_mode"] == ColorMode.UNKNOWN # Brightness is None - assert state.attributes["supported_color_modes"] == supported_color_modes - assert state.attributes["supported_features"] == supported_features + assert state.attributes["color_mode"] == ColorMode.BRIGHTNESS + assert state.attributes["supported_color_modes"] == [ColorMode.BRIGHTNESS] + assert state.attributes["supported_features"] == SUPPORT_TRANSITION @pytest.mark.parametrize("count", [1]) -@pytest.mark.parametrize( - "supported_features,supported_color_modes", - [(SUPPORT_BRIGHTNESS, [ColorMode.BRIGHTNESS])], -) @pytest.mark.parametrize( "light_config", [ @@ -576,15 +526,13 @@ async def test_off_action_with_transition( }, ], ) -async def test_off_action_optimistic( - hass, setup_light, calls, supported_features, supported_color_modes -): +async def test_off_action_optimistic(hass, setup_light, calls): """Test off action with optimistic state.""" state = hass.states.get("light.test_template_light") assert state.state == STATE_OFF assert "color_mode" not in state.attributes - assert state.attributes["supported_color_modes"] == supported_color_modes - assert state.attributes["supported_features"] == supported_features + assert state.attributes["supported_color_modes"] == [ColorMode.BRIGHTNESS] + assert state.attributes["supported_features"] == 0 await hass.services.async_call( light.DOMAIN, @@ -597,15 +545,11 @@ async def test_off_action_optimistic( state = hass.states.get("light.test_template_light") assert state.state == STATE_OFF assert "color_mode" not in state.attributes - assert state.attributes["supported_color_modes"] == supported_color_modes - assert state.attributes["supported_features"] == supported_features + assert state.attributes["supported_color_modes"] == [ColorMode.BRIGHTNESS] + assert state.attributes["supported_features"] == 0 @pytest.mark.parametrize("count", [1]) -@pytest.mark.parametrize( - "supported_features,supported_color_modes,expected_color_mode", - [(SUPPORT_BRIGHTNESS, [ColorMode.BRIGHTNESS], ColorMode.BRIGHTNESS)], -) @pytest.mark.parametrize( "light_config", [ @@ -621,9 +565,6 @@ async def test_level_action_no_template( hass, setup_light, calls, - supported_features, - supported_color_modes, - expected_color_mode, ): """Test setting brightness with optimistic template.""" state = hass.states.get("light.test_template_light") @@ -644,9 +585,9 @@ async def test_level_action_no_template( state = hass.states.get("light.test_template_light") assert state.state == STATE_ON assert state.attributes["brightness"] == 124 - assert state.attributes["color_mode"] == expected_color_mode - assert state.attributes["supported_color_modes"] == supported_color_modes - assert state.attributes["supported_features"] == supported_features + assert state.attributes["color_mode"] == ColorMode.BRIGHTNESS + assert state.attributes["supported_color_modes"] == [ColorMode.BRIGHTNESS] + assert state.attributes["supported_features"] == 0 @pytest.mark.parametrize("count", [1]) @@ -654,26 +595,20 @@ async def test_level_action_no_template( "expected_level,level_template,expected_color_mode", [ (255, "{{255}}", ColorMode.BRIGHTNESS), - (None, "{{256}}", ColorMode.UNKNOWN), - (None, "{{x - 12}}", ColorMode.UNKNOWN), - (None, "{{ none }}", ColorMode.UNKNOWN), - (None, "", ColorMode.UNKNOWN), + (None, "{{256}}", ColorMode.BRIGHTNESS), + (None, "{{x - 12}}", ColorMode.BRIGHTNESS), + (None, "{{ none }}", ColorMode.BRIGHTNESS), + (None, "", ColorMode.BRIGHTNESS), ( None, "{{ state_attr('light.nolight', 'brightness') }}", - ColorMode.UNKNOWN, + ColorMode.BRIGHTNESS, ), ], ) -@pytest.mark.parametrize( - "supported_features,supported_color_modes", - [(SUPPORT_BRIGHTNESS, [ColorMode.BRIGHTNESS])], -) async def test_level_template( hass, expected_level, - supported_features, - supported_color_modes, expected_color_mode, count, level_template, @@ -691,8 +626,8 @@ async def test_level_template( assert state.attributes.get("brightness") == expected_level assert state.state == STATE_ON assert state.attributes["color_mode"] == expected_color_mode - assert state.attributes["supported_color_modes"] == supported_color_modes - assert state.attributes["supported_features"] == supported_features + assert state.attributes["supported_color_modes"] == [ColorMode.BRIGHTNESS] + assert state.attributes["supported_features"] == 0 @pytest.mark.parametrize("count", [1]) @@ -700,22 +635,16 @@ async def test_level_template( "expected_temp,temperature_template,expected_color_mode", [ (500, "{{500}}", ColorMode.COLOR_TEMP), - (None, "{{501}}", ColorMode.UNKNOWN), - (None, "{{x - 12}}", ColorMode.UNKNOWN), - (None, "None", ColorMode.UNKNOWN), - (None, "{{ none }}", ColorMode.UNKNOWN), - (None, "", ColorMode.UNKNOWN), + (None, "{{501}}", ColorMode.COLOR_TEMP), + (None, "{{x - 12}}", ColorMode.COLOR_TEMP), + (None, "None", ColorMode.COLOR_TEMP), + (None, "{{ none }}", ColorMode.COLOR_TEMP), + (None, "", ColorMode.COLOR_TEMP), ], ) -@pytest.mark.parametrize( - "supported_features,supported_color_modes", - [(SUPPORT_COLOR_TEMP, [ColorMode.COLOR_TEMP])], -) async def test_temperature_template( hass, expected_temp, - supported_features, - supported_color_modes, expected_color_mode, count, temperature_template, @@ -733,15 +662,11 @@ async def test_temperature_template( assert state.attributes.get("color_temp") == expected_temp assert state.state == STATE_ON assert state.attributes["color_mode"] == expected_color_mode - assert state.attributes["supported_color_modes"] == supported_color_modes - assert state.attributes["supported_features"] == supported_features + assert state.attributes["supported_color_modes"] == [ColorMode.COLOR_TEMP] + assert state.attributes["supported_features"] == 0 @pytest.mark.parametrize("count", [1]) -@pytest.mark.parametrize( - "supported_features,supported_color_modes,expected_color_mode", - [(SUPPORT_COLOR_TEMP, [ColorMode.COLOR_TEMP], ColorMode.COLOR_TEMP)], -) @pytest.mark.parametrize( "light_config", [ @@ -757,9 +682,6 @@ async def test_temperature_action_no_template( hass, setup_light, calls, - supported_features, - supported_color_modes, - expected_color_mode, ): """Test setting temperature with optimistic template.""" state = hass.states.get("light.test_template_light") @@ -781,9 +703,9 @@ async def test_temperature_action_no_template( assert state is not None assert state.attributes.get("color_temp") == 345 assert state.state == STATE_ON - assert state.attributes["color_mode"] == expected_color_mode - assert state.attributes["supported_color_modes"] == supported_color_modes - assert state.attributes["supported_features"] == supported_features + assert state.attributes["color_mode"] == ColorMode.COLOR_TEMP + assert state.attributes["supported_color_modes"] == [ColorMode.COLOR_TEMP] + assert state.attributes["supported_features"] == 0 @pytest.mark.parametrize("count", [1]) @@ -867,10 +789,6 @@ async def test_entity_picture_template(hass, setup_light): @pytest.mark.parametrize("count", [1]) -@pytest.mark.parametrize( - "supported_features,supported_color_modes, expected_color_mode", - [(SUPPORT_COLOR, [ColorMode.HS], ColorMode.HS)], -) @pytest.mark.parametrize( "light_config", [ @@ -886,9 +804,6 @@ async def test_color_action_no_template( hass, setup_light, calls, - supported_features, - supported_color_modes, - expected_color_mode, ): """Test setting color with optimistic template.""" state = hass.states.get("light.test_template_light") @@ -909,10 +824,10 @@ async def test_color_action_no_template( state = hass.states.get("light.test_template_light") assert state.state == STATE_ON - assert state.attributes["color_mode"] == expected_color_mode + assert state.attributes["color_mode"] == ColorMode.HS assert state.attributes.get("hs_color") == (40, 50) - assert state.attributes["supported_color_modes"] == supported_color_modes - assert state.attributes["supported_features"] == supported_features + assert state.attributes["supported_color_modes"] == [ColorMode.HS] + assert state.attributes["supported_features"] == 0 @pytest.mark.parametrize("count", [1]) @@ -921,23 +836,17 @@ async def test_color_action_no_template( [ ((360, 100), "{{(360, 100)}}", ColorMode.HS), ((359.9, 99.9), "{{(359.9, 99.9)}}", ColorMode.HS), - (None, "{{(361, 100)}}", ColorMode.UNKNOWN), - (None, "{{(360, 101)}}", ColorMode.UNKNOWN), - (None, "[{{(360)}},{{null}}]", ColorMode.UNKNOWN), - (None, "{{x - 12}}", ColorMode.UNKNOWN), - (None, "", ColorMode.UNKNOWN), - (None, "{{ none }}", ColorMode.UNKNOWN), + (None, "{{(361, 100)}}", ColorMode.HS), + (None, "{{(360, 101)}}", ColorMode.HS), + (None, "[{{(360)}},{{null}}]", ColorMode.HS), + (None, "{{x - 12}}", ColorMode.HS), + (None, "", ColorMode.HS), + (None, "{{ none }}", ColorMode.HS), ], ) -@pytest.mark.parametrize( - "supported_features,supported_color_modes", - [(SUPPORT_COLOR, [ColorMode.HS])], -) async def test_color_template( hass, expected_hs, - supported_features, - supported_color_modes, expected_color_mode, count, color_template, @@ -955,15 +864,11 @@ async def test_color_template( assert state.attributes.get("hs_color") == expected_hs assert state.state == STATE_ON assert state.attributes["color_mode"] == expected_color_mode - assert state.attributes["supported_color_modes"] == supported_color_modes - assert state.attributes["supported_features"] == supported_features + assert state.attributes["supported_color_modes"] == [ColorMode.HS] + assert state.attributes["supported_features"] == 0 @pytest.mark.parametrize("count", [1]) -@pytest.mark.parametrize( - "supported_features,supported_color_modes", - [(SUPPORT_COLOR | SUPPORT_COLOR_TEMP, [ColorMode.COLOR_TEMP, ColorMode.HS])], -) @pytest.mark.parametrize( "light_config", [ @@ -992,9 +897,7 @@ async def test_color_template( }, ], ) -async def test_color_and_temperature_actions_no_template( - hass, setup_light, calls, supported_features, supported_color_modes -): +async def test_color_and_temperature_actions_no_template(hass, setup_light, calls): """Test setting color and color temperature with optimistic template.""" state = hass.states.get("light.test_template_light") assert state.attributes.get("hs_color") is None @@ -1015,8 +918,11 @@ async def test_color_and_temperature_actions_no_template( assert state.attributes["color_mode"] == ColorMode.HS assert "color_temp" not in state.attributes assert state.attributes["hs_color"] == (40, 50) - assert state.attributes["supported_color_modes"] == supported_color_modes - assert state.attributes["supported_features"] == supported_features + assert state.attributes["supported_color_modes"] == [ + ColorMode.COLOR_TEMP, + ColorMode.HS, + ] + assert state.attributes["supported_features"] == 0 # Optimistically set color temp, light should be in color temp mode await hass.services.async_call( @@ -1033,8 +939,11 @@ async def test_color_and_temperature_actions_no_template( assert state.attributes["color_mode"] == ColorMode.COLOR_TEMP assert state.attributes["color_temp"] == 123 assert "hs_color" in state.attributes # Color temp represented as hs_color - assert state.attributes["supported_color_modes"] == supported_color_modes - assert state.attributes["supported_features"] == supported_features + assert state.attributes["supported_color_modes"] == [ + ColorMode.COLOR_TEMP, + ColorMode.HS, + ] + assert state.attributes["supported_features"] == 0 # Optimistically set color, light should again be in hs_color mode await hass.services.async_call( @@ -1052,8 +961,11 @@ async def test_color_and_temperature_actions_no_template( assert state.attributes["color_mode"] == ColorMode.HS assert "color_temp" not in state.attributes assert state.attributes["hs_color"] == (10, 20) - assert state.attributes["supported_color_modes"] == supported_color_modes - assert state.attributes["supported_features"] == supported_features + assert state.attributes["supported_color_modes"] == [ + ColorMode.COLOR_TEMP, + ColorMode.HS, + ] + assert state.attributes["supported_features"] == 0 # Optimistically set color temp, light should again be in color temp mode await hass.services.async_call( @@ -1070,8 +982,11 @@ async def test_color_and_temperature_actions_no_template( assert state.attributes["color_mode"] == ColorMode.COLOR_TEMP assert state.attributes["color_temp"] == 234 assert "hs_color" in state.attributes # Color temp represented as hs_color - assert state.attributes["supported_color_modes"] == supported_color_modes - assert state.attributes["supported_features"] == supported_features + assert state.attributes["supported_color_modes"] == [ + ColorMode.COLOR_TEMP, + ColorMode.HS, + ] + assert state.attributes["supported_features"] == 0 @pytest.mark.parametrize("count", [1]) From f14bb8195ff2c5118b58b5959ab22a73a884c630 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 8 Sep 2022 11:24:15 +0200 Subject: [PATCH 200/955] Import climate constants from root [a-l] (#78021) --- homeassistant/components/adax/climate.py | 7 +++++-- homeassistant/components/airzone/climate.py | 4 ++-- homeassistant/components/ambiclimate/climate.py | 7 +++++-- homeassistant/components/blebox/climate.py | 4 ++-- homeassistant/components/coolmaster/climate.py | 7 +++++-- homeassistant/components/coolmaster/config_flow.py | 2 +- homeassistant/components/demo/climate.py | 4 ++-- homeassistant/components/devolo_home_control/climate.py | 3 ++- homeassistant/components/emulated_hue/hue_api.py | 2 +- homeassistant/components/ephember/climate.py | 5 +++-- homeassistant/components/flexit/climate.py | 5 +++-- homeassistant/components/freedompro/climate.py | 4 ++-- homeassistant/components/heatmiser/climate.py | 8 ++++++-- homeassistant/components/iaqualink/climate.py | 4 ++-- homeassistant/components/incomfort/climate.py | 8 ++++++-- homeassistant/components/intellifire/climate.py | 2 +- homeassistant/components/isy994/helpers.py | 2 +- homeassistant/components/knx/schema.py | 2 +- homeassistant/components/lcn/climate.py | 8 ++++++-- homeassistant/components/lightwave/climate.py | 2 -- homeassistant/components/lyric/climate.py | 5 +++-- 21 files changed, 59 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/adax/climate.py b/homeassistant/components/adax/climate.py index 85532d9aadb..8703619ca92 100644 --- a/homeassistant/components/adax/climate.py +++ b/homeassistant/components/adax/climate.py @@ -6,8 +6,11 @@ from typing import Any from adax import Adax from adax_local import Adax as AdaxLocal -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ClimateEntityFeature, HVACMode +from homeassistant.components.climate import ( + ClimateEntity, + ClimateEntityFeature, + HVACMode, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_TEMPERATURE, diff --git a/homeassistant/components/airzone/climate.py b/homeassistant/components/airzone/climate.py index ce67142547f..fa64efa355b 100644 --- a/homeassistant/components/airzone/climate.py +++ b/homeassistant/components/airzone/climate.py @@ -27,8 +27,8 @@ from aioairzone.const import ( ) from aioairzone.exceptions import AirzoneError -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, diff --git a/homeassistant/components/ambiclimate/climate.py b/homeassistant/components/ambiclimate/climate.py index 99fefbb180e..5a5fea6c230 100644 --- a/homeassistant/components/ambiclimate/climate.py +++ b/homeassistant/components/ambiclimate/climate.py @@ -8,8 +8,11 @@ from typing import Any import ambiclimate import voluptuous as vol -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ClimateEntityFeature, HVACMode +from homeassistant.components.climate import ( + ClimateEntity, + ClimateEntityFeature, + HVACMode, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_NAME, diff --git a/homeassistant/components/blebox/climate.py b/homeassistant/components/blebox/climate.py index 78f47b1ba46..65920b170c5 100644 --- a/homeassistant/components/blebox/climate.py +++ b/homeassistant/components/blebox/climate.py @@ -2,8 +2,8 @@ from datetime import timedelta from typing import Any -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, diff --git a/homeassistant/components/coolmaster/climate.py b/homeassistant/components/coolmaster/climate.py index 8333e66753e..d2b0685cdf0 100644 --- a/homeassistant/components/coolmaster/climate.py +++ b/homeassistant/components/coolmaster/climate.py @@ -2,8 +2,11 @@ import logging from typing import Any -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ClimateEntityFeature, HVACMode +from homeassistant.components.climate import ( + ClimateEntity, + ClimateEntityFeature, + HVACMode, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.core import HomeAssistant, callback diff --git a/homeassistant/components/coolmaster/config_flow.py b/homeassistant/components/coolmaster/config_flow.py index 2c5592c156b..9ad88d36574 100644 --- a/homeassistant/components/coolmaster/config_flow.py +++ b/homeassistant/components/coolmaster/config_flow.py @@ -6,7 +6,7 @@ from typing import Any from pycoolmasternet_async import CoolMasterNet import voluptuous as vol -from homeassistant.components.climate.const import HVACMode +from homeassistant.components.climate import HVACMode from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import callback diff --git a/homeassistant/components/demo/climate.py b/homeassistant/components/demo/climate.py index 546a580f576..91594423744 100644 --- a/homeassistant/components/demo/climate.py +++ b/homeassistant/components/demo/climate.py @@ -3,10 +3,10 @@ from __future__ import annotations from typing import Any -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, diff --git a/homeassistant/components/devolo_home_control/climate.py b/homeassistant/components/devolo_home_control/climate.py index 4b8e8fc00e6..95e0628d534 100644 --- a/homeassistant/components/devolo_home_control/climate.py +++ b/homeassistant/components/devolo_home_control/climate.py @@ -10,8 +10,9 @@ from homeassistant.components.climate import ( ATTR_TEMPERATURE, TEMP_CELSIUS, ClimateEntity, + ClimateEntityFeature, + HVACMode, ) -from homeassistant.components.climate.const import ClimateEntityFeature, HVACMode from homeassistant.config_entries import ConfigEntry from homeassistant.const import PRECISION_HALVES, PRECISION_TENTHS from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index c5ff9654f90..6e840f794a6 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -23,7 +23,7 @@ from homeassistant.components import ( scene, script, ) -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( SERVICE_SET_TEMPERATURE, ClimateEntityFeature, ) diff --git a/homeassistant/components/ephember/climate.py b/homeassistant/components/ephember/climate.py index f308e116ed6..9c83a1c8a67 100644 --- a/homeassistant/components/ephember/climate.py +++ b/homeassistant/components/ephember/climate.py @@ -18,8 +18,9 @@ from pyephember.pyephember import ( ) import voluptuous as vol -from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( + PLATFORM_SCHEMA, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, diff --git a/homeassistant/components/flexit/climate.py b/homeassistant/components/flexit/climate.py index 8129f063a86..a26febd5a47 100644 --- a/homeassistant/components/flexit/climate.py +++ b/homeassistant/components/flexit/climate.py @@ -6,8 +6,9 @@ from typing import Any import voluptuous as vol -from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( + PLATFORM_SCHEMA, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, diff --git a/homeassistant/components/freedompro/climate.py b/homeassistant/components/freedompro/climate.py index 7076e48c29c..0b5f147c141 100644 --- a/homeassistant/components/freedompro/climate.py +++ b/homeassistant/components/freedompro/climate.py @@ -8,9 +8,9 @@ from typing import Any from aiohttp.client import ClientSession from pyfreedompro import put_state -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_HVAC_MODE, + ClimateEntity, ClimateEntityFeature, HVACMode, ) diff --git a/homeassistant/components/heatmiser/climate.py b/homeassistant/components/heatmiser/climate.py index bae65107f55..7f6bd0ccf9c 100644 --- a/homeassistant/components/heatmiser/climate.py +++ b/homeassistant/components/heatmiser/climate.py @@ -7,8 +7,12 @@ from typing import Any from heatmiserV3 import connection, heatmiser import voluptuous as vol -from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity -from homeassistant.components.climate.const import ClimateEntityFeature, HVACMode +from homeassistant.components.climate import ( + PLATFORM_SCHEMA, + ClimateEntity, + ClimateEntityFeature, + HVACMode, +) from homeassistant.const import ( ATTR_TEMPERATURE, CONF_HOST, diff --git a/homeassistant/components/iaqualink/climate.py b/homeassistant/components/iaqualink/climate.py index 5a8cd0ce09f..725f1b9084e 100644 --- a/homeassistant/components/iaqualink/climate.py +++ b/homeassistant/components/iaqualink/climate.py @@ -12,9 +12,9 @@ from iaqualink.const import ( ) from iaqualink.device import AqualinkHeater, AqualinkPump, AqualinkSensor, AqualinkState -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( DOMAIN as CLIMATE_DOMAIN, + ClimateEntity, ClimateEntityFeature, HVACMode, ) diff --git a/homeassistant/components/incomfort/climate.py b/homeassistant/components/incomfort/climate.py index b7b66e2b25d..1d3c18fa608 100644 --- a/homeassistant/components/incomfort/climate.py +++ b/homeassistant/components/incomfort/climate.py @@ -3,8 +3,12 @@ from __future__ import annotations from typing import Any -from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN, ClimateEntity -from homeassistant.components.climate.const import ClimateEntityFeature, HVACMode +from homeassistant.components.climate import ( + DOMAIN as CLIMATE_DOMAIN, + ClimateEntity, + ClimateEntityFeature, + HVACMode, +) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback diff --git a/homeassistant/components/intellifire/climate.py b/homeassistant/components/intellifire/climate.py index 1656be621e6..cf36e6b48a0 100644 --- a/homeassistant/components/intellifire/climate.py +++ b/homeassistant/components/intellifire/climate.py @@ -7,8 +7,8 @@ from homeassistant.components.climate import ( ClimateEntity, ClimateEntityDescription, ClimateEntityFeature, + HVACMode, ) -from homeassistant.components.climate.const import HVACMode from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/isy994/helpers.py b/homeassistant/components/isy994/helpers.py index 3b0de172a85..736cd12b9ea 100644 --- a/homeassistant/components/isy994/helpers.py +++ b/homeassistant/components/isy994/helpers.py @@ -17,7 +17,7 @@ from pyisy.programs import Programs from pyisy.variables import Variables from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR -from homeassistant.components.climate.const import DOMAIN as CLIMATE +from homeassistant.components.climate import DOMAIN as CLIMATE from homeassistant.components.fan import DOMAIN as FAN from homeassistant.components.light import DOMAIN as LIGHT from homeassistant.components.sensor import DOMAIN as SENSOR diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py index c7c1e264975..55602d6153a 100644 --- a/homeassistant/components/knx/schema.py +++ b/homeassistant/components/knx/schema.py @@ -16,7 +16,7 @@ from xknx.telegram.address import IndividualAddress, parse_device_group_address from homeassistant.components.binary_sensor import ( DEVICE_CLASSES_SCHEMA as BINARY_SENSOR_DEVICE_CLASSES_SCHEMA, ) -from homeassistant.components.climate.const import HVACMode +from homeassistant.components.climate import HVACMode from homeassistant.components.cover import ( DEVICE_CLASSES_SCHEMA as COVER_DEVICE_CLASSES_SCHEMA, ) diff --git a/homeassistant/components/lcn/climate.py b/homeassistant/components/lcn/climate.py index 31aedab2fe6..8d701fbfa2f 100644 --- a/homeassistant/components/lcn/climate.py +++ b/homeassistant/components/lcn/climate.py @@ -5,8 +5,12 @@ from typing import Any, cast import pypck -from homeassistant.components.climate import DOMAIN as DOMAIN_CLIMATE, ClimateEntity -from homeassistant.components.climate.const import ClimateEntityFeature, HVACMode +from homeassistant.components.climate import ( + DOMAIN as DOMAIN_CLIMATE, + ClimateEntity, + ClimateEntityFeature, + HVACMode, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_TEMPERATURE, diff --git a/homeassistant/components/lightwave/climate.py b/homeassistant/components/lightwave/climate.py index 968d67bbcd2..834fc7eaeca 100644 --- a/homeassistant/components/lightwave/climate.py +++ b/homeassistant/components/lightwave/climate.py @@ -7,8 +7,6 @@ from homeassistant.components.climate import ( DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, ClimateEntity, -) -from homeassistant.components.climate.const import ( ClimateEntityFeature, HVACAction, HVACMode, diff --git a/homeassistant/components/lyric/climate.py b/homeassistant/components/lyric/climate.py index 8353ae15b3c..ae4afa0b0c6 100644 --- a/homeassistant/components/lyric/climate.py +++ b/homeassistant/components/lyric/climate.py @@ -10,10 +10,11 @@ from aiolyric.objects.device import LyricDevice from aiolyric.objects.location import LyricLocation import voluptuous as vol -from homeassistant.components.climate import ClimateEntity, ClimateEntityDescription -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, + ClimateEntity, + ClimateEntityDescription, ClimateEntityFeature, HVACAction, HVACMode, From ff356205bf1f970e2661d6006a318f539ed7afef Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 8 Sep 2022 12:24:45 +0200 Subject: [PATCH 201/955] Use platform constants from root (#78032) --- homeassistant/components/alexa/logbook.py | 2 +- homeassistant/components/amcrest/camera.py | 7 +++++-- homeassistant/components/canary/__init__.py | 2 +- homeassistant/components/deconz/logbook.py | 5 +---- homeassistant/components/demo/humidifier.py | 7 +++++-- homeassistant/components/doorbird/logbook.py | 2 +- homeassistant/components/elkm1/logbook.py | 5 +---- homeassistant/components/emulated_hue/hue_api.py | 5 +---- homeassistant/components/google_assistant/logbook.py | 5 +---- homeassistant/components/homekit/logbook.py | 2 +- homeassistant/components/homekit/type_humidifiers.py | 4 ++-- homeassistant/components/hue/logbook.py | 5 +---- homeassistant/components/lutron_caseta/logbook.py | 5 +---- homeassistant/components/motioneye/__init__.py | 2 +- homeassistant/components/mqtt/siren.py | 10 ++++------ homeassistant/components/nest/camera_sdm.py | 3 +-- homeassistant/components/overkiz/siren.py | 7 +++++-- homeassistant/components/prometheus/__init__.py | 5 +---- homeassistant/components/push/camera.py | 3 +-- homeassistant/components/rfxtrx/siren.py | 3 +-- homeassistant/components/shelly/logbook.py | 5 +---- homeassistant/components/zha/logbook.py | 5 +---- homeassistant/components/zha/siren.py | 3 ++- homeassistant/components/zwave_js/humidifier.py | 8 +++----- homeassistant/components/zwave_js/logbook.py | 5 +---- homeassistant/components/zwave_js/siren.py | 3 ++- 26 files changed, 46 insertions(+), 72 deletions(-) diff --git a/homeassistant/components/alexa/logbook.py b/homeassistant/components/alexa/logbook.py index b72b884ec29..079fea99fdf 100644 --- a/homeassistant/components/alexa/logbook.py +++ b/homeassistant/components/alexa/logbook.py @@ -1,5 +1,5 @@ """Describe logbook events.""" -from homeassistant.components.logbook.const import ( +from homeassistant.components.logbook import ( LOGBOOK_ENTRY_ENTITY_ID, LOGBOOK_ENTRY_MESSAGE, LOGBOOK_ENTRY_NAME, diff --git a/homeassistant/components/amcrest/camera.py b/homeassistant/components/amcrest/camera.py index da5e046a88a..df742b320db 100644 --- a/homeassistant/components/amcrest/camera.py +++ b/homeassistant/components/amcrest/camera.py @@ -12,8 +12,11 @@ from amcrest import AmcrestError from haffmpeg.camera import CameraMjpeg import voluptuous as vol -from homeassistant.components.camera import Camera, CameraEntityFeature -from homeassistant.components.camera.const import DOMAIN as CAMERA_DOMAIN +from homeassistant.components.camera import ( + DOMAIN as CAMERA_DOMAIN, + Camera, + CameraEntityFeature, +) from homeassistant.components.ffmpeg import FFmpegManager, get_ffmpeg_manager from homeassistant.const import ATTR_ENTITY_ID, CONF_NAME, STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/canary/__init__.py b/homeassistant/components/canary/__init__.py index e6ea20e0768..bc360f99581 100644 --- a/homeassistant/components/canary/__init__.py +++ b/homeassistant/components/canary/__init__.py @@ -9,7 +9,7 @@ from canary.api import Api from requests.exceptions import ConnectTimeout, HTTPError import voluptuous as vol -from homeassistant.components.camera.const import DOMAIN as CAMERA_DOMAIN +from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/deconz/logbook.py b/homeassistant/components/deconz/logbook.py index 07dc7cb0124..65ed7f8e31d 100644 --- a/homeassistant/components/deconz/logbook.py +++ b/homeassistant/components/deconz/logbook.py @@ -3,10 +3,7 @@ from __future__ import annotations from collections.abc import Callable -from homeassistant.components.logbook.const import ( - LOGBOOK_ENTRY_MESSAGE, - LOGBOOK_ENTRY_NAME, -) +from homeassistant.components.logbook import LOGBOOK_ENTRY_MESSAGE, LOGBOOK_ENTRY_NAME from homeassistant.const import ATTR_DEVICE_ID, CONF_EVENT from homeassistant.core import Event, HomeAssistant, callback import homeassistant.helpers.device_registry as dr diff --git a/homeassistant/components/demo/humidifier.py b/homeassistant/components/demo/humidifier.py index c998a32ab55..571cfbe8db9 100644 --- a/homeassistant/components/demo/humidifier.py +++ b/homeassistant/components/demo/humidifier.py @@ -3,8 +3,11 @@ from __future__ import annotations from typing import Any -from homeassistant.components.humidifier import HumidifierDeviceClass, HumidifierEntity -from homeassistant.components.humidifier.const import HumidifierEntityFeature +from homeassistant.components.humidifier import ( + HumidifierDeviceClass, + HumidifierEntity, + HumidifierEntityFeature, +) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback diff --git a/homeassistant/components/doorbird/logbook.py b/homeassistant/components/doorbird/logbook.py index 3b1563c2880..f3beebe6971 100644 --- a/homeassistant/components/doorbird/logbook.py +++ b/homeassistant/components/doorbird/logbook.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import Any -from homeassistant.components.logbook.const import ( +from homeassistant.components.logbook import ( LOGBOOK_ENTRY_ENTITY_ID, LOGBOOK_ENTRY_MESSAGE, LOGBOOK_ENTRY_NAME, diff --git a/homeassistant/components/elkm1/logbook.py b/homeassistant/components/elkm1/logbook.py index 9aa85b599e0..e86e58d23fd 100644 --- a/homeassistant/components/elkm1/logbook.py +++ b/homeassistant/components/elkm1/logbook.py @@ -3,10 +3,7 @@ from __future__ import annotations from collections.abc import Callable -from homeassistant.components.logbook.const import ( - LOGBOOK_ENTRY_MESSAGE, - LOGBOOK_ENTRY_NAME, -) +from homeassistant.components.logbook import LOGBOOK_ENTRY_MESSAGE, LOGBOOK_ENTRY_NAME from homeassistant.core import Event, HomeAssistant, callback from .const import ( diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index 6e840f794a6..aa43fbad910 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -34,10 +34,7 @@ from homeassistant.components.cover import ( ) from homeassistant.components.fan import ATTR_PERCENTAGE, FanEntityFeature from homeassistant.components.http import HomeAssistantView -from homeassistant.components.humidifier.const import ( - ATTR_HUMIDITY, - SERVICE_SET_HUMIDITY, -) +from homeassistant.components.humidifier import ATTR_HUMIDITY, SERVICE_SET_HUMIDITY from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, diff --git a/homeassistant/components/google_assistant/logbook.py b/homeassistant/components/google_assistant/logbook.py index 0ed5745004d..ac12ae2cb8c 100644 --- a/homeassistant/components/google_assistant/logbook.py +++ b/homeassistant/components/google_assistant/logbook.py @@ -1,8 +1,5 @@ """Describe logbook events.""" -from homeassistant.components.logbook.const import ( - LOGBOOK_ENTRY_MESSAGE, - LOGBOOK_ENTRY_NAME, -) +from homeassistant.components.logbook import LOGBOOK_ENTRY_MESSAGE, LOGBOOK_ENTRY_NAME from homeassistant.core import callback from .const import DOMAIN, EVENT_COMMAND_RECEIVED, SOURCE_CLOUD diff --git a/homeassistant/components/homekit/logbook.py b/homeassistant/components/homekit/logbook.py index a513b31b232..17cdac51799 100644 --- a/homeassistant/components/homekit/logbook.py +++ b/homeassistant/components/homekit/logbook.py @@ -2,7 +2,7 @@ from collections.abc import Callable from typing import Any -from homeassistant.components.logbook.const import ( +from homeassistant.components.logbook import ( LOGBOOK_ENTRY_ENTITY_ID, LOGBOOK_ENTRY_MESSAGE, LOGBOOK_ENTRY_NAME, diff --git a/homeassistant/components/homekit/type_humidifiers.py b/homeassistant/components/homekit/type_humidifiers.py index 09cfc02dcce..0babb285bed 100644 --- a/homeassistant/components/homekit/type_humidifiers.py +++ b/homeassistant/components/homekit/type_humidifiers.py @@ -3,8 +3,7 @@ import logging from pyhap.const import CATEGORY_HUMIDIFIER -from homeassistant.components.humidifier import HumidifierDeviceClass -from homeassistant.components.humidifier.const import ( +from homeassistant.components.humidifier import ( ATTR_HUMIDITY, ATTR_MAX_HUMIDITY, ATTR_MIN_HUMIDITY, @@ -12,6 +11,7 @@ from homeassistant.components.humidifier.const import ( DEFAULT_MIN_HUMIDITY, DOMAIN, SERVICE_SET_HUMIDITY, + HumidifierDeviceClass, ) from homeassistant.const import ( ATTR_DEVICE_CLASS, diff --git a/homeassistant/components/hue/logbook.py b/homeassistant/components/hue/logbook.py index 412ca044b58..ce09c4c7ac9 100644 --- a/homeassistant/components/hue/logbook.py +++ b/homeassistant/components/hue/logbook.py @@ -3,10 +3,7 @@ from __future__ import annotations from collections.abc import Callable -from homeassistant.components.logbook.const import ( - LOGBOOK_ENTRY_MESSAGE, - LOGBOOK_ENTRY_NAME, -) +from homeassistant.components.logbook import LOGBOOK_ENTRY_MESSAGE, LOGBOOK_ENTRY_NAME from homeassistant.const import CONF_DEVICE_ID, CONF_EVENT, CONF_ID, CONF_TYPE from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers import device_registry as dr diff --git a/homeassistant/components/lutron_caseta/logbook.py b/homeassistant/components/lutron_caseta/logbook.py index bcca548f64b..7bf1b467ff6 100644 --- a/homeassistant/components/lutron_caseta/logbook.py +++ b/homeassistant/components/lutron_caseta/logbook.py @@ -3,10 +3,7 @@ from __future__ import annotations from collections.abc import Callable -from homeassistant.components.logbook.const import ( - LOGBOOK_ENTRY_MESSAGE, - LOGBOOK_ENTRY_NAME, -) +from homeassistant.components.logbook import LOGBOOK_ENTRY_MESSAGE, LOGBOOK_ENTRY_NAME from homeassistant.core import Event, HomeAssistant, callback from .const import ( diff --git a/homeassistant/components/motioneye/__init__.py b/homeassistant/components/motioneye/__init__.py index 6a650142995..37562d7d15f 100644 --- a/homeassistant/components/motioneye/__init__.py +++ b/homeassistant/components/motioneye/__init__.py @@ -35,7 +35,7 @@ from motioneye_client.const import ( KEY_WEB_HOOK_STORAGE_URL, ) -from homeassistant.components.camera.const import DOMAIN as CAMERA_DOMAIN +from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN from homeassistant.components.media_source.const import URI_SCHEME from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN diff --git a/homeassistant/components/mqtt/siren.py b/homeassistant/components/mqtt/siren.py index 5ed76fd6330..1f5111a5499 100644 --- a/homeassistant/components/mqtt/siren.py +++ b/homeassistant/components/mqtt/siren.py @@ -10,16 +10,14 @@ import voluptuous as vol from homeassistant.components import siren from homeassistant.components.siren import ( - TURN_ON_SCHEMA, - SirenEntity, - SirenEntityFeature, - process_turn_on_params, -) -from homeassistant.components.siren.const import ( ATTR_AVAILABLE_TONES, ATTR_DURATION, ATTR_TONE, ATTR_VOLUME_LEVEL, + TURN_ON_SCHEMA, + SirenEntity, + SirenEntityFeature, + process_turn_on_params, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( diff --git a/homeassistant/components/nest/camera_sdm.py b/homeassistant/components/nest/camera_sdm.py index e148916d7e8..f83f914385e 100644 --- a/homeassistant/components/nest/camera_sdm.py +++ b/homeassistant/components/nest/camera_sdm.py @@ -18,8 +18,7 @@ from google_nest_sdm.device import Device from google_nest_sdm.device_manager import DeviceManager from google_nest_sdm.exceptions import ApiException -from homeassistant.components.camera import Camera, CameraEntityFeature -from homeassistant.components.camera.const import StreamType +from homeassistant.components.camera import Camera, CameraEntityFeature, StreamType from homeassistant.components.stream import CONF_EXTRA_PART_WAIT_TIME from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/overkiz/siren.py b/homeassistant/components/overkiz/siren.py index 02736f6f50a..f60eb04cfd3 100644 --- a/homeassistant/components/overkiz/siren.py +++ b/homeassistant/components/overkiz/siren.py @@ -4,8 +4,11 @@ from typing import Any from pyoverkiz.enums import OverkizState from pyoverkiz.enums.command import OverkizCommand, OverkizCommandParam -from homeassistant.components.siren import SirenEntity, SirenEntityFeature -from homeassistant.components.siren.const import ATTR_DURATION +from homeassistant.components.siren import ( + ATTR_DURATION, + SirenEntity, + SirenEntityFeature, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/prometheus/__init__.py b/homeassistant/components/prometheus/__init__.py index d54074088b0..d247af922d3 100644 --- a/homeassistant/components/prometheus/__init__.py +++ b/homeassistant/components/prometheus/__init__.py @@ -17,10 +17,7 @@ from homeassistant.components.climate import ( HVACAction, ) from homeassistant.components.http import HomeAssistantView -from homeassistant.components.humidifier.const import ( - ATTR_AVAILABLE_MODES, - ATTR_HUMIDITY, -) +from homeassistant.components.humidifier import ATTR_AVAILABLE_MODES, ATTR_HUMIDITY from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_DEVICE_CLASS, diff --git a/homeassistant/components/push/camera.py b/homeassistant/components/push/camera.py index 36c59732ee2..77bcf63e17e 100644 --- a/homeassistant/components/push/camera.py +++ b/homeassistant/components/push/camera.py @@ -11,8 +11,7 @@ import async_timeout import voluptuous as vol from homeassistant.components import webhook -from homeassistant.components.camera import PLATFORM_SCHEMA, STATE_IDLE, Camera -from homeassistant.components.camera.const import DOMAIN +from homeassistant.components.camera import DOMAIN, PLATFORM_SCHEMA, STATE_IDLE, Camera from homeassistant.const import CONF_NAME, CONF_TIMEOUT, CONF_WEBHOOK_ID from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv diff --git a/homeassistant/components/rfxtrx/siren.py b/homeassistant/components/rfxtrx/siren.py index acf06518959..0b49a7d4d8c 100644 --- a/homeassistant/components/rfxtrx/siren.py +++ b/homeassistant/components/rfxtrx/siren.py @@ -6,8 +6,7 @@ from typing import Any import RFXtrx as rfxtrxmod -from homeassistant.components.siren import SirenEntity, SirenEntityFeature -from homeassistant.components.siren.const import ATTR_TONE +from homeassistant.components.siren import ATTR_TONE, SirenEntity, SirenEntityFeature from homeassistant.config_entries import ConfigEntry from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers.entity import Entity diff --git a/homeassistant/components/shelly/logbook.py b/homeassistant/components/shelly/logbook.py index a91f4e1cf56..337b40fff04 100644 --- a/homeassistant/components/shelly/logbook.py +++ b/homeassistant/components/shelly/logbook.py @@ -3,10 +3,7 @@ from __future__ import annotations from collections.abc import Callable -from homeassistant.components.logbook.const import ( - LOGBOOK_ENTRY_MESSAGE, - LOGBOOK_ENTRY_NAME, -) +from homeassistant.components.logbook import LOGBOOK_ENTRY_MESSAGE, LOGBOOK_ENTRY_NAME from homeassistant.const import ATTR_DEVICE_ID from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.typing import EventType diff --git a/homeassistant/components/zha/logbook.py b/homeassistant/components/zha/logbook.py index 90d433be210..0c8fd6523a8 100644 --- a/homeassistant/components/zha/logbook.py +++ b/homeassistant/components/zha/logbook.py @@ -4,10 +4,7 @@ from __future__ import annotations from collections.abc import Callable from typing import TYPE_CHECKING -from homeassistant.components.logbook.const import ( - LOGBOOK_ENTRY_MESSAGE, - LOGBOOK_ENTRY_NAME, -) +from homeassistant.components.logbook import LOGBOOK_ENTRY_MESSAGE, LOGBOOK_ENTRY_NAME from homeassistant.const import ATTR_COMMAND, ATTR_DEVICE_ID from homeassistant.core import Event, HomeAssistant, callback import homeassistant.helpers.device_registry as dr diff --git a/homeassistant/components/zha/siren.py b/homeassistant/components/zha/siren.py index 66cd2bf4002..dedb339292e 100644 --- a/homeassistant/components/zha/siren.py +++ b/homeassistant/components/zha/siren.py @@ -9,10 +9,11 @@ from zigpy.zcl.clusters.security import IasWd as WD from homeassistant.components.siren import ( ATTR_DURATION, + ATTR_TONE, + ATTR_VOLUME_LEVEL, SirenEntity, SirenEntityFeature, ) -from homeassistant.components.siren.const import ATTR_TONE, ATTR_VOLUME_LEVEL from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant, callback diff --git a/homeassistant/components/zwave_js/humidifier.py b/homeassistant/components/zwave_js/humidifier.py index 5aeb6d0272f..80760930c2e 100644 --- a/homeassistant/components/zwave_js/humidifier.py +++ b/homeassistant/components/zwave_js/humidifier.py @@ -15,14 +15,12 @@ from zwave_js_server.model.driver import Driver from zwave_js_server.model.value import Value as ZwaveValue from homeassistant.components.humidifier import ( - HumidifierDeviceClass, - HumidifierEntity, - HumidifierEntityDescription, -) -from homeassistant.components.humidifier.const import ( DEFAULT_MAX_HUMIDITY, DEFAULT_MIN_HUMIDITY, DOMAIN as HUMIDIFIER_DOMAIN, + HumidifierDeviceClass, + HumidifierEntity, + HumidifierEntityDescription, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback diff --git a/homeassistant/components/zwave_js/logbook.py b/homeassistant/components/zwave_js/logbook.py index 1fe1ff79ec6..1f634ba5ffd 100644 --- a/homeassistant/components/zwave_js/logbook.py +++ b/homeassistant/components/zwave_js/logbook.py @@ -5,10 +5,7 @@ from collections.abc import Callable from zwave_js_server.const import CommandClass -from homeassistant.components.logbook.const import ( - LOGBOOK_ENTRY_MESSAGE, - LOGBOOK_ENTRY_NAME, -) +from homeassistant.components.logbook import LOGBOOK_ENTRY_MESSAGE, LOGBOOK_ENTRY_NAME from homeassistant.const import ATTR_DEVICE_ID from homeassistant.core import Event, HomeAssistant, callback import homeassistant.helpers.device_registry as dr diff --git a/homeassistant/components/zwave_js/siren.py b/homeassistant/components/zwave_js/siren.py index 67e6aa4afb4..5a53d115528 100644 --- a/homeassistant/components/zwave_js/siren.py +++ b/homeassistant/components/zwave_js/siren.py @@ -8,11 +8,12 @@ from zwave_js_server.const.command_class.sound_switch import ToneID from zwave_js_server.model.driver import Driver from homeassistant.components.siren import ( + ATTR_TONE, + ATTR_VOLUME_LEVEL, DOMAIN as SIREN_DOMAIN, SirenEntity, SirenEntityFeature, ) -from homeassistant.components.siren.const import ATTR_TONE, ATTR_VOLUME_LEVEL from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect From 03f4eb84a0c1ed05657b87b1512e7910797a90e2 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 8 Sep 2022 12:46:41 +0200 Subject: [PATCH 202/955] Improve type hints in demo and dependencies (#78022) --- homeassistant/components/demo/__init__.py | 2 +- .../components/demo/image_processing.py | 21 +++++++--------- .../components/image_processing/__init__.py | 24 ++++++++++++++----- homeassistant/components/mailbox/__init__.py | 4 ++-- .../openalpr_local/image_processing.py | 10 ++++---- 5 files changed, 35 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/demo/__init__.py b/homeassistant/components/demo/__init__.py index 45ce061fb1d..7ed989903e5 100644 --- a/homeassistant/components/demo/__init__.py +++ b/homeassistant/components/demo/__init__.py @@ -260,7 +260,7 @@ async def _insert_sum_statistics( start: datetime.datetime, end: datetime.datetime, max_diff: float, -): +) -> None: statistics: list[StatisticData] = [] now = start sum_ = 0.0 diff --git a/homeassistant/components/demo/image_processing.py b/homeassistant/components/demo/image_processing.py index 6ba498114a4..5d158db46ab 100644 --- a/homeassistant/components/demo/image_processing.py +++ b/homeassistant/components/demo/image_processing.py @@ -3,10 +3,7 @@ from __future__ import annotations from homeassistant.components.camera import Image from homeassistant.components.image_processing import ( - ATTR_AGE, - ATTR_CONFIDENCE, - ATTR_GENDER, - ATTR_NAME, + FaceInformation, ImageProcessingFaceEntity, ) from homeassistant.components.openalpr_local.image_processing import ( @@ -87,14 +84,14 @@ class DemoImageProcessingFace(ImageProcessingFaceEntity): def process_image(self, image: Image) -> None: """Process image.""" demo_data = [ - { - ATTR_CONFIDENCE: 98.34, - ATTR_NAME: "Hans", - ATTR_AGE: 16.0, - ATTR_GENDER: "male", - }, - {ATTR_NAME: "Helena", ATTR_AGE: 28.0, ATTR_GENDER: "female"}, - {ATTR_CONFIDENCE: 62.53, ATTR_NAME: "Luna"}, + FaceInformation( + confidence=98.34, + name="Hans", + age=16.0, + gender="male", + ), + FaceInformation(name="Helena", age=28.0, gender="female"), + FaceInformation(confidence=62.53, name="Luna"), ] self.process_faces(demo_data, 4) diff --git a/homeassistant/components/image_processing/__init__.py b/homeassistant/components/image_processing/__init__.py index 5c932826197..115d32c5d5b 100644 --- a/homeassistant/components/image_processing/__init__.py +++ b/homeassistant/components/image_processing/__init__.py @@ -2,7 +2,7 @@ import asyncio from datetime import timedelta import logging -from typing import final +from typing import Final, TypedDict, final import voluptuous as vol @@ -40,7 +40,7 @@ SERVICE_SCAN = "scan" EVENT_DETECT_FACE = "image_processing.detect_face" ATTR_AGE = "age" -ATTR_CONFIDENCE = "confidence" +ATTR_CONFIDENCE: Final = "confidence" ATTR_FACES = "faces" ATTR_GENDER = "gender" ATTR_GLASSES = "glasses" @@ -70,6 +70,18 @@ PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend( PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE.extend(PLATFORM_SCHEMA.schema) +class FaceInformation(TypedDict, total=False): + """Face information.""" + + confidence: float + name: str + age: float + gender: str + motion: str + glasses: str + entity_id: str + + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the image processing.""" component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL) @@ -142,9 +154,9 @@ class ImageProcessingEntity(Entity): class ImageProcessingFaceEntity(ImageProcessingEntity): """Base entity class for face image processing.""" - def __init__(self): + def __init__(self) -> None: """Initialize base face identify/verify entity.""" - self.faces = [] + self.faces: list[FaceInformation] = [] self.total_faces = 0 @property @@ -182,14 +194,14 @@ class ImageProcessingFaceEntity(ImageProcessingEntity): """Return device specific state attributes.""" return {ATTR_FACES: self.faces, ATTR_TOTAL_FACES: self.total_faces} - def process_faces(self, faces, total): + def process_faces(self, faces: list[FaceInformation], total: int) -> None: """Send event with detected faces and store data.""" run_callback_threadsafe( self.hass.loop, self.async_process_faces, faces, total ).result() @callback - def async_process_faces(self, faces, total): + def async_process_faces(self, faces: list[FaceInformation], total: int) -> None: """Send event with detected faces and store data. known are a dict in follow format: diff --git a/homeassistant/components/mailbox/__init__.py b/homeassistant/components/mailbox/__init__.py index 89b646beee4..b6a727083c9 100644 --- a/homeassistant/components/mailbox/__init__.py +++ b/homeassistant/components/mailbox/__init__.py @@ -144,13 +144,13 @@ class MailboxEntity(Entity): class Mailbox: """Represent a mailbox device.""" - def __init__(self, hass, name): + def __init__(self, hass: HomeAssistant, name: str) -> None: """Initialize mailbox object.""" self.hass = hass self.name = name @callback - def async_update(self): + def async_update(self) -> None: """Send event notification of updated mailbox.""" self.hass.bus.async_fire(EVENT) diff --git a/homeassistant/components/openalpr_local/image_processing.py b/homeassistant/components/openalpr_local/image_processing.py index ef8c942189f..a2c0bc99287 100644 --- a/homeassistant/components/openalpr_local/image_processing.py +++ b/homeassistant/components/openalpr_local/image_processing.py @@ -102,9 +102,9 @@ async def async_setup_platform( class ImageProcessingAlprEntity(ImageProcessingEntity): """Base entity class for ALPR image processing.""" - def __init__(self): + def __init__(self) -> None: """Initialize base ALPR entity.""" - self.plates = {} + self.plates: dict[str, float] = {} self.vehicles = 0 @property @@ -130,18 +130,18 @@ class ImageProcessingAlprEntity(ImageProcessingEntity): """Return device specific state attributes.""" return {ATTR_PLATES: self.plates, ATTR_VEHICLES: self.vehicles} - def process_plates(self, plates, vehicles): + def process_plates(self, plates: dict[str, float], vehicles: int) -> None: """Send event with new plates and store data.""" run_callback_threadsafe( self.hass.loop, self.async_process_plates, plates, vehicles ).result() @callback - def async_process_plates(self, plates, vehicles): + def async_process_plates(self, plates: dict[str, float], vehicles: int) -> None: """Send event with new plates and store data. plates are a dict in follow format: - { 'plate': confidence } + { '': confidence } This method must be run in the event loop. """ From 2a23792b23fbc127f45715faa5d47504129fc297 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 8 Sep 2022 08:58:53 -0500 Subject: [PATCH 203/955] Bump bluetooth-adapters to 0.3.5 (#78052) --- 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 ca6a76c55ae..aa890a47a41 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -6,7 +6,7 @@ "quality_scale": "internal", "requirements": [ "bleak==0.16.0", - "bluetooth-adapters==0.3.4", + "bluetooth-adapters==0.3.5", "bluetooth-auto-recovery==0.3.1" ], "codeowners": ["@bdraco"], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7c11725e460..9608cdbfe9e 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ attrs==21.2.0 awesomeversion==22.8.0 bcrypt==3.1.7 bleak==0.16.0 -bluetooth-adapters==0.3.4 +bluetooth-adapters==0.3.5 bluetooth-auto-recovery==0.3.1 certifi>=2021.5.30 ciso8601==2.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 59949dfffd1..dccbd7946c8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -430,7 +430,7 @@ bluemaestro-ble==0.2.0 # bluepy==1.3.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.3.4 +bluetooth-adapters==0.3.5 # homeassistant.components.bluetooth bluetooth-auto-recovery==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 76b39c62cbd..8bb07f63b8c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -341,7 +341,7 @@ blinkpy==0.19.0 bluemaestro-ble==0.2.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.3.4 +bluetooth-adapters==0.3.5 # homeassistant.components.bluetooth bluetooth-auto-recovery==0.3.1 From 01189b023c67ee98361223d658ded575447e2460 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 8 Sep 2022 12:53:30 -0400 Subject: [PATCH 204/955] Increase rate limit for zwave_js updates Al provided a new key which bumps the rate limit from 10k per hour to 100k per hour --- homeassistant/components/zwave_js/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index db3da247e7d..ff1a97d6ecc 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -123,5 +123,5 @@ ENTITY_DESC_KEY_TOTAL_INCREASING = "total_increasing" # This API key is only for use with Home Assistant. Reach out to Z-Wave JS to apply for # your own (https://github.com/zwave-js/firmware-updates/). API_KEY_FIRMWARE_UPDATE_SERVICE = ( - "55eea74f055bef2ad893348112df6a38980600aaf82d2b02011297fc7ba495f830ca2b70" + "2e39d98fc56386389fbb35e5a98fa1b44b9fdd8f971460303587cff408430d4cfcde6134" ) From 03e6bd08113180e682ea77c1d2f407c90474b083 Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Thu, 8 Sep 2022 11:13:20 -0600 Subject: [PATCH 205/955] Bump pylitterbot to 2022.9.1 (#78071) --- homeassistant/components/litterrobot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/litterrobot/conftest.py | 8 +++++--- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/litterrobot/manifest.json b/homeassistant/components/litterrobot/manifest.json index f25b4525877..a4c9f3cd54e 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.8.2"], + "requirements": ["pylitterbot==2022.9.1"], "codeowners": ["@natekspencer", "@tkdrob"], "dhcp": [{ "hostname": "litter-robot4" }], "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index dccbd7946c8..b02587fffb1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1668,7 +1668,7 @@ pylibrespot-java==0.1.0 pylitejet==0.3.0 # homeassistant.components.litterrobot -pylitterbot==2022.8.2 +pylitterbot==2022.9.1 # homeassistant.components.lutron_caseta pylutron-caseta==0.13.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8bb07f63b8c..16abe872d33 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1166,7 +1166,7 @@ pylibrespot-java==0.1.0 pylitejet==0.3.0 # homeassistant.components.litterrobot -pylitterbot==2022.8.2 +pylitterbot==2022.9.1 # homeassistant.components.lutron_caseta pylutron-caseta==0.13.1 diff --git a/tests/components/litterrobot/conftest.py b/tests/components/litterrobot/conftest.py index e5d5e730b61..34132ec66d6 100644 --- a/tests/components/litterrobot/conftest.py +++ b/tests/components/litterrobot/conftest.py @@ -17,13 +17,13 @@ from tests.common import MockConfigEntry def create_mock_robot( - robot_data: dict | None = None, side_effect: Any | None = None + robot_data: dict | None, account: Account, side_effect: Any | None = None ) -> Robot: """Create a mock Litter-Robot device.""" if not robot_data: robot_data = {} - robot = LitterRobot3(data={**ROBOT_DATA, **robot_data}) + robot = LitterRobot3(data={**ROBOT_DATA, **robot_data}, account=account) robot.start_cleaning = AsyncMock(side_effect=side_effect) robot.set_power_status = AsyncMock(side_effect=side_effect) robot.reset_waste_drawer = AsyncMock(side_effect=side_effect) @@ -44,7 +44,9 @@ def create_mock_account( account = MagicMock(spec=Account) account.connect = AsyncMock() account.refresh_robots = AsyncMock() - account.robots = [] if skip_robots else [create_mock_robot(robot_data, side_effect)] + account.robots = ( + [] if skip_robots else [create_mock_robot(robot_data, account, side_effect)] + ) return account From be064bfeefc3e1bdc26bf04adcca7d1bb4e2285c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 8 Sep 2022 12:15:26 -0500 Subject: [PATCH 206/955] Bump bluetooth-auto-recovery to 0.3.2 (#78063) --- 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 aa890a47a41..3043d6412a4 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -7,7 +7,7 @@ "requirements": [ "bleak==0.16.0", "bluetooth-adapters==0.3.5", - "bluetooth-auto-recovery==0.3.1" + "bluetooth-auto-recovery==0.3.2" ], "codeowners": ["@bdraco"], "config_flow": true, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 9608cdbfe9e..1a1fcb9da84 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,7 +12,7 @@ awesomeversion==22.8.0 bcrypt==3.1.7 bleak==0.16.0 bluetooth-adapters==0.3.5 -bluetooth-auto-recovery==0.3.1 +bluetooth-auto-recovery==0.3.2 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==37.0.4 diff --git a/requirements_all.txt b/requirements_all.txt index b02587fffb1..e3a2be7b16a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -433,7 +433,7 @@ bluemaestro-ble==0.2.0 bluetooth-adapters==0.3.5 # homeassistant.components.bluetooth -bluetooth-auto-recovery==0.3.1 +bluetooth-auto-recovery==0.3.2 # homeassistant.components.bond bond-async==0.1.22 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 16abe872d33..c75ad99b6dc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -344,7 +344,7 @@ bluemaestro-ble==0.2.0 bluetooth-adapters==0.3.5 # homeassistant.components.bluetooth -bluetooth-auto-recovery==0.3.1 +bluetooth-auto-recovery==0.3.2 # homeassistant.components.bond bond-async==0.1.22 From f11b51e12b0549819400f13f95cdd6cd9b78c026 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 8 Sep 2022 20:15:27 +0200 Subject: [PATCH 207/955] Fix zwave_js device re-interview (#78046) * Handle stale node and entity info on re-interview * Add test * Unsubscribe on config entry unload --- homeassistant/components/zwave_js/__init__.py | 34 ++++++++--- homeassistant/components/zwave_js/entity.py | 13 ++++- homeassistant/components/zwave_js/update.py | 8 +++ tests/components/zwave_js/test_init.py | 57 +++++++++++++++++++ 4 files changed, 103 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 98219520693..066bc5101ae 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -313,19 +313,24 @@ class ControllerEvents: node, ) + LOGGER.debug("Node added: %s", node.node_id) + + # Listen for ready node events, both new and re-interview. + self.config_entry.async_on_unload( + node.on( + "ready", + lambda event: self.hass.async_create_task( + self.node_events.async_on_node_ready(event["node"]) + ), + ) + ) + # we only want to run discovery when the node has reached ready state, # otherwise we'll have all kinds of missing info issues. if node.ready: await self.node_events.async_on_node_ready(node) return - # if node is not yet ready, register one-time callback for ready state - LOGGER.debug("Node added: %s - waiting for it to become ready", node.node_id) - node.once( - "ready", - lambda event: self.hass.async_create_task( - self.node_events.async_on_node_ready(event["node"]) - ), - ) + # we do submit the node to device registry so user has # some visual feedback that something is (in the process of) being added self.register_node_in_dev_reg(node) @@ -414,12 +419,25 @@ class NodeEvents: async def async_on_node_ready(self, node: ZwaveNode) -> None: """Handle node ready event.""" LOGGER.debug("Processing node %s", node) + driver = self.controller_events.driver_events.driver # register (or update) node in device registry device = self.controller_events.register_node_in_dev_reg(node) # We only want to create the defaultdict once, even on reinterviews if device.id not in self.controller_events.registered_unique_ids: self.controller_events.registered_unique_ids[device.id] = defaultdict(set) + # Remove any old value ids if this is a reinterview. + self.controller_events.discovered_value_ids.pop(device.id, None) + # Remove stale entities that may exist from a previous interview. + async_dispatcher_send( + self.hass, + ( + f"{DOMAIN}_" + f"{get_valueless_base_unique_id(driver, node)}_" + "remove_entity_on_ready_node" + ), + ) + value_updates_disc_info: dict[str, ZwaveDiscoveryInfo] = {} # run discovery on all node values and create/update entities diff --git a/homeassistant/components/zwave_js/entity.py b/homeassistant/components/zwave_js/entity.py index 79dd1d27a4c..65f00b5022a 100644 --- a/homeassistant/components/zwave_js/entity.py +++ b/homeassistant/components/zwave_js/entity.py @@ -12,7 +12,7 @@ from homeassistant.helpers.entity import DeviceInfo, Entity from .const import DOMAIN, LOGGER from .discovery import ZwaveDiscoveryInfo -from .helpers import get_device_id, get_unique_id +from .helpers import get_device_id, get_unique_id, get_valueless_base_unique_id EVENT_VALUE_UPDATED = "value updated" EVENT_VALUE_REMOVED = "value removed" @@ -96,6 +96,17 @@ class ZWaveBaseEntity(Entity): self.async_on_remove( self.info.node.on(EVENT_VALUE_REMOVED, self._value_removed) ) + self.async_on_remove( + async_dispatcher_connect( + self.hass, + ( + f"{DOMAIN}_" + f"{get_valueless_base_unique_id(self.driver, self.info.node)}_" + "remove_entity_on_ready_node" + ), + self.async_remove, + ) + ) for status_event in (EVENT_ALIVE, EVENT_DEAD): self.async_on_remove( diff --git a/homeassistant/components/zwave_js/update.py b/homeassistant/components/zwave_js/update.py index 97c14746dd9..d106c7d6dd3 100644 --- a/homeassistant/components/zwave_js/update.py +++ b/homeassistant/components/zwave_js/update.py @@ -189,6 +189,14 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): ) ) + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{DOMAIN}_{self._base_unique_id}_remove_entity_on_ready_node", + self.async_remove, + ) + ) + self.async_on_remove(async_at_start(self.hass, self._async_update)) async def async_will_remove_from_hass(self) -> None: diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index d038949d494..63cbc090e7d 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -3,6 +3,7 @@ from copy import deepcopy from unittest.mock import call, patch import pytest +from zwave_js_server.client import Client from zwave_js_server.event import Event from zwave_js_server.exceptions import BaseZwaveJSServerError, InvalidServerVersion from zwave_js_server.model.node import Node @@ -12,6 +13,7 @@ from homeassistant.components.zwave_js.const import DOMAIN from homeassistant.components.zwave_js.helpers import get_device_id from homeassistant.config_entries import ConfigEntryDisabler, ConfigEntryState from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.core import HomeAssistant from homeassistant.helpers import ( area_registry as ar, device_registry as dr, @@ -242,6 +244,61 @@ async def test_existing_node_ready(hass, client, multisensor_6, integration): ) +async def test_existing_node_reinterview( + hass: HomeAssistant, + client: Client, + multisensor_6_state: dict, + multisensor_6: Node, + integration: MockConfigEntry, +) -> None: + """Test we handle a node re-interview firing a node ready event.""" + dev_reg = dr.async_get(hass) + node = multisensor_6 + assert client.driver is not None + air_temperature_device_id = f"{client.driver.controller.home_id}-{node.node_id}" + air_temperature_device_id_ext = ( + f"{air_temperature_device_id}-{node.manufacturer_id}:" + f"{node.product_type}:{node.product_id}" + ) + + state = hass.states.get(AIR_TEMPERATURE_SENSOR) + + assert state # entity and device added + assert state.state != STATE_UNAVAILABLE + + device = dev_reg.async_get_device(identifiers={(DOMAIN, air_temperature_device_id)}) + assert device + assert device == dev_reg.async_get_device( + identifiers={(DOMAIN, air_temperature_device_id_ext)} + ) + assert device.sw_version == "1.12" + + node_state = deepcopy(multisensor_6_state) + node_state["firmwareVersion"] = "1.13" + event = Event( + type="ready", + data={ + "source": "node", + "event": "ready", + "nodeId": node.node_id, + "nodeState": node_state, + }, + ) + client.driver.receive_event(event) + await hass.async_block_till_done() + + state = hass.states.get(AIR_TEMPERATURE_SENSOR) + + assert state + assert state.state != STATE_UNAVAILABLE + device = dev_reg.async_get_device(identifiers={(DOMAIN, air_temperature_device_id)}) + assert device + assert device == dev_reg.async_get_device( + identifiers={(DOMAIN, air_temperature_device_id_ext)} + ) + assert device.sw_version == "1.13" + + async def test_existing_node_not_ready(hass, zp3111_not_ready, client, integration): """Test we handle a non-ready node that exists during integration setup.""" dev_reg = dr.async_get(hass) From c528a2d2cd19f1c70184ed9162f499dd99e3e53d Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Thu, 8 Sep 2022 20:28:40 +0200 Subject: [PATCH 208/955] Bump velbus-aio to 2022.9.1 (#78039) Bump velbusaio to 2022.9.1 --- homeassistant/components/velbus/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/velbus/manifest.json b/homeassistant/components/velbus/manifest.json index ec0c0f5f2d9..cbc8db0ca9f 100644 --- a/homeassistant/components/velbus/manifest.json +++ b/homeassistant/components/velbus/manifest.json @@ -2,7 +2,7 @@ "domain": "velbus", "name": "Velbus", "documentation": "https://www.home-assistant.io/integrations/velbus", - "requirements": ["velbus-aio==2022.6.2"], + "requirements": ["velbus-aio==2022.9.1"], "config_flow": true, "codeowners": ["@Cereal2nd", "@brefra"], "dependencies": ["usb"], diff --git a/requirements_all.txt b/requirements_all.txt index e3a2be7b16a..2947ea72798 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2455,7 +2455,7 @@ vallox-websocket-api==2.12.0 vehicle==0.4.0 # homeassistant.components.velbus -velbus-aio==2022.6.2 +velbus-aio==2022.9.1 # homeassistant.components.venstar venstarcolortouch==0.18 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c75ad99b6dc..f08a496dc5c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1683,7 +1683,7 @@ vallox-websocket-api==2.12.0 vehicle==0.4.0 # homeassistant.components.velbus -velbus-aio==2022.6.2 +velbus-aio==2022.9.1 # homeassistant.components.venstar venstarcolortouch==0.18 From 7937bfeedb51b6c4b6d66e5b171e861cd5373f5f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 8 Sep 2022 22:03:43 +0200 Subject: [PATCH 209/955] Deprecate history integration's statistics API (#78056) --- homeassistant/components/history/__init__.py | 83 +--- .../components/recorder/websocket_api.py | 129 +++++- tests/components/history/test_init.py | 397 ++---------------- .../components/recorder/test_websocket_api.py | 366 +++++++++++++++- 4 files changed, 535 insertions(+), 440 deletions(-) diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index 77301532d3d..bae6c95507e 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -6,22 +6,22 @@ from datetime import datetime as dt, timedelta from http import HTTPStatus import logging import time -from typing import Literal, cast +from typing import cast from aiohttp import web import voluptuous as vol from homeassistant.components import frontend, websocket_api from homeassistant.components.http import HomeAssistantView -from homeassistant.components.recorder import get_instance, history +from homeassistant.components.recorder import ( + get_instance, + history, + websocket_api as recorder_ws, +) from homeassistant.components.recorder.filters import ( Filters, sqlalchemy_filter_from_include_exclude_conf, ) -from homeassistant.components.recorder.statistics import ( - list_statistic_ids, - statistics_during_period, -) from homeassistant.components.recorder.util import session_scope from homeassistant.components.websocket_api import messages from homeassistant.core import HomeAssistant @@ -68,23 +68,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -def _ws_get_statistics_during_period( - hass: HomeAssistant, - msg_id: int, - start_time: dt, - end_time: dt | None = None, - statistic_ids: list[str] | None = None, - period: Literal["5minute", "day", "hour", "month"] = "hour", -) -> str: - """Fetch statistics and convert them to json in the executor.""" - return JSON_DUMP( - messages.result_message( - msg_id, - statistics_during_period(hass, start_time, end_time, statistic_ids, period), - ) - ) - - @websocket_api.websocket_command( { vol.Required("type"): "history/statistics_during_period", @@ -99,46 +82,11 @@ async def ws_get_statistics_during_period( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict ) -> None: """Handle statistics websocket command.""" - start_time_str = msg["start_time"] - end_time_str = msg.get("end_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: - 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 - else: - end_time = None - - connection.send_message( - await get_instance(hass).async_add_executor_job( - _ws_get_statistics_during_period, - hass, - msg["id"], - start_time, - end_time, - msg.get("statistic_ids"), - msg.get("period"), - ) - ) - - -def _ws_get_list_statistic_ids( - hass: HomeAssistant, - msg_id: int, - statistic_type: Literal["mean"] | Literal["sum"] | None = None, -) -> str: - """Fetch a list of available statistic_id and convert them to json in the executor.""" - return JSON_DUMP( - messages.result_message(msg_id, list_statistic_ids(hass, None, statistic_type)) + _LOGGER.warning( + "WS API 'history/statistics_during_period' is deprecated and will be removed in " + "Home Assistant Core 2022.12. Use 'recorder/statistics_during_period' instead" ) + await recorder_ws.ws_handle_get_statistics_during_period(hass, connection, msg) @websocket_api.websocket_command( @@ -152,14 +100,11 @@ async def ws_get_list_statistic_ids( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict ) -> None: """Fetch a list of available statistic_id.""" - connection.send_message( - await get_instance(hass).async_add_executor_job( - _ws_get_list_statistic_ids, - hass, - msg["id"], - msg.get("statistic_type"), - ) + _LOGGER.warning( + "WS API 'history/list_statistic_ids' is deprecated and will be removed in " + "Home Assistant Core 2022.12. Use 'recorder/list_statistic_ids' instead" ) + await recorder_ws.ws_handle_list_statistic_ids(hass, connection, msg) def _ws_get_significant_states( diff --git a/homeassistant/components/recorder/websocket_api.py b/homeassistant/components/recorder/websocket_api.py index 16813944780..70552bca67e 100644 --- a/homeassistant/components/recorder/websocket_api.py +++ b/homeassistant/components/recorder/websocket_api.py @@ -1,13 +1,17 @@ """The Recorder websocket API.""" from __future__ import annotations +from datetime import datetime as dt import logging +from typing import Literal import voluptuous as vol from homeassistant.components import websocket_api +from homeassistant.components.websocket_api import messages from homeassistant.core import HomeAssistant, callback, valid_entity_id from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.json import JSON_DUMP from homeassistant.util import dt as dt_util from .const import MAX_QUEUE_BACKLOG @@ -15,6 +19,7 @@ from .statistics import ( async_add_external_statistics, async_import_statistics, list_statistic_ids, + statistics_during_period, validate_statistics, ) from .util import async_migration_in_progress, async_migration_is_live, get_instance @@ -25,15 +30,125 @@ _LOGGER: logging.Logger = logging.getLogger(__package__) @callback def async_setup(hass: HomeAssistant) -> None: """Set up the recorder websocket API.""" - websocket_api.async_register_command(hass, ws_validate_statistics) - websocket_api.async_register_command(hass, ws_clear_statistics) - websocket_api.async_register_command(hass, ws_get_statistics_metadata) - websocket_api.async_register_command(hass, ws_update_statistics_metadata) - websocket_api.async_register_command(hass, ws_info) - websocket_api.async_register_command(hass, ws_backup_start) - websocket_api.async_register_command(hass, ws_backup_end) websocket_api.async_register_command(hass, ws_adjust_sum_statistics) + websocket_api.async_register_command(hass, ws_backup_end) + websocket_api.async_register_command(hass, ws_backup_start) + websocket_api.async_register_command(hass, ws_clear_statistics) + 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) websocket_api.async_register_command(hass, ws_import_statistics) + websocket_api.async_register_command(hass, ws_info) + websocket_api.async_register_command(hass, ws_update_statistics_metadata) + websocket_api.async_register_command(hass, ws_validate_statistics) + + +def _ws_get_statistics_during_period( + hass: HomeAssistant, + msg_id: int, + start_time: dt, + end_time: dt | None = None, + statistic_ids: list[str] | None = None, + period: Literal["5minute", "day", "hour", "month"] = "hour", +) -> str: + """Fetch statistics and convert them to json in the executor.""" + return JSON_DUMP( + messages.result_message( + msg_id, + statistics_during_period(hass, start_time, end_time, statistic_ids, period), + ) + ) + + +async def ws_handle_get_statistics_during_period( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict +) -> None: + """Handle statistics websocket command.""" + start_time_str = msg["start_time"] + end_time_str = msg.get("end_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: + 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 + else: + end_time = None + + connection.send_message( + await get_instance(hass).async_add_executor_job( + _ws_get_statistics_during_period, + hass, + msg["id"], + start_time, + end_time, + msg.get("statistic_ids"), + msg.get("period"), + ) + ) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "recorder/statistics_during_period", + vol.Required("start_time"): str, + vol.Optional("end_time"): str, + vol.Optional("statistic_ids"): [str], + vol.Required("period"): vol.Any("5minute", "hour", "day", "month"), + } +) +@websocket_api.async_response +async def ws_get_statistics_during_period( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict +) -> None: + """Handle statistics websocket command.""" + await ws_handle_get_statistics_during_period(hass, connection, msg) + + +def _ws_get_list_statistic_ids( + hass: HomeAssistant, + msg_id: int, + statistic_type: Literal["mean"] | Literal["sum"] | None = None, +) -> str: + """Fetch a list of available statistic_id and convert them to json in the executor.""" + return JSON_DUMP( + messages.result_message(msg_id, list_statistic_ids(hass, None, statistic_type)) + ) + + +async def ws_handle_list_statistic_ids( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict +) -> None: + """Fetch a list of available statistic_id.""" + connection.send_message( + await get_instance(hass).async_add_executor_job( + _ws_get_list_statistic_ids, + hass, + msg["id"], + msg.get("statistic_type"), + ) + ) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "recorder/list_statistic_ids", + vol.Optional("statistic_type"): vol.Any("sum", "mean"), + } +) +@websocket_api.async_response +async def ws_list_statistic_ids( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict +) -> None: + """Fetch a list of available statistic_id.""" + await ws_handle_list_statistic_ids(hass, connection, msg) @websocket_api.websocket_command( diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index 854ddb76191..5441722c9d7 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -5,24 +5,24 @@ from http import HTTPStatus import json from unittest.mock import patch, sentinel -from freezegun import freeze_time import pytest -from pytest import approx from homeassistant.components import history from homeassistant.components.recorder.history import get_significant_states from homeassistant.components.recorder.models import process_timestamp +from homeassistant.components.recorder.websocket_api import ( + ws_handle_get_statistics_during_period, + ws_handle_list_statistic_ids, +) from homeassistant.const import CONF_DOMAINS, CONF_ENTITIES, CONF_EXCLUDE, CONF_INCLUDE import homeassistant.core as ha from homeassistant.helpers.json import JSONEncoder from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM from tests.components.recorder.common import ( async_recorder_block_till_done, async_wait_recording_done, - do_adhoc_statistics, wait_recording_done, ) @@ -844,51 +844,13 @@ async def test_entity_ids_limit_via_api_with_skip_initial_state( assert response_json[1][0]["entity_id"] == "light.cow" -POWER_SENSOR_ATTRIBUTES = { - "device_class": "power", - "state_class": "measurement", - "unit_of_measurement": "kW", -} -PRESSURE_SENSOR_ATTRIBUTES = { - "device_class": "pressure", - "state_class": "measurement", - "unit_of_measurement": "hPa", -} -TEMPERATURE_SENSOR_ATTRIBUTES = { - "device_class": "temperature", - "state_class": "measurement", - "unit_of_measurement": "°C", -} - - -@pytest.mark.parametrize( - "units, attributes, state, value", - [ - (IMPERIAL_SYSTEM, POWER_SENSOR_ATTRIBUTES, 10, 10000), - (METRIC_SYSTEM, POWER_SENSOR_ATTRIBUTES, 10, 10000), - (IMPERIAL_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, 10, 50), - (METRIC_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, 10, 10), - (IMPERIAL_SYSTEM, PRESSURE_SENSOR_ATTRIBUTES, 1000, 14.503774389728312), - (METRIC_SYSTEM, PRESSURE_SENSOR_ATTRIBUTES, 1000, 100000), - ], -) -async def test_statistics_during_period( - hass, hass_ws_client, recorder_mock, units, attributes, state, value -): - """Test statistics_during_period.""" +async def test_statistics_during_period(hass, hass_ws_client, recorder_mock, caplog): + """Test history/statistics_during_period forwards to recorder.""" now = dt_util.utcnow() - - hass.config.units = units await async_setup_component(hass, "history", {}) - await async_setup_component(hass, "sensor", {}) - await async_recorder_block_till_done(hass) - hass.states.async_set("sensor.test", state, attributes=attributes) - await async_wait_recording_done(hass) - - do_adhoc_statistics(hass, start=now) - await async_wait_recording_done(hass) - client = await hass_ws_client() + + # Test the WS API works and issues a warning await client.send_json( { "id": 1, @@ -903,322 +865,53 @@ async def test_statistics_during_period( assert response["success"] assert response["result"] == {} - await client.send_json( - { - "id": 2, - "type": "history/statistics_during_period", - "start_time": now.isoformat(), - "statistic_ids": ["sensor.test"], - "period": "5minute", - } - ) - response = await client.receive_json() - assert response["success"] - assert response["result"] == { - "sensor.test": [ + assert ( + "WS API 'history/statistics_during_period' is deprecated and will be removed in " + "Home Assistant Core 2022.12. Use 'recorder/statistics_during_period' instead" + ) in caplog.text + + # Test the WS API forwards to recorder + with patch( + "homeassistant.components.history.recorder_ws.ws_handle_get_statistics_during_period", + wraps=ws_handle_get_statistics_during_period, + ) as ws_mock: + await client.send_json( { - "statistic_id": "sensor.test", - "start": now.isoformat(), - "end": (now + timedelta(minutes=5)).isoformat(), - "mean": approx(value), - "min": approx(value), - "max": approx(value), - "last_reset": None, - "state": None, - "sum": None, + "id": 2, + "type": "history/statistics_during_period", + "start_time": now.isoformat(), + "end_time": now.isoformat(), + "statistic_ids": ["sensor.test"], + "period": "hour", } - ] - } + ) + await client.receive_json() + ws_mock.assert_awaited_once() -@pytest.mark.parametrize( - "units, attributes, state, value", - [ - (IMPERIAL_SYSTEM, POWER_SENSOR_ATTRIBUTES, 10, 10000), - (METRIC_SYSTEM, POWER_SENSOR_ATTRIBUTES, 10, 10000), - (IMPERIAL_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, 10, 50), - (METRIC_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, 10, 10), - (IMPERIAL_SYSTEM, PRESSURE_SENSOR_ATTRIBUTES, 1000, 14.503774389728312), - (METRIC_SYSTEM, PRESSURE_SENSOR_ATTRIBUTES, 1000, 100000), - ], -) -async def test_statistics_during_period_in_the_past( - hass, hass_ws_client, recorder_mock, units, attributes, state, value -): - """Test statistics_during_period in the past.""" - hass.config.set_time_zone("UTC") - now = dt_util.utcnow().replace() - - hass.config.units = units +async def test_list_statistic_ids(hass, hass_ws_client, recorder_mock, caplog): + """Test history/list_statistic_ids forwards to recorder.""" await async_setup_component(hass, "history", {}) - await async_setup_component(hass, "sensor", {}) - await async_recorder_block_till_done(hass) - - past = now - timedelta(days=3) - - with freeze_time(past): - hass.states.async_set("sensor.test", state, attributes=attributes) - await async_wait_recording_done(hass) - - sensor_state = hass.states.get("sensor.test") - assert sensor_state.last_updated == past - - stats_top_of_hour = past.replace(minute=0, second=0, microsecond=0) - stats_start = past.replace(minute=55) - do_adhoc_statistics(hass, start=stats_start) - await async_wait_recording_done(hass) - client = await hass_ws_client() - await client.send_json( - { - "id": 1, - "type": "history/statistics_during_period", - "start_time": now.isoformat(), - "end_time": now.isoformat(), - "statistic_ids": ["sensor.test"], - "period": "hour", - } - ) - response = await client.receive_json() - assert response["success"] - assert response["result"] == {} - await client.send_json( - { - "id": 2, - "type": "history/statistics_during_period", - "start_time": now.isoformat(), - "statistic_ids": ["sensor.test"], - "period": "5minute", - } - ) - response = await client.receive_json() - assert response["success"] - assert response["result"] == {} - - past = now - timedelta(days=3, hours=1) - await client.send_json( - { - "id": 3, - "type": "history/statistics_during_period", - "start_time": past.isoformat(), - "statistic_ids": ["sensor.test"], - "period": "5minute", - } - ) - response = await client.receive_json() - assert response["success"] - assert response["result"] == { - "sensor.test": [ - { - "statistic_id": "sensor.test", - "start": stats_start.isoformat(), - "end": (stats_start + timedelta(minutes=5)).isoformat(), - "mean": approx(value), - "min": approx(value), - "max": approx(value), - "last_reset": None, - "state": None, - "sum": None, - } - ] - } - - start_of_day = stats_top_of_hour.replace(hour=0, minute=0) - await client.send_json( - { - "id": 4, - "type": "history/statistics_during_period", - "start_time": stats_top_of_hour.isoformat(), - "statistic_ids": ["sensor.test"], - "period": "day", - } - ) - response = await client.receive_json() - assert response["success"] - assert response["result"] == { - "sensor.test": [ - { - "statistic_id": "sensor.test", - "start": start_of_day.isoformat(), - "end": (start_of_day + timedelta(days=1)).isoformat(), - "mean": approx(value), - "min": approx(value), - "max": approx(value), - "last_reset": None, - "state": None, - "sum": None, - } - ] - } - - await client.send_json( - { - "id": 5, - "type": "history/statistics_during_period", - "start_time": now.isoformat(), - "statistic_ids": ["sensor.test"], - "period": "5minute", - } - ) - response = await client.receive_json() - assert response["success"] - assert response["result"] == {} - - -async def test_statistics_during_period_bad_start_time( - hass, hass_ws_client, recorder_mock -): - """Test statistics_during_period.""" - await async_setup_component( - hass, - "history", - {"history": {}}, - ) - - client = await hass_ws_client() - await client.send_json( - { - "id": 1, - "type": "history/statistics_during_period", - "start_time": "cats", - "period": "5minute", - } - ) - response = await client.receive_json() - assert not response["success"] - assert response["error"]["code"] == "invalid_start_time" - - -async def test_statistics_during_period_bad_end_time( - hass, hass_ws_client, recorder_mock -): - """Test statistics_during_period.""" - now = dt_util.utcnow() - - await async_setup_component( - hass, - "history", - {"history": {}}, - ) - - client = await hass_ws_client() - await client.send_json( - { - "id": 1, - "type": "history/statistics_during_period", - "start_time": now.isoformat(), - "end_time": "dogs", - "period": "5minute", - } - ) - response = await client.receive_json() - assert not response["success"] - assert response["error"]["code"] == "invalid_end_time" - - -@pytest.mark.parametrize( - "units, attributes, display_unit, statistics_unit", - [ - (IMPERIAL_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W", "W"), - (METRIC_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W", "W"), - (IMPERIAL_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, "°F", "°C"), - (METRIC_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, "°C", "°C"), - (IMPERIAL_SYSTEM, PRESSURE_SENSOR_ATTRIBUTES, "psi", "Pa"), - (METRIC_SYSTEM, PRESSURE_SENSOR_ATTRIBUTES, "Pa", "Pa"), - ], -) -async def test_list_statistic_ids( - hass, - hass_ws_client, - recorder_mock, - units, - attributes, - display_unit, - statistics_unit, -): - """Test list_statistic_ids.""" - now = dt_util.utcnow() - - hass.config.units = units - await async_setup_component(hass, "history", {"history": {}}) - await async_setup_component(hass, "sensor", {}) - await async_recorder_block_till_done(hass) - - client = await hass_ws_client() + # Test the WS API works and issues a warning await client.send_json({"id": 1, "type": "history/list_statistic_ids"}) response = await client.receive_json() assert response["success"] assert response["result"] == [] - hass.states.async_set("sensor.test", 10, attributes=attributes) - await async_wait_recording_done(hass) + assert ( + "WS API 'history/list_statistic_ids' is deprecated and will be removed in " + "Home Assistant Core 2022.12. Use 'recorder/list_statistic_ids' instead" + ) in caplog.text - await client.send_json({"id": 2, "type": "history/list_statistic_ids"}) - response = await client.receive_json() - assert response["success"] - assert response["result"] == [ - { - "statistic_id": "sensor.test", - "has_mean": True, - "has_sum": False, - "name": None, - "source": "recorder", - "display_unit_of_measurement": display_unit, - "statistics_unit_of_measurement": statistics_unit, - } - ] - - do_adhoc_statistics(hass, start=now) - await async_recorder_block_till_done(hass) - # Remove the state, statistics will now be fetched from the database - hass.states.async_remove("sensor.test") - await hass.async_block_till_done() - - await client.send_json({"id": 3, "type": "history/list_statistic_ids"}) - response = await client.receive_json() - assert response["success"] - assert response["result"] == [ - { - "statistic_id": "sensor.test", - "has_mean": True, - "has_sum": False, - "name": None, - "source": "recorder", - "display_unit_of_measurement": display_unit, - "statistics_unit_of_measurement": statistics_unit, - } - ] - - await client.send_json( - {"id": 4, "type": "history/list_statistic_ids", "statistic_type": "dogs"} - ) - response = await client.receive_json() - assert not response["success"] - - await client.send_json( - {"id": 5, "type": "history/list_statistic_ids", "statistic_type": "mean"} - ) - response = await client.receive_json() - assert response["success"] - assert response["result"] == [ - { - "statistic_id": "sensor.test", - "has_mean": True, - "has_sum": False, - "name": None, - "source": "recorder", - "display_unit_of_measurement": display_unit, - "statistics_unit_of_measurement": statistics_unit, - } - ] - - await client.send_json( - {"id": 6, "type": "history/list_statistic_ids", "statistic_type": "sum"} - ) - response = await client.receive_json() - assert response["success"] - assert response["result"] == [] + with patch( + "homeassistant.components.history.recorder_ws.ws_handle_list_statistic_ids", + wraps=ws_handle_list_statistic_ids, + ) as ws_mock: + await client.send_json({"id": 2, "type": "history/list_statistic_ids"}) + await client.receive_json() + ws_mock.assert_called_once() async def test_history_during_period(hass, hass_ws_client, recorder_mock): @@ -1239,7 +932,6 @@ async def test_history_during_period(hass, hass_ws_client, recorder_mock): hass.states.async_set("sensor.test", "on", attributes={"any": "attr"}) await async_wait_recording_done(hass) - do_adhoc_statistics(hass, start=now) await async_wait_recording_done(hass) client = await hass_ws_client() @@ -1358,8 +1050,6 @@ async def test_history_during_period_impossible_conditions( hass, hass_ws_client, recorder_mock ): """Test history_during_period returns when condition cannot be true.""" - now = dt_util.utcnow() - await async_setup_component(hass, "history", {}) await async_setup_component(hass, "sensor", {}) await async_recorder_block_till_done(hass) @@ -1374,7 +1064,6 @@ async def test_history_during_period_impossible_conditions( hass.states.async_set("sensor.test", "on", attributes={"any": "attr"}) await async_wait_recording_done(hass) - do_adhoc_statistics(hass, start=now) await async_wait_recording_done(hass) after = dt_util.utcnow() @@ -1440,7 +1129,6 @@ async def test_history_during_period_significant_domain( hass.states.async_set("climate.test", "on", attributes={"temperature": "5"}) await async_wait_recording_done(hass) - do_adhoc_statistics(hass, start=now) await async_wait_recording_done(hass) client = await hass_ws_client() @@ -1664,7 +1352,6 @@ async def test_history_during_period_with_use_include_order( hass.states.async_set("switch.excluded", "off", attributes={"any": "again"}) await async_wait_recording_done(hass) - do_adhoc_statistics(hass, start=now) await async_wait_recording_done(hass) client = await hass_ws_client() diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index 269cebcba9f..cdec26be26d 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -4,6 +4,7 @@ from datetime import timedelta import threading from unittest.mock import patch +from freezegun import freeze_time import pytest from pytest import approx @@ -18,7 +19,7 @@ from homeassistant.components.recorder.statistics import ( from homeassistant.helpers import recorder as recorder_helper from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from homeassistant.util.unit_system import METRIC_SYSTEM +from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM from .common import ( async_recorder_block_till_done, @@ -34,6 +35,11 @@ POWER_SENSOR_ATTRIBUTES = { "state_class": "measurement", "unit_of_measurement": "kW", } +PRESSURE_SENSOR_ATTRIBUTES = { + "device_class": "pressure", + "state_class": "measurement", + "unit_of_measurement": "hPa", +} TEMPERATURE_SENSOR_ATTRIBUTES = { "device_class": "temperature", "state_class": "measurement", @@ -51,6 +57,351 @@ GAS_SENSOR_ATTRIBUTES = { } +@pytest.mark.parametrize( + "units, attributes, state, value", + [ + (IMPERIAL_SYSTEM, POWER_SENSOR_ATTRIBUTES, 10, 10000), + (METRIC_SYSTEM, POWER_SENSOR_ATTRIBUTES, 10, 10000), + (IMPERIAL_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, 10, 50), + (METRIC_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, 10, 10), + (IMPERIAL_SYSTEM, PRESSURE_SENSOR_ATTRIBUTES, 1000, 14.503774389728312), + (METRIC_SYSTEM, PRESSURE_SENSOR_ATTRIBUTES, 1000, 100000), + ], +) +async def test_statistics_during_period( + hass, hass_ws_client, recorder_mock, units, attributes, state, value +): + """Test statistics_during_period.""" + now = dt_util.utcnow() + + hass.config.units = units + await async_setup_component(hass, "sensor", {}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.test", state, attributes=attributes) + await async_wait_recording_done(hass) + + do_adhoc_statistics(hass, start=now) + await async_wait_recording_done(hass) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "recorder/statistics_during_period", + "start_time": now.isoformat(), + "end_time": now.isoformat(), + "statistic_ids": ["sensor.test"], + "period": "hour", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == {} + + await client.send_json( + { + "id": 2, + "type": "recorder/statistics_during_period", + "start_time": now.isoformat(), + "statistic_ids": ["sensor.test"], + "period": "5minute", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + "sensor.test": [ + { + "statistic_id": "sensor.test", + "start": now.isoformat(), + "end": (now + timedelta(minutes=5)).isoformat(), + "mean": approx(value), + "min": approx(value), + "max": approx(value), + "last_reset": None, + "state": None, + "sum": None, + } + ] + } + + +@pytest.mark.parametrize( + "units, attributes, state, value", + [ + (IMPERIAL_SYSTEM, POWER_SENSOR_ATTRIBUTES, 10, 10000), + (METRIC_SYSTEM, POWER_SENSOR_ATTRIBUTES, 10, 10000), + (IMPERIAL_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, 10, 50), + (METRIC_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, 10, 10), + (IMPERIAL_SYSTEM, PRESSURE_SENSOR_ATTRIBUTES, 1000, 14.503774389728312), + (METRIC_SYSTEM, PRESSURE_SENSOR_ATTRIBUTES, 1000, 100000), + ], +) +async def test_statistics_during_period_in_the_past( + hass, hass_ws_client, recorder_mock, units, attributes, state, value +): + """Test statistics_during_period in the past.""" + hass.config.set_time_zone("UTC") + now = dt_util.utcnow().replace() + + hass.config.units = units + await async_setup_component(hass, "sensor", {}) + await async_recorder_block_till_done(hass) + + past = now - timedelta(days=3) + + with freeze_time(past): + hass.states.async_set("sensor.test", state, attributes=attributes) + await async_wait_recording_done(hass) + + sensor_state = hass.states.get("sensor.test") + assert sensor_state.last_updated == past + + stats_top_of_hour = past.replace(minute=0, second=0, microsecond=0) + stats_start = past.replace(minute=55) + do_adhoc_statistics(hass, start=stats_start) + await async_wait_recording_done(hass) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "recorder/statistics_during_period", + "start_time": now.isoformat(), + "end_time": now.isoformat(), + "statistic_ids": ["sensor.test"], + "period": "hour", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == {} + + await client.send_json( + { + "id": 2, + "type": "recorder/statistics_during_period", + "start_time": now.isoformat(), + "statistic_ids": ["sensor.test"], + "period": "5minute", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == {} + + past = now - timedelta(days=3, hours=1) + await client.send_json( + { + "id": 3, + "type": "recorder/statistics_during_period", + "start_time": past.isoformat(), + "statistic_ids": ["sensor.test"], + "period": "5minute", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + "sensor.test": [ + { + "statistic_id": "sensor.test", + "start": stats_start.isoformat(), + "end": (stats_start + timedelta(minutes=5)).isoformat(), + "mean": approx(value), + "min": approx(value), + "max": approx(value), + "last_reset": None, + "state": None, + "sum": None, + } + ] + } + + start_of_day = stats_top_of_hour.replace(hour=0, minute=0) + await client.send_json( + { + "id": 4, + "type": "recorder/statistics_during_period", + "start_time": stats_top_of_hour.isoformat(), + "statistic_ids": ["sensor.test"], + "period": "day", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + "sensor.test": [ + { + "statistic_id": "sensor.test", + "start": start_of_day.isoformat(), + "end": (start_of_day + timedelta(days=1)).isoformat(), + "mean": approx(value), + "min": approx(value), + "max": approx(value), + "last_reset": None, + "state": None, + "sum": None, + } + ] + } + + await client.send_json( + { + "id": 5, + "type": "recorder/statistics_during_period", + "start_time": now.isoformat(), + "statistic_ids": ["sensor.test"], + "period": "5minute", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == {} + + +async def test_statistics_during_period_bad_start_time( + hass, hass_ws_client, recorder_mock +): + """Test statistics_during_period.""" + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "recorder/statistics_during_period", + "start_time": "cats", + "period": "5minute", + } + ) + response = await client.receive_json() + assert not response["success"] + assert response["error"]["code"] == "invalid_start_time" + + +async def test_statistics_during_period_bad_end_time( + hass, hass_ws_client, recorder_mock +): + """Test statistics_during_period.""" + now = dt_util.utcnow() + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "recorder/statistics_during_period", + "start_time": now.isoformat(), + "end_time": "dogs", + "period": "5minute", + } + ) + response = await client.receive_json() + assert not response["success"] + assert response["error"]["code"] == "invalid_end_time" + + +@pytest.mark.parametrize( + "units, attributes, display_unit, statistics_unit", + [ + (IMPERIAL_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W", "W"), + (METRIC_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W", "W"), + (IMPERIAL_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, "°F", "°C"), + (METRIC_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, "°C", "°C"), + (IMPERIAL_SYSTEM, PRESSURE_SENSOR_ATTRIBUTES, "psi", "Pa"), + (METRIC_SYSTEM, PRESSURE_SENSOR_ATTRIBUTES, "Pa", "Pa"), + ], +) +async def test_list_statistic_ids( + hass, + hass_ws_client, + recorder_mock, + units, + attributes, + display_unit, + statistics_unit, +): + """Test list_statistic_ids.""" + now = dt_util.utcnow() + + hass.config.units = units + await async_setup_component(hass, "sensor", {}) + await async_recorder_block_till_done(hass) + + client = await hass_ws_client() + await client.send_json({"id": 1, "type": "recorder/list_statistic_ids"}) + response = await client.receive_json() + assert response["success"] + assert response["result"] == [] + + hass.states.async_set("sensor.test", 10, attributes=attributes) + await async_wait_recording_done(hass) + + await client.send_json({"id": 2, "type": "recorder/list_statistic_ids"}) + response = await client.receive_json() + assert response["success"] + assert response["result"] == [ + { + "statistic_id": "sensor.test", + "has_mean": True, + "has_sum": False, + "name": None, + "source": "recorder", + "display_unit_of_measurement": display_unit, + "statistics_unit_of_measurement": statistics_unit, + } + ] + + do_adhoc_statistics(hass, start=now) + await async_recorder_block_till_done(hass) + # Remove the state, statistics will now be fetched from the database + hass.states.async_remove("sensor.test") + await hass.async_block_till_done() + + await client.send_json({"id": 3, "type": "recorder/list_statistic_ids"}) + response = await client.receive_json() + assert response["success"] + assert response["result"] == [ + { + "statistic_id": "sensor.test", + "has_mean": True, + "has_sum": False, + "name": None, + "source": "recorder", + "display_unit_of_measurement": display_unit, + "statistics_unit_of_measurement": statistics_unit, + } + ] + + await client.send_json( + {"id": 4, "type": "recorder/list_statistic_ids", "statistic_type": "dogs"} + ) + response = await client.receive_json() + assert not response["success"] + + await client.send_json( + {"id": 5, "type": "recorder/list_statistic_ids", "statistic_type": "mean"} + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == [ + { + "statistic_id": "sensor.test", + "has_mean": True, + "has_sum": False, + "name": None, + "source": "recorder", + "display_unit_of_measurement": display_unit, + "statistics_unit_of_measurement": statistics_unit, + } + ] + + await client.send_json( + {"id": 6, "type": "recorder/list_statistic_ids", "statistic_type": "sum"} + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == [] + + async def test_validate_statistics(hass, hass_ws_client, recorder_mock): """Test validate_statistics can be called.""" id = 1 @@ -83,7 +434,6 @@ async def test_clear_statistics(hass, hass_ws_client, recorder_mock): value = 10000 hass.config.units = units - await async_setup_component(hass, "history", {}) await async_setup_component(hass, "sensor", {}) await async_recorder_block_till_done(hass) hass.states.async_set("sensor.test1", state, attributes=attributes) @@ -98,7 +448,7 @@ async def test_clear_statistics(hass, hass_ws_client, recorder_mock): await client.send_json( { "id": 1, - "type": "history/statistics_during_period", + "type": "recorder/statistics_during_period", "start_time": now.isoformat(), "period": "5minute", } @@ -163,7 +513,7 @@ async def test_clear_statistics(hass, hass_ws_client, recorder_mock): await client.send_json( { "id": 3, - "type": "history/statistics_during_period", + "type": "recorder/statistics_during_period", "start_time": now.isoformat(), "period": "5minute", } @@ -187,7 +537,7 @@ async def test_clear_statistics(hass, hass_ws_client, recorder_mock): await client.send_json( { "id": 5, - "type": "history/statistics_during_period", + "type": "recorder/statistics_during_period", "start_time": now.isoformat(), "period": "5minute", } @@ -209,7 +559,6 @@ async def test_update_statistics_metadata( state = 10 hass.config.units = units - await async_setup_component(hass, "history", {}) await async_setup_component(hass, "sensor", {}) await async_recorder_block_till_done(hass) hass.states.async_set("sensor.test", state, attributes=attributes) @@ -220,7 +569,7 @@ async def test_update_statistics_metadata( client = await hass_ws_client() - await client.send_json({"id": 1, "type": "history/list_statistic_ids"}) + await client.send_json({"id": 1, "type": "recorder/list_statistic_ids"}) response = await client.receive_json() assert response["success"] assert response["result"] == [ @@ -247,7 +596,7 @@ async def test_update_statistics_metadata( assert response["success"] await async_recorder_block_till_done(hass) - await client.send_json({"id": 3, "type": "history/list_statistic_ids"}) + await client.send_json({"id": 3, "type": "recorder/list_statistic_ids"}) response = await client.receive_json() assert response["success"] assert response["result"] == [ @@ -457,7 +806,6 @@ async def test_get_statistics_metadata( now = dt_util.utcnow() hass.config.units = units - await async_setup_component(hass, "history", {"history": {}}) await async_setup_component(hass, "sensor", {}) await async_recorder_block_till_done(hass) From 823e7e8830118a8c500a0492c9cc8905bf5effb4 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 8 Sep 2022 22:35:53 +0200 Subject: [PATCH 210/955] Use new media player enums [i-l] (#78054) --- .../components/itunes/media_player.py | 51 +++++++------------ .../components/kaleidescape/media_player.py | 12 ++--- homeassistant/components/kef/media_player.py | 18 +++---- .../components/lg_netcast/media_player.py | 30 ++++------- .../components/lg_soundbar/media_player.py | 9 ++-- .../components/lookin/media_player.py | 11 ++-- 6 files changed, 51 insertions(+), 80 deletions(-) diff --git a/homeassistant/components/itunes/media_player.py b/homeassistant/components/itunes/media_player.py index ea2cad37c77..c9b0e4a07af 100644 --- a/homeassistant/components/itunes/media_player.py +++ b/homeassistant/components/itunes/media_player.py @@ -10,22 +10,10 @@ from homeassistant.components.media_player import ( PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, + MediaType, ) -from homeassistant.components.media_player.const import ( - MEDIA_TYPE_MUSIC, - MEDIA_TYPE_PLAYLIST, -) -from homeassistant.const import ( - CONF_HOST, - CONF_NAME, - CONF_PORT, - CONF_SSL, - STATE_IDLE, - STATE_OFF, - STATE_ON, - STATE_PAUSED, - STATE_PLAYING, -) +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, CONF_SSL from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -195,6 +183,7 @@ def setup_platform( class ItunesDevice(MediaPlayerEntity): """Representation of an iTunes API instance.""" + _attr_media_content_type = MediaType.MUSIC _attr_supported_features = ( MediaPlayerEntityFeature.PAUSE | MediaPlayerEntityFeature.VOLUME_SET @@ -263,12 +252,12 @@ class ItunesDevice(MediaPlayerEntity): return "error" if self.player_state == "stopped": - return STATE_IDLE + return MediaPlayerState.IDLE if self.player_state == "paused": - return STATE_PAUSED + return MediaPlayerState.PAUSED - return STATE_PLAYING + return MediaPlayerState.PLAYING def update(self) -> None: """Retrieve latest state.""" @@ -312,16 +301,16 @@ class ItunesDevice(MediaPlayerEntity): """Content ID of current playing media.""" return self.content_id - @property - def media_content_type(self): - """Content type of current playing media.""" - return MEDIA_TYPE_MUSIC - @property def media_image_url(self): """Image url of current playing media.""" if ( - self.player_state in (STATE_PLAYING, STATE_IDLE, STATE_PAUSED) + self.player_state + in { + MediaPlayerState.PLAYING, + MediaPlayerState.IDLE, + MediaPlayerState.PAUSED, + } and self.current_title is not None ): return f"{self.client.artwork_url()}?id={self.content_id}" @@ -393,7 +382,7 @@ class ItunesDevice(MediaPlayerEntity): def play_media(self, media_type: str, media_id: str, **kwargs: Any) -> None: """Send the play_media command to the media player.""" - if media_type == MEDIA_TYPE_PLAYLIST: + if media_type == MediaType.PLAYLIST: response = self.client.play_playlist(media_id) self.update_state(response) @@ -406,6 +395,7 @@ class ItunesDevice(MediaPlayerEntity): class AirPlayDevice(MediaPlayerEntity): """Representation an AirPlay device via an iTunes API instance.""" + _attr_media_content_type = MediaType.MUSIC _attr_supported_features = ( MediaPlayerEntityFeature.VOLUME_SET | MediaPlayerEntityFeature.TURN_ON @@ -466,12 +456,12 @@ class AirPlayDevice(MediaPlayerEntity): return "mdi:volume-off" @property - def state(self): + def state(self) -> MediaPlayerState: """Return the state of the device.""" if self.selected is True: - return STATE_ON + return MediaPlayerState.ON - return STATE_OFF + return MediaPlayerState.OFF def update(self) -> None: """Retrieve latest state.""" @@ -481,11 +471,6 @@ class AirPlayDevice(MediaPlayerEntity): """Return the volume.""" return float(self.volume) / 100.0 - @property - def media_content_type(self): - """Flag of media content that is supported.""" - return MEDIA_TYPE_MUSIC - def set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" volume = int(volume * 100) diff --git a/homeassistant/components/kaleidescape/media_player.py b/homeassistant/components/kaleidescape/media_player.py index da70643f8ee..cbae7f0df76 100644 --- a/homeassistant/components/kaleidescape/media_player.py +++ b/homeassistant/components/kaleidescape/media_player.py @@ -9,8 +9,8 @@ from kaleidescape import const as kaleidescape_const from homeassistant.components.media_player import ( MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, ) -from homeassistant.const import STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING from homeassistant.util.dt import utcnow from .const import DOMAIN as KALEIDESCAPE_DOMAIN @@ -86,15 +86,15 @@ class KaleidescapeMediaPlayer(KaleidescapeEntity, MediaPlayerEntity): await self._device.previous() @property - def state(self) -> str: + def state(self) -> MediaPlayerState: """State of device.""" if self._device.power.state == kaleidescape_const.DEVICE_POWER_STATE_STANDBY: - return STATE_OFF + return MediaPlayerState.OFF if self._device.movie.play_status in KALEIDESCAPE_PLAYING_STATES: - return STATE_PLAYING + return MediaPlayerState.PLAYING if self._device.movie.play_status in KALEIDESCAPE_PAUSED_STATES: - return STATE_PAUSED - return STATE_IDLE + return MediaPlayerState.PAUSED + return MediaPlayerState.IDLE @property def available(self) -> bool: diff --git a/homeassistant/components/kef/media_player.py b/homeassistant/components/kef/media_player.py index 0ad56f92725..a28819be406 100644 --- a/homeassistant/components/kef/media_player.py +++ b/homeassistant/components/kef/media_player.py @@ -15,15 +15,9 @@ from homeassistant.components.media_player import ( PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, ) -from homeassistant.const import ( - CONF_HOST, - CONF_NAME, - CONF_PORT, - CONF_TYPE, - STATE_OFF, - STATE_ON, -) +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, CONF_TYPE from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers import config_validation as cv, entity_platform @@ -265,7 +259,9 @@ class KefMediaPlayer(MediaPlayerEntity): ) = await self._speaker.get_volume_and_is_muted() state = await self._speaker.get_state() self._source = state.source - self._state = STATE_ON if state.is_on else STATE_OFF + self._state = ( + MediaPlayerState.ON if state.is_on else MediaPlayerState.OFF + ) if self._dsp is None: # Only do this when necessary because it is a slow operation await self.update_dsp() @@ -273,7 +269,7 @@ class KefMediaPlayer(MediaPlayerEntity): self._muted = None self._source = None self._volume = None - self._state = STATE_OFF + self._state = MediaPlayerState.OFF except (ConnectionError, TimeoutError) as err: _LOGGER.debug("Error in `update`: %s", err) self._state = None @@ -367,7 +363,7 @@ class KefMediaPlayer(MediaPlayerEntity): async def update_dsp(self, _=None) -> None: """Update the DSP settings.""" - if self._speaker_type == "LS50" and self._state == STATE_OFF: + if self._speaker_type == "LS50" and self._state == MediaPlayerState.OFF: # The LSX is able to respond when off the LS50 has to be on. return diff --git a/homeassistant/components/lg_netcast/media_player.py b/homeassistant/components/lg_netcast/media_player.py index 19046316803..922cb1de4a0 100644 --- a/homeassistant/components/lg_netcast/media_player.py +++ b/homeassistant/components/lg_netcast/media_player.py @@ -13,16 +13,10 @@ from homeassistant.components.media_player import ( MediaPlayerDeviceClass, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, + MediaType, ) -from homeassistant.components.media_player.const import MEDIA_TYPE_CHANNEL -from homeassistant.const import ( - CONF_ACCESS_TOKEN, - CONF_HOST, - CONF_NAME, - STATE_OFF, - STATE_PAUSED, - STATE_PLAYING, -) +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -81,6 +75,7 @@ class LgTVDevice(MediaPlayerEntity): """Representation of a LG TV.""" _attr_device_class = MediaPlayerDeviceClass.TV + _attr_media_content_type = MediaType.CHANNEL def __init__(self, client, name, on_action_script): """Initialize the LG TV device.""" @@ -105,14 +100,14 @@ class LgTVDevice(MediaPlayerEntity): with self._client as client: client.send_command(command) except (LgNetCastError, RequestException): - self._state = STATE_OFF + self._state = MediaPlayerState.OFF def update(self) -> None: """Retrieve the latest data from the LG TV.""" try: with self._client as client: - self._state = STATE_PLAYING + self._state = MediaPlayerState.PLAYING self.__update_volume() @@ -147,7 +142,7 @@ class LgTVDevice(MediaPlayerEntity): ) self._source_names = [n for n, k in sorted_sources] except (LgNetCastError, RequestException): - self._state = STATE_OFF + self._state = MediaPlayerState.OFF def __update_volume(self): volume_info = self._client.get_volume() @@ -191,11 +186,6 @@ class LgTVDevice(MediaPlayerEntity): """Content id of current playing media.""" return self._channel_id - @property - def media_content_type(self): - """Content type of current playing media.""" - return MEDIA_TYPE_CHANNEL - @property def media_channel(self): """Channel currently playing.""" @@ -259,13 +249,13 @@ class LgTVDevice(MediaPlayerEntity): def media_play(self) -> None: """Send play command.""" self._playing = True - self._state = STATE_PLAYING + self._state = MediaPlayerState.PLAYING self.send_command(33) def media_pause(self) -> None: """Send media pause command to media player.""" self._playing = False - self._state = STATE_PAUSED + self._state = MediaPlayerState.PAUSED self.send_command(34) def media_next_track(self) -> None: @@ -278,7 +268,7 @@ class LgTVDevice(MediaPlayerEntity): def play_media(self, media_type: str, media_id: str, **kwargs: Any) -> None: """Tune to channel.""" - if media_type != MEDIA_TYPE_CHANNEL: + if media_type != MediaType.CHANNEL: raise ValueError(f"Invalid media type: {media_type}") for name, channel in self._sources.items(): diff --git a/homeassistant/components/lg_soundbar/media_player.py b/homeassistant/components/lg_soundbar/media_player.py index 5ff5f63a544..c4491a1d257 100644 --- a/homeassistant/components/lg_soundbar/media_player.py +++ b/homeassistant/components/lg_soundbar/media_player.py @@ -6,9 +6,10 @@ import temescal from homeassistant.components.media_player import ( MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_PORT, STATE_ON +from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -34,6 +35,7 @@ class LGDevice(MediaPlayerEntity): """Representation of an LG soundbar device.""" _attr_should_poll = False + _attr_state = MediaPlayerState.ON _attr_supported_features = ( MediaPlayerEntityFeature.VOLUME_SET | MediaPlayerEntityFeature.VOLUME_MUTE @@ -145,11 +147,6 @@ class LGDevice(MediaPlayerEntity): """Boolean if volume is currently muted.""" return self._mute - @property - def state(self): - """Return the state of the device.""" - return STATE_ON - @property def sound_mode(self): """Return the current sound mode.""" diff --git a/homeassistant/components/lookin/media_player.py b/homeassistant/components/lookin/media_player.py index fa3a3622a31..f0e9c7e5928 100644 --- a/homeassistant/components/lookin/media_player.py +++ b/homeassistant/components/lookin/media_player.py @@ -9,9 +9,10 @@ from homeassistant.components.media_player import ( MediaPlayerDeviceClass, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import STATE_ON, STATE_STANDBY, Platform +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -112,13 +113,13 @@ class LookinMedia(LookinPowerPushRemoteEntity, MediaPlayerEntity): async def async_turn_off(self) -> None: """Turn the media player off.""" await self._async_send_command(self._power_off_command) - self._attr_state = STATE_STANDBY + self._attr_state = MediaPlayerState.STANDBY self.async_write_ha_state() async def async_turn_on(self) -> None: """Turn the media player on.""" await self._async_send_command(self._power_on_command) - self._attr_state = STATE_ON + self._attr_state = MediaPlayerState.ON self.async_write_ha_state() def _update_from_status(self, status: str) -> None: @@ -135,5 +136,7 @@ class LookinMedia(LookinPowerPushRemoteEntity, MediaPlayerEntity): state = status[0] mute = status[2] - self._attr_state = STATE_ON if state == "1" else STATE_STANDBY + self._attr_state = ( + MediaPlayerState.ON if state == "1" else MediaPlayerState.STANDBY + ) self._attr_is_volume_muted = mute == "0" From 52d2ebd2c8af4aaea95bc1310c658f9223a94fd4 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 8 Sep 2022 16:40:55 -0400 Subject: [PATCH 211/955] Show progress for zwave_js.update entity (#77905) Co-authored-by: Martin Hjelmare --- homeassistant/components/zwave_js/update.py | 68 ++++++++++++++++++--- 1 file changed, 59 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/zwave_js/update.py b/homeassistant/components/zwave_js/update.py index d106c7d6dd3..4f25d138aea 100644 --- a/homeassistant/components/zwave_js/update.py +++ b/homeassistant/components/zwave_js/update.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio from collections.abc import Callable from datetime import datetime, timedelta +from math import floor from typing import Any from awesomeversion import AwesomeVersion @@ -11,7 +12,7 @@ from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.const import NodeStatus from zwave_js_server.exceptions import BaseZwaveJSServerError, FailedZWaveCommand from zwave_js_server.model.driver import Driver -from zwave_js_server.model.firmware import FirmwareUpdateInfo +from zwave_js_server.model.firmware import FirmwareUpdateInfo, FirmwareUpdateProgress from zwave_js_server.model.node import Node as ZwaveNode from homeassistant.components.update import UpdateDeviceClass, UpdateEntity @@ -63,7 +64,9 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): _attr_entity_category = EntityCategory.CONFIG _attr_device_class = UpdateDeviceClass.FIRMWARE _attr_supported_features = ( - UpdateEntityFeature.INSTALL | UpdateEntityFeature.RELEASE_NOTES + UpdateEntityFeature.INSTALL + | UpdateEntityFeature.RELEASE_NOTES + | UpdateEntityFeature.PROGRESS ) _attr_has_entity_name = True _attr_should_poll = False @@ -78,6 +81,8 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): self._latest_version_firmware: FirmwareUpdateInfo | None = None self._status_unsub: Callable[[], None] | None = None self._poll_unsub: Callable[[], None] | None = None + self._progress_unsub: Callable[[], None] | None = None + self._num_files_installed: int = 0 # Entity class attributes self._attr_name = "Firmware" @@ -93,6 +98,36 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): self._status_unsub = None self.hass.async_create_task(self._async_update()) + @callback + def _update_progress(self, event: dict[str, Any]) -> None: + """Update install progress on event.""" + progress: FirmwareUpdateProgress = event["firmware_update_progress"] + if not self._latest_version_firmware: + return + # We will assume that each file in the firmware update represents an equal + # percentage of the overall progress. This is likely not true because each file + # may be a different size, but it's the best we can do since we don't know the + # total number of fragments across all files. + self._attr_in_progress = floor( + 100 + * ( + self._num_files_installed + + (progress.sent_fragments / progress.total_fragments) + ) + / len(self._latest_version_firmware.files) + ) + self.async_write_ha_state() + + @callback + def _reset_progress(self) -> None: + """Reset update install progress.""" + if self._progress_unsub: + self._progress_unsub() + self._progress_unsub = None + self._num_files_installed = 0 + self._attr_in_progress = False + self.async_write_ha_state() + async def _async_update(self, _: HomeAssistant | datetime | None = None) -> None: """Update the entity.""" self._poll_unsub = None @@ -152,18 +187,29 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): """Install an update.""" firmware = self._latest_version_firmware assert firmware - try: - for file in firmware.files: + self._attr_in_progress = 0 + self.async_write_ha_state() + self._progress_unsub = self.node.on( + "firmware update progress", self._update_progress + ) + for file in firmware.files: + try: await self.driver.controller.async_begin_ota_firmware_update( self.node, file ) - except BaseZwaveJSServerError as err: - raise HomeAssistantError(err) from err - else: - self._attr_installed_version = self._attr_latest_version = firmware.version - self._latest_version_firmware = None + except BaseZwaveJSServerError as err: + self._reset_progress() + raise HomeAssistantError(err) from err + self._num_files_installed += 1 + self._attr_in_progress = floor( + 100 * self._num_files_installed / len(firmware.files) + ) self.async_write_ha_state() + self._attr_installed_version = self._attr_latest_version = firmware.version + self._latest_version_firmware = None + self._reset_progress() + async def async_poll_value(self, _: bool) -> None: """Poll a value.""" LOGGER.error( @@ -208,3 +254,7 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): if self._poll_unsub: self._poll_unsub() self._poll_unsub = None + + if self._progress_unsub: + self._progress_unsub() + self._progress_unsub = None From 9fc9d50e077d17cd35822701a8c7b85efa80e49d Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 8 Sep 2022 14:41:09 -0600 Subject: [PATCH 212/955] Fix bug with 1st gen RainMachine controllers and unknown API calls (#78070) Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- homeassistant/components/rainmachine/__init__.py | 12 ++++++++++-- .../components/rainmachine/binary_sensor.py | 4 +++- homeassistant/components/rainmachine/manifest.json | 2 +- homeassistant/components/rainmachine/sensor.py | 12 +++++++----- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 23 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index 52de2e1c61a..756dc9b958d 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -9,7 +9,7 @@ from typing import Any from regenmaschine import Client from regenmaschine.controller import Controller -from regenmaschine.errors import RainMachineError +from regenmaschine.errors import RainMachineError, UnknownAPICallError import voluptuous as vol from homeassistant.config_entries import ConfigEntry, ConfigEntryState @@ -190,7 +190,9 @@ async def async_update_programs_and_zones( ) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry( # noqa: C901 + hass: HomeAssistant, entry: ConfigEntry +) -> bool: """Set up RainMachine as config entry.""" websession = aiohttp_client.async_get_clientsession(hass) client = Client(session=websession) @@ -244,6 +246,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: data = await controller.restrictions.universal() else: data = await controller.zones.all(details=True, include_inactive=True) + except UnknownAPICallError: + LOGGER.info( + "Skipping unsupported API call for controller %s: %s", + controller.name, + api_category, + ) except RainMachineError as err: raise UpdateFailed(err) from err diff --git a/homeassistant/components/rainmachine/binary_sensor.py b/homeassistant/components/rainmachine/binary_sensor.py index 3db64240788..48f11f598c9 100644 --- a/homeassistant/components/rainmachine/binary_sensor.py +++ b/homeassistant/components/rainmachine/binary_sensor.py @@ -175,7 +175,9 @@ class ProvisionSettingsBinarySensor(RainMachineEntity, BinarySensorEntity): def update_from_latest_data(self) -> None: """Update the state.""" if self.entity_description.key == TYPE_FLOW_SENSOR: - self._attr_is_on = self.coordinator.data["system"].get("useFlowSensor") + self._attr_is_on = self.coordinator.data.get("system", {}).get( + "useFlowSensor" + ) class UniversalRestrictionsBinarySensor(RainMachineEntity, BinarySensorEntity): diff --git a/homeassistant/components/rainmachine/manifest.json b/homeassistant/components/rainmachine/manifest.json index b183fc1b24f..a6cc86e5055 100644 --- a/homeassistant/components/rainmachine/manifest.json +++ b/homeassistant/components/rainmachine/manifest.json @@ -3,7 +3,7 @@ "name": "RainMachine", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/rainmachine", - "requirements": ["regenmaschine==2022.08.0"], + "requirements": ["regenmaschine==2022.09.0"], "codeowners": ["@bachya"], "iot_class": "local_polling", "homekit": { diff --git a/homeassistant/components/rainmachine/sensor.py b/homeassistant/components/rainmachine/sensor.py index e2e602b945b..32364e08199 100644 --- a/homeassistant/components/rainmachine/sensor.py +++ b/homeassistant/components/rainmachine/sensor.py @@ -273,12 +273,14 @@ class ProvisionSettingsSensor(RainMachineEntity, SensorEntity): def update_from_latest_data(self) -> None: """Update the state.""" if self.entity_description.key == TYPE_FLOW_SENSOR_CLICK_M3: - self._attr_native_value = self.coordinator.data["system"].get( + self._attr_native_value = self.coordinator.data.get("system", {}).get( "flowSensorClicksPerCubicMeter" ) elif self.entity_description.key == TYPE_FLOW_SENSOR_CONSUMED_LITERS: - clicks = self.coordinator.data["system"].get("flowSensorWateringClicks") - clicks_per_m3 = self.coordinator.data["system"].get( + clicks = self.coordinator.data.get("system", {}).get( + "flowSensorWateringClicks" + ) + clicks_per_m3 = self.coordinator.data.get("system", {}).get( "flowSensorClicksPerCubicMeter" ) @@ -287,11 +289,11 @@ class ProvisionSettingsSensor(RainMachineEntity, SensorEntity): else: self._attr_native_value = None elif self.entity_description.key == TYPE_FLOW_SENSOR_START_INDEX: - self._attr_native_value = self.coordinator.data["system"].get( + self._attr_native_value = self.coordinator.data.get("system", {}).get( "flowSensorStartIndex" ) elif self.entity_description.key == TYPE_FLOW_SENSOR_WATERING_CLICKS: - self._attr_native_value = self.coordinator.data["system"].get( + self._attr_native_value = self.coordinator.data.get("system", {}).get( "flowSensorWateringClicks" ) diff --git a/requirements_all.txt b/requirements_all.txt index 2947ea72798..3e8ebabbdcd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2121,7 +2121,7 @@ raincloudy==0.0.7 raspyrfm-client==1.2.8 # homeassistant.components.rainmachine -regenmaschine==2022.08.0 +regenmaschine==2022.09.0 # homeassistant.components.renault renault-api==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f08a496dc5c..d476445a4d6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1454,7 +1454,7 @@ radios==0.1.1 radiotherm==2.1.0 # homeassistant.components.rainmachine -regenmaschine==2022.08.0 +regenmaschine==2022.09.0 # homeassistant.components.renault renault-api==0.1.11 From 56c4e0391dd4696ee52b20cf2660da8c9cac480b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 8 Sep 2022 22:44:30 +0200 Subject: [PATCH 213/955] Use new media player enums [e-h] (#78049) --- .../components/enigma2/media_player.py | 18 +++----- .../components/epson/media_player.py | 16 +++---- .../components/fully_kiosk/media_player.py | 12 +++--- .../components/gstreamer/media_player.py | 17 +++----- .../harman_kardon_avr/media_player.py | 7 ++-- .../components/hdmi_cec/media_player.py | 30 ++++++------- homeassistant/components/heos/media_player.py | 34 ++++++--------- .../components/horizon/media_player.py | 42 ++++++++----------- 8 files changed, 72 insertions(+), 104 deletions(-) diff --git a/homeassistant/components/enigma2/media_player.py b/homeassistant/components/enigma2/media_player.py index aab3514b8e0..a479590f464 100644 --- a/homeassistant/components/enigma2/media_player.py +++ b/homeassistant/components/enigma2/media_player.py @@ -7,8 +7,9 @@ import voluptuous as vol from homeassistant.components.media_player import ( MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, + MediaType, ) -from homeassistant.components.media_player.const import MEDIA_TYPE_TVSHOW from homeassistant.const import ( CONF_HOST, CONF_NAME, @@ -16,9 +17,6 @@ from homeassistant.const import ( CONF_PORT, CONF_SSL, CONF_USERNAME, - STATE_OFF, - STATE_ON, - STATE_PLAYING, ) from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -104,6 +102,7 @@ def setup_platform( class Enigma2Device(MediaPlayerEntity): """Representation of an Enigma2 box.""" + _attr_media_content_type = MediaType.TVSHOW _attr_supported_features = ( MediaPlayerEntityFeature.VOLUME_SET | MediaPlayerEntityFeature.VOLUME_MUTE @@ -133,11 +132,11 @@ class Enigma2Device(MediaPlayerEntity): return self.e2_box.mac_address @property - def state(self): + def state(self) -> MediaPlayerState: """Return the state of the device.""" if self.e2_box.is_recording_playback: - return STATE_PLAYING - return STATE_OFF if self.e2_box.in_standby else STATE_ON + return MediaPlayerState.PLAYING + return MediaPlayerState.OFF if self.e2_box.in_standby else MediaPlayerState.ON @property def available(self) -> bool: @@ -172,11 +171,6 @@ class Enigma2Device(MediaPlayerEntity): """Service Ref of current playing media.""" return self.e2_box.current_service_ref - @property - def media_content_type(self): - """Type of video currently playing.""" - return MEDIA_TYPE_TVSHOW - @property def is_volume_muted(self): """Boolean if volume is currently muted.""" diff --git a/homeassistant/components/epson/media_player.py b/homeassistant/components/epson/media_player.py index 0e70984ac31..a0d1476aea7 100644 --- a/homeassistant/components/epson/media_player.py +++ b/homeassistant/components/epson/media_player.py @@ -31,9 +31,9 @@ import voluptuous as vol from homeassistant.components.media_player import ( MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv @@ -134,7 +134,7 @@ class EpsonProjectorMediaPlayer(MediaPlayerEntity): _LOGGER.debug("Projector status: %s", power_state) self._attr_available = True if power_state == EPSON_CODES[POWER]: - self._attr_state = STATE_ON + self._attr_state = MediaPlayerState.ON if await self.set_unique_id(): return self._attr_source_list = list(DEFAULT_SOURCES.values()) @@ -148,21 +148,21 @@ class EpsonProjectorMediaPlayer(MediaPlayerEntity): except ValueError: self._attr_volume_level = None elif power_state == BUSY: - self._attr_state = STATE_ON + self._attr_state = MediaPlayerState.ON else: - self._attr_state = STATE_OFF + self._attr_state = MediaPlayerState.OFF async def async_turn_on(self) -> None: """Turn on epson.""" - if self.state == STATE_OFF: + if self.state == MediaPlayerState.OFF: await self._projector.send_command(TURN_ON) - self._attr_state = STATE_ON + self._attr_state = MediaPlayerState.ON async def async_turn_off(self) -> None: """Turn off epson.""" - if self.state == STATE_ON: + if self.state == MediaPlayerState.ON: await self._projector.send_command(TURN_OFF) - self._attr_state = STATE_OFF + self._attr_state = MediaPlayerState.OFF async def select_cmode(self, cmode: str) -> None: """Set color mode in Epson.""" diff --git a/homeassistant/components/fully_kiosk/media_player.py b/homeassistant/components/fully_kiosk/media_player.py index 732f88170e1..ae6cf083ed1 100644 --- a/homeassistant/components/fully_kiosk/media_player.py +++ b/homeassistant/components/fully_kiosk/media_player.py @@ -4,13 +4,13 @@ from __future__ import annotations from typing import Any from homeassistant.components import media_source -from homeassistant.components.media_player import MediaPlayerEntity -from homeassistant.components.media_player.browse_media import ( +from homeassistant.components.media_player import ( BrowseMedia, + MediaPlayerEntity, + MediaPlayerState, async_process_play_media_url, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import STATE_IDLE, STATE_PLAYING from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -34,12 +34,12 @@ class FullyMediaPlayer(FullyKioskEntity, MediaPlayerEntity): _attr_supported_features = MEDIA_SUPPORT_FULLYKIOSK _attr_assumed_state = True - _attr_state = STATE_IDLE def __init__(self, coordinator: FullyKioskDataUpdateCoordinator) -> None: """Initialize the media player entity.""" super().__init__(coordinator) self._attr_unique_id = f"{coordinator.data['deviceID']}-mediaplayer" + self._attr_state = MediaPlayerState.IDLE async def async_play_media( self, media_type: str, media_id: str, **kwargs: Any @@ -52,13 +52,13 @@ class FullyMediaPlayer(FullyKioskEntity, MediaPlayerEntity): media_id = async_process_play_media_url(self.hass, play_item.url) await self.coordinator.fully.playSound(media_id, AUDIOMANAGER_STREAM_MUSIC) - self._attr_state = STATE_PLAYING + self._attr_state = MediaPlayerState.PLAYING self.async_write_ha_state() async def async_media_stop(self) -> None: """Stop playing media.""" await self.coordinator.fully.stopSound() - self._attr_state = STATE_IDLE + self._attr_state = MediaPlayerState.IDLE self.async_write_ha_state() async def async_set_volume_level(self, volume: float) -> None: diff --git a/homeassistant/components/gstreamer/media_player.py b/homeassistant/components/gstreamer/media_player.py index 87329bdbc66..2861dc4516b 100644 --- a/homeassistant/components/gstreamer/media_player.py +++ b/homeassistant/components/gstreamer/media_player.py @@ -13,12 +13,11 @@ from homeassistant.components.media_player import ( BrowseMedia, MediaPlayerEntity, MediaPlayerEntityFeature, -) -from homeassistant.components.media_player.browse_media import ( + MediaPlayerState, + MediaType, async_process_play_media_url, ) -from homeassistant.components.media_player.const import MEDIA_TYPE_MUSIC -from homeassistant.const import CONF_NAME, EVENT_HOMEASSISTANT_STOP, STATE_IDLE +from homeassistant.const import CONF_NAME, EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -58,6 +57,7 @@ def setup_platform( class GstreamerDevice(MediaPlayerEntity): """Representation of a Gstreamer device.""" + _attr_media_content_type = MediaType.MUSIC _attr_supported_features = ( MediaPlayerEntityFeature.VOLUME_SET | MediaPlayerEntityFeature.PLAY @@ -71,7 +71,7 @@ class GstreamerDevice(MediaPlayerEntity): """Initialize the Gstreamer device.""" self._player = player self._name = name or DOMAIN - self._state = STATE_IDLE + self._state = MediaPlayerState.IDLE self._volume = None self._duration = None self._uri = None @@ -104,7 +104,7 @@ class GstreamerDevice(MediaPlayerEntity): ) media_id = sourced_media.url - elif media_type != MEDIA_TYPE_MUSIC: + elif media_type != MediaType.MUSIC: _LOGGER.error("Invalid media type") return @@ -129,11 +129,6 @@ class GstreamerDevice(MediaPlayerEntity): """Content ID of currently playing media.""" return self._uri - @property - def content_type(self): - """Content type of currently playing media.""" - return MEDIA_TYPE_MUSIC - @property def name(self): """Return the name of the device.""" diff --git a/homeassistant/components/harman_kardon_avr/media_player.py b/homeassistant/components/harman_kardon_avr/media_player.py index c6272626f94..f222d4bd739 100644 --- a/homeassistant/components/harman_kardon_avr/media_player.py +++ b/homeassistant/components/harman_kardon_avr/media_player.py @@ -8,8 +8,9 @@ from homeassistant.components.media_player import ( PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, ) -from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, STATE_OFF, STATE_ON +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -72,9 +73,9 @@ class HkAvrDevice(MediaPlayerEntity): def update(self) -> None: """Update the state of this media_player.""" if self._avr.is_on(): - self._state = STATE_ON + self._state = MediaPlayerState.ON elif self._avr.is_off(): - self._state = STATE_OFF + self._state = MediaPlayerState.OFF else: self._state = None diff --git a/homeassistant/components/hdmi_cec/media_player.py b/homeassistant/components/hdmi_cec/media_player.py index e3ec00749c1..203ce85a6b2 100644 --- a/homeassistant/components/hdmi_cec/media_player.py +++ b/homeassistant/components/hdmi_cec/media_player.py @@ -26,16 +26,10 @@ from pycec.const import ( ) from homeassistant.components.media_player import ( + DOMAIN as MP_DOMAIN, MediaPlayerEntity, MediaPlayerEntityFeature, -) -from homeassistant.components.media_player.const import DOMAIN as MP_DOMAIN -from homeassistant.const import ( - STATE_IDLE, - STATE_OFF, - STATE_ON, - STATE_PAUSED, - STATE_PLAYING, + MediaPlayerState, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -95,7 +89,7 @@ class CecPlayerEntity(CecEntity, MediaPlayerEntity): def turn_on(self) -> None: """Turn device on.""" self._device.turn_on() - self._state = STATE_ON + self._state = MediaPlayerState.ON def clear_playlist(self) -> None: """Clear players playlist.""" @@ -104,12 +98,12 @@ class CecPlayerEntity(CecEntity, MediaPlayerEntity): def turn_off(self) -> None: """Turn device off.""" self._device.turn_off() - self._state = STATE_OFF + self._state = MediaPlayerState.OFF def media_stop(self) -> None: """Stop playback.""" self.send_keypress(KEY_STOP) - self._state = STATE_IDLE + self._state = MediaPlayerState.IDLE def play_media(self, media_type: str, media_id: str, **kwargs: Any) -> None: """Not supported.""" @@ -130,7 +124,7 @@ class CecPlayerEntity(CecEntity, MediaPlayerEntity): def media_pause(self) -> None: """Pause playback.""" self.send_keypress(KEY_PAUSE) - self._state = STATE_PAUSED + self._state = MediaPlayerState.PAUSED def select_source(self, source: str) -> None: """Not supported.""" @@ -139,7 +133,7 @@ class CecPlayerEntity(CecEntity, MediaPlayerEntity): def media_play(self) -> None: """Start playback.""" self.send_keypress(KEY_PLAY) - self._state = STATE_PLAYING + self._state = MediaPlayerState.PLAYING def volume_up(self) -> None: """Increase volume.""" @@ -160,16 +154,16 @@ class CecPlayerEntity(CecEntity, MediaPlayerEntity): """Update device status.""" device = self._device if device.power_status in [POWER_OFF, 3]: - self._state = STATE_OFF + self._state = MediaPlayerState.OFF elif not self.support_pause: if device.power_status in [POWER_ON, 4]: - self._state = STATE_ON + self._state = MediaPlayerState.ON elif device.status == STATUS_PLAY: - self._state = STATE_PLAYING + self._state = MediaPlayerState.PLAYING elif device.status == STATUS_STOP: - self._state = STATE_IDLE + self._state = MediaPlayerState.IDLE elif device.status == STATUS_STILL: - self._state = STATE_PAUSED + self._state = MediaPlayerState.PAUSED else: _LOGGER.warning("Unknown state: %s", device.status) diff --git a/homeassistant/components/heos/media_player.py b/homeassistant/components/heos/media_player.py index b72e3ec73c1..0a5bcdc4d0a 100644 --- a/homeassistant/components/heos/media_player.py +++ b/homeassistant/components/heos/media_player.py @@ -12,23 +12,17 @@ from typing_extensions import ParamSpec from homeassistant.components import media_source from homeassistant.components.media_player import ( + ATTR_MEDIA_ENQUEUE, + DOMAIN, + BrowseMedia, MediaPlayerEnqueue, MediaPlayerEntity, MediaPlayerEntityFeature, -) -from homeassistant.components.media_player.browse_media import ( - BrowseMedia, + MediaPlayerState, + MediaType, async_process_play_media_url, ) -from homeassistant.components.media_player.const import ( - ATTR_MEDIA_ENQUEUE, - DOMAIN, - MEDIA_TYPE_MUSIC, - MEDIA_TYPE_PLAYLIST, - MEDIA_TYPE_URL, -) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import STATE_IDLE, STATE_PAUSED, STATE_PLAYING from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, @@ -62,9 +56,9 @@ BASE_SUPPORTED_FEATURES = ( ) PLAY_STATE_TO_STATE = { - heos_const.PLAY_STATE_PLAY: STATE_PLAYING, - heos_const.PLAY_STATE_STOP: STATE_IDLE, - heos_const.PLAY_STATE_PAUSE: STATE_PAUSED, + heos_const.PLAY_STATE_PLAY: MediaPlayerState.PLAYING, + heos_const.PLAY_STATE_STOP: MediaPlayerState.IDLE, + heos_const.PLAY_STATE_PAUSE: MediaPlayerState.PAUSED, } CONTROL_TO_SUPPORT = { @@ -118,6 +112,7 @@ def log_command_error( class HeosMediaPlayer(MediaPlayerEntity): """The HEOS player.""" + _attr_media_content_type = MediaType.MUSIC _attr_should_poll = False def __init__(self, player): @@ -205,13 +200,13 @@ class HeosMediaPlayer(MediaPlayerEntity): ) -> None: """Play a piece of media.""" if media_source.is_media_source_id(media_id): - media_type = MEDIA_TYPE_URL + media_type = MediaType.URL play_item = await media_source.async_resolve_media( self.hass, media_id, self.entity_id ) media_id = play_item.url - if media_type in (MEDIA_TYPE_URL, MEDIA_TYPE_MUSIC): + if media_type in {MediaType.URL, MediaType.MUSIC}: media_id = async_process_play_media_url(self.hass, media_id) await self._player.play_url(media_id) @@ -233,7 +228,7 @@ class HeosMediaPlayer(MediaPlayerEntity): await self._player.play_quick_select(index) return - if media_type == MEDIA_TYPE_PLAYLIST: + if media_type == MediaType.PLAYLIST: playlists = await self._player.heos.get_playlists() playlist = next((p for p in playlists if p.name == media_id), None) if not playlist: @@ -356,11 +351,6 @@ class HeosMediaPlayer(MediaPlayerEntity): """Content ID of current playing media.""" return self._player.now_playing_media.media_id - @property - def media_content_type(self) -> str: - """Content type of current playing media.""" - return MEDIA_TYPE_MUSIC - @property def media_duration(self): """Duration of current playing media in seconds.""" diff --git a/homeassistant/components/horizon/media_player.py b/homeassistant/components/horizon/media_player.py index a19199eb5b3..c75d47d06eb 100644 --- a/homeassistant/components/horizon/media_player.py +++ b/homeassistant/components/horizon/media_player.py @@ -14,16 +14,10 @@ from homeassistant.components.media_player import ( PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, + MediaType, ) -from homeassistant.components.media_player.const import MEDIA_TYPE_CHANNEL -from homeassistant.const import ( - CONF_HOST, - CONF_NAME, - CONF_PORT, - STATE_OFF, - STATE_PAUSED, - STATE_PLAYING, -) +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv @@ -110,63 +104,63 @@ class HorizonDevice(MediaPlayerEntity): """Update State using the media server running on the Horizon.""" try: if self._client.is_powered_on(): - self._state = STATE_PLAYING + self._state = MediaPlayerState.PLAYING else: - self._state = STATE_OFF + self._state = MediaPlayerState.OFF except OSError: - self._state = STATE_OFF + self._state = MediaPlayerState.OFF def turn_on(self) -> None: """Turn the device on.""" - if self._state == STATE_OFF: + if self._state == MediaPlayerState.OFF: self._send_key(self._keys.POWER) def turn_off(self) -> None: """Turn the device off.""" - if self._state != STATE_OFF: + if self._state != MediaPlayerState.OFF: self._send_key(self._keys.POWER) def media_previous_track(self) -> None: """Channel down.""" self._send_key(self._keys.CHAN_DOWN) - self._state = STATE_PLAYING + self._state = MediaPlayerState.PLAYING def media_next_track(self) -> None: """Channel up.""" self._send_key(self._keys.CHAN_UP) - self._state = STATE_PLAYING + self._state = MediaPlayerState.PLAYING def media_play(self) -> None: """Send play command.""" self._send_key(self._keys.PAUSE) - self._state = STATE_PLAYING + self._state = MediaPlayerState.PLAYING def media_pause(self) -> None: """Send pause command.""" self._send_key(self._keys.PAUSE) - self._state = STATE_PAUSED + self._state = MediaPlayerState.PAUSED def media_play_pause(self) -> None: """Send play/pause command.""" self._send_key(self._keys.PAUSE) - if self._state == STATE_PAUSED: - self._state = STATE_PLAYING + if self._state == MediaPlayerState.PAUSED: + self._state = MediaPlayerState.PLAYING else: - self._state = STATE_PAUSED + self._state = MediaPlayerState.PAUSED def play_media(self, media_type: str, media_id: str, **kwargs: Any) -> None: """Play media / switch to channel.""" - if MEDIA_TYPE_CHANNEL == media_type: + if MediaType.CHANNEL == media_type: try: self._select_channel(int(media_id)) - self._state = STATE_PLAYING + self._state = MediaPlayerState.PLAYING except ValueError: _LOGGER.error("Invalid channel: %s", media_id) else: _LOGGER.error( "Invalid media type %s. Supported type: %s", media_type, - MEDIA_TYPE_CHANNEL, + MediaType.CHANNEL, ) def _select_channel(self, channel): From 2f8af92735b938929016cd451eb2ad464cd2f501 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 8 Sep 2022 22:47:59 +0200 Subject: [PATCH 214/955] Use new media player enums [m-o] (#78057) --- .../components/monoprice/media_player.py | 5 +- homeassistant/components/mpd/media_player.py | 63 +++++++------------ homeassistant/components/nad/media_player.py | 20 +++--- .../components/onkyo/media_player.py | 19 +++--- .../components/openhome/media_player.py | 26 ++++---- 5 files changed, 53 insertions(+), 80 deletions(-) diff --git a/homeassistant/components/monoprice/media_player.py b/homeassistant/components/monoprice/media_player.py index 19692b43854..2f4a4a33f49 100644 --- a/homeassistant/components/monoprice/media_player.py +++ b/homeassistant/components/monoprice/media_player.py @@ -7,9 +7,10 @@ from homeassistant import core from homeassistant.components.media_player import ( MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_PORT, STATE_OFF, STATE_ON +from homeassistant.const import CONF_PORT from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform, service from homeassistant.helpers.entity import DeviceInfo @@ -155,7 +156,7 @@ class MonopriceZone(MediaPlayerEntity): self._update_success = False return - self._state = STATE_ON if state.power else STATE_OFF + self._state = MediaPlayerState.ON if state.power else MediaPlayerState.OFF self._volume = state.volume self._mute = state.mute idx = state.source diff --git a/homeassistant/components/mpd/media_player.py b/homeassistant/components/mpd/media_player.py index 4680870c3ec..44d94bc649f 100644 --- a/homeassistant/components/mpd/media_player.py +++ b/homeassistant/components/mpd/media_player.py @@ -15,29 +15,15 @@ import voluptuous as vol from homeassistant.components import media_source from homeassistant.components.media_player import ( PLATFORM_SCHEMA, + BrowseMedia, MediaPlayerEntity, MediaPlayerEntityFeature, -) -from homeassistant.components.media_player.browse_media import ( - BrowseMedia, + MediaPlayerState, + MediaType, + RepeatMode, async_process_play_media_url, ) -from homeassistant.components.media_player.const import ( - MEDIA_TYPE_MUSIC, - MEDIA_TYPE_PLAYLIST, - REPEAT_MODE_ALL, - REPEAT_MODE_OFF, - REPEAT_MODE_ONE, -) -from homeassistant.const import ( - CONF_HOST, - CONF_NAME, - CONF_PASSWORD, - CONF_PORT, - STATE_OFF, - STATE_PAUSED, - STATE_PLAYING, -) +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -97,6 +83,8 @@ async def async_setup_platform( class MpdDevice(MediaPlayerEntity): """Representation of a MPD server.""" + _attr_media_content_type = MediaType.MUSIC + # pylint: disable=no-member def __init__(self, server, port, password, name): """Initialize the MPD device.""" @@ -185,18 +173,18 @@ class MpdDevice(MediaPlayerEntity): return self._name @property - def state(self): + def state(self) -> MediaPlayerState: """Return the media state.""" if self._status is None: - return STATE_OFF + return MediaPlayerState.OFF if self._status["state"] == "play": - return STATE_PLAYING + return MediaPlayerState.PLAYING if self._status["state"] == "pause": - return STATE_PAUSED + return MediaPlayerState.PAUSED if self._status["state"] == "stop": - return STATE_OFF + return MediaPlayerState.OFF - return STATE_OFF + return MediaPlayerState.OFF @property def is_volume_muted(self): @@ -208,11 +196,6 @@ class MpdDevice(MediaPlayerEntity): """Return the content ID of current playing media.""" return self._currentsong.get("file") - @property - def media_content_type(self): - """Return the content type of current playing media.""" - return MEDIA_TYPE_MUSIC - @property def media_duration(self): """Return the duration of current playing media in seconds.""" @@ -384,7 +367,7 @@ class MpdDevice(MediaPlayerEntity): async def async_select_source(self, source: str) -> None: """Choose a different available playlist and play it.""" - await self.async_play_media(MEDIA_TYPE_PLAYLIST, source) + await self.async_play_media(MediaType.PLAYLIST, source) @Throttle(PLAYLIST_UPDATE_INTERVAL) async def _update_playlists(self, **kwargs: Any) -> None: @@ -456,13 +439,13 @@ class MpdDevice(MediaPlayerEntity): ) -> None: """Send the media player the command for playing a playlist.""" if media_source.is_media_source_id(media_id): - media_type = MEDIA_TYPE_MUSIC + media_type = MediaType.MUSIC play_item = await media_source.async_resolve_media( self.hass, media_id, self.entity_id ) media_id = async_process_play_media_url(self.hass, play_item.url) - if media_type == MEDIA_TYPE_PLAYLIST: + if media_type == MediaType.PLAYLIST: _LOGGER.debug("Playing playlist: %s", media_id) if media_id in self._playlists: self._currentplaylist = media_id @@ -479,22 +462,22 @@ class MpdDevice(MediaPlayerEntity): await self._client.play() @property - def repeat(self): + def repeat(self) -> RepeatMode: """Return current repeat mode.""" if self._status["repeat"] == "1": if self._status["single"] == "1": - return REPEAT_MODE_ONE - return REPEAT_MODE_ALL - return REPEAT_MODE_OFF + return RepeatMode.ONE + return RepeatMode.ALL + return RepeatMode.OFF - async def async_set_repeat(self, repeat: str) -> None: + async def async_set_repeat(self, repeat: RepeatMode) -> None: """Set repeat mode.""" - if repeat == REPEAT_MODE_OFF: + if repeat == RepeatMode.OFF: await self._client.repeat(0) await self._client.single(0) else: await self._client.repeat(1) - if repeat == REPEAT_MODE_ONE: + if repeat == RepeatMode.ONE: await self._client.single(1) else: await self._client.single(0) diff --git a/homeassistant/components/nad/media_player.py b/homeassistant/components/nad/media_player.py index 6304109c325..531dabfde70 100644 --- a/homeassistant/components/nad/media_player.py +++ b/homeassistant/components/nad/media_player.py @@ -8,15 +8,9 @@ from homeassistant.components.media_player import ( PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, ) -from homeassistant.const import ( - CONF_HOST, - CONF_NAME, - CONF_PORT, - CONF_TYPE, - STATE_OFF, - STATE_ON, -) +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, CONF_TYPE from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -186,10 +180,12 @@ class NAD(MediaPlayerEntity): self._state = None return self._state = ( - STATE_ON if self._nad_receiver.main_power("?") == "On" else STATE_OFF + MediaPlayerState.ON + if self._nad_receiver.main_power("?") == "On" + else MediaPlayerState.OFF ) - if self._state == STATE_ON: + if self._state == MediaPlayerState.ON: self._mute = self._nad_receiver.main_mute("?") == "On" volume = self._nad_receiver.main_volume("?") # Some receivers cannot report the volume, e.g. C 356BEE, @@ -312,9 +308,9 @@ class NADtcp(MediaPlayerEntity): # Update on/off state if nad_status["power"]: - self._state = STATE_ON + self._state = MediaPlayerState.ON else: - self._state = STATE_OFF + self._state = MediaPlayerState.OFF # Update current volume self._volume = self.nad_vol_to_internal_vol(nad_status["volume"]) diff --git a/homeassistant/components/onkyo/media_player.py b/homeassistant/components/onkyo/media_player.py index 82f662e1bad..5465bee9ecf 100644 --- a/homeassistant/components/onkyo/media_player.py +++ b/homeassistant/components/onkyo/media_player.py @@ -12,15 +12,10 @@ from homeassistant.components.media_player import ( PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, ) from homeassistant.components.media_player.const import DOMAIN -from homeassistant.const import ( - ATTR_ENTITY_ID, - CONF_HOST, - CONF_NAME, - STATE_OFF, - STATE_ON, -) +from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant, ServiceCall import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -259,7 +254,7 @@ class OnkyoDevice(MediaPlayerEntity): self._receiver = receiver self._muted = False self._volume = 0 - self._pwstate = STATE_OFF + self._pwstate = MediaPlayerState.OFF if name: # not discovered self._name = name @@ -303,9 +298,9 @@ class OnkyoDevice(MediaPlayerEntity): if not status: return if status[1] == "on": - self._pwstate = STATE_ON + self._pwstate = MediaPlayerState.ON else: - self._pwstate = STATE_OFF + self._pwstate = MediaPlayerState.OFF self._attributes.pop(ATTR_AUDIO_INFORMATION, None) self._attributes.pop(ATTR_VIDEO_INFORMATION, None) self._attributes.pop(ATTR_PRESET, None) @@ -514,9 +509,9 @@ class OnkyoDeviceZone(OnkyoDevice): if not status: return if status[1] == "on": - self._pwstate = STATE_ON + self._pwstate = MediaPlayerState.ON else: - self._pwstate = STATE_OFF + self._pwstate = MediaPlayerState.OFF return volume_raw = self.command(f"zone{self._zone}.volume=query") diff --git a/homeassistant/components/openhome/media_player.py b/homeassistant/components/openhome/media_player.py index d528bb8dad4..f352d7101ac 100644 --- a/homeassistant/components/openhome/media_player.py +++ b/homeassistant/components/openhome/media_player.py @@ -15,15 +15,13 @@ import voluptuous as vol from homeassistant.components import media_source from homeassistant.components.media_player import ( + BrowseMedia, MediaPlayerEntity, MediaPlayerEntityFeature, -) -from homeassistant.components.media_player.browse_media import ( - BrowseMedia, + MediaPlayerState, + MediaType, async_process_play_media_url, ) -from homeassistant.components.media_player.const import MEDIA_TYPE_MUSIC -from homeassistant.const import STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -126,7 +124,7 @@ class OpenhomeDevice(MediaPlayerEntity): self._source_index = {} self._source = {} self._name = None - self._state = STATE_PLAYING + self._state = MediaPlayerState.PLAYING self._available = True @property @@ -180,16 +178,16 @@ class OpenhomeDevice(MediaPlayerEntity): ) if self._in_standby: - self._state = STATE_OFF + self._state = MediaPlayerState.OFF elif self._transport_state == "Paused": - self._state = STATE_PAUSED + self._state = MediaPlayerState.PAUSED elif self._transport_state in ("Playing", "Buffering"): - self._state = STATE_PLAYING + self._state = MediaPlayerState.PLAYING elif self._transport_state == "Stopped": - self._state = STATE_IDLE + self._state = MediaPlayerState.IDLE else: # Device is playing an external source with no transport controls - self._state = STATE_PLAYING + self._state = MediaPlayerState.PLAYING self._available = True except (asyncio.TimeoutError, aiohttp.ClientError, UpnpError): @@ -211,17 +209,17 @@ class OpenhomeDevice(MediaPlayerEntity): ) -> None: """Send the play_media command to the media player.""" if media_source.is_media_source_id(media_id): - media_type = MEDIA_TYPE_MUSIC + media_type = MediaType.MUSIC play_item = await media_source.async_resolve_media( self.hass, media_id, self.entity_id ) media_id = play_item.url - if media_type != MEDIA_TYPE_MUSIC: + if media_type != MediaType.MUSIC: _LOGGER.error( "Invalid media type %s. Only %s is supported", media_type, - MEDIA_TYPE_MUSIC, + MediaType.MUSIC, ) return From 9c192dea9c66cc66bfbef2b20baa28fa0d8a418f Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Thu, 8 Sep 2022 22:49:49 +0200 Subject: [PATCH 215/955] Allow OpenWeatherMap config flow to test using old API to pass (#78074) Co-authored-by: Paulus Schoutsen --- homeassistant/components/openweathermap/config_flow.py | 2 +- tests/components/openweathermap/test_config_flow.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/openweathermap/config_flow.py b/homeassistant/components/openweathermap/config_flow.py index 612965bdb2f..c418231946f 100644 --- a/homeassistant/components/openweathermap/config_flow.py +++ b/homeassistant/components/openweathermap/config_flow.py @@ -130,4 +130,4 @@ class OpenWeatherMapOptionsFlow(config_entries.OptionsFlow): async def _is_owm_api_online(hass, api_key, lat, lon): owm = OWM(api_key).weather_manager() - return await hass.async_add_executor_job(owm.one_call, lat, lon) + return await hass.async_add_executor_job(owm.weather_at_coords, lat, lon) diff --git a/tests/components/openweathermap/test_config_flow.py b/tests/components/openweathermap/test_config_flow.py index 12ee849d3d2..40931dc2ce2 100644 --- a/tests/components/openweathermap/test_config_flow.py +++ b/tests/components/openweathermap/test_config_flow.py @@ -208,6 +208,8 @@ def _create_mocked_owm(is_api_online: bool): mocked_owm.one_call.return_value = one_call - mocked_owm.weather_manager.return_value.one_call.return_value = is_api_online + mocked_owm.weather_manager.return_value.weather_at_coords.return_value = ( + is_api_online + ) return mocked_owm From 52b5e1779f1ed6e5005dc0bdff4137040d7216fb Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 8 Sep 2022 22:54:43 +0200 Subject: [PATCH 216/955] Use new media player enums [p] (#78058) --- .../panasonic_bluray/media_player.py | 25 ++--- .../panasonic_viera/media_player.py | 10 +- .../components/pandora/media_player.py | 33 +++---- .../components/philips_js/media_player.py | 91 +++++++++---------- .../components/pioneer/media_player.py | 18 ++-- .../components/pjlink/media_player.py | 20 ++-- homeassistant/components/plex/media_player.py | 22 ++--- homeassistant/components/ps4/media_player.py | 29 +++--- 8 files changed, 105 insertions(+), 143 deletions(-) diff --git a/homeassistant/components/panasonic_bluray/media_player.py b/homeassistant/components/panasonic_bluray/media_player.py index 20e3bf3b901..54062b36b1b 100644 --- a/homeassistant/components/panasonic_bluray/media_player.py +++ b/homeassistant/components/panasonic_bluray/media_player.py @@ -10,14 +10,9 @@ from homeassistant.components.media_player import ( PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, ) -from homeassistant.const import ( - CONF_HOST, - CONF_NAME, - STATE_IDLE, - STATE_OFF, - STATE_PLAYING, -) +from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -65,7 +60,7 @@ class PanasonicBluRay(MediaPlayerEntity): """Initialize the Panasonic Blue-ray device.""" self._device = PanasonicBD(ip) self._name = name - self._state = STATE_OFF + self._state = MediaPlayerState.OFF self._position = 0 self._duration = 0 self._position_valid = 0 @@ -111,11 +106,11 @@ class PanasonicBluRay(MediaPlayerEntity): # We map both of these to off. If it's really off we can't # turn it on, but from standby we can go to idle by pressing # POWER. - self._state = STATE_OFF + self._state = MediaPlayerState.OFF elif state[0] in ["paused", "stopped"]: - self._state = STATE_IDLE + self._state = MediaPlayerState.IDLE elif state[0] == "playing": - self._state = STATE_PLAYING + self._state = MediaPlayerState.PLAYING # Update our current media position + length if state[1] >= 0: @@ -134,17 +129,17 @@ class PanasonicBluRay(MediaPlayerEntity): our favour as it means the device is still accepting commands and we can thus turn it back on when desired. """ - if self._state != STATE_OFF: + if self._state != MediaPlayerState.OFF: self._device.send_key("POWER") - self._state = STATE_OFF + self._state = MediaPlayerState.OFF def turn_on(self) -> None: """Wake the device back up from standby.""" - if self._state == STATE_OFF: + if self._state == MediaPlayerState.OFF: self._device.send_key("POWER") - self._state = STATE_IDLE + self._state = MediaPlayerState.IDLE def media_play(self) -> None: """Send play command.""" diff --git a/homeassistant/components/panasonic_viera/media_player.py b/homeassistant/components/panasonic_viera/media_player.py index 4de18e5afe9..14c440f0ec1 100644 --- a/homeassistant/components/panasonic_viera/media_player.py +++ b/homeassistant/components/panasonic_viera/media_player.py @@ -8,15 +8,13 @@ from panasonic_viera import Keys from homeassistant.components import media_source from homeassistant.components.media_player import ( + BrowseMedia, MediaPlayerDeviceClass, MediaPlayerEntity, MediaPlayerEntityFeature, -) -from homeassistant.components.media_player.browse_media import ( - BrowseMedia, + MediaType, async_process_play_media_url, ) -from homeassistant.components.media_player.const import MEDIA_TYPE_URL from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant @@ -191,13 +189,13 @@ class PanasonicVieraTVEntity(MediaPlayerEntity): ) -> None: """Play media.""" if media_source.is_media_source_id(media_id): - media_type = MEDIA_TYPE_URL + media_type = MediaType.URL play_item = await media_source.async_resolve_media( self.hass, media_id, self.entity_id ) media_id = play_item.url - if media_type != MEDIA_TYPE_URL: + if media_type != MediaType.URL: _LOGGER.warning("Unsupported media_type: %s", media_type) return diff --git a/homeassistant/components/pandora/media_player.py b/homeassistant/components/pandora/media_player.py index 6c2b9379a3c..c45c04a330d 100644 --- a/homeassistant/components/pandora/media_player.py +++ b/homeassistant/components/pandora/media_player.py @@ -14,8 +14,9 @@ from homeassistant import util from homeassistant.components.media_player import ( MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, + MediaType, ) -from homeassistant.components.media_player.const import MEDIA_TYPE_MUSIC from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, SERVICE_MEDIA_NEXT_TRACK, @@ -23,10 +24,6 @@ from homeassistant.const import ( SERVICE_MEDIA_PLAY_PAUSE, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_UP, - STATE_IDLE, - STATE_OFF, - STATE_PAUSED, - STATE_PLAYING, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -70,6 +67,7 @@ def setup_platform( class PandoraMediaPlayer(MediaPlayerEntity): """A media player that uses the Pianobar interface to Pandora.""" + _attr_media_content_type = MediaType.MUSIC # MediaPlayerEntityFeature.VOLUME_SET is close to available but we need volume up/down # controls in the GUI. _attr_supported_features = ( @@ -84,7 +82,7 @@ class PandoraMediaPlayer(MediaPlayerEntity): def __init__(self, name): """Initialize the Pandora device.""" self._name = name - self._player_state = STATE_OFF + self._player_state = MediaPlayerState.OFF self._station = "" self._media_title = "" self._media_artist = "" @@ -106,7 +104,7 @@ class PandoraMediaPlayer(MediaPlayerEntity): def turn_on(self) -> None: """Turn the media player on.""" - if self._player_state != STATE_OFF: + if self._player_state != MediaPlayerState.OFF: return self._pianobar = pexpect.spawn("pianobar") _LOGGER.info("Started pianobar subprocess") @@ -131,7 +129,7 @@ class PandoraMediaPlayer(MediaPlayerEntity): self._update_stations() self.update_playing_status() - self._player_state = STATE_IDLE + self._player_state = MediaPlayerState.IDLE self.schedule_update_ha_state() def turn_off(self) -> None: @@ -148,19 +146,19 @@ class PandoraMediaPlayer(MediaPlayerEntity): os.killpg(os.getpgid(self._pianobar.pid), signal.SIGTERM) _LOGGER.debug("Killed Pianobar subprocess") self._pianobar = None - self._player_state = STATE_OFF + self._player_state = MediaPlayerState.OFF self.schedule_update_ha_state() def media_play(self) -> None: """Send play command.""" self._send_pianobar_command(SERVICE_MEDIA_PLAY_PAUSE) - self._player_state = STATE_PLAYING + self._player_state = MediaPlayerState.PLAYING self.schedule_update_ha_state() def media_pause(self) -> None: """Send pause command.""" self._send_pianobar_command(SERVICE_MEDIA_PLAY_PAUSE) - self._player_state = STATE_PAUSED + self._player_state = MediaPlayerState.PAUSED self.schedule_update_ha_state() def media_next_track(self) -> None: @@ -184,11 +182,6 @@ class PandoraMediaPlayer(MediaPlayerEntity): self.update_playing_status() return self._media_title - @property - def media_content_type(self): - """Content type of current playing media.""" - return MEDIA_TYPE_MUSIC - @property def media_artist(self): """Artist of current playing media, music track only.""" @@ -215,7 +208,7 @@ class PandoraMediaPlayer(MediaPlayerEntity): self._send_station_list_command() self._pianobar.sendline(f"{station_index}") self._pianobar.expect("\r\n") - self._player_state = STATE_PLAYING + self._player_state = MediaPlayerState.PLAYING def _send_station_list_command(self): """Send a station list command.""" @@ -312,9 +305,9 @@ class PandoraMediaPlayer(MediaPlayerEntity): self._media_duration = int(total_minutes) * 60 + int(total_seconds) if time_remaining not in (self._time_remaining, self._media_duration): - self._player_state = STATE_PLAYING - elif self._player_state == STATE_PLAYING: - self._player_state = STATE_PAUSED + self._player_state = MediaPlayerState.PLAYING + elif self._player_state == MediaPlayerState.PLAYING: + self._player_state = MediaPlayerState.PAUSED self._time_remaining = time_remaining def _log_match(self): diff --git a/homeassistant/components/philips_js/media_player.py b/homeassistant/components/philips_js/media_player.py index 8ef6fc8d4bd..116833d8a97 100644 --- a/homeassistant/components/philips_js/media_player.py +++ b/homeassistant/components/philips_js/media_player.py @@ -6,23 +6,16 @@ from typing import Any from haphilipsjs import ConnectionFailure from homeassistant.components.media_player import ( + BrowseError, BrowseMedia, + MediaClass, MediaPlayerDeviceClass, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, + MediaType, ) -from homeassistant.components.media_player.const import ( - MEDIA_CLASS_APP, - MEDIA_CLASS_CHANNEL, - MEDIA_CLASS_DIRECTORY, - MEDIA_TYPE_APP, - MEDIA_TYPE_APPS, - MEDIA_TYPE_CHANNEL, - MEDIA_TYPE_CHANNELS, -) -from homeassistant.components.media_player.errors import BrowseError from homeassistant.config_entries import ConfigEntry -from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -96,7 +89,7 @@ class PhilipsTVMediaPlayer( sw_version=coordinator.system.get("softwareversion"), name=coordinator.system["name"], ) - self._state = STATE_OFF + self._state = MediaPlayerState.OFF self._media_content_type: str | None = None self._media_content_id: str | None = None self._media_title: str | None = None @@ -121,11 +114,11 @@ class PhilipsTVMediaPlayer( return supports @property - def state(self): + def state(self) -> MediaPlayerState: """Get the device state. An exception means OFF state.""" if self._tv.on and (self._tv.powerstate == "On" or self._tv.powerstate is None): - return STATE_ON - return STATE_OFF + return MediaPlayerState.ON + return MediaPlayerState.OFF @property def source(self): @@ -157,16 +150,16 @@ class PhilipsTVMediaPlayer( """Turn on the device.""" if self._tv.on and self._tv.powerstate: await self._tv.setPowerState("On") - self._state = STATE_ON + self._state = MediaPlayerState.ON else: await self.coordinator.turn_on.async_run(self.hass, self._context) await self._async_update_soon() async def async_turn_off(self) -> None: """Turn off the device.""" - if self._state == STATE_ON: + if self._state == MediaPlayerState.ON: await self._tv.sendKey("Standby") - self._state = STATE_OFF + self._state = MediaPlayerState.OFF await self._async_update_soon() else: _LOGGER.debug("Ignoring turn off when already in expected state") @@ -251,8 +244,8 @@ class PhilipsTVMediaPlayer( def media_image_url(self): """Image url of current playing media.""" if self._media_content_id and self._media_content_type in ( - MEDIA_TYPE_APP, - MEDIA_TYPE_CHANNEL, + MediaType.APP, + MediaType.CHANNEL, ): return self.get_browse_image_url( self._media_content_type, self._media_content_id, media_image_id=None @@ -276,14 +269,14 @@ class PhilipsTVMediaPlayer( """Play a piece of media.""" _LOGGER.debug("Call play media type <%s>, Id <%s>", media_type, media_id) - if media_type == MEDIA_TYPE_CHANNEL: + if media_type == MediaType.CHANNEL: list_id, _, channel_id = media_id.partition("/") if channel_id: await self._tv.setChannel(channel_id, list_id) await self._async_update_soon() else: _LOGGER.error("Unable to find channel <%s>", media_id) - elif media_type == MEDIA_TYPE_APP: + elif media_type == MediaType.APP: if app := self._tv.applications.get(media_id): await self._tv.setApplication(app["intent"]) await self._async_update_soon() @@ -298,9 +291,9 @@ class PhilipsTVMediaPlayer( children = [ BrowseMedia( title=channel.get("name", f"Channel: {channel_id}"), - media_class=MEDIA_CLASS_CHANNEL, + media_class=MediaClass.CHANNEL, media_content_id=f"alltv/{channel_id}", - media_content_type=MEDIA_TYPE_CHANNEL, + media_content_type=MediaType.CHANNEL, can_play=True, can_expand=False, ) @@ -311,10 +304,10 @@ class PhilipsTVMediaPlayer( return BrowseMedia( title="Channels", - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_id="channels", - media_content_type=MEDIA_TYPE_CHANNELS, - children_media_class=MEDIA_CLASS_CHANNEL, + media_content_type=MediaType.CHANNELS, + children_media_class=MediaClass.CHANNEL, can_play=False, can_expand=True, children=children, @@ -335,9 +328,9 @@ class PhilipsTVMediaPlayer( children = [ BrowseMedia( title=get_name(channel), - media_class=MEDIA_CLASS_CHANNEL, + media_class=MediaClass.CHANNEL, media_content_id=f"{list_id}/{channel['ccid']}", - media_content_type=MEDIA_TYPE_CHANNEL, + media_content_type=MediaType.CHANNEL, can_play=True, can_expand=False, ) @@ -351,10 +344,10 @@ class PhilipsTVMediaPlayer( favorite = self._tv.favorite_lists[list_id] return BrowseMedia( title=favorite.get("name", f"Favorites {list_id}"), - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_id=f"favorites/{list_id}", - media_content_type=MEDIA_TYPE_CHANNELS, - children_media_class=MEDIA_CLASS_CHANNEL, + media_content_type=MediaType.CHANNELS, + children_media_class=MediaClass.CHANNEL, can_play=False, can_expand=True, children=children, @@ -366,13 +359,13 @@ class PhilipsTVMediaPlayer( children = [ BrowseMedia( title=application["label"], - media_class=MEDIA_CLASS_APP, + media_class=MediaClass.APP, media_content_id=application_id, - media_content_type=MEDIA_TYPE_APP, + media_content_type=MediaType.APP, can_play=True, can_expand=False, thumbnail=self.get_browse_image_url( - MEDIA_TYPE_APP, application_id, media_image_id=None + MediaType.APP, application_id, media_image_id=None ), ) for application_id, application in self._tv.applications.items() @@ -382,10 +375,10 @@ class PhilipsTVMediaPlayer( return BrowseMedia( title="Applications", - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_id="applications", - media_content_type=MEDIA_TYPE_APPS, - children_media_class=MEDIA_CLASS_APP, + media_content_type=MediaType.APPS, + children_media_class=MediaClass.APP, can_play=False, can_expand=True, children=children, @@ -403,10 +396,10 @@ class PhilipsTVMediaPlayer( return BrowseMedia( title="Favorites", - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_id="favorite_lists", - media_content_type=MEDIA_TYPE_CHANNELS, - children_media_class=MEDIA_CLASS_CHANNEL, + media_content_type=MediaType.CHANNELS, + children_media_class=MediaClass.CHANNEL, can_play=False, can_expand=True, children=children, @@ -417,7 +410,7 @@ class PhilipsTVMediaPlayer( return BrowseMedia( title="Philips TV", - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_id="", media_content_type="", can_play=False, @@ -456,9 +449,9 @@ class PhilipsTVMediaPlayer( ) -> tuple[bytes | None, str | None]: """Serve album art. Returns (content, content_type).""" try: - if media_content_type == MEDIA_TYPE_APP and media_content_id: + if media_content_type == MediaType.APP and media_content_id: return await self._tv.getApplicationIcon(media_content_id) - if media_content_type == MEDIA_TYPE_CHANNEL and media_content_id: + if media_content_type == MediaType.CHANNEL and media_content_id: return await self._tv.getChannelLogo(media_content_id) except ConnectionFailure: _LOGGER.warning("Failed to fetch image") @@ -475,11 +468,11 @@ class PhilipsTVMediaPlayer( if self._tv.on: if self._tv.powerstate in ("Standby", "StandbyKeep"): - self._state = STATE_OFF + self._state = MediaPlayerState.OFF else: - self._state = STATE_ON + self._state = MediaPlayerState.ON else: - self._state = STATE_OFF + self._state = MediaPlayerState.OFF self._sources = { srcid: source.get("name") or f"Source {srcid}" @@ -487,14 +480,14 @@ class PhilipsTVMediaPlayer( } if self._tv.channel_active: - self._media_content_type = MEDIA_TYPE_CHANNEL + self._media_content_type = MediaType.CHANNEL self._media_content_id = f"all/{self._tv.channel_id}" self._media_title = self._tv.channels.get(self._tv.channel_id, {}).get( "name" ) self._media_channel = self._media_title elif self._tv.application_id: - self._media_content_type = MEDIA_TYPE_APP + self._media_content_type = MediaType.APP self._media_content_id = self._tv.application_id self._media_title = self._tv.applications.get( self._tv.application_id, {} diff --git a/homeassistant/components/pioneer/media_player.py b/homeassistant/components/pioneer/media_player.py index 1836d433cf7..620e314fdd8 100644 --- a/homeassistant/components/pioneer/media_player.py +++ b/homeassistant/components/pioneer/media_player.py @@ -10,15 +10,9 @@ from homeassistant.components.media_player import ( PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, ) -from homeassistant.const import ( - CONF_HOST, - CONF_NAME, - CONF_PORT, - CONF_TIMEOUT, - STATE_OFF, - STATE_ON, -) +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, CONF_TIMEOUT from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -174,14 +168,14 @@ class PioneerDevice(MediaPlayerEntity): return self._name @property - def state(self): + def state(self) -> MediaPlayerState | None: """Return the state of the device.""" if self._pwstate == "PWR2": - return STATE_OFF + return MediaPlayerState.OFF if self._pwstate == "PWR1": - return STATE_OFF + return MediaPlayerState.OFF if self._pwstate == "PWR0": - return STATE_ON + return MediaPlayerState.ON return None diff --git a/homeassistant/components/pjlink/media_player.py b/homeassistant/components/pjlink/media_player.py index 1f5641486ef..0e1151e3dc4 100644 --- a/homeassistant/components/pjlink/media_player.py +++ b/homeassistant/components/pjlink/media_player.py @@ -9,15 +9,9 @@ from homeassistant.components.media_player import ( PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, ) -from homeassistant.const import ( - CONF_HOST, - CONF_NAME, - CONF_PASSWORD, - CONF_PORT, - STATE_OFF, - STATE_ON, -) +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -89,7 +83,7 @@ class PjLinkDevice(MediaPlayerEntity): self._password = password self._encoding = encoding self._muted = False - self._pwstate = STATE_OFF + self._pwstate = MediaPlayerState.OFF self._current_source = None with self.projector() as projector: if not self._name: @@ -114,23 +108,23 @@ class PjLinkDevice(MediaPlayerEntity): try: pwstate = projector.get_power() if pwstate in ("on", "warm-up"): - self._pwstate = STATE_ON + self._pwstate = MediaPlayerState.ON self._muted = projector.get_mute()[1] self._current_source = format_input_source(*projector.get_input()) else: - self._pwstate = STATE_OFF + self._pwstate = MediaPlayerState.OFF self._muted = False self._current_source = None except KeyError as err: if str(err) == "'OK'": - self._pwstate = STATE_OFF + self._pwstate = MediaPlayerState.OFF self._muted = False self._current_source = None else: raise except ProjectorError as err: if str(err) == "unavailable time": - self._pwstate = STATE_OFF + self._pwstate = MediaPlayerState.OFF self._muted = False self._current_source = None else: diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index 9cab1d25fc2..ffb3cab251d 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -12,13 +12,13 @@ from typing_extensions import Concatenate, ParamSpec from homeassistant.components.media_player import ( DOMAIN as MP_DOMAIN, + BrowseMedia, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, + MediaType, ) -from homeassistant.components.media_player.browse_media import BrowseMedia -from homeassistant.components.media_player.const import MEDIA_TYPE_MUSIC from homeassistant.config_entries import ConfigEntry -from homeassistant.const import STATE_IDLE, STATE_PAUSED, STATE_PLAYING from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er @@ -140,7 +140,7 @@ class PlexMediaPlayer(MediaPlayerEntity): self._attr_available = False self._attr_should_poll = False - self._attr_state = STATE_IDLE + self._attr_state = MediaPlayerState.IDLE self._attr_unique_id = ( f"{self.plex_server.machine_identifier}:{self.machine_identifier}" ) @@ -229,7 +229,7 @@ class PlexMediaPlayer(MediaPlayerEntity): def force_idle(self): """Force client to idle.""" - self._attr_state = STATE_IDLE + self._attr_state = MediaPlayerState.IDLE if self.player_source == "session": self.device = None self.session_device = None @@ -247,7 +247,7 @@ class PlexMediaPlayer(MediaPlayerEntity): self.session_device = self.session.player self.update_state(self.session.state) else: - self._attr_state = STATE_IDLE + self._attr_state = MediaPlayerState.IDLE @property # type: ignore[misc] @needs_session @@ -258,24 +258,24 @@ class PlexMediaPlayer(MediaPlayerEntity): def update_state(self, state): """Set the state of the device, handle session termination.""" if state == "playing": - self._attr_state = STATE_PLAYING + self._attr_state = MediaPlayerState.PLAYING elif state == "paused": - self._attr_state = STATE_PAUSED + self._attr_state = MediaPlayerState.PAUSED elif state == "stopped": self.session = None self.force_idle() else: - self._attr_state = STATE_IDLE + self._attr_state = MediaPlayerState.IDLE @property def _is_player_active(self): """Report if the client is playing media.""" - return self.state in (STATE_PLAYING, STATE_PAUSED) + return self.state in {MediaPlayerState.PLAYING, MediaPlayerState.PAUSED} @property def _active_media_plexapi_type(self): """Get the active media type required by PlexAPI commands.""" - if self.media_content_type is MEDIA_TYPE_MUSIC: + if self.media_content_type == MediaType.MUSIC: return "music" return "video" diff --git a/homeassistant/components/ps4/media_player.py b/homeassistant/components/ps4/media_player.py index 29bd0732029..5202825c85f 100644 --- a/homeassistant/components/ps4/media_player.py +++ b/homeassistant/components/ps4/media_player.py @@ -8,14 +8,12 @@ from pyps4_2ndscreen.media_art import TYPE_APP as PS_TYPE_APP import pyps4_2ndscreen.ps4 as pyps4 from homeassistant.components.media_player import ( - MediaPlayerEntity, - MediaPlayerEntityFeature, -) -from homeassistant.components.media_player.const import ( ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_TITLE, - MEDIA_TYPE_APP, - MEDIA_TYPE_GAME, + MediaPlayerEntity, + MediaPlayerEntityFeature, + MediaPlayerState, + MediaType, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -24,9 +22,6 @@ from homeassistant.const import ( CONF_NAME, CONF_REGION, CONF_TOKEN, - STATE_IDLE, - STATE_PLAYING, - STATE_STANDBY, ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry, entity_registry @@ -177,7 +172,7 @@ class PS4Device(MediaPlayerEntity): name = status.get("running-app-name") if title_id and name is not None: - self._state = STATE_PLAYING + self._state = MediaPlayerState.PLAYING if self._media_content_id != title_id: self._media_content_id = title_id @@ -191,10 +186,10 @@ class PS4Device(MediaPlayerEntity): # Get data from PS Store. asyncio.ensure_future(self.async_get_title_data(title_id, name)) else: - if self._state != STATE_IDLE: + if self._state != MediaPlayerState.IDLE: self.idle() else: - if self._state != STATE_STANDBY: + if self._state != MediaPlayerState.STANDBY: self.state_standby() elif self._retry > DEFAULT_RETRIES: @@ -219,12 +214,12 @@ class PS4Device(MediaPlayerEntity): def idle(self): """Set states for state idle.""" self.reset_title() - self._state = STATE_IDLE + self._state = MediaPlayerState.IDLE def state_standby(self): """Set states for state standby.""" self.reset_title() - self._state = STATE_STANDBY + self._state = MediaPlayerState.STANDBY def state_unknown(self): """Set states for state unknown.""" @@ -265,9 +260,9 @@ class PS4Device(MediaPlayerEntity): art = title.cover_art # Assume media type is game if not app. if title.game_type != PS_TYPE_APP: - media_type = MEDIA_TYPE_GAME + media_type = MediaType.GAME else: - media_type = MEDIA_TYPE_APP + media_type = MediaType.APP else: _LOGGER.error( "Could not find data in region: %s for PS ID: %s", @@ -382,7 +377,7 @@ class PS4Device(MediaPlayerEntity): def entity_picture(self): """Return picture.""" if ( - self._state == STATE_PLAYING + self._state == MediaPlayerState.PLAYING and self._media_content_id is not None and (image_hash := self.media_image_hash) is not None ): From 45d0ec7150acc4caa97f9754058a3f33a93868b6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 8 Sep 2022 22:59:52 +0200 Subject: [PATCH 217/955] Use new media player enums [r] (#78062) --- homeassistant/components/roku/media_player.py | 88 ++++++++----------- homeassistant/components/roon/media_player.py | 29 +++--- .../components/russound_rio/media_player.py | 15 ++-- .../components/russound_rnet/media_player.py | 7 +- 4 files changed, 59 insertions(+), 80 deletions(-) diff --git a/homeassistant/components/roku/media_player.py b/homeassistant/components/roku/media_player.py index d9866a3d77a..4df38ca874a 100644 --- a/homeassistant/components/roku/media_player.py +++ b/homeassistant/components/roku/media_player.py @@ -12,30 +12,18 @@ import yarl from homeassistant.components import media_source from homeassistant.components.media_player import ( + ATTR_MEDIA_EXTRA, BrowseMedia, MediaPlayerDeviceClass, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, + MediaType, async_process_play_media_url, ) -from homeassistant.components.media_player.const import ( - ATTR_MEDIA_EXTRA, - MEDIA_TYPE_APP, - MEDIA_TYPE_CHANNEL, - MEDIA_TYPE_MUSIC, - MEDIA_TYPE_URL, - MEDIA_TYPE_VIDEO, -) from homeassistant.components.stream.const import FORMAT_CONTENT_TYPE, HLS_PROVIDER from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_NAME, - STATE_IDLE, - STATE_ON, - STATE_PAUSED, - STATE_PLAYING, - STATE_STANDBY, -) +from homeassistant.const import ATTR_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -59,16 +47,16 @@ _LOGGER = logging.getLogger(__name__) STREAM_FORMAT_TO_MEDIA_TYPE = { - "dash": MEDIA_TYPE_VIDEO, - "hls": MEDIA_TYPE_VIDEO, - "ism": MEDIA_TYPE_VIDEO, - "m4a": MEDIA_TYPE_MUSIC, - "m4v": MEDIA_TYPE_VIDEO, - "mka": MEDIA_TYPE_MUSIC, - "mkv": MEDIA_TYPE_VIDEO, - "mks": MEDIA_TYPE_VIDEO, - "mp3": MEDIA_TYPE_MUSIC, - "mp4": MEDIA_TYPE_VIDEO, + "dash": MediaType.VIDEO, + "hls": MediaType.VIDEO, + "ism": MediaType.VIDEO, + "m4a": MediaType.MUSIC, + "m4v": MediaType.VIDEO, + "mka": MediaType.MUSIC, + "mkv": MediaType.VIDEO, + "mks": MediaType.VIDEO, + "mp3": MediaType.MUSIC, + "mp4": MediaType.VIDEO, } ATTRS_TO_LAUNCH_PARAMS = { @@ -150,10 +138,10 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): return MediaPlayerDeviceClass.RECEIVER @property - def state(self) -> str | None: + def state(self) -> MediaPlayerState | None: """Return the state of the device.""" if self.coordinator.data.state.standby: - return STATE_STANDBY + return MediaPlayerState.STANDBY if self.coordinator.data.app is None: return None @@ -163,28 +151,28 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): or self.coordinator.data.app.name == "Roku" or self.coordinator.data.app.screensaver ): - return STATE_IDLE + return MediaPlayerState.IDLE if self.coordinator.data.media: if self.coordinator.data.media.paused: - return STATE_PAUSED - return STATE_PLAYING + return MediaPlayerState.PAUSED + return MediaPlayerState.PLAYING if self.coordinator.data.app.name: - return STATE_ON + return MediaPlayerState.ON return None @property - def media_content_type(self) -> str | None: + def media_content_type(self) -> MediaType | None: """Content type of current playing media.""" if self.app_id is None or self.app_name in ("Power Saver", "Roku"): return None if self.app_id == "tvinput.dtv" and self.coordinator.data.channel is not None: - return MEDIA_TYPE_CHANNEL + return MediaType.CHANNEL - return MEDIA_TYPE_APP + return MediaType.APP @property def media_image_url(self) -> str | None: @@ -282,7 +270,7 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): media_image_id: str | None = None, ) -> tuple[bytes | None, str | None]: """Fetch media browser image to serve via proxy.""" - if media_content_type == MEDIA_TYPE_APP and media_content_id: + if media_content_type == MediaType.APP and media_content_id: image_url = self.coordinator.roku.app_icon_url(media_content_id) return await self._async_fetch_image(image_url) @@ -317,21 +305,21 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): @roku_exception_handler() async def async_media_pause(self) -> None: """Send pause command.""" - if self.state not in (STATE_STANDBY, STATE_PAUSED): + if self.state not in {MediaPlayerState.STANDBY, MediaPlayerState.PAUSED}: await self.coordinator.roku.remote("play") await self.coordinator.async_request_refresh() @roku_exception_handler() async def async_media_play(self) -> None: """Send play command.""" - if self.state not in (STATE_STANDBY, STATE_PLAYING): + if self.state not in {MediaPlayerState.STANDBY, MediaPlayerState.PLAYING}: await self.coordinator.roku.remote("play") await self.coordinator.async_request_refresh() @roku_exception_handler() async def async_media_play_pause(self) -> None: """Send play/pause command.""" - if self.state != STATE_STANDBY: + if self.state != MediaPlayerState.STANDBY: await self.coordinator.roku.remote("play") await self.coordinator.async_request_refresh() @@ -380,19 +368,19 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): sourced_media = await media_source.async_resolve_media( self.hass, media_id, self.entity_id ) - media_type = MEDIA_TYPE_URL + media_type = MediaType.URL media_id = sourced_media.url mime_type = sourced_media.mime_type stream_name = original_media_id stream_format = guess_stream_format(media_id, mime_type) if media_type == FORMAT_CONTENT_TYPE[HLS_PROVIDER]: - media_type = MEDIA_TYPE_VIDEO + media_type = MediaType.VIDEO mime_type = FORMAT_CONTENT_TYPE[HLS_PROVIDER] stream_name = "Camera Stream" stream_format = "hls" - if media_type in (MEDIA_TYPE_MUSIC, MEDIA_TYPE_URL, MEDIA_TYPE_VIDEO): + if media_type in {MediaType.MUSIC, MediaType.URL, MediaType.VIDEO}: # If media ID is a relative URL, we serve it from HA. media_id = async_process_play_media_url(self.hass, media_id) @@ -417,12 +405,12 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): return if ( - media_type == MEDIA_TYPE_URL - and STREAM_FORMAT_TO_MEDIA_TYPE[extra[ATTR_FORMAT]] == MEDIA_TYPE_MUSIC + media_type == MediaType.URL + and STREAM_FORMAT_TO_MEDIA_TYPE[extra[ATTR_FORMAT]] == MediaType.MUSIC ): - media_type = MEDIA_TYPE_MUSIC + media_type = MediaType.MUSIC - if media_type == MEDIA_TYPE_MUSIC and "tts_proxy" in media_id: + if media_type == MediaType.MUSIC and "tts_proxy" in media_id: stream_name = "Text to Speech" elif stream_name is None: if stream_format == "ism": @@ -433,7 +421,7 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): if extra.get(ATTR_NAME) is None: extra[ATTR_NAME] = stream_name - if media_type == MEDIA_TYPE_APP: + if media_type == MediaType.APP: params = { param: extra[attr] for attr, param in ATTRS_TO_LAUNCH_PARAMS.items() @@ -441,9 +429,9 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): } await self.coordinator.roku.launch(media_id, params) - elif media_type == MEDIA_TYPE_CHANNEL: + elif media_type == MediaType.CHANNEL: await self.coordinator.roku.tune(media_id) - elif media_type == MEDIA_TYPE_MUSIC: + elif media_type == MediaType.MUSIC: if extra.get(ATTR_ARTIST_NAME) is None: extra[ATTR_ARTIST_NAME] = "Home Assistant" @@ -456,7 +444,7 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): params = {"t": "a", **params} await self.coordinator.roku.play_on_roku(media_id, params) - elif media_type in (MEDIA_TYPE_URL, MEDIA_TYPE_VIDEO): + elif media_type in {MediaType.URL, MediaType.VIDEO}: params = { param: extra[attr] for (attr, param) in ATTRS_TO_PLAY_ON_ROKU_PARAMS.items() diff --git a/homeassistant/components/roon/media_player.py b/homeassistant/components/roon/media_player.py index 58737b30423..c80f92834bb 100644 --- a/homeassistant/components/roon/media_player.py +++ b/homeassistant/components/roon/media_player.py @@ -8,18 +8,13 @@ from roonapi import split_media_path import voluptuous as vol from homeassistant.components.media_player import ( + BrowseMedia, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, ) -from homeassistant.components.media_player.browse_media import BrowseMedia from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - DEVICE_DEFAULT_NAME, - STATE_IDLE, - STATE_OFF, - STATE_PAUSED, - STATE_PLAYING, -) +from homeassistant.const import DEVICE_DEFAULT_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.dispatcher import ( @@ -106,7 +101,7 @@ class RoonDevice(MediaPlayerEntity): self._available = True self._last_position_update = None self._supports_standby = False - self._state = STATE_IDLE + self._state = MediaPlayerState.IDLE self._unique_id = None self._zone_id = None self._output_id = None @@ -172,12 +167,12 @@ class RoonDevice(MediaPlayerEntity): if not self.player_data["is_available"]: # this player was removed self._available = False - self._state = STATE_OFF + self._state = MediaPlayerState.OFF else: self._available = True # determine player state self.update_state() - if self.state == STATE_PLAYING: + if self.state == MediaPlayerState.PLAYING: self._last_position_update = utcnow() @classmethod @@ -254,20 +249,20 @@ class RoonDevice(MediaPlayerEntity): if source["supports_standby"] and source["status"] != "indeterminate": self._supports_standby = True if source["status"] in ["standby", "deselected"]: - new_state = STATE_OFF + new_state = MediaPlayerState.OFF break # determine player state if not new_state: if self.player_data["state"] == "playing": - new_state = STATE_PLAYING + new_state = MediaPlayerState.PLAYING elif self.player_data["state"] == "loading": - new_state = STATE_PLAYING + new_state = MediaPlayerState.PLAYING elif self.player_data["state"] == "stopped": - new_state = STATE_IDLE + new_state = MediaPlayerState.IDLE elif self.player_data["state"] == "paused": - new_state = STATE_PAUSED + new_state = MediaPlayerState.PAUSED else: - new_state = STATE_IDLE + new_state = MediaPlayerState.IDLE self._state = new_state self._unique_id = self.player_data["dev_id"] self._zone_id = self.player_data["zone_id"] diff --git a/homeassistant/components/russound_rio/media_player.py b/homeassistant/components/russound_rio/media_player.py index afd96289594..c639e5ddc90 100644 --- a/homeassistant/components/russound_rio/media_player.py +++ b/homeassistant/components/russound_rio/media_player.py @@ -8,15 +8,14 @@ from homeassistant.components.media_player import ( PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, + MediaType, ) -from homeassistant.components.media_player.const import MEDIA_TYPE_MUSIC from homeassistant.const import ( CONF_HOST, CONF_NAME, CONF_PORT, EVENT_HOMEASSISTANT_STOP, - STATE_OFF, - STATE_ON, ) from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv @@ -70,6 +69,7 @@ async def async_setup_platform( class RussoundZoneDevice(MediaPlayerEntity): """Representation of a Russound Zone.""" + _attr_media_content_type = MediaType.MUSIC _attr_should_poll = False _attr_supported_features = ( MediaPlayerEntityFeature.VOLUME_MUTE @@ -130,9 +130,9 @@ class RussoundZoneDevice(MediaPlayerEntity): """Return the state of the device.""" status = self._zone_var("status", "OFF") if status == "ON": - return STATE_ON + return MediaPlayerState.ON if status == "OFF": - return STATE_OFF + return MediaPlayerState.OFF @property def source(self): @@ -144,11 +144,6 @@ class RussoundZoneDevice(MediaPlayerEntity): """Return a list of available input sources.""" return [x[1] for x in self._sources] - @property - def media_content_type(self): - """Content type of current playing media.""" - return MEDIA_TYPE_MUSIC - @property def media_title(self): """Title of current playing media.""" diff --git a/homeassistant/components/russound_rnet/media_player.py b/homeassistant/components/russound_rnet/media_player.py index 6e1074c9837..6782d783a83 100644 --- a/homeassistant/components/russound_rnet/media_player.py +++ b/homeassistant/components/russound_rnet/media_player.py @@ -10,8 +10,9 @@ from homeassistant.components.media_player import ( PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, ) -from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, STATE_OFF, STATE_ON +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -100,9 +101,9 @@ class RussoundRNETDevice(MediaPlayerEntity): if ret is not None: _LOGGER.debug("Updating status for zone %s", self._zone_id) if ret[0] == 0: - self._state = STATE_OFF + self._state = MediaPlayerState.OFF else: - self._state = STATE_ON + self._state = MediaPlayerState.ON self._volume = ret[2] * 2 / 100.0 # Returns 0 based index for source. index = ret[1] From 6b157921ea197b72334d2299c7eb44723c8baf7d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 8 Sep 2022 23:05:10 +0200 Subject: [PATCH 218/955] Use new media player enums [s] (#78064) --- .../components/samsungtv/media_player.py | 29 ++++------- .../components/sisyphus/media_player.py | 19 +++---- .../components/slimproto/media_player.py | 14 +++-- .../components/songpal/media_player.py | 9 ++-- .../components/sonos/media_player.py | 49 ++++++++--------- .../components/spotify/media_player.py | 38 ++++++-------- .../components/squeezebox/media_player.py | 52 ++++++++----------- 7 files changed, 87 insertions(+), 123 deletions(-) diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index cf3bfcd64a1..2d457eb29bd 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -26,20 +26,11 @@ from homeassistant.components.media_player import ( MediaPlayerDeviceClass, MediaPlayerEntity, MediaPlayerEntityFeature, -) -from homeassistant.components.media_player.const import ( - MEDIA_TYPE_APP, - MEDIA_TYPE_CHANNEL, + MediaPlayerState, + MediaType, ) from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry -from homeassistant.const import ( - CONF_HOST, - CONF_MAC, - CONF_MODEL, - CONF_NAME, - STATE_OFF, - STATE_ON, -) +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_MODEL, CONF_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_component from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -199,15 +190,17 @@ class SamsungTVDevice(MediaPlayerEntity): return old_state = self._attr_state if self._power_off_in_progress(): - self._attr_state = STATE_OFF + self._attr_state = MediaPlayerState.OFF else: self._attr_state = ( - STATE_ON if await self._bridge.async_is_on() else STATE_OFF + MediaPlayerState.ON + if await self._bridge.async_is_on() + else MediaPlayerState.OFF ) if self._attr_state != old_state: LOGGER.debug("TV %s state updated to %s", self._host, self._attr_state) - if self._attr_state != STATE_ON: + if self._attr_state != MediaPlayerState.ON: if self._dmr_device and self._dmr_device.is_subscribed: await self._dmr_device.async_unsubscribe_services() return @@ -364,7 +357,7 @@ class SamsungTVDevice(MediaPlayerEntity): if self._auth_failed: return False return ( - self._attr_state == STATE_ON + self._attr_state == MediaPlayerState.ON or self._on_script is not None or self._mac is not None or self._power_off_in_progress() @@ -426,11 +419,11 @@ class SamsungTVDevice(MediaPlayerEntity): self, media_type: str, media_id: str, **kwargs: Any ) -> None: """Support changing a channel.""" - if media_type == MEDIA_TYPE_APP: + if media_type == MediaType.APP: await self._async_launch_app(media_id) return - if media_type != MEDIA_TYPE_CHANNEL: + if media_type != MediaType.CHANNEL: LOGGER.error("Unsupported media type") return diff --git a/homeassistant/components/sisyphus/media_player.py b/homeassistant/components/sisyphus/media_player.py index ddd9da23e98..e13924a51e9 100644 --- a/homeassistant/components/sisyphus/media_player.py +++ b/homeassistant/components/sisyphus/media_player.py @@ -7,14 +7,9 @@ from sisyphus_control import Track from homeassistant.components.media_player import ( MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, ) -from homeassistant.const import ( - CONF_HOST, - STATE_IDLE, - STATE_OFF, - STATE_PAUSED, - STATE_PLAYING, -) +from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -89,17 +84,17 @@ class SisyphusPlayer(MediaPlayerEntity): return self._name @property - def state(self): + def state(self) -> MediaPlayerState | None: """Return the current state of the table; sleeping maps to off.""" if self._table.state in ["homing", "playing"]: - return STATE_PLAYING + return MediaPlayerState.PLAYING if self._table.state == "paused": if self._table.is_sleeping: - return STATE_OFF + return MediaPlayerState.OFF - return STATE_PAUSED + return MediaPlayerState.PAUSED if self._table.state == "waiting": - return STATE_IDLE + return MediaPlayerState.IDLE return None diff --git a/homeassistant/components/slimproto/media_player.py b/homeassistant/components/slimproto/media_player.py index a241cd2cd93..784ae3fa6f5 100644 --- a/homeassistant/components/slimproto/media_player.py +++ b/homeassistant/components/slimproto/media_player.py @@ -14,12 +14,10 @@ from homeassistant.components.media_player import ( MediaPlayerDeviceClass, MediaPlayerEntity, MediaPlayerEntityFeature, -) -from homeassistant.components.media_player.browse_media import ( + MediaPlayerState, async_process_play_media_url, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -28,9 +26,9 @@ from homeassistant.util.dt import utcnow from .const import DEFAULT_NAME, DOMAIN, PLAYER_EVENT STATE_MAPPING = { - PlayerState.IDLE: STATE_IDLE, - PlayerState.PLAYING: STATE_PLAYING, - PlayerState.PAUSED: STATE_PAUSED, + PlayerState.IDLE: MediaPlayerState.IDLE, + PlayerState.PLAYING: MediaPlayerState.PLAYING, + PlayerState.PAUSED: MediaPlayerState.PAUSED, } @@ -132,10 +130,10 @@ class SlimProtoPlayer(MediaPlayerEntity): return self.player.connected @property - def state(self) -> str: + def state(self) -> MediaPlayerState: """Return current state.""" if not self.player.powered: - return STATE_OFF + return MediaPlayerState.OFF return STATE_MAPPING[self.player.state] @callback diff --git a/homeassistant/components/songpal/media_player.py b/homeassistant/components/songpal/media_player.py index 4414d590fc1..56b5c2d60b0 100644 --- a/homeassistant/components/songpal/media_player.py +++ b/homeassistant/components/songpal/media_player.py @@ -19,9 +19,10 @@ import voluptuous as vol from homeassistant.components.media_player import ( MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_NAME, EVENT_HOMEASSISTANT_STOP, STATE_OFF, STATE_ON +from homeassistant.const import CONF_NAME, EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers import ( @@ -296,11 +297,11 @@ class SongpalEntity(MediaPlayerEntity): return [src.title for src in self._sources.values()] @property - def state(self): + def state(self) -> MediaPlayerState: """Return current state.""" if self._state: - return STATE_ON - return STATE_OFF + return MediaPlayerState.ON + return MediaPlayerState.OFF @property def source(self): diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 658fcf01ec0..8d541bd8f0d 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -18,28 +18,21 @@ import voluptuous as vol from homeassistant.components import media_source, spotify from homeassistant.components.media_player import ( + ATTR_INPUT_SOURCE, + ATTR_MEDIA_ENQUEUE, + BrowseMedia, MediaPlayerEnqueue, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, + MediaType, + RepeatMode, async_process_play_media_url, ) -from homeassistant.components.media_player.browse_media import BrowseMedia -from homeassistant.components.media_player.const import ( - ATTR_INPUT_SOURCE, - ATTR_MEDIA_ENQUEUE, - MEDIA_TYPE_ALBUM, - MEDIA_TYPE_ARTIST, - MEDIA_TYPE_MUSIC, - MEDIA_TYPE_PLAYLIST, - MEDIA_TYPE_TRACK, - REPEAT_MODE_ALL, - REPEAT_MODE_OFF, - REPEAT_MODE_ONE, -) from homeassistant.components.plex.const import PLEX_URI_SCHEME from homeassistant.components.plex.services import process_plex_payload from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_TIME, STATE_IDLE, STATE_PAUSED, STATE_PLAYING +from homeassistant.const import ATTR_TIME from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, entity_platform, service @@ -74,9 +67,9 @@ UNJOIN_SERVICE_TIMEOUT = 0.1 VOLUME_INCREMENT = 2 REPEAT_TO_SONOS = { - REPEAT_MODE_OFF: False, - REPEAT_MODE_ALL: True, - REPEAT_MODE_ONE: "ONE", + RepeatMode.OFF: False, + RepeatMode.ALL: True, + RepeatMode.ONE: "ONE", } SONOS_TO_REPEAT = {meaning: mode for mode, meaning in REPEAT_TO_SONOS.items()} @@ -211,7 +204,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): | MediaPlayerEntityFeature.VOLUME_MUTE | MediaPlayerEntityFeature.VOLUME_SET ) - _attr_media_content_type = MEDIA_TYPE_MUSIC + _attr_media_content_type = MediaType.MUSIC def __init__(self, speaker: SonosSpeaker) -> None: """Initialize the media player entity.""" @@ -259,7 +252,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): return hash(self.unique_id) @property - def state(self) -> str: + def state(self) -> MediaPlayerState: """Return the state of the entity.""" if self.media.playback_status in ( "PAUSED_PLAYBACK", @@ -268,14 +261,14 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): # Sonos can consider itself "paused" but without having media loaded # (happens if playing Spotify and via Spotify app you pick another device to play on) if self.media.title is None: - return STATE_IDLE - return STATE_PAUSED + return MediaPlayerState.IDLE + return MediaPlayerState.PAUSED if self.media.playback_status in ( SONOS_STATE_PLAYING, SONOS_STATE_TRANSITIONING, ): - return STATE_PLAYING - return STATE_IDLE + return MediaPlayerState.PLAYING + return MediaPlayerState.IDLE async def _async_fallback_poll(self) -> None: """Retrieve latest state by polling.""" @@ -397,7 +390,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): ] @soco_error(UPNP_ERRORS_TO_IGNORE) - def set_repeat(self, repeat: str) -> None: + def set_repeat(self, repeat: RepeatMode) -> None: """Set repeat mode.""" sonos_shuffle = PLAY_MODES[self.media.play_mode][0] sonos_repeat = REPEAT_TO_SONOS[repeat] @@ -521,7 +514,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): if media_source.is_media_source_id(media_id): is_radio = media_id.startswith("media-source://radio_browser/") - media_type = MEDIA_TYPE_MUSIC + media_type = MediaType.MUSIC media_id = ( run_coroutine_threadsafe( media_source.async_resolve_media( @@ -588,7 +581,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): media_id, timeout=LONG_SERVICE_TIMEOUT ) soco.play_from_queue(0) - elif media_type in (MEDIA_TYPE_MUSIC, MEDIA_TYPE_TRACK): + elif media_type in {MediaType.MUSIC, MediaType.TRACK}: # If media ID is a relative URL, we serve it from HA. media_id = async_process_play_media_url(self.hass, media_id) @@ -604,7 +597,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): soco.play_from_queue(new_pos - 1) elif enqueue == MediaPlayerEnqueue.REPLACE: soco.play_uri(media_id, force_radio=is_radio) - elif media_type == MEDIA_TYPE_PLAYLIST: + elif media_type == MediaType.PLAYLIST: if media_id.startswith("S:"): item = media_browser.get_media(self.media.library, media_id, media_type) soco.play_uri(item.get_uri()) @@ -700,7 +693,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): ) -> tuple[bytes | None, str | None]: """Fetch media browser image to serve via proxy.""" if ( - media_content_type in [MEDIA_TYPE_ALBUM, MEDIA_TYPE_ARTIST] + media_content_type in {MediaType.ALBUM, MediaType.ARTIST} and media_content_id ): item = await self.hass.async_add_executor_job( diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index 263cf322cf1..00354e84b92 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -15,18 +15,12 @@ from homeassistant.components.media_player import ( BrowseMedia, MediaPlayerEntity, MediaPlayerEntityFeature, -) -from homeassistant.components.media_player.const import ( - MEDIA_TYPE_EPISODE, - MEDIA_TYPE_MUSIC, - MEDIA_TYPE_PLAYLIST, - MEDIA_TYPE_TRACK, - REPEAT_MODE_ALL, - REPEAT_MODE_OFF, - REPEAT_MODE_ONE, + MediaPlayerState, + MediaType, + RepeatMode, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_ID, STATE_IDLE, STATE_PAUSED, STATE_PLAYING +from homeassistant.const import CONF_ID from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.device_registry import DeviceEntryType @@ -58,9 +52,9 @@ SUPPORT_SPOTIFY = ( ) REPEAT_MODE_MAPPING_TO_HA = { - "context": REPEAT_MODE_ALL, - "off": REPEAT_MODE_OFF, - "track": REPEAT_MODE_ONE, + "context": RepeatMode.ALL, + "off": RepeatMode.OFF, + "track": RepeatMode.ONE, } REPEAT_MODE_MAPPING_TO_SPOTIFY = { @@ -110,7 +104,7 @@ class SpotifyMediaPlayer(MediaPlayerEntity): _attr_has_entity_name = True _attr_icon = "mdi:spotify" - _attr_media_content_type = MEDIA_TYPE_MUSIC + _attr_media_content_type = MediaType.MUSIC _attr_media_image_remotely_accessible = False def __init__( @@ -144,13 +138,13 @@ class SpotifyMediaPlayer(MediaPlayerEntity): self._playlist: dict | None = None @property - def state(self) -> str | None: + def state(self) -> MediaPlayerState: """Return the playback state.""" if not self._currently_playing: - return STATE_IDLE + return MediaPlayerState.IDLE if self._currently_playing["is_playing"]: - return STATE_PLAYING - return STATE_PAUSED + return MediaPlayerState.PLAYING + return MediaPlayerState.PAUSED @property def volume_level(self) -> float | None: @@ -315,7 +309,7 @@ class SpotifyMediaPlayer(MediaPlayerEntity): # Yet, they do generate those types of URI in their official clients. media_id = str(URL(media_id).with_query(None).with_fragment(None)) - if media_type in (MEDIA_TYPE_TRACK, MEDIA_TYPE_EPISODE, MEDIA_TYPE_MUSIC): + if media_type in {MediaType.TRACK, MediaType.EPISODE, MediaType.MUSIC}: kwargs["uris"] = [media_id] elif media_type in PLAYABLE_MEDIA_TYPES: kwargs["context_uri"] = media_id @@ -338,7 +332,7 @@ class SpotifyMediaPlayer(MediaPlayerEntity): for device in self.data.devices.data: if device["name"] == source: self.data.client.transfer_playback( - device["id"], self.state == STATE_PLAYING + device["id"], self.state == MediaPlayerState.PLAYING ) return @@ -348,7 +342,7 @@ class SpotifyMediaPlayer(MediaPlayerEntity): self.data.client.shuffle(shuffle) @spotify_exception_handler - def set_repeat(self, repeat: str) -> None: + def set_repeat(self, repeat: RepeatMode) -> None: """Set repeat mode.""" if repeat not in REPEAT_MODE_MAPPING_TO_SPOTIFY: raise ValueError(f"Unsupported repeat mode: {repeat}") @@ -374,7 +368,7 @@ class SpotifyMediaPlayer(MediaPlayerEntity): self._playlist is None or self._playlist["uri"] != context["uri"] ): self._playlist = None - if context["type"] == MEDIA_TYPE_PLAYLIST: + if context["type"] == MediaType.PLAYLIST: self._playlist = self.data.client.playlist(current["context"]["uri"]) async def async_browse_media( diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index bc6f7298a4f..394f57d1c2d 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -11,21 +11,15 @@ import voluptuous as vol from homeassistant.components import media_source from homeassistant.components.media_player import ( + ATTR_MEDIA_ENQUEUE, MediaPlayerEnqueue, MediaPlayerEntity, MediaPlayerEntityFeature, -) -from homeassistant.components.media_player.browse_media import ( + MediaPlayerState, + MediaType, + RepeatMode, async_process_play_media_url, ) -from homeassistant.components.media_player.const import ( - ATTR_MEDIA_ENQUEUE, - MEDIA_TYPE_MUSIC, - MEDIA_TYPE_PLAYLIST, - REPEAT_MODE_ALL, - REPEAT_MODE_OFF, - REPEAT_MODE_ONE, -) from homeassistant.config_entries import SOURCE_INTEGRATION_DISCOVERY, ConfigEntry from homeassistant.const import ( ATTR_COMMAND, @@ -34,10 +28,6 @@ from homeassistant.const import ( CONF_PORT, CONF_USERNAME, EVENT_HOMEASSISTANT_START, - STATE_IDLE, - STATE_OFF, - STATE_PAUSED, - STATE_PLAYING, ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import ( @@ -94,9 +84,9 @@ ATTR_TO_PROPERTY = [ ] SQUEEZEBOX_MODE = { - "pause": STATE_PAUSED, - "play": STATE_PLAYING, - "stop": STATE_IDLE, + "pause": MediaPlayerState.PAUSED, + "play": MediaPlayerState.PLAYING, + "stop": MediaPlayerState.IDLE, } @@ -289,10 +279,10 @@ class SqueezeBoxEntity(MediaPlayerEntity): self._remove_dispatcher() @property - def state(self): + def state(self) -> MediaPlayerState | None: """Return the state of the device.""" if not self._player.power: - return STATE_OFF + return MediaPlayerState.OFF if self._player.mode: return SQUEEZEBOX_MODE.get(self._player.mode) return None @@ -345,8 +335,8 @@ class SqueezeBoxEntity(MediaPlayerEntity): if not self._player.playlist: return None if len(self._player.playlist) > 1: - return MEDIA_TYPE_PLAYLIST - return MEDIA_TYPE_MUSIC + return MediaType.PLAYLIST + return MediaType.MUSIC @property def media_duration(self): @@ -387,10 +377,10 @@ class SqueezeBoxEntity(MediaPlayerEntity): def repeat(self): """Repeat setting.""" if self._player.repeat == "song": - return REPEAT_MODE_ONE + return RepeatMode.ONE if self._player.repeat == "playlist": - return REPEAT_MODE_ALL - return REPEAT_MODE_OFF + return RepeatMode.ALL + return RepeatMode.OFF @property def shuffle(self): @@ -491,13 +481,13 @@ class SqueezeBoxEntity(MediaPlayerEntity): cmd = "play" if media_source.is_media_source_id(media_id): - media_type = MEDIA_TYPE_MUSIC + media_type = MediaType.MUSIC play_item = await media_source.async_resolve_media( self.hass, media_id, self.entity_id ) media_id = play_item.url - if media_type in MEDIA_TYPE_MUSIC: + if media_type in MediaType.MUSIC: if not media_id.startswith(SQUEEZEBOX_SOURCE_STRINGS): # do not process special squeezebox "source" media ids media_id = async_process_play_media_url(self.hass, media_id) @@ -505,12 +495,12 @@ class SqueezeBoxEntity(MediaPlayerEntity): await self._player.async_load_url(media_id, cmd) return - if media_type == MEDIA_TYPE_PLAYLIST: + if media_type == MediaType.PLAYLIST: try: # a saved playlist by number payload = { "search_id": int(media_id), - "search_type": MEDIA_TYPE_PLAYLIST, + "search_type": MediaType.PLAYLIST, } playlist = await generate_playlist(self._player, payload) except ValueError: @@ -531,11 +521,11 @@ class SqueezeBoxEntity(MediaPlayerEntity): if index is not None: await self._player.async_index(index) - async def async_set_repeat(self, repeat: str) -> None: + async def async_set_repeat(self, repeat: RepeatMode) -> None: """Set the repeat mode.""" - if repeat == REPEAT_MODE_ALL: + if repeat == RepeatMode.ALL: repeat_mode = "playlist" - elif repeat == REPEAT_MODE_ONE: + elif repeat == RepeatMode.ONE: repeat_mode = "song" else: repeat_mode = "none" From 8bdeb3ca5b27b5d92163a14c7dd7c5eca37cfe13 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 8 Sep 2022 23:22:16 +0200 Subject: [PATCH 219/955] Use new media player enums [u-w] (#78067) --- .../components/ue_smart_radio/media_player.py | 26 ++++------ .../components/unifiprotect/media_player.py | 22 ++++---- .../components/vizio/media_player.py | 7 ++- homeassistant/components/vlc/media_player.py | 37 +++++++------- .../components/vlc_telnet/media_player.py | 39 +++++++-------- .../components/volumio/media_player.py | 50 ++++++++----------- .../components/webostv/media_player.py | 25 +++++----- .../components/ws66i/media_player.py | 4 +- 8 files changed, 95 insertions(+), 115 deletions(-) diff --git a/homeassistant/components/ue_smart_radio/media_player.py b/homeassistant/components/ue_smart_radio/media_player.py index 6c83f207762..4dbbb1d5964 100644 --- a/homeassistant/components/ue_smart_radio/media_player.py +++ b/homeassistant/components/ue_smart_radio/media_player.py @@ -10,16 +10,10 @@ from homeassistant.components.media_player import ( PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, + MediaType, ) -from homeassistant.components.media_player.const import MEDIA_TYPE_MUSIC -from homeassistant.const import ( - CONF_PASSWORD, - CONF_USERNAME, - STATE_IDLE, - STATE_OFF, - STATE_PAUSED, - STATE_PLAYING, -) +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -30,7 +24,11 @@ _LOGGER = logging.getLogger(__name__) ICON = "mdi:radio" URL = "http://decibel.logitechmusic.com/jsonrpc.js" -PLAYBACK_DICT = {"play": STATE_PLAYING, "pause": STATE_PAUSED, "stop": STATE_IDLE} +PLAYBACK_DICT = { + "play": MediaPlayerState.PLAYING, + "pause": MediaPlayerState.PAUSED, + "stop": MediaPlayerState.IDLE, +} PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( {vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string} @@ -84,6 +82,7 @@ def setup_platform( class UERadioDevice(MediaPlayerEntity): """Representation of a Logitech UE Smart Radio device.""" + _attr_media_content_type = MediaType.MUSIC _attr_supported_features = ( MediaPlayerEntityFeature.PLAY | MediaPlayerEntityFeature.PAUSE @@ -133,7 +132,7 @@ class UERadioDevice(MediaPlayerEntity): return if request["result"]["power"] == 0: - self._state = STATE_OFF + self._state = MediaPlayerState.OFF else: self._state = PLAYBACK_DICT[request["result"]["mode"]] @@ -172,11 +171,6 @@ class UERadioDevice(MediaPlayerEntity): """Volume level of the media player (0..1).""" return self._volume - @property - def media_content_type(self): - """Return the media content type.""" - return MEDIA_TYPE_MUSIC - @property def media_image_url(self): """Image URL of current playing media.""" diff --git a/homeassistant/components/unifiprotect/media_player.py b/homeassistant/components/unifiprotect/media_player.py index d8edc7fe4e9..1dd1938ff49 100644 --- a/homeassistant/components/unifiprotect/media_player.py +++ b/homeassistant/components/unifiprotect/media_player.py @@ -20,13 +20,11 @@ from homeassistant.components.media_player import ( MediaPlayerEntity, MediaPlayerEntityDescription, MediaPlayerEntityFeature, -) -from homeassistant.components.media_player.browse_media import ( + MediaPlayerState, + MediaType, async_process_play_media_url, ) -from homeassistant.components.media_player.const import MEDIA_TYPE_MUSIC from homeassistant.config_entries import ConfigEntry -from homeassistant.const import STATE_IDLE, STATE_PLAYING from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -95,7 +93,7 @@ class ProtectMediaPlayer(ProtectDeviceEntity, MediaPlayerEntity): ) self._attr_name = f"{self.device.display_name} Speaker" - self._attr_media_content_type = MEDIA_TYPE_MUSIC + self._attr_media_content_type = MediaType.MUSIC @callback def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: @@ -106,9 +104,9 @@ class ProtectMediaPlayer(ProtectDeviceEntity, MediaPlayerEntity): self.device.talkback_stream is not None and self.device.talkback_stream.is_running ): - self._attr_state = STATE_PLAYING + self._attr_state = MediaPlayerState.PLAYING else: - self._attr_state = STATE_IDLE + self._attr_state = MediaPlayerState.IDLE is_connected = self.data.last_update_success and ( self.device.state == StateType.CONNECTED @@ -134,17 +132,17 @@ class ProtectMediaPlayer(ProtectDeviceEntity, MediaPlayerEntity): self._async_updated_event(self.device) async def async_play_media( - self, media_type: str, media_id: str, **kwargs: Any + self, media_type: MediaType | str, media_id: str, **kwargs: Any ) -> None: """Play a piece of media.""" if media_source.is_media_source_id(media_id): - media_type = MEDIA_TYPE_MUSIC + media_type = MediaType.MUSIC play_item = await media_source.async_resolve_media( self.hass, media_id, self.entity_id ) media_id = async_process_play_media_url(self.hass, play_item.url) - if media_type != MEDIA_TYPE_MUSIC: + if media_type != MediaType.MUSIC: raise HomeAssistantError("Only music media type is supported") _LOGGER.debug( @@ -164,7 +162,9 @@ class ProtectMediaPlayer(ProtectDeviceEntity, MediaPlayerEntity): self._async_updated_event(self.device) async def async_browse_media( - self, media_content_type: str | None = None, media_content_id: str | None = None + self, + media_content_type: MediaType | str | None = None, + media_content_id: str | None = None, ) -> BrowseMedia: """Implement the websocket media browsing helper.""" return await media_source.async_browse_media( diff --git a/homeassistant/components/vizio/media_player.py b/homeassistant/components/vizio/media_player.py index ab48f1405a9..ea341f1ca02 100644 --- a/homeassistant/components/vizio/media_player.py +++ b/homeassistant/components/vizio/media_player.py @@ -12,6 +12,7 @@ from homeassistant.components.media_player import ( MediaPlayerDeviceClass, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -21,8 +22,6 @@ from homeassistant.const import ( CONF_HOST, CONF_INCLUDE, CONF_NAME, - STATE_OFF, - STATE_ON, ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_platform @@ -207,7 +206,7 @@ class VizioDevice(MediaPlayerEntity): ) if not is_on: - self._attr_state = STATE_OFF + self._attr_state = MediaPlayerState.OFF self._attr_volume_level = None self._attr_is_volume_muted = None self._current_input = None @@ -216,7 +215,7 @@ class VizioDevice(MediaPlayerEntity): self._attr_sound_mode = None return - self._attr_state = STATE_ON + self._attr_state = MediaPlayerState.ON if audio_settings := await self._device.get_all_settings( VIZIO_AUDIO_SETTINGS, log_api_exception=False diff --git a/homeassistant/components/vlc/media_player.py b/homeassistant/components/vlc/media_player.py index c8517adff91..a2f13de179d 100644 --- a/homeassistant/components/vlc/media_player.py +++ b/homeassistant/components/vlc/media_player.py @@ -13,12 +13,11 @@ from homeassistant.components.media_player import ( BrowseMedia, MediaPlayerEntity, MediaPlayerEntityFeature, -) -from homeassistant.components.media_player.browse_media import ( + MediaPlayerState, + MediaType, async_process_play_media_url, ) -from homeassistant.components.media_player.const import MEDIA_TYPE_MUSIC -from homeassistant.const import CONF_NAME, STATE_IDLE, STATE_PAUSED, STATE_PLAYING +from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -53,6 +52,7 @@ def setup_platform( class VlcDevice(MediaPlayerEntity): """Representation of a vlc player.""" + _attr_media_content_type = MediaType.MUSIC _attr_supported_features = ( MediaPlayerEntityFeature.PAUSE | MediaPlayerEntityFeature.VOLUME_SET @@ -79,11 +79,11 @@ class VlcDevice(MediaPlayerEntity): """Get the latest details from the device.""" status = self._vlc.get_state() if status == vlc.State.Playing: - self._state = STATE_PLAYING + self._state = MediaPlayerState.PLAYING elif status == vlc.State.Paused: - self._state = STATE_PAUSED + self._state = MediaPlayerState.PAUSED else: - self._state = STATE_IDLE + self._state = MediaPlayerState.IDLE self._media_duration = self._vlc.get_length() / 1000 position = self._vlc.get_position() * self._media_duration if position != self._media_position: @@ -115,11 +115,6 @@ class VlcDevice(MediaPlayerEntity): """Boolean if volume is currently muted.""" return self._muted - @property - def media_content_type(self): - """Content type of current playing media.""" - return MEDIA_TYPE_MUSIC - @property def media_duration(self): """Duration of current playing media in seconds.""" @@ -153,20 +148,20 @@ class VlcDevice(MediaPlayerEntity): def media_play(self) -> None: """Send play command.""" self._vlc.play() - self._state = STATE_PLAYING + self._state = MediaPlayerState.PLAYING def media_pause(self) -> None: """Send pause command.""" self._vlc.pause() - self._state = STATE_PAUSED + self._state = MediaPlayerState.PAUSED def media_stop(self) -> None: """Send stop command.""" self._vlc.stop() - self._state = STATE_IDLE + self._state = MediaPlayerState.IDLE async def async_play_media( - self, media_type: str, media_id: str, **kwargs: Any + self, media_type: MediaType | str, media_id: str, **kwargs: Any ) -> None: """Play media from a URL or file.""" # Handle media_source @@ -176,11 +171,11 @@ class VlcDevice(MediaPlayerEntity): ) media_id = sourced_media.url - elif media_type != MEDIA_TYPE_MUSIC: + elif media_type != MediaType.MUSIC: _LOGGER.error( "Invalid media type %s. Only %s is supported", media_type, - MEDIA_TYPE_MUSIC, + MediaType.MUSIC, ) return @@ -191,10 +186,12 @@ class VlcDevice(MediaPlayerEntity): self._vlc.play() await self.hass.async_add_executor_job(play) - self._state = STATE_PLAYING + self._state = MediaPlayerState.PLAYING async def async_browse_media( - self, media_content_type: str | None = None, media_content_id: str | None = None + self, + media_content_type: MediaType | str | None = None, + media_content_id: str | None = None, ) -> BrowseMedia: """Implement the websocket media browsing helper.""" return await media_source.async_browse_media( diff --git a/homeassistant/components/vlc_telnet/media_player.py b/homeassistant/components/vlc_telnet/media_player.py index 75305acbb0c..130d092cbd7 100644 --- a/homeassistant/components/vlc_telnet/media_player.py +++ b/homeassistant/components/vlc_telnet/media_player.py @@ -15,11 +15,12 @@ from homeassistant.components.media_player import ( BrowseMedia, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, + MediaType, async_process_play_media_url, ) -from homeassistant.components.media_player.const import MEDIA_TYPE_MUSIC from homeassistant.config_entries import SOURCE_HASSIO, ConfigEntry -from homeassistant.const import CONF_NAME, STATE_IDLE, STATE_PAUSED, STATE_PLAYING +from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.device_registry import DeviceEntryType @@ -71,6 +72,7 @@ def catch_vlc_errors( class VlcDevice(MediaPlayerEntity): """Representation of a vlc player.""" + _attr_media_content_type = MediaType.MUSIC _attr_supported_features = ( MediaPlayerEntityFeature.CLEAR_PLAYLIST | MediaPlayerEntityFeature.NEXT_TRACK @@ -132,7 +134,7 @@ class VlcDevice(MediaPlayerEntity): ) return - self._state = STATE_IDLE + self._state = MediaPlayerState.IDLE self._available = True LOGGER.info("Connected to vlc host: %s", self._vlc.host) @@ -142,13 +144,13 @@ class VlcDevice(MediaPlayerEntity): self._volume = status.audio_volume / MAX_VOLUME state = status.state if state == "playing": - self._state = STATE_PLAYING + self._state = MediaPlayerState.PLAYING elif state == "paused": - self._state = STATE_PAUSED + self._state = MediaPlayerState.PAUSED else: - self._state = STATE_IDLE + self._state = MediaPlayerState.IDLE - if self._state != STATE_IDLE: + if self._state != MediaPlayerState.IDLE: self._media_duration = (await self._vlc.get_length()).length time_output = await self._vlc.get_time() vlc_position = time_output.time @@ -209,11 +211,6 @@ class VlcDevice(MediaPlayerEntity): """Boolean if volume is currently muted.""" return self._muted - @property - def media_content_type(self) -> str: - """Content type of current playing media.""" - return MEDIA_TYPE_MUSIC - @property def media_duration(self) -> int | None: """Duration of current playing media in seconds.""" @@ -270,7 +267,7 @@ class VlcDevice(MediaPlayerEntity): async def async_media_play(self) -> None: """Send play command.""" await self._vlc.play() - self._state = STATE_PLAYING + self._state = MediaPlayerState.PLAYING @catch_vlc_errors async def async_media_pause(self) -> None: @@ -281,17 +278,17 @@ class VlcDevice(MediaPlayerEntity): # pause. await self._vlc.pause() - self._state = STATE_PAUSED + self._state = MediaPlayerState.PAUSED @catch_vlc_errors async def async_media_stop(self) -> None: """Send stop command.""" await self._vlc.stop() - self._state = STATE_IDLE + self._state = MediaPlayerState.IDLE @catch_vlc_errors async def async_play_media( - self, media_type: str, media_id: str, **kwargs: Any + self, media_type: MediaType | str, media_id: str, **kwargs: Any ) -> None: """Play media from a URL or file.""" # Handle media_source @@ -302,9 +299,9 @@ class VlcDevice(MediaPlayerEntity): media_type = sourced_media.mime_type media_id = sourced_media.url - if media_type != MEDIA_TYPE_MUSIC and not media_type.startswith("audio/"): + if media_type != MediaType.MUSIC and not media_type.startswith("audio/"): raise HomeAssistantError( - f"Invalid media type {media_type}. Only {MEDIA_TYPE_MUSIC} is supported" + f"Invalid media type {media_type}. Only {MediaType.MUSIC} is supported" ) # If media ID is a relative URL, we serve it from HA. @@ -313,7 +310,7 @@ class VlcDevice(MediaPlayerEntity): ) await self._vlc.add(media_id) - self._state = STATE_PLAYING + self._state = MediaPlayerState.PLAYING @catch_vlc_errors async def async_media_previous_track(self) -> None: @@ -337,7 +334,9 @@ class VlcDevice(MediaPlayerEntity): await self._vlc.random(shuffle_command) async def async_browse_media( - self, media_content_type: str | None = None, media_content_id: str | None = None + self, + media_content_type: MediaType | str | None = None, + media_content_id: str | None = None, ) -> BrowseMedia: """Implement the websocket media browsing helper.""" return await media_source.async_browse_media( diff --git a/homeassistant/components/volumio/media_player.py b/homeassistant/components/volumio/media_player.py index 2b07c719f58..befe90e1cf4 100644 --- a/homeassistant/components/volumio/media_player.py +++ b/homeassistant/components/volumio/media_player.py @@ -10,23 +10,15 @@ import json from typing import Any from homeassistant.components.media_player import ( + BrowseMedia, MediaPlayerEntity, MediaPlayerEntityFeature, -) -from homeassistant.components.media_player.browse_media import BrowseMedia -from homeassistant.components.media_player.const import ( - MEDIA_TYPE_MUSIC, - REPEAT_MODE_ALL, - REPEAT_MODE_OFF, + MediaPlayerState, + MediaType, + RepeatMode, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - CONF_ID, - CONF_NAME, - STATE_IDLE, - STATE_PAUSED, - STATE_PLAYING, -) +from homeassistant.const import CONF_ID, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -58,6 +50,7 @@ async def async_setup_entry( class Volumio(MediaPlayerEntity): """Volumio Player Object.""" + _attr_media_content_type = MediaType.MUSIC _attr_supported_features = ( MediaPlayerEntityFeature.PAUSE | MediaPlayerEntityFeature.VOLUME_SET @@ -114,20 +107,15 @@ class Volumio(MediaPlayerEntity): ) @property - def media_content_type(self): - """Content type of current playing media.""" - return MEDIA_TYPE_MUSIC - - @property - def state(self): + def state(self) -> MediaPlayerState: """Return the state of the device.""" status = self._state.get("status", None) if status == "pause": - return STATE_PAUSED + return MediaPlayerState.PAUSED if status == "play": - return STATE_PLAYING + return MediaPlayerState.PLAYING - return STATE_IDLE + return MediaPlayerState.IDLE @property def media_title(self): @@ -179,11 +167,11 @@ class Volumio(MediaPlayerEntity): return self._state.get("random", False) @property - def repeat(self): + def repeat(self) -> RepeatMode: """Return current repeat mode.""" if self._state.get("repeat", None): - return REPEAT_MODE_ALL - return REPEAT_MODE_OFF + return RepeatMode.ALL + return RepeatMode.OFF @property def source_list(self): @@ -241,9 +229,9 @@ class Volumio(MediaPlayerEntity): """Enable/disable shuffle mode.""" await self._volumio.set_shuffle(shuffle) - async def async_set_repeat(self, repeat: str) -> None: + async def async_set_repeat(self, repeat: RepeatMode) -> None: """Set repeat mode.""" - if repeat == REPEAT_MODE_OFF: + if repeat == RepeatMode.OFF: await self._volumio.repeatAll("false") else: await self._volumio.repeatAll("true") @@ -264,13 +252,15 @@ class Volumio(MediaPlayerEntity): self._playlists = await self._volumio.get_playlists() async def async_play_media( - self, media_type: str, media_id: str, **kwargs: Any + self, media_type: MediaType | str, media_id: str, **kwargs: Any ) -> None: """Send the play_media command to the media player.""" await self._volumio.replace_and_play(json.loads(media_id)) async def async_browse_media( - self, media_content_type: str | None = None, media_content_id: str | None = None + self, + media_content_type: MediaType | str | None = None, + media_content_id: str | None = None, ) -> BrowseMedia: """Implement the websocket media browsing helper.""" self.thumbnail_cache = {} @@ -283,7 +273,7 @@ class Volumio(MediaPlayerEntity): async def async_get_browse_image( self, - media_content_type: str, + media_content_type: MediaType | str, media_content_id: str, media_image_id: str | None = None, ) -> tuple[bytes | None, str | None]: diff --git a/homeassistant/components/webostv/media_player.py b/homeassistant/components/webostv/media_player.py index 36941b15240..10fed607ee8 100644 --- a/homeassistant/components/webostv/media_player.py +++ b/homeassistant/components/webostv/media_player.py @@ -16,16 +16,15 @@ from homeassistant.components.media_player import ( MediaPlayerDeviceClass, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, + MediaType, ) -from homeassistant.components.media_player.const import MEDIA_TYPE_CHANNEL from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ENTITY_MATCH_ALL, ENTITY_MATCH_NONE, - STATE_OFF, - STATE_ON, ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -97,7 +96,7 @@ def cmd( try: await func(self, *args, **kwargs) except WEBOSTV_EXCEPTIONS as exc: - if self.state != STATE_OFF: + if self.state != MediaPlayerState.OFF: raise HomeAssistantError( f"Error calling {func.__name__} on entity {self.entity_id}, state:{self.state}" ) from exc @@ -154,7 +153,7 @@ class LgWebOSMediaPlayerEntity(RestoreEntity, MediaPlayerEntity): ) if ( - self.state == STATE_OFF + self.state == MediaPlayerState.OFF and (state := await self.async_get_last_state()) is not None ): self._supported_features = ( @@ -188,7 +187,9 @@ class LgWebOSMediaPlayerEntity(RestoreEntity, MediaPlayerEntity): """Update entity state attributes.""" self._update_sources() - self._attr_state = STATE_ON if self._client.is_on else STATE_OFF + self._attr_state = ( + MediaPlayerState.ON if self._client.is_on else MediaPlayerState.OFF + ) self._attr_is_volume_muted = cast(bool, self._client.muted) self._attr_volume_level = None @@ -200,7 +201,7 @@ class LgWebOSMediaPlayerEntity(RestoreEntity, MediaPlayerEntity): self._attr_media_content_type = None if self._client.current_app_id == LIVE_TV_APP_ID: - self._attr_media_content_type = MEDIA_TYPE_CHANNEL + self._attr_media_content_type = MediaType.CHANNEL self._attr_media_title = None if (self._client.current_app_id == LIVE_TV_APP_ID) and ( @@ -217,7 +218,7 @@ class LgWebOSMediaPlayerEntity(RestoreEntity, MediaPlayerEntity): icon = self._client.apps[self._client.current_app_id]["icon"] self._attr_media_image_url = icon - if self.state != STATE_OFF or not self._supported_features: + if self.state != MediaPlayerState.OFF or not self._supported_features: supported = SUPPORT_WEBOSTV if self._client.sound_output in ("external_arc", "external_speaker"): supported = supported | SUPPORT_WEBOSTV_VOLUME @@ -236,7 +237,7 @@ class LgWebOSMediaPlayerEntity(RestoreEntity, MediaPlayerEntity): name=self.name, ) - if self._client.system_info is not None or self.state != STATE_OFF: + if self._client.system_info is not None or self.state != MediaPlayerState.OFF: maj_v = self._client.software_info.get("major_ver") min_v = self._client.software_info.get("minor_ver") if maj_v and min_v: @@ -246,7 +247,7 @@ class LgWebOSMediaPlayerEntity(RestoreEntity, MediaPlayerEntity): self._attr_device_info["model"] = model self._attr_extra_state_attributes = {} - if self._client.sound_output is not None or self.state != STATE_OFF: + if self._client.sound_output is not None or self.state != MediaPlayerState.OFF: self._attr_extra_state_attributes = { ATTR_SOUND_OUTPUT: self._client.sound_output } @@ -376,12 +377,12 @@ class LgWebOSMediaPlayerEntity(RestoreEntity, MediaPlayerEntity): @cmd async def async_play_media( - self, media_type: str, media_id: str, **kwargs: Any + self, media_type: MediaType | str, media_id: str, **kwargs: Any ) -> None: """Play a piece of media.""" _LOGGER.debug("Call play media type <%s>, Id <%s>", media_type, media_id) - if media_type == MEDIA_TYPE_CHANNEL: + if media_type == MediaType.CHANNEL: _LOGGER.debug("Searching channel") partial_match_channel_id = None perfect_match_channel_id = None diff --git a/homeassistant/components/ws66i/media_player.py b/homeassistant/components/ws66i/media_player.py index 05fd4133885..1101c0c9fbc 100644 --- a/homeassistant/components/ws66i/media_player.py +++ b/homeassistant/components/ws66i/media_player.py @@ -4,9 +4,9 @@ from pyws66i import WS66i, ZoneStatus from homeassistant.components.media_player import ( MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -96,7 +96,7 @@ class Ws66iZone(CoordinatorEntity[Ws66iDataUpdateCoordinator], MediaPlayerEntity def _set_attrs_from_status(self) -> None: status = self._status sources = self._ws66i_data.sources.id_name - self._attr_state = STATE_ON if status.power else STATE_OFF + self._attr_state = MediaPlayerState.ON if status.power else MediaPlayerState.OFF self._attr_volume_level = status.volume / float(MAX_VOL) self._attr_is_volume_muted = status.mute self._attr_source = self._attr_media_title = sources[status.source] From a9b5e276bb3a9bb460de45962481949171d1e3be Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 8 Sep 2022 23:25:51 +0200 Subject: [PATCH 220/955] Use new media player enums [x-z] (#78068) --- homeassistant/components/xbox/media_player.py | 22 ++++----- .../components/xiaomi_tv/media_player.py | 13 ++--- .../components/yamaha/media_player.py | 24 ++++----- .../components/yamaha_musiccast/const.py | 20 +++----- .../yamaha_musiccast/media_player.py | 49 ++++++++----------- .../ziggo_mediabox_xl/media_player.py | 31 +++++------- 6 files changed, 67 insertions(+), 92 deletions(-) diff --git a/homeassistant/components/xbox/media_player.py b/homeassistant/components/xbox/media_player.py index 8209136fa23..5b9dcd77f2b 100644 --- a/homeassistant/components/xbox/media_player.py +++ b/homeassistant/components/xbox/media_player.py @@ -17,10 +17,10 @@ from xbox.webapi.api.provider.smartglass.models import ( from homeassistant.components.media_player import ( MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, + MediaType, ) -from homeassistant.components.media_player.const import MEDIA_TYPE_APP, MEDIA_TYPE_GAME from homeassistant.config_entries import ConfigEntry -from homeassistant.const import STATE_OFF, STATE_ON, STATE_PAUSED, STATE_PLAYING from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -44,12 +44,12 @@ SUPPORT_XBOX = ( ) XBOX_STATE_MAP = { - PlaybackState.Playing: STATE_PLAYING, - PlaybackState.Paused: STATE_PAUSED, - PowerState.On: STATE_ON, - PowerState.SystemUpdate: STATE_OFF, - PowerState.ConnectedStandby: STATE_OFF, - PowerState.Off: STATE_OFF, + PlaybackState.Playing: MediaPlayerState.PLAYING, + PlaybackState.Paused: MediaPlayerState.PAUSED, + PowerState.On: MediaPlayerState.ON, + PowerState.SystemUpdate: MediaPlayerState.OFF, + PowerState.ConnectedStandby: MediaPlayerState.OFF, + PowerState.Off: MediaPlayerState.OFF, PowerState.Unknown: None, } @@ -109,7 +109,7 @@ class XboxMediaPlayer(CoordinatorEntity[XboxUpdateCoordinator], MediaPlayerEntit @property def supported_features(self): """Flag media player features that are supported.""" - if self.state not in [STATE_PLAYING, STATE_PAUSED]: + if self.state not in [MediaPlayerState.PLAYING, MediaPlayerState.PAUSED]: return ( SUPPORT_XBOX & ~MediaPlayerEntityFeature.NEXT_TRACK @@ -122,8 +122,8 @@ class XboxMediaPlayer(CoordinatorEntity[XboxUpdateCoordinator], MediaPlayerEntit """Media content type.""" app_details = self.data.app_details if app_details and app_details.product_family == "Games": - return MEDIA_TYPE_GAME - return MEDIA_TYPE_APP + return MediaType.GAME + return MediaType.APP @property def media_title(self): diff --git a/homeassistant/components/xiaomi_tv/media_player.py b/homeassistant/components/xiaomi_tv/media_player.py index df174b6b0f0..9b9971f9568 100644 --- a/homeassistant/components/xiaomi_tv/media_player.py +++ b/homeassistant/components/xiaomi_tv/media_player.py @@ -10,8 +10,9 @@ from homeassistant.components.media_player import ( PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, ) -from homeassistant.const import CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON +from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -70,7 +71,7 @@ class XiaomiTV(MediaPlayerEntity): self._tv = pymitv.TV(ip) # Default name value, only to be overridden by user. self._name = name - self._state = STATE_OFF + self._state = MediaPlayerState.OFF @property def name(self): @@ -95,17 +96,17 @@ class XiaomiTV(MediaPlayerEntity): because the TV won't accept any input when turned off. Thus, the user would be unable to turn the TV back on, unless it's done manually. """ - if self._state != STATE_OFF: + if self._state != MediaPlayerState.OFF: self._tv.sleep() - self._state = STATE_OFF + self._state = MediaPlayerState.OFF def turn_on(self) -> None: """Wake the TV back up from sleep.""" - if self._state != STATE_ON: + if self._state != MediaPlayerState.ON: self._tv.wake() - self._state = STATE_ON + self._state = MediaPlayerState.ON def volume_up(self) -> None: """Increase volume by one.""" diff --git a/homeassistant/components/yamaha/media_player.py b/homeassistant/components/yamaha/media_player.py index 9a2b1eafbc2..7d98ae5d62a 100644 --- a/homeassistant/components/yamaha/media_player.py +++ b/homeassistant/components/yamaha/media_player.py @@ -12,16 +12,10 @@ from homeassistant.components.media_player import ( PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, + MediaType, ) -from homeassistant.components.media_player.const import MEDIA_TYPE_MUSIC -from homeassistant.const import ( - CONF_HOST, - CONF_NAME, - STATE_IDLE, - STATE_OFF, - STATE_ON, - STATE_PLAYING, -) +from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -201,7 +195,7 @@ class YamahaDevice(MediaPlayerEntity): self.receiver = receiver self._muted = False self._volume = 0 - self._pwstate = STATE_OFF + self._pwstate = MediaPlayerState.OFF self._current_source = None self._sound_mode = None self._sound_mode_list = None @@ -226,13 +220,13 @@ class YamahaDevice(MediaPlayerEntity): if self.receiver.on: if self._play_status is None: - self._pwstate = STATE_ON + self._pwstate = MediaPlayerState.ON elif self._play_status.playing: - self._pwstate = STATE_PLAYING + self._pwstate = MediaPlayerState.PLAYING else: - self._pwstate = STATE_IDLE + self._pwstate = MediaPlayerState.IDLE else: - self._pwstate = STATE_OFF + self._pwstate = MediaPlayerState.OFF self._muted = self.receiver.mute self._volume = (self.receiver.volume / 100) + 1 @@ -443,7 +437,7 @@ class YamahaDevice(MediaPlayerEntity): """Content type of current playing media.""" # Loose assumption that if playback is supported, we are playing music if self._is_playback_supported: - return MEDIA_TYPE_MUSIC + return MediaType.MUSIC return None @property diff --git a/homeassistant/components/yamaha_musiccast/const.py b/homeassistant/components/yamaha_musiccast/const.py index 7846cab1754..79ee3b8e95d 100644 --- a/homeassistant/components/yamaha_musiccast/const.py +++ b/homeassistant/components/yamaha_musiccast/const.py @@ -2,13 +2,7 @@ from aiomusiccast.capabilities import EntityType -from homeassistant.components.media_player.const import ( - MEDIA_CLASS_DIRECTORY, - MEDIA_CLASS_TRACK, - REPEAT_MODE_ALL, - REPEAT_MODE_OFF, - REPEAT_MODE_ONE, -) +from homeassistant.components.media_player import MediaClass, RepeatMode from homeassistant.helpers.entity import EntityCategory DOMAIN = "yamaha_musiccast" @@ -27,9 +21,9 @@ CONF_SERIAL = "serial" DEFAULT_ZONE = "main" HA_REPEAT_MODE_TO_MC_MAPPING = { - REPEAT_MODE_OFF: "off", - REPEAT_MODE_ONE: "one", - REPEAT_MODE_ALL: "all", + RepeatMode.OFF: "off", + RepeatMode.ONE: "one", + RepeatMode.ALL: "all", } NULL_GROUP = "00000000000000000000000000000000" @@ -40,9 +34,9 @@ MC_REPEAT_MODE_TO_HA_MAPPING = { } MEDIA_CLASS_MAPPING = { - "track": MEDIA_CLASS_TRACK, - "directory": MEDIA_CLASS_DIRECTORY, - "categories": MEDIA_CLASS_DIRECTORY, + "track": MediaClass.TRACK, + "directory": MediaClass.DIRECTORY, + "categories": MediaClass.DIRECTORY, } ENTITY_CATEGORY_MAPPING = { diff --git a/homeassistant/components/yamaha_musiccast/media_player.py b/homeassistant/components/yamaha_musiccast/media_player.py index 9251728beda..504c56d73ec 100644 --- a/homeassistant/components/yamaha_musiccast/media_player.py +++ b/homeassistant/components/yamaha_musiccast/media_player.py @@ -11,20 +11,15 @@ from aiomusiccast.features import ZoneFeature from homeassistant.components import media_source from homeassistant.components.media_player import ( BrowseMedia, + MediaClass, MediaPlayerEntity, MediaPlayerEntityFeature, -) -from homeassistant.components.media_player.browse_media import ( + MediaPlayerState, + MediaType, + RepeatMode, async_process_play_media_url, ) -from homeassistant.components.media_player.const import ( - MEDIA_CLASS_DIRECTORY, - MEDIA_CLASS_TRACK, - MEDIA_TYPE_MUSIC, - REPEAT_MODE_OFF, -) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity import Entity @@ -80,11 +75,12 @@ async def async_setup_entry( class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): """The musiccast media player.""" + _attr_media_content_type = MediaType.MUSIC _attr_should_poll = False def __init__(self, zone_id, name, entry_id, coordinator): """Initialize the musiccast device.""" - self._player_state = STATE_PLAYING + self._player_state = MediaPlayerState.PLAYING self._volume_muted = False self._shuffle = False self._zone_id = zone_id @@ -99,7 +95,7 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): self._volume_max = self.coordinator.data.zones[self._zone_id].max_volume self._cur_track = 0 - self._repeat = REPEAT_MODE_OFF + self._repeat = RepeatMode.OFF async def async_added_to_hass(self) -> None: """Run when this Entity has been added to HA.""" @@ -148,21 +144,16 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): """Return the content ID of current playing media.""" return None - @property - def media_content_type(self): - """Return the content type of current playing media.""" - return MEDIA_TYPE_MUSIC - @property def state(self): """Return the state of the player.""" if self.coordinator.data.zones[self._zone_id].power == "on": if self._is_netusb and self.coordinator.data.netusb_playback == "pause": - return STATE_PAUSED + return MediaPlayerState.PAUSED if self._is_netusb and self.coordinator.data.netusb_playback == "stop": - return STATE_IDLE - return STATE_PLAYING - return STATE_OFF + return MediaPlayerState.IDLE + return MediaPlayerState.PLAYING + return MediaPlayerState.OFF @property def volume_level(self): @@ -281,7 +272,7 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): ) media_id = play_item.url - if self.state == STATE_OFF: + if self.state == MediaPlayerState.OFF: await self.async_turn_on() if media_id: @@ -324,7 +315,7 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): ), ) - if self.state == STATE_OFF: + if self.state == MediaPlayerState.OFF: raise HomeAssistantError( "The device has to be turned on to be able to browse media." ) @@ -344,8 +335,8 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): def get_content_type(item): if item.can_play: - return MEDIA_CLASS_TRACK - return MEDIA_CLASS_DIRECTORY + return MediaClass.TRACK + return MediaClass.DIRECTORY children = [ BrowseMedia( @@ -429,7 +420,7 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): return ( MC_REPEAT_MODE_TO_HA_MAPPING.get(self.coordinator.data.netusb_repeat) if self._is_netusb - else REPEAT_MODE_OFF + else RepeatMode.OFF ) @property @@ -459,7 +450,7 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): supported_features |= MediaPlayerEntityFeature.PLAY supported_features |= MediaPlayerEntityFeature.STOP - if self.state != STATE_OFF: + if self.state != MediaPlayerState.OFF: supported_features |= MediaPlayerEntityFeature.BROWSE_MEDIA return supported_features @@ -486,7 +477,7 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): "Service next track is not supported for non NetUSB or Tuner sources." ) - async def async_set_repeat(self, repeat: str) -> None: + async def async_set_repeat(self, repeat: RepeatMode) -> None: """Enable/disable repeat mode.""" if self._is_netusb: await self.coordinator.musiccast.netusb_repeat( @@ -705,7 +696,7 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): if entity.entity_id in group_members ] - if self.state == STATE_OFF: + if self.state == MediaPlayerState.OFF: await self.async_turn_on() if not self.is_server and self.musiccast_zone_entity.is_server: @@ -779,7 +770,7 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): """ # If we should join the group, which is served by the main zone, we can simply select main_sync as input. _LOGGER.debug("%s called service client join", self.entity_id) - if self.state == STATE_OFF: + if self.state == MediaPlayerState.OFF: await self.async_turn_on() if self.ip_address == server.ip_address: if server.zone == DEFAULT_ZONE: diff --git a/homeassistant/components/ziggo_mediabox_xl/media_player.py b/homeassistant/components/ziggo_mediabox_xl/media_player.py index fd2ca59013a..48859cfb167 100644 --- a/homeassistant/components/ziggo_mediabox_xl/media_player.py +++ b/homeassistant/components/ziggo_mediabox_xl/media_player.py @@ -11,14 +11,9 @@ from homeassistant.components.media_player import ( PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, ) -from homeassistant.const import ( - CONF_HOST, - CONF_NAME, - STATE_OFF, - STATE_PAUSED, - STATE_PLAYING, -) +from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -112,10 +107,10 @@ class ZiggoMediaboxXLDevice(MediaPlayerEntity): try: if self._mediabox.test_connection(): if self._mediabox.turned_on(): - if self._state != STATE_PAUSED: - self._state = STATE_PLAYING + if self._state != MediaPlayerState.PAUSED: + self._state = MediaPlayerState.PLAYING else: - self._state = STATE_OFF + self._state = MediaPlayerState.OFF self._available = True else: self._available = False @@ -164,30 +159,30 @@ class ZiggoMediaboxXLDevice(MediaPlayerEntity): def media_play(self) -> None: """Send play command.""" self.send_keys(["PLAY"]) - self._state = STATE_PLAYING + self._state = MediaPlayerState.PLAYING def media_pause(self) -> None: """Send pause command.""" self.send_keys(["PAUSE"]) - self._state = STATE_PAUSED + self._state = MediaPlayerState.PAUSED def media_play_pause(self) -> None: """Simulate play pause media player.""" self.send_keys(["PAUSE"]) - if self._state == STATE_PAUSED: - self._state = STATE_PLAYING + if self._state == MediaPlayerState.PAUSED: + self._state = MediaPlayerState.PLAYING else: - self._state = STATE_PAUSED + self._state = MediaPlayerState.PAUSED def media_next_track(self) -> None: """Channel up.""" self.send_keys(["CHAN_UP"]) - self._state = STATE_PLAYING + self._state = MediaPlayerState.PLAYING def media_previous_track(self) -> None: """Channel down.""" self.send_keys(["CHAN_DOWN"]) - self._state = STATE_PLAYING + self._state = MediaPlayerState.PLAYING def select_source(self, source): """Select the channel.""" @@ -206,4 +201,4 @@ class ZiggoMediaboxXLDevice(MediaPlayerEntity): return self.send_keys([f"NUM_{digit}" for digit in str(digits)]) - self._state = STATE_PLAYING + self._state = MediaPlayerState.PLAYING From 45b69618d34a6e09e2d17b7697cfab878720fb59 Mon Sep 17 00:00:00 2001 From: Alex Thompson Date: Thu, 8 Sep 2022 19:21:58 -0400 Subject: [PATCH 221/955] Add iBeacon start byte to allowed Apple Bluetooth advertisements (#78088) --- homeassistant/components/bluetooth/manager.py | 7 ++- tests/components/bluetooth/test_init.py | 60 +++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index 80817deb2a1..672ef91542e 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -55,9 +55,14 @@ if TYPE_CHECKING: FILTER_UUIDS: Final = "UUIDs" APPLE_MFR_ID: Final = 76 +APPLE_IBEACON_START_BYTE: Final = 0x02 # iBeacon (tilt_ble) APPLE_HOMEKIT_START_BYTE: Final = 0x06 # homekit_controller APPLE_DEVICE_ID_START_BYTE: Final = 0x10 # bluetooth_le_tracker -APPLE_START_BYTES_WANTED: Final = {APPLE_DEVICE_ID_START_BYTE, APPLE_HOMEKIT_START_BYTE} +APPLE_START_BYTES_WANTED: Final = { + APPLE_IBEACON_START_BYTE, + APPLE_HOMEKIT_START_BYTE, + APPLE_DEVICE_ID_START_BYTE, +} RSSI_SWITCH_THRESHOLD = 6 diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index e4b84b943b4..527ab879164 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -1321,6 +1321,66 @@ async def test_register_callback_by_manufacturer_id( assert service_info.manufacturer_id == 21 +async def test_not_filtering_wanted_apple_devices( + hass, mock_bleak_scanner_start, enable_bluetooth +): + """Test filtering noisy apple devices.""" + mock_bt = [] + callbacks = [] + + def _fake_subscriber( + service_info: BluetoothServiceInfo, change: BluetoothChange + ) -> None: + """Fake subscriber for the BleakScanner.""" + callbacks.append((service_info, change)) + + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ): + await async_setup_with_default_adapter(hass) + + with patch.object(hass.config_entries.flow, "async_init"): + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + cancel = bluetooth.async_register_callback( + hass, + _fake_subscriber, + {MANUFACTURER_ID: 76}, + BluetoothScanningMode.ACTIVE, + ) + + assert len(mock_bleak_scanner_start.mock_calls) == 1 + + ibeacon_device = BLEDevice("44:44:33:11:23:45", "rtx") + ibeacon_adv = AdvertisementData( + local_name="ibeacon", + manufacturer_data={76: b"\x02\x00\x00\x00"}, + ) + + inject_advertisement(hass, ibeacon_device, ibeacon_adv) + + homekit_device = BLEDevice("44:44:33:11:23:46", "rtx") + homekit_adv = AdvertisementData( + local_name="homekit", + manufacturer_data={76: b"\x06\x00\x00\x00"}, + ) + + inject_advertisement(hass, homekit_device, homekit_adv) + + apple_device = BLEDevice("44:44:33:11:23:47", "rtx") + apple_adv = AdvertisementData( + local_name="apple", + manufacturer_data={76: b"\x10\x00\x00\x00"}, + ) + + inject_advertisement(hass, apple_device, apple_adv) + + cancel() + + assert len(callbacks) == 3 + + async def test_filtering_noisy_apple_devices( hass, mock_bleak_scanner_start, enable_bluetooth ): From 718d4ac6ccf8e8c8f0c58803456d2c79bc197b3b Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 9 Sep 2022 00:28:55 +0000 Subject: [PATCH 222/955] [ci skip] Translation update --- .../android_ip_webcam/translations/bg.json | 7 + .../android_ip_webcam/translations/sv.json | 27 ++++ .../automation/translations/sv.json | 13 ++ .../components/awair/translations/sv.json | 37 ++++- .../bluemaestro/translations/bg.json | 18 +++ .../bluemaestro/translations/pt-BR.json | 4 +- .../bluemaestro/translations/sv.json | 22 +++ .../components/bluetooth/translations/bg.json | 14 ++ .../components/bluetooth/translations/sv.json | 15 +- .../components/bthome/translations/sv.json | 32 +++++ .../components/ecowitt/translations/sv.json | 20 +++ .../components/escea/translations/sv.json | 13 ++ .../components/fibaro/translations/bg.json | 10 +- .../components/fibaro/translations/sv.json | 10 +- .../fully_kiosk/translations/sv.json | 20 +++ .../components/guardian/translations/sv.json | 13 ++ .../components/hue/translations/sv.json | 5 +- .../components/icloud/translations/bg.json | 3 +- .../components/icloud/translations/sv.json | 7 + .../justnimbus/translations/sv.json | 19 +++ .../lacrosse_view/translations/bg.json | 7 + .../lacrosse_view/translations/sv.json | 3 +- .../components/lametric/translations/bg.json | 19 +++ .../components/lametric/translations/sv.json | 50 +++++++ .../landisgyr_heat_meter/translations/bg.json | 11 ++ .../landisgyr_heat_meter/translations/sv.json | 23 +++ .../components/led_ble/translations/bg.json | 3 +- .../components/led_ble/translations/sv.json | 23 +++ .../litterrobot/translations/bg.json | 3 +- .../litterrobot/translations/sv.json | 10 +- .../components/melnor/translations/sv.json | 13 ++ .../components/mqtt/translations/sv.json | 6 + .../components/mysensors/translations/sv.json | 9 ++ .../nam/translations/sensor.bg.json | 11 ++ .../nam/translations/sensor.sv.json | 11 ++ .../components/nobo_hub/translations/bg.json | 3 +- .../nobo_hub/translations/pt-BR.json | 2 +- .../components/nobo_hub/translations/sv.json | 44 ++++++ .../openexchangerates/translations/sv.json | 33 +++++ .../components/overkiz/translations/sv.json | 3 +- .../p1_monitor/translations/sv.json | 3 + .../components/prusalink/translations/bg.json | 1 + .../prusalink/translations/sensor.bg.json | 7 + .../prusalink/translations/sensor.sv.json | 11 ++ .../components/prusalink/translations/sv.json | 18 +++ .../pure_energie/translations/sv.json | 3 + .../components/pushover/translations/bg.json | 28 ++++ .../components/pushover/translations/sv.json | 34 +++++ .../components/qingping/translations/sv.json | 22 +++ .../components/risco/translations/bg.json | 9 ++ .../components/risco/translations/sv.json | 18 +++ .../components/schedule/translations/sv.json | 9 ++ .../components/sensibo/translations/bg.json | 6 + .../components/sensibo/translations/sv.json | 6 + .../components/sensor/translations/sv.json | 2 + .../components/sensorpro/translations/bg.json | 3 + .../components/sensorpro/translations/sv.json | 22 +++ .../components/skybell/translations/bg.json | 3 +- .../components/skybell/translations/sv.json | 13 ++ .../speedtestdotnet/translations/sv.json | 13 ++ .../components/switchbot/translations/sv.json | 9 ++ .../thermobeacon/translations/bg.json | 3 +- .../thermobeacon/translations/sv.json | 22 +++ .../components/thermopro/translations/sv.json | 21 +++ .../components/tilt_ble/translations/bg.json | 18 +++ .../components/tilt_ble/translations/el.json | 21 +++ .../components/tilt_ble/translations/hu.json | 21 +++ .../components/tilt_ble/translations/id.json | 21 +++ .../tilt_ble/translations/pt-BR.json | 4 +- .../components/tilt_ble/translations/sv.json | 21 +++ .../tilt_ble/translations/zh-Hant.json | 21 +++ .../unifiprotect/translations/sv.json | 5 + .../volvooncall/translations/bg.json | 15 ++ .../volvooncall/translations/sv.json | 29 ++++ .../xiaomi_miio/translations/select.sv.json | 10 ++ .../yalexs_ble/translations/sv.json | 31 ++++ .../components/zha/translations/bg.json | 3 + .../components/zha/translations/sv.json | 132 +++++++++++++++++- 78 files changed, 1211 insertions(+), 23 deletions(-) create mode 100644 homeassistant/components/android_ip_webcam/translations/bg.json create mode 100644 homeassistant/components/android_ip_webcam/translations/sv.json create mode 100644 homeassistant/components/bluemaestro/translations/bg.json create mode 100644 homeassistant/components/bluemaestro/translations/sv.json create mode 100644 homeassistant/components/bluetooth/translations/bg.json create mode 100644 homeassistant/components/bthome/translations/sv.json create mode 100644 homeassistant/components/ecowitt/translations/sv.json create mode 100644 homeassistant/components/escea/translations/sv.json create mode 100644 homeassistant/components/fully_kiosk/translations/sv.json create mode 100644 homeassistant/components/justnimbus/translations/sv.json create mode 100644 homeassistant/components/lacrosse_view/translations/bg.json create mode 100644 homeassistant/components/lametric/translations/bg.json create mode 100644 homeassistant/components/lametric/translations/sv.json create mode 100644 homeassistant/components/landisgyr_heat_meter/translations/bg.json create mode 100644 homeassistant/components/landisgyr_heat_meter/translations/sv.json create mode 100644 homeassistant/components/led_ble/translations/sv.json create mode 100644 homeassistant/components/melnor/translations/sv.json create mode 100644 homeassistant/components/nam/translations/sensor.bg.json create mode 100644 homeassistant/components/nam/translations/sensor.sv.json create mode 100644 homeassistant/components/nobo_hub/translations/sv.json create mode 100644 homeassistant/components/openexchangerates/translations/sv.json create mode 100644 homeassistant/components/prusalink/translations/sensor.bg.json create mode 100644 homeassistant/components/prusalink/translations/sensor.sv.json create mode 100644 homeassistant/components/prusalink/translations/sv.json create mode 100644 homeassistant/components/pushover/translations/bg.json create mode 100644 homeassistant/components/pushover/translations/sv.json create mode 100644 homeassistant/components/qingping/translations/sv.json create mode 100644 homeassistant/components/schedule/translations/sv.json create mode 100644 homeassistant/components/sensorpro/translations/sv.json create mode 100644 homeassistant/components/thermobeacon/translations/sv.json create mode 100644 homeassistant/components/thermopro/translations/sv.json create mode 100644 homeassistant/components/tilt_ble/translations/bg.json create mode 100644 homeassistant/components/tilt_ble/translations/el.json create mode 100644 homeassistant/components/tilt_ble/translations/hu.json create mode 100644 homeassistant/components/tilt_ble/translations/id.json create mode 100644 homeassistant/components/tilt_ble/translations/sv.json create mode 100644 homeassistant/components/tilt_ble/translations/zh-Hant.json create mode 100644 homeassistant/components/volvooncall/translations/sv.json create mode 100644 homeassistant/components/yalexs_ble/translations/sv.json diff --git a/homeassistant/components/android_ip_webcam/translations/bg.json b/homeassistant/components/android_ip_webcam/translations/bg.json new file mode 100644 index 00000000000..946b62a8690 --- /dev/null +++ b/homeassistant/components/android_ip_webcam/translations/bg.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "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" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/android_ip_webcam/translations/sv.json b/homeassistant/components/android_ip_webcam/translations/sv.json new file mode 100644 index 00000000000..9c15e903d9f --- /dev/null +++ b/homeassistant/components/android_ip_webcam/translations/sv.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering" + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rd", + "password": "L\u00f6senord", + "port": "Port", + "username": "Anv\u00e4ndarnamn" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfigurering av Android IP-webbkamera med YAML tas bort. \n\n Din befintliga YAML-konfiguration har automatiskt importerats till anv\u00e4ndargr\u00e4nssnittet. \n\n Ta bort Android IP Webcam YAML-konfigurationen fr\u00e5n filen configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", + "title": "Android IP Webcam YAML-konfigurationen tas bort" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/automation/translations/sv.json b/homeassistant/components/automation/translations/sv.json index 8a5e2e58a9c..b506f524870 100644 --- a/homeassistant/components/automation/translations/sv.json +++ b/homeassistant/components/automation/translations/sv.json @@ -1,4 +1,17 @@ { + "issues": { + "service_not_found": { + "fix_flow": { + "step": { + "confirm": { + "description": "Automatiseringen \" {name} \" (` {entity_id} `) har en \u00e5tg\u00e4rd som anropar en ok\u00e4nd tj\u00e4nst: ` {service} `. \n\n Detta fel hindrar automatiseringen fr\u00e5n att fungera korrekt. Kanske \u00e4r den h\u00e4r tj\u00e4nsten inte l\u00e4ngre tillg\u00e4nglig, eller kanske ett stavfel orsakade det. \n\n F\u00f6r att \u00e5tg\u00e4rda detta fel, [redigera automatiseringen]( {edit} ) och ta bort \u00e5tg\u00e4rden som anropar den h\u00e4r tj\u00e4nsten. \n\n Klicka p\u00e5 Skicka nedan f\u00f6r att bekr\u00e4fta att du har \u00e5tg\u00e4rdat denna automatisering.", + "title": "{name} anv\u00e4nder en ok\u00e4nd tj\u00e4nst" + } + } + }, + "title": "{name} anv\u00e4nder en ok\u00e4nd tj\u00e4nst" + } + }, "state": { "_": { "off": "Av", diff --git a/homeassistant/components/awair/translations/sv.json b/homeassistant/components/awair/translations/sv.json index 017247b4e1d..3fbdec53f83 100644 --- a/homeassistant/components/awair/translations/sv.json +++ b/homeassistant/components/awair/translations/sv.json @@ -2,14 +2,41 @@ "config": { "abort": { "already_configured": "Konto har redan konfigurerats", + "already_configured_account": "Konto har redan konfigurerats", + "already_configured_device": "Enheten \u00e4r redan konfigurerad", "no_devices_found": "Inga enheter hittades i n\u00e4tverket", - "reauth_successful": "\u00c5terautentisering lyckades" + "reauth_successful": "\u00c5terautentisering lyckades", + "unreachable": "Det gick inte att ansluta." }, "error": { "invalid_access_token": "Ogiltig \u00e5tkomstnyckel", - "unknown": "Ov\u00e4ntat fel" + "unknown": "Ov\u00e4ntat fel", + "unreachable": "Det gick inte att ansluta." }, + "flow_title": "{model} ({device_id})", "step": { + "cloud": { + "data": { + "access_token": "\u00c5tkomstnyckel", + "email": "E-post" + }, + "description": "Du m\u00e5ste registrera dig f\u00f6r en Awair-utvecklar\u00e5tkomsttoken p\u00e5: {url}" + }, + "discovery_confirm": { + "description": "Vill du konfigurera {model} ( {device_id} )?" + }, + "local": { + "data": { + "host": "IP-adress" + }, + "description": "F\u00f6lj [dessa instruktioner]({url}) om hur du aktiverar Awair Local API.\n\nKlicka p\u00e5 skicka n\u00e4r du \u00e4r klar." + }, + "local_pick": { + "data": { + "device": "Enhet", + "host": "IP-adress" + } + }, "reauth": { "data": { "access_token": "\u00c5tkomstnyckel", @@ -29,7 +56,11 @@ "access_token": "\u00c5tkomstnyckel", "email": "E-post" }, - "description": "Du m\u00e5ste registrera dig f\u00f6r en Awair-utvecklar\u00e5tkomsttoken p\u00e5: https://developer.getawair.com/onboard/login" + "description": "Du m\u00e5ste registrera dig f\u00f6r en Awair-utvecklar\u00e5tkomsttoken p\u00e5: https://developer.getawair.com/onboard/login", + "menu_options": { + "cloud": "Anslut via molnet", + "local": "Anslut lokalt (rekommenderas)" + } } } } diff --git a/homeassistant/components/bluemaestro/translations/bg.json b/homeassistant/components/bluemaestro/translations/bg.json new file mode 100644 index 00000000000..39900c2a9b2 --- /dev/null +++ b/homeassistant/components/bluemaestro/translations/bg.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u043e" + }, + "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" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluemaestro/translations/pt-BR.json b/homeassistant/components/bluemaestro/translations/pt-BR.json index 0da7639fa2a..5b654163201 100644 --- a/homeassistant/components/bluemaestro/translations/pt-BR.json +++ b/homeassistant/components/bluemaestro/translations/pt-BR.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", - "already_in_progress": "A configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", "no_devices_found": "Nenhum dispositivo encontrado na rede", "not_supported": "Dispositivo n\u00e3o suportado" }, diff --git a/homeassistant/components/bluemaestro/translations/sv.json b/homeassistant/components/bluemaestro/translations/sv.json new file mode 100644 index 00000000000..6c6f3f5f1bb --- /dev/null +++ b/homeassistant/components/bluemaestro/translations/sv.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", + "no_devices_found": "Inga enheter hittades i n\u00e4tverket", + "not_supported": "Enheten st\u00f6ds inte" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vill du konfigurera {name}?" + }, + "user": { + "data": { + "address": "Enhet" + }, + "description": "V\u00e4lj en enhet att konfigurera" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/bg.json b/homeassistant/components/bluetooth/translations/bg.json new file mode 100644 index 00000000000..0740fe1b952 --- /dev/null +++ b/homeassistant/components/bluetooth/translations/bg.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "multiple_adapters": { + "data": { + "adapter": "\u0410\u0434\u0430\u043f\u0442\u0435\u0440" + } + }, + "single_adapter": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 Bluetooth \u0430\u0434\u0430\u043f\u0442\u0435\u0440\u0430 {name}?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/sv.json b/homeassistant/components/bluetooth/translations/sv.json index fe07d338101..4f1e43c1490 100644 --- a/homeassistant/components/bluetooth/translations/sv.json +++ b/homeassistant/components/bluetooth/translations/sv.json @@ -12,6 +12,15 @@ "enable_bluetooth": { "description": "Vill du s\u00e4tta upp Bluetooth?" }, + "multiple_adapters": { + "data": { + "adapter": "Adapter" + }, + "description": "V\u00e4lj en Bluetooth-adapter som ska konfigureras" + }, + "single_adapter": { + "description": "Vill du konfigurera Bluetooth-adaptern {name} ?" + }, "user": { "data": { "address": "Enhet" @@ -24,8 +33,10 @@ "step": { "init": { "data": { - "adapter": "Bluetooth-adaptern som ska anv\u00e4ndas f\u00f6r skanning" - } + "adapter": "Bluetooth-adaptern som ska anv\u00e4ndas f\u00f6r skanning", + "passive": "Passiv skanning" + }, + "description": "Passiv lyssning kr\u00e4ver BlueZ 5.63 eller senare med experimentella funktioner aktiverade." } } } diff --git a/homeassistant/components/bthome/translations/sv.json b/homeassistant/components/bthome/translations/sv.json new file mode 100644 index 00000000000..d7ff3b69339 --- /dev/null +++ b/homeassistant/components/bthome/translations/sv.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", + "no_devices_found": "Inga enheter hittades i n\u00e4tverket", + "reauth_successful": "\u00c5terautentisering lyckades" + }, + "error": { + "decryption_failed": "Den tillhandah\u00e5llna bindningsnyckeln fungerade inte, sensordata kunde inte dekrypteras. Kontrollera den och f\u00f6rs\u00f6k igen.", + "expected_32_characters": "F\u00f6rv\u00e4ntade ett hexadecimalt bindningsnyckel med 32 tecken." + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vill du konfigurera {name}?" + }, + "get_encryption_key": { + "data": { + "bindkey": "Bindningsnyckel" + }, + "description": "De sensordata som s\u00e4nds av sensorn \u00e4r krypterade. F\u00f6r att dekryptera dem beh\u00f6ver vi en hexadecimal bindningsnyckel med 32 tecken." + }, + "user": { + "data": { + "address": "Enhet" + }, + "description": "V\u00e4lj en enhet att konfigurera" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecowitt/translations/sv.json b/homeassistant/components/ecowitt/translations/sv.json new file mode 100644 index 00000000000..0edd1d70fa9 --- /dev/null +++ b/homeassistant/components/ecowitt/translations/sv.json @@ -0,0 +1,20 @@ +{ + "config": { + "create_entry": { + "default": "F\u00f6r att avsluta inst\u00e4llningen av integrationen, anv\u00e4nd Ecowitt-appen (p\u00e5 din telefon) eller g\u00e5 till Ecowitt WebUI i en webbl\u00e4sare p\u00e5 stationens IP-adress. \n\n V\u00e4lj din station - > Meny \u00d6vriga - > DIY Upload Servers. Klicka p\u00e5 n\u00e4sta och v\u00e4lj \"Anpassad\" \n\n - Server-IP: ` {server} `\n - S\u00f6kv\u00e4g: ` {path} `\n - Port: ` {port} ` \n\n Klicka p\u00e5 'Spara'." + }, + "error": { + "invalid_port": "Porten anv\u00e4nds redan.", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "path": "S\u00f6kv\u00e4g med s\u00e4kerhetstoken", + "port": "Lyssningsport" + }, + "description": "\u00c4r du s\u00e4ker p\u00e5 att du vill konfigurera Ecowitt?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/escea/translations/sv.json b/homeassistant/components/escea/translations/sv.json new file mode 100644 index 00000000000..fca3faef5f4 --- /dev/null +++ b/homeassistant/components/escea/translations/sv.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Inga enheter hittades i n\u00e4tverket", + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, + "step": { + "confirm": { + "description": "Vill du s\u00e4tta upp en Escea eldstad?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fibaro/translations/bg.json b/homeassistant/components/fibaro/translations/bg.json index 9f39b8c9185..3eab800c93f 100644 --- a/homeassistant/components/fibaro/translations/bg.json +++ b/homeassistant/components/fibaro/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", + "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", @@ -9,6 +10,13 @@ "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + }, + "description": "\u041c\u043e\u043b\u044f, \u0430\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u0439\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u0430\u0442\u0430 \u0441\u0438 \u0437\u0430 {username}", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" + }, "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u0430", diff --git a/homeassistant/components/fibaro/translations/sv.json b/homeassistant/components/fibaro/translations/sv.json index e35b7b41d46..0bc73f04dac 100644 --- a/homeassistant/components/fibaro/translations/sv.json +++ b/homeassistant/components/fibaro/translations/sv.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Enheten \u00e4r redan konfigurerad" + "already_configured": "Enheten \u00e4r redan konfigurerad", + "reauth_successful": "\u00c5terautentisering lyckades" }, "error": { "cannot_connect": "Det gick inte att ansluta.", @@ -9,6 +10,13 @@ "unknown": "Ov\u00e4ntat fel" }, "step": { + "reauth_confirm": { + "data": { + "password": "L\u00f6senord" + }, + "description": "Uppdatera ditt l\u00f6senord f\u00f6r {username}", + "title": "\u00c5terautenticera integration" + }, "user": { "data": { "import_plugins": "Importera enheter fr\u00e5n fibaro plugin?", diff --git a/homeassistant/components/fully_kiosk/translations/sv.json b/homeassistant/components/fully_kiosk/translations/sv.json new file mode 100644 index 00000000000..3aaa8ccb3aa --- /dev/null +++ b/homeassistant/components/fully_kiosk/translations/sv.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rd", + "password": "L\u00f6senord" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/sv.json b/homeassistant/components/guardian/translations/sv.json index 54fcf49904d..af41cc85efe 100644 --- a/homeassistant/components/guardian/translations/sv.json +++ b/homeassistant/components/guardian/translations/sv.json @@ -17,5 +17,18 @@ "description": "Konfigurera en lokal Elexa Guardian-enhet." } } + }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Uppdatera alla automatiseringar eller skript som anv\u00e4nder den h\u00e4r tj\u00e4nsten f\u00f6r att ist\u00e4llet anv\u00e4nda tj\u00e4nsten ` {alternate_service} ` med ett m\u00e5lenhets-ID p\u00e5 ` {alternate_target} `. Klicka sedan p\u00e5 SKICKA nedan f\u00f6r att markera problemet som l\u00f6st.", + "title": "Tj\u00e4nsten {deprecated_service} tas bort" + } + } + }, + "title": "Tj\u00e4nsten {deprecated_service} tas bort" + } } } \ No newline at end of file diff --git a/homeassistant/components/hue/translations/sv.json b/homeassistant/components/hue/translations/sv.json index 519c35370fa..1707e345983 100644 --- a/homeassistant/components/hue/translations/sv.json +++ b/homeassistant/components/hue/translations/sv.json @@ -44,6 +44,8 @@ "button_2": "Andra knappen", "button_3": "Tredje knappen", "button_4": "Fj\u00e4rde knappen", + "clock_wise": "Rotation medurs", + "counter_clock_wise": "Rotation moturs", "dim_down": "Dimma ned", "dim_up": "Dimma upp", "double_buttons_1_3": "F\u00f6rsta och tredje knapparna", @@ -61,7 +63,8 @@ "remote_double_button_long_press": "B\u00e5da \"{subtype}\" sl\u00e4pptes efter en l\u00e5ngtryckning", "remote_double_button_short_press": "B\u00e5da \"{subtyp}\" sl\u00e4pptes", "repeat": "Knappen \" {subtype} \" h\u00f6lls nedtryckt", - "short_release": "Knappen \" {subtype} \" sl\u00e4pps efter kort tryckning" + "short_release": "Knappen \" {subtype} \" sl\u00e4pps efter kort tryckning", + "start": "\" {subtype} \" trycktes f\u00f6rst" } }, "options": { diff --git a/homeassistant/components/icloud/translations/bg.json b/homeassistant/components/icloud/translations/bg.json index c4ebf72f36a..b3c75ad207e 100644 --- a/homeassistant/components/icloud/translations/bg.json +++ b/homeassistant/components/icloud/translations/bg.json @@ -17,7 +17,8 @@ "reauth_confirm": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u0430" - } + }, + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" }, "user": { "data": { diff --git a/homeassistant/components/icloud/translations/sv.json b/homeassistant/components/icloud/translations/sv.json index bb07cc8291e..b76a5408319 100644 --- a/homeassistant/components/icloud/translations/sv.json +++ b/homeassistant/components/icloud/translations/sv.json @@ -18,6 +18,13 @@ "description": "Ditt tidigare angivna l\u00f6senord f\u00f6r {username} fungerar inte l\u00e4ngre. Uppdatera ditt l\u00f6senord f\u00f6r att forts\u00e4tta anv\u00e4nda denna integration.", "title": "\u00c5terautenticera integration" }, + "reauth_confirm": { + "data": { + "password": "L\u00f6senord" + }, + "description": "Ditt tidigare angivna l\u00f6senord f\u00f6r {username} fungerar inte l\u00e4ngre. Uppdatera ditt l\u00f6senord f\u00f6r att forts\u00e4tta anv\u00e4nda denna integration.", + "title": "\u00c5terautenticera integration" + }, "trusted_device": { "data": { "trusted_device": "Betrodd enhet" diff --git a/homeassistant/components/justnimbus/translations/sv.json b/homeassistant/components/justnimbus/translations/sv.json new file mode 100644 index 00000000000..a255fd3d8fb --- /dev/null +++ b/homeassistant/components/justnimbus/translations/sv.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "client_id": "Klient ID" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lacrosse_view/translations/bg.json b/homeassistant/components/lacrosse_view/translations/bg.json new file mode 100644 index 00000000000..c0ccf23f5b5 --- /dev/null +++ b/homeassistant/components/lacrosse_view/translations/bg.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "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" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lacrosse_view/translations/sv.json b/homeassistant/components/lacrosse_view/translations/sv.json index 241263b2c53..cea713d675c 100644 --- a/homeassistant/components/lacrosse_view/translations/sv.json +++ b/homeassistant/components/lacrosse_view/translations/sv.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Enheten \u00e4r redan konfigurerad" + "already_configured": "Enheten \u00e4r redan konfigurerad", + "reauth_successful": "\u00c5terautentisering lyckades" }, "error": { "invalid_auth": "Ogiltig autentisering", diff --git a/homeassistant/components/lametric/translations/bg.json b/homeassistant/components/lametric/translations/bg.json new file mode 100644 index 00000000000..05b1a3f63b7 --- /dev/null +++ b/homeassistant/components/lametric/translations/bg.json @@ -0,0 +1,19 @@ +{ + "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", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "manual_entry": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447", + "host": "\u0425\u043e\u0441\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lametric/translations/sv.json b/homeassistant/components/lametric/translations/sv.json new file mode 100644 index 00000000000..4ea1aa31e3f --- /dev/null +++ b/homeassistant/components/lametric/translations/sv.json @@ -0,0 +1,50 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "authorize_url_timeout": "Timeout vid generering av en auktoriserings-URL.", + "invalid_discovery_info": "Felaktig uppt\u00e4cktsinformation har tagits emot", + "link_local_address": "Lokala l\u00e4nkadresser st\u00f6ds inte", + "missing_configuration": "LaMetric-integrationen \u00e4r inte konfigurerad. V\u00e4nligen f\u00f6lj dokumentationen.", + "no_devices": "Den auktoriserade anv\u00e4ndaren har inga LaMetric-enheter", + "no_url_available": "Ingen webbadress tillg\u00e4nglig. F\u00f6r information om detta fel, [kolla hj\u00e4lpavsnittet]({docs_url})" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "choice_enter_manual_or_fetch_cloud": { + "description": "En LaMetric-enhet kan st\u00e4llas in i Home Assistant p\u00e5 tv\u00e5 olika s\u00e4tt. \n\n Du kan sj\u00e4lv ange all enhetsinformation och API-tokens, eller s\u00e5 kan Home Assistent importera dem fr\u00e5n ditt LaMetric.com-konto.", + "menu_options": { + "manual_entry": "Ange manuellt", + "pick_implementation": "Importera fr\u00e5n LaMetric.com (rekommenderas)" + } + }, + "manual_entry": { + "data": { + "api_key": "API-nyckel", + "host": "V\u00e4rd" + }, + "data_description": { + "api_key": "Du hittar denna API-nyckel p\u00e5 [enhetssidan i ditt LaMetric-utvecklarkonto](https://developer.lametric.com/user/devices).", + "host": "IP-adressen eller v\u00e4rdnamnet f\u00f6r din LaMetric TIME p\u00e5 ditt n\u00e4tverk." + } + }, + "pick_implementation": { + "title": "V\u00e4lj autentiseringsmetod" + }, + "user_cloud_select_device": { + "data": { + "device": "V\u00e4lj den LaMetric-enhet du vill l\u00e4gga till" + } + } + } + }, + "issues": { + "manual_migration": { + "description": "LaMetric-integrationen har moderniserats: den \u00e4r nu konfigurerad och konfigurerad via anv\u00e4ndargr\u00e4nssnittet och kommunikationen \u00e4r nu lokal. \n\n Tyv\u00e4rr finns det ingen automatisk migreringsv\u00e4g m\u00f6jlig och kr\u00e4ver d\u00e4rf\u00f6r att du \u00e5terst\u00e4ller din LaMetric med Home Assistant. Se Home Assistant LaMetric-integreringsdokumentationen om hur du st\u00e4ller in den. \n\n Ta bort den gamla LaMetric YAML-konfigurationen fr\u00e5n filen configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", + "title": "Manuell migrering kr\u00e4vs f\u00f6r LaMetric" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/landisgyr_heat_meter/translations/bg.json b/homeassistant/components/landisgyr_heat_meter/translations/bg.json new file mode 100644 index 00000000000..9862b6b3a2a --- /dev/null +++ b/homeassistant/components/landisgyr_heat_meter/translations/bg.json @@ -0,0 +1,11 @@ +{ + "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", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/landisgyr_heat_meter/translations/sv.json b/homeassistant/components/landisgyr_heat_meter/translations/sv.json new file mode 100644 index 00000000000..4fde3cf2755 --- /dev/null +++ b/homeassistant/components/landisgyr_heat_meter/translations/sv.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "setup_serial_manual_path": { + "data": { + "device": "USB-enhetens s\u00f6kv\u00e4g" + } + }, + "user": { + "data": { + "device": "V\u00e4lj enhet" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/led_ble/translations/bg.json b/homeassistant/components/led_ble/translations/bg.json index 049c2684415..d264cc05431 100644 --- a/homeassistant/components/led_ble/translations/bg.json +++ b/homeassistant/components/led_ble/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", + "not_supported": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u043d\u0435 \u0441\u0435 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\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/led_ble/translations/sv.json b/homeassistant/components/led_ble/translations/sv.json new file mode 100644 index 00000000000..0e45348e74d --- /dev/null +++ b/homeassistant/components/led_ble/translations/sv.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", + "no_devices_found": "Inga enheter hittades i n\u00e4tverket", + "no_unconfigured_devices": "Inga okonfigurerade enheter hittades.", + "not_supported": "Enheten st\u00f6ds inte" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "unknown": "Ov\u00e4ntat fel" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "Bluetooth-adress" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/bg.json b/homeassistant/components/litterrobot/translations/bg.json index 657ad4878be..7664989fae5 100644 --- a/homeassistant/components/litterrobot/translations/bg.json +++ b/homeassistant/components/litterrobot/translations/bg.json @@ -11,7 +11,8 @@ "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u0430" }, - "description": "\u041c\u043e\u043b\u044f, \u0430\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u0439\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u0430\u0442\u0430 \u0441\u0438 \u0437\u0430 {username}" + "description": "\u041c\u043e\u043b\u044f, \u0430\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u0439\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u0430\u0442\u0430 \u0441\u0438 \u0437\u0430 {username}", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" }, "user": { "data": { diff --git a/homeassistant/components/litterrobot/translations/sv.json b/homeassistant/components/litterrobot/translations/sv.json index 939b543adea..e8919b760d8 100644 --- a/homeassistant/components/litterrobot/translations/sv.json +++ b/homeassistant/components/litterrobot/translations/sv.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Konto har redan konfigurerats" + "already_configured": "Konto har redan konfigurerats", + "reauth_successful": "\u00c5terautentisering lyckades" }, "error": { "cannot_connect": "Det gick inte att ansluta.", @@ -9,6 +10,13 @@ "unknown": "Ov\u00e4ntat fel" }, "step": { + "reauth_confirm": { + "data": { + "password": "L\u00f6senord" + }, + "description": "Uppdatera ditt l\u00f6senord f\u00f6r {username}", + "title": "\u00c5terautenticera integration" + }, "user": { "data": { "password": "L\u00f6senord", diff --git a/homeassistant/components/melnor/translations/sv.json b/homeassistant/components/melnor/translations/sv.json new file mode 100644 index 00000000000..0f7f7b8c06d --- /dev/null +++ b/homeassistant/components/melnor/translations/sv.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Det finns inga Melnor Bluetooth-enheter i n\u00e4rheten." + }, + "step": { + "bluetooth_confirm": { + "description": "Vill du l\u00e4gga till Melnor Bluetooth-ventilen ` {name} ` till Home Assistant?", + "title": "Uppt\u00e4ckte Melnor Bluetooth-ventil" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/sv.json b/homeassistant/components/mqtt/translations/sv.json index 46e3325f990..811da569992 100644 --- a/homeassistant/components/mqtt/translations/sv.json +++ b/homeassistant/components/mqtt/translations/sv.json @@ -49,6 +49,12 @@ "button_triple_press": "\" {subtype}\" trippelklickad" } }, + "issues": { + "deprecated_yaml": { + "description": "Manuellt konfigurerad MQTT {platform} (s) finns under plattformsnyckel ` {platform} `. \n\n Flytta konfigurationen till \"mqtt\"-integreringsnyckeln och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet. Se [dokumentationen]( {more_info_url} ), f\u00f6r mer information.", + "title": "Din manuellt konfigurerade MQTT {platform} (s) beh\u00f6ver \u00e5tg\u00e4rdas" + } + }, "options": { "error": { "bad_birth": "Ogiltigt f\u00f6delse\u00e4mne.", diff --git a/homeassistant/components/mysensors/translations/sv.json b/homeassistant/components/mysensors/translations/sv.json index c41dc508691..7398b60346a 100644 --- a/homeassistant/components/mysensors/translations/sv.json +++ b/homeassistant/components/mysensors/translations/sv.json @@ -14,6 +14,7 @@ "invalid_serial": "Ogiltig serieport", "invalid_subscribe_topic": "Ogiltigt \u00e4mne f\u00f6r prenumeration", "invalid_version": "Ogiltig version av MySensors", + "mqtt_required": "MQTT-integrationen \u00e4r inte konfigurerad", "not_a_number": "Ange ett nummer", "port_out_of_range": "Portnummer m\u00e5ste vara minst 1 och h\u00f6gst 65535", "same_topic": "\u00c4mnen f\u00f6r prenumeration och publicering \u00e4r desamma", @@ -68,6 +69,14 @@ }, "description": "Ethernet-gateway-inst\u00e4llning" }, + "select_gateway_type": { + "description": "V\u00e4lj vilken gateway som ska konfigureras.", + "menu_options": { + "gw_mqtt": "Konfigurera en MQTT-gateway", + "gw_serial": "Konfigurera en seriell gateway", + "gw_tcp": "Konfigurera en TCP-gateway" + } + }, "user": { "data": { "gateway_type": "Gateway typ" diff --git a/homeassistant/components/nam/translations/sensor.bg.json b/homeassistant/components/nam/translations/sensor.bg.json new file mode 100644 index 00000000000..5c772d85ca1 --- /dev/null +++ b/homeassistant/components/nam/translations/sensor.bg.json @@ -0,0 +1,11 @@ +{ + "state": { + "nam__caqi_level": { + "high": "\u0412\u0438\u0441\u043e\u043a\u043e", + "low": "\u041d\u0438\u0441\u043a\u043e", + "medium": "\u0421\u0440\u0435\u0434\u043d\u043e", + "very high": "\u041c\u043d\u043e\u0433\u043e \u0432\u0438\u0441\u043e\u043a\u043e", + "very low": "\u041c\u043d\u043e\u0433\u043e \u043d\u0438\u0441\u043a\u043e" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nam/translations/sensor.sv.json b/homeassistant/components/nam/translations/sensor.sv.json new file mode 100644 index 00000000000..5039ab0d5ee --- /dev/null +++ b/homeassistant/components/nam/translations/sensor.sv.json @@ -0,0 +1,11 @@ +{ + "state": { + "nam__caqi_level": { + "high": "H\u00f6g", + "low": "L\u00e5g", + "medium": "Medium", + "very high": "V\u00e4ldigt h\u00f6gt", + "very low": "Mycket l\u00e5g" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nobo_hub/translations/bg.json b/homeassistant/components/nobo_hub/translations/bg.json index 421434da3fd..ed6e5d4bba2 100644 --- a/homeassistant/components/nobo_hub/translations/bg.json +++ b/homeassistant/components/nobo_hub/translations/bg.json @@ -11,7 +11,8 @@ "step": { "manual": { "data": { - "ip_address": "IP \u0430\u0434\u0440\u0435\u0441" + "ip_address": "IP \u0430\u0434\u0440\u0435\u0441", + "serial": "\u0421\u0435\u0440\u0438\u0435\u043d \u043d\u043e\u043c\u0435\u0440 (12 \u0446\u0438\u0444\u0440\u0438)" } } } diff --git a/homeassistant/components/nobo_hub/translations/pt-BR.json b/homeassistant/components/nobo_hub/translations/pt-BR.json index 9de87f8dad5..48278bfe1cb 100644 --- a/homeassistant/components/nobo_hub/translations/pt-BR.json +++ b/homeassistant/components/nobo_hub/translations/pt-BR.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" }, "error": { "cannot_connect": "Falha ao conectar - verifique o n\u00famero de s\u00e9rie", diff --git a/homeassistant/components/nobo_hub/translations/sv.json b/homeassistant/components/nobo_hub/translations/sv.json new file mode 100644 index 00000000000..c4306e930d2 --- /dev/null +++ b/homeassistant/components/nobo_hub/translations/sv.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta - kontrollera serienumret", + "invalid_ip": "Ogiltig IP-adress", + "invalid_serial": "Ogiltigt serienummer", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "manual": { + "data": { + "ip_address": "IP-adress", + "serial": "Serienummer (12 siffror)" + }, + "description": "Konfigurera en Nob\u00f8 Ecohub som inte uppt\u00e4ckts i ditt lokala n\u00e4tverk. Om din hubb \u00e4r p\u00e5 ett annat n\u00e4tverk kan du fortfarande ansluta till det genom att ange hela serienumret (12 siffror) och dess IP-adress." + }, + "selected": { + "data": { + "serial_suffix": "Serienummersuffix (3 siffror)" + }, + "description": "Konfigurerar {hub} . F\u00f6r att ansluta till hubben m\u00e5ste du ange de tre sista siffrorna i hubbens serienummer." + }, + "user": { + "data": { + "device": "Identifierade hubbar" + }, + "description": "V\u00e4lj Nob\u00f8 Ecohub f\u00f6r att konfigurera." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "override_type": "\u00c5sidos\u00e4tt typ" + }, + "description": "V\u00e4lj \u00e5sidos\u00e4ttningstyp \"Nu\" f\u00f6r att avsluta \u00e5sidos\u00e4ttningen vid n\u00e4sta veckas profil\u00e4ndring." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openexchangerates/translations/sv.json b/homeassistant/components/openexchangerates/translations/sv.json new file mode 100644 index 00000000000..578d74864ba --- /dev/null +++ b/homeassistant/components/openexchangerates/translations/sv.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad", + "cannot_connect": "Det gick inte att ansluta.", + "reauth_successful": "\u00c5terautentisering lyckades", + "timeout_connect": "Timeout uppr\u00e4ttar anslutning" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "timeout_connect": "Timeout uppr\u00e4ttar anslutning", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "api_key": "API-nyckel", + "base": "Basvaluta" + }, + "data_description": { + "base": "Att anv\u00e4nda en annan basvaluta \u00e4n USD kr\u00e4ver en [betald plan]( {signup} )." + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfigurering av Open Exchange Rates med YAML tas bort. \n\n Din befintliga YAML-konfiguration har automatiskt importerats till anv\u00e4ndargr\u00e4nssnittet. \n\n Ta bort Open Exchange Rates YAML-konfigurationen fr\u00e5n filen configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", + "title": "Open Exchange Rates YAML-konfigurationen tas bort" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/sv.json b/homeassistant/components/overkiz/translations/sv.json index b173a0bef99..32565cac512 100644 --- a/homeassistant/components/overkiz/translations/sv.json +++ b/homeassistant/components/overkiz/translations/sv.json @@ -11,7 +11,8 @@ "server_in_maintenance": "Servern ligger nere f\u00f6r underh\u00e5ll", "too_many_attempts": "F\u00f6r m\u00e5nga f\u00f6rs\u00f6k med en ogiltig token, tillf\u00e4lligt avst\u00e4ngd", "too_many_requests": "F\u00f6r m\u00e5nga f\u00f6rfr\u00e5gningar, f\u00f6rs\u00f6k igen senare", - "unknown": "Ov\u00e4ntat fel" + "unknown": "Ov\u00e4ntat fel", + "unknown_user": "Ok\u00e4nd anv\u00e4ndare. Somfy Protect-konton st\u00f6ds inte av denna integration." }, "flow_title": "Gateway: {gateway_id}", "step": { diff --git a/homeassistant/components/p1_monitor/translations/sv.json b/homeassistant/components/p1_monitor/translations/sv.json index 2dcba36c76a..2a4c3c62277 100644 --- a/homeassistant/components/p1_monitor/translations/sv.json +++ b/homeassistant/components/p1_monitor/translations/sv.json @@ -9,6 +9,9 @@ "host": "V\u00e4rd", "name": "Namn" }, + "data_description": { + "host": "IP-adressen eller v\u00e4rdnamnet f\u00f6r din P1 Monitor-installation." + }, "description": "Konfigurera P1 Monitor f\u00f6r att integrera med Home Assistant." } } diff --git a/homeassistant/components/prusalink/translations/bg.json b/homeassistant/components/prusalink/translations/bg.json index 54d6eb68c8b..f0eea7f98b2 100644 --- a/homeassistant/components/prusalink/translations/bg.json +++ b/homeassistant/components/prusalink/translations/bg.json @@ -3,6 +3,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\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", + "not_supported": "\u041f\u043e\u0434\u0434\u044a\u0440\u0436\u0430 \u0441\u0435 \u0441\u0430\u043c\u043e PrusaLink API v2", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "step": { diff --git a/homeassistant/components/prusalink/translations/sensor.bg.json b/homeassistant/components/prusalink/translations/sensor.bg.json new file mode 100644 index 00000000000..e44192730e7 --- /dev/null +++ b/homeassistant/components/prusalink/translations/sensor.bg.json @@ -0,0 +1,7 @@ +{ + "state": { + "prusalink__printer_state": { + "printing": "\u041e\u0442\u043f\u0435\u0447\u0430\u0442\u0432\u0430\u043d\u0435" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/sensor.sv.json b/homeassistant/components/prusalink/translations/sensor.sv.json new file mode 100644 index 00000000000..47f894b1dcd --- /dev/null +++ b/homeassistant/components/prusalink/translations/sensor.sv.json @@ -0,0 +1,11 @@ +{ + "state": { + "prusalink__printer_state": { + "cancelling": "Avbryter", + "idle": "Inaktiv", + "paused": "Pausad", + "pausing": "Pausar", + "printing": "Skriver ut" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/sv.json b/homeassistant/components/prusalink/translations/sv.json new file mode 100644 index 00000000000..56d0b970314 --- /dev/null +++ b/homeassistant/components/prusalink/translations/sv.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "not_supported": "Endast PrusaLink API v2 st\u00f6ds", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "api_key": "API-nyckel", + "host": "V\u00e4rd" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pure_energie/translations/sv.json b/homeassistant/components/pure_energie/translations/sv.json index d85762bb0b4..b77f7babf41 100644 --- a/homeassistant/components/pure_energie/translations/sv.json +++ b/homeassistant/components/pure_energie/translations/sv.json @@ -12,6 +12,9 @@ "user": { "data": { "host": "V\u00e4rd" + }, + "data_description": { + "host": "IP-adressen eller v\u00e4rdnamnet f\u00f6r din Pure Energie-m\u00e4tare." } }, "zeroconf_confirm": { diff --git a/homeassistant/components/pushover/translations/bg.json b/homeassistant/components/pushover/translations/bg.json new file mode 100644 index 00000000000..36e77da587c --- /dev/null +++ b/homeassistant/components/pushover/translations/bg.json @@ -0,0 +1,28 @@ +{ + "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", + "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_api_key": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d API \u043a\u043b\u044e\u0447", + "invalid_user_key": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u0438 \u043a\u043b\u044e\u0447" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447" + }, + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" + }, + "user": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447", + "name": "\u0418\u043c\u0435", + "user_key": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u0438 \u043a\u043b\u044e\u0447" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pushover/translations/sv.json b/homeassistant/components/pushover/translations/sv.json new file mode 100644 index 00000000000..5e85503bb4b --- /dev/null +++ b/homeassistant/components/pushover/translations/sv.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad", + "reauth_successful": "\u00c5terautentisering lyckades" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_api_key": "Ogiltig API-nyckel", + "invalid_user_key": "Ogiltig anv\u00e4ndarnyckel" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API-nyckel" + }, + "title": "\u00c5terautenticera integration" + }, + "user": { + "data": { + "api_key": "API-nyckel", + "name": "Namn", + "user_key": "Anv\u00e4ndarnyckel" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfigurering av Pushover med YAML tas bort. \n\n Din befintliga YAML-konfiguration har automatiskt importerats till anv\u00e4ndargr\u00e4nssnittet. \n\n Ta bort Pushover YAML-konfigurationen fr\u00e5n filen configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", + "title": "Pushover YAML-konfigurationen tas bort" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qingping/translations/sv.json b/homeassistant/components/qingping/translations/sv.json new file mode 100644 index 00000000000..6c6f3f5f1bb --- /dev/null +++ b/homeassistant/components/qingping/translations/sv.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", + "no_devices_found": "Inga enheter hittades i n\u00e4tverket", + "not_supported": "Enheten st\u00f6ds inte" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vill du konfigurera {name}?" + }, + "user": { + "data": { + "address": "Enhet" + }, + "description": "V\u00e4lj en enhet att konfigurera" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/risco/translations/bg.json b/homeassistant/components/risco/translations/bg.json index 14bd2a15ccb..3cc6e1c2bbc 100644 --- a/homeassistant/components/risco/translations/bg.json +++ b/homeassistant/components/risco/translations/bg.json @@ -9,8 +9,17 @@ "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "step": { + "cloud": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "pin": "\u041f\u0418\u041d \u043a\u043e\u0434", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + }, "local": { "data": { + "host": "\u0425\u043e\u0441\u0442", + "pin": "\u041f\u0418\u041d \u043a\u043e\u0434", "port": "\u041f\u043e\u0440\u0442" } }, diff --git a/homeassistant/components/risco/translations/sv.json b/homeassistant/components/risco/translations/sv.json index f0194b4edf5..dd89d9c43f7 100644 --- a/homeassistant/components/risco/translations/sv.json +++ b/homeassistant/components/risco/translations/sv.json @@ -9,11 +9,29 @@ "unknown": "Ov\u00e4ntat fel" }, "step": { + "cloud": { + "data": { + "password": "L\u00f6senord", + "pin": "Pin-kod", + "username": "Anv\u00e4ndarnamn" + } + }, + "local": { + "data": { + "host": "V\u00e4rd", + "pin": "Pin-kod", + "port": "Port" + } + }, "user": { "data": { "password": "L\u00f6senord", "pin": "Pin-kod", "username": "Anv\u00e4ndarnamn" + }, + "menu_options": { + "cloud": "Risco Cloud (rekommenderas)", + "local": "Lokal Risco Panel (avancerat)" } } } diff --git a/homeassistant/components/schedule/translations/sv.json b/homeassistant/components/schedule/translations/sv.json new file mode 100644 index 00000000000..04cd29ef2eb --- /dev/null +++ b/homeassistant/components/schedule/translations/sv.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "Av", + "on": "P\u00e5" + } + }, + "title": "Schema" +} \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/bg.json b/homeassistant/components/sensibo/translations/bg.json index 1435a77a2fb..6bdc0050748 100644 --- a/homeassistant/components/sensibo/translations/bg.json +++ b/homeassistant/components/sensibo/translations/bg.json @@ -12,11 +12,17 @@ "reauth_confirm": { "data": { "api_key": "API \u043a\u043b\u044e\u0447" + }, + "data_description": { + "api_key": "\u0421\u043b\u0435\u0434\u0432\u0430\u0439\u0442\u0435 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0442\u0430, \u0437\u0430 \u0434\u0430 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u0435 \u043d\u043e\u0432 api \u043a\u043b\u044e\u0447." } }, "user": { "data": { "api_key": "API \u043a\u043b\u044e\u0447" + }, + "data_description": { + "api_key": "\u0421\u043b\u0435\u0434\u0432\u0430\u0439\u0442\u0435 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0442\u0430, \u0437\u0430 \u0434\u0430 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u0435 \u0441\u0432\u043e\u044f API \u043a\u043b\u044e\u0447." } } } diff --git a/homeassistant/components/sensibo/translations/sv.json b/homeassistant/components/sensibo/translations/sv.json index 04f12a04cd4..3c5b5dd2a23 100644 --- a/homeassistant/components/sensibo/translations/sv.json +++ b/homeassistant/components/sensibo/translations/sv.json @@ -15,11 +15,17 @@ "reauth_confirm": { "data": { "api_key": "API-nyckel" + }, + "data_description": { + "api_key": "F\u00f6lj dokumentationen f\u00f6r att f\u00e5 en ny api-nyckel." } }, "user": { "data": { "api_key": "API-nyckel" + }, + "data_description": { + "api_key": "F\u00f6lj dokumentationen f\u00f6r att f\u00e5 din api-nyckel." } } } diff --git a/homeassistant/components/sensor/translations/sv.json b/homeassistant/components/sensor/translations/sv.json index 91f79c0c703..544bf563bcc 100644 --- a/homeassistant/components/sensor/translations/sv.json +++ b/homeassistant/components/sensor/translations/sv.json @@ -11,6 +11,7 @@ "is_gas": "Nuvarande {entity_name} gas", "is_humidity": "Nuvarande {entity_name} fuktighet", "is_illuminance": "Nuvarande {entity_name} belysning", + "is_moisture": "Aktuell {entity_name} fukt", "is_nitrogen_dioxide": "Nuvarande {entity_name} koncentration av kv\u00e4vedioxid", "is_nitrogen_monoxide": "Nuvarande {entity_name} koncentration av kv\u00e4veoxid", "is_nitrous_oxide": "Nuvarande koncentration av lustgas i {entity_name}.", @@ -40,6 +41,7 @@ "gas": "{entity_name} gasf\u00f6r\u00e4ndringar", "humidity": "{entity_name} fuktighet \u00e4ndras", "illuminance": "{entity_name} belysning \u00e4ndras", + "moisture": "{entity_name} fuktf\u00f6r\u00e4ndringar", "nitrogen_dioxide": "{entity_name} kv\u00e4vedioxidkoncentrationen f\u00f6r\u00e4ndras.", "nitrogen_monoxide": "{entity_name} koncentrationen av kv\u00e4vemonoxid \u00e4ndras", "nitrous_oxide": "{entity_name} f\u00f6r\u00e4ndringar i koncentrationen av lustgas", diff --git a/homeassistant/components/sensorpro/translations/bg.json b/homeassistant/components/sensorpro/translations/bg.json index 0d059b2bf51..c79e057d5c0 100644 --- a/homeassistant/components/sensorpro/translations/bg.json +++ b/homeassistant/components/sensorpro/translations/bg.json @@ -1,5 +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" + }, "flow_title": "{name}", "step": { "bluetooth_confirm": { diff --git a/homeassistant/components/sensorpro/translations/sv.json b/homeassistant/components/sensorpro/translations/sv.json new file mode 100644 index 00000000000..6c6f3f5f1bb --- /dev/null +++ b/homeassistant/components/sensorpro/translations/sv.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", + "no_devices_found": "Inga enheter hittades i n\u00e4tverket", + "not_supported": "Enheten st\u00f6ds inte" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vill du konfigurera {name}?" + }, + "user": { + "data": { + "address": "Enhet" + }, + "description": "V\u00e4lj en enhet att konfigurera" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/skybell/translations/bg.json b/homeassistant/components/skybell/translations/bg.json index bdec7688abf..a8057a452ab 100644 --- a/homeassistant/components/skybell/translations/bg.json +++ b/homeassistant/components/skybell/translations/bg.json @@ -14,7 +14,8 @@ "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u0430" }, - "description": "\u041c\u043e\u043b\u044f, \u0430\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u0439\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u0430\u0442\u0430 \u0441\u0438 \u0437\u0430 {email}" + "description": "\u041c\u043e\u043b\u044f, \u0430\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u0439\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u0430\u0442\u0430 \u0441\u0438 \u0437\u0430 {email}", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" }, "user": { "data": { diff --git a/homeassistant/components/skybell/translations/sv.json b/homeassistant/components/skybell/translations/sv.json index bb5b9d188a6..9e21d41d675 100644 --- a/homeassistant/components/skybell/translations/sv.json +++ b/homeassistant/components/skybell/translations/sv.json @@ -10,6 +10,13 @@ "unknown": "Ov\u00e4ntat fel" }, "step": { + "reauth_confirm": { + "data": { + "password": "L\u00f6senord" + }, + "description": "Uppdatera ditt l\u00f6senord f\u00f6r {email}", + "title": "\u00c5terautenticera integration" + }, "user": { "data": { "email": "E-post", @@ -17,5 +24,11 @@ } } } + }, + "issues": { + "removed_yaml": { + "description": "Konfigurering av Skybell med YAML har tagits bort. \n\n Din befintliga YAML-konfiguration anv\u00e4nds inte av Home Assistant. \n\n Ta bort YAML-konfigurationen fr\u00e5n filen configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", + "title": "Skybell YAML-konfigurationen har tagits bort" + } } } \ No newline at end of file diff --git a/homeassistant/components/speedtestdotnet/translations/sv.json b/homeassistant/components/speedtestdotnet/translations/sv.json index cde9095fba8..81e07b927c7 100644 --- a/homeassistant/components/speedtestdotnet/translations/sv.json +++ b/homeassistant/components/speedtestdotnet/translations/sv.json @@ -9,6 +9,19 @@ } } }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Uppdatera eventuella automatiseringar eller skript som anv\u00e4nder den h\u00e4r tj\u00e4nsten f\u00f6r att ist\u00e4llet anv\u00e4nda tj\u00e4nsten `homeassistant.update_entity` med ett m\u00e5l Speedtest-entity_id. Klicka sedan p\u00e5 SKICKA nedan f\u00f6r att markera problemet som l\u00f6st.", + "title": "Tj\u00e4nsten speedtest tas bort" + } + } + }, + "title": "Tj\u00e4nsten speedtest tas bort" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/switchbot/translations/sv.json b/homeassistant/components/switchbot/translations/sv.json index 717b00d3d6c..8124e80fd73 100644 --- a/homeassistant/components/switchbot/translations/sv.json +++ b/homeassistant/components/switchbot/translations/sv.json @@ -9,6 +9,15 @@ }, "flow_title": "{name} ({address})", "step": { + "confirm": { + "description": "Vill du konfigurera {name}?" + }, + "password": { + "data": { + "password": "L\u00f6senord" + }, + "description": "{name} -enheten kr\u00e4ver ett l\u00f6senord" + }, "user": { "data": { "address": "Enhetsadress", diff --git a/homeassistant/components/thermobeacon/translations/bg.json b/homeassistant/components/thermobeacon/translations/bg.json index c79e057d5c0..325b3d59a46 100644 --- a/homeassistant/components/thermobeacon/translations/bg.json +++ b/homeassistant/components/thermobeacon/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", + "not_supported": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u043d\u0435 \u0441\u0435 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/thermobeacon/translations/sv.json b/homeassistant/components/thermobeacon/translations/sv.json new file mode 100644 index 00000000000..6c6f3f5f1bb --- /dev/null +++ b/homeassistant/components/thermobeacon/translations/sv.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", + "no_devices_found": "Inga enheter hittades i n\u00e4tverket", + "not_supported": "Enheten st\u00f6ds inte" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vill du konfigurera {name}?" + }, + "user": { + "data": { + "address": "Enhet" + }, + "description": "V\u00e4lj en enhet att konfigurera" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/thermopro/translations/sv.json b/homeassistant/components/thermopro/translations/sv.json new file mode 100644 index 00000000000..7606ba7df45 --- /dev/null +++ b/homeassistant/components/thermopro/translations/sv.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", + "no_devices_found": "Inga enheter hittades i n\u00e4tverket" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vill du konfigurera {name}?" + }, + "user": { + "data": { + "address": "Enhet" + }, + "description": "V\u00e4lj en enhet att konfigurera" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tilt_ble/translations/bg.json b/homeassistant/components/tilt_ble/translations/bg.json new file mode 100644 index 00000000000..39900c2a9b2 --- /dev/null +++ b/homeassistant/components/tilt_ble/translations/bg.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u043e" + }, + "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" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tilt_ble/translations/el.json b/homeassistant/components/tilt_ble/translations/el.json new file mode 100644 index 00000000000..0a802a0bc89 --- /dev/null +++ b/homeassistant/components/tilt_ble/translations/el.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "already_in_progress": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", + "no_devices_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name};" + }, + "user": { + "data": { + "address": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b3\u03b9\u03b1 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tilt_ble/translations/hu.json b/homeassistant/components/tilt_ble/translations/hu.json new file mode 100644 index 00000000000..7ef0d3a6301 --- /dev/null +++ b/homeassistant/components/tilt_ble/translations/hu.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name}?" + }, + "user": { + "data": { + "address": "Eszk\u00f6z" + }, + "description": "V\u00e1lassza ki a be\u00e1ll\u00edtani k\u00edv\u00e1nt eszk\u00f6zt" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tilt_ble/translations/id.json b/homeassistant/components/tilt_ble/translations/id.json new file mode 100644 index 00000000000..07426a0e290 --- /dev/null +++ b/homeassistant/components/tilt_ble/translations/id.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan" + }, + "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/tilt_ble/translations/pt-BR.json b/homeassistant/components/tilt_ble/translations/pt-BR.json index 2067d7f9312..3f93e65c087 100644 --- a/homeassistant/components/tilt_ble/translations/pt-BR.json +++ b/homeassistant/components/tilt_ble/translations/pt-BR.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", - "already_in_progress": "A configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", "no_devices_found": "Nenhum dispositivo encontrado na rede" }, "flow_title": "{name}", diff --git a/homeassistant/components/tilt_ble/translations/sv.json b/homeassistant/components/tilt_ble/translations/sv.json new file mode 100644 index 00000000000..7606ba7df45 --- /dev/null +++ b/homeassistant/components/tilt_ble/translations/sv.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", + "no_devices_found": "Inga enheter hittades i n\u00e4tverket" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vill du konfigurera {name}?" + }, + "user": { + "data": { + "address": "Enhet" + }, + "description": "V\u00e4lj en enhet att konfigurera" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tilt_ble/translations/zh-Hant.json b/homeassistant/components/tilt_ble/translations/zh-Hant.json new file mode 100644 index 00000000000..d4eaa8cb41f --- /dev/null +++ b/homeassistant/components/tilt_ble/translations/zh-Hant.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e" + }, + "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/unifiprotect/translations/sv.json b/homeassistant/components/unifiprotect/translations/sv.json index 81afcc7c14b..98b1862cc3d 100644 --- a/homeassistant/components/unifiprotect/translations/sv.json +++ b/homeassistant/components/unifiprotect/translations/sv.json @@ -42,11 +42,16 @@ } }, "options": { + "error": { + "invalid_mac_list": "M\u00e5ste vara en lista \u00f6ver MAC-adresser separerade med kommatecken" + }, "step": { "init": { "data": { "all_updates": "Realtidsm\u00e4tningar (VARNING: \u00d6kar CPU-anv\u00e4ndningen avsev\u00e4rt)", "disable_rtsp": "Inaktivera RTSP-str\u00f6mmen", + "ignored_devices": "Kommaseparerad lista \u00f6ver MAC-adresser f\u00f6r enheter att ignorera", + "max_media": "Max antal h\u00e4ndelser som ska laddas f\u00f6r Media Browser (\u00f6kar RAM-anv\u00e4ndning)", "override_connection_host": "\u00c5sidos\u00e4tt anslutningsv\u00e4rd" }, "description": "Alternativet Realtidsm\u00e4tv\u00e4rden b\u00f6r endast aktiveras om du har aktiverat diagnostiksensorerna och vill att de ska uppdateras i realtid. Om det inte \u00e4r aktiverat kommer de bara att uppdateras en g\u00e5ng var 15:e minut.", diff --git a/homeassistant/components/volvooncall/translations/bg.json b/homeassistant/components/volvooncall/translations/bg.json index c0ccf23f5b5..aa879c9649c 100644 --- a/homeassistant/components/volvooncall/translations/bg.json +++ b/homeassistant/components/volvooncall/translations/bg.json @@ -1,7 +1,22 @@ { "config": { "abort": { + "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \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\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" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "region": "\u0420\u0435\u0433\u0438\u043e\u043d", + "scandinavian_miles": "\u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 \u0441\u043a\u0430\u043d\u0434\u0438\u043d\u0430\u0432\u0441\u043a\u0438 \u043c\u0438\u043b\u0438", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/volvooncall/translations/sv.json b/homeassistant/components/volvooncall/translations/sv.json new file mode 100644 index 00000000000..03658c137df --- /dev/null +++ b/homeassistant/components/volvooncall/translations/sv.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats", + "reauth_successful": "\u00c5terautentisering lyckades" + }, + "error": { + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "mutable": "Till\u00e5t fj\u00e4rrstart / l\u00e5s / etc.", + "password": "L\u00f6senord", + "region": "Region", + "scandinavian_miles": "Anv\u00e4nd Skandinaviska mil", + "username": "Anv\u00e4ndarnamn" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfigurering av Volvo On Call-plattformen med YAML tas bort i en framtida version av Home Assistant. \n\n Din befintliga konfiguration har automatiskt importerats till anv\u00e4ndargr\u00e4nssnittet. Ta bort YAML-konfigurationen fr\u00e5n filen configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", + "title": "Volvo On Call YAML-konfigurationen tas bort" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/select.sv.json b/homeassistant/components/xiaomi_miio/translations/select.sv.json index 47c2ffaa90d..0449b235891 100644 --- a/homeassistant/components/xiaomi_miio/translations/select.sv.json +++ b/homeassistant/components/xiaomi_miio/translations/select.sv.json @@ -1,9 +1,19 @@ { "state": { + "xiaomi_miio__display_orientation": { + "forward": "Fram\u00e5t", + "left": "V\u00e4nster", + "right": "H\u00f6ger" + }, "xiaomi_miio__led_brightness": { "bright": "Ljus", "dim": "Dimma", "off": "Av" + }, + "xiaomi_miio__ptc_level": { + "high": "H\u00f6g", + "low": "L\u00e5g", + "medium": "Medium" } } } \ No newline at end of file diff --git a/homeassistant/components/yalexs_ble/translations/sv.json b/homeassistant/components/yalexs_ble/translations/sv.json new file mode 100644 index 00000000000..799928c0c5c --- /dev/null +++ b/homeassistant/components/yalexs_ble/translations/sv.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", + "no_devices_found": "Inga enheter hittades i n\u00e4tverket", + "no_unconfigured_devices": "Inga okonfigurerade enheter hittades." + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "invalid_key_format": "Offlinenyckeln m\u00e5ste vara en 32-byte hex-str\u00e4ng.", + "invalid_key_index": "Offline-nyckelplatsen m\u00e5ste vara ett heltal mellan 0 och 255.", + "unknown": "Ov\u00e4ntat fel" + }, + "flow_title": "{name}", + "step": { + "integration_discovery_confirm": { + "description": "Vill du konfigurera {name} via Bluetooth med adressen {address} ?" + }, + "user": { + "data": { + "address": "Bluetooth-adress", + "key": "Offlinenyckel (32-byte hex-str\u00e4ng)", + "slot": "Offlinenyckelplats (heltal mellan 0 och 255)" + }, + "description": "Se dokumentationen f\u00f6r hur du hittar offlinenyckeln." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/translations/bg.json b/homeassistant/components/zha/translations/bg.json index 75741130cc8..e3d72388dee 100644 --- a/homeassistant/components/zha/translations/bg.json +++ b/homeassistant/components/zha/translations/bg.json @@ -11,6 +11,9 @@ "confirm": { "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 {name}?" }, + "confirm_hardware": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 {name}?" + }, "pick_radio": { "data": { "radio_type": "\u0422\u0438\u043f \u0440\u0430\u0434\u0438\u043e" diff --git a/homeassistant/components/zha/translations/sv.json b/homeassistant/components/zha/translations/sv.json index 57be69d4f9f..ca9fc90f5e9 100644 --- a/homeassistant/components/zha/translations/sv.json +++ b/homeassistant/components/zha/translations/sv.json @@ -6,13 +6,64 @@ "usb_probe_failed": "Det gick inte att unders\u00f6ka usb-enheten" }, "error": { - "cannot_connect": "Det gick inte att ansluta till ZHA enhet." + "cannot_connect": "Det gick inte att ansluta till ZHA enhet.", + "invalid_backup_json": "Ogiltig JSON f\u00f6r s\u00e4kerhetskopiering" }, "flow_title": "{name}", "step": { + "choose_automatic_backup": { + "data": { + "choose_automatic_backup": "V\u00e4lj en automatisk s\u00e4kerhetskopiering" + }, + "description": "\u00c5terst\u00e4ll dina n\u00e4tverksinst\u00e4llningar fr\u00e5n en automatisk s\u00e4kerhetskopiering", + "title": "\u00c5terst\u00e4ll automatisk s\u00e4kerhetskopiering" + }, + "choose_formation_strategy": { + "description": "V\u00e4lj n\u00e4tverksinst\u00e4llningar f\u00f6r din radio.", + "menu_options": { + "choose_automatic_backup": "\u00c5terst\u00e4ll en automatisk s\u00e4kerhetskopia", + "form_new_network": "Radera n\u00e4tverksinst\u00e4llningar och skapa ett nytt n\u00e4tverk", + "reuse_settings": "Beh\u00e5ll radion\u00e4tverksinst\u00e4llningar", + "upload_manual_backup": "Ladda upp en manuell s\u00e4kerhetskopia" + }, + "title": "Bildande av n\u00e4tverk" + }, + "choose_serial_port": { + "data": { + "path": "Seriell enhetsv\u00e4g" + }, + "description": "V\u00e4lj serieporten f\u00f6r din Zigbee-radio", + "title": "V\u00e4lj en seriell port" + }, "confirm": { "description": "Vill du konfigurera {name}?" }, + "confirm_hardware": { + "description": "Vill du konfigurera {name}?" + }, + "manual_pick_radio_type": { + "data": { + "radio_type": "Radiotyp" + }, + "description": "V\u00e4lj din Zigbee-radiotyp", + "title": "Radiotyp" + }, + "manual_port_config": { + "data": { + "baudrate": "port hastighet", + "flow_control": "datafl\u00f6deskontroll", + "path": "Seriell enhetsv\u00e4g" + }, + "description": "Ange inst\u00e4llningarna f\u00f6r serieporten", + "title": "Inst\u00e4llningar f\u00f6r seriell port" + }, + "maybe_confirm_ezsp_restore": { + "data": { + "overwrite_coordinator_ieee": "Byt ut radions IEEE-adress permanent" + }, + "description": "Din s\u00e4kerhetskopia har en annan IEEE-adress \u00e4n din radio. F\u00f6r att ditt n\u00e4tverk ska fungera korrekt b\u00f6r IEEE-adressen f\u00f6r din radio ocks\u00e5 \u00e4ndras. \n\n Detta \u00e4r en permanent \u00e5tg\u00e4rd.", + "title": "Skriv \u00f6ver Radio IEEE-adress" + }, "pick_radio": { "data": { "radio_type": "Radiotyp" @@ -29,6 +80,13 @@ "description": "Ange portspecifika inst\u00e4llningar", "title": "Inst\u00e4llningar" }, + "upload_manual_backup": { + "data": { + "uploaded_backup_file": "Ladda upp en fil" + }, + "description": "\u00c5terst\u00e4ll dina n\u00e4tverksinst\u00e4llningar fr\u00e5n en uppladdad backup-JSON-fil. Du kan ladda ner en fr\u00e5n en annan ZHA-installation fr\u00e5n **N\u00e4tverksinst\u00e4llningar**, eller anv\u00e4nda en Zigbee2MQTT `coordinator_backup.json`-fil.", + "title": "Ladda upp en manuell s\u00e4kerhetskopia" + }, "user": { "data": { "path": "Seriell enhetsv\u00e4g" @@ -111,5 +169,77 @@ "remote_button_short_release": "\"{subtype}\"-knappen sl\u00e4ppt", "remote_button_triple_press": "\"{subtype}\"-knappen trippelklickades" } + }, + "options": { + "abort": { + "not_zha_device": "Den h\u00e4r enheten \u00e4r inte en zha-enhet", + "single_instance_allowed": "Endast en enda konfiguration av ZHA \u00e4r till\u00e5ten.", + "usb_probe_failed": "Det gick inte att unders\u00f6ka usb-enheten" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta till ZHA enhet.", + "invalid_backup_json": "Ogiltig JSON f\u00f6r s\u00e4kerhetskopiering" + }, + "flow_title": "{name}", + "step": { + "choose_automatic_backup": { + "data": { + "choose_automatic_backup": "V\u00e4lj en automatisk s\u00e4kerhetskopiering" + }, + "description": "\u00c5terst\u00e4ll dina n\u00e4tverksinst\u00e4llningar fr\u00e5n en automatisk s\u00e4kerhetskopiering", + "title": "\u00c5terst\u00e4ll automatisk s\u00e4kerhetskopiering" + }, + "choose_formation_strategy": { + "description": "V\u00e4lj n\u00e4tverksinst\u00e4llningar f\u00f6r din radio.", + "menu_options": { + "choose_automatic_backup": "\u00c5terst\u00e4ll en automatisk s\u00e4kerhetskopia", + "form_new_network": "Radera n\u00e4tverksinst\u00e4llningar och skapa ett nytt n\u00e4tverk", + "reuse_settings": "Beh\u00e5ll radion\u00e4tverksinst\u00e4llningar", + "upload_manual_backup": "Ladda upp en manuell s\u00e4kerhetskopia" + }, + "title": "Bildande av n\u00e4tverk" + }, + "choose_serial_port": { + "data": { + "path": "Seriell enhetsv\u00e4g" + }, + "description": "V\u00e4lj serieporten f\u00f6r din Zigbee-radio", + "title": "V\u00e4lj en seriell port" + }, + "init": { + "description": "ZHA kommer att stoppas. Vill du forts\u00e4tta?", + "title": "Konfigurera om ZHA" + }, + "manual_pick_radio_type": { + "data": { + "radio_type": "Radiotyp" + }, + "description": "V\u00e4lj din Zigbee-radiotyp", + "title": "Radiotyp" + }, + "manual_port_config": { + "data": { + "baudrate": "port hastighet", + "flow_control": "datafl\u00f6deskontroll", + "path": "Seriell enhetsv\u00e4g" + }, + "description": "Ange inst\u00e4llningarna f\u00f6r serieporten", + "title": "Inst\u00e4llningar f\u00f6r seriell port" + }, + "maybe_confirm_ezsp_restore": { + "data": { + "overwrite_coordinator_ieee": "Byt ut radions IEEE-adress permanent" + }, + "description": "Din s\u00e4kerhetskopia har en annan IEEE-adress \u00e4n din radio. F\u00f6r att ditt n\u00e4tverk ska fungera korrekt b\u00f6r IEEE-adressen f\u00f6r din radio ocks\u00e5 \u00e4ndras. \n\n Detta \u00e4r en permanent \u00e5tg\u00e4rd.", + "title": "Skriv \u00f6ver Radio IEEE-adress" + }, + "upload_manual_backup": { + "data": { + "uploaded_backup_file": "Ladda upp en fil" + }, + "description": "\u00c5terst\u00e4ll dina n\u00e4tverksinst\u00e4llningar fr\u00e5n en uppladdad backup-JSON-fil. Du kan ladda ner en fr\u00e5n en annan ZHA-installation fr\u00e5n **N\u00e4tverksinst\u00e4llningar**, eller anv\u00e4nda en Zigbee2MQTT `coordinator_backup.json`-fil.", + "title": "Ladda upp en manuell s\u00e4kerhetskopia" + } + } } } \ No newline at end of file From fe04af87985dcbfae0239d7bd45ff7cab73e030e Mon Sep 17 00:00:00 2001 From: rlippmann <70883373+rlippmann@users.noreply.github.com> Date: Thu, 8 Sep 2022 21:01:43 -0400 Subject: [PATCH 223/955] Fix issue #77920 - ecobee remote sensors not updating (#78035) --- homeassistant/components/ecobee/sensor.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/ecobee/sensor.py b/homeassistant/components/ecobee/sensor.py index a7d5639ae2c..9d8793efc29 100644 --- a/homeassistant/components/ecobee/sensor.py +++ b/homeassistant/components/ecobee/sensor.py @@ -29,7 +29,7 @@ from .const import DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER class EcobeeSensorEntityDescriptionMixin: """Represent the required ecobee entity description attributes.""" - runtime_key: str + runtime_key: str | None @dataclass @@ -46,7 +46,7 @@ SENSOR_TYPES: tuple[EcobeeSensorEntityDescription, ...] = ( native_unit_of_measurement=TEMP_FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, - runtime_key="actualTemperature", + runtime_key=None, ), EcobeeSensorEntityDescription( key="humidity", @@ -54,7 +54,7 @@ SENSOR_TYPES: tuple[EcobeeSensorEntityDescription, ...] = ( native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, - runtime_key="actualHumidity", + runtime_key=None, ), EcobeeSensorEntityDescription( key="co2PPM", @@ -194,6 +194,11 @@ class EcobeeSensor(SensorEntity): for item in sensor["capability"]: if item["type"] != self.entity_description.key: continue - thermostat = self.data.ecobee.get_thermostat(self.index) - self._state = thermostat["runtime"][self.entity_description.runtime_key] + if self.entity_description.runtime_key is None: + self._state = item["value"] + else: + thermostat = self.data.ecobee.get_thermostat(self.index) + self._state = thermostat["runtime"][ + self.entity_description.runtime_key + ] break From 0e734e629c762c5970cb399f9f6f95a8341c5d1b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 9 Sep 2022 01:47:33 -0400 Subject: [PATCH 224/955] Handle missing supported brands (#78090) --- homeassistant/components/websocket_api/commands.py | 3 +++ tests/components/websocket_api/test_commands.py | 8 +++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index 1761323a60d..d4596619241 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -722,6 +722,9 @@ async def handle_supported_brands( for int_or_exc in ints_or_excs.values(): if isinstance(int_or_exc, Exception): raise int_or_exc + # Happens if a custom component without supported brands overrides a built-in one with supported brands + if "supported_brands" not in int_or_exc.manifest: + continue data[int_or_exc.domain] = int_or_exc.manifest["supported_brands"] connection.send_result(msg["id"], data) diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index fe748e2c47c..354a4edeb0d 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -1760,6 +1760,12 @@ async def test_validate_config_invalid(websocket_client, key, config, error): async def test_supported_brands(hass, websocket_client): """Test supported brands.""" + # Custom components without supported brands that override a built-in component with + # supported brand will still be listed in HAS_SUPPORTED_BRANDS and should be ignored. + mock_integration( + hass, + MockModule("override_without_brands"), + ) mock_integration( hass, MockModule("test", partial_manifest={"supported_brands": {"hello": "World"}}), @@ -1773,7 +1779,7 @@ async def test_supported_brands(hass, websocket_client): with patch( "homeassistant.generated.supported_brands.HAS_SUPPORTED_BRANDS", - ("abcd", "test"), + ("abcd", "test", "override_without_brands"), ): await websocket_client.send_json({"id": 7, "type": "supported_brands"}) msg = await websocket_client.receive_json() From eb28d7188b87b4022c45b53584b8f88a4e26e190 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 9 Sep 2022 08:06:14 +0200 Subject: [PATCH 225/955] Fix DB migration to schema version 29 (#78037) * Fix DB migration to schema version 29 * Fix misspelled constants --- homeassistant/components/recorder/db_schema.py | 12 ++++++------ homeassistant/components/recorder/migration.py | 2 +- homeassistant/components/recorder/statistics.py | 10 ++++++++-- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/recorder/db_schema.py b/homeassistant/components/recorder/db_schema.py index 4777eeb500e..40c0453ea0b 100644 --- a/homeassistant/components/recorder/db_schema.py +++ b/homeassistant/components/recorder/db_schema.py @@ -104,10 +104,10 @@ class FAST_PYSQLITE_DATETIME(sqlite.DATETIME): # type: ignore[misc] return lambda value: None if value is None else ciso8601.parse_datetime(value) -JSON_VARIENT_CAST = Text().with_variant( +JSON_VARIANT_CAST = Text().with_variant( postgresql.JSON(none_as_null=True), "postgresql" ) -JSONB_VARIENT_CAST = Text().with_variant( +JSONB_VARIANT_CAST = Text().with_variant( postgresql.JSONB(none_as_null=True), "postgresql" ) DATETIME_TYPE = ( @@ -590,17 +590,17 @@ class StatisticsRuns(Base): # type: ignore[misc,valid-type] EVENT_DATA_JSON = type_coerce( - EventData.shared_data.cast(JSONB_VARIENT_CAST), JSONLiteral(none_as_null=True) + EventData.shared_data.cast(JSONB_VARIANT_CAST), JSONLiteral(none_as_null=True) ) OLD_FORMAT_EVENT_DATA_JSON = type_coerce( - Events.event_data.cast(JSONB_VARIENT_CAST), JSONLiteral(none_as_null=True) + Events.event_data.cast(JSONB_VARIANT_CAST), JSONLiteral(none_as_null=True) ) SHARED_ATTRS_JSON = type_coerce( - StateAttributes.shared_attrs.cast(JSON_VARIENT_CAST), JSON(none_as_null=True) + StateAttributes.shared_attrs.cast(JSON_VARIANT_CAST), JSON(none_as_null=True) ) OLD_FORMAT_ATTRS_JSON = type_coerce( - States.attributes.cast(JSON_VARIENT_CAST), JSON(none_as_null=True) + States.attributes.cast(JSON_VARIANT_CAST), JSON(none_as_null=True) ) ENTITY_ID_IN_EVENT: Column = EVENT_DATA_JSON["entity_id"] diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 6e4a67c9da5..ab9b93de5e5 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -636,7 +636,7 @@ def _apply_update( # noqa: C901 fake_start_time += timedelta(minutes=5) # When querying the database, be careful to only explicitly query for columns - # which were present in schema version 21. If querying the table, SQLAlchemy + # which were present in schema version 22. If querying the table, SQLAlchemy # will refer to future columns. with session_scope(session=session_maker()) as session: for sum_statistic in session.query(StatisticsMeta.id).filter_by( diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 0a9e29747a9..a1ab58ee011 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -420,6 +420,9 @@ def delete_statistics_duplicates(hass: HomeAssistant, session: Session) -> None: def _find_statistics_meta_duplicates(session: Session) -> list[int]: """Find duplicated statistics_meta.""" + # When querying the database, be careful to only explicitly query for columns + # which were present in schema version 29. If querying the table, SQLAlchemy + # will refer to future columns. subquery = ( session.query( StatisticsMeta.statistic_id, @@ -430,7 +433,7 @@ def _find_statistics_meta_duplicates(session: Session) -> list[int]: .subquery() ) query = ( - session.query(StatisticsMeta) + session.query(StatisticsMeta.statistic_id, StatisticsMeta.id) .outerjoin( subquery, (subquery.c.statistic_id == StatisticsMeta.statistic_id), @@ -473,7 +476,10 @@ def _delete_statistics_meta_duplicates(session: Session) -> int: def delete_statistics_meta_duplicates(session: Session) -> None: - """Identify and delete duplicated statistics_meta.""" + """Identify and delete duplicated statistics_meta. + + This is used when migrating from schema version 28 to schema version 29. + """ deleted_statistics_rows = _delete_statistics_meta_duplicates(session) if deleted_statistics_rows: _LOGGER.info( From 7ff23506fe33af647a48a0c4d681cdbc835dca01 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 9 Sep 2022 08:57:14 +0200 Subject: [PATCH 226/955] Use new enums in cast (#77946) Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> --- homeassistant/components/cast/__init__.py | 6 +- homeassistant/components/cast/media_player.py | 57 +++++++++---------- homeassistant/components/lovelace/cast.py | 20 ++++--- homeassistant/components/plex/cast.py | 9 ++- pylint/plugins/hass_enforce_type_hints.py | 4 +- tests/components/lovelace/test_cast.py | 11 ++-- 6 files changed, 55 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/cast/__init__.py b/homeassistant/components/cast/__init__.py index 63f0693b01a..467678ba82b 100644 --- a/homeassistant/components/cast/__init__.py +++ b/homeassistant/components/cast/__init__.py @@ -7,7 +7,7 @@ from typing import Protocol from pychromecast import Chromecast import voluptuous as vol -from homeassistant.components.media_player import BrowseMedia +from homeassistant.components.media_player import BrowseMedia, MediaType from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant @@ -74,7 +74,7 @@ class CastProtocol(Protocol): async def async_browse_media( self, hass: HomeAssistant, - media_content_type: str, + media_content_type: MediaType | str, media_content_id: str, cast_type: str, ) -> BrowseMedia | None: @@ -88,7 +88,7 @@ class CastProtocol(Protocol): hass: HomeAssistant, cast_entity_id: str, chromecast: Chromecast, - media_type: str, + media_type: MediaType | str, media_id: str, ) -> bool: """Play media. diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index 75d3de06856..edd8e0331d9 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -7,7 +7,7 @@ from contextlib import suppress from datetime import datetime import json import logging -from typing import Any +from typing import TYPE_CHECKING, Any import pychromecast from pychromecast.controllers.homeassistant import HomeAssistantController @@ -29,29 +29,21 @@ import yarl from homeassistant.components import media_source, zeroconf from homeassistant.components.media_player import ( + ATTR_MEDIA_EXTRA, BrowseError, BrowseMedia, + MediaClass, MediaPlayerDeviceClass, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, + MediaType, async_process_play_media_url, ) -from homeassistant.components.media_player.const import ( - ATTR_MEDIA_EXTRA, - MEDIA_CLASS_DIRECTORY, - MEDIA_TYPE_MOVIE, - MEDIA_TYPE_MUSIC, - MEDIA_TYPE_TVSHOW, -) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CAST_APP_ID_HOMEASSISTANT_LOVELACE, EVENT_HOMEASSISTANT_STOP, - STATE_BUFFERING, - STATE_IDLE, - STATE_OFF, - STATE_PAUSED, - STATE_PLAYING, ) from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError @@ -83,6 +75,9 @@ from .helpers import ( parse_playlist, ) +if TYPE_CHECKING: + from . import CastProtocol + _LOGGER = logging.getLogger(__name__) APP_IDS_UNRELIABLE_MEDIA_INFO = ("Netflix",) @@ -590,7 +585,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): return BrowseMedia( title="Cast", - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_id="", media_content_type="", can_play=False, @@ -599,7 +594,9 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): ) async def async_browse_media( - self, media_content_type: str | None = None, media_content_id: str | None = None + self, + media_content_type: MediaType | str | None = None, + media_content_id: str | None = None, ) -> BrowseMedia: """Implement the websocket media browsing helper.""" content_filter = None @@ -619,6 +616,8 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): if media_content_id is None: return await self._async_root_payload(content_filter) + platform: CastProtocol + assert media_content_type is not None for platform in self.hass.data[CAST_DOMAIN]["cast_platform"].values(): browse_media = await platform.async_browse_media( self.hass, @@ -634,7 +633,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): ) async def async_play_media( - self, media_type: str, media_id: str, **kwargs: Any + self, media_type: MediaType | str, media_id: str, **kwargs: Any ) -> None: """Play a piece of media.""" chromecast = self._get_chromecast() @@ -774,27 +773,27 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): return (media_status, media_status_received) @property - def state(self) -> str | None: + def state(self) -> MediaPlayerState | None: """Return the state of the player.""" # The lovelace app loops media to prevent timing out, don't show that if self.app_id == CAST_APP_ID_HOMEASSISTANT_LOVELACE: - return STATE_PLAYING + return MediaPlayerState.PLAYING if (media_status := self._media_status()[0]) is not None: if media_status.player_state == MEDIA_PLAYER_STATE_PLAYING: - return STATE_PLAYING + return MediaPlayerState.PLAYING if media_status.player_state == MEDIA_PLAYER_STATE_BUFFERING: - return STATE_BUFFERING + return MediaPlayerState.BUFFERING if media_status.player_is_paused: - return STATE_PAUSED + return MediaPlayerState.PAUSED if media_status.player_is_idle: - return STATE_IDLE + return MediaPlayerState.IDLE if self.app_id is not None and self.app_id != pychromecast.IDLE_APP_ID: if self.app_id in APP_IDS_UNRELIABLE_MEDIA_INFO: # Some apps don't report media status, show the player as playing - return STATE_PLAYING - return STATE_IDLE + return MediaPlayerState.PLAYING + return MediaPlayerState.IDLE if self._chromecast is not None and self._chromecast.is_idle: - return STATE_OFF + return MediaPlayerState.OFF return None @property @@ -807,7 +806,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): return media_status.content_id if media_status else None @property - def media_content_type(self) -> str | None: + def media_content_type(self) -> MediaType | None: """Content type of current playing media.""" # The lovelace app loops media to prevent timing out, don't show that if self.app_id == CAST_APP_ID_HOMEASSISTANT_LOVELACE: @@ -815,11 +814,11 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): if (media_status := self._media_status()[0]) is None: return None if media_status.media_is_tvshow: - return MEDIA_TYPE_TVSHOW + return MediaType.TVSHOW if media_status.media_is_movie: - return MEDIA_TYPE_MOVIE + return MediaType.MOVIE if media_status.media_is_musictrack: - return MEDIA_TYPE_MUSIC + return MediaType.MUSIC return None @property diff --git a/homeassistant/components/lovelace/cast.py b/homeassistant/components/lovelace/cast.py index bd06d142bd3..73645e66ddb 100644 --- a/homeassistant/components/lovelace/cast.py +++ b/homeassistant/components/lovelace/cast.py @@ -12,8 +12,12 @@ from homeassistant.components.cast.home_assistant_cast import ( NO_URL_AVAILABLE_ERROR, SERVICE_SHOW_VIEW, ) -from homeassistant.components.media_player import BrowseError, BrowseMedia -from homeassistant.components.media_player.const import MEDIA_CLASS_APP +from homeassistant.components.media_player import ( + BrowseError, + BrowseMedia, + MediaClass, + MediaType, +) from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError @@ -34,7 +38,7 @@ async def async_get_media_browser_root_object( return [ BrowseMedia( title="Dashboards", - media_class=MEDIA_CLASS_APP, + media_class=MediaClass.APP, media_content_id="", media_content_type=DOMAIN, thumbnail="https://brands.home-assistant.io/_/lovelace/logo.png", @@ -46,7 +50,7 @@ async def async_get_media_browser_root_object( async def async_browse_media( hass: HomeAssistant, - media_content_type: str, + media_content_type: MediaType | str, media_content_id: str, cast_type: str, ) -> BrowseMedia | None: @@ -64,7 +68,7 @@ async def async_browse_media( children = [ BrowseMedia( title="Default", - media_class=MEDIA_CLASS_APP, + media_class=MediaClass.APP, media_content_id=DEFAULT_DASHBOARD, media_content_type=DOMAIN, thumbnail="https://brands.home-assistant.io/_/lovelace/logo.png", @@ -96,7 +100,7 @@ async def async_browse_media( children.append( BrowseMedia( title=view["title"], - media_class=MEDIA_CLASS_APP, + media_class=MediaClass.APP, media_content_id=f'{info["url_path"]}/{view["path"]}', media_content_type=DOMAIN, thumbnail="https://brands.home-assistant.io/_/lovelace/logo.png", @@ -114,7 +118,7 @@ async def async_play_media( hass: HomeAssistant, cast_entity_id: str, chromecast: Chromecast, - media_type: str, + media_type: MediaType | str, media_id: str, ) -> bool: """Play media.""" @@ -195,7 +199,7 @@ def _item_from_info(info: dict) -> BrowseMedia: """Convert dashboard info to browse item.""" return BrowseMedia( title=info["title"], - media_class=MEDIA_CLASS_APP, + media_class=MediaClass.APP, media_content_id=info["url_path"], media_content_type=DOMAIN, thumbnail="https://brands.home-assistant.io/_/lovelace/logo.png", diff --git a/homeassistant/components/plex/cast.py b/homeassistant/components/plex/cast.py index c85b6ee3a78..720875bec6b 100644 --- a/homeassistant/components/plex/cast.py +++ b/homeassistant/components/plex/cast.py @@ -5,8 +5,7 @@ from pychromecast import Chromecast from pychromecast.controllers.plex import PlexController from homeassistant.components.cast.const import DOMAIN as CAST_DOMAIN -from homeassistant.components.media_player import BrowseMedia -from homeassistant.components.media_player.const import MEDIA_CLASS_APP +from homeassistant.components.media_player import BrowseMedia, MediaClass, MediaType from homeassistant.core import HomeAssistant from . import async_browse_media as async_browse_plex_media, is_plex_media_id @@ -20,7 +19,7 @@ async def async_get_media_browser_root_object( return [ BrowseMedia( title="Plex", - media_class=MEDIA_CLASS_APP, + media_class=MediaClass.APP, media_content_id="", media_content_type="plex", thumbnail="https://brands.home-assistant.io/_/plex/logo.png", @@ -32,7 +31,7 @@ async def async_get_media_browser_root_object( async def async_browse_media( hass: HomeAssistant, - media_content_type: str, + media_content_type: MediaType | str, media_content_id: str, cast_type: str, ) -> BrowseMedia | None: @@ -61,7 +60,7 @@ async def async_play_media( hass: HomeAssistant, cast_entity_id: str, chromecast: Chromecast, - media_type: str, + media_type: MediaType | str, media_id: str, ) -> bool: """Play media.""" diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 379c19b044d..1cfb36f9398 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -203,7 +203,7 @@ _FUNCTION_MATCH: dict[str, list[TypeHintMatch]] = { function_name="async_browse_media", arg_types={ 0: "HomeAssistant", - 1: "str", + 1: "MediaType | str", 2: "str", 3: "str", }, @@ -215,7 +215,7 @@ _FUNCTION_MATCH: dict[str, list[TypeHintMatch]] = { 0: "HomeAssistant", 1: "str", 2: "Chromecast", - 3: "str", + 3: "MediaType | str", 4: "str", }, return_type="bool", diff --git a/tests/components/lovelace/test_cast.py b/tests/components/lovelace/test_cast.py index 6f6035b54b6..4945a2cab83 100644 --- a/tests/components/lovelace/test_cast.py +++ b/tests/components/lovelace/test_cast.py @@ -5,6 +5,7 @@ from unittest.mock import patch import pytest from homeassistant.components.lovelace import cast as lovelace_cast +from homeassistant.components.media_player import MediaClass from homeassistant.config import async_process_ha_core_config from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component @@ -71,7 +72,7 @@ async def test_root_object(hass): assert len(root) == 1 item = root[0] assert item.title == "Dashboards" - assert item.media_class == lovelace_cast.MEDIA_CLASS_APP + assert item.media_class == MediaClass.APP assert item.media_content_id == "" assert item.media_content_type == lovelace_cast.DOMAIN assert item.thumbnail == "https://brands.home-assistant.io/_/lovelace/logo.png" @@ -106,7 +107,7 @@ async def test_browse_media(hass, mock_yaml_dashboard, mock_https_url): child_1 = top_level_items.children[0] assert child_1.title == "Default" - assert child_1.media_class == lovelace_cast.MEDIA_CLASS_APP + assert child_1.media_class == MediaClass.APP assert child_1.media_content_id == lovelace_cast.DEFAULT_DASHBOARD assert child_1.media_content_type == lovelace_cast.DOMAIN assert child_1.thumbnail == "https://brands.home-assistant.io/_/lovelace/logo.png" @@ -115,7 +116,7 @@ async def test_browse_media(hass, mock_yaml_dashboard, mock_https_url): child_2 = top_level_items.children[1] assert child_2.title == "YAML Title" - assert child_2.media_class == lovelace_cast.MEDIA_CLASS_APP + assert child_2.media_class == MediaClass.APP assert child_2.media_content_id == "yaml-with-views" assert child_2.media_content_type == lovelace_cast.DOMAIN assert child_2.thumbnail == "https://brands.home-assistant.io/_/lovelace/logo.png" @@ -130,7 +131,7 @@ async def test_browse_media(hass, mock_yaml_dashboard, mock_https_url): grandchild_1 = child_2.children[0] assert grandchild_1.title == "Hello" - assert grandchild_1.media_class == lovelace_cast.MEDIA_CLASS_APP + assert grandchild_1.media_class == MediaClass.APP assert grandchild_1.media_content_id == "yaml-with-views/0" assert grandchild_1.media_content_type == lovelace_cast.DOMAIN assert ( @@ -141,7 +142,7 @@ async def test_browse_media(hass, mock_yaml_dashboard, mock_https_url): grandchild_2 = child_2.children[1] assert grandchild_2.title == "second-view" - assert grandchild_2.media_class == lovelace_cast.MEDIA_CLASS_APP + assert grandchild_2.media_class == MediaClass.APP assert grandchild_2.media_content_id == "yaml-with-views/second-view" assert grandchild_2.media_content_type == lovelace_cast.DOMAIN assert ( From 8b3ce8c58c584de8ef0643453b1bf14c8c4f561b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 9 Sep 2022 09:03:59 +0200 Subject: [PATCH 227/955] Use new constants in dlna_dmr media player (#78045) --- homeassistant/components/dlna_dmr/const.py | 140 +++++++------- .../components/dlna_dmr/media_player.py | 49 ++--- .../components/dlna_dmr/test_media_player.py | 173 +++++++++--------- 3 files changed, 179 insertions(+), 183 deletions(-) diff --git a/homeassistant/components/dlna_dmr/const.py b/homeassistant/components/dlna_dmr/const.py index 4f9982061fb..4cea664f058 100644 --- a/homeassistant/components/dlna_dmr/const.py +++ b/homeassistant/components/dlna_dmr/const.py @@ -7,7 +7,7 @@ from typing import Final from async_upnp_client.profiles.dlna import PlayMode as _PlayMode -from homeassistant.components.media_player import const as _mp_const +from homeassistant.components.media_player import MediaType, RepeatMode LOGGER = logging.getLogger(__package__) @@ -28,66 +28,66 @@ PROTOCOL_ANY: Final = "*" STREAMABLE_PROTOCOLS: Final = [PROTOCOL_HTTP, PROTOCOL_RTSP, PROTOCOL_ANY] # Map UPnP class to media_player media_content_type -MEDIA_TYPE_MAP: Mapping[str, str] = { - "object": _mp_const.MEDIA_TYPE_URL, - "object.item": _mp_const.MEDIA_TYPE_URL, - "object.item.imageItem": _mp_const.MEDIA_TYPE_IMAGE, - "object.item.imageItem.photo": _mp_const.MEDIA_TYPE_IMAGE, - "object.item.audioItem": _mp_const.MEDIA_TYPE_MUSIC, - "object.item.audioItem.musicTrack": _mp_const.MEDIA_TYPE_MUSIC, - "object.item.audioItem.audioBroadcast": _mp_const.MEDIA_TYPE_MUSIC, - "object.item.audioItem.audioBook": _mp_const.MEDIA_TYPE_PODCAST, - "object.item.videoItem": _mp_const.MEDIA_TYPE_VIDEO, - "object.item.videoItem.movie": _mp_const.MEDIA_TYPE_MOVIE, - "object.item.videoItem.videoBroadcast": _mp_const.MEDIA_TYPE_TVSHOW, - "object.item.videoItem.musicVideoClip": _mp_const.MEDIA_TYPE_VIDEO, - "object.item.playlistItem": _mp_const.MEDIA_TYPE_PLAYLIST, - "object.item.textItem": _mp_const.MEDIA_TYPE_URL, - "object.item.bookmarkItem": _mp_const.MEDIA_TYPE_URL, - "object.item.epgItem": _mp_const.MEDIA_TYPE_EPISODE, - "object.item.epgItem.audioProgram": _mp_const.MEDIA_TYPE_EPISODE, - "object.item.epgItem.videoProgram": _mp_const.MEDIA_TYPE_EPISODE, - "object.container": _mp_const.MEDIA_TYPE_PLAYLIST, - "object.container.person": _mp_const.MEDIA_TYPE_ARTIST, - "object.container.person.musicArtist": _mp_const.MEDIA_TYPE_ARTIST, - "object.container.playlistContainer": _mp_const.MEDIA_TYPE_PLAYLIST, - "object.container.album": _mp_const.MEDIA_TYPE_ALBUM, - "object.container.album.musicAlbum": _mp_const.MEDIA_TYPE_ALBUM, - "object.container.album.photoAlbum": _mp_const.MEDIA_TYPE_ALBUM, - "object.container.genre": _mp_const.MEDIA_TYPE_GENRE, - "object.container.genre.musicGenre": _mp_const.MEDIA_TYPE_GENRE, - "object.container.genre.movieGenre": _mp_const.MEDIA_TYPE_GENRE, - "object.container.channelGroup": _mp_const.MEDIA_TYPE_CHANNELS, - "object.container.channelGroup.audioChannelGroup": _mp_const.MEDIA_TYPE_CHANNELS, - "object.container.channelGroup.videoChannelGroup": _mp_const.MEDIA_TYPE_CHANNELS, - "object.container.epgContainer": _mp_const.MEDIA_TYPE_TVSHOW, - "object.container.storageSystem": _mp_const.MEDIA_TYPE_PLAYLIST, - "object.container.storageVolume": _mp_const.MEDIA_TYPE_PLAYLIST, - "object.container.storageFolder": _mp_const.MEDIA_TYPE_PLAYLIST, - "object.container.bookmarkFolder": _mp_const.MEDIA_TYPE_PLAYLIST, +MEDIA_TYPE_MAP: Mapping[str, MediaType] = { + "object": MediaType.URL, + "object.item": MediaType.URL, + "object.item.imageItem": MediaType.IMAGE, + "object.item.imageItem.photo": MediaType.IMAGE, + "object.item.audioItem": MediaType.MUSIC, + "object.item.audioItem.musicTrack": MediaType.MUSIC, + "object.item.audioItem.audioBroadcast": MediaType.MUSIC, + "object.item.audioItem.audioBook": MediaType.PODCAST, + "object.item.videoItem": MediaType.VIDEO, + "object.item.videoItem.movie": MediaType.MOVIE, + "object.item.videoItem.videoBroadcast": MediaType.TVSHOW, + "object.item.videoItem.musicVideoClip": MediaType.VIDEO, + "object.item.playlistItem": MediaType.PLAYLIST, + "object.item.textItem": MediaType.URL, + "object.item.bookmarkItem": MediaType.URL, + "object.item.epgItem": MediaType.EPISODE, + "object.item.epgItem.audioProgram": MediaType.EPISODE, + "object.item.epgItem.videoProgram": MediaType.EPISODE, + "object.container": MediaType.PLAYLIST, + "object.container.person": MediaType.ARTIST, + "object.container.person.musicArtist": MediaType.ARTIST, + "object.container.playlistContainer": MediaType.PLAYLIST, + "object.container.album": MediaType.ALBUM, + "object.container.album.musicAlbum": MediaType.ALBUM, + "object.container.album.photoAlbum": MediaType.ALBUM, + "object.container.genre": MediaType.GENRE, + "object.container.genre.musicGenre": MediaType.GENRE, + "object.container.genre.movieGenre": MediaType.GENRE, + "object.container.channelGroup": MediaType.CHANNELS, + "object.container.channelGroup.audioChannelGroup": MediaType.CHANNELS, + "object.container.channelGroup.videoChannelGroup": MediaType.CHANNELS, + "object.container.epgContainer": MediaType.TVSHOW, + "object.container.storageSystem": MediaType.PLAYLIST, + "object.container.storageVolume": MediaType.PLAYLIST, + "object.container.storageFolder": MediaType.PLAYLIST, + "object.container.bookmarkFolder": MediaType.PLAYLIST, } # Map media_player media_content_type to UPnP class. Not everything will map # directly, in which case it's not specified and other defaults will be used. -MEDIA_UPNP_CLASS_MAP: Mapping[str, str] = { - _mp_const.MEDIA_TYPE_ALBUM: "object.container.album.musicAlbum", - _mp_const.MEDIA_TYPE_ARTIST: "object.container.person.musicArtist", - _mp_const.MEDIA_TYPE_CHANNEL: "object.item.videoItem.videoBroadcast", - _mp_const.MEDIA_TYPE_CHANNELS: "object.container.channelGroup", - _mp_const.MEDIA_TYPE_COMPOSER: "object.container.person.musicArtist", - _mp_const.MEDIA_TYPE_CONTRIBUTING_ARTIST: "object.container.person.musicArtist", - _mp_const.MEDIA_TYPE_EPISODE: "object.item.epgItem.videoProgram", - _mp_const.MEDIA_TYPE_GENRE: "object.container.genre", - _mp_const.MEDIA_TYPE_IMAGE: "object.item.imageItem", - _mp_const.MEDIA_TYPE_MOVIE: "object.item.videoItem.movie", - _mp_const.MEDIA_TYPE_MUSIC: "object.item.audioItem.musicTrack", - _mp_const.MEDIA_TYPE_PLAYLIST: "object.item.playlistItem", - _mp_const.MEDIA_TYPE_PODCAST: "object.item.audioItem.audioBook", - _mp_const.MEDIA_TYPE_SEASON: "object.item.epgItem.videoProgram", - _mp_const.MEDIA_TYPE_TRACK: "object.item.audioItem.musicTrack", - _mp_const.MEDIA_TYPE_TVSHOW: "object.item.videoItem.videoBroadcast", - _mp_const.MEDIA_TYPE_URL: "object.item.bookmarkItem", - _mp_const.MEDIA_TYPE_VIDEO: "object.item.videoItem", +MEDIA_UPNP_CLASS_MAP: Mapping[MediaType | str, str] = { + MediaType.ALBUM: "object.container.album.musicAlbum", + MediaType.ARTIST: "object.container.person.musicArtist", + MediaType.CHANNEL: "object.item.videoItem.videoBroadcast", + MediaType.CHANNELS: "object.container.channelGroup", + MediaType.COMPOSER: "object.container.person.musicArtist", + MediaType.CONTRIBUTING_ARTIST: "object.container.person.musicArtist", + MediaType.EPISODE: "object.item.epgItem.videoProgram", + MediaType.GENRE: "object.container.genre", + MediaType.IMAGE: "object.item.imageItem", + MediaType.MOVIE: "object.item.videoItem.movie", + MediaType.MUSIC: "object.item.audioItem.musicTrack", + MediaType.PLAYLIST: "object.item.playlistItem", + MediaType.PODCAST: "object.item.audioItem.audioBook", + MediaType.SEASON: "object.item.epgItem.videoProgram", + MediaType.TRACK: "object.item.audioItem.musicTrack", + MediaType.TVSHOW: "object.item.videoItem.videoBroadcast", + MediaType.URL: "object.item.bookmarkItem", + MediaType.VIDEO: "object.item.videoItem", } # Translation of MediaMetadata keys to DIDL-Lite keys. @@ -109,32 +109,32 @@ MEDIA_METADATA_DIDL: Mapping[str, str] = { # of play modes in order of suitability. Fall back to _PlayMode.NORMAL in any # case. NOTE: This list is slightly different to that in SHUFFLE_PLAY_MODES, # due to fallback behaviour when turning on repeat modes. -REPEAT_PLAY_MODES: Mapping[tuple[bool, str], list[_PlayMode]] = { - (False, _mp_const.REPEAT_MODE_OFF): [ +REPEAT_PLAY_MODES: Mapping[tuple[bool, RepeatMode], list[_PlayMode]] = { + (False, RepeatMode.OFF): [ _PlayMode.NORMAL, ], - (False, _mp_const.REPEAT_MODE_ONE): [ + (False, RepeatMode.ONE): [ _PlayMode.REPEAT_ONE, _PlayMode.REPEAT_ALL, _PlayMode.NORMAL, ], - (False, _mp_const.REPEAT_MODE_ALL): [ + (False, RepeatMode.ALL): [ _PlayMode.REPEAT_ALL, _PlayMode.REPEAT_ONE, _PlayMode.NORMAL, ], - (True, _mp_const.REPEAT_MODE_OFF): [ + (True, RepeatMode.OFF): [ _PlayMode.SHUFFLE, _PlayMode.RANDOM, _PlayMode.NORMAL, ], - (True, _mp_const.REPEAT_MODE_ONE): [ + (True, RepeatMode.ONE): [ _PlayMode.REPEAT_ONE, _PlayMode.RANDOM, _PlayMode.SHUFFLE, _PlayMode.NORMAL, ], - (True, _mp_const.REPEAT_MODE_ALL): [ + (True, RepeatMode.ALL): [ _PlayMode.RANDOM, _PlayMode.REPEAT_ALL, _PlayMode.SHUFFLE, @@ -146,31 +146,31 @@ REPEAT_PLAY_MODES: Mapping[tuple[bool, str], list[_PlayMode]] = { # of play modes in order of suitability. Fall back to _PlayMode.NORMAL in any # case. SHUFFLE_PLAY_MODES: Mapping[tuple[bool, str], list[_PlayMode]] = { - (False, _mp_const.REPEAT_MODE_OFF): [ + (False, RepeatMode.OFF): [ _PlayMode.NORMAL, ], - (False, _mp_const.REPEAT_MODE_ONE): [ + (False, RepeatMode.ONE): [ _PlayMode.REPEAT_ONE, _PlayMode.REPEAT_ALL, _PlayMode.NORMAL, ], - (False, _mp_const.REPEAT_MODE_ALL): [ + (False, RepeatMode.ALL): [ _PlayMode.REPEAT_ALL, _PlayMode.REPEAT_ONE, _PlayMode.NORMAL, ], - (True, _mp_const.REPEAT_MODE_OFF): [ + (True, RepeatMode.OFF): [ _PlayMode.SHUFFLE, _PlayMode.RANDOM, _PlayMode.NORMAL, ], - (True, _mp_const.REPEAT_MODE_ONE): [ + (True, RepeatMode.ONE): [ _PlayMode.RANDOM, _PlayMode.SHUFFLE, _PlayMode.REPEAT_ONE, _PlayMode.NORMAL, ], - (True, _mp_const.REPEAT_MODE_ALL): [ + (True, RepeatMode.ALL): [ _PlayMode.RANDOM, _PlayMode.SHUFFLE, _PlayMode.REPEAT_ALL, diff --git a/homeassistant/components/dlna_dmr/media_player.py b/homeassistant/components/dlna_dmr/media_player.py index 156e8fdffef..ff09f018639 100644 --- a/homeassistant/components/dlna_dmr/media_player.py +++ b/homeassistant/components/dlna_dmr/media_player.py @@ -19,27 +19,16 @@ from typing_extensions import Concatenate, ParamSpec from homeassistant import config_entries from homeassistant.components import media_source, ssdp from homeassistant.components.media_player import ( + ATTR_MEDIA_EXTRA, BrowseMedia, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, + MediaType, + RepeatMode, async_process_play_media_url, ) -from homeassistant.components.media_player.const import ( - ATTR_MEDIA_EXTRA, - REPEAT_MODE_ALL, - REPEAT_MODE_OFF, - REPEAT_MODE_ONE, -) -from homeassistant.const import ( - CONF_DEVICE_ID, - CONF_TYPE, - CONF_URL, - STATE_IDLE, - STATE_OFF, - STATE_ON, - STATE_PAUSED, - STATE_PLAYING, -) +from homeassistant.const import CONF_DEVICE_ID, CONF_TYPE, CONF_URL from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry, entity_registry from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -466,27 +455,27 @@ class DlnaDmrEntity(MediaPlayerEntity): return f"{self.udn}::{self.device_type}" @property - def state(self) -> str | None: + def state(self) -> MediaPlayerState | None: """State of the player.""" if not self._device or not self.available: - return STATE_OFF + return MediaPlayerState.OFF if self._device.transport_state is None: - return STATE_ON + return MediaPlayerState.ON if self._device.transport_state in ( TransportState.PLAYING, TransportState.TRANSITIONING, ): - return STATE_PLAYING + return MediaPlayerState.PLAYING if self._device.transport_state in ( TransportState.PAUSED_PLAYBACK, TransportState.PAUSED_RECORDING, ): - return STATE_PAUSED + return MediaPlayerState.PAUSED if self._device.transport_state == TransportState.VENDOR_DEFINED: # Unable to map this state to anything reasonable, so it's "Unknown" return None - return STATE_IDLE + return MediaPlayerState.IDLE @property def supported_features(self) -> int: @@ -586,7 +575,7 @@ class DlnaDmrEntity(MediaPlayerEntity): @catch_request_errors async def async_play_media( - self, media_type: str, media_id: str, **kwargs: Any + self, media_type: MediaType | str, media_id: str, **kwargs: Any ) -> None: """Play a piece of media.""" _LOGGER.debug("Playing media: %s, %s, %s", media_type, media_id, kwargs) @@ -683,7 +672,7 @@ class DlnaDmrEntity(MediaPlayerEntity): """Enable/disable shuffle mode.""" assert self._device is not None - repeat = self.repeat or REPEAT_MODE_OFF + repeat = self.repeat or RepeatMode.OFF potential_play_modes = SHUFFLE_PLAY_MODES[(shuffle, repeat)] valid_play_modes = self._device.valid_play_modes @@ -698,7 +687,7 @@ class DlnaDmrEntity(MediaPlayerEntity): ) @property - def repeat(self) -> str | None: + def repeat(self) -> RepeatMode | None: """Return current repeat mode.""" if not self._device: return None @@ -710,15 +699,15 @@ class DlnaDmrEntity(MediaPlayerEntity): return None if play_mode == PlayMode.REPEAT_ONE: - return REPEAT_MODE_ONE + return RepeatMode.ONE if play_mode in (PlayMode.REPEAT_ALL, PlayMode.RANDOM): - return REPEAT_MODE_ALL + return RepeatMode.ALL - return REPEAT_MODE_OFF + return RepeatMode.OFF @catch_request_errors - async def async_set_repeat(self, repeat: str) -> None: + async def async_set_repeat(self, repeat: RepeatMode) -> None: """Set repeat mode.""" assert self._device is not None @@ -839,7 +828,7 @@ class DlnaDmrEntity(MediaPlayerEntity): return self._device.current_track_uri @property - def media_content_type(self) -> str | None: + def media_content_type(self) -> MediaType | None: """Content type of current playing media.""" if not self._device or not self._device.media_class: return None diff --git a/tests/components/dlna_dmr/test_media_player.py b/tests/components/dlna_dmr/test_media_player.py index eef5d936396..0091826db84 100644 --- a/tests/components/dlna_dmr/test_media_player.py +++ b/tests/components/dlna_dmr/test_media_player.py @@ -21,7 +21,6 @@ import pytest from homeassistant import const as ha_const from homeassistant.components import ssdp -from homeassistant.components.dlna_dmr import media_player from homeassistant.components.dlna_dmr.const import ( CONF_BROWSE_UNFILTERED, CONF_CALLBACK_URL_OVERRIDE, @@ -30,10 +29,17 @@ from homeassistant.components.dlna_dmr.const import ( DOMAIN as DLNA_DOMAIN, ) from homeassistant.components.dlna_dmr.data import EventListenAddr -from homeassistant.components.media_player import ATTR_TO_PROPERTY, const as mp_const -from homeassistant.components.media_player.const import DOMAIN as MP_DOMAIN -from homeassistant.components.media_source.const import DOMAIN as MS_DOMAIN -from homeassistant.components.media_source.models import PlayMedia +from homeassistant.components.dlna_dmr.media_player import DlnaDmrEntity +from homeassistant.components.media_player import ( + ATTR_TO_PROPERTY, + DOMAIN as MP_DOMAIN, + MediaPlayerEntityFeature, + MediaPlayerState, + MediaType, + RepeatMode, + const as mp_const, +) +from homeassistant.components.media_source import DOMAIN as MS_DOMAIN, PlayMedia from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import async_get as async_get_dr @@ -223,7 +229,7 @@ async def test_setup_entry_no_options( ANY, {"_udn": MOCK_DEVICE_UDN, "NTS": "ssdp:byebye"} ) # Quick check of the state to verify the entity has a connected DmrDevice - assert mock_state.state == media_player.STATE_IDLE + assert mock_state.state == MediaPlayerState.IDLE # Check the name matches that supplied assert mock_state.name == MOCK_DEVICE_NAME @@ -292,7 +298,7 @@ async def test_setup_entry_with_options( ANY, {"_udn": MOCK_DEVICE_UDN, "NTS": "ssdp:byebye"} ) # Quick check of the state to verify the entity has a connected DmrDevice - assert mock_state.state == media_player.STATE_IDLE + assert mock_state.state == MediaPlayerState.IDLE # Check the name matches that supplied assert mock_state.name == MOCK_DEVICE_NAME @@ -359,7 +365,7 @@ async def test_event_subscribe_rejected( assert mock_state is not None # Device should be connected - assert mock_state.state == ha_const.STATE_IDLE + assert mock_state.state == MediaPlayerState.IDLE # Device should not be unsubscribed dmr_device_mock.async_unsubscribe_services.assert_not_awaited() @@ -385,14 +391,14 @@ async def test_available_device( # Check entity state gets updated when device changes state for (dev_state, ent_state) in [ - (None, ha_const.STATE_ON), - (TransportState.STOPPED, ha_const.STATE_IDLE), - (TransportState.PLAYING, ha_const.STATE_PLAYING), - (TransportState.TRANSITIONING, ha_const.STATE_PLAYING), - (TransportState.PAUSED_PLAYBACK, ha_const.STATE_PAUSED), - (TransportState.PAUSED_RECORDING, ha_const.STATE_PAUSED), - (TransportState.RECORDING, ha_const.STATE_IDLE), - (TransportState.NO_MEDIA_PRESENT, ha_const.STATE_IDLE), + (None, MediaPlayerState.ON), + (TransportState.STOPPED, MediaPlayerState.IDLE), + (TransportState.PLAYING, MediaPlayerState.PLAYING), + (TransportState.TRANSITIONING, MediaPlayerState.PLAYING), + (TransportState.PAUSED_PLAYBACK, MediaPlayerState.PAUSED), + (TransportState.PAUSED_RECORDING, MediaPlayerState.PAUSED), + (TransportState.RECORDING, MediaPlayerState.IDLE), + (TransportState.NO_MEDIA_PRESENT, MediaPlayerState.IDLE), (TransportState.VENDOR_DEFINED, ha_const.STATE_UNKNOWN), ]: dmr_device_mock.profile_device.available = True @@ -416,16 +422,19 @@ async def test_feature_flags( """Test feature flags of a connected DlnaDmrEntity.""" # Check supported feature flags, one at a time. FEATURE_FLAGS: list[tuple[str, int]] = [ - ("has_volume_level", mp_const.SUPPORT_VOLUME_SET), - ("has_volume_mute", mp_const.SUPPORT_VOLUME_MUTE), - ("can_play", mp_const.SUPPORT_PLAY), - ("can_pause", mp_const.SUPPORT_PAUSE), - ("can_stop", mp_const.SUPPORT_STOP), - ("can_previous", mp_const.SUPPORT_PREVIOUS_TRACK), - ("can_next", mp_const.SUPPORT_NEXT_TRACK), - ("has_play_media", mp_const.SUPPORT_PLAY_MEDIA | mp_const.SUPPORT_BROWSE_MEDIA), - ("can_seek_rel_time", mp_const.SUPPORT_SEEK), - ("has_presets", mp_const.SUPPORT_SELECT_SOUND_MODE), + ("has_volume_level", MediaPlayerEntityFeature.VOLUME_SET), + ("has_volume_mute", MediaPlayerEntityFeature.VOLUME_MUTE), + ("can_play", MediaPlayerEntityFeature.PLAY), + ("can_pause", MediaPlayerEntityFeature.PAUSE), + ("can_stop", MediaPlayerEntityFeature.STOP), + ("can_previous", MediaPlayerEntityFeature.PREVIOUS_TRACK), + ("can_next", MediaPlayerEntityFeature.NEXT_TRACK), + ( + "has_play_media", + MediaPlayerEntityFeature.PLAY_MEDIA | MediaPlayerEntityFeature.BROWSE_MEDIA, + ), + ("can_seek_rel_time", MediaPlayerEntityFeature.SEEK), + ("has_presets", MediaPlayerEntityFeature.SELECT_SOUND_MODE), ] # Clear all feature properties @@ -446,10 +455,10 @@ async def test_feature_flags( # shuffle and repeat features depend on the available play modes PLAY_MODE_FEATURE_FLAGS: list[tuple[PlayMode, int]] = [ (PlayMode.NORMAL, 0), - (PlayMode.SHUFFLE, mp_const.SUPPORT_SHUFFLE_SET), - (PlayMode.REPEAT_ONE, mp_const.SUPPORT_REPEAT_SET), - (PlayMode.REPEAT_ALL, mp_const.SUPPORT_REPEAT_SET), - (PlayMode.RANDOM, mp_const.SUPPORT_SHUFFLE_SET), + (PlayMode.SHUFFLE, MediaPlayerEntityFeature.SHUFFLE_SET), + (PlayMode.REPEAT_ONE, MediaPlayerEntityFeature.REPEAT_SET), + (PlayMode.REPEAT_ALL, MediaPlayerEntityFeature.REPEAT_SET), + (PlayMode.RANDOM, MediaPlayerEntityFeature.SHUFFLE_SET), (PlayMode.DIRECT_1, 0), (PlayMode.INTRO, 0), (PlayMode.VENDOR_DEFINED, 0), @@ -497,13 +506,13 @@ async def test_attributes( # media_content_type is mapped from UPnP class to MediaPlayer type dmr_device_mock.media_class = "object.item.audioItem.musicTrack" attrs = await get_attrs(hass, mock_entity_id) - assert attrs[mp_const.ATTR_MEDIA_CONTENT_TYPE] == mp_const.MEDIA_TYPE_MUSIC + assert attrs[mp_const.ATTR_MEDIA_CONTENT_TYPE] == MediaType.MUSIC dmr_device_mock.media_class = "object.item.videoItem.movie" attrs = await get_attrs(hass, mock_entity_id) - assert attrs[mp_const.ATTR_MEDIA_CONTENT_TYPE] == mp_const.MEDIA_TYPE_MOVIE + assert attrs[mp_const.ATTR_MEDIA_CONTENT_TYPE] == MediaType.MOVIE dmr_device_mock.media_class = "object.item.videoItem.videoBroadcast" attrs = await get_attrs(hass, mock_entity_id) - assert attrs[mp_const.ATTR_MEDIA_CONTENT_TYPE] == mp_const.MEDIA_TYPE_TVSHOW + assert attrs[mp_const.ATTR_MEDIA_CONTENT_TYPE] == MediaType.TVSHOW # media_season & media_episode have a special case dmr_device_mock.media_season_number = "0" @@ -519,13 +528,13 @@ async def test_attributes( # shuffle and repeat is based on device's play mode for play_mode, shuffle, repeat in [ - (PlayMode.NORMAL, False, mp_const.REPEAT_MODE_OFF), - (PlayMode.SHUFFLE, True, mp_const.REPEAT_MODE_OFF), - (PlayMode.REPEAT_ONE, False, mp_const.REPEAT_MODE_ONE), - (PlayMode.REPEAT_ALL, False, mp_const.REPEAT_MODE_ALL), - (PlayMode.RANDOM, True, mp_const.REPEAT_MODE_ALL), - (PlayMode.DIRECT_1, False, mp_const.REPEAT_MODE_OFF), - (PlayMode.INTRO, False, mp_const.REPEAT_MODE_OFF), + (PlayMode.NORMAL, False, RepeatMode.OFF), + (PlayMode.SHUFFLE, True, RepeatMode.OFF), + (PlayMode.REPEAT_ONE, False, RepeatMode.ONE), + (PlayMode.REPEAT_ALL, False, RepeatMode.ALL), + (PlayMode.RANDOM, True, RepeatMode.ALL), + (PlayMode.DIRECT_1, False, RepeatMode.OFF), + (PlayMode.INTRO, False, RepeatMode.OFF), ]: dmr_device_mock.play_mode = play_mode attrs = await get_attrs(hass, mock_entity_id) @@ -620,7 +629,7 @@ async def test_play_media_stopped( mp_const.SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: mock_entity_id, - mp_const.ATTR_MEDIA_CONTENT_TYPE: mp_const.MEDIA_TYPE_MUSIC, + mp_const.ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC, mp_const.ATTR_MEDIA_CONTENT_ID: "http://192.88.99.20:8200/MediaItems/17621.mp3", mp_const.ATTR_MEDIA_ENQUEUE: False, }, @@ -652,7 +661,7 @@ async def test_play_media_playing( mp_const.SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: mock_entity_id, - mp_const.ATTR_MEDIA_CONTENT_TYPE: mp_const.MEDIA_TYPE_MUSIC, + mp_const.ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC, mp_const.ATTR_MEDIA_CONTENT_ID: "http://192.88.99.20:8200/MediaItems/17621.mp3", mp_const.ATTR_MEDIA_ENQUEUE: False, }, @@ -685,7 +694,7 @@ async def test_play_media_no_autoplay( mp_const.SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: mock_entity_id, - mp_const.ATTR_MEDIA_CONTENT_TYPE: mp_const.MEDIA_TYPE_MUSIC, + mp_const.ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC, mp_const.ATTR_MEDIA_CONTENT_ID: "http://192.88.99.20:8200/MediaItems/17621.mp3", mp_const.ATTR_MEDIA_ENQUEUE: False, mp_const.ATTR_MEDIA_EXTRA: {"autoplay": False}, @@ -716,7 +725,7 @@ async def test_play_media_metadata( mp_const.SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: mock_entity_id, - mp_const.ATTR_MEDIA_CONTENT_TYPE: mp_const.MEDIA_TYPE_MUSIC, + mp_const.ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC, mp_const.ATTR_MEDIA_CONTENT_ID: "http://192.88.99.20:8200/MediaItems/17621.mp3", mp_const.ATTR_MEDIA_ENQUEUE: False, mp_const.ATTR_MEDIA_EXTRA: { @@ -746,7 +755,7 @@ async def test_play_media_metadata( mp_const.SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: mock_entity_id, - mp_const.ATTR_MEDIA_CONTENT_TYPE: mp_const.MEDIA_TYPE_TVSHOW, + mp_const.ATTR_MEDIA_CONTENT_TYPE: MediaType.TVSHOW, mp_const.ATTR_MEDIA_CONTENT_ID: "http://192.88.99.20:8200/MediaItems/123.mkv", mp_const.ATTR_MEDIA_ENQUEUE: False, mp_const.ATTR_MEDIA_EXTRA: { @@ -878,21 +887,21 @@ async def test_shuffle_repeat_modes( # Test repeat with all variations of existing play mode for init_mode, repeat_set, expect_mode in [ - (PlayMode.NORMAL, mp_const.REPEAT_MODE_OFF, PlayMode.NORMAL), - (PlayMode.SHUFFLE, mp_const.REPEAT_MODE_OFF, PlayMode.SHUFFLE), - (PlayMode.REPEAT_ONE, mp_const.REPEAT_MODE_OFF, PlayMode.NORMAL), - (PlayMode.REPEAT_ALL, mp_const.REPEAT_MODE_OFF, PlayMode.NORMAL), - (PlayMode.RANDOM, mp_const.REPEAT_MODE_OFF, PlayMode.SHUFFLE), - (PlayMode.NORMAL, mp_const.REPEAT_MODE_ONE, PlayMode.REPEAT_ONE), - (PlayMode.SHUFFLE, mp_const.REPEAT_MODE_ONE, PlayMode.REPEAT_ONE), - (PlayMode.REPEAT_ONE, mp_const.REPEAT_MODE_ONE, PlayMode.REPEAT_ONE), - (PlayMode.REPEAT_ALL, mp_const.REPEAT_MODE_ONE, PlayMode.REPEAT_ONE), - (PlayMode.RANDOM, mp_const.REPEAT_MODE_ONE, PlayMode.REPEAT_ONE), - (PlayMode.NORMAL, mp_const.REPEAT_MODE_ALL, PlayMode.REPEAT_ALL), - (PlayMode.SHUFFLE, mp_const.REPEAT_MODE_ALL, PlayMode.RANDOM), - (PlayMode.REPEAT_ONE, mp_const.REPEAT_MODE_ALL, PlayMode.REPEAT_ALL), - (PlayMode.REPEAT_ALL, mp_const.REPEAT_MODE_ALL, PlayMode.REPEAT_ALL), - (PlayMode.RANDOM, mp_const.REPEAT_MODE_ALL, PlayMode.RANDOM), + (PlayMode.NORMAL, RepeatMode.OFF, PlayMode.NORMAL), + (PlayMode.SHUFFLE, RepeatMode.OFF, PlayMode.SHUFFLE), + (PlayMode.REPEAT_ONE, RepeatMode.OFF, PlayMode.NORMAL), + (PlayMode.REPEAT_ALL, RepeatMode.OFF, PlayMode.NORMAL), + (PlayMode.RANDOM, RepeatMode.OFF, PlayMode.SHUFFLE), + (PlayMode.NORMAL, RepeatMode.ONE, PlayMode.REPEAT_ONE), + (PlayMode.SHUFFLE, RepeatMode.ONE, PlayMode.REPEAT_ONE), + (PlayMode.REPEAT_ONE, RepeatMode.ONE, PlayMode.REPEAT_ONE), + (PlayMode.REPEAT_ALL, RepeatMode.ONE, PlayMode.REPEAT_ONE), + (PlayMode.RANDOM, RepeatMode.ONE, PlayMode.REPEAT_ONE), + (PlayMode.NORMAL, RepeatMode.ALL, PlayMode.REPEAT_ALL), + (PlayMode.SHUFFLE, RepeatMode.ALL, PlayMode.RANDOM), + (PlayMode.REPEAT_ONE, RepeatMode.ALL, PlayMode.REPEAT_ALL), + (PlayMode.REPEAT_ALL, RepeatMode.ALL, PlayMode.REPEAT_ALL), + (PlayMode.RANDOM, RepeatMode.ALL, PlayMode.RANDOM), ]: dmr_device_mock.play_mode = init_mode await hass.services.async_call( @@ -926,7 +935,7 @@ async def test_shuffle_repeat_modes( ha_const.SERVICE_REPEAT_SET, { ATTR_ENTITY_ID: mock_entity_id, - mp_const.ATTR_MEDIA_REPEAT: mp_const.REPEAT_MODE_OFF, + mp_const.ATTR_MEDIA_REPEAT: RepeatMode.OFF, }, blocking=True, ) @@ -1226,7 +1235,7 @@ async def test_unavailable_device( ( mp_const.SERVICE_PLAY_MEDIA, { - mp_const.ATTR_MEDIA_CONTENT_TYPE: mp_const.MEDIA_TYPE_MUSIC, + mp_const.ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC, mp_const.ATTR_MEDIA_CONTENT_ID: "http://192.88.99.20:8200/MediaItems/17621.mp3", mp_const.ATTR_MEDIA_ENQUEUE: False, }, @@ -1318,7 +1327,7 @@ async def test_become_available( # Quick check of the state to verify the entity has a connected DmrDevice mock_state = hass.states.get(mock_entity_id) assert mock_state is not None - assert mock_state.state == ha_const.STATE_IDLE + assert mock_state.state == MediaPlayerState.IDLE # Check hass device information is now filled in dev_reg = async_get_dr(hass) device = dev_reg.async_get_device(identifiers={(DLNA_DOMAIN, MOCK_DEVICE_UDN)}) @@ -1496,7 +1505,7 @@ async def test_multiple_ssdp_alive( # Device should be available mock_state = hass.states.get(mock_disconnected_entity_id) assert mock_state is not None - assert mock_state.state == media_player.STATE_IDLE + assert mock_state.state == MediaPlayerState.IDLE async def test_ssdp_byebye( @@ -1592,7 +1601,7 @@ async def test_ssdp_update_seen_bootid( # Device was not reconnected, even with a new boot ID mock_state = hass.states.get(entity_id) assert mock_state is not None - assert mock_state.state == ha_const.STATE_IDLE + assert mock_state.state == MediaPlayerState.IDLE assert dmr_device_mock.async_unsubscribe_services.await_count == 0 assert dmr_device_mock.async_subscribe_services.await_count == 1 @@ -1617,7 +1626,7 @@ async def test_ssdp_update_seen_bootid( # Nothing should change mock_state = hass.states.get(entity_id) assert mock_state is not None - assert mock_state.state == ha_const.STATE_IDLE + assert mock_state.state == MediaPlayerState.IDLE assert dmr_device_mock.async_unsubscribe_services.await_count == 0 assert dmr_device_mock.async_subscribe_services.await_count == 1 @@ -1642,7 +1651,7 @@ async def test_ssdp_update_seen_bootid( # Nothing should change mock_state = hass.states.get(entity_id) assert mock_state is not None - assert mock_state.state == ha_const.STATE_IDLE + assert mock_state.state == MediaPlayerState.IDLE assert dmr_device_mock.async_unsubscribe_services.await_count == 0 assert dmr_device_mock.async_subscribe_services.await_count == 1 @@ -1662,7 +1671,7 @@ async def test_ssdp_update_seen_bootid( mock_state = hass.states.get(entity_id) assert mock_state is not None - assert mock_state.state == ha_const.STATE_IDLE + assert mock_state.state == MediaPlayerState.IDLE assert dmr_device_mock.async_unsubscribe_services.await_count == 0 assert dmr_device_mock.async_subscribe_services.await_count == 1 @@ -1719,7 +1728,7 @@ async def test_ssdp_update_missed_bootid( # Device should not reconnect yet mock_state = hass.states.get(entity_id) assert mock_state is not None - assert mock_state.state == ha_const.STATE_IDLE + assert mock_state.state == MediaPlayerState.IDLE assert dmr_device_mock.async_unsubscribe_services.await_count == 0 assert dmr_device_mock.async_subscribe_services.await_count == 1 @@ -1739,7 +1748,7 @@ async def test_ssdp_update_missed_bootid( mock_state = hass.states.get(entity_id) assert mock_state is not None - assert mock_state.state == ha_const.STATE_IDLE + assert mock_state.state == MediaPlayerState.IDLE assert dmr_device_mock.async_unsubscribe_services.await_count == 1 assert dmr_device_mock.async_subscribe_services.await_count == 2 @@ -1778,7 +1787,7 @@ async def test_ssdp_bootid( mock_state = hass.states.get(entity_id) assert mock_state is not None - assert mock_state.state == ha_const.STATE_IDLE + assert mock_state.state == MediaPlayerState.IDLE assert dmr_device_mock.async_subscribe_services.call_count == 1 assert dmr_device_mock.async_unsubscribe_services.call_count == 0 @@ -1798,7 +1807,7 @@ async def test_ssdp_bootid( mock_state = hass.states.get(entity_id) assert mock_state is not None - assert mock_state.state == ha_const.STATE_IDLE + assert mock_state.state == MediaPlayerState.IDLE assert dmr_device_mock.async_subscribe_services.call_count == 1 assert dmr_device_mock.async_unsubscribe_services.call_count == 0 @@ -1818,7 +1827,7 @@ async def test_ssdp_bootid( mock_state = hass.states.get(entity_id) assert mock_state is not None - assert mock_state.state == ha_const.STATE_IDLE + assert mock_state.state == MediaPlayerState.IDLE assert dmr_device_mock.async_subscribe_services.call_count == 2 assert dmr_device_mock.async_unsubscribe_services.call_count == 1 @@ -1849,14 +1858,14 @@ async def test_become_unavailable( mock_state = hass.states.get(mock_entity_id) assert mock_state is not None - assert mock_state.state == ha_const.STATE_IDLE + assert mock_state.state == MediaPlayerState.IDLE # With a working connection, the state should be restored await async_update_entity(hass, mock_entity_id) dmr_device_mock.async_update.assert_any_call(do_ping=True) mock_state = hass.states.get(mock_entity_id) assert mock_state is not None - assert mock_state.state == ha_const.STATE_IDLE + assert mock_state.state == MediaPlayerState.IDLE # Break the service again, and the connection too. An update will cause the # device to be disconnected @@ -1918,7 +1927,7 @@ async def test_poll_availability( mock_state = hass.states.get(mock_entity_id) assert mock_state is not None - assert mock_state.state == ha_const.STATE_IDLE + assert mock_state.state == MediaPlayerState.IDLE # Clean up assert await hass.config_entries.async_remove(config_entry_mock.entry_id) == { @@ -1938,9 +1947,7 @@ async def test_disappearing_device( directly to skip the availability check. """ # Retrieve entity directly. - entity: media_player.DlnaDmrEntity = hass.data[MP_DOMAIN].get_entity( - mock_disconnected_entity_id - ) + entity: DlnaDmrEntity = hass.data[MP_DOMAIN].get_entity(mock_disconnected_entity_id) # Test attribute access for attr in ATTR_TO_PROPERTY: @@ -1964,7 +1971,7 @@ async def test_disappearing_device( await entity.async_media_previous_track() await entity.async_media_next_track() await entity.async_set_shuffle(True) - await entity.async_set_repeat(mp_const.REPEAT_MODE_ALL) + await entity.async_set_repeat(RepeatMode.ALL) await entity.async_select_sound_mode("Default") @@ -2023,7 +2030,7 @@ async def test_config_update_listen_port( # Check that its still connected mock_state = hass.states.get(mock_entity_id) assert mock_state is not None - assert mock_state.state == ha_const.STATE_IDLE + assert mock_state.state == MediaPlayerState.IDLE async def test_config_update_connect_failure( @@ -2097,7 +2104,7 @@ async def test_config_update_callback_url( # Check that its still connected mock_state = hass.states.get(mock_entity_id) assert mock_state is not None - assert mock_state.state == ha_const.STATE_IDLE + assert mock_state.state == MediaPlayerState.IDLE async def test_config_update_poll_availability( @@ -2138,4 +2145,4 @@ async def test_config_update_poll_availability( # Check that its still connected mock_state = hass.states.get(mock_entity_id) assert mock_state is not None - assert mock_state.state == ha_const.STATE_IDLE + assert mock_state.state == MediaPlayerState.IDLE From d53d59eb6c3e4f65bcbcf294dea9f0607c117810 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 9 Sep 2022 11:12:09 +0200 Subject: [PATCH 228/955] Improve warning messages on invalid received modes (#77909) --- .../components/mqtt/light/schema_json.py | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index f7703b0f1a4..8843e8542eb 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -256,7 +256,9 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): except KeyError: pass except ValueError: - _LOGGER.warning("Invalid RGB color value received") + _LOGGER.warning( + "Invalid RGB color value received for entity %s", self.entity_id + ) return try: @@ -266,7 +268,9 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): except KeyError: pass except ValueError: - _LOGGER.warning("Invalid XY color value received") + _LOGGER.warning( + "Invalid XY color value received for entity %s", self.entity_id + ) return try: @@ -276,12 +280,16 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): except KeyError: pass except ValueError: - _LOGGER.warning("Invalid HS color value received") + _LOGGER.warning( + "Invalid HS color value received for entity %s", self.entity_id + ) return else: color_mode = values["color_mode"] if not self._supports_color_mode(color_mode): - _LOGGER.warning("Invalid color mode received") + _LOGGER.warning( + "Invalid color mode received for entity %s", self.entity_id + ) return try: if color_mode == ColorMode.COLOR_TEMP: @@ -321,7 +329,10 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): self._color_mode = ColorMode.XY self._xy = (x, y) except (KeyError, ValueError): - _LOGGER.warning("Invalid or incomplete color value received") + _LOGGER.warning( + "Invalid or incomplete color value received for entity %s", + self.entity_id, + ) def _prepare_subscribe_topics(self): """(Re)Subscribe to topics.""" @@ -363,7 +374,10 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): except KeyError: pass except (TypeError, ValueError): - _LOGGER.warning("Invalid brightness value received") + _LOGGER.warning( + "Invalid brightness value received for entity %s", + self.entity_id, + ) if ( ColorMode.COLOR_TEMP in self._supported_color_modes @@ -378,7 +392,10 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): except KeyError: pass except ValueError: - _LOGGER.warning("Invalid color temp value received") + _LOGGER.warning( + "Invalid color temp value received for entity %s", + self.entity_id, + ) if self._supported_features and LightEntityFeature.EFFECT: with suppress(KeyError): From 9a4c8f5f0e756afadb86e8416ff081e71418c788 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 9 Sep 2022 11:15:48 +0200 Subject: [PATCH 229/955] Refactor common MQTT tests to use modern schema (#77583) * Common tests availability * Common tests attributes * Common tests unique id * Common tests discovery * Common tests encoding * Common tests device info * Common tests entity_id updated * Common tests entity debug info * Common test entity category * Common tests setup reload unload+corrections * Cleanup sweep * Comments from curent change * Cleanup * Remove unused legacy config --- .../mqtt/test_alarm_control_panel.py | 103 +++++---- tests/components/mqtt/test_binary_sensor.py | 103 +++++---- tests/components/mqtt/test_button.py | 103 ++++----- tests/components/mqtt/test_camera.py | 83 ++++--- tests/components/mqtt/test_climate.py | 113 +++++----- tests/components/mqtt/test_common.py | 202 +++++++++--------- tests/components/mqtt/test_cover.py | 85 ++++---- .../mqtt/test_device_tracker_discovery.py | 14 +- tests/components/mqtt/test_fan.py | 89 ++++---- tests/components/mqtt/test_humidifier.py | 93 ++++---- tests/components/mqtt/test_init.py | 38 +++- tests/components/mqtt/test_legacy_vacuum.py | 117 +++++----- tests/components/mqtt/test_light.py | 95 ++++---- tests/components/mqtt/test_light_json.py | 99 ++++----- tests/components/mqtt/test_light_template.py | 116 +++++----- tests/components/mqtt/test_lock.py | 87 ++++---- tests/components/mqtt/test_number.py | 93 ++++---- tests/components/mqtt/test_scene.py | 71 +++--- tests/components/mqtt/test_select.py | 99 +++++---- tests/components/mqtt/test_sensor.py | 110 +++++----- tests/components/mqtt/test_siren.py | 125 +++++------ tests/components/mqtt/test_state_vacuum.py | 87 ++++---- tests/components/mqtt/test_switch.py | 123 +++++------ 23 files changed, 1125 insertions(+), 1123 deletions(-) diff --git a/tests/components/mqtt/test_alarm_control_panel.py b/tests/components/mqtt/test_alarm_control_panel.py index e51ed9aeae9..d305d2ae7aa 100644 --- a/tests/components/mqtt/test_alarm_control_panel.py +++ b/tests/components/mqtt/test_alarm_control_panel.py @@ -61,7 +61,7 @@ from .test_common import ( help_test_setup_manual_entity_from_yaml, help_test_unique_id, help_test_unload_config_entry_with_platform, - help_test_update_with_json_attrs_bad_JSON, + help_test_update_with_json_attrs_bad_json, help_test_update_with_json_attrs_not_dict, ) @@ -121,8 +121,6 @@ DEFAULT_CONFIG_REMOTE_CODE_TEXT = { # Scheduled to be removed in HA core 2022.12 DEFAULT_CONFIG_LEGACY = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) DEFAULT_CONFIG_LEGACY[alarm_control_panel.DOMAIN]["platform"] = mqtt.DOMAIN -DEFAULT_CONFIG_CODE_LEGACY = copy.deepcopy(DEFAULT_CONFIG_CODE[mqtt.DOMAIN]) -DEFAULT_CONFIG_CODE_LEGACY[alarm_control_panel.DOMAIN]["platform"] = mqtt.DOMAIN @pytest.fixture(autouse=True) @@ -588,7 +586,7 @@ async def test_availability_when_connection_lost( hass, mqtt_mock_entry_with_yaml_config, alarm_control_panel.DOMAIN, - DEFAULT_CONFIG_CODE_LEGACY, + DEFAULT_CONFIG_CODE, ) @@ -598,7 +596,7 @@ async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config hass, mqtt_mock_entry_with_yaml_config, alarm_control_panel.DOMAIN, - DEFAULT_CONFIG_CODE_LEGACY, + DEFAULT_CONFIG_CODE, ) @@ -608,7 +606,7 @@ async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_conf hass, mqtt_mock_entry_with_yaml_config, alarm_control_panel.DOMAIN, - DEFAULT_CONFIG_CODE_LEGACY, + DEFAULT_CONFIG_CODE, ) @@ -618,7 +616,7 @@ async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_confi hass, mqtt_mock_entry_with_yaml_config, alarm_control_panel.DOMAIN, - DEFAULT_CONFIG_CODE_LEGACY, + DEFAULT_CONFIG, ) @@ -630,7 +628,7 @@ async def test_setting_attribute_via_mqtt_json_message( hass, mqtt_mock_entry_with_yaml_config, alarm_control_panel.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -642,7 +640,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( hass, mqtt_mock_entry_no_yaml_config, alarm_control_panel.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, MQTT_ALARM_ATTRIBUTES_BLOCKED, ) @@ -653,7 +651,7 @@ async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_c hass, mqtt_mock_entry_with_yaml_config, alarm_control_panel.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -666,20 +664,20 @@ async def test_update_with_json_attrs_not_dict( mqtt_mock_entry_with_yaml_config, caplog, alarm_control_panel.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) -async def test_update_with_json_attrs_bad_JSON( +async def test_update_with_json_attrs_bad_json( hass, mqtt_mock_entry_with_yaml_config, caplog ): """Test attributes get extracted from a JSON result.""" - await help_test_update_with_json_attrs_bad_JSON( + await help_test_update_with_json_attrs_bad_json( hass, mqtt_mock_entry_with_yaml_config, caplog, alarm_control_panel.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -690,29 +688,29 @@ async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplo mqtt_mock_entry_no_yaml_config, caplog, alarm_control_panel.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one alarm per unique_id.""" config = { - alarm_control_panel.DOMAIN: [ - { - "platform": "mqtt", - "name": "Test 1", - "state_topic": "test-topic", - "command_topic": "command-topic", - "unique_id": "TOTALLY_UNIQUE", - }, - { - "platform": "mqtt", - "name": "Test 2", - "state_topic": "test-topic", - "command_topic": "command-topic", - "unique_id": "TOTALLY_UNIQUE", - }, - ] + mqtt.DOMAIN: { + alarm_control_panel.DOMAIN: [ + { + "name": "Test 1", + "state_topic": "test-topic", + "command_topic": "command-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + { + "name": "Test 2", + "state_topic": "test-topic", + "command_topic": "command-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + ] + } } await help_test_unique_id( hass, mqtt_mock_entry_with_yaml_config, alarm_control_panel.DOMAIN, config @@ -721,7 +719,7 @@ async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): async def test_discovery_removal_alarm(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered alarm_control_panel.""" - data = json.dumps(DEFAULT_CONFIG_LEGACY[alarm_control_panel.DOMAIN]) + data = json.dumps(DEFAULT_CONFIG[mqtt.DOMAIN][alarm_control_panel.DOMAIN]) await help_test_discovery_removal( hass, mqtt_mock_entry_no_yaml_config, caplog, alarm_control_panel.DOMAIN, data ) @@ -731,8 +729,8 @@ async def test_discovery_update_alarm_topic_and_template( hass, mqtt_mock_entry_no_yaml_config, caplog ): """Test update of discovered alarm_control_panel.""" - config1 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[alarm_control_panel.DOMAIN]) - config2 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[alarm_control_panel.DOMAIN]) + config1 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][alarm_control_panel.DOMAIN]) + config2 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][alarm_control_panel.DOMAIN]) config1["name"] = "Beer" config2["name"] = "Milk" config1["state_topic"] = "alarm/state1" @@ -766,8 +764,8 @@ async def test_discovery_update_alarm_template( hass, mqtt_mock_entry_no_yaml_config, caplog ): """Test update of discovered alarm_control_panel.""" - config1 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[alarm_control_panel.DOMAIN]) - config2 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[alarm_control_panel.DOMAIN]) + config1 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][alarm_control_panel.DOMAIN]) + config2 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][alarm_control_panel.DOMAIN]) config1["name"] = "Beer" config2["name"] = "Milk" config1["state_topic"] = "alarm/state1" @@ -799,7 +797,7 @@ async def test_discovery_update_unchanged_alarm( hass, mqtt_mock_entry_no_yaml_config, caplog ): """Test update of discovered alarm_control_panel.""" - config1 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[alarm_control_panel.DOMAIN]) + config1 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][alarm_control_panel.DOMAIN]) config1["name"] = "Beer" data1 = json.dumps(config1) @@ -851,7 +849,7 @@ async def test_encoding_subscribable_topics( mqtt_mock_entry_with_yaml_config, caplog, alarm_control_panel.DOMAIN, - DEFAULT_CONFIG_LEGACY[alarm_control_panel.DOMAIN], + DEFAULT_CONFIG[mqtt.DOMAIN][alarm_control_panel.DOMAIN], topic, value, ) @@ -863,7 +861,7 @@ async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_ hass, mqtt_mock_entry_no_yaml_config, alarm_control_panel.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -873,7 +871,7 @@ async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_ hass, mqtt_mock_entry_no_yaml_config, alarm_control_panel.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -883,7 +881,7 @@ async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): hass, mqtt_mock_entry_no_yaml_config, alarm_control_panel.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -893,7 +891,7 @@ async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): hass, mqtt_mock_entry_no_yaml_config, alarm_control_panel.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -903,7 +901,7 @@ async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_co hass, mqtt_mock_entry_with_yaml_config, alarm_control_panel.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -913,7 +911,7 @@ async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_c hass, mqtt_mock_entry_no_yaml_config, alarm_control_panel.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -923,7 +921,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): hass, mqtt_mock_entry_no_yaml_config, alarm_control_panel.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, alarm_control_panel.SERVICE_ALARM_DISARM, command_payload="DISARM", ) @@ -966,7 +964,7 @@ async def test_publishing_with_custom_encoding( ): """Test publishing MQTT payload with different encoding.""" domain = alarm_control_panel.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_publishing_with_custom_encoding( hass, @@ -987,12 +985,14 @@ async def test_publishing_with_custom_encoding( async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = alarm_control_panel.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = alarm_control_panel.DOMAIN @@ -1003,17 +1003,14 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = alarm_control_panel.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) - config["name"] = "test" - del config["platform"] - await help_test_setup_manual_entity_from_yaml(hass, platform, config) - assert hass.states.get(f"{platform}.test") is not None + await help_test_setup_manual_entity_from_yaml(hass, DEFAULT_CONFIG) + assert hass.states.get(f"{platform}.test") async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): """Test unloading the config entry.""" domain = alarm_control_panel.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py index bbe8b978707..48acde5c6c9 100644 --- a/tests/components/mqtt/test_binary_sensor.py +++ b/tests/components/mqtt/test_binary_sensor.py @@ -45,7 +45,7 @@ from .test_common import ( help_test_setup_manual_entity_from_yaml, help_test_unique_id, help_test_unload_config_entry_with_platform, - help_test_update_with_json_attrs_bad_JSON, + help_test_update_with_json_attrs_bad_json, help_test_update_with_json_attrs_not_dict, ) @@ -542,7 +542,7 @@ async def test_availability_when_connection_lost( hass, mqtt_mock_entry_with_yaml_config, binary_sensor.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -552,7 +552,7 @@ async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config hass, mqtt_mock_entry_with_yaml_config, binary_sensor.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -562,7 +562,7 @@ async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_conf hass, mqtt_mock_entry_with_yaml_config, binary_sensor.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -572,7 +572,7 @@ async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_confi hass, mqtt_mock_entry_with_yaml_config, binary_sensor.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -708,7 +708,7 @@ async def test_setting_attribute_via_mqtt_json_message( hass, mqtt_mock_entry_with_yaml_config, binary_sensor.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -718,7 +718,7 @@ async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_c hass, mqtt_mock_entry_with_yaml_config, binary_sensor.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -731,20 +731,20 @@ async def test_update_with_json_attrs_not_dict( mqtt_mock_entry_with_yaml_config, caplog, binary_sensor.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) -async def test_update_with_json_attrs_bad_JSON( +async def test_update_with_json_attrs_bad_json( hass, mqtt_mock_entry_with_yaml_config, caplog ): """Test attributes get extracted from a JSON result.""" - await help_test_update_with_json_attrs_bad_JSON( + await help_test_update_with_json_attrs_bad_json( hass, mqtt_mock_entry_with_yaml_config, caplog, binary_sensor.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -755,27 +755,27 @@ async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplo mqtt_mock_entry_no_yaml_config, caplog, binary_sensor.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one sensor per unique_id.""" config = { - binary_sensor.DOMAIN: [ - { - "platform": "mqtt", - "name": "Test 1", - "state_topic": "test-topic", - "unique_id": "TOTALLY_UNIQUE", - }, - { - "platform": "mqtt", - "name": "Test 2", - "state_topic": "test-topic", - "unique_id": "TOTALLY_UNIQUE", - }, - ] + mqtt.DOMAIN: { + binary_sensor.DOMAIN: [ + { + "name": "Test 1", + "state_topic": "test-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + { + "name": "Test 2", + "state_topic": "test-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + ] + } } await help_test_unique_id( hass, mqtt_mock_entry_with_yaml_config, binary_sensor.DOMAIN, config @@ -786,7 +786,7 @@ async def test_discovery_removal_binary_sensor( hass, mqtt_mock_entry_no_yaml_config, caplog ): """Test removal of discovered binary_sensor.""" - data = json.dumps(DEFAULT_CONFIG_LEGACY[binary_sensor.DOMAIN]) + data = json.dumps(DEFAULT_CONFIG[mqtt.DOMAIN][binary_sensor.DOMAIN]) await help_test_discovery_removal( hass, mqtt_mock_entry_no_yaml_config, caplog, binary_sensor.DOMAIN, data ) @@ -796,8 +796,8 @@ async def test_discovery_update_binary_sensor_topic_template( hass, mqtt_mock_entry_no_yaml_config, caplog ): """Test update of discovered binary_sensor.""" - config1 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[binary_sensor.DOMAIN]) - config2 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[binary_sensor.DOMAIN]) + config1 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][binary_sensor.DOMAIN]) + config2 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][binary_sensor.DOMAIN]) config1["name"] = "Beer" config2["name"] = "Milk" config1["state_topic"] = "sensor/state1" @@ -833,8 +833,8 @@ async def test_discovery_update_binary_sensor_template( hass, mqtt_mock_entry_no_yaml_config, caplog ): """Test update of discovered binary_sensor.""" - config1 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[binary_sensor.DOMAIN]) - config2 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[binary_sensor.DOMAIN]) + config1 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][binary_sensor.DOMAIN]) + config2 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][binary_sensor.DOMAIN]) config1["name"] = "Beer" config2["name"] = "Milk" config1["state_topic"] = "sensor/state1" @@ -892,7 +892,7 @@ async def test_encoding_subscribable_topics( mqtt_mock_entry_with_yaml_config, caplog, binary_sensor.DOMAIN, - DEFAULT_CONFIG_LEGACY[binary_sensor.DOMAIN], + DEFAULT_CONFIG[mqtt.DOMAIN][binary_sensor.DOMAIN], topic, value, attribute, @@ -904,7 +904,7 @@ async def test_discovery_update_unchanged_binary_sensor( hass, mqtt_mock_entry_no_yaml_config, caplog ): """Test update of discovered binary_sensor.""" - config1 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[binary_sensor.DOMAIN]) + config1 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][binary_sensor.DOMAIN]) config1["name"] = "Beer" data1 = json.dumps(config1) @@ -942,7 +942,7 @@ async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_ hass, mqtt_mock_entry_no_yaml_config, binary_sensor.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -952,7 +952,7 @@ async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_ hass, mqtt_mock_entry_no_yaml_config, binary_sensor.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -962,7 +962,7 @@ async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): hass, mqtt_mock_entry_no_yaml_config, binary_sensor.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -972,7 +972,7 @@ async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): hass, mqtt_mock_entry_no_yaml_config, binary_sensor.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -982,7 +982,7 @@ async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_co hass, mqtt_mock_entry_with_yaml_config, binary_sensor.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -992,7 +992,7 @@ async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_c hass, mqtt_mock_entry_no_yaml_config, binary_sensor.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -1002,7 +1002,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): hass, mqtt_mock_entry_no_yaml_config, binary_sensor.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, None, ) @@ -1010,12 +1010,14 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = binary_sensor.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = binary_sensor.DOMAIN @@ -1040,11 +1042,11 @@ async def test_cleanup_triggers_and_restoring_state( ): """Test cleanup old triggers at reloading and restoring the state.""" domain = binary_sensor.DOMAIN - config1 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) + config1 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][domain]) config1["name"] = "test1" config1["expire_after"] = 30 config1["state_topic"] = "test-topic1" - config2 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) + config2 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][domain]) config2["name"] = "test2" config2["expire_after"] = 5 config2["state_topic"] = "test-topic2" @@ -1053,8 +1055,8 @@ async def test_cleanup_triggers_and_restoring_state( assert await async_setup_component( hass, - binary_sensor.DOMAIN, - {binary_sensor.DOMAIN: [config1, config2]}, + mqtt.DOMAIN, + {mqtt.DOMAIN: {binary_sensor.DOMAIN: [config1, config2]}}, ) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -1070,7 +1072,7 @@ async def test_cleanup_triggers_and_restoring_state( freezer.move_to("2022-02-02 12:01:10+01:00") await help_test_reload_with_config( - hass, caplog, tmp_path, {domain: [config1, config2]} + hass, caplog, tmp_path, {mqtt.DOMAIN: {domain: [config1, config2]}} ) assert "Clean up expire after trigger for binary_sensor.test1" in caplog.text assert "Clean up expire after trigger for binary_sensor.test2" not in caplog.text @@ -1125,17 +1127,14 @@ async def test_skip_restoring_state_with_over_due_expire_trigger( async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = binary_sensor.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) - config["name"] = "test" - del config["platform"] - await help_test_setup_manual_entity_from_yaml(hass, platform, config) - assert hass.states.get(f"{platform}.test") is not None + await help_test_setup_manual_entity_from_yaml(hass, DEFAULT_CONFIG) + assert hass.states.get(f"{platform}.test") async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): """Test unloading the config entry.""" domain = binary_sensor.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) diff --git a/tests/components/mqtt/test_button.py b/tests/components/mqtt/test_button.py index 1274c700800..80e5ce60a47 100644 --- a/tests/components/mqtt/test_button.py +++ b/tests/components/mqtt/test_button.py @@ -38,7 +38,7 @@ from .test_common import ( help_test_setup_manual_entity_from_yaml, help_test_unique_id, help_test_unload_config_entry_with_platform, - help_test_update_with_json_attrs_bad_JSON, + help_test_update_with_json_attrs_bad_json, help_test_update_with_json_attrs_not_dict, ) @@ -140,25 +140,26 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, button.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, button.DOMAIN, DEFAULT_CONFIG ) async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock_entry_with_yaml_config, button.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, button.DOMAIN, DEFAULT_CONFIG ) async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" config = { - button.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "command-topic", - "payload_press": 1, + mqtt.DOMAIN: { + button.DOMAIN: { + "name": "test", + "command_topic": "command-topic", + "payload_press": 1, + } } } @@ -176,11 +177,12 @@ async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_conf async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" config = { - button.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "command-topic", - "payload_press": 1, + mqtt.DOMAIN: { + button.DOMAIN: { + "name": "test", + "command_topic": "command-topic", + "payload_press": 1, + } } } @@ -200,7 +202,7 @@ async def test_setting_attribute_via_mqtt_json_message( ): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock_entry_with_yaml_config, button.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, button.DOMAIN, DEFAULT_CONFIG ) @@ -209,14 +211,14 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( ): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, mqtt_mock_entry_no_yaml_config, button.DOMAIN, DEFAULT_CONFIG_LEGACY, None + hass, mqtt_mock_entry_no_yaml_config, button.DOMAIN, DEFAULT_CONFIG, None ) async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock_entry_with_yaml_config, button.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, button.DOMAIN, DEFAULT_CONFIG ) @@ -229,20 +231,20 @@ async def test_update_with_json_attrs_not_dict( mqtt_mock_entry_with_yaml_config, caplog, button.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) -async def test_update_with_json_attrs_bad_JSON( +async def test_update_with_json_attrs_bad_json( hass, mqtt_mock_entry_with_yaml_config, caplog ): """Test attributes get extracted from a JSON result.""" - await help_test_update_with_json_attrs_bad_JSON( + await help_test_update_with_json_attrs_bad_json( hass, mqtt_mock_entry_with_yaml_config, caplog, button.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -253,27 +255,27 @@ async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplo mqtt_mock_entry_no_yaml_config, caplog, button.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one button per unique_id.""" config = { - button.DOMAIN: [ - { - "platform": "mqtt", - "name": "Test 1", - "command_topic": "command-topic", - "unique_id": "TOTALLY_UNIQUE", - }, - { - "platform": "mqtt", - "name": "Test 2", - "command_topic": "command-topic", - "unique_id": "TOTALLY_UNIQUE", - }, - ] + mqtt.DOMAIN: { + button.DOMAIN: [ + { + "name": "Test 1", + "command_topic": "command-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + { + "name": "Test 2", + "command_topic": "command-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + ] + } } await help_test_unique_id( hass, mqtt_mock_entry_with_yaml_config, button.DOMAIN, config @@ -290,8 +292,8 @@ async def test_discovery_removal_button(hass, mqtt_mock_entry_no_yaml_config, ca async def test_discovery_update_button(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered button.""" - config1 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[button.DOMAIN]) - config2 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[button.DOMAIN]) + config1 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][button.DOMAIN]) + config2 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][button.DOMAIN]) config1["name"] = "Beer" config2["name"] = "Milk" @@ -340,35 +342,35 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT button device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock_entry_no_yaml_config, button.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, button.DOMAIN, DEFAULT_CONFIG ) async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT button device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock_entry_no_yaml_config, button.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, button.DOMAIN, DEFAULT_CONFIG ) async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock_entry_no_yaml_config, button.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, button.DOMAIN, DEFAULT_CONFIG ) async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock_entry_no_yaml_config, button.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, button.DOMAIN, DEFAULT_CONFIG ) async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock_entry_no_yaml_config, button.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, button.DOMAIN, DEFAULT_CONFIG ) @@ -378,7 +380,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): hass, mqtt_mock_entry_no_yaml_config, button.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, button.SERVICE_PRESS, command_payload="PRESS", state_topic=None, @@ -457,7 +459,7 @@ async def test_publishing_with_custom_encoding( ): """Test publishing MQTT payload with different encoding.""" domain = button.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_publishing_with_custom_encoding( hass, @@ -476,12 +478,14 @@ async def test_publishing_with_custom_encoding( async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = button.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = button.DOMAIN @@ -492,17 +496,14 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = button.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) - config["name"] = "test" - del config["platform"] - await help_test_setup_manual_entity_from_yaml(hass, platform, config) - assert hass.states.get(f"{platform}.test") is not None + await help_test_setup_manual_entity_from_yaml(hass, DEFAULT_CONFIG) + assert hass.states.get(f"{platform}.test") async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): """Test unloading the config entry.""" domain = button.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) diff --git a/tests/components/mqtt/test_camera.py b/tests/components/mqtt/test_camera.py index 4d0b2fbfca0..4cb6afb6495 100644 --- a/tests/components/mqtt/test_camera.py +++ b/tests/components/mqtt/test_camera.py @@ -37,7 +37,7 @@ from .test_common import ( help_test_setup_manual_entity_from_yaml, help_test_unique_id, help_test_unload_config_entry_with_platform, - help_test_update_with_json_attrs_bad_JSON, + help_test_update_with_json_attrs_bad_json, help_test_update_with_json_attrs_not_dict, ) @@ -195,28 +195,28 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, camera.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, camera.DOMAIN, DEFAULT_CONFIG ) async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock_entry_with_yaml_config, camera.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, camera.DOMAIN, DEFAULT_CONFIG ) async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock_entry_with_yaml_config, camera.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, camera.DOMAIN, DEFAULT_CONFIG ) async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock_entry_with_yaml_config, camera.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, camera.DOMAIN, DEFAULT_CONFIG ) @@ -225,7 +225,7 @@ async def test_setting_attribute_via_mqtt_json_message( ): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock_entry_with_yaml_config, camera.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, camera.DOMAIN, DEFAULT_CONFIG ) @@ -237,7 +237,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( hass, mqtt_mock_entry_no_yaml_config, camera.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, MQTT_CAMERA_ATTRIBUTES_BLOCKED, ) @@ -245,7 +245,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock_entry_with_yaml_config, camera.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, camera.DOMAIN, DEFAULT_CONFIG ) @@ -258,20 +258,20 @@ async def test_update_with_json_attrs_not_dict( mqtt_mock_entry_with_yaml_config, caplog, camera.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) -async def test_update_with_json_attrs_bad_JSON( +async def test_update_with_json_attrs_bad_json( hass, mqtt_mock_entry_with_yaml_config, caplog ): """Test attributes get extracted from a JSON result.""" - await help_test_update_with_json_attrs_bad_JSON( + await help_test_update_with_json_attrs_bad_json( hass, mqtt_mock_entry_with_yaml_config, caplog, camera.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -282,27 +282,27 @@ async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplo mqtt_mock_entry_no_yaml_config, caplog, camera.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one camera per unique_id.""" config = { - camera.DOMAIN: [ - { - "platform": "mqtt", - "name": "Test 1", - "topic": "test-topic", - "unique_id": "TOTALLY_UNIQUE", - }, - { - "platform": "mqtt", - "name": "Test 2", - "topic": "test-topic", - "unique_id": "TOTALLY_UNIQUE", - }, - ] + mqtt.DOMAIN: { + camera.DOMAIN: [ + { + "name": "Test 1", + "topic": "test-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + { + "name": "Test 2", + "topic": "test-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + ] + } } await help_test_unique_id( hass, mqtt_mock_entry_with_yaml_config, camera.DOMAIN, config @@ -311,7 +311,7 @@ async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): async def test_discovery_removal_camera(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered camera.""" - data = json.dumps(DEFAULT_CONFIG_LEGACY[camera.DOMAIN]) + data = json.dumps(DEFAULT_CONFIG[mqtt.DOMAIN][camera.DOMAIN]) await help_test_discovery_removal( hass, mqtt_mock_entry_no_yaml_config, caplog, camera.DOMAIN, data ) @@ -359,28 +359,28 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT camera device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock_entry_no_yaml_config, camera.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, camera.DOMAIN, DEFAULT_CONFIG ) async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT camera device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock_entry_no_yaml_config, camera.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, camera.DOMAIN, DEFAULT_CONFIG ) async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock_entry_no_yaml_config, camera.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, camera.DOMAIN, DEFAULT_CONFIG ) async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock_entry_no_yaml_config, camera.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, camera.DOMAIN, DEFAULT_CONFIG ) @@ -390,7 +390,7 @@ async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_co hass, mqtt_mock_entry_with_yaml_config, camera.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ["test_topic"], ) @@ -398,7 +398,7 @@ async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_co async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock_entry_no_yaml_config, camera.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, camera.DOMAIN, DEFAULT_CONFIG ) @@ -408,7 +408,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): hass, mqtt_mock_entry_no_yaml_config, camera.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, None, state_topic="test_topic", state_payload=b"ON", @@ -418,12 +418,14 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = camera.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = camera.DOMAIN @@ -434,17 +436,14 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = camera.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) - config["name"] = "test" - del config["platform"] - await help_test_setup_manual_entity_from_yaml(hass, platform, config) - assert hass.states.get(f"{platform}.test") is not None + await help_test_setup_manual_entity_from_yaml(hass, DEFAULT_CONFIG) + assert hass.states.get(f"{platform}.test") async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): """Test unloading the config entry.""" domain = camera.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index 14bd9084abe..ec2501e11d3 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -52,7 +52,7 @@ from .test_common import ( help_test_setup_manual_entity_from_yaml, help_test_unique_id, help_test_unload_config_entry_with_platform, - help_test_update_with_json_attrs_bad_JSON, + help_test_update_with_json_attrs_bad_json, help_test_update_with_json_attrs_not_dict, ) @@ -686,28 +686,28 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, climate.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, climate.DOMAIN, DEFAULT_CONFIG ) async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock_entry_with_yaml_config, climate.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, climate.DOMAIN, DEFAULT_CONFIG ) async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock_entry_with_yaml_config, climate.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, climate.DOMAIN, DEFAULT_CONFIG ) async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock_entry_with_yaml_config, climate.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, climate.DOMAIN, DEFAULT_CONFIG ) @@ -994,7 +994,7 @@ async def test_setting_attribute_via_mqtt_json_message( ): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock_entry_with_yaml_config, climate.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, climate.DOMAIN, DEFAULT_CONFIG ) @@ -1006,7 +1006,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( hass, mqtt_mock_entry_no_yaml_config, climate.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, MQTT_CLIMATE_ATTRIBUTES_BLOCKED, ) @@ -1014,7 +1014,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock_entry_with_yaml_config, climate.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, climate.DOMAIN, DEFAULT_CONFIG ) @@ -1027,20 +1027,20 @@ async def test_update_with_json_attrs_not_dict( mqtt_mock_entry_with_yaml_config, caplog, climate.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) -async def test_update_with_json_attrs_bad_JSON( +async def test_update_with_json_attrs_bad_json( hass, mqtt_mock_entry_with_yaml_config, caplog ): """Test attributes get extracted from a JSON result.""" - await help_test_update_with_json_attrs_bad_JSON( + await help_test_update_with_json_attrs_bad_json( hass, mqtt_mock_entry_with_yaml_config, caplog, climate.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -1051,29 +1051,29 @@ async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplo mqtt_mock_entry_no_yaml_config, caplog, climate.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one climate per unique_id.""" config = { - climate.DOMAIN: [ - { - "platform": "mqtt", - "name": "Test 1", - "power_state_topic": "test-topic", - "power_command_topic": "test_topic", - "unique_id": "TOTALLY_UNIQUE", - }, - { - "platform": "mqtt", - "name": "Test 2", - "power_state_topic": "test-topic", - "power_command_topic": "test_topic", - "unique_id": "TOTALLY_UNIQUE", - }, - ] + mqtt.DOMAIN: { + climate.DOMAIN: [ + { + "name": "Test 1", + "power_state_topic": "test-topic", + "power_command_topic": "test_topic", + "unique_id": "TOTALLY_UNIQUE", + }, + { + "name": "Test 2", + "power_state_topic": "test-topic", + "power_command_topic": "test_topic", + "unique_id": "TOTALLY_UNIQUE", + }, + ] + } } await help_test_unique_id( hass, mqtt_mock_entry_with_yaml_config, climate.DOMAIN, config @@ -1106,7 +1106,7 @@ async def test_encoding_subscribable_topics( attribute_value, ): """Test handling of incoming encoded payload.""" - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[climate.DOMAIN]) + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][climate.DOMAIN]) await help_test_encoding_subscribable_topics( hass, mqtt_mock_entry_with_yaml_config, @@ -1122,7 +1122,7 @@ async def test_encoding_subscribable_topics( async def test_discovery_removal_climate(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered climate.""" - data = json.dumps(DEFAULT_CONFIG_LEGACY[climate.DOMAIN]) + data = json.dumps(DEFAULT_CONFIG[mqtt.DOMAIN][climate.DOMAIN]) await help_test_discovery_removal( hass, mqtt_mock_entry_no_yaml_config, caplog, climate.DOMAIN, data ) @@ -1168,39 +1168,40 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT climate device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock_entry_no_yaml_config, climate.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, climate.DOMAIN, DEFAULT_CONFIG ) async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT climate device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock_entry_no_yaml_config, climate.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, climate.DOMAIN, DEFAULT_CONFIG ) async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock_entry_no_yaml_config, climate.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, climate.DOMAIN, DEFAULT_CONFIG ) async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock_entry_no_yaml_config, climate.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, climate.DOMAIN, DEFAULT_CONFIG ) async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" config = { - climate.DOMAIN: { - "platform": "mqtt", - "name": "test", - "mode_state_topic": "test-topic", - "availability_topic": "avty-topic", + mqtt.DOMAIN: { + climate.DOMAIN: { + "name": "test", + "mode_state_topic": "test-topic", + "availability_topic": "avty-topic", + } } } await help_test_entity_id_update_subscriptions( @@ -1215,18 +1216,19 @@ async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_co async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock_entry_no_yaml_config, climate.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, climate.DOMAIN, DEFAULT_CONFIG ) async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" config = { - climate.DOMAIN: { - "platform": "mqtt", - "name": "test", - "mode_command_topic": "command-topic", - "mode_state_topic": "test-topic", + mqtt.DOMAIN: { + climate.DOMAIN: { + "name": "test", + "mode_command_topic": "command-topic", + "mode_state_topic": "test-topic", + } } } await help_test_entity_debug_info_message( @@ -1375,10 +1377,10 @@ async def test_publishing_with_custom_encoding( ): """Test publishing MQTT payload with different encoding.""" domain = climate.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) + config = copy.deepcopy(DEFAULT_CONFIG) if topic != "preset_mode_command_topic": - del config["preset_mode_command_topic"] - del config["preset_modes"] + del config[mqtt.DOMAIN][domain]["preset_mode_command_topic"] + del config[mqtt.DOMAIN][domain]["preset_modes"] await help_test_publishing_with_custom_encoding( hass, @@ -1397,12 +1399,14 @@ async def test_publishing_with_custom_encoding( async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = climate.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = climate.DOMAIN @@ -1413,17 +1417,14 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = climate.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) - config["name"] = "test" - del config["platform"] - await help_test_setup_manual_entity_from_yaml(hass, platform, config) - assert hass.states.get(f"{platform}.test") is not None + await help_test_setup_manual_entity_from_yaml(hass, DEFAULT_CONFIG) + assert hass.states.get(f"{platform}.test") async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): """Test unloading the config entry.""" domain = climate.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) diff --git a/tests/components/mqtt/test_common.py b/tests/components/mqtt/test_common.py index ac8ef98531e..e2411f9fc6c 100644 --- a/tests/components/mqtt/test_common.py +++ b/tests/components/mqtt/test_common.py @@ -53,7 +53,7 @@ async def help_test_availability_when_connection_lost( hass, mqtt_mock_entry_with_yaml_config, domain, config ): """Test availability after MQTT disconnection.""" - assert await async_setup_component(hass, domain, config) + assert await async_setup_component(hass, mqtt.DOMAIN, config) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -72,8 +72,8 @@ async def help_test_availability_without_topic( hass, mqtt_mock_entry_with_yaml_config, domain, config ): """Test availability without defined availability topic.""" - assert "availability_topic" not in config[domain] - assert await async_setup_component(hass, domain, config) + assert "availability_topic" not in config[mqtt.DOMAIN][domain] + assert await async_setup_component(hass, mqtt.DOMAIN, config) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -96,10 +96,10 @@ async def help_test_default_availability_payload( """ # Add availability settings to config config = copy.deepcopy(config) - config[domain]["availability_topic"] = "availability-topic" + config[mqtt.DOMAIN][domain]["availability_topic"] = "availability-topic" assert await async_setup_component( hass, - domain, + mqtt.DOMAIN, config, ) await hass.async_block_till_done() @@ -147,13 +147,13 @@ async def help_test_default_availability_list_payload( """ # Add availability settings to config config = copy.deepcopy(config) - config[domain]["availability"] = [ + config[mqtt.DOMAIN][domain]["availability"] = [ {"topic": "availability-topic1"}, {"topic": "availability-topic2"}, ] assert await async_setup_component( hass, - domain, + mqtt.DOMAIN, config, ) await hass.async_block_till_done() @@ -213,14 +213,14 @@ async def help_test_default_availability_list_payload_all( """ # Add availability settings to config config = copy.deepcopy(config) - config[domain]["availability_mode"] = "all" - config[domain]["availability"] = [ + config[mqtt.DOMAIN][domain]["availability_mode"] = "all" + config[mqtt.DOMAIN][domain]["availability"] = [ {"topic": "availability-topic1"}, {"topic": "availability-topic2"}, ] assert await async_setup_component( hass, - domain, + mqtt.DOMAIN, config, ) await hass.async_block_till_done() @@ -281,14 +281,14 @@ async def help_test_default_availability_list_payload_any( """ # Add availability settings to config config = copy.deepcopy(config) - config[domain]["availability_mode"] = "any" - config[domain]["availability"] = [ + config[mqtt.DOMAIN][domain]["availability_mode"] = "any" + config[mqtt.DOMAIN][domain]["availability"] = [ {"topic": "availability-topic1"}, {"topic": "availability-topic2"}, ] assert await async_setup_component( hass, - domain, + mqtt.DOMAIN, config, ) await hass.async_block_till_done() @@ -331,7 +331,6 @@ async def help_test_default_availability_list_payload_any( async def help_test_default_availability_list_single( hass, - mqtt_mock_entry_with_yaml_config, caplog, domain, config, @@ -345,22 +344,17 @@ async def help_test_default_availability_list_single( """ # Add availability settings to config config = copy.deepcopy(config) - config[domain]["availability"] = [ + config[mqtt.DOMAIN][domain]["availability"] = [ {"topic": "availability-topic1"}, ] - config[domain]["availability_topic"] = "availability-topic" - assert await async_setup_component( + config[mqtt.DOMAIN][domain]["availability_topic"] = "availability-topic" + assert not await async_setup_component( hass, - domain, + mqtt.DOMAIN, config, ) - await hass.async_block_till_done() - await mqtt_mock_entry_with_yaml_config() - - state = hass.states.get(f"{domain}.test") - assert state is None assert ( - "Invalid config for [sensor.mqtt]: two or more values in the same group of exclusion 'availability'" + "Invalid config for [mqtt]: two or more values in the same group of exclusion 'availability'" in caplog.text ) @@ -380,12 +374,12 @@ async def help_test_custom_availability_payload( """ # Add availability settings to config config = copy.deepcopy(config) - config[domain]["availability_topic"] = "availability-topic" - config[domain]["payload_available"] = "good" - config[domain]["payload_not_available"] = "nogood" + config[mqtt.DOMAIN][domain]["availability_topic"] = "availability-topic" + config[mqtt.DOMAIN][domain]["payload_available"] = "good" + config[mqtt.DOMAIN][domain]["payload_not_available"] = "nogood" assert await async_setup_component( hass, - domain, + mqtt.DOMAIN, config, ) await hass.async_block_till_done() @@ -434,17 +428,17 @@ async def help_test_discovery_update_availability( await mqtt_mock_entry_no_yaml_config() # Add availability settings to config config1 = copy.deepcopy(config) - config1[domain]["availability_topic"] = "availability-topic1" + config1[mqtt.DOMAIN][domain]["availability_topic"] = "availability-topic1" config2 = copy.deepcopy(config) - config2[domain]["availability"] = [ + config2[mqtt.DOMAIN][domain]["availability"] = [ {"topic": "availability-topic2"}, {"topic": "availability-topic3"}, ] config3 = copy.deepcopy(config) - config3[domain]["availability_topic"] = "availability-topic4" - data1 = json.dumps(config1[domain]) - data2 = json.dumps(config2[domain]) - data3 = json.dumps(config3[domain]) + config3[mqtt.DOMAIN][domain]["availability_topic"] = "availability-topic4" + data1 = json.dumps(config1[mqtt.DOMAIN][domain]) + data2 = json.dumps(config2[mqtt.DOMAIN][domain]) + data3 = json.dumps(config3[mqtt.DOMAIN][domain]) async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data1) await hass.async_block_till_done() @@ -508,10 +502,10 @@ async def help_test_setting_attribute_via_mqtt_json_message( """ # Add JSON attributes settings to config config = copy.deepcopy(config) - config[domain]["json_attributes_topic"] = "attr-topic" + config[mqtt.DOMAIN][domain]["json_attributes_topic"] = "attr-topic" assert await async_setup_component( hass, - domain, + mqtt.DOMAIN, config, ) await hass.async_block_till_done() @@ -535,8 +529,8 @@ async def help_test_setting_blocked_attribute_via_mqtt_json_message( # Add JSON attributes settings to config config = copy.deepcopy(config) - config[domain]["json_attributes_topic"] = "attr-topic" - data = json.dumps(config[domain]) + config[mqtt.DOMAIN][domain]["json_attributes_topic"] = "attr-topic" + data = json.dumps(config[mqtt.DOMAIN][domain]) async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data) await hass.async_block_till_done() val = "abc123" @@ -561,11 +555,13 @@ async def help_test_setting_attribute_with_template( """ # Add JSON attributes settings to config config = copy.deepcopy(config) - config[domain]["json_attributes_topic"] = "attr-topic" - config[domain]["json_attributes_template"] = "{{ value_json['Timer1'] | tojson }}" + config[mqtt.DOMAIN][domain]["json_attributes_topic"] = "attr-topic" + config[mqtt.DOMAIN][domain][ + "json_attributes_template" + ] = "{{ value_json['Timer1'] | tojson }}" assert await async_setup_component( hass, - domain, + mqtt.DOMAIN, config, ) await hass.async_block_till_done() @@ -589,10 +585,10 @@ async def help_test_update_with_json_attrs_not_dict( """ # Add JSON attributes settings to config config = copy.deepcopy(config) - config[domain]["json_attributes_topic"] = "attr-topic" + config[mqtt.DOMAIN][domain]["json_attributes_topic"] = "attr-topic" assert await async_setup_component( hass, - domain, + mqtt.DOMAIN, config, ) await hass.async_block_till_done() @@ -605,7 +601,7 @@ async def help_test_update_with_json_attrs_not_dict( assert "JSON result was not a dictionary" in caplog.text -async def help_test_update_with_json_attrs_bad_JSON( +async def help_test_update_with_json_attrs_bad_json( hass, mqtt_mock_entry_with_yaml_config, caplog, domain, config ): """Test JSON validation of attributes. @@ -614,10 +610,10 @@ async def help_test_update_with_json_attrs_bad_JSON( """ # Add JSON attributes settings to config config = copy.deepcopy(config) - config[domain]["json_attributes_topic"] = "attr-topic" + config[mqtt.DOMAIN][domain]["json_attributes_topic"] = "attr-topic" assert await async_setup_component( hass, - domain, + mqtt.DOMAIN, config, ) await hass.async_block_till_done() @@ -640,11 +636,11 @@ async def help_test_discovery_update_attr( await mqtt_mock_entry_no_yaml_config() # Add JSON attributes settings to config config1 = copy.deepcopy(config) - config1[domain]["json_attributes_topic"] = "attr-topic1" + config1[mqtt.DOMAIN][domain]["json_attributes_topic"] = "attr-topic1" config2 = copy.deepcopy(config) - config2[domain]["json_attributes_topic"] = "attr-topic2" - data1 = json.dumps(config1[domain]) - data2 = json.dumps(config2[domain]) + config2[mqtt.DOMAIN][domain]["json_attributes_topic"] = "attr-topic2" + data1 = json.dumps(config1[mqtt.DOMAIN][domain]) + data2 = json.dumps(config2[mqtt.DOMAIN][domain]) async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data1) await hass.async_block_till_done() @@ -669,7 +665,7 @@ async def help_test_discovery_update_attr( async def help_test_unique_id(hass, mqtt_mock_entry_with_yaml_config, domain, config): """Test unique id option only creates one entity per unique_id.""" - assert await async_setup_component(hass, domain, config) + assert await async_setup_component(hass, mqtt.DOMAIN, config) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() assert len(hass.states.async_entity_ids(domain)) == 1 @@ -881,7 +877,7 @@ async def help_test_encoding_subscribable_topics( await hass.async_block_till_done() assert await async_setup_component( - hass, domain, {domain: [config1, config2, config3]} + hass, mqtt.DOMAIN, {mqtt.DOMAIN: {domain: [config1, config2, config3]}} ) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -944,7 +940,7 @@ async def help_test_entity_device_info_with_identifier( """ await mqtt_mock_entry_no_yaml_config() # Add device settings to config - config = copy.deepcopy(config[domain]) + config = copy.deepcopy(config[mqtt.DOMAIN][domain]) config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) config["unique_id"] = "veryunique" @@ -975,7 +971,7 @@ async def help_test_entity_device_info_with_connection( """ await mqtt_mock_entry_no_yaml_config() # Add device settings to config - config = copy.deepcopy(config[domain]) + config = copy.deepcopy(config[mqtt.DOMAIN][domain]) config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_MAC) config["unique_id"] = "veryunique" @@ -1005,7 +1001,7 @@ async def help_test_entity_device_info_remove( """Test device registry remove.""" await mqtt_mock_entry_no_yaml_config() # Add device settings to config - config = copy.deepcopy(config[domain]) + config = copy.deepcopy(config[mqtt.DOMAIN][domain]) config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) config["unique_id"] = "veryunique" @@ -1037,7 +1033,7 @@ async def help_test_entity_device_info_update( """ await mqtt_mock_entry_no_yaml_config() # Add device settings to config - config = copy.deepcopy(config[domain]) + config = copy.deepcopy(config[mqtt.DOMAIN][domain]) config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) config["unique_id"] = "veryunique" @@ -1067,19 +1063,19 @@ async def help_test_entity_id_update_subscriptions( """Test MQTT subscriptions are managed when entity_id is updated.""" # Add unique_id to config config = copy.deepcopy(config) - config[domain]["unique_id"] = "TOTALLY_UNIQUE" + config[mqtt.DOMAIN][domain]["unique_id"] = "TOTALLY_UNIQUE" if topics is None: # Add default topics to config - config[domain]["availability_topic"] = "avty-topic" - config[domain]["state_topic"] = "test-topic" + config[mqtt.DOMAIN][domain]["availability_topic"] = "avty-topic" + config[mqtt.DOMAIN][domain]["state_topic"] = "test-topic" topics = ["avty-topic", "test-topic"] assert len(topics) > 0 registry = mock_registry(hass, {}) assert await async_setup_component( hass, - domain, + mqtt.DOMAIN, config, ) await hass.async_block_till_done() @@ -1111,16 +1107,16 @@ async def help_test_entity_id_update_discovery_update( # Add unique_id to config await mqtt_mock_entry_no_yaml_config() config = copy.deepcopy(config) - config[domain]["unique_id"] = "TOTALLY_UNIQUE" + config[mqtt.DOMAIN][domain]["unique_id"] = "TOTALLY_UNIQUE" if topic is None: # Add default topic to config - config[domain]["availability_topic"] = "avty-topic" + config[mqtt.DOMAIN][domain]["availability_topic"] = "avty-topic" topic = "avty-topic" ent_registry = mock_registry(hass, {}) - data = json.dumps(config[domain]) + data = json.dumps(config[mqtt.DOMAIN][domain]) async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data) await hass.async_block_till_done() @@ -1135,8 +1131,8 @@ async def help_test_entity_id_update_discovery_update( ent_registry.async_update_entity(f"{domain}.test", new_entity_id=f"{domain}.milk") await hass.async_block_till_done() - config[domain]["availability_topic"] = f"{topic}_2" - data = json.dumps(config[domain]) + config[mqtt.DOMAIN][domain]["availability_topic"] = f"{topic}_2" + data = json.dumps(config[mqtt.DOMAIN][domain]) async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data) await hass.async_block_till_done() assert len(hass.states.async_entity_ids(domain)) == 1 @@ -1155,9 +1151,10 @@ async def help_test_entity_debug_info( """ await mqtt_mock_entry_no_yaml_config() # Add device settings to config - config = copy.deepcopy(config[domain]) + config = copy.deepcopy(config[mqtt.DOMAIN][domain]) config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) config["unique_id"] = "veryunique" + config["platform"] = "mqtt" registry = dr.async_get(hass) @@ -1192,7 +1189,7 @@ async def help_test_entity_debug_info_max_messages( """ await mqtt_mock_entry_no_yaml_config() # Add device settings to config - config = copy.deepcopy(config[domain]) + config = copy.deepcopy(config[mqtt.DOMAIN][domain]) config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) config["unique_id"] = "veryunique" @@ -1256,7 +1253,7 @@ async def help_test_entity_debug_info_message( """ # Add device settings to config await mqtt_mock_entry_no_yaml_config() - config = copy.deepcopy(config[domain]) + config = copy.deepcopy(config[mqtt.DOMAIN][domain]) config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) config["unique_id"] = "veryunique" @@ -1359,9 +1356,10 @@ async def help_test_entity_debug_info_remove( """ await mqtt_mock_entry_no_yaml_config() # Add device settings to config - config = copy.deepcopy(config[domain]) + config = copy.deepcopy(config[mqtt.DOMAIN][domain]) config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) config["unique_id"] = "veryunique" + config["platform"] = "mqtt" registry = dr.async_get(hass) @@ -1405,9 +1403,10 @@ async def help_test_entity_debug_info_update_entity_id( """ await mqtt_mock_entry_no_yaml_config() # Add device settings to config - config = copy.deepcopy(config[domain]) + config = copy.deepcopy(config[mqtt.DOMAIN][domain]) config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) config["unique_id"] = "veryunique" + config["platform"] = "mqtt" dev_registry = dr.async_get(hass) ent_registry = mock_registry(hass, {}) @@ -1461,7 +1460,7 @@ async def help_test_entity_disabled_by_default( """Test device registry remove.""" await mqtt_mock_entry_no_yaml_config() # Add device settings to config - config = copy.deepcopy(config[domain]) + config = copy.deepcopy(config[mqtt.DOMAIN][domain]) config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) config["enabled_by_default"] = False config["unique_id"] = "veryunique1" @@ -1500,7 +1499,7 @@ async def help_test_entity_category( """Test device registry remove.""" await mqtt_mock_entry_no_yaml_config() # Add device settings to config - config = copy.deepcopy(config[domain]) + config = copy.deepcopy(config[mqtt.DOMAIN][domain]) config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) ent_registry = er.async_get(hass) @@ -1564,7 +1563,7 @@ async def help_test_publishing_with_custom_encoding( setup_config = [] service_data = {} for test_id, test_data in test_config.items(): - test_config_setup = copy.deepcopy(config) + test_config_setup = copy.deepcopy(config[mqtt.DOMAIN][domain]) test_config_setup.update( { topic: f"cmd/{test_id}", @@ -1573,7 +1572,7 @@ async def help_test_publishing_with_custom_encoding( ) if test_data["encoding"] is not None: test_config_setup["encoding"] = test_data["encoding"] - if test_data["cmd_tpl"]: + if template and test_data["cmd_tpl"]: test_config_setup[ template ] = f"{{{{ (('%.1f'|format({tpl_par}))[0] if is_number({tpl_par}) else {tpl_par}[0]) | ord | pack('b') }}}}" @@ -1587,8 +1586,8 @@ async def help_test_publishing_with_custom_encoding( # setup test entities assert await async_setup_component( hass, - domain, - {domain: setup_config}, + mqtt.DOMAIN, + {mqtt.DOMAIN: {domain: setup_config}}, ) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -1698,24 +1697,29 @@ async def help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ): """Test reloading an MQTT platform.""" + config = copy.deepcopy(config[mqtt.DOMAIN][domain]) # Create and test an old config of 2 entities based on the config supplied old_config_1 = copy.deepcopy(config) old_config_1["name"] = "test_old_1" old_config_2 = copy.deepcopy(config) old_config_2["name"] = "test_old_2" + + # Test deprecated YAML configuration under the platform key + # Scheduled to be removed in HA core 2022.12 old_config_3 = copy.deepcopy(config) old_config_3["name"] = "test_old_3" - old_config_3.pop("platform") + old_config_3["platform"] = mqtt.DOMAIN old_config_4 = copy.deepcopy(config) old_config_4["name"] = "test_old_4" - old_config_4.pop("platform") + old_config_4["platform"] = mqtt.DOMAIN old_config = { - domain: [old_config_1, old_config_2], - "mqtt": {domain: [old_config_3, old_config_4]}, + mqtt.DOMAIN: {domain: [old_config_1, old_config_2]}, + domain: [old_config_3, old_config_4], } assert await async_setup_component(hass, domain, old_config) + assert await async_setup_component(hass, mqtt.DOMAIN, old_config) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -1731,21 +1735,24 @@ async def help_test_reloadable( new_config_1["name"] = "test_new_1" new_config_2 = copy.deepcopy(config) new_config_2["name"] = "test_new_2" + new_config_extra = copy.deepcopy(config) + new_config_extra["name"] = "test_new_5" + + # Test deprecated YAML configuration under the platform key + # Scheduled to be removed in HA core 2022.12 new_config_3 = copy.deepcopy(config) new_config_3["name"] = "test_new_3" - new_config_3.pop("platform") + new_config_3["platform"] = mqtt.DOMAIN new_config_4 = copy.deepcopy(config) new_config_4["name"] = "test_new_4" - new_config_4.pop("platform") - new_config_5 = copy.deepcopy(config) - new_config_5["name"] = "test_new_5" - new_config_6 = copy.deepcopy(config) - new_config_6["name"] = "test_new_6" - new_config_6.pop("platform") + new_config_4["platform"] = mqtt.DOMAIN + new_config_extra_legacy = copy.deepcopy(config) + new_config_extra_legacy["name"] = "test_new_6" + new_config_extra_legacy["platform"] = mqtt.DOMAIN new_config = { - domain: [new_config_1, new_config_2, new_config_5], - "mqtt": {domain: [new_config_3, new_config_4, new_config_6]}, + mqtt.DOMAIN: {domain: [new_config_1, new_config_2, new_config_extra]}, + domain: [new_config_3, new_config_4, new_config_extra_legacy], } await help_test_reload_with_config(hass, caplog, tmp_path, new_config) @@ -1760,9 +1767,12 @@ async def help_test_reloadable( assert hass.states.get(f"{domain}.test_new_6") +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 async def help_test_reloadable_late(hass, caplog, tmp_path, domain, config): - """Test reloading an MQTT platform when config entry is setup late.""" + """Test reloading an MQTT platform when config entry is setup is late.""" # Create and test an old config of 2 entities based on the config supplied + # using the deprecated platform schema old_config_1 = copy.deepcopy(config) old_config_1["name"] = "test_old_1" old_config_2 = copy.deepcopy(config) @@ -1815,7 +1825,7 @@ async def help_test_reloadable_late(hass, caplog, tmp_path, domain, config): assert hass.states.get(f"{domain}.test_new_3") -async def help_test_setup_manual_entity_from_yaml(hass, platform, config): +async def help_test_setup_manual_entity_from_yaml(hass, config): """Help to test setup from yaml through configuration entry.""" calls = MagicMock() @@ -1823,9 +1833,7 @@ async def help_test_setup_manual_entity_from_yaml(hass, platform, config): """Mock reload.""" calls() - config_structure = {mqtt.DOMAIN: {platform: config}} - - await async_setup_component(hass, mqtt.DOMAIN, config_structure) + assert await async_setup_component(hass, mqtt.DOMAIN, config) # Mock config entry entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"}) entry.add_to_hass(hass) @@ -1864,14 +1872,14 @@ async def help_test_unload_config_entry_with_platform( """Test unloading the MQTT config entry with a specific platform domain.""" # prepare setup through configuration.yaml config_setup = copy.deepcopy(config) - config_setup["name"] = "config_setup" + config_setup[mqtt.DOMAIN][domain]["name"] = "config_setup" config_name = config_setup - assert await async_setup_component(hass, domain, {domain: [config_setup]}) + assert await async_setup_component(hass, mqtt.DOMAIN, config_setup) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() # prepare setup through discovery - discovery_setup = copy.deepcopy(config) + discovery_setup = copy.deepcopy(config[mqtt.DOMAIN][domain]) discovery_setup["name"] = "discovery_setup" async_fire_mqtt_message( hass, f"homeassistant/{domain}/bla/config", json.dumps(discovery_setup) diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index a91f0ecc6ca..fb7df111d96 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -74,7 +74,7 @@ from .test_common import ( help_test_setup_manual_entity_from_yaml, help_test_unique_id, help_test_unload_config_entry_with_platform, - help_test_update_with_json_attrs_bad_JSON, + help_test_update_with_json_attrs_bad_json, help_test_update_with_json_attrs_not_dict, ) @@ -2444,28 +2444,28 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, cover.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, cover.DOMAIN, DEFAULT_CONFIG ) async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock_entry_with_yaml_config, cover.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, cover.DOMAIN, DEFAULT_CONFIG ) async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock_entry_with_yaml_config, cover.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, cover.DOMAIN, DEFAULT_CONFIG ) async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock_entry_with_yaml_config, cover.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, cover.DOMAIN, DEFAULT_CONFIG ) @@ -2514,7 +2514,7 @@ async def test_setting_attribute_via_mqtt_json_message( ): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock_entry_with_yaml_config, cover.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, cover.DOMAIN, DEFAULT_CONFIG ) @@ -2526,7 +2526,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( hass, mqtt_mock_entry_no_yaml_config, cover.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, MQTT_COVER_ATTRIBUTES_BLOCKED, ) @@ -2534,7 +2534,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock_entry_with_yaml_config, cover.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, cover.DOMAIN, DEFAULT_CONFIG ) @@ -2547,7 +2547,7 @@ async def test_update_with_json_attrs_not_dict( mqtt_mock_entry_with_yaml_config, caplog, cover.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -2555,12 +2555,12 @@ async def test_update_with_json_attrs_bad_json( hass, mqtt_mock_entry_with_yaml_config, caplog ): """Test attributes get extracted from a JSON result.""" - await help_test_update_with_json_attrs_bad_JSON( + await help_test_update_with_json_attrs_bad_json( hass, mqtt_mock_entry_with_yaml_config, caplog, cover.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -2571,27 +2571,27 @@ async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplo mqtt_mock_entry_no_yaml_config, caplog, cover.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique_id option only creates one cover per id.""" config = { - cover.DOMAIN: [ - { - "platform": "mqtt", - "name": "Test 1", - "state_topic": "test-topic", - "unique_id": "TOTALLY_UNIQUE", - }, - { - "platform": "mqtt", - "name": "Test 2", - "state_topic": "test-topic", - "unique_id": "TOTALLY_UNIQUE", - }, - ] + mqtt.DOMAIN: { + cover.DOMAIN: [ + { + "name": "Test 1", + "state_topic": "test-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + { + "name": "Test 2", + "state_topic": "test-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + ] + } } await help_test_unique_id( hass, mqtt_mock_entry_with_yaml_config, cover.DOMAIN, config @@ -2646,42 +2646,42 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT cover device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock_entry_no_yaml_config, cover.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, cover.DOMAIN, DEFAULT_CONFIG ) async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT cover device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock_entry_no_yaml_config, cover.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, cover.DOMAIN, DEFAULT_CONFIG ) async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock_entry_no_yaml_config, cover.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, cover.DOMAIN, DEFAULT_CONFIG ) async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock_entry_no_yaml_config, cover.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, cover.DOMAIN, DEFAULT_CONFIG ) async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock_entry_with_yaml_config, cover.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, cover.DOMAIN, DEFAULT_CONFIG ) async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock_entry_no_yaml_config, cover.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, cover.DOMAIN, DEFAULT_CONFIG ) @@ -2691,7 +2691,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): hass, mqtt_mock_entry_no_yaml_config, cover.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, SERVICE_OPEN_COVER, command_payload="OPEN", ) @@ -3342,8 +3342,8 @@ async def test_publishing_with_custom_encoding( ): """Test publishing MQTT payload with different encoding.""" domain = cover.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] - config["position_topic"] = "some-position-topic" + config = DEFAULT_CONFIG + config[mqtt.DOMAIN][domain]["position_topic"] = "some-position-topic" await help_test_publishing_with_custom_encoding( hass, @@ -3362,12 +3362,14 @@ async def test_publishing_with_custom_encoding( async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = cover.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = cover.DOMAIN @@ -3399,7 +3401,7 @@ async def test_encoding_subscribable_topics( mqtt_mock_entry_with_yaml_config, caplog, cover.DOMAIN, - DEFAULT_CONFIG_LEGACY[cover.DOMAIN], + DEFAULT_CONFIG[mqtt.DOMAIN][cover.DOMAIN], topic, value, attribute, @@ -3411,17 +3413,14 @@ async def test_encoding_subscribable_topics( async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = cover.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) - config["name"] = "test" - del config["platform"] - await help_test_setup_manual_entity_from_yaml(hass, platform, config) - assert hass.states.get(f"{platform}.test") is not None + await help_test_setup_manual_entity_from_yaml(hass, DEFAULT_CONFIG) + assert hass.states.get(f"{platform}.test") async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): """Test unloading the config entry.""" domain = cover.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) diff --git a/tests/components/mqtt/test_device_tracker_discovery.py b/tests/components/mqtt/test_device_tracker_discovery.py index 923ae7c9f75..876b66e8a4d 100644 --- a/tests/components/mqtt/test_device_tracker_discovery.py +++ b/tests/components/mqtt/test_device_tracker_discovery.py @@ -1,6 +1,5 @@ """The tests for the MQTT device_tracker platform.""" -import copy from unittest.mock import patch import pytest @@ -27,11 +26,6 @@ DEFAULT_CONFIG = { } } -# Test deprecated YAML configuration under the platform key -# Scheduled to be removed in HA core 2022.12 -DEFAULT_CONFIG_LEGACY = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) -DEFAULT_CONFIG_LEGACY[device_tracker.DOMAIN]["platform"] = mqtt.DOMAIN - @pytest.fixture(autouse=True) def device_tracker_platform_only(): @@ -440,7 +434,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( hass, mqtt_mock_entry_no_yaml_config, device_tracker.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, None, ) @@ -451,8 +445,10 @@ async def test_setup_with_modern_schema(hass, mock_device_tracker_conf): entity_id = f"{device_tracker.DOMAIN}.{dev_id}" topic = "/location/jan" - config = {"name": dev_id, "state_topic": topic} + config = { + mqtt.DOMAIN: {device_tracker.DOMAIN: {"name": dev_id, "state_topic": topic}} + } - await help_test_setup_manual_entity_from_yaml(hass, device_tracker.DOMAIN, config) + await help_test_setup_manual_entity_from_yaml(hass, config) assert hass.states.get(entity_id) is not None diff --git a/tests/components/mqtt/test_fan.py b/tests/components/mqtt/test_fan.py index efe38234aee..1666ccee6ce 100644 --- a/tests/components/mqtt/test_fan.py +++ b/tests/components/mqtt/test_fan.py @@ -59,7 +59,7 @@ from .test_common import ( help_test_setup_manual_entity_from_yaml, help_test_unique_id, help_test_unload_config_entry_with_platform, - help_test_update_with_json_attrs_bad_JSON, + help_test_update_with_json_attrs_bad_json, help_test_update_with_json_attrs_not_dict, ) @@ -1365,7 +1365,7 @@ async def test_encoding_subscribable_topics( attribute_value, ): """Test handling of incoming encoded payload.""" - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[fan.DOMAIN]) + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][fan.DOMAIN]) config[ATTR_PRESET_MODES] = ["eco", "auto"] config[CONF_PRESET_MODE_COMMAND_TOPIC] = "fan/some_preset_mode_command_topic" config[CONF_PERCENTAGE_COMMAND_TOPIC] = "fan/some_percentage_command_topic" @@ -1639,14 +1639,14 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, fan.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, fan.DOMAIN, DEFAULT_CONFIG ) async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock_entry_with_yaml_config, fan.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, fan.DOMAIN, DEFAULT_CONFIG ) @@ -1656,7 +1656,7 @@ async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_conf hass, mqtt_mock_entry_with_yaml_config, fan.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, True, "state-topic", "1", @@ -1669,7 +1669,7 @@ async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_confi hass, mqtt_mock_entry_with_yaml_config, fan.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, True, "state-topic", "1", @@ -1681,7 +1681,7 @@ async def test_setting_attribute_via_mqtt_json_message( ): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock_entry_with_yaml_config, fan.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, fan.DOMAIN, DEFAULT_CONFIG ) @@ -1693,7 +1693,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( hass, mqtt_mock_entry_no_yaml_config, fan.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, MQTT_FAN_ATTRIBUTES_BLOCKED, ) @@ -1701,7 +1701,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock_entry_with_yaml_config, fan.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, fan.DOMAIN, DEFAULT_CONFIG ) @@ -1714,7 +1714,7 @@ async def test_update_with_json_attrs_not_dict( mqtt_mock_entry_with_yaml_config, caplog, fan.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -1722,41 +1722,41 @@ async def test_update_with_json_attrs_bad_json( hass, mqtt_mock_entry_with_yaml_config, caplog ): """Test attributes get extracted from a JSON result.""" - await help_test_update_with_json_attrs_bad_JSON( + await help_test_update_with_json_attrs_bad_json( hass, mqtt_mock_entry_with_yaml_config, caplog, fan.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock_entry_no_yaml_config, caplog, fan.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, caplog, fan.DOMAIN, DEFAULT_CONFIG ) async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique_id option only creates one fan per id.""" config = { - fan.DOMAIN: [ - { - "platform": "mqtt", - "name": "Test 1", - "state_topic": "test-topic", - "command_topic": "test_topic", - "unique_id": "TOTALLY_UNIQUE", - }, - { - "platform": "mqtt", - "name": "Test 2", - "state_topic": "test-topic", - "command_topic": "test_topic", - "unique_id": "TOTALLY_UNIQUE", - }, - ] + mqtt.DOMAIN: { + fan.DOMAIN: [ + { + "name": "Test 1", + "state_topic": "test-topic", + "command_topic": "test_topic", + "unique_id": "TOTALLY_UNIQUE", + }, + { + "name": "Test 2", + "state_topic": "test-topic", + "command_topic": "test_topic", + "unique_id": "TOTALLY_UNIQUE", + }, + ] + } } await help_test_unique_id( hass, mqtt_mock_entry_with_yaml_config, fan.DOMAIN, config @@ -1812,42 +1812,42 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT fan device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock_entry_no_yaml_config, fan.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, fan.DOMAIN, DEFAULT_CONFIG ) async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT fan device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock_entry_no_yaml_config, fan.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, fan.DOMAIN, DEFAULT_CONFIG ) async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock_entry_no_yaml_config, fan.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, fan.DOMAIN, DEFAULT_CONFIG ) async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock_entry_no_yaml_config, fan.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, fan.DOMAIN, DEFAULT_CONFIG ) async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock_entry_with_yaml_config, fan.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, fan.DOMAIN, DEFAULT_CONFIG ) async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock_entry_no_yaml_config, fan.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, fan.DOMAIN, DEFAULT_CONFIG ) @@ -1857,7 +1857,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): hass, mqtt_mock_entry_no_yaml_config, fan.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, fan.SERVICE_TURN_ON, ) @@ -1914,9 +1914,9 @@ async def test_publishing_with_custom_encoding( ): """Test publishing MQTT payload with different encoding.""" domain = fan.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) + config = copy.deepcopy(DEFAULT_CONFIG) if topic == "preset_mode_command_topic": - config["preset_modes"] = ["auto", "eco"] + config[mqtt.DOMAIN][domain]["preset_modes"] = ["auto", "eco"] await help_test_publishing_with_custom_encoding( hass, @@ -1935,12 +1935,14 @@ async def test_publishing_with_custom_encoding( async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = fan.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = fan.DOMAIN @@ -1951,17 +1953,14 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = fan.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) - config["name"] = "test" - del config["platform"] - await help_test_setup_manual_entity_from_yaml(hass, platform, config) - assert hass.states.get(f"{platform}.test") is not None + await help_test_setup_manual_entity_from_yaml(hass, DEFAULT_CONFIG) + assert hass.states.get(f"{platform}.test") async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): """Test unloading the config entry.""" domain = fan.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) diff --git a/tests/components/mqtt/test_humidifier.py b/tests/components/mqtt/test_humidifier.py index 0cc2be638bf..1e2d64b66cf 100644 --- a/tests/components/mqtt/test_humidifier.py +++ b/tests/components/mqtt/test_humidifier.py @@ -61,7 +61,7 @@ from .test_common import ( help_test_setup_manual_entity_from_yaml, help_test_unique_id, help_test_unload_config_entry_with_platform, - help_test_update_with_json_attrs_bad_JSON, + help_test_update_with_json_attrs_bad_json, help_test_update_with_json_attrs_not_dict, ) @@ -750,7 +750,7 @@ async def test_encoding_subscribable_topics( attribute_value, ): """Test handling of incoming encoded payload.""" - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[humidifier.DOMAIN]) + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][humidifier.DOMAIN]) config["modes"] = ["eco", "auto"] config[CONF_MODE_COMMAND_TOPIC] = "humidifier/some_mode_command_topic" await help_test_encoding_subscribable_topics( @@ -996,14 +996,14 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG ) async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock_entry_with_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG ) @@ -1013,7 +1013,7 @@ async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_conf hass, mqtt_mock_entry_with_yaml_config, humidifier.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, True, "state-topic", "1", @@ -1026,7 +1026,7 @@ async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_confi hass, mqtt_mock_entry_with_yaml_config, humidifier.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, True, "state-topic", "1", @@ -1038,7 +1038,7 @@ async def test_setting_attribute_via_mqtt_json_message( ): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock_entry_with_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG ) @@ -1050,7 +1050,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( hass, mqtt_mock_entry_no_yaml_config, humidifier.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, MQTT_HUMIDIFIER_ATTRIBUTES_BLOCKED, ) @@ -1058,7 +1058,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock_entry_with_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG ) @@ -1071,7 +1071,7 @@ async def test_update_with_json_attrs_not_dict( mqtt_mock_entry_with_yaml_config, caplog, humidifier.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -1079,12 +1079,12 @@ async def test_update_with_json_attrs_bad_json( hass, mqtt_mock_entry_with_yaml_config, caplog ): """Test attributes get extracted from a JSON result.""" - await help_test_update_with_json_attrs_bad_JSON( + await help_test_update_with_json_attrs_bad_json( hass, mqtt_mock_entry_with_yaml_config, caplog, humidifier.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -1095,31 +1095,31 @@ async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplo mqtt_mock_entry_no_yaml_config, caplog, humidifier.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique_id option only creates one fan per id.""" config = { - humidifier.DOMAIN: [ - { - "platform": "mqtt", - "name": "Test 1", - "state_topic": "test-topic", - "command_topic": "test_topic", - "target_humidity_command_topic": "humidity-command-topic", - "unique_id": "TOTALLY_UNIQUE", - }, - { - "platform": "mqtt", - "name": "Test 2", - "state_topic": "test-topic", - "command_topic": "test_topic", - "target_humidity_command_topic": "humidity-command-topic", - "unique_id": "TOTALLY_UNIQUE", - }, - ] + mqtt.DOMAIN: { + humidifier.DOMAIN: [ + { + "name": "Test 1", + "state_topic": "test-topic", + "command_topic": "test_topic", + "target_humidity_command_topic": "humidity-command-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + { + "name": "Test 2", + "state_topic": "test-topic", + "command_topic": "test_topic", + "target_humidity_command_topic": "humidity-command-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + ] + } } await help_test_unique_id( hass, mqtt_mock_entry_with_yaml_config, humidifier.DOMAIN, config @@ -1191,42 +1191,42 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT fan device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock_entry_no_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG ) async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT fan device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock_entry_no_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG ) async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock_entry_no_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG ) async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock_entry_no_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG ) async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock_entry_with_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG ) async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock_entry_no_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG ) @@ -1236,7 +1236,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): hass, mqtt_mock_entry_no_yaml_config, humidifier.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, humidifier.SERVICE_TURN_ON, ) @@ -1286,9 +1286,9 @@ async def test_publishing_with_custom_encoding( ): """Test publishing MQTT payload with different encoding.""" domain = humidifier.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) + config = copy.deepcopy(DEFAULT_CONFIG) if topic == "mode_command_topic": - config["modes"] = ["auto", "eco"] + config[mqtt.DOMAIN][domain]["modes"] = ["auto", "eco"] await help_test_publishing_with_custom_encoding( hass, @@ -1307,12 +1307,14 @@ async def test_publishing_with_custom_encoding( async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = humidifier.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = humidifier.DOMAIN @@ -1323,11 +1325,8 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = humidifier.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) - config["name"] = "test" - del config["platform"] - await help_test_setup_manual_entity_from_yaml(hass, platform, config) - assert hass.states.get(f"{platform}.test") is not None + await help_test_setup_manual_entity_from_yaml(hass, DEFAULT_CONFIG) + assert hass.states.get(f"{platform}.test") async def test_config_schema_validation(hass): @@ -1344,7 +1343,7 @@ async def test_config_schema_validation(hass): async def test_unload_config_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): """Test unloading the config entry.""" domain = humidifier.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index b794d9260ba..b76979cc990 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -1485,9 +1485,17 @@ async def test_setup_override_configuration(hass, caplog, tmp_path): @patch("homeassistant.components.mqtt.PLATFORMS", []) async def test_setup_manual_mqtt_with_platform_key(hass, caplog): """Test set up a manual MQTT item with a platform key.""" - config = {"platform": "mqtt", "name": "test", "command_topic": "test-topic"} + config = { + mqtt.DOMAIN: { + "light": { + "platform": "mqtt", + "name": "test", + "command_topic": "test-topic", + } + } + } with pytest.raises(AssertionError): - await help_test_setup_manual_entity_from_yaml(hass, "light", config) + await help_test_setup_manual_entity_from_yaml(hass, config) assert ( "Invalid config for [mqtt]: [platform] is an invalid option for [mqtt]" in caplog.text @@ -1497,9 +1505,9 @@ async def test_setup_manual_mqtt_with_platform_key(hass, caplog): @patch("homeassistant.components.mqtt.PLATFORMS", []) async def test_setup_manual_mqtt_with_invalid_config(hass, caplog): """Test set up a manual MQTT item with an invalid config.""" - config = {"name": "test"} + config = {mqtt.DOMAIN: {"light": {"name": "test"}}} with pytest.raises(AssertionError): - await help_test_setup_manual_entity_from_yaml(hass, "light", config) + await help_test_setup_manual_entity_from_yaml(hass, config) assert ( "Invalid config for [mqtt]: required key not provided @ data['mqtt']['light'][0]['command_topic']." " Got None. (See ?, line ?)" in caplog.text @@ -1509,8 +1517,8 @@ async def test_setup_manual_mqtt_with_invalid_config(hass, caplog): @patch("homeassistant.components.mqtt.PLATFORMS", []) async def test_setup_manual_mqtt_empty_platform(hass, caplog): """Test set up a manual MQTT platform without items.""" - config = [] - await help_test_setup_manual_entity_from_yaml(hass, "light", config) + config = {mqtt.DOMAIN: {"light": []}} + await help_test_setup_manual_entity_from_yaml(hass, config) assert "voluptuous.error.MultipleInvalid" not in caplog.text @@ -2797,7 +2805,11 @@ async def test_publish_or_subscribe_without_valid_config_entry(hass, caplog): @patch("homeassistant.components.mqtt.PLATFORMS", [Platform.LIGHT]) async def test_reload_entry_with_new_config(hass, tmp_path): """Test reloading the config entry with a new yaml config.""" - config_old = [{"name": "test_old1", "command_topic": "test-topic_old"}] + # Test deprecated YAML configuration under the platform key + # Scheduled to be removed in HA core 2022.12 + config_old = { + "mqtt": {"light": [{"name": "test_old1", "command_topic": "test-topic_old"}]} + } config_yaml_new = { "mqtt": { "light": [{"name": "test_new_modern", "command_topic": "test-topic_new"}] @@ -2812,7 +2824,7 @@ async def test_reload_entry_with_new_config(hass, tmp_path): } ], } - await help_test_setup_manual_entity_from_yaml(hass, "light", config_old) + await help_test_setup_manual_entity_from_yaml(hass, config_old) assert hass.states.get("light.test_old1") is not None await help_test_entry_reload_with_new_config(hass, tmp_path, config_yaml_new) @@ -2824,7 +2836,9 @@ async def test_reload_entry_with_new_config(hass, tmp_path): @patch("homeassistant.components.mqtt.PLATFORMS", [Platform.LIGHT]) async def test_disabling_and_enabling_entry(hass, tmp_path, caplog): """Test disabling and enabling the config entry.""" - config_old = [{"name": "test_old1", "command_topic": "test-topic_old"}] + config_old = { + "mqtt": {"light": [{"name": "test_old1", "command_topic": "test-topic_old"}]} + } config_yaml_new = { "mqtt": { "light": [{"name": "test_new_modern", "command_topic": "test-topic_new"}] @@ -2839,7 +2853,7 @@ async def test_disabling_and_enabling_entry(hass, tmp_path, caplog): } ], } - await help_test_setup_manual_entity_from_yaml(hass, "light", config_old) + await help_test_setup_manual_entity_from_yaml(hass, config_old) assert hass.states.get("light.test_old1") is not None mqtt_config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] @@ -2929,7 +2943,9 @@ async def test_setup_manual_items_with_unique_ids( hass, tmp_path, caplog, config, unique ): """Test setup manual items is generating unique id's.""" - await help_test_setup_manual_entity_from_yaml(hass, "light", config) + await help_test_setup_manual_entity_from_yaml( + hass, {mqtt.DOMAIN: {"light": config}} + ) assert hass.states.get("light.test1") is not None assert (hass.states.get("light.test2") is not None) == unique diff --git a/tests/components/mqtt/test_legacy_vacuum.py b/tests/components/mqtt/test_legacy_vacuum.py index ffe9183fe4c..4b44807b7c9 100644 --- a/tests/components/mqtt/test_legacy_vacuum.py +++ b/tests/components/mqtt/test_legacy_vacuum.py @@ -58,7 +58,7 @@ from .test_common import ( help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_setup_manual_entity_from_yaml, help_test_unique_id, - help_test_update_with_json_attrs_bad_JSON, + help_test_update_with_json_attrs_bad_json, help_test_update_with_json_attrs_not_dict, ) @@ -95,8 +95,6 @@ DEFAULT_CONFIG_2 = {mqtt.DOMAIN: {vacuum.DOMAIN: {"name": "test"}}} # Scheduled to be removed in HA core 2022.12 DEFAULT_CONFIG_LEGACY = deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) DEFAULT_CONFIG_LEGACY[vacuum.DOMAIN][CONF_PLATFORM] = mqtt.DOMAIN -DEFAULT_CONFIG_2_LEGACY = deepcopy(DEFAULT_CONFIG_2[mqtt.DOMAIN]) -DEFAULT_CONFIG_2_LEGACY[vacuum.DOMAIN][CONF_PLATFORM] = mqtt.DOMAIN @pytest.fixture(autouse=True) @@ -108,7 +106,7 @@ def vacuum_platform_only(): async def test_default_supported_features(hass, mqtt_mock_entry_with_yaml_config): """Test that the correct supported features.""" - assert await async_setup_component(hass, vacuum.DOMAIN, DEFAULT_CONFIG_LEGACY) + assert await async_setup_component(hass, mqtt.DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() entity = hass.states.get("vacuum.mqtttest") @@ -616,28 +614,28 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) @@ -646,7 +644,7 @@ async def test_setting_attribute_via_mqtt_json_message( ): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) @@ -658,7 +656,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, - DEFAULT_CONFIG_2_LEGACY, + DEFAULT_CONFIG_2, MQTT_LEGACY_VACUUM_ATTRIBUTES_BLOCKED, ) @@ -666,7 +664,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) @@ -679,20 +677,20 @@ async def test_update_with_json_attrs_not_dict( mqtt_mock_entry_with_yaml_config, caplog, vacuum.DOMAIN, - DEFAULT_CONFIG_2_LEGACY, + DEFAULT_CONFIG_2, ) -async def test_update_with_json_attrs_bad_JSON( +async def test_update_with_json_attrs_bad_json( hass, mqtt_mock_entry_with_yaml_config, caplog ): """Test attributes get extracted from a JSON result.""" - await help_test_update_with_json_attrs_bad_JSON( + await help_test_update_with_json_attrs_bad_json( hass, mqtt_mock_entry_with_yaml_config, caplog, vacuum.DOMAIN, - DEFAULT_CONFIG_2_LEGACY, + DEFAULT_CONFIG_2, ) @@ -703,27 +701,27 @@ async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplo mqtt_mock_entry_no_yaml_config, caplog, vacuum.DOMAIN, - DEFAULT_CONFIG_2_LEGACY, + DEFAULT_CONFIG_2, ) async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one vacuum per unique_id.""" config = { - vacuum.DOMAIN: [ - { - "platform": "mqtt", - "name": "Test 1", - "command_topic": "test_topic", - "unique_id": "TOTALLY_UNIQUE", - }, - { - "platform": "mqtt", - "name": "Test 2", - "command_topic": "test_topic", - "unique_id": "TOTALLY_UNIQUE", - }, - ] + mqtt.DOMAIN: { + vacuum.DOMAIN: [ + { + "name": "Test 1", + "command_topic": "test_topic", + "unique_id": "TOTALLY_UNIQUE", + }, + { + "name": "Test 2", + "command_topic": "test_topic", + "unique_id": "TOTALLY_UNIQUE", + }, + ] + } } await help_test_unique_id( hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, config @@ -732,7 +730,7 @@ async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): async def test_discovery_removal_vacuum(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered vacuum.""" - data = json.dumps(DEFAULT_CONFIG_2_LEGACY[vacuum.DOMAIN]) + data = json.dumps(DEFAULT_CONFIG_2[mqtt.DOMAIN][vacuum.DOMAIN]) await help_test_discovery_removal( hass, mqtt_mock_entry_no_yaml_config, caplog, vacuum.DOMAIN, data ) @@ -778,41 +776,42 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT vacuum device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT vacuum device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" config = { - vacuum.DOMAIN: { - "platform": "mqtt", - "name": "test", - "battery_level_topic": "test-topic", - "battery_level_template": "{{ value_json.battery_level }}", - "command_topic": "command-topic", - "availability_topic": "avty-topic", + mqtt.DOMAIN: { + vacuum.DOMAIN: { + "name": "test", + "battery_level_topic": "test-topic", + "battery_level_template": "{{ value_json.battery_level }}", + "command_topic": "command-topic", + "availability_topic": "avty-topic", + } } } await help_test_entity_id_update_subscriptions( @@ -827,20 +826,21 @@ async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_co async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" config = { - vacuum.DOMAIN: { - "platform": "mqtt", - "name": "test", - "battery_level_topic": "state-topic", - "battery_level_template": "{{ value_json.battery_level }}", - "command_topic": "command-topic", - "payload_turn_on": "ON", + mqtt.DOMAIN: { + vacuum.DOMAIN: { + "name": "test", + "battery_level_topic": "state-topic", + "battery_level_template": "{{ value_json.battery_level }}", + "command_topic": "command-topic", + "payload_turn_on": "ON", + } } } await help_test_entity_debug_info_message( @@ -904,8 +904,8 @@ async def test_publishing_with_custom_encoding( ): """Test publishing MQTT payload with different encoding.""" domain = vacuum.DOMAIN - config = deepcopy(DEFAULT_CONFIG_LEGACY[domain]) - config["supported_features"] = [ + config = deepcopy(DEFAULT_CONFIG) + config[mqtt.DOMAIN][domain]["supported_features"] = [ "turn_on", "turn_off", "clean_spot", @@ -930,12 +930,14 @@ async def test_publishing_with_custom_encoding( async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = vacuum.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = vacuum.DOMAIN @@ -975,7 +977,7 @@ async def test_encoding_subscribable_topics( ): """Test handling of incoming encoded payload.""" domain = vacuum.DOMAIN - config = deepcopy(DEFAULT_CONFIG_LEGACY[domain]) + config = deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][domain]) config[CONF_SUPPORTED_FEATURES] = [ "turn_on", "turn_off", @@ -1007,11 +1009,8 @@ async def test_encoding_subscribable_topics( async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = vacuum.DOMAIN - config = deepcopy(DEFAULT_CONFIG_LEGACY[platform]) - config["name"] = "test" - del config["platform"] - await help_test_setup_manual_entity_from_yaml(hass, platform, config) - assert hass.states.get(f"{platform}.test") is not None + await help_test_setup_manual_entity_from_yaml(hass, DEFAULT_CONFIG) + assert hass.states.get(f"{platform}.mqtttest") # Test deprecated YAML configuration under the platform key diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index b34406f42bc..05f9d0e72f0 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -223,7 +223,7 @@ from .test_common import ( help_test_setup_manual_entity_from_yaml, help_test_unique_id, help_test_unload_config_entry_with_platform, - help_test_update_with_json_attrs_bad_JSON, + help_test_update_with_json_attrs_bad_json, help_test_update_with_json_attrs_not_dict, ) @@ -2094,28 +2094,28 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) @@ -2124,7 +2124,7 @@ async def test_setting_attribute_via_mqtt_json_message( ): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) @@ -2136,7 +2136,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, MQTT_LIGHT_ATTRIBUTES_BLOCKED, ) @@ -2144,7 +2144,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) @@ -2157,20 +2157,20 @@ async def test_update_with_json_attrs_not_dict( mqtt_mock_entry_with_yaml_config, caplog, light.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) -async def test_update_with_json_attrs_bad_JSON( +async def test_update_with_json_attrs_bad_json( hass, mqtt_mock_entry_with_yaml_config, caplog ): """Test attributes get extracted from a JSON result.""" - await help_test_update_with_json_attrs_bad_JSON( + await help_test_update_with_json_attrs_bad_json( hass, mqtt_mock_entry_with_yaml_config, caplog, light.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -2181,29 +2181,29 @@ async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplo mqtt_mock_entry_no_yaml_config, caplog, light.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one light per unique_id.""" config = { - light.DOMAIN: [ - { - "platform": "mqtt", - "name": "Test 1", - "state_topic": "test-topic", - "command_topic": "test_topic", - "unique_id": "TOTALLY_UNIQUE", - }, - { - "platform": "mqtt", - "name": "Test 2", - "state_topic": "test-topic", - "command_topic": "test_topic", - "unique_id": "TOTALLY_UNIQUE", - }, - ] + mqtt.DOMAIN: { + light.DOMAIN: [ + { + "name": "Test 1", + "state_topic": "test-topic", + "command_topic": "test_topic", + "unique_id": "TOTALLY_UNIQUE", + }, + { + "name": "Test 2", + "state_topic": "test-topic", + "command_topic": "test_topic", + "unique_id": "TOTALLY_UNIQUE", + }, + ] + } } await help_test_unique_id( hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, config @@ -2744,42 +2744,42 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT light device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT light device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) @@ -2789,7 +2789,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, light.SERVICE_TURN_ON, ) @@ -2914,11 +2914,11 @@ async def test_publishing_with_custom_encoding( ): """Test publishing MQTT payload with different encoding.""" domain = light.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) + config = copy.deepcopy(DEFAULT_CONFIG) if topic == "effect_command_topic": - config["effect_list"] = ["random", "color_loop"] + config[mqtt.DOMAIN][domain]["effect_list"] = ["random", "color_loop"] elif topic == "white_command_topic": - config["rgb_command_topic"] = "some-cmd-topic" + config[mqtt.DOMAIN][domain]["rgb_command_topic"] = "some-cmd-topic" await help_test_publishing_with_custom_encoding( hass, @@ -2939,12 +2939,14 @@ async def test_publishing_with_custom_encoding( async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = light.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = light.DOMAIN @@ -2993,7 +2995,7 @@ async def test_encoding_subscribable_topics( init_payload, ): """Test handling of incoming encoded payload.""" - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[light.DOMAIN]) + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][light.DOMAIN]) config[CONF_EFFECT_COMMAND_TOPIC] = "light/CONF_EFFECT_COMMAND_TOPIC" config[CONF_RGB_COMMAND_TOPIC] = "light/CONF_RGB_COMMAND_TOPIC" config[CONF_BRIGHTNESS_COMMAND_TOPIC] = "light/CONF_BRIGHTNESS_COMMAND_TOPIC" @@ -3036,7 +3038,7 @@ async def test_encoding_subscribable_topics_brightness( init_payload, ): """Test handling of incoming encoded payload for a brightness only light.""" - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[light.DOMAIN]) + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][light.DOMAIN]) config[CONF_BRIGHTNESS_COMMAND_TOPIC] = "light/CONF_BRIGHTNESS_COMMAND_TOPIC" await help_test_encoding_subscribable_topics( @@ -3138,17 +3140,14 @@ async def test_sending_mqtt_effect_command_with_template( async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = light.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) - config["name"] = "test" - del config["platform"] - await help_test_setup_manual_entity_from_yaml(hass, platform, config) - assert hass.states.get(f"{platform}.test") is not None + await help_test_setup_manual_entity_from_yaml(hass, DEFAULT_CONFIG) + assert hass.states.get(f"{platform}.test") async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): """Test unloading the config entry.""" domain = light.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index af6daf6b7e4..f2835121b86 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -125,7 +125,7 @@ from .test_common import ( help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_setup_manual_entity_from_yaml, help_test_unique_id, - help_test_update_with_json_attrs_bad_JSON, + help_test_update_with_json_attrs_bad_json, help_test_update_with_json_attrs_not_dict, ) @@ -1854,28 +1854,28 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) @@ -1884,7 +1884,7 @@ async def test_setting_attribute_via_mqtt_json_message( ): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) @@ -1896,7 +1896,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, MQTT_LIGHT_ATTRIBUTES_BLOCKED, ) @@ -1904,7 +1904,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) @@ -1917,20 +1917,20 @@ async def test_update_with_json_attrs_not_dict( mqtt_mock_entry_with_yaml_config, caplog, light.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) -async def test_update_with_json_attrs_bad_JSON( +async def test_update_with_json_attrs_bad_json( hass, mqtt_mock_entry_with_yaml_config, caplog ): """Test attributes get extracted from a JSON result.""" - await help_test_update_with_json_attrs_bad_JSON( + await help_test_update_with_json_attrs_bad_json( hass, mqtt_mock_entry_with_yaml_config, caplog, light.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -1941,31 +1941,31 @@ async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplo mqtt_mock_entry_no_yaml_config, caplog, light.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one light per unique_id.""" config = { - light.DOMAIN: [ - { - "platform": "mqtt", - "name": "Test 1", - "schema": "json", - "state_topic": "test-topic", - "command_topic": "test_topic", - "unique_id": "TOTALLY_UNIQUE", - }, - { - "platform": "mqtt", - "name": "Test 2", - "schema": "json", - "state_topic": "test-topic", - "command_topic": "test_topic", - "unique_id": "TOTALLY_UNIQUE", - }, - ] + mqtt.DOMAIN: { + light.DOMAIN: [ + { + "name": "Test 1", + "schema": "json", + "state_topic": "test-topic", + "command_topic": "test_topic", + "unique_id": "TOTALLY_UNIQUE", + }, + { + "name": "Test 2", + "schema": "json", + "state_topic": "test-topic", + "command_topic": "test_topic", + "unique_id": "TOTALLY_UNIQUE", + }, + ] + } } await help_test_unique_id( hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, config @@ -2057,7 +2057,7 @@ async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_ hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -2067,7 +2067,7 @@ async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_ hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -2077,7 +2077,7 @@ async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -2087,27 +2087,21 @@ async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, - mqtt_mock_entry_with_yaml_config, - light.DOMAIN, - DEFAULT_CONFIG_LEGACY, + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, - mqtt_mock_entry_no_yaml_config, - light.DOMAIN, - DEFAULT_CONFIG_LEGACY, + hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) @@ -2117,7 +2111,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, light.SERVICE_TURN_ON, command_payload='{"state":"ON"}', state_payload='{"state":"ON"}', @@ -2182,9 +2176,9 @@ async def test_publishing_with_custom_encoding( ): """Test publishing MQTT payload with different encoding.""" domain = light.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) + config = copy.deepcopy(DEFAULT_CONFIG) if topic == "effect_command_topic": - config["effect_list"] = ["random", "color_loop"] + config[mqtt.DOMAIN][domain]["effect_list"] = ["random", "color_loop"] await help_test_publishing_with_custom_encoding( hass, @@ -2205,12 +2199,14 @@ async def test_publishing_with_custom_encoding( async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = light.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = light.DOMAIN @@ -2241,7 +2237,7 @@ async def test_encoding_subscribable_topics( init_payload, ): """Test handling of incoming encoded payload.""" - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[light.DOMAIN]) + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][light.DOMAIN]) config["color_mode"] = True config["supported_color_modes"] = [ "color_temp", @@ -2269,11 +2265,8 @@ async def test_encoding_subscribable_topics( async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = light.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) - config["name"] = "test" - del config["platform"] - await help_test_setup_manual_entity_from_yaml(hass, platform, config) - assert hass.states.get(f"{platform}.test") is not None + await help_test_setup_manual_entity_from_yaml(hass, DEFAULT_CONFIG) + assert hass.states.get(f"{platform}.test") # Test deprecated YAML configuration under the platform key diff --git a/tests/components/mqtt/test_light_template.py b/tests/components/mqtt/test_light_template.py index 8ecdcc2c872..dd3f267b0d0 100644 --- a/tests/components/mqtt/test_light_template.py +++ b/tests/components/mqtt/test_light_template.py @@ -71,7 +71,7 @@ from .test_common import ( help_test_setup_manual_entity_from_yaml, help_test_unique_id, help_test_unload_config_entry_with_platform, - help_test_update_with_json_attrs_bad_JSON, + help_test_update_with_json_attrs_bad_json, help_test_update_with_json_attrs_not_dict, ) @@ -846,28 +846,28 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) @@ -876,7 +876,7 @@ async def test_setting_attribute_via_mqtt_json_message( ): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) @@ -888,7 +888,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, MQTT_LIGHT_ATTRIBUTES_BLOCKED, ) @@ -896,7 +896,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) @@ -909,20 +909,20 @@ async def test_update_with_json_attrs_not_dict( mqtt_mock_entry_with_yaml_config, caplog, light.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) -async def test_update_with_json_attrs_bad_JSON( +async def test_update_with_json_attrs_bad_json( hass, mqtt_mock_entry_with_yaml_config, caplog ): """Test attributes get extracted from a JSON result.""" - await help_test_update_with_json_attrs_bad_JSON( + await help_test_update_with_json_attrs_bad_json( hass, mqtt_mock_entry_with_yaml_config, caplog, light.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -933,33 +933,35 @@ async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplo mqtt_mock_entry_no_yaml_config, caplog, light.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one light per unique_id.""" config = { - light.DOMAIN: [ - { - "platform": "mqtt", - "name": "Test 1", - "schema": "template", - "state_topic": "test-topic", - "command_topic": "test_topic", - "command_on_template": "on,{{ transition }}", - "command_off_template": "off,{{ transition|d }}", - "unique_id": "TOTALLY_UNIQUE", - }, - { - "platform": "mqtt", - "name": "Test 2", - "schema": "template", - "state_topic": "test-topic", - "command_topic": "test_topic", - "unique_id": "TOTALLY_UNIQUE", - }, - ] + mqtt.DOMAIN: { + light.DOMAIN: [ + { + "name": "Test 1", + "schema": "template", + "state_topic": "test-topic", + "command_topic": "test_topic", + "command_on_template": "on,{{ transition }}", + "command_off_template": "off,{{ transition|d }}", + "unique_id": "TOTALLY_UNIQUE", + }, + { + "name": "Test 2", + "schema": "template", + "state_topic": "test-topic2", + "command_topic": "test_topic2", + "command_on_template": "on,{{ transition }}", + "command_off_template": "off,{{ transition|d }}", + "unique_id": "TOTALLY_UNIQUE", + }, + ] + } } await help_test_unique_id( hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, config @@ -1048,56 +1050,57 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT light device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT light device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" config = { - light.DOMAIN: { - "platform": "mqtt", - "schema": "template", - "name": "test", - "command_topic": "test-topic", - "command_on_template": "ON", - "command_off_template": "off,{{ transition|d }}", - "state_template": '{{ value.split(",")[0] }}', + mqtt.DOMAIN: { + light.DOMAIN: { + "schema": "template", + "name": "test", + "command_topic": "test-topic", + "command_on_template": "ON", + "command_off_template": "off,{{ transition|d }}", + "state_template": '{{ value.split(",")[0] }}', + } } } await help_test_entity_debug_info_message( @@ -1169,9 +1172,9 @@ async def test_publishing_with_custom_encoding( ): """Test publishing MQTT payload with different encoding.""" domain = light.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) + config = copy.deepcopy(DEFAULT_CONFIG) if topic == "effect_command_topic": - config["effect_list"] = ["random", "color_loop"] + config[mqtt.DOMAIN][domain]["effect_list"] = ["random", "color_loop"] await help_test_publishing_with_custom_encoding( hass, @@ -1192,12 +1195,14 @@ async def test_publishing_with_custom_encoding( async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = light.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = light.DOMAIN @@ -1222,7 +1227,7 @@ async def test_encoding_subscribable_topics( init_payload, ): """Test handling of incoming encoded payload.""" - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[light.DOMAIN]) + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][light.DOMAIN]) config["state_template"] = "{{ value }}" await help_test_encoding_subscribable_topics( hass, @@ -1241,17 +1246,14 @@ async def test_encoding_subscribable_topics( async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = light.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) - config["name"] = "test" - del config["platform"] - await help_test_setup_manual_entity_from_yaml(hass, platform, config) - assert hass.states.get(f"{platform}.test") is not None + await help_test_setup_manual_entity_from_yaml(hass, DEFAULT_CONFIG) + assert hass.states.get(f"{platform}.test") async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): """Test unloading the config entry.""" domain = light.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) diff --git a/tests/components/mqtt/test_lock.py b/tests/components/mqtt/test_lock.py index de97de23a1e..0784065ddbe 100644 --- a/tests/components/mqtt/test_lock.py +++ b/tests/components/mqtt/test_lock.py @@ -49,7 +49,7 @@ from .test_common import ( help_test_setup_manual_entity_from_yaml, help_test_unique_id, help_test_unload_config_entry_with_platform, - help_test_update_with_json_attrs_bad_JSON, + help_test_update_with_json_attrs_bad_json, help_test_update_with_json_attrs_not_dict, ) @@ -452,28 +452,28 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, lock.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, lock.DOMAIN, DEFAULT_CONFIG ) async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock_entry_with_yaml_config, lock.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, lock.DOMAIN, DEFAULT_CONFIG ) async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock_entry_with_yaml_config, lock.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, lock.DOMAIN, DEFAULT_CONFIG ) async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock_entry_with_yaml_config, lock.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, lock.DOMAIN, DEFAULT_CONFIG ) @@ -482,7 +482,7 @@ async def test_setting_attribute_via_mqtt_json_message( ): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock_entry_with_yaml_config, lock.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, lock.DOMAIN, DEFAULT_CONFIG ) @@ -494,7 +494,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( hass, mqtt_mock_entry_no_yaml_config, lock.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, MQTT_LOCK_ATTRIBUTES_BLOCKED, ) @@ -502,7 +502,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock_entry_with_yaml_config, lock.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, lock.DOMAIN, DEFAULT_CONFIG ) @@ -515,7 +515,7 @@ async def test_update_with_json_attrs_not_dict( mqtt_mock_entry_with_yaml_config, caplog, lock.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -523,41 +523,41 @@ async def test_update_with_json_attrs_bad_json( hass, mqtt_mock_entry_with_yaml_config, caplog ): """Test attributes get extracted from a JSON result.""" - await help_test_update_with_json_attrs_bad_JSON( + await help_test_update_with_json_attrs_bad_json( hass, mqtt_mock_entry_with_yaml_config, caplog, lock.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock_entry_no_yaml_config, caplog, lock.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, caplog, lock.DOMAIN, DEFAULT_CONFIG ) async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one lock per unique_id.""" config = { - lock.DOMAIN: [ - { - "platform": "mqtt", - "name": "Test 1", - "state_topic": "test-topic", - "command_topic": "test_topic", - "unique_id": "TOTALLY_UNIQUE", - }, - { - "platform": "mqtt", - "name": "Test 2", - "state_topic": "test-topic", - "command_topic": "test_topic", - "unique_id": "TOTALLY_UNIQUE", - }, - ] + mqtt.DOMAIN: { + lock.DOMAIN: [ + { + "name": "Test 1", + "state_topic": "test-topic", + "command_topic": "test_topic", + "unique_id": "TOTALLY_UNIQUE", + }, + { + "name": "Test 2", + "state_topic": "test-topic", + "command_topic": "test_topic", + "unique_id": "TOTALLY_UNIQUE", + }, + ] + } } await help_test_unique_id( hass, mqtt_mock_entry_with_yaml_config, lock.DOMAIN, config @@ -626,42 +626,42 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT lock device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock_entry_no_yaml_config, lock.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, lock.DOMAIN, DEFAULT_CONFIG ) async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT lock device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock_entry_no_yaml_config, lock.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, lock.DOMAIN, DEFAULT_CONFIG ) async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock_entry_no_yaml_config, lock.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, lock.DOMAIN, DEFAULT_CONFIG ) async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock_entry_no_yaml_config, lock.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, lock.DOMAIN, DEFAULT_CONFIG ) async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock_entry_with_yaml_config, lock.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, lock.DOMAIN, DEFAULT_CONFIG ) async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock_entry_no_yaml_config, lock.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, lock.DOMAIN, DEFAULT_CONFIG ) @@ -671,7 +671,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): hass, mqtt_mock_entry_no_yaml_config, lock.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, SERVICE_LOCK, command_payload="LOCK", ) @@ -701,7 +701,7 @@ async def test_publishing_with_custom_encoding( ): """Test publishing MQTT payload with different encoding.""" domain = lock.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_publishing_with_custom_encoding( hass, @@ -720,12 +720,14 @@ async def test_publishing_with_custom_encoding( async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = lock.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = lock.DOMAIN @@ -754,7 +756,7 @@ async def test_encoding_subscribable_topics( mqtt_mock_entry_with_yaml_config, caplog, lock.DOMAIN, - DEFAULT_CONFIG_LEGACY[lock.DOMAIN], + DEFAULT_CONFIG[mqtt.DOMAIN][lock.DOMAIN], topic, value, attribute, @@ -765,17 +767,14 @@ async def test_encoding_subscribable_topics( async def test_setup_manual_entity_from_yaml(hass, caplog, tmp_path): """Test setup manual configured MQTT entity.""" platform = lock.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) - config["name"] = "test" - del config["platform"] - await help_test_setup_manual_entity_from_yaml(hass, platform, config) - assert hass.states.get(f"{platform}.test") is not None + await help_test_setup_manual_entity_from_yaml(hass, DEFAULT_CONFIG) + assert hass.states.get(f"{platform}.test") async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): """Test unloading the config entry.""" domain = lock.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) diff --git a/tests/components/mqtt/test_number.py b/tests/components/mqtt/test_number.py index 69b0473fb9d..65c8655472c 100644 --- a/tests/components/mqtt/test_number.py +++ b/tests/components/mqtt/test_number.py @@ -59,7 +59,7 @@ from .test_common import ( help_test_setup_manual_entity_from_yaml, help_test_unique_id, help_test_unload_config_entry_with_platform, - help_test_update_with_json_attrs_bad_JSON, + help_test_update_with_json_attrs_bad_json, help_test_update_with_json_attrs_not_dict, ) @@ -446,28 +446,28 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, number.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, number.DOMAIN, DEFAULT_CONFIG ) async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock_entry_with_yaml_config, number.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, number.DOMAIN, DEFAULT_CONFIG ) async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock_entry_with_yaml_config, number.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, number.DOMAIN, DEFAULT_CONFIG ) async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock_entry_with_yaml_config, number.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, number.DOMAIN, DEFAULT_CONFIG ) @@ -476,7 +476,7 @@ async def test_setting_attribute_via_mqtt_json_message( ): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock_entry_with_yaml_config, number.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, number.DOMAIN, DEFAULT_CONFIG ) @@ -488,7 +488,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( hass, mqtt_mock_entry_no_yaml_config, number.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, MQTT_NUMBER_ATTRIBUTES_BLOCKED, ) @@ -496,7 +496,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock_entry_with_yaml_config, number.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, number.DOMAIN, DEFAULT_CONFIG ) @@ -509,20 +509,20 @@ async def test_update_with_json_attrs_not_dict( mqtt_mock_entry_with_yaml_config, caplog, number.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) -async def test_update_with_json_attrs_bad_JSON( +async def test_update_with_json_attrs_bad_json( hass, mqtt_mock_entry_with_yaml_config, caplog ): """Test attributes get extracted from a JSON result.""" - await help_test_update_with_json_attrs_bad_JSON( + await help_test_update_with_json_attrs_bad_json( hass, mqtt_mock_entry_with_yaml_config, caplog, number.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -533,29 +533,29 @@ async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplo mqtt_mock_entry_no_yaml_config, caplog, number.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one number per unique_id.""" config = { - number.DOMAIN: [ - { - "platform": "mqtt", - "name": "Test 1", - "state_topic": "test-topic", - "command_topic": "test-topic", - "unique_id": "TOTALLY_UNIQUE", - }, - { - "platform": "mqtt", - "name": "Test 2", - "state_topic": "test-topic", - "command_topic": "test-topic", - "unique_id": "TOTALLY_UNIQUE", - }, - ] + mqtt.DOMAIN: { + number.DOMAIN: [ + { + "name": "Test 1", + "state_topic": "test-topic", + "command_topic": "test-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + { + "name": "Test 2", + "state_topic": "test-topic", + "command_topic": "test-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + ] + } } await help_test_unique_id( hass, mqtt_mock_entry_with_yaml_config, number.DOMAIN, config @@ -564,7 +564,7 @@ async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): async def test_discovery_removal_number(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered number.""" - data = json.dumps(DEFAULT_CONFIG_LEGACY[number.DOMAIN]) + data = json.dumps(DEFAULT_CONFIG[mqtt.DOMAIN][number.DOMAIN]) await help_test_discovery_removal( hass, mqtt_mock_entry_no_yaml_config, caplog, number.DOMAIN, data ) @@ -624,42 +624,42 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT number device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock_entry_no_yaml_config, number.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, number.DOMAIN, DEFAULT_CONFIG ) async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT number device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock_entry_no_yaml_config, number.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, number.DOMAIN, DEFAULT_CONFIG ) async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock_entry_no_yaml_config, number.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, number.DOMAIN, DEFAULT_CONFIG ) async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock_entry_no_yaml_config, number.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, number.DOMAIN, DEFAULT_CONFIG ) async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock_entry_with_yaml_config, number.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, number.DOMAIN, DEFAULT_CONFIG ) async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock_entry_no_yaml_config, number.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, number.DOMAIN, DEFAULT_CONFIG ) @@ -669,7 +669,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): hass, mqtt_mock_entry_no_yaml_config, number.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, SERVICE_SET_VALUE, service_parameters={ATTR_VALUE: 45}, command_payload="45", @@ -882,7 +882,7 @@ async def test_publishing_with_custom_encoding( ): """Test publishing MQTT payload with different encoding.""" domain = NUMBER_DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_publishing_with_custom_encoding( hass, @@ -901,12 +901,14 @@ async def test_publishing_with_custom_encoding( async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = number.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = number.DOMAIN @@ -935,8 +937,8 @@ async def test_encoding_subscribable_topics( hass, mqtt_mock_entry_with_yaml_config, caplog, - "number", - DEFAULT_CONFIG_LEGACY["number"], + number.DOMAIN, + DEFAULT_CONFIG[mqtt.DOMAIN][number.DOMAIN], topic, value, attribute, @@ -947,17 +949,14 @@ async def test_encoding_subscribable_topics( async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = number.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) - config["name"] = "test" - del config["platform"] - await help_test_setup_manual_entity_from_yaml(hass, platform, config) - assert hass.states.get(f"{platform}.test") is not None + await help_test_setup_manual_entity_from_yaml(hass, DEFAULT_CONFIG) + assert hass.states.get(f"{platform}.test") async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): """Test unloading the config entry.""" domain = number.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) diff --git a/tests/components/mqtt/test_scene.py b/tests/components/mqtt/test_scene.py index a1c1644cc37..122c32caa03 100644 --- a/tests/components/mqtt/test_scene.py +++ b/tests/components/mqtt/test_scene.py @@ -87,25 +87,26 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, scene.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, scene.DOMAIN, DEFAULT_CONFIG ) async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock_entry_with_yaml_config, scene.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, scene.DOMAIN, DEFAULT_CONFIG ) async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" config = { - scene.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "command-topic", - "payload_on": 1, + mqtt.DOMAIN: { + scene.DOMAIN: { + "name": "test", + "command_topic": "command-topic", + "payload_on": 1, + } } } @@ -123,11 +124,12 @@ async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_conf async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" config = { - scene.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "command-topic", - "payload_on": 1, + mqtt.DOMAIN: { + scene.DOMAIN: { + "name": "test", + "command_topic": "command-topic", + "payload_on": 1, + } } } @@ -145,20 +147,20 @@ async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_confi async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one scene per unique_id.""" config = { - scene.DOMAIN: [ - { - "platform": "mqtt", - "name": "Test 1", - "command_topic": "command-topic", - "unique_id": "TOTALLY_UNIQUE", - }, - { - "platform": "mqtt", - "name": "Test 2", - "command_topic": "command-topic", - "unique_id": "TOTALLY_UNIQUE", - }, - ] + mqtt.DOMAIN: { + scene.DOMAIN: [ + { + "name": "Test 1", + "command_topic": "command-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + { + "name": "Test 2", + "command_topic": "command-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + ] + } } await help_test_unique_id( hass, mqtt_mock_entry_with_yaml_config, scene.DOMAIN, config @@ -175,8 +177,8 @@ async def test_discovery_removal_scene(hass, mqtt_mock_entry_no_yaml_config, cap async def test_discovery_update_payload(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered scene.""" - config1 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[scene.DOMAIN]) - config2 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[scene.DOMAIN]) + config1 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][scene.DOMAIN]) + config2 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][scene.DOMAIN]) config1["name"] = "Beer" config2["name"] = "Milk" config1["payload_on"] = "ON" @@ -223,12 +225,14 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = scene.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = scene.DOMAIN @@ -239,17 +243,14 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = scene.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) - config["name"] = "test" - del config["platform"] - await help_test_setup_manual_entity_from_yaml(hass, platform, config) - assert hass.states.get(f"{platform}.test") is not None + await help_test_setup_manual_entity_from_yaml(hass, DEFAULT_CONFIG) + assert hass.states.get(f"{platform}.test") async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): """Test unloading the config entry.""" domain = scene.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) diff --git a/tests/components/mqtt/test_select.py b/tests/components/mqtt/test_select.py index c66638a18af..5b588304061 100644 --- a/tests/components/mqtt/test_select.py +++ b/tests/components/mqtt/test_select.py @@ -49,7 +49,7 @@ from .test_common import ( help_test_setup_manual_entity_from_yaml, help_test_unique_id, help_test_unload_config_entry_with_platform, - help_test_update_with_json_attrs_bad_JSON, + help_test_update_with_json_attrs_bad_json, help_test_update_with_json_attrs_not_dict, ) @@ -323,28 +323,28 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, select.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, select.DOMAIN, DEFAULT_CONFIG ) async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock_entry_with_yaml_config, select.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, select.DOMAIN, DEFAULT_CONFIG ) async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock_entry_with_yaml_config, select.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, select.DOMAIN, DEFAULT_CONFIG ) async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock_entry_with_yaml_config, select.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, select.DOMAIN, DEFAULT_CONFIG ) @@ -353,7 +353,7 @@ async def test_setting_attribute_via_mqtt_json_message( ): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock_entry_with_yaml_config, select.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, select.DOMAIN, DEFAULT_CONFIG ) @@ -365,7 +365,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( hass, mqtt_mock_entry_no_yaml_config, select.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, MQTT_SELECT_ATTRIBUTES_BLOCKED, ) @@ -373,7 +373,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock_entry_with_yaml_config, select.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, select.DOMAIN, DEFAULT_CONFIG ) @@ -386,20 +386,20 @@ async def test_update_with_json_attrs_not_dict( mqtt_mock_entry_with_yaml_config, caplog, select.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) -async def test_update_with_json_attrs_bad_JSON( +async def test_update_with_json_attrs_bad_json( hass, mqtt_mock_entry_with_yaml_config, caplog ): """Test attributes get extracted from a JSON result.""" - await help_test_update_with_json_attrs_bad_JSON( + await help_test_update_with_json_attrs_bad_json( hass, mqtt_mock_entry_with_yaml_config, caplog, select.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -410,31 +410,31 @@ async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplo mqtt_mock_entry_no_yaml_config, caplog, select.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one select per unique_id.""" config = { - select.DOMAIN: [ - { - "platform": "mqtt", - "name": "Test 1", - "state_topic": "test-topic", - "command_topic": "test-topic", - "unique_id": "TOTALLY_UNIQUE", - "options": ["milk", "beer"], - }, - { - "platform": "mqtt", - "name": "Test 2", - "state_topic": "test-topic", - "command_topic": "test-topic", - "unique_id": "TOTALLY_UNIQUE", - "options": ["milk", "beer"], - }, - ] + mqtt.DOMAIN: { + select.DOMAIN: [ + { + "name": "Test 1", + "state_topic": "test-topic", + "command_topic": "test-topic", + "unique_id": "TOTALLY_UNIQUE", + "options": ["milk", "beer"], + }, + { + "name": "Test 2", + "state_topic": "test-topic", + "command_topic": "test-topic", + "unique_id": "TOTALLY_UNIQUE", + "options": ["milk", "beer"], + }, + ] + } } await help_test_unique_id( hass, mqtt_mock_entry_with_yaml_config, select.DOMAIN, config @@ -443,7 +443,7 @@ async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): async def test_discovery_removal_select(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered select.""" - data = json.dumps(DEFAULT_CONFIG_LEGACY[select.DOMAIN]) + data = json.dumps(DEFAULT_CONFIG[mqtt.DOMAIN][select.DOMAIN]) await help_test_discovery_removal( hass, mqtt_mock_entry_no_yaml_config, caplog, select.DOMAIN, data ) @@ -501,42 +501,42 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT select device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock_entry_no_yaml_config, select.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, select.DOMAIN, DEFAULT_CONFIG ) async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT select device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock_entry_no_yaml_config, select.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, select.DOMAIN, DEFAULT_CONFIG ) async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock_entry_no_yaml_config, select.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, select.DOMAIN, DEFAULT_CONFIG ) async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock_entry_no_yaml_config, select.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, select.DOMAIN, DEFAULT_CONFIG ) async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock_entry_with_yaml_config, select.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, select.DOMAIN, DEFAULT_CONFIG ) async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock_entry_no_yaml_config, select.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, select.DOMAIN, DEFAULT_CONFIG ) @@ -546,7 +546,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): hass, mqtt_mock_entry_no_yaml_config, select.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, select.SERVICE_SELECT_OPTION, service_parameters={ATTR_OPTION: "beer"}, command_payload="beer", @@ -635,8 +635,8 @@ async def test_publishing_with_custom_encoding( ): """Test publishing MQTT payload with different encoding.""" domain = select.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] - config["options"] = ["milk", "beer"] + config = DEFAULT_CONFIG + config[mqtt.DOMAIN][domain]["options"] = ["milk", "beer"] await help_test_publishing_with_custom_encoding( hass, @@ -655,12 +655,14 @@ async def test_publishing_with_custom_encoding( async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = select.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = select.DOMAIN @@ -685,13 +687,13 @@ async def test_encoding_subscribable_topics( attribute_value, ): """Test handling of incoming encoded payload.""" - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY["select"]) + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][select.DOMAIN]) config["options"] = ["milk", "beer"] await help_test_encoding_subscribable_topics( hass, mqtt_mock_entry_with_yaml_config, caplog, - "select", + select.DOMAIN, config, topic, value, @@ -703,17 +705,14 @@ async def test_encoding_subscribable_topics( async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = select.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) - config["name"] = "test" - del config["platform"] - await help_test_setup_manual_entity_from_yaml(hass, platform, config) - assert hass.states.get(f"{platform}.test") is not None + await help_test_setup_manual_entity_from_yaml(hass, DEFAULT_CONFIG) + assert hass.states.get(f"{platform}.test") async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): """Test unloading the config entry.""" domain = select.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index c5cf377cf1a..6cfaa9678bb 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -59,7 +59,7 @@ from .test_common import ( help_test_setup_manual_entity_from_yaml, help_test_unique_id, help_test_unload_config_entry_with_platform, - help_test_update_with_json_attrs_bad_JSON, + help_test_update_with_json_attrs_bad_json, help_test_update_with_json_attrs_not_dict, ) @@ -585,21 +585,21 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) @@ -608,7 +608,7 @@ async def test_default_availability_list_payload( ): """Test availability by default payload with defined topic.""" await help_test_default_availability_list_payload( - hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) @@ -617,7 +617,7 @@ async def test_default_availability_list_payload_all( ): """Test availability by default payload with defined topic.""" await help_test_default_availability_list_payload_all( - hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) @@ -626,34 +626,31 @@ async def test_default_availability_list_payload_any( ): """Test availability by default payload with defined topic.""" await help_test_default_availability_list_payload_any( - hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_default_availability_list_single( - hass, mqtt_mock_entry_no_yaml_config, caplog -): +async def test_default_availability_list_single(hass, caplog): """Test availability list and availability_topic are mutually exclusive.""" await help_test_default_availability_list_single( hass, - mqtt_mock_entry_no_yaml_config, caplog, sensor.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) async def test_discovery_update_availability(hass, mqtt_mock_entry_no_yaml_config): """Test availability discovery update.""" await help_test_discovery_update_availability( - hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) @@ -760,7 +757,7 @@ async def test_setting_attribute_via_mqtt_json_message( ): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) @@ -772,7 +769,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, MQTT_SENSOR_ATTRIBUTES_BLOCKED, ) @@ -780,7 +777,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) @@ -793,20 +790,20 @@ async def test_update_with_json_attrs_not_dict( mqtt_mock_entry_with_yaml_config, caplog, sensor.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) -async def test_update_with_json_attrs_bad_JSON( +async def test_update_with_json_attrs_bad_json( hass, mqtt_mock_entry_with_yaml_config, caplog ): """Test attributes get extracted from a JSON result.""" - await help_test_update_with_json_attrs_bad_JSON( + await help_test_update_with_json_attrs_bad_json( hass, mqtt_mock_entry_with_yaml_config, caplog, sensor.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -817,27 +814,27 @@ async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplo mqtt_mock_entry_no_yaml_config, caplog, sensor.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one sensor per unique_id.""" config = { - sensor.DOMAIN: [ - { - "platform": "mqtt", - "name": "Test 1", - "state_topic": "test-topic", - "unique_id": "TOTALLY_UNIQUE", - }, - { - "platform": "mqtt", - "name": "Test 2", - "state_topic": "test-topic", - "unique_id": "TOTALLY_UNIQUE", - }, - ] + mqtt.DOMAIN: { + sensor.DOMAIN: [ + { + "name": "Test 1", + "state_topic": "test-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + { + "name": "Test 2", + "state_topic": "test-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + ] + } } await help_test_unique_id( hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, config @@ -951,42 +948,42 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT sensor device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT sensor device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) @@ -1021,42 +1018,42 @@ async def test_entity_device_info_with_hub(hass, mqtt_mock_entry_no_yaml_config) async def test_entity_debug_info(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT sensor debug info.""" await help_test_entity_debug_info( - hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) async def test_entity_debug_info_max_messages(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT sensor debug info.""" await help_test_entity_debug_info_max_messages( - hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" await help_test_entity_debug_info_message( - hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY, None + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG, None ) async def test_entity_debug_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT sensor debug info.""" await help_test_entity_debug_info_remove( - hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) async def test_entity_debug_info_update_entity_id(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT sensor debug info.""" await help_test_entity_debug_info_update_entity_id( - hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) async def test_entity_disabled_by_default(hass, mqtt_mock_entry_no_yaml_config): """Test entity disabled by default.""" await help_test_entity_disabled_by_default( - hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) @@ -1064,7 +1061,7 @@ async def test_entity_disabled_by_default(hass, mqtt_mock_entry_no_yaml_config): async def test_entity_category(hass, mqtt_mock_entry_no_yaml_config): """Test entity category.""" await help_test_entity_category( - hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) @@ -1101,12 +1098,14 @@ async def test_value_template_with_entity_id(hass, mqtt_mock_entry_with_yaml_con async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = sensor.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = sensor.DOMAIN @@ -1230,7 +1229,7 @@ async def test_encoding_subscribable_topics( mqtt_mock_entry_with_yaml_config, caplog, sensor.DOMAIN, - DEFAULT_CONFIG_LEGACY[sensor.DOMAIN], + DEFAULT_CONFIG[mqtt.DOMAIN][sensor.DOMAIN], topic, value, attribute, @@ -1242,17 +1241,14 @@ async def test_encoding_subscribable_topics( async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = sensor.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) - config["name"] = "test" - del config["platform"] - await help_test_setup_manual_entity_from_yaml(hass, platform, config) - assert hass.states.get(f"{platform}.test") is not None + await help_test_setup_manual_entity_from_yaml(hass, DEFAULT_CONFIG) + assert hass.states.get(f"{platform}.test") async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): """Test unloading the config entry.""" domain = sensor.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) diff --git a/tests/components/mqtt/test_siren.py b/tests/components/mqtt/test_siren.py index fd91847f767..7c16f3f01a3 100644 --- a/tests/components/mqtt/test_siren.py +++ b/tests/components/mqtt/test_siren.py @@ -46,7 +46,7 @@ from .test_common import ( help_test_setup_manual_entity_from_yaml, help_test_unique_id, help_test_unload_config_entry_with_platform, - help_test_update_with_json_attrs_bad_JSON, + help_test_update_with_json_attrs_bad_json, help_test_update_with_json_attrs_not_dict, ) @@ -457,27 +457,28 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, siren.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, siren.DOMAIN, DEFAULT_CONFIG ) async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock_entry_with_yaml_config, siren.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, siren.DOMAIN, DEFAULT_CONFIG ) async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" config = { - siren.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "payload_on": 1, - "payload_off": 0, + mqtt.DOMAIN: { + siren.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "payload_on": 1, + "payload_off": 0, + } } } @@ -495,13 +496,14 @@ async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_conf async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" config = { - siren.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "payload_on": 1, - "payload_off": 0, + mqtt.DOMAIN: { + siren.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "payload_on": 1, + "payload_off": 0, + } } } @@ -558,7 +560,7 @@ async def test_setting_attribute_via_mqtt_json_message( ): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock_entry_with_yaml_config, siren.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, siren.DOMAIN, DEFAULT_CONFIG ) @@ -567,14 +569,14 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( ): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, mqtt_mock_entry_no_yaml_config, siren.DOMAIN, DEFAULT_CONFIG_LEGACY, {} + hass, mqtt_mock_entry_no_yaml_config, siren.DOMAIN, DEFAULT_CONFIG, {} ) async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock_entry_with_yaml_config, siren.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, siren.DOMAIN, DEFAULT_CONFIG ) @@ -587,20 +589,20 @@ async def test_update_with_json_attrs_not_dict( mqtt_mock_entry_with_yaml_config, caplog, siren.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) -async def test_update_with_json_attrs_bad_JSON( +async def test_update_with_json_attrs_bad_json( hass, mqtt_mock_entry_with_yaml_config, caplog ): """Test attributes get extracted from a JSON result.""" - await help_test_update_with_json_attrs_bad_JSON( + await help_test_update_with_json_attrs_bad_json( hass, mqtt_mock_entry_with_yaml_config, caplog, siren.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -611,29 +613,29 @@ async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplo mqtt_mock_entry_no_yaml_config, caplog, siren.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one siren per unique_id.""" config = { - siren.DOMAIN: [ - { - "platform": "mqtt", - "name": "Test 1", - "state_topic": "test-topic", - "command_topic": "command-topic", - "unique_id": "TOTALLY_UNIQUE", - }, - { - "platform": "mqtt", - "name": "Test 2", - "state_topic": "test-topic", - "command_topic": "command-topic", - "unique_id": "TOTALLY_UNIQUE", - }, - ] + mqtt.DOMAIN: { + siren.DOMAIN: [ + { + "name": "Test 1", + "state_topic": "test-topic", + "command_topic": "command-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + { + "name": "Test 2", + "state_topic": "test-topic", + "command_topic": "command-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + ] + } } await help_test_unique_id( hass, mqtt_mock_entry_with_yaml_config, siren.DOMAIN, config @@ -656,8 +658,8 @@ async def test_discovery_update_siren_topic_template( hass, mqtt_mock_entry_no_yaml_config, caplog ): """Test update of discovered siren.""" - config1 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[siren.DOMAIN]) - config2 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[siren.DOMAIN]) + config1 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][siren.DOMAIN]) + config2 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][siren.DOMAIN]) config1["name"] = "Beer" config2["name"] = "Milk" config1["state_topic"] = "siren/state1" @@ -693,8 +695,8 @@ async def test_discovery_update_siren_template( hass, mqtt_mock_entry_no_yaml_config, caplog ): """Test update of discovered siren.""" - config1 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[siren.DOMAIN]) - config2 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[siren.DOMAIN]) + config1 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][siren.DOMAIN]) + config2 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][siren.DOMAIN]) config1["name"] = "Beer" config2["name"] = "Milk" config1["state_topic"] = "siren/state1" @@ -844,42 +846,42 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT siren device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock_entry_no_yaml_config, siren.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, siren.DOMAIN, DEFAULT_CONFIG ) async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT siren device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock_entry_no_yaml_config, siren.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, siren.DOMAIN, DEFAULT_CONFIG ) async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock_entry_no_yaml_config, siren.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, siren.DOMAIN, DEFAULT_CONFIG ) async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock_entry_no_yaml_config, siren.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, siren.DOMAIN, DEFAULT_CONFIG ) async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock_entry_with_yaml_config, siren.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, siren.DOMAIN, DEFAULT_CONFIG ) async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock_entry_no_yaml_config, siren.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, siren.DOMAIN, DEFAULT_CONFIG ) @@ -889,7 +891,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): hass, mqtt_mock_entry_no_yaml_config, siren.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, siren.SERVICE_TURN_ON, command_payload='{"state":"ON"}', ) @@ -926,8 +928,8 @@ async def test_publishing_with_custom_encoding( ): """Test publishing MQTT payload with command templates and different encoding.""" domain = siren.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) - config[siren.ATTR_AVAILABLE_TONES] = ["siren", "xylophone"] + config = copy.deepcopy(DEFAULT_CONFIG) + config[mqtt.DOMAIN][domain][siren.ATTR_AVAILABLE_TONES] = ["siren", "xylophone"] await help_test_publishing_with_custom_encoding( hass, @@ -946,12 +948,14 @@ async def test_publishing_with_custom_encoding( async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = siren.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = siren.DOMAIN @@ -980,7 +984,7 @@ async def test_encoding_subscribable_topics( mqtt_mock_entry_with_yaml_config, caplog, siren.DOMAIN, - DEFAULT_CONFIG_LEGACY[siren.DOMAIN], + DEFAULT_CONFIG[mqtt.DOMAIN][siren.DOMAIN], topic, value, attribute, @@ -991,17 +995,14 @@ async def test_encoding_subscribable_topics( async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = siren.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) - config["name"] = "test" - del config["platform"] - await help_test_setup_manual_entity_from_yaml(hass, platform, config) - assert hass.states.get(f"{platform}.test") is not None + await help_test_setup_manual_entity_from_yaml(hass, DEFAULT_CONFIG) + assert hass.states.get(f"{platform}.test") async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): """Test unloading the config entry.""" domain = siren.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) diff --git a/tests/components/mqtt/test_state_vacuum.py b/tests/components/mqtt/test_state_vacuum.py index 4c2fbf3a596..917c5fa43d8 100644 --- a/tests/components/mqtt/test_state_vacuum.py +++ b/tests/components/mqtt/test_state_vacuum.py @@ -61,7 +61,7 @@ from .test_common import ( help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_setup_manual_entity_from_yaml, help_test_unique_id, - help_test_update_with_json_attrs_bad_JSON, + help_test_update_with_json_attrs_bad_json, help_test_update_with_json_attrs_not_dict, ) @@ -375,28 +375,28 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) @@ -405,7 +405,7 @@ async def test_setting_attribute_via_mqtt_json_message( ): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) @@ -417,7 +417,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, - DEFAULT_CONFIG_2_LEGACY, + DEFAULT_CONFIG_2, MQTT_VACUUM_ATTRIBUTES_BLOCKED, ) @@ -425,7 +425,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) @@ -438,7 +438,7 @@ async def test_update_with_json_attrs_not_dict( mqtt_mock_entry_with_yaml_config, caplog, vacuum.DOMAIN, - DEFAULT_CONFIG_2_LEGACY, + DEFAULT_CONFIG_2, ) @@ -446,12 +446,12 @@ async def test_update_with_json_attrs_bad_json( hass, mqtt_mock_entry_with_yaml_config, caplog ): """Test attributes get extracted from a JSON result.""" - await help_test_update_with_json_attrs_bad_JSON( + await help_test_update_with_json_attrs_bad_json( hass, mqtt_mock_entry_with_yaml_config, caplog, vacuum.DOMAIN, - DEFAULT_CONFIG_2_LEGACY, + DEFAULT_CONFIG_2, ) @@ -462,29 +462,29 @@ async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplo mqtt_mock_entry_no_yaml_config, caplog, vacuum.DOMAIN, - DEFAULT_CONFIG_2_LEGACY, + DEFAULT_CONFIG_2, ) async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one vacuum per unique_id.""" config = { - vacuum.DOMAIN: [ - { - "platform": "mqtt", - "schema": "state", - "name": "Test 1", - "command_topic": "command-topic", - "unique_id": "TOTALLY_UNIQUE", - }, - { - "platform": "mqtt", - "schema": "state", - "name": "Test 2", - "command_topic": "command-topic", - "unique_id": "TOTALLY_UNIQUE", - }, - ] + mqtt.DOMAIN: { + vacuum.DOMAIN: [ + { + "schema": "state", + "name": "Test 1", + "command_topic": "command-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + { + "schema": "state", + "name": "Test 2", + "command_topic": "command-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + ] + } } await help_test_unique_id( hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, config @@ -539,42 +539,42 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT vacuum device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT vacuum device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) @@ -584,7 +584,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, - DEFAULT_CONFIG_2_LEGACY, + DEFAULT_CONFIG_2, vacuum.SERVICE_START, command_payload="start", state_payload="{}", @@ -643,8 +643,8 @@ async def test_publishing_with_custom_encoding( ): """Test publishing MQTT payload with different encoding.""" domain = vacuum.DOMAIN - config = deepcopy(DEFAULT_CONFIG_LEGACY[domain]) - config["supported_features"] = [ + config = deepcopy(DEFAULT_CONFIG) + config[mqtt.DOMAIN][domain]["supported_features"] = [ "battery", "clean_spot", "fan_speed", @@ -674,12 +674,14 @@ async def test_publishing_with_custom_encoding( async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = vacuum.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = vacuum.DOMAIN @@ -719,7 +721,7 @@ async def test_encoding_subscribable_topics( mqtt_mock_entry_with_yaml_config, caplog, vacuum.DOMAIN, - DEFAULT_CONFIG_LEGACY[vacuum.DOMAIN], + DEFAULT_CONFIG[mqtt.DOMAIN][vacuum.DOMAIN], topic, value, attribute, @@ -731,11 +733,8 @@ async def test_encoding_subscribable_topics( async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = vacuum.DOMAIN - config = deepcopy(DEFAULT_CONFIG_LEGACY[platform]) - config["name"] = "test" - del config["platform"] - await help_test_setup_manual_entity_from_yaml(hass, platform, config) - assert hass.states.get(f"{platform}.test") is not None + await help_test_setup_manual_entity_from_yaml(hass, DEFAULT_CONFIG) + assert hass.states.get(f"{platform}.mqtttest") # Test deprecated YAML configuration under the platform key diff --git a/tests/components/mqtt/test_switch.py b/tests/components/mqtt/test_switch.py index 87d919f41c5..72a09529242 100644 --- a/tests/components/mqtt/test_switch.py +++ b/tests/components/mqtt/test_switch.py @@ -43,7 +43,7 @@ from .test_common import ( help_test_setup_manual_entity_from_yaml, help_test_unique_id, help_test_unload_config_entry_with_platform, - help_test_update_with_json_attrs_bad_JSON, + help_test_update_with_json_attrs_bad_json, help_test_update_with_json_attrs_not_dict, ) @@ -227,27 +227,28 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, switch.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, switch.DOMAIN, DEFAULT_CONFIG ) async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock_entry_with_yaml_config, switch.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, switch.DOMAIN, DEFAULT_CONFIG ) async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" config = { - switch.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "payload_on": 1, - "payload_off": 0, + mqtt.DOMAIN: { + switch.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "payload_on": 1, + "payload_off": 0, + } } } @@ -265,13 +266,14 @@ async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_conf async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" config = { - switch.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "payload_on": 1, - "payload_off": 0, + mqtt.DOMAIN: { + switch.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "payload_on": 1, + "payload_off": 0, + } } } @@ -328,7 +330,7 @@ async def test_setting_attribute_via_mqtt_json_message( ): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock_entry_with_yaml_config, switch.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, switch.DOMAIN, DEFAULT_CONFIG ) @@ -337,14 +339,14 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( ): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, mqtt_mock_entry_no_yaml_config, switch.DOMAIN, DEFAULT_CONFIG_LEGACY, {} + hass, mqtt_mock_entry_no_yaml_config, switch.DOMAIN, DEFAULT_CONFIG, {} ) async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock_entry_with_yaml_config, switch.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, switch.DOMAIN, DEFAULT_CONFIG ) @@ -357,20 +359,20 @@ async def test_update_with_json_attrs_not_dict( mqtt_mock_entry_with_yaml_config, caplog, switch.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) -async def test_update_with_json_attrs_bad_JSON( +async def test_update_with_json_attrs_bad_json( hass, mqtt_mock_entry_with_yaml_config, caplog ): """Test attributes get extracted from a JSON result.""" - await help_test_update_with_json_attrs_bad_JSON( + await help_test_update_with_json_attrs_bad_json( hass, mqtt_mock_entry_with_yaml_config, caplog, switch.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -381,29 +383,29 @@ async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplo mqtt_mock_entry_no_yaml_config, caplog, switch.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one switch per unique_id.""" config = { - switch.DOMAIN: [ - { - "platform": "mqtt", - "name": "Test 1", - "state_topic": "test-topic", - "command_topic": "command-topic", - "unique_id": "TOTALLY_UNIQUE", - }, - { - "platform": "mqtt", - "name": "Test 2", - "state_topic": "test-topic", - "command_topic": "command-topic", - "unique_id": "TOTALLY_UNIQUE", - }, - ] + mqtt.DOMAIN: { + switch.DOMAIN: [ + { + "name": "Test 1", + "state_topic": "test-topic", + "command_topic": "command-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + { + "name": "Test 2", + "state_topic": "test-topic", + "command_topic": "command-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + ] + } } await help_test_unique_id( hass, mqtt_mock_entry_with_yaml_config, switch.DOMAIN, config @@ -426,8 +428,8 @@ async def test_discovery_update_switch_topic_template( hass, mqtt_mock_entry_no_yaml_config, caplog ): """Test update of discovered switch.""" - config1 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[switch.DOMAIN]) - config2 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[switch.DOMAIN]) + config1 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][switch.DOMAIN]) + config2 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][switch.DOMAIN]) config1["name"] = "Beer" config2["name"] = "Milk" config1["state_topic"] = "switch/state1" @@ -463,8 +465,8 @@ async def test_discovery_update_switch_template( hass, mqtt_mock_entry_no_yaml_config, caplog ): """Test update of discovered switch.""" - config1 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[switch.DOMAIN]) - config2 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[switch.DOMAIN]) + config1 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][switch.DOMAIN]) + config2 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][switch.DOMAIN]) config1["name"] = "Beer" config2["name"] = "Milk" config1["state_topic"] = "switch/state1" @@ -534,42 +536,42 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT switch device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock_entry_no_yaml_config, switch.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, switch.DOMAIN, DEFAULT_CONFIG ) async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT switch device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock_entry_no_yaml_config, switch.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, switch.DOMAIN, DEFAULT_CONFIG ) async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock_entry_no_yaml_config, switch.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, switch.DOMAIN, DEFAULT_CONFIG ) async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock_entry_no_yaml_config, switch.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, switch.DOMAIN, DEFAULT_CONFIG ) async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock_entry_with_yaml_config, switch.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, switch.DOMAIN, DEFAULT_CONFIG ) async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock_entry_no_yaml_config, switch.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, switch.DOMAIN, DEFAULT_CONFIG ) @@ -579,7 +581,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): hass, mqtt_mock_entry_no_yaml_config, switch.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, switch.SERVICE_TURN_ON, ) @@ -615,7 +617,7 @@ async def test_publishing_with_custom_encoding( ): """Test publishing MQTT payload with different encoding.""" domain = switch.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_publishing_with_custom_encoding( hass, @@ -634,12 +636,14 @@ async def test_publishing_with_custom_encoding( async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = switch.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = switch.DOMAIN @@ -668,7 +672,7 @@ async def test_encoding_subscribable_topics( mqtt_mock_entry_with_yaml_config, caplog, switch.DOMAIN, - DEFAULT_CONFIG_LEGACY[switch.DOMAIN], + DEFAULT_CONFIG[mqtt.DOMAIN][switch.DOMAIN], topic, value, attribute, @@ -679,17 +683,14 @@ async def test_encoding_subscribable_topics( async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = switch.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) - config["name"] = "test" - del config["platform"] - await help_test_setup_manual_entity_from_yaml(hass, platform, config) - assert hass.states.get(f"{platform}.test") is not None + await help_test_setup_manual_entity_from_yaml(hass, DEFAULT_CONFIG) + assert hass.states.get(f"{platform}.test") async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): """Test unloading the config entry.""" domain = switch.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) From b369c2f54c4d68c1183b6b2944cd09adf871ad07 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 9 Sep 2022 11:27:16 +0200 Subject: [PATCH 230/955] Deprecate SUPPORT_* constants for color_mode (#69269) * Deprecate SUPPORT_* constants for color_mode * Simplify Co-authored-by: Erik Montnemery Co-authored-by: Erik Montnemery Co-authored-by: Franck Nijhof --- pylint/plugins/hass_imports.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pylint/plugins/hass_imports.py b/pylint/plugins/hass_imports.py index a45e2c91996..0a0d9c8c7b1 100644 --- a/pylint/plugins/hass_imports.py +++ b/pylint/plugins/hass_imports.py @@ -157,6 +157,10 @@ _OBSOLETE_IMPORT: dict[str, list[ObsoleteImportMatch]] = { reason="replaced by ColorMode enum", constant=re.compile(r"^COLOR_MODE_(\w*)$"), ), + ObsoleteImportMatch( + reason="replaced by color modes", + constant=re.compile("^SUPPORT_(BRIGHTNESS|COLOR_TEMP|COLOR)$"), + ), ObsoleteImportMatch( reason="replaced by LightEntityFeature enum", constant=re.compile("^SUPPORT_(EFFECT|FLASH|TRANSITION)$"), From c3b2e03ce8b1b07a9775d03af553d84ae6095893 Mon Sep 17 00:00:00 2001 From: holysoles <31580846+holysoles@users.noreply.github.com> Date: Fri, 9 Sep 2022 04:50:39 -0500 Subject: [PATCH 231/955] Support unique_id for Universal Media Player (#77461) * support unique id * tests for unique_id * use unique_id attribute --- .../components/universal/media_player.py | 5 +++++ .../components/universal/test_media_player.py | 22 +++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/homeassistant/components/universal/media_player.py b/homeassistant/components/universal/media_player.py index 75c2b5d0432..55adadad845 100644 --- a/homeassistant/components/universal/media_player.py +++ b/homeassistant/components/universal/media_player.py @@ -53,6 +53,7 @@ from homeassistant.const import ( CONF_NAME, CONF_STATE, CONF_STATE_TEMPLATE, + CONF_UNIQUE_ID, EVENT_HOMEASSISTANT_START, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE, @@ -124,6 +125,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_ATTRS, default={}): vol.Or( cv.ensure_list(ATTRS_SCHEMA), ATTRS_SCHEMA ), + vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_STATE_TEMPLATE): cv.template, }, @@ -146,6 +148,7 @@ async def async_setup_platform( config.get(CONF_CHILDREN), config.get(CONF_COMMANDS), config.get(CONF_ATTRS), + config.get(CONF_UNIQUE_ID), config.get(CONF_DEVICE_CLASS), config.get(CONF_STATE_TEMPLATE), ) @@ -165,6 +168,7 @@ class UniversalMediaPlayer(MediaPlayerEntity): children, commands, attributes, + unique_id=None, device_class=None, state_template=None, ): @@ -183,6 +187,7 @@ class UniversalMediaPlayer(MediaPlayerEntity): self._state_template_result = None self._state_template = state_template self._device_class = device_class + self._attr_unique_id = unique_id async def async_added_to_hass(self) -> None: """Subscribe to children and template state changes.""" diff --git a/tests/components/universal/test_media_player.py b/tests/components/universal/test_media_player.py index b589fc09a6a..059a19caf45 100644 --- a/tests/components/universal/test_media_player.py +++ b/tests/components/universal/test_media_player.py @@ -21,6 +21,7 @@ from homeassistant.const import ( STATE_UNKNOWN, ) from homeassistant.core import Context, callback +from homeassistant.helpers import entity_registry from homeassistant.helpers.event import async_track_state_change_event from homeassistant.setup import async_setup_component @@ -1092,6 +1093,26 @@ async def test_device_class(hass): assert hass.states.get("media_player.tv").attributes["device_class"] == "tv" +async def test_unique_id(hass): + """Test unique_id property.""" + hass.states.async_set("sensor.test_sensor", "on") + + await async_setup_component( + hass, + "media_player", + { + "media_player": { + "platform": "universal", + "name": "tv", + "unique_id": "universal_master_bed_tv", + } + }, + ) + await hass.async_block_till_done() + er = entity_registry.async_get(hass) + assert er.async_get("media_player.tv").unique_id == "universal_master_bed_tv" + + async def test_invalid_state_template(hass): """Test invalid state template sets state to None.""" hass.states.async_set("sensor.test_sensor", "on") @@ -1220,3 +1241,4 @@ async def test_reload(hass): assert ( "device_class" not in hass.states.get("media_player.master_bed_tv").attributes ) + assert "unique_id" not in hass.states.get("media_player.master_bed_tv").attributes From 9b2d17cd0082caa58f0153262b9b51aa10e32d72 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Fri, 9 Sep 2022 19:18:24 +0800 Subject: [PATCH 232/955] Escape media_content_id in media player proxy (#77811) * Escape media_content_id in media player proxy * Change usage in kodi * Change usage in roku * Change usage in sonos * Add test * Add comment * Change path regex instead of double quoting * Use .+ instead of .* --- homeassistant/components/kodi/media_player.py | 3 +- .../components/media_player/__init__.py | 11 +++++-- homeassistant/components/roku/browse_media.py | 3 +- .../components/sonos/media_browser.py | 5 ++- tests/components/media_player/test_init.py | 31 +++++++++++++++++++ 5 files changed, 43 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index 80331794114..a41738eaa3e 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -7,7 +7,6 @@ from functools import wraps import logging import re from typing import Any, TypeVar -import urllib.parse from jsonrpc_base.jsonrpc import ProtocolError, TransportError from pykodi import CannotConnectError @@ -920,7 +919,7 @@ class KodiEntity(MediaPlayerEntity): return self.get_browse_image_url( media_content_type, - urllib.parse.quote_plus(media_content_id), + media_content_id, media_image_id, ) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 2c24a43ffc3..177d7f5ad74 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -13,7 +13,7 @@ from http import HTTPStatus import logging import secrets from typing import Any, cast, final -from urllib.parse import urlparse +from urllib.parse import quote, urlparse from aiohttp import web from aiohttp.hdrs import CACHE_CONTROL, CONTENT_TYPE @@ -1100,7 +1100,9 @@ class MediaPlayerEntity(Entity): """Generate an url for a media browser image.""" url_path = ( f"/api/media_player_proxy/{self.entity_id}/browse_media" - f"/{media_content_type}/{media_content_id}" + # quote the media_content_id as it may contain url unsafe characters + # aiohttp will unquote the path automatically + f"/{media_content_type}/{quote(media_content_id)}" ) url_query = {"token": self.access_token} @@ -1117,7 +1119,10 @@ class MediaPlayerImageView(HomeAssistantView): url = "/api/media_player_proxy/{entity_id}" name = "api:media_player:image" extra_urls = [ - url + "/browse_media/{media_content_type}/{media_content_id}", + # Need to modify the default regex for media_content_id as it may + # include arbitrary characters including '/','{', or '}' + url + + "/browse_media/{media_content_type}/{media_content_id:.+}", ] def __init__(self, component: EntityComponent) -> None: diff --git a/homeassistant/components/roku/browse_media.py b/homeassistant/components/roku/browse_media.py index 72b572e8d3e..495ff910fd9 100644 --- a/homeassistant/components/roku/browse_media.py +++ b/homeassistant/components/roku/browse_media.py @@ -3,7 +3,6 @@ from __future__ import annotations from collections.abc import Callable from functools import partial -from urllib.parse import quote_plus from homeassistant.components import media_source from homeassistant.components.media_player import BrowseMedia @@ -64,7 +63,7 @@ def get_thumbnail_url_full( return get_browse_image_url( media_content_type, - quote_plus(media_content_id), + media_content_id, media_image_id, ) diff --git a/homeassistant/components/sonos/media_browser.py b/homeassistant/components/sonos/media_browser.py index 95ff08cb87b..3859f691179 100644 --- a/homeassistant/components/sonos/media_browser.py +++ b/homeassistant/components/sonos/media_browser.py @@ -6,7 +6,6 @@ from contextlib import suppress from functools import partial import logging from typing import cast -from urllib.parse import quote_plus, unquote from soco.data_structures import DidlFavorite, DidlObject from soco.ms_data_structures import MusicServiceItem @@ -64,7 +63,7 @@ def get_thumbnail_url_full( return get_browse_image_url( media_content_type, - quote_plus(media_content_id), + media_content_id, media_image_id, ) @@ -201,7 +200,7 @@ def build_item_response( if not title: try: - title = unquote(payload["idstring"].split("/")[1]) + title = payload["idstring"].split("/")[1] except IndexError: title = LIBRARY_TITLES_MAPPING[payload["idstring"]] diff --git a/tests/components/media_player/test_init.py b/tests/components/media_player/test_init.py index f0666a11545..e7946d447e9 100644 --- a/tests/components/media_player/test_init.py +++ b/tests/components/media_player/test_init.py @@ -280,3 +280,34 @@ async def test_enqueue_alert_exclusive(hass): }, blocking=True, ) + + +async def test_get_async_get_browse_image_quoting( + hass, hass_client_no_auth, hass_ws_client +): + """Test get browse image using media_content_id with special characters. + + async_get_browse_image() should get called with the same string that is + passed into get_browse_image_url(). + """ + await async_setup_component( + hass, "media_player", {"media_player": {"platform": "demo"}} + ) + await hass.async_block_till_done() + + entity_comp = hass.data.get("entity_components", {}).get("media_player") + assert entity_comp + + player = entity_comp.get_entity("media_player.bedroom") + assert player + + client = await hass_client_no_auth() + + with patch( + "homeassistant.components.media_player.MediaPlayerEntity." + "async_get_browse_image", + ) as mock_browse_image: + media_content_id = "a/b c/d+e%2Fg{}" + url = player.get_browse_image_url("album", media_content_id) + await client.get(url) + mock_browse_image.assert_called_with("album", media_content_id, None) From e332091d76450e954f6d3b1c051929fb798d4248 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 9 Sep 2022 14:35:23 +0200 Subject: [PATCH 233/955] Improve unique_id collision checks in entity_platform (#78132) --- homeassistant/helpers/entity_platform.py | 82 +++++++++++++++--------- tests/helpers/test_entity_platform.py | 37 +++++++++-- 2 files changed, 83 insertions(+), 36 deletions(-) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index c5c61cc1b0d..81487bbb627 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -454,6 +454,22 @@ class EntityPlatform: self.scan_interval, ) + def _entity_id_already_exists(self, entity_id: str) -> tuple[bool, bool]: + """Check if an entity_id already exists. + + Returns a tuple [already_exists, restored] + """ + already_exists = entity_id in self.entities + restored = False + + if not already_exists and not self.hass.states.async_available(entity_id): + existing = self.hass.states.get(entity_id) + if existing is not None and ATTR_RESTORED in existing.attributes: + restored = True + else: + already_exists = True + return (already_exists, restored) + async def _async_add_entity( # noqa: C901 self, entity: Entity, @@ -480,12 +496,31 @@ class EntityPlatform: entity.add_to_platform_abort() return - requested_entity_id = None suggested_object_id: str | None = None generate_new_entity_id = False # Get entity_id from unique ID registration if entity.unique_id is not None: + registered_entity_id = entity_registry.async_get_entity_id( + self.domain, self.platform_name, entity.unique_id + ) + if registered_entity_id: + already_exists, _ = self._entity_id_already_exists(registered_entity_id) + + if already_exists: + # If there's a collision, the entry belongs to another entity + entity.registry_entry = None + msg = ( + f"Platform {self.platform_name} does not generate unique IDs. " + ) + if entity.entity_id: + msg += f"ID {entity.unique_id} is already used by {registered_entity_id} - ignoring {entity.entity_id}" + else: + msg += f"ID {entity.unique_id} already exists - ignoring {registered_entity_id}" + self.logger.error(msg) + entity.add_to_platform_abort() + return + if self.config_entry is not None: config_entry_id: str | None = self.config_entry.entry_id else: @@ -541,7 +576,6 @@ class EntityPlatform: pass if entity.entity_id is not None: - requested_entity_id = entity.entity_id suggested_object_id = split_entity_id(entity.entity_id)[1] else: if device and entity.has_entity_name: # type: ignore[unreachable] @@ -592,16 +626,6 @@ class EntityPlatform: entity.registry_entry = entry entity.entity_id = entry.entity_id - if entry.disabled: - self.logger.debug( - "Not adding entity %s because it's disabled", - entry.name - or entity.name - or f'"{self.platform_name} {entity.unique_id}"', - ) - entity.add_to_platform_abort() - return - # We won't generate an entity ID if the platform has already set one # We will however make sure that platform cannot pick a registered ID elif entity.entity_id is not None and entity_registry.async_is_registered( @@ -628,28 +652,22 @@ class EntityPlatform: entity.add_to_platform_abort() raise HomeAssistantError(f"Invalid entity ID: {entity.entity_id}") - already_exists = entity.entity_id in self.entities - restored = False - - if not already_exists and not self.hass.states.async_available( - entity.entity_id - ): - existing = self.hass.states.get(entity.entity_id) - if existing is not None and ATTR_RESTORED in existing.attributes: - restored = True - else: - already_exists = True + already_exists, restored = self._entity_id_already_exists(entity.entity_id) if already_exists: - if entity.unique_id is not None: - msg = f"Platform {self.platform_name} does not generate unique IDs. " - if requested_entity_id: - msg += f"ID {entity.unique_id} is already used by {entity.entity_id} - ignoring {requested_entity_id}" - else: - msg += f"ID {entity.unique_id} already exists - ignoring {entity.entity_id}" - else: - msg = f"Entity id already exists - ignoring: {entity.entity_id}" - self.logger.error(msg) + self.logger.error( + f"Entity id already exists - ignoring: {entity.entity_id}" + ) + entity.add_to_platform_abort() + return + + if entity.registry_entry and entity.registry_entry.disabled: + self.logger.debug( + "Not adding entity %s because it's disabled", + entry.name + or entity.name + or f'"{self.platform_name} {entity.unique_id}"', + ) entity.add_to_platform_abort() return diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index d7f77eeacda..b2af85ca631 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -438,13 +438,15 @@ async def test_async_remove_with_platform_update_finishes(hass): async def test_not_adding_duplicate_entities_with_unique_id(hass, caplog): - """Test for not adding duplicate entities.""" + """Test for not adding duplicate entities. + + Also test that the entity registry is not updated for duplicates. + """ caplog.set_level(logging.ERROR) component = EntityComponent(_LOGGER, DOMAIN, hass) - await component.async_add_entities( - [MockEntity(name="test1", unique_id="not_very_unique")] - ) + ent1 = MockEntity(name="test1", unique_id="not_very_unique") + await component.async_add_entities([ent1]) assert len(hass.states.async_entity_ids()) == 1 assert not caplog.text @@ -466,6 +468,11 @@ async def test_not_adding_duplicate_entities_with_unique_id(hass, caplog): assert ent2.platform is None assert len(hass.states.async_entity_ids()) == 1 + registry = er.async_get(hass) + # test the entity name was not updated + entry = registry.async_get_or_create(DOMAIN, DOMAIN, "not_very_unique") + assert entry.original_name == "test1" + async def test_using_prescribed_entity_id(hass): """Test for using predefined entity ID.""" @@ -577,6 +584,28 @@ async def test_registry_respect_entity_disabled(hass): assert hass.states.async_entity_ids() == [] +async def test_unique_id_conflict_has_priority_over_disabled_entity(hass, caplog): + """Test that an entity that is not unique has priority over a disabled entity.""" + component = EntityComponent(_LOGGER, DOMAIN, hass) + entity1 = MockEntity( + name="test1", unique_id="not_very_unique", enabled_by_default=False + ) + entity2 = MockEntity( + name="test2", unique_id="not_very_unique", enabled_by_default=False + ) + await component.async_add_entities([entity1]) + await component.async_add_entities([entity2]) + + assert len(hass.states.async_entity_ids()) == 1 + assert "Platform test_domain does not generate unique IDs." in caplog.text + assert entity1.registry_entry is not None + assert entity2.registry_entry is None + registry = er.async_get(hass) + # test the entity name was not updated + entry = registry.async_get_or_create(DOMAIN, DOMAIN, "not_very_unique") + assert entry.original_name == "test1" + + async def test_entity_registry_updates_name(hass): """Test that updates on the entity registry update platform entities.""" registry = mock_registry( From 0a143ac5960390a7447ec2cb44e87e7c7444b00e Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Fri, 9 Sep 2022 14:43:54 +0200 Subject: [PATCH 234/955] Fix LIFX light turning on while fading off (#78095) --- homeassistant/components/lifx/light.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index 4df04f2d1e7..ec3223c03a2 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -237,15 +237,10 @@ class LIFXLight(LIFXEntity, LightEntity): elif power_on: await self.set_power(True, duration=fade) else: + if power_on: + await self.set_power(True) if hsbk: await self.set_color(hsbk, kwargs, duration=fade) - # The response from set_color will tell us if the - # bulb is actually on or not, so we don't need to - # call power_on if its already on - if power_on and self.bulb.power_level == 0: - await self.set_power(True) - elif power_on: - await self.set_power(True) if power_off: await self.set_power(False, duration=fade) From fb67123d77292a7a1ffd50139938dd2816dfec05 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 9 Sep 2022 15:24:26 +0200 Subject: [PATCH 235/955] Clear MQTT discovery topic when a disabled entity is removed (#77757) * Cleanup discovery on entity removal * Add test * Cleanup and test * Test with clearing payload not unique id * Address comments * Tests cover and typing * Just pass hass * reuse code * Follow up comments revert changes to cover tests * Add test unique_id has priority over disabled * Update homeassistant/components/mqtt/__init__.py Co-authored-by: Erik Montnemery Co-authored-by: Erik Montnemery --- homeassistant/components/mqtt/__init__.py | 16 +- homeassistant/components/mqtt/const.py | 1 + homeassistant/components/mqtt/mixins.py | 51 ++++++- tests/components/mqtt/test_discovery.py | 170 ++++++++++++++++++++++ 4 files changed, 232 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 3ec9a7e9d4e..842e5b6405f 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -20,7 +20,13 @@ from homeassistant.const import ( CONF_USERNAME, SERVICE_RELOAD, ) -from homeassistant.core import HassJob, HomeAssistant, ServiceCall, callback +from homeassistant.core import ( + CALLBACK_TYPE, + HassJob, + HomeAssistant, + ServiceCall, + callback, +) from homeassistant.exceptions import TemplateError, Unauthorized from homeassistant.helpers import ( config_validation as cv, @@ -68,6 +74,7 @@ from .const import ( # noqa: F401 CONFIG_ENTRY_IS_SETUP, DATA_MQTT, DATA_MQTT_CONFIG, + DATA_MQTT_DISCOVERY_REGISTRY_HOOKS, DATA_MQTT_RELOAD_DISPATCHERS, DATA_MQTT_RELOAD_ENTRY, DATA_MQTT_RELOAD_NEEDED, @@ -315,6 +322,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Bail out return False + hass.data[DATA_MQTT_DISCOVERY_REGISTRY_HOOKS] = {} hass.data[DATA_MQTT] = MQTT(hass, entry, conf) # Restore saved subscriptions if DATA_MQTT_SUBSCRIPTIONS_TO_RESTORE in hass.data: @@ -638,6 +646,12 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DATA_MQTT_RELOAD_ENTRY] = True # Reload the legacy yaml platform to make entities unavailable await async_reload_integration_platforms(hass, DOMAIN, RELOADABLE_PLATFORMS) + # Cleanup entity registry hooks + registry_hooks: dict[tuple, CALLBACK_TYPE] = hass.data[ + DATA_MQTT_DISCOVERY_REGISTRY_HOOKS + ] + while registry_hooks: + registry_hooks.popitem()[1]() # Wait for all ACKs and stop the loop await mqtt_client.async_disconnect() # Store remaining subscriptions to be able to restore or reload them diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index 0c711e097d5..c8af58862e0 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -33,6 +33,7 @@ CONF_TLS_VERSION = "tls_version" CONFIG_ENTRY_IS_SETUP = "mqtt_config_entry_is_setup" DATA_MQTT = "mqtt" DATA_MQTT_SUBSCRIPTIONS_TO_RESTORE = "mqtt_client_subscriptions" +DATA_MQTT_DISCOVERY_REGISTRY_HOOKS = "mqtt_discovery_registry_hooks" DATA_MQTT_CONFIG = "mqtt_config" MQTT_DATA_DEVICE_TRACKER_LEGACY = "mqtt_device_tracker_legacy" DATA_MQTT_RELOAD_DISPATCHERS = "mqtt_reload_dispatchers" diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 75532f75c13..fddbe838303 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -28,7 +28,13 @@ from homeassistant.const import ( CONF_UNIQUE_ID, CONF_VALUE_TEMPLATE, ) -from homeassistant.core import Event, HomeAssistant, async_get_hass, callback +from homeassistant.core import ( + CALLBACK_TYPE, + Event, + HomeAssistant, + async_get_hass, + callback, +) from homeassistant.helpers import ( config_validation as cv, device_registry as dr, @@ -48,6 +54,7 @@ from homeassistant.helpers.entity import ( async_generate_entity_id, ) from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.event import async_track_entity_registry_updated_event from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.json import json_loads from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -64,6 +71,7 @@ from .const import ( CONF_TOPIC, DATA_MQTT, DATA_MQTT_CONFIG, + DATA_MQTT_DISCOVERY_REGISTRY_HOOKS, DATA_MQTT_RELOAD_DISPATCHERS, DATA_MQTT_RELOAD_ENTRY, DATA_MQTT_UPDATED_CONFIG, @@ -654,6 +662,17 @@ async def async_remove_discovery_payload(hass: HomeAssistant, discovery_data: di await async_publish(hass, discovery_topic, "", retain=True) +async def async_clear_discovery_topic_if_entity_removed( + hass: HomeAssistant, + discovery_data: dict[str, Any], + event: Event, +) -> None: + """Clear the discovery topic if the entity is removed.""" + if event.data["action"] == "remove": + # publish empty payload to config topic to avoid re-adding + await async_remove_discovery_payload(hass, discovery_data) + + class MqttDiscoveryDeviceUpdate: """Add support for auto discovery for platforms without an entity.""" @@ -787,7 +806,8 @@ class MqttDiscoveryUpdate(Entity): def __init__( self, - discovery_data: dict, + hass: HomeAssistant, + discovery_data: dict | None, discovery_update: Callable | None = None, ) -> None: """Initialize the discovery update mixin.""" @@ -795,6 +815,14 @@ class MqttDiscoveryUpdate(Entity): self._discovery_update = discovery_update self._remove_discovery_updated: Callable | None = None self._removed_from_hass = False + if discovery_data is None: + return + self._registry_hooks: dict[tuple, CALLBACK_TYPE] = hass.data[ + DATA_MQTT_DISCOVERY_REGISTRY_HOOKS + ] + discovery_hash: tuple[str, str] = discovery_data[ATTR_DISCOVERY_HASH] + if discovery_hash in self._registry_hooks: + self._registry_hooks.pop(discovery_hash)() async def async_added_to_hass(self) -> None: """Subscribe to discovery updates.""" @@ -857,7 +885,7 @@ class MqttDiscoveryUpdate(Entity): async def async_removed_from_registry(self) -> None: """Clear retained discovery topic in broker.""" - if not self._removed_from_hass: + if not self._removed_from_hass and self._discovery_data is not None: # Stop subscribing to discovery updates to not trigger when we clear the # discovery topic self._cleanup_discovery_on_remove() @@ -868,7 +896,20 @@ class MqttDiscoveryUpdate(Entity): @callback def add_to_platform_abort(self) -> None: """Abort adding an entity to a platform.""" - if self._discovery_data: + if self._discovery_data is not None: + discovery_hash: tuple = self._discovery_data[ATTR_DISCOVERY_HASH] + if self.registry_entry is not None: + self._registry_hooks[ + discovery_hash + ] = async_track_entity_registry_updated_event( + self.hass, + self.entity_id, + partial( + async_clear_discovery_topic_if_entity_removed, + self.hass, + self._discovery_data, + ), + ) stop_discovery_updates(self.hass, self._discovery_data) send_discovery_done(self.hass, self._discovery_data) super().add_to_platform_abort() @@ -976,7 +1017,7 @@ class MqttEntity( # Initialize mixin classes MqttAttributes.__init__(self, config) MqttAvailability.__init__(self, config) - MqttDiscoveryUpdate.__init__(self, discovery_data, self.discovery_update) + MqttDiscoveryUpdate.__init__(self, hass, discovery_data, self.discovery_update) MqttEntityDeviceInfo.__init__(self, config.get(CONF_DEVICE), config_entry) def _init_entity_id(self): diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index 29ca1f11743..c625d0a21f9 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -1,4 +1,5 @@ """The tests for the MQTT discovery.""" +import copy import json from pathlib import Path import re @@ -23,6 +24,8 @@ from homeassistant.const import ( import homeassistant.core as ha from homeassistant.setup import async_setup_component +from .test_common import help_test_unload_config_entry + from tests.common import ( MockConfigEntry, async_capture_events, @@ -1356,3 +1359,170 @@ async def test_mqtt_discovery_unsubscribe_once( await hass.async_block_till_done() await hass.async_block_till_done() mqtt_client_mock.unsubscribe.assert_called_once_with("comp/discovery/#") + + +@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.SENSOR]) +async def test_clear_config_topic_disabled_entity( + hass, mqtt_mock_entry_no_yaml_config, device_reg, caplog +): + """Test the discovery topic is removed when a disabled entity is removed.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() + # discover an entity that is not enabled by default + config = { + "name": "sbfspot_12345", + "state_topic": "homeassistant_test/sensor/sbfspot_0/sbfspot_12345/", + "unique_id": "sbfspot_12345", + "enabled_by_default": False, + "device": { + "identifiers": ["sbfspot_12345"], + "name": "sbfspot_12345", + "sw_version": "1.0", + "connections": [["mac", "12:34:56:AB:CD:EF"]], + }, + } + async_fire_mqtt_message( + hass, + "homeassistant/sensor/sbfspot_0/sbfspot_12345/config", + json.dumps(config), + ) + await hass.async_block_till_done() + # discover an entity that is not unique (part 1), will be added + config_not_unique1 = copy.deepcopy(config) + config_not_unique1["name"] = "sbfspot_12345_1" + config_not_unique1["unique_id"] = "not_unique" + config_not_unique1.pop("enabled_by_default") + async_fire_mqtt_message( + hass, + "homeassistant/sensor/sbfspot_0/sbfspot_12345_1/config", + json.dumps(config_not_unique1), + ) + # discover an entity that is not unique (part 2), will not be added + config_not_unique2 = copy.deepcopy(config_not_unique1) + config_not_unique2["name"] = "sbfspot_12345_2" + async_fire_mqtt_message( + hass, + "homeassistant/sensor/sbfspot_0/sbfspot_12345_2/config", + json.dumps(config_not_unique2), + ) + await hass.async_block_till_done() + assert "Platform mqtt does not generate unique IDs" in caplog.text + + assert hass.states.get("sensor.sbfspot_12345") is None # disabled + assert hass.states.get("sensor.sbfspot_12345_1") is not None # enabled + assert hass.states.get("sensor.sbfspot_12345_2") is None # not unique + + # Verify device is created + device_entry = device_reg.async_get_device(set(), {("mac", "12:34:56:AB:CD:EF")}) + assert device_entry is not None + + # Remove the device from the registry + device_reg.async_remove_device(device_entry.id) + await hass.async_block_till_done() + await hass.async_block_till_done() + + # Assert all valid discovery topics are cleared + assert mqtt_mock.async_publish.call_count == 2 + assert ( + call("homeassistant/sensor/sbfspot_0/sbfspot_12345/config", "", 0, True) + in mqtt_mock.async_publish.mock_calls + ) + assert ( + call("homeassistant/sensor/sbfspot_0/sbfspot_12345_1/config", "", 0, True) + in mqtt_mock.async_publish.mock_calls + ) + + +@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.SENSOR]) +async def test_clean_up_registry_monitoring( + hass, mqtt_mock_entry_no_yaml_config, device_reg, tmp_path +): + """Test registry monitoring hook is removed after a reload.""" + await mqtt_mock_entry_no_yaml_config() + hooks: dict = hass.data[mqtt.const.DATA_MQTT_DISCOVERY_REGISTRY_HOOKS] + # discover an entity that is not enabled by default + config1 = { + "name": "sbfspot_12345", + "state_topic": "homeassistant_test/sensor/sbfspot_0/sbfspot_12345/", + "unique_id": "sbfspot_12345", + "enabled_by_default": False, + "device": { + "identifiers": ["sbfspot_12345"], + "name": "sbfspot_12345", + "sw_version": "1.0", + "connections": [["mac", "12:34:56:AB:CD:EF"]], + }, + } + # Publish it config + # Since it is not enabled_by_default the sensor will not be loaded + # it should register a hook for monitoring the entiry registry + async_fire_mqtt_message( + hass, + "homeassistant/sensor/sbfspot_0/sbfspot_12345/config", + json.dumps(config1), + ) + await hass.async_block_till_done() + assert len(hooks) == 1 + + # Publish it again no new monitor should be started + async_fire_mqtt_message( + hass, + "homeassistant/sensor/sbfspot_0/sbfspot_12345/config", + json.dumps(config1), + ) + await hass.async_block_till_done() + assert len(hooks) == 1 + + # Verify device is created + device_entry = device_reg.async_get_device(set(), {("mac", "12:34:56:AB:CD:EF")}) + assert device_entry is not None + + # Enload the entry + # The monitoring should be cleared + await help_test_unload_config_entry(hass, tmp_path, {}) + assert len(hooks) == 0 + + +@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.SENSOR]) +async def test_unique_id_collission_has_priority( + hass, mqtt_mock_entry_no_yaml_config, entity_reg +): + """Test tehe unique_id collision detection has priority over registry disabled items.""" + await mqtt_mock_entry_no_yaml_config() + config = { + "name": "sbfspot_12345", + "state_topic": "homeassistant_test/sensor/sbfspot_0/sbfspot_12345/", + "unique_id": "sbfspot_12345", + "enabled_by_default": False, + "device": { + "identifiers": ["sbfspot_12345"], + "name": "sbfspot_12345", + "sw_version": "1.0", + "connections": [["mac", "12:34:56:AB:CD:EF"]], + }, + } + # discover an entity that is not unique and disabled by default (part 1), will be added + config_not_unique1 = copy.deepcopy(config) + config_not_unique1["name"] = "sbfspot_12345_1" + config_not_unique1["unique_id"] = "not_unique" + async_fire_mqtt_message( + hass, + "homeassistant/sensor/sbfspot_0/sbfspot_12345_1/config", + json.dumps(config_not_unique1), + ) + # discover an entity that is not unique (part 2), will not be added, and the registry entry is cleared + config_not_unique2 = copy.deepcopy(config_not_unique1) + config_not_unique2["name"] = "sbfspot_12345_2" + async_fire_mqtt_message( + hass, + "homeassistant/sensor/sbfspot_0/sbfspot_12345_2/config", + json.dumps(config_not_unique2), + ) + await hass.async_block_till_done() + + assert hass.states.get("sensor.sbfspot_12345_1") is None # not enabled + assert hass.states.get("sensor.sbfspot_12345_2") is None # not unique + + # Verify the first entity is created + assert entity_reg.async_get("sensor.sbfspot_12345_1") is not None + # Verify the second entity is not created because it is not unique + assert entity_reg.async_get("sensor.sbfspot_12345_2") is None From dd86d7f0ea56e1d04d20ec6016f3dae3079e677c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 9 Sep 2022 15:27:30 +0200 Subject: [PATCH 236/955] Use new media player enums in mediaroom (#78108) --- .../components/mediaroom/media_player.py | 52 ++++++++----------- 1 file changed, 23 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/mediaroom/media_player.py b/homeassistant/components/mediaroom/media_player.py index 6dd267e7a12..b5089c64b14 100644 --- a/homeassistant/components/mediaroom/media_player.py +++ b/homeassistant/components/mediaroom/media_player.py @@ -17,19 +17,15 @@ from homeassistant.components.media_player import ( PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, + MediaType, ) -from homeassistant.components.media_player.const import MEDIA_TYPE_CHANNEL from homeassistant.const import ( CONF_HOST, CONF_NAME, CONF_OPTIMISTIC, CONF_TIMEOUT, EVENT_HOMEASSISTANT_STOP, - STATE_OFF, - STATE_PAUSED, - STATE_PLAYING, - STATE_STANDBY, - STATE_UNAVAILABLE, ) from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv @@ -118,6 +114,7 @@ async def async_setup_platform( class MediaroomDevice(MediaPlayerEntity): """Representation of a Mediaroom set-up-box on the network.""" + _attr_media_content_type = MediaType.CHANNEL _attr_should_poll = False _attr_supported_features = ( MediaPlayerEntityFeature.PAUSE @@ -136,13 +133,13 @@ class MediaroomDevice(MediaPlayerEntity): """Map pymediaroom state to HA state.""" state_map = { - State.OFF: STATE_OFF, - State.STANDBY: STATE_STANDBY, - State.PLAYING_LIVE_TV: STATE_PLAYING, - State.PLAYING_RECORDED_TV: STATE_PLAYING, - State.PLAYING_TIMESHIFT_TV: STATE_PLAYING, - State.STOPPED: STATE_PAUSED, - State.UNKNOWN: STATE_UNAVAILABLE, + State.OFF: MediaPlayerState.OFF, + State.STANDBY: MediaPlayerState.STANDBY, + State.PLAYING_LIVE_TV: MediaPlayerState.PLAYING, + State.PLAYING_RECORDED_TV: MediaPlayerState.PLAYING, + State.PLAYING_TIMESHIFT_TV: MediaPlayerState.PLAYING, + State.STOPPED: MediaPlayerState.PAUSED, + State.UNKNOWN: None, } self._state = state_map[mediaroom_state] @@ -157,7 +154,9 @@ class MediaroomDevice(MediaPlayerEntity): ) self._channel = None self._optimistic = optimistic - self._state = STATE_PLAYING if optimistic else STATE_STANDBY + self._state = ( + MediaPlayerState.PLAYING if optimistic else MediaPlayerState.STANDBY + ) self._name = f"Mediaroom {device_id if device_id else host}" self._available = True if device_id: @@ -191,7 +190,7 @@ class MediaroomDevice(MediaPlayerEntity): ) async def async_play_media( - self, media_type: str, media_id: str, **kwargs: Any + self, media_type: MediaType | str, media_id: str, **kwargs: Any ) -> None: """Play media.""" @@ -199,7 +198,7 @@ class MediaroomDevice(MediaPlayerEntity): "STB(%s) Play media: %s (%s)", self.stb.stb_ip, media_id, media_type ) command: str | int - if media_type == MEDIA_TYPE_CHANNEL: + if media_type == MediaType.CHANNEL: if not media_id.isdigit(): _LOGGER.error("Invalid media_id %s: Must be a channel number", media_id) return @@ -216,7 +215,7 @@ class MediaroomDevice(MediaPlayerEntity): try: await self.stb.send_cmd(command) if self._optimistic: - self._state = STATE_PLAYING + self._state = MediaPlayerState.PLAYING self._available = True except PyMediaroomError: self._available = False @@ -237,11 +236,6 @@ class MediaroomDevice(MediaPlayerEntity): """Return the state of the device.""" return self._state - @property - def media_content_type(self): - """Return the content type of current playing media.""" - return MEDIA_TYPE_CHANNEL - @property def media_channel(self): """Channel currently playing.""" @@ -253,7 +247,7 @@ class MediaroomDevice(MediaPlayerEntity): try: self.set_state(await self.stb.turn_on()) if self._optimistic: - self._state = STATE_PLAYING + self._state = MediaPlayerState.PLAYING self._available = True except PyMediaroomError: self._available = False @@ -265,7 +259,7 @@ class MediaroomDevice(MediaPlayerEntity): try: self.set_state(await self.stb.turn_off()) if self._optimistic: - self._state = STATE_STANDBY + self._state = MediaPlayerState.STANDBY self._available = True except PyMediaroomError: self._available = False @@ -278,7 +272,7 @@ class MediaroomDevice(MediaPlayerEntity): _LOGGER.debug("media_play()") await self.stb.send_cmd("PlayPause") if self._optimistic: - self._state = STATE_PLAYING + self._state = MediaPlayerState.PLAYING self._available = True except PyMediaroomError: self._available = False @@ -290,7 +284,7 @@ class MediaroomDevice(MediaPlayerEntity): try: await self.stb.send_cmd("PlayPause") if self._optimistic: - self._state = STATE_PAUSED + self._state = MediaPlayerState.PAUSED self._available = True except PyMediaroomError: self._available = False @@ -302,7 +296,7 @@ class MediaroomDevice(MediaPlayerEntity): try: await self.stb.send_cmd("Stop") if self._optimistic: - self._state = STATE_PAUSED + self._state = MediaPlayerState.PAUSED self._available = True except PyMediaroomError: self._available = False @@ -314,7 +308,7 @@ class MediaroomDevice(MediaPlayerEntity): try: await self.stb.send_cmd("ProgDown") if self._optimistic: - self._state = STATE_PLAYING + self._state = MediaPlayerState.PLAYING self._available = True except PyMediaroomError: self._available = False @@ -326,7 +320,7 @@ class MediaroomDevice(MediaPlayerEntity): try: await self.stb.send_cmd("ProgUp") if self._optimistic: - self._state = STATE_PLAYING + self._state = MediaPlayerState.PLAYING self._available = True except PyMediaroomError: self._available = False From 5c40dffb29912fc7d1317e9ab74531297b757ba1 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 9 Sep 2022 16:05:14 +0200 Subject: [PATCH 237/955] Allow non-integers in threshold sensor config flow (#78137) --- homeassistant/components/threshold/config_flow.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/threshold/config_flow.py b/homeassistant/components/threshold/config_flow.py index 1e6236259bd..45ccdcb4a5c 100644 --- a/homeassistant/components/threshold/config_flow.py +++ b/homeassistant/components/threshold/config_flow.py @@ -30,13 +30,19 @@ OPTIONS_SCHEMA = vol.Schema( vol.Required( CONF_HYSTERESIS, default=DEFAULT_HYSTERESIS ): selector.NumberSelector( - selector.NumberSelectorConfig(mode=selector.NumberSelectorMode.BOX), + selector.NumberSelectorConfig( + mode=selector.NumberSelectorMode.BOX, step=1e-3 + ), ), vol.Optional(CONF_LOWER): selector.NumberSelector( - selector.NumberSelectorConfig(mode=selector.NumberSelectorMode.BOX), + selector.NumberSelectorConfig( + mode=selector.NumberSelectorMode.BOX, step=1e-3 + ), ), vol.Optional(CONF_UPPER): selector.NumberSelector( - selector.NumberSelectorConfig(mode=selector.NumberSelectorMode.BOX), + selector.NumberSelectorConfig( + mode=selector.NumberSelectorMode.BOX, step=1e-3 + ), ), } ) From 6a41a631db3d40881c37fe530a192b68385d3161 Mon Sep 17 00:00:00 2001 From: Yevhenii Vaskivskyi Date: Fri, 9 Sep 2022 16:36:48 +0200 Subject: [PATCH 238/955] Add missing strings for errors in amberelectric config flow (#78140) --- homeassistant/components/amberelectric/strings.json | 5 +++++ homeassistant/components/amberelectric/translations/en.json | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/homeassistant/components/amberelectric/strings.json b/homeassistant/components/amberelectric/strings.json index 61d2c061955..5235a8bf325 100644 --- a/homeassistant/components/amberelectric/strings.json +++ b/homeassistant/components/amberelectric/strings.json @@ -15,6 +15,11 @@ }, "description": "Select the NMI of the site you would like to add" } + }, + "error": { + "invalid_api_token": "[%key:common::config_flow::error::invalid_api_key%]", + "no_site": "No site provided", + "unknown_error": "[%key:common::config_flow::error::unknown%]" } } } diff --git a/homeassistant/components/amberelectric/translations/en.json b/homeassistant/components/amberelectric/translations/en.json index b4d30925aa7..0a974298134 100644 --- a/homeassistant/components/amberelectric/translations/en.json +++ b/homeassistant/components/amberelectric/translations/en.json @@ -15,6 +15,11 @@ }, "description": "Go to {api_url} to generate an API key" } + }, + "error": { + "invalid_api_token": "Invalid API key", + "no_site": "No site provided", + "unknown_error": "Unexpected error" } } } \ No newline at end of file From 7a3ca8278da686202ab4dd4303929a07e5dff7b9 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 9 Sep 2022 16:42:01 +0200 Subject: [PATCH 239/955] Expose climate constants at the top level (#78018) * Expose climate constants at the top level * Add new climate enums * Add new climate enums * Import new enums * Adjust ClimateEntity * Adjust pylint * Fix mypy * Revert "Fix mypy" This reverts commit 3dbe2fab01e30091ab51bcd090ae5ddc5c807045. * Revert "Adjust pylint" This reverts commit b19b085b2281e1cb42b814f4dbe8838d1b6db7b2. * Revert "Adjust ClimateEntity" This reverts commit 6a822c58f1b6bb997038eeb750030ddf13872ec2. * Revert "Import new enums" This reverts commit 7d70007c60d6a4156466c9aad428b8583a8b70bd. * Revert "Add new climate enums" This reverts commit dcd7716106e66449255f807b0e1467912ed3ed11. * Revert "Add new climate enums" This reverts commit a9aaa08a1cb5e6376e0f2d082d0809301cf68d7a. --- homeassistant/components/climate/__init__.py | 23 ++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index 818caaaa78d..0f3e5666bc6 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -55,11 +55,29 @@ from .const import ( # noqa: F401 ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_STEP, DOMAIN, + FAN_AUTO, + FAN_DIFFUSE, + FAN_FOCUS, + FAN_HIGH, + FAN_LOW, + FAN_MEDIUM, + FAN_MIDDLE, + FAN_OFF, + FAN_ON, + FAN_TOP, HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, HVAC_MODES, + PRESET_ACTIVITY, + PRESET_AWAY, + PRESET_BOOST, + PRESET_COMFORT, + PRESET_ECO, + PRESET_HOME, + PRESET_NONE, + PRESET_SLEEP, SERVICE_SET_AUX_HEAT, SERVICE_SET_FAN_MODE, SERVICE_SET_HUMIDITY, @@ -74,6 +92,11 @@ from .const import ( # noqa: F401 SUPPORT_TARGET_HUMIDITY, SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, + SWING_BOTH, + SWING_HORIZONTAL, + SWING_OFF, + SWING_ON, + SWING_VERTICAL, ClimateEntityFeature, HVACAction, HVACMode, From 167b9cb1a0f1083df0ed07e7f68b5b9cc2cbdc7c Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 9 Sep 2022 17:06:57 +0200 Subject: [PATCH 240/955] Additional cleanup for Sensibo (#78144) * Clean sensibo code * Add function to description --- homeassistant/components/sensibo/button.py | 7 +-- homeassistant/components/sensibo/climate.py | 25 +------- homeassistant/components/sensibo/number.py | 8 +-- homeassistant/components/sensibo/select.py | 5 +- homeassistant/components/sensibo/switch.py | 64 ++++++++------------- 5 files changed, 30 insertions(+), 79 deletions(-) diff --git a/homeassistant/components/sensibo/button.py b/homeassistant/components/sensibo/button.py index b91bddaf882..f1b2c53408d 100644 --- a/homeassistant/components/sensibo/button.py +++ b/homeassistant/components/sensibo/button.py @@ -4,8 +4,6 @@ from __future__ import annotations from dataclasses import dataclass from typing import Any -from pysensibo.model import SensiboDevice - from homeassistant.components.button import ButtonEntity, ButtonEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -77,15 +75,12 @@ class SensiboDeviceButton(SensiboDeviceBaseEntity, ButtonEntity): async def async_press(self) -> None: """Press the button.""" await self.async_send_api_call( - device_data=self.device_data, key=self.entity_description.data_key, value=False, ) @async_handle_api_call - async def async_send_api_call( - self, device_data: SensiboDevice, key: Any, value: Any - ) -> bool: + async def async_send_api_call(self, key: str, value: Any) -> bool: """Make service call to api.""" result = await self._client.async_reset_filter( self._device_id, diff --git a/homeassistant/components/sensibo/climate.py b/homeassistant/components/sensibo/climate.py index 8bd225f2e4d..36047fe0ddd 100644 --- a/homeassistant/components/sensibo/climate.py +++ b/homeassistant/components/sensibo/climate.py @@ -4,7 +4,6 @@ from __future__ import annotations from bisect import bisect_left from typing import TYPE_CHECKING, Any -from pysensibo.model import SensiboDevice import voluptuous as vol from homeassistant.components.climate import ( @@ -252,7 +251,6 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity): new_temp = _find_valid_target_temp(temperature, self.device_data.temp_list) await self.async_send_api_call( - device_data=self.device_data, key=AC_STATE_TO_DATA["targetTemperature"], value=new_temp, name="targetTemperature", @@ -265,7 +263,6 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity): raise HomeAssistantError("Current mode doesn't support setting Fanlevel") await self.async_send_api_call( - device_data=self.device_data, key=AC_STATE_TO_DATA["fanLevel"], value=fan_mode, name="fanLevel", @@ -276,7 +273,6 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity): """Set new target operation mode.""" if hvac_mode == HVACMode.OFF: await self.async_send_api_call( - device_data=self.device_data, key=AC_STATE_TO_DATA["on"], value=False, name="on", @@ -287,7 +283,6 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity): # Turn on if not currently on. if not self.device_data.device_on: await self.async_send_api_call( - device_data=self.device_data, key=AC_STATE_TO_DATA["on"], value=True, name="on", @@ -295,7 +290,6 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity): ) await self.async_send_api_call( - device_data=self.device_data, key=AC_STATE_TO_DATA["mode"], value=HA_TO_SENSIBO[hvac_mode], name="mode", @@ -308,7 +302,6 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity): raise HomeAssistantError("Current mode doesn't support setting Swing") await self.async_send_api_call( - device_data=self.device_data, key=AC_STATE_TO_DATA["swing"], value=swing_mode, name="swing", @@ -318,7 +311,6 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity): async def async_turn_on(self) -> None: """Turn Sensibo unit on.""" await self.async_send_api_call( - device_data=self.device_data, key=AC_STATE_TO_DATA["on"], value=True, name="on", @@ -328,7 +320,6 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity): async def async_turn_off(self) -> None: """Turn Sensibo unit on.""" await self.async_send_api_call( - device_data=self.device_data, key=AC_STATE_TO_DATA["on"], value=False, name="on", @@ -338,7 +329,6 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity): async def async_assume_state(self, state: str) -> None: """Sync state with api.""" await self.async_send_api_call( - device_data=self.device_data, key=AC_STATE_TO_DATA["on"], value=state != HVACMode.OFF, name="on", @@ -353,10 +343,8 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity): "acState": {**self.device_data.ac_states, "on": new_state}, } await self.api_call_custom_service_timer( - device_data=self.device_data, key="timer_on", value=True, - command="set_timer", data=params, ) @@ -385,18 +373,15 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity): params["primeIntegration"] = outdoor_integration await self.api_call_custom_service_pure_boost( - device_data=self.device_data, key="pure_boost_enabled", value=True, - command="set_pure_boost", data=params, ) @async_handle_api_call async def async_send_api_call( self, - device_data: SensiboDevice, - key: Any, + key: str, value: Any, name: str, assumed_state: bool = False, @@ -414,10 +399,8 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity): @async_handle_api_call async def api_call_custom_service_timer( self, - device_data: SensiboDevice, - key: Any, + key: str, value: Any, - command: str, data: dict, ) -> bool: """Make service call to api.""" @@ -428,10 +411,8 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity): @async_handle_api_call async def api_call_custom_service_pure_boost( self, - device_data: SensiboDevice, - key: Any, + key: str, value: Any, - command: str, data: dict, ) -> bool: """Make service call to api.""" diff --git a/homeassistant/components/sensibo/number.py b/homeassistant/components/sensibo/number.py index 6550d7382d3..40ae8d7601b 100644 --- a/homeassistant/components/sensibo/number.py +++ b/homeassistant/components/sensibo/number.py @@ -100,14 +100,10 @@ class SensiboNumber(SensiboDeviceBaseEntity, NumberEntity): async def async_set_native_value(self, value: float) -> None: """Set value for calibration.""" - await self.async_send_api_call( - device_data=self.device_data, key=self.entity_description.key, value=value - ) + await self.async_send_api_call(key=self.entity_description.key, value=value) @async_handle_api_call - async def async_send_api_call( - self, device_data: SensiboDevice, key: Any, value: Any - ) -> bool: + async def async_send_api_call(self, key: str, value: Any) -> bool: """Make service call to api.""" data = {self.entity_description.remote_key: value} result = await self._client.async_set_calibration( diff --git a/homeassistant/components/sensibo/select.py b/homeassistant/components/sensibo/select.py index ab95377d016..afb961b429a 100644 --- a/homeassistant/components/sensibo/select.py +++ b/homeassistant/components/sensibo/select.py @@ -108,15 +108,12 @@ class SensiboSelect(SensiboDeviceBaseEntity, SelectEntity): ) await self.async_send_api_call( - device_data=self.device_data, key=self.entity_description.data_key, value=option, ) @async_handle_api_call - async def async_send_api_call( - self, device_data: SensiboDevice, key: Any, value: Any - ) -> bool: + async def async_send_api_call(self, key: str, value: Any) -> bool: """Make service call to api.""" data = { "name": self.entity_description.key, diff --git a/homeassistant/components/sensibo/switch.py b/homeassistant/components/sensibo/switch.py index c06bf4d1ac6..cefa8ece084 100644 --- a/homeassistant/components/sensibo/switch.py +++ b/homeassistant/components/sensibo/switch.py @@ -49,8 +49,8 @@ DEVICE_SWITCH_TYPES: tuple[SensiboDeviceSwitchEntityDescription, ...] = ( icon="mdi:timer", value_fn=lambda data: data.timer_on, extra_fn=lambda data: {"id": data.timer_id, "turn_on": data.timer_state_on}, - command_on="set_timer", - command_off="del_timer", + command_on="async_turn_on_timer", + command_off="async_turn_off_timer", data_key="timer_on", ), ) @@ -62,8 +62,8 @@ PURE_SWITCH_TYPES: tuple[SensiboDeviceSwitchEntityDescription, ...] = ( name="Pure Boost", value_fn=lambda data: data.pure_boost_enabled, extra_fn=None, - command_on="set_pure_boost", - command_off="set_pure_boost", + command_on="async_turn_on_off_pure_boost", + command_off="async_turn_on_off_pure_boost", data_key="pure_boost_enabled", ), ) @@ -113,33 +113,21 @@ class SensiboDeviceSwitch(SensiboDeviceBaseEntity, SwitchEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" - if self.entity_description.key == "timer_on_switch": - await self.async_turn_on_timer( - device_data=self.device_data, - key=self.entity_description.data_key, - value=True, - ) - if self.entity_description.key == "pure_boost_switch": - await self.async_turn_on_off_pure_boost( - device_data=self.device_data, - key=self.entity_description.data_key, - value=True, - ) + func = getattr(SensiboDeviceSwitch, self.entity_description.command_on) + await func( + self, + key=self.entity_description.data_key, + value=True, + ) async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" - if self.entity_description.key == "timer_on_switch": - await self.async_turn_off_timer( - device_data=self.device_data, - key=self.entity_description.data_key, - value=False, - ) - if self.entity_description.key == "pure_boost_switch": - await self.async_turn_on_off_pure_boost( - device_data=self.device_data, - key=self.entity_description.data_key, - value=False, - ) + func = getattr(SensiboDeviceSwitch, self.entity_description.command_off) + await func( + self, + key=self.entity_description.data_key, + value=True, + ) @property def extra_state_attributes(self) -> Mapping[str, Any] | None: @@ -149,37 +137,31 @@ class SensiboDeviceSwitch(SensiboDeviceBaseEntity, SwitchEntity): return None @async_handle_api_call - async def async_turn_on_timer( - self, device_data: SensiboDevice, key: Any, value: Any - ) -> bool: + async def async_turn_on_timer(self, key: str, value: Any) -> bool: """Make service call to api for setting timer.""" result = {} - new_state = bool(device_data.ac_states["on"] is False) + new_state = bool(self.device_data.ac_states["on"] is False) data = { "minutesFromNow": 60, - "acState": {**device_data.ac_states, "on": new_state}, + "acState": {**self.device_data.ac_states, "on": new_state}, } result = await self._client.async_set_timer(self._device_id, data) return bool(result.get("status") == "success") @async_handle_api_call - async def async_turn_off_timer( - self, device_data: SensiboDevice, key: Any, value: Any - ) -> bool: + async def async_turn_off_timer(self, key: str, value: Any) -> bool: """Make service call to api for deleting timer.""" result = {} result = await self._client.async_del_timer(self._device_id) return bool(result.get("status") == "success") @async_handle_api_call - async def async_turn_on_off_pure_boost( - self, device_data: SensiboDevice, key: Any, value: Any - ) -> bool: + async def async_turn_on_off_pure_boost(self, key: str, value: Any) -> bool: """Make service call to api for setting Pure Boost.""" result = {} - new_state = bool(device_data.pure_boost_enabled is False) + new_state = bool(self.device_data.pure_boost_enabled is False) data: dict[str, Any] = {"enabled": new_state} - if device_data.pure_measure_integration is None: + if self.device_data.pure_measure_integration is None: data["sensitivity"] = "N" data["measurementsIntegration"] = True data["acIntegration"] = False From 8084d163d3431a37272bf09c42b4dcb501978c87 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 9 Sep 2022 12:56:21 -0500 Subject: [PATCH 241/955] Bump bluetooth-adapters to 0.3.6 (#78138) --- 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 3043d6412a4..cda4158086f 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -6,7 +6,7 @@ "quality_scale": "internal", "requirements": [ "bleak==0.16.0", - "bluetooth-adapters==0.3.5", + "bluetooth-adapters==0.3.6", "bluetooth-auto-recovery==0.3.2" ], "codeowners": ["@bdraco"], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 1a1fcb9da84..bf93aeba6e5 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ attrs==21.2.0 awesomeversion==22.8.0 bcrypt==3.1.7 bleak==0.16.0 -bluetooth-adapters==0.3.5 +bluetooth-adapters==0.3.6 bluetooth-auto-recovery==0.3.2 certifi>=2021.5.30 ciso8601==2.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 3e8ebabbdcd..e516fc66736 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -430,7 +430,7 @@ bluemaestro-ble==0.2.0 # bluepy==1.3.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.3.5 +bluetooth-adapters==0.3.6 # homeassistant.components.bluetooth bluetooth-auto-recovery==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d476445a4d6..c182079c026 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -341,7 +341,7 @@ blinkpy==0.19.0 bluemaestro-ble==0.2.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.3.5 +bluetooth-adapters==0.3.6 # homeassistant.components.bluetooth bluetooth-auto-recovery==0.3.2 From 19cf5dfc6d85912357e95508570d184652ea105e Mon Sep 17 00:00:00 2001 From: Michael Kowalchuk Date: Fri, 9 Sep 2022 13:06:01 -0700 Subject: [PATCH 242/955] Add zwave_js speed configuration for Leviton ZW4SF fans (#60677) * Add speed info for Leviton 4 speed fans * Use new format for fan speed configuration * Add a fixture and test for the Leviton ZW4SF * Use pytest.approx --- .../components/zwave_js/discovery.py | 4 + tests/components/zwave_js/conftest.py | 14 + .../fixtures/leviton_zw4sf_state.json | 9748 +++++++++++++++++ tests/components/zwave_js/test_fan.py | 79 +- 4 files changed, 9839 insertions(+), 6 deletions(-) create mode 100644 tests/components/zwave_js/fixtures/leviton_zw4sf_state.json diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index 565d8af4ab0..a30731b1ce8 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -289,10 +289,14 @@ DISCOVERY_SCHEMAS = [ # Leviton ZW4SF fan controllers using switch multilevel CC ZWaveDiscoverySchema( platform=Platform.FAN, + hint="has_fan_value_mapping", manufacturer_id={0x001D}, product_id={0x0002}, product_type={0x0038}, primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA, + data_template=FixedFanValueMappingDataTemplate( + FanValueMapping(speeds=[(1, 25), (26, 50), (51, 75), (76, 99)]), + ), ), # Inovelli LZW36 light / fan controller combo using switch multilevel CC # The fan is endpoint 2, the light is endpoint 1. diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 04a9c5671f9..6585deddbdb 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -404,6 +404,12 @@ def hs_fc200_state_fixture(): return json.loads(load_fixture("zwave_js/fan_hs_fc200_state.json")) +@pytest.fixture(name="leviton_zw4sf_state", scope="session") +def leviton_zw4sf_state_fixture(): + """Load the Leviton ZW4SF node state fixture data.""" + return json.loads(load_fixture("zwave_js/leviton_zw4sf_state.json")) + + @pytest.fixture(name="gdc_zw062_state", scope="session") def motorized_barrier_cover_state_fixture(): """Load the motorized barrier cover node state fixture data.""" @@ -874,6 +880,14 @@ def hs_fc200_fixture(client, hs_fc200_state): return node +@pytest.fixture(name="leviton_zw4sf") +def leviton_zw4sf_fixture(client, leviton_zw4sf_state): + """Mock a fan node.""" + node = Node(client, copy.deepcopy(leviton_zw4sf_state)) + client.driver.controller.nodes[node.node_id] = node + return node + + @pytest.fixture(name="null_name_check") def null_name_check_fixture(client, null_name_check_state): """Mock a node with no name.""" diff --git a/tests/components/zwave_js/fixtures/leviton_zw4sf_state.json b/tests/components/zwave_js/fixtures/leviton_zw4sf_state.json new file mode 100644 index 00000000000..28b59a0b844 --- /dev/null +++ b/tests/components/zwave_js/fixtures/leviton_zw4sf_state.json @@ -0,0 +1,9748 @@ +{ + "nodeId": 88, + "index": 0, + "installerIcon": 1024, + "userIcon": 1024, + "status": 4, + "ready": true, + "isListening": true, + "isRouting": true, + "isSecure": false, + "manufacturerId": 29, + "productId": 2, + "productType": 56, + "firmwareVersion": "1.8.1", + "zwavePlusVersion": 2, + "deviceConfig": { + "filename": "/cache/db/devices/0x001d/zw4sf.json", + "isEmbedded": true, + "manufacturer": "Leviton", + "manufacturerId": 29, + "label": "ZW4SF", + "description": "4 Speed Fan Controller", + "devices": [ + { + "productType": 56, + "productId": 2 + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + }, + "paramInformation": { + "_map": {} + }, + "metadata": { + "inclusion": "Classic Inclusion To A Z-Wave Network\nFor older controllers Classic Inclusion is supported. Depending on the age of the controller the controller will need to be 3 to 35 feet from the device when including.\n1. To enter programming mode, hold the button for 7 seconds. The status light will turn amber, release and the status light will blink.\n2. Follow the Z-Wave controller instructions to enter inclusion mode.\n3. Tap the top or the paddle of the paddle one time. The status light will quickly flash green.\n4. The Z-Wave controller will confirm successful inclusion to the network", + "exclusion": "Exclusion From A Z-Wave Network\nWhen removing an fan speed controller from a Z-Wave network,\nbest practice is to use the exclusion command found in the Z-Wave\ncontroller.\n1. To enter programming mode, hold the button for 7 seconds. The\nstatus light will turn amber, release and the status light will blink.\n2. Follow Z-Wave controller directions to enter exclusion mode\n3. Tap the the top of the paddle 1 time. The status light will quickly\nflash green.\n4. The Z-Wave controller will remove the device from the network", + "reset": "Factory Default\nWhen removing a fan speed controller from a network it is best\npractice to use the exclusion process. In situations where a device\nneeds to be returned to factory default follow the following steps. A\nreset should only be used when a controller is\ninoperable or missing.\n1. Hold the top of the paddle for 7 seconds, the status light will turn amber.\nContinue holding the top paddle for another 7 seconds (total of 14 seconds).\nThe status light will quickly flash red/ amber.\n2. Release the top of the paddle and the device will reset", + "manual": "https://products.z-wavealliance.org/ProductManual/File?folder=&filename=product_documents/3832/Draft%20ZW4SF%203-25-20.pdf" + } + }, + "label": "ZW4SF", + "interviewAttempts": 0, + "endpoints": [ + { + "nodeId": 88, + "index": 0, + "installerIcon": 1024, + "userIcon": 1024, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 17, + "label": "Multilevel Switch" + }, + "specific": { + "key": 0, + "label": "Unused" + }, + "mandatorySupportedCCs": [32, 38], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 38, + "name": "Multilevel Switch", + "version": 4, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 3, + "isSecure": false + }, + { + "id": 85, + "name": "Transport Service", + "version": 2, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 3, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 135, + "name": "Indicator", + "version": 3, + "isSecure": false + }, + { + "id": 115, + "name": "Powerlevel", + "version": 1, + "isSecure": false + }, + { + "id": 152, + "name": "Security", + "version": 1, + "isSecure": true + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 5, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 4, + "isSecure": false + }, + { + "id": 43, + "name": "Scene Activation", + "version": 1, + "isSecure": false + }, + { + "id": 44, + "name": "Scene Actuator Configuration", + "version": 1, + "isSecure": false + }, + { + "id": 159, + "name": "Security 2", + "version": 1, + "isSecure": true + } + ] + } + ], + "values": [ + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 99 + }, + "value": 50 + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "duration", + "propertyName": "duration", + "ccVersion": 4, + "metadata": { + "type": "duration", + "readable": true, + "writeable": false, + "label": "Remaining duration" + }, + "value": { + "value": 0, + "unit": "seconds" + } + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current value", + "min": 0, + "max": 99 + }, + "value": 50 + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "Up", + "propertyName": "Up", + "ccVersion": 4, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Perform a level change (Up)", + "ccSpecific": { + "switchType": 2 + }, + "valueChangeOptions": ["transitionDuration"] + } + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "Down", + "propertyName": "Down", + "ccVersion": 4, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Perform a level change (Down)", + "ccSpecific": { + "switchType": 2 + }, + "valueChangeOptions": ["transitionDuration"] + } + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "restorePrevious", + "propertyName": "restorePrevious", + "ccVersion": 4, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Restore previous value" + } + }, + { + "endpoint": 0, + "commandClass": 43, + "commandClassName": "Scene Activation", + "property": "sceneId", + "propertyName": "sceneId", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Scene ID", + "valueChangeOptions": ["transitionDuration"], + "min": 1, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 43, + "commandClassName": "Scene Activation", + "property": "dimmingDuration", + "propertyName": "dimmingDuration", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 1, + "propertyName": "level", + "propertyKeyName": "1", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (1)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 1, + "propertyName": "dimmingDuration", + "propertyKeyName": "1", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (1)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 2, + "propertyName": "level", + "propertyKeyName": "2", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (2)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 2, + "propertyName": "dimmingDuration", + "propertyKeyName": "2", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (2)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 3, + "propertyName": "level", + "propertyKeyName": "3", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (3)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 3, + "propertyName": "dimmingDuration", + "propertyKeyName": "3", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (3)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 4, + "propertyName": "level", + "propertyKeyName": "4", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (4)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 4, + "propertyName": "dimmingDuration", + "propertyKeyName": "4", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (4)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 5, + "propertyName": "level", + "propertyKeyName": "5", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (5)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 5, + "propertyName": "dimmingDuration", + "propertyKeyName": "5", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (5)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 6, + "propertyName": "level", + "propertyKeyName": "6", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (6)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 6, + "propertyName": "dimmingDuration", + "propertyKeyName": "6", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (6)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 7, + "propertyName": "level", + "propertyKeyName": "7", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (7)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 7, + "propertyName": "dimmingDuration", + "propertyKeyName": "7", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (7)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 8, + "propertyName": "level", + "propertyKeyName": "8", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (8)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 8, + "propertyName": "dimmingDuration", + "propertyKeyName": "8", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (8)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 9, + "propertyName": "level", + "propertyKeyName": "9", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (9)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 9, + "propertyName": "dimmingDuration", + "propertyKeyName": "9", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (9)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 10, + "propertyName": "level", + "propertyKeyName": "10", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (10)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 10, + "propertyName": "dimmingDuration", + "propertyKeyName": "10", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (10)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 11, + "propertyName": "level", + "propertyKeyName": "11", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (11)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 11, + "propertyName": "dimmingDuration", + "propertyKeyName": "11", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (11)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 12, + "propertyName": "level", + "propertyKeyName": "12", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (12)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 12, + "propertyName": "dimmingDuration", + "propertyKeyName": "12", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (12)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 13, + "propertyName": "level", + "propertyKeyName": "13", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (13)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 13, + "propertyName": "dimmingDuration", + "propertyKeyName": "13", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (13)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 14, + "propertyName": "level", + "propertyKeyName": "14", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (14)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 14, + "propertyName": "dimmingDuration", + "propertyKeyName": "14", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (14)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 15, + "propertyName": "level", + "propertyKeyName": "15", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (15)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 15, + "propertyName": "dimmingDuration", + "propertyKeyName": "15", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (15)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 16, + "propertyName": "level", + "propertyKeyName": "16", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (16)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 16, + "propertyName": "dimmingDuration", + "propertyKeyName": "16", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (16)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 17, + "propertyName": "level", + "propertyKeyName": "17", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (17)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 17, + "propertyName": "dimmingDuration", + "propertyKeyName": "17", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (17)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 18, + "propertyName": "level", + "propertyKeyName": "18", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (18)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 18, + "propertyName": "dimmingDuration", + "propertyKeyName": "18", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (18)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 19, + "propertyName": "level", + "propertyKeyName": "19", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (19)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 19, + "propertyName": "dimmingDuration", + "propertyKeyName": "19", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (19)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 20, + "propertyName": "level", + "propertyKeyName": "20", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (20)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 20, + "propertyName": "dimmingDuration", + "propertyKeyName": "20", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (20)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 21, + "propertyName": "level", + "propertyKeyName": "21", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (21)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 21, + "propertyName": "dimmingDuration", + "propertyKeyName": "21", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (21)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 22, + "propertyName": "level", + "propertyKeyName": "22", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (22)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 22, + "propertyName": "dimmingDuration", + "propertyKeyName": "22", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (22)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 23, + "propertyName": "level", + "propertyKeyName": "23", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (23)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 23, + "propertyName": "dimmingDuration", + "propertyKeyName": "23", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (23)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 24, + "propertyName": "level", + "propertyKeyName": "24", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (24)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 24, + "propertyName": "dimmingDuration", + "propertyKeyName": "24", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (24)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 25, + "propertyName": "level", + "propertyKeyName": "25", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (25)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 25, + "propertyName": "dimmingDuration", + "propertyKeyName": "25", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (25)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 26, + "propertyName": "level", + "propertyKeyName": "26", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (26)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 26, + "propertyName": "dimmingDuration", + "propertyKeyName": "26", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (26)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 27, + "propertyName": "level", + "propertyKeyName": "27", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (27)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 27, + "propertyName": "dimmingDuration", + "propertyKeyName": "27", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (27)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 28, + "propertyName": "level", + "propertyKeyName": "28", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (28)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 28, + "propertyName": "dimmingDuration", + "propertyKeyName": "28", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (28)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 29, + "propertyName": "level", + "propertyKeyName": "29", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (29)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 29, + "propertyName": "dimmingDuration", + "propertyKeyName": "29", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (29)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 30, + "propertyName": "level", + "propertyKeyName": "30", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (30)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 30, + "propertyName": "dimmingDuration", + "propertyKeyName": "30", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (30)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 31, + "propertyName": "level", + "propertyKeyName": "31", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (31)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 31, + "propertyName": "dimmingDuration", + "propertyKeyName": "31", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (31)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 32, + "propertyName": "level", + "propertyKeyName": "32", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (32)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 32, + "propertyName": "dimmingDuration", + "propertyKeyName": "32", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (32)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 33, + "propertyName": "level", + "propertyKeyName": "33", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (33)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 33, + "propertyName": "dimmingDuration", + "propertyKeyName": "33", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (33)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 34, + "propertyName": "level", + "propertyKeyName": "34", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (34)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 34, + "propertyName": "dimmingDuration", + "propertyKeyName": "34", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (34)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 35, + "propertyName": "level", + "propertyKeyName": "35", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (35)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 35, + "propertyName": "dimmingDuration", + "propertyKeyName": "35", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (35)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 36, + "propertyName": "level", + "propertyKeyName": "36", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (36)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 36, + "propertyName": "dimmingDuration", + "propertyKeyName": "36", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (36)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 37, + "propertyName": "level", + "propertyKeyName": "37", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (37)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 37, + "propertyName": "dimmingDuration", + "propertyKeyName": "37", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (37)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 38, + "propertyName": "level", + "propertyKeyName": "38", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (38)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 38, + "propertyName": "dimmingDuration", + "propertyKeyName": "38", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (38)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 39, + "propertyName": "level", + "propertyKeyName": "39", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (39)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 39, + "propertyName": "dimmingDuration", + "propertyKeyName": "39", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (39)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 40, + "propertyName": "level", + "propertyKeyName": "40", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (40)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 40, + "propertyName": "dimmingDuration", + "propertyKeyName": "40", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (40)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 41, + "propertyName": "level", + "propertyKeyName": "41", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (41)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 41, + "propertyName": "dimmingDuration", + "propertyKeyName": "41", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (41)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 42, + "propertyName": "level", + "propertyKeyName": "42", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (42)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 42, + "propertyName": "dimmingDuration", + "propertyKeyName": "42", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (42)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 43, + "propertyName": "level", + "propertyKeyName": "43", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (43)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 43, + "propertyName": "dimmingDuration", + "propertyKeyName": "43", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (43)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 44, + "propertyName": "level", + "propertyKeyName": "44", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (44)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 44, + "propertyName": "dimmingDuration", + "propertyKeyName": "44", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (44)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 45, + "propertyName": "level", + "propertyKeyName": "45", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (45)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 45, + "propertyName": "dimmingDuration", + "propertyKeyName": "45", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (45)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 46, + "propertyName": "level", + "propertyKeyName": "46", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (46)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 46, + "propertyName": "dimmingDuration", + "propertyKeyName": "46", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (46)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 47, + "propertyName": "level", + "propertyKeyName": "47", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (47)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 47, + "propertyName": "dimmingDuration", + "propertyKeyName": "47", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (47)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 48, + "propertyName": "level", + "propertyKeyName": "48", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (48)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 48, + "propertyName": "dimmingDuration", + "propertyKeyName": "48", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (48)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 49, + "propertyName": "level", + "propertyKeyName": "49", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (49)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 49, + "propertyName": "dimmingDuration", + "propertyKeyName": "49", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (49)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 50, + "propertyName": "level", + "propertyKeyName": "50", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (50)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 50, + "propertyName": "dimmingDuration", + "propertyKeyName": "50", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (50)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 51, + "propertyName": "level", + "propertyKeyName": "51", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (51)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 51, + "propertyName": "dimmingDuration", + "propertyKeyName": "51", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (51)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 52, + "propertyName": "level", + "propertyKeyName": "52", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (52)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 52, + "propertyName": "dimmingDuration", + "propertyKeyName": "52", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (52)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 53, + "propertyName": "level", + "propertyKeyName": "53", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (53)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 53, + "propertyName": "dimmingDuration", + "propertyKeyName": "53", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (53)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 54, + "propertyName": "level", + "propertyKeyName": "54", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (54)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 54, + "propertyName": "dimmingDuration", + "propertyKeyName": "54", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (54)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 55, + "propertyName": "level", + "propertyKeyName": "55", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (55)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 55, + "propertyName": "dimmingDuration", + "propertyKeyName": "55", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (55)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 56, + "propertyName": "level", + "propertyKeyName": "56", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (56)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 56, + "propertyName": "dimmingDuration", + "propertyKeyName": "56", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (56)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 57, + "propertyName": "level", + "propertyKeyName": "57", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (57)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 57, + "propertyName": "dimmingDuration", + "propertyKeyName": "57", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (57)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 58, + "propertyName": "level", + "propertyKeyName": "58", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (58)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 58, + "propertyName": "dimmingDuration", + "propertyKeyName": "58", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (58)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 59, + "propertyName": "level", + "propertyKeyName": "59", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (59)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 59, + "propertyName": "dimmingDuration", + "propertyKeyName": "59", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (59)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 60, + "propertyName": "level", + "propertyKeyName": "60", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (60)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 60, + "propertyName": "dimmingDuration", + "propertyKeyName": "60", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (60)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 61, + "propertyName": "level", + "propertyKeyName": "61", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (61)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 61, + "propertyName": "dimmingDuration", + "propertyKeyName": "61", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (61)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 62, + "propertyName": "level", + "propertyKeyName": "62", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (62)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 62, + "propertyName": "dimmingDuration", + "propertyKeyName": "62", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (62)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 63, + "propertyName": "level", + "propertyKeyName": "63", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (63)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 63, + "propertyName": "dimmingDuration", + "propertyKeyName": "63", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (63)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 64, + "propertyName": "level", + "propertyKeyName": "64", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (64)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 64, + "propertyName": "dimmingDuration", + "propertyKeyName": "64", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (64)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 65, + "propertyName": "level", + "propertyKeyName": "65", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (65)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 65, + "propertyName": "dimmingDuration", + "propertyKeyName": "65", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (65)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 66, + "propertyName": "level", + "propertyKeyName": "66", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (66)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 66, + "propertyName": "dimmingDuration", + "propertyKeyName": "66", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (66)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 67, + "propertyName": "level", + "propertyKeyName": "67", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (67)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 67, + "propertyName": "dimmingDuration", + "propertyKeyName": "67", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (67)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 68, + "propertyName": "level", + "propertyKeyName": "68", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (68)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 68, + "propertyName": "dimmingDuration", + "propertyKeyName": "68", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (68)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 69, + "propertyName": "level", + "propertyKeyName": "69", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (69)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 69, + "propertyName": "dimmingDuration", + "propertyKeyName": "69", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (69)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 70, + "propertyName": "level", + "propertyKeyName": "70", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (70)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 70, + "propertyName": "dimmingDuration", + "propertyKeyName": "70", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (70)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 71, + "propertyName": "level", + "propertyKeyName": "71", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (71)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 71, + "propertyName": "dimmingDuration", + "propertyKeyName": "71", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (71)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 72, + "propertyName": "level", + "propertyKeyName": "72", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (72)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 72, + "propertyName": "dimmingDuration", + "propertyKeyName": "72", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (72)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 73, + "propertyName": "level", + "propertyKeyName": "73", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (73)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 73, + "propertyName": "dimmingDuration", + "propertyKeyName": "73", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (73)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 74, + "propertyName": "level", + "propertyKeyName": "74", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (74)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 74, + "propertyName": "dimmingDuration", + "propertyKeyName": "74", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (74)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 75, + "propertyName": "level", + "propertyKeyName": "75", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (75)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 75, + "propertyName": "dimmingDuration", + "propertyKeyName": "75", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (75)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 76, + "propertyName": "level", + "propertyKeyName": "76", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (76)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 76, + "propertyName": "dimmingDuration", + "propertyKeyName": "76", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (76)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 77, + "propertyName": "level", + "propertyKeyName": "77", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (77)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 77, + "propertyName": "dimmingDuration", + "propertyKeyName": "77", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (77)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 78, + "propertyName": "level", + "propertyKeyName": "78", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (78)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 78, + "propertyName": "dimmingDuration", + "propertyKeyName": "78", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (78)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 79, + "propertyName": "level", + "propertyKeyName": "79", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (79)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 79, + "propertyName": "dimmingDuration", + "propertyKeyName": "79", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (79)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 80, + "propertyName": "level", + "propertyKeyName": "80", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (80)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 80, + "propertyName": "dimmingDuration", + "propertyKeyName": "80", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (80)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 81, + "propertyName": "level", + "propertyKeyName": "81", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (81)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 81, + "propertyName": "dimmingDuration", + "propertyKeyName": "81", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (81)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 82, + "propertyName": "level", + "propertyKeyName": "82", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (82)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 82, + "propertyName": "dimmingDuration", + "propertyKeyName": "82", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (82)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 83, + "propertyName": "level", + "propertyKeyName": "83", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (83)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 83, + "propertyName": "dimmingDuration", + "propertyKeyName": "83", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (83)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 84, + "propertyName": "level", + "propertyKeyName": "84", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (84)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 84, + "propertyName": "dimmingDuration", + "propertyKeyName": "84", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (84)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 85, + "propertyName": "level", + "propertyKeyName": "85", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (85)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 85, + "propertyName": "dimmingDuration", + "propertyKeyName": "85", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (85)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 86, + "propertyName": "level", + "propertyKeyName": "86", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (86)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 86, + "propertyName": "dimmingDuration", + "propertyKeyName": "86", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (86)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 87, + "propertyName": "level", + "propertyKeyName": "87", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (87)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 87, + "propertyName": "dimmingDuration", + "propertyKeyName": "87", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (87)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 88, + "propertyName": "level", + "propertyKeyName": "88", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (88)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 88, + "propertyName": "dimmingDuration", + "propertyKeyName": "88", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (88)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 89, + "propertyName": "level", + "propertyKeyName": "89", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (89)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 89, + "propertyName": "dimmingDuration", + "propertyKeyName": "89", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (89)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 90, + "propertyName": "level", + "propertyKeyName": "90", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (90)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 90, + "propertyName": "dimmingDuration", + "propertyKeyName": "90", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (90)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 91, + "propertyName": "level", + "propertyKeyName": "91", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (91)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 91, + "propertyName": "dimmingDuration", + "propertyKeyName": "91", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (91)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 92, + "propertyName": "level", + "propertyKeyName": "92", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (92)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 92, + "propertyName": "dimmingDuration", + "propertyKeyName": "92", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (92)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 93, + "propertyName": "level", + "propertyKeyName": "93", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (93)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 93, + "propertyName": "dimmingDuration", + "propertyKeyName": "93", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (93)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 94, + "propertyName": "level", + "propertyKeyName": "94", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (94)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 94, + "propertyName": "dimmingDuration", + "propertyKeyName": "94", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (94)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 95, + "propertyName": "level", + "propertyKeyName": "95", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (95)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 95, + "propertyName": "dimmingDuration", + "propertyKeyName": "95", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (95)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 96, + "propertyName": "level", + "propertyKeyName": "96", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (96)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 96, + "propertyName": "dimmingDuration", + "propertyKeyName": "96", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (96)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 97, + "propertyName": "level", + "propertyKeyName": "97", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (97)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 97, + "propertyName": "dimmingDuration", + "propertyKeyName": "97", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (97)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 98, + "propertyName": "level", + "propertyKeyName": "98", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (98)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 98, + "propertyName": "dimmingDuration", + "propertyKeyName": "98", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (98)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 99, + "propertyName": "level", + "propertyKeyName": "99", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (99)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 99, + "propertyName": "dimmingDuration", + "propertyKeyName": "99", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (99)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 100, + "propertyName": "level", + "propertyKeyName": "100", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (100)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 100, + "propertyName": "dimmingDuration", + "propertyKeyName": "100", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (100)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 101, + "propertyName": "level", + "propertyKeyName": "101", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (101)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 101, + "propertyName": "dimmingDuration", + "propertyKeyName": "101", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (101)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 102, + "propertyName": "level", + "propertyKeyName": "102", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (102)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 102, + "propertyName": "dimmingDuration", + "propertyKeyName": "102", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (102)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 103, + "propertyName": "level", + "propertyKeyName": "103", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (103)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 103, + "propertyName": "dimmingDuration", + "propertyKeyName": "103", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (103)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 104, + "propertyName": "level", + "propertyKeyName": "104", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (104)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 104, + "propertyName": "dimmingDuration", + "propertyKeyName": "104", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (104)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 105, + "propertyName": "level", + "propertyKeyName": "105", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (105)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 105, + "propertyName": "dimmingDuration", + "propertyKeyName": "105", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (105)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 106, + "propertyName": "level", + "propertyKeyName": "106", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (106)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 106, + "propertyName": "dimmingDuration", + "propertyKeyName": "106", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (106)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 107, + "propertyName": "level", + "propertyKeyName": "107", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (107)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 107, + "propertyName": "dimmingDuration", + "propertyKeyName": "107", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (107)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 108, + "propertyName": "level", + "propertyKeyName": "108", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (108)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 108, + "propertyName": "dimmingDuration", + "propertyKeyName": "108", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (108)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 109, + "propertyName": "level", + "propertyKeyName": "109", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (109)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 109, + "propertyName": "dimmingDuration", + "propertyKeyName": "109", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (109)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 110, + "propertyName": "level", + "propertyKeyName": "110", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (110)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 110, + "propertyName": "dimmingDuration", + "propertyKeyName": "110", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (110)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 111, + "propertyName": "level", + "propertyKeyName": "111", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (111)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 111, + "propertyName": "dimmingDuration", + "propertyKeyName": "111", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (111)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 112, + "propertyName": "level", + "propertyKeyName": "112", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (112)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 112, + "propertyName": "dimmingDuration", + "propertyKeyName": "112", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (112)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 113, + "propertyName": "level", + "propertyKeyName": "113", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (113)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 113, + "propertyName": "dimmingDuration", + "propertyKeyName": "113", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (113)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 114, + "propertyName": "level", + "propertyKeyName": "114", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (114)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 114, + "propertyName": "dimmingDuration", + "propertyKeyName": "114", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (114)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 115, + "propertyName": "level", + "propertyKeyName": "115", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (115)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 115, + "propertyName": "dimmingDuration", + "propertyKeyName": "115", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (115)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 116, + "propertyName": "level", + "propertyKeyName": "116", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (116)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 116, + "propertyName": "dimmingDuration", + "propertyKeyName": "116", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (116)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 117, + "propertyName": "level", + "propertyKeyName": "117", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (117)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 117, + "propertyName": "dimmingDuration", + "propertyKeyName": "117", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (117)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 118, + "propertyName": "level", + "propertyKeyName": "118", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (118)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 118, + "propertyName": "dimmingDuration", + "propertyKeyName": "118", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (118)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 119, + "propertyName": "level", + "propertyKeyName": "119", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (119)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 119, + "propertyName": "dimmingDuration", + "propertyKeyName": "119", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (119)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 120, + "propertyName": "level", + "propertyKeyName": "120", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (120)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 120, + "propertyName": "dimmingDuration", + "propertyKeyName": "120", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (120)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 121, + "propertyName": "level", + "propertyKeyName": "121", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (121)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 121, + "propertyName": "dimmingDuration", + "propertyKeyName": "121", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (121)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 122, + "propertyName": "level", + "propertyKeyName": "122", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (122)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 122, + "propertyName": "dimmingDuration", + "propertyKeyName": "122", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (122)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 123, + "propertyName": "level", + "propertyKeyName": "123", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (123)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 123, + "propertyName": "dimmingDuration", + "propertyKeyName": "123", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (123)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 124, + "propertyName": "level", + "propertyKeyName": "124", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (124)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 124, + "propertyName": "dimmingDuration", + "propertyKeyName": "124", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (124)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 125, + "propertyName": "level", + "propertyKeyName": "125", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (125)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 125, + "propertyName": "dimmingDuration", + "propertyKeyName": "125", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (125)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 126, + "propertyName": "level", + "propertyKeyName": "126", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (126)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 126, + "propertyName": "dimmingDuration", + "propertyKeyName": "126", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (126)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 127, + "propertyName": "level", + "propertyKeyName": "127", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (127)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 127, + "propertyName": "dimmingDuration", + "propertyKeyName": "127", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (127)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 128, + "propertyName": "level", + "propertyKeyName": "128", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (128)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 128, + "propertyName": "dimmingDuration", + "propertyKeyName": "128", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (128)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 129, + "propertyName": "level", + "propertyKeyName": "129", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (129)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 129, + "propertyName": "dimmingDuration", + "propertyKeyName": "129", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (129)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 130, + "propertyName": "level", + "propertyKeyName": "130", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (130)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 130, + "propertyName": "dimmingDuration", + "propertyKeyName": "130", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (130)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 131, + "propertyName": "level", + "propertyKeyName": "131", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (131)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 131, + "propertyName": "dimmingDuration", + "propertyKeyName": "131", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (131)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 132, + "propertyName": "level", + "propertyKeyName": "132", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (132)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 132, + "propertyName": "dimmingDuration", + "propertyKeyName": "132", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (132)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 133, + "propertyName": "level", + "propertyKeyName": "133", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (133)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 133, + "propertyName": "dimmingDuration", + "propertyKeyName": "133", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (133)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 134, + "propertyName": "level", + "propertyKeyName": "134", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (134)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 134, + "propertyName": "dimmingDuration", + "propertyKeyName": "134", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (134)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 135, + "propertyName": "level", + "propertyKeyName": "135", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (135)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 135, + "propertyName": "dimmingDuration", + "propertyKeyName": "135", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (135)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 136, + "propertyName": "level", + "propertyKeyName": "136", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (136)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 136, + "propertyName": "dimmingDuration", + "propertyKeyName": "136", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (136)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 137, + "propertyName": "level", + "propertyKeyName": "137", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (137)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 137, + "propertyName": "dimmingDuration", + "propertyKeyName": "137", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (137)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 138, + "propertyName": "level", + "propertyKeyName": "138", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (138)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 138, + "propertyName": "dimmingDuration", + "propertyKeyName": "138", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (138)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 139, + "propertyName": "level", + "propertyKeyName": "139", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (139)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 139, + "propertyName": "dimmingDuration", + "propertyKeyName": "139", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (139)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 140, + "propertyName": "level", + "propertyKeyName": "140", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (140)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 140, + "propertyName": "dimmingDuration", + "propertyKeyName": "140", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (140)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 141, + "propertyName": "level", + "propertyKeyName": "141", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (141)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 141, + "propertyName": "dimmingDuration", + "propertyKeyName": "141", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (141)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 142, + "propertyName": "level", + "propertyKeyName": "142", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (142)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 142, + "propertyName": "dimmingDuration", + "propertyKeyName": "142", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (142)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 143, + "propertyName": "level", + "propertyKeyName": "143", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (143)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 143, + "propertyName": "dimmingDuration", + "propertyKeyName": "143", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (143)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 144, + "propertyName": "level", + "propertyKeyName": "144", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (144)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 144, + "propertyName": "dimmingDuration", + "propertyKeyName": "144", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (144)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 145, + "propertyName": "level", + "propertyKeyName": "145", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (145)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 145, + "propertyName": "dimmingDuration", + "propertyKeyName": "145", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (145)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 146, + "propertyName": "level", + "propertyKeyName": "146", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (146)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 146, + "propertyName": "dimmingDuration", + "propertyKeyName": "146", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (146)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 147, + "propertyName": "level", + "propertyKeyName": "147", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (147)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 147, + "propertyName": "dimmingDuration", + "propertyKeyName": "147", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (147)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 148, + "propertyName": "level", + "propertyKeyName": "148", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (148)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 148, + "propertyName": "dimmingDuration", + "propertyKeyName": "148", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (148)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 149, + "propertyName": "level", + "propertyKeyName": "149", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (149)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 149, + "propertyName": "dimmingDuration", + "propertyKeyName": "149", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (149)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 150, + "propertyName": "level", + "propertyKeyName": "150", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (150)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 150, + "propertyName": "dimmingDuration", + "propertyKeyName": "150", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (150)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 151, + "propertyName": "level", + "propertyKeyName": "151", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (151)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 151, + "propertyName": "dimmingDuration", + "propertyKeyName": "151", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (151)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 152, + "propertyName": "level", + "propertyKeyName": "152", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (152)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 152, + "propertyName": "dimmingDuration", + "propertyKeyName": "152", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (152)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 153, + "propertyName": "level", + "propertyKeyName": "153", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (153)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 153, + "propertyName": "dimmingDuration", + "propertyKeyName": "153", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (153)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 154, + "propertyName": "level", + "propertyKeyName": "154", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (154)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 154, + "propertyName": "dimmingDuration", + "propertyKeyName": "154", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (154)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 155, + "propertyName": "level", + "propertyKeyName": "155", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (155)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 155, + "propertyName": "dimmingDuration", + "propertyKeyName": "155", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (155)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 156, + "propertyName": "level", + "propertyKeyName": "156", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (156)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 156, + "propertyName": "dimmingDuration", + "propertyKeyName": "156", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (156)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 157, + "propertyName": "level", + "propertyKeyName": "157", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (157)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 157, + "propertyName": "dimmingDuration", + "propertyKeyName": "157", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (157)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 158, + "propertyName": "level", + "propertyKeyName": "158", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (158)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 158, + "propertyName": "dimmingDuration", + "propertyKeyName": "158", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (158)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 159, + "propertyName": "level", + "propertyKeyName": "159", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (159)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 159, + "propertyName": "dimmingDuration", + "propertyKeyName": "159", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (159)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 160, + "propertyName": "level", + "propertyKeyName": "160", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (160)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 160, + "propertyName": "dimmingDuration", + "propertyKeyName": "160", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (160)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 161, + "propertyName": "level", + "propertyKeyName": "161", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (161)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 161, + "propertyName": "dimmingDuration", + "propertyKeyName": "161", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (161)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 162, + "propertyName": "level", + "propertyKeyName": "162", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (162)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 162, + "propertyName": "dimmingDuration", + "propertyKeyName": "162", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (162)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 163, + "propertyName": "level", + "propertyKeyName": "163", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (163)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 163, + "propertyName": "dimmingDuration", + "propertyKeyName": "163", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (163)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 164, + "propertyName": "level", + "propertyKeyName": "164", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (164)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 164, + "propertyName": "dimmingDuration", + "propertyKeyName": "164", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (164)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 165, + "propertyName": "level", + "propertyKeyName": "165", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (165)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 165, + "propertyName": "dimmingDuration", + "propertyKeyName": "165", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (165)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 166, + "propertyName": "level", + "propertyKeyName": "166", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (166)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 166, + "propertyName": "dimmingDuration", + "propertyKeyName": "166", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (166)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 167, + "propertyName": "level", + "propertyKeyName": "167", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (167)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 167, + "propertyName": "dimmingDuration", + "propertyKeyName": "167", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (167)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 168, + "propertyName": "level", + "propertyKeyName": "168", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (168)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 168, + "propertyName": "dimmingDuration", + "propertyKeyName": "168", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (168)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 169, + "propertyName": "level", + "propertyKeyName": "169", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (169)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 169, + "propertyName": "dimmingDuration", + "propertyKeyName": "169", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (169)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 170, + "propertyName": "level", + "propertyKeyName": "170", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (170)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 170, + "propertyName": "dimmingDuration", + "propertyKeyName": "170", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (170)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 171, + "propertyName": "level", + "propertyKeyName": "171", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (171)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 171, + "propertyName": "dimmingDuration", + "propertyKeyName": "171", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (171)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 172, + "propertyName": "level", + "propertyKeyName": "172", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (172)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 172, + "propertyName": "dimmingDuration", + "propertyKeyName": "172", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (172)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 173, + "propertyName": "level", + "propertyKeyName": "173", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (173)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 173, + "propertyName": "dimmingDuration", + "propertyKeyName": "173", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (173)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 174, + "propertyName": "level", + "propertyKeyName": "174", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (174)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 174, + "propertyName": "dimmingDuration", + "propertyKeyName": "174", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (174)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 175, + "propertyName": "level", + "propertyKeyName": "175", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (175)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 175, + "propertyName": "dimmingDuration", + "propertyKeyName": "175", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (175)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 176, + "propertyName": "level", + "propertyKeyName": "176", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (176)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 176, + "propertyName": "dimmingDuration", + "propertyKeyName": "176", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (176)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 177, + "propertyName": "level", + "propertyKeyName": "177", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (177)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 177, + "propertyName": "dimmingDuration", + "propertyKeyName": "177", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (177)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 178, + "propertyName": "level", + "propertyKeyName": "178", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (178)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 178, + "propertyName": "dimmingDuration", + "propertyKeyName": "178", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (178)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 179, + "propertyName": "level", + "propertyKeyName": "179", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (179)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 179, + "propertyName": "dimmingDuration", + "propertyKeyName": "179", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (179)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 180, + "propertyName": "level", + "propertyKeyName": "180", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (180)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 180, + "propertyName": "dimmingDuration", + "propertyKeyName": "180", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (180)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 181, + "propertyName": "level", + "propertyKeyName": "181", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (181)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 181, + "propertyName": "dimmingDuration", + "propertyKeyName": "181", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (181)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 182, + "propertyName": "level", + "propertyKeyName": "182", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (182)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 182, + "propertyName": "dimmingDuration", + "propertyKeyName": "182", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (182)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 183, + "propertyName": "level", + "propertyKeyName": "183", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (183)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 183, + "propertyName": "dimmingDuration", + "propertyKeyName": "183", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (183)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 184, + "propertyName": "level", + "propertyKeyName": "184", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (184)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 184, + "propertyName": "dimmingDuration", + "propertyKeyName": "184", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (184)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 185, + "propertyName": "level", + "propertyKeyName": "185", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (185)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 185, + "propertyName": "dimmingDuration", + "propertyKeyName": "185", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (185)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 186, + "propertyName": "level", + "propertyKeyName": "186", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (186)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 186, + "propertyName": "dimmingDuration", + "propertyKeyName": "186", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (186)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 187, + "propertyName": "level", + "propertyKeyName": "187", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (187)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 187, + "propertyName": "dimmingDuration", + "propertyKeyName": "187", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (187)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 188, + "propertyName": "level", + "propertyKeyName": "188", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (188)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 188, + "propertyName": "dimmingDuration", + "propertyKeyName": "188", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (188)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 189, + "propertyName": "level", + "propertyKeyName": "189", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (189)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 189, + "propertyName": "dimmingDuration", + "propertyKeyName": "189", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (189)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 190, + "propertyName": "level", + "propertyKeyName": "190", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (190)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 190, + "propertyName": "dimmingDuration", + "propertyKeyName": "190", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (190)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 191, + "propertyName": "level", + "propertyKeyName": "191", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (191)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 191, + "propertyName": "dimmingDuration", + "propertyKeyName": "191", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (191)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 192, + "propertyName": "level", + "propertyKeyName": "192", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (192)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 192, + "propertyName": "dimmingDuration", + "propertyKeyName": "192", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (192)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 193, + "propertyName": "level", + "propertyKeyName": "193", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (193)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 193, + "propertyName": "dimmingDuration", + "propertyKeyName": "193", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (193)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 194, + "propertyName": "level", + "propertyKeyName": "194", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (194)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 194, + "propertyName": "dimmingDuration", + "propertyKeyName": "194", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (194)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 195, + "propertyName": "level", + "propertyKeyName": "195", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (195)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 195, + "propertyName": "dimmingDuration", + "propertyKeyName": "195", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (195)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 196, + "propertyName": "level", + "propertyKeyName": "196", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (196)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 196, + "propertyName": "dimmingDuration", + "propertyKeyName": "196", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (196)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 197, + "propertyName": "level", + "propertyKeyName": "197", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (197)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 197, + "propertyName": "dimmingDuration", + "propertyKeyName": "197", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (197)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 198, + "propertyName": "level", + "propertyKeyName": "198", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (198)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 198, + "propertyName": "dimmingDuration", + "propertyKeyName": "198", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (198)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 199, + "propertyName": "level", + "propertyKeyName": "199", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (199)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 199, + "propertyName": "dimmingDuration", + "propertyKeyName": "199", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (199)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 200, + "propertyName": "level", + "propertyKeyName": "200", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (200)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 200, + "propertyName": "dimmingDuration", + "propertyKeyName": "200", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (200)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 201, + "propertyName": "level", + "propertyKeyName": "201", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (201)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 201, + "propertyName": "dimmingDuration", + "propertyKeyName": "201", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (201)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 202, + "propertyName": "level", + "propertyKeyName": "202", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (202)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 202, + "propertyName": "dimmingDuration", + "propertyKeyName": "202", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (202)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 203, + "propertyName": "level", + "propertyKeyName": "203", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (203)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 203, + "propertyName": "dimmingDuration", + "propertyKeyName": "203", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (203)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 204, + "propertyName": "level", + "propertyKeyName": "204", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (204)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 204, + "propertyName": "dimmingDuration", + "propertyKeyName": "204", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (204)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 205, + "propertyName": "level", + "propertyKeyName": "205", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (205)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 205, + "propertyName": "dimmingDuration", + "propertyKeyName": "205", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (205)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 206, + "propertyName": "level", + "propertyKeyName": "206", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (206)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 206, + "propertyName": "dimmingDuration", + "propertyKeyName": "206", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (206)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 207, + "propertyName": "level", + "propertyKeyName": "207", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (207)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 207, + "propertyName": "dimmingDuration", + "propertyKeyName": "207", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (207)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 208, + "propertyName": "level", + "propertyKeyName": "208", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (208)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 208, + "propertyName": "dimmingDuration", + "propertyKeyName": "208", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (208)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 209, + "propertyName": "level", + "propertyKeyName": "209", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (209)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 209, + "propertyName": "dimmingDuration", + "propertyKeyName": "209", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (209)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 210, + "propertyName": "level", + "propertyKeyName": "210", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (210)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 210, + "propertyName": "dimmingDuration", + "propertyKeyName": "210", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (210)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 211, + "propertyName": "level", + "propertyKeyName": "211", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (211)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 211, + "propertyName": "dimmingDuration", + "propertyKeyName": "211", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (211)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 212, + "propertyName": "level", + "propertyKeyName": "212", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (212)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 212, + "propertyName": "dimmingDuration", + "propertyKeyName": "212", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (212)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 213, + "propertyName": "level", + "propertyKeyName": "213", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (213)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 213, + "propertyName": "dimmingDuration", + "propertyKeyName": "213", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (213)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 214, + "propertyName": "level", + "propertyKeyName": "214", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (214)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 214, + "propertyName": "dimmingDuration", + "propertyKeyName": "214", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (214)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 215, + "propertyName": "level", + "propertyKeyName": "215", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (215)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 215, + "propertyName": "dimmingDuration", + "propertyKeyName": "215", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (215)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 216, + "propertyName": "level", + "propertyKeyName": "216", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (216)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 216, + "propertyName": "dimmingDuration", + "propertyKeyName": "216", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (216)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 217, + "propertyName": "level", + "propertyKeyName": "217", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (217)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 217, + "propertyName": "dimmingDuration", + "propertyKeyName": "217", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (217)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 218, + "propertyName": "level", + "propertyKeyName": "218", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (218)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 218, + "propertyName": "dimmingDuration", + "propertyKeyName": "218", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (218)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 219, + "propertyName": "level", + "propertyKeyName": "219", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (219)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 219, + "propertyName": "dimmingDuration", + "propertyKeyName": "219", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (219)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 220, + "propertyName": "level", + "propertyKeyName": "220", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (220)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 220, + "propertyName": "dimmingDuration", + "propertyKeyName": "220", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (220)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 221, + "propertyName": "level", + "propertyKeyName": "221", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (221)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 221, + "propertyName": "dimmingDuration", + "propertyKeyName": "221", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (221)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 222, + "propertyName": "level", + "propertyKeyName": "222", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (222)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 222, + "propertyName": "dimmingDuration", + "propertyKeyName": "222", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (222)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 223, + "propertyName": "level", + "propertyKeyName": "223", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (223)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 223, + "propertyName": "dimmingDuration", + "propertyKeyName": "223", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (223)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 224, + "propertyName": "level", + "propertyKeyName": "224", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (224)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 224, + "propertyName": "dimmingDuration", + "propertyKeyName": "224", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (224)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 225, + "propertyName": "level", + "propertyKeyName": "225", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (225)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 225, + "propertyName": "dimmingDuration", + "propertyKeyName": "225", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (225)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 226, + "propertyName": "level", + "propertyKeyName": "226", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (226)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 226, + "propertyName": "dimmingDuration", + "propertyKeyName": "226", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (226)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 227, + "propertyName": "level", + "propertyKeyName": "227", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (227)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 227, + "propertyName": "dimmingDuration", + "propertyKeyName": "227", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (227)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 228, + "propertyName": "level", + "propertyKeyName": "228", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (228)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 228, + "propertyName": "dimmingDuration", + "propertyKeyName": "228", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (228)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 229, + "propertyName": "level", + "propertyKeyName": "229", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (229)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 229, + "propertyName": "dimmingDuration", + "propertyKeyName": "229", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (229)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 230, + "propertyName": "level", + "propertyKeyName": "230", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (230)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 230, + "propertyName": "dimmingDuration", + "propertyKeyName": "230", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (230)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 231, + "propertyName": "level", + "propertyKeyName": "231", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (231)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 231, + "propertyName": "dimmingDuration", + "propertyKeyName": "231", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (231)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 232, + "propertyName": "level", + "propertyKeyName": "232", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (232)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 232, + "propertyName": "dimmingDuration", + "propertyKeyName": "232", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (232)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 233, + "propertyName": "level", + "propertyKeyName": "233", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (233)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 233, + "propertyName": "dimmingDuration", + "propertyKeyName": "233", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (233)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 234, + "propertyName": "level", + "propertyKeyName": "234", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (234)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 234, + "propertyName": "dimmingDuration", + "propertyKeyName": "234", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (234)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 235, + "propertyName": "level", + "propertyKeyName": "235", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (235)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 235, + "propertyName": "dimmingDuration", + "propertyKeyName": "235", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (235)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 236, + "propertyName": "level", + "propertyKeyName": "236", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (236)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 236, + "propertyName": "dimmingDuration", + "propertyKeyName": "236", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (236)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 237, + "propertyName": "level", + "propertyKeyName": "237", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (237)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 237, + "propertyName": "dimmingDuration", + "propertyKeyName": "237", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (237)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 238, + "propertyName": "level", + "propertyKeyName": "238", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (238)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 238, + "propertyName": "dimmingDuration", + "propertyKeyName": "238", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (238)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 239, + "propertyName": "level", + "propertyKeyName": "239", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (239)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 239, + "propertyName": "dimmingDuration", + "propertyKeyName": "239", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (239)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 240, + "propertyName": "level", + "propertyKeyName": "240", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (240)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 240, + "propertyName": "dimmingDuration", + "propertyKeyName": "240", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (240)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 241, + "propertyName": "level", + "propertyKeyName": "241", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (241)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 241, + "propertyName": "dimmingDuration", + "propertyKeyName": "241", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (241)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 242, + "propertyName": "level", + "propertyKeyName": "242", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (242)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 242, + "propertyName": "dimmingDuration", + "propertyKeyName": "242", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (242)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 243, + "propertyName": "level", + "propertyKeyName": "243", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (243)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 243, + "propertyName": "dimmingDuration", + "propertyKeyName": "243", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (243)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 244, + "propertyName": "level", + "propertyKeyName": "244", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (244)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 244, + "propertyName": "dimmingDuration", + "propertyKeyName": "244", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (244)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 245, + "propertyName": "level", + "propertyKeyName": "245", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (245)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 245, + "propertyName": "dimmingDuration", + "propertyKeyName": "245", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (245)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 246, + "propertyName": "level", + "propertyKeyName": "246", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (246)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 246, + "propertyName": "dimmingDuration", + "propertyKeyName": "246", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (246)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 247, + "propertyName": "level", + "propertyKeyName": "247", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (247)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 247, + "propertyName": "dimmingDuration", + "propertyKeyName": "247", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (247)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 248, + "propertyName": "level", + "propertyKeyName": "248", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (248)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 248, + "propertyName": "dimmingDuration", + "propertyKeyName": "248", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (248)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 249, + "propertyName": "level", + "propertyKeyName": "249", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (249)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 249, + "propertyName": "dimmingDuration", + "propertyKeyName": "249", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (249)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 250, + "propertyName": "level", + "propertyKeyName": "250", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (250)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 250, + "propertyName": "dimmingDuration", + "propertyKeyName": "250", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (250)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 251, + "propertyName": "level", + "propertyKeyName": "251", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (251)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 251, + "propertyName": "dimmingDuration", + "propertyKeyName": "251", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (251)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 252, + "propertyName": "level", + "propertyKeyName": "252", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (252)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 252, + "propertyName": "dimmingDuration", + "propertyKeyName": "252", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (252)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 253, + "propertyName": "level", + "propertyKeyName": "253", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (253)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 253, + "propertyName": "dimmingDuration", + "propertyKeyName": "253", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (253)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 254, + "propertyName": "level", + "propertyKeyName": "254", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (254)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 254, + "propertyName": "dimmingDuration", + "propertyKeyName": "254", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (254)" + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "level", + "propertyKey": 255, + "propertyName": "level", + "propertyKeyName": "255", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Level (255)", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 44, + "commandClassName": "Scene Actuator Configuration", + "property": "dimmingDuration", + "propertyKey": 255, + "propertyName": "dimmingDuration", + "propertyKeyName": "255", + "ccVersion": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration (255)" + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 7, + "propertyName": "Locator LED Status", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Locator LED Status", + "default": 255, + "min": 0, + "max": 255, + "states": { + "0": "LED always off", + "254": "LED on when switch is on", + "255": "LED on when switch is off" + }, + "valueSize": 1, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 254 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 5, + "propertyName": "Initial Dim Level", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Initial Dim Level", + "default": 0, + "min": 0, + "max": 100, + "states": { + "0": "Last dim level" + }, + "unit": "%", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 4, + "propertyName": "Maximum Dim Level", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Maximum Dim Level", + "default": 100, + "min": 0, + "max": 100, + "valueSize": 1, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 99 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 3, + "propertyName": "Minimum Dim Level", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Minimum Dim Level", + "default": 10, + "min": 1, + "max": 99, + "valueSize": 1, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 6, + "propertyName": "LED Dim Level Indicator Timeout", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "How long the level indicators should stay illuminated after the dimming level is changed", + "label": "LED Dim Level Indicator Timeout", + "default": 3, + "min": 0, + "max": 255, + "states": { + "0": "Always Off", + "255": "Always On" + }, + "unit": "seconds", + "valueSize": 1, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 30 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "manufacturerId", + "propertyName": "manufacturerId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Manufacturer ID", + "min": 0, + "max": 65535 + }, + "value": 29 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productType", + "propertyName": "productType", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product type", + "min": 0, + "max": 65535 + }, + "value": 56 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productId", + "propertyName": "productId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product ID", + "min": 0, + "max": 65535 + }, + "value": 2 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "libraryType", + "propertyName": "libraryType", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Library type", + "states": { + "0": "Unknown", + "1": "Static Controller", + "2": "Controller", + "3": "Enhanced Slave", + "4": "Slave", + "5": "Installer", + "6": "Routing Slave", + "7": "Bridge Controller", + "8": "Device under Test", + "9": "N/A", + "10": "AV Remote", + "11": "AV Device" + } + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "protocolVersion", + "propertyName": "protocolVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "7.12" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "ccVersion": 3, + "metadata": { + "type": "string[]", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + }, + "value": ["1.8"] + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hardwareVersion", + "propertyName": "hardwareVersion", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Z-Wave chip hardware version" + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "sdkVersion", + "propertyName": "sdkVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "SDK version" + }, + "value": "7.12.2" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationFrameworkAPIVersion", + "propertyName": "applicationFrameworkAPIVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave application framework API version" + }, + "value": "1.8.1" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationFrameworkBuildNumber", + "propertyName": "applicationFrameworkBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave application framework API build number" + }, + "value": 35 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hostInterfaceVersion", + "propertyName": "hostInterfaceVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Serial API version" + }, + "value": "unused" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hostInterfaceBuildNumber", + "propertyName": "hostInterfaceBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Serial API build number" + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "zWaveProtocolVersion", + "propertyName": "zWaveProtocolVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "7.12.2" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "zWaveProtocolBuildNumber", + "propertyName": "zWaveProtocolBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol build number" + }, + "value": 35 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationVersion", + "propertyName": "applicationVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Application version" + }, + "value": "1.8.1" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationBuildNumber", + "propertyName": "applicationBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Application build number" + }, + "value": 43707 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 80, + "propertyKey": 3, + "propertyName": "Node Identify", + "propertyKeyName": "On/Off Period: Duration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the duration of an on/off period in 1/10th seconds. Must be set together with \"On/Off Cycle Count\"", + "label": "Node Identify - On/Off Period: Duration", + "ccSpecific": { + "indicatorId": 80, + "propertyId": 3 + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 80, + "propertyKey": 4, + "propertyName": "Node Identify", + "propertyKeyName": "On/Off Cycle Count", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the number of on/off periods. 0xff means infinite. Must be set together with \"On/Off Period duration\"", + "label": "Node Identify - On/Off Cycle Count", + "ccSpecific": { + "indicatorId": 80, + "propertyId": 4 + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 80, + "propertyKey": 5, + "propertyName": "Node Identify", + "propertyKeyName": "On/Off Period: On time", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the length of the On time during an On/Off period. It allows asymmetric On/Off periods. The value 0x00 MUST represent symmetric On/Off period (On time equal to Off time)", + "label": "Node Identify - On/Off Period: On time", + "ccSpecific": { + "indicatorId": 80, + "propertyId": 5 + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": "value", + "propertyName": "value", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Indicator value", + "ccSpecific": { + "indicatorId": 0 + }, + "min": 0, + "max": 255 + } + } + ], + "isFrequentListening": false, + "maxDataRate": 100000, + "supportedDataRates": [40000, 100000], + "protocolVersion": 3, + "supportsBeaming": true, + "supportsSecurity": false, + "nodeType": 1, + "zwavePlusNodeType": 0, + "zwavePlusRoleType": 5, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 17, + "label": "Multilevel Switch" + }, + "specific": { + "key": 0, + "label": "Unused" + }, + "mandatorySupportedCCs": [32, 38], + "mandatoryControlledCCs": [] + }, + "interviewStage": "Complete", + "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x001d:0x0038:0x0002:1.8.1", + "statistics": { + "commandsTX": 5, + "commandsRX": 4, + "commandsDroppedRX": 0, + "commandsDroppedTX": 0, + "timeoutResponse": 0, + "rtt": 216.7 + }, + "highestSecurityClass": -1, + "isControllerNode": false, + "keepAwake": false +} diff --git a/tests/components/zwave_js/test_fan.py b/tests/components/zwave_js/test_fan.py index 27e300286d0..83863174d6f 100644 --- a/tests/components/zwave_js/test_fan.py +++ b/tests/components/zwave_js/test_fan.py @@ -1,6 +1,5 @@ """Test the Z-Wave JS fan platform.""" import copy -import math import pytest from voluptuous.error import MultipleInvalid @@ -259,7 +258,7 @@ async def test_configurable_speeds_fan(hass, client, hs_fc200, integration): assert actual_percentage in percentages state = hass.states.get(entity_id) - assert math.isclose(state.attributes[ATTR_PERCENTAGE_STEP], 33.3333, rel_tol=1e-3) + assert state.attributes[ATTR_PERCENTAGE_STEP] == pytest.approx(33.3333, rel=1e-3) assert state.attributes[ATTR_PRESET_MODES] == [] @@ -321,8 +320,8 @@ async def test_configurable_speeds_fan_with_bad_config_value( assert state.state == STATE_UNAVAILABLE -async def test_fixed_speeds_fan(hass, client, ge_12730, integration): - """Test a fan entity with fixed speeds.""" +async def test_ge_12730_fan(hass, client, ge_12730, integration): + """Test a GE 12730 fan with 3 fixed speeds.""" node = ge_12730 node_id = 24 entity_id = "fan.in_wall_smart_fan_control" @@ -384,7 +383,7 @@ async def test_fixed_speeds_fan(hass, client, ge_12730, integration): assert actual_percentage in percentages state = hass.states.get(entity_id) - assert math.isclose(state.attributes[ATTR_PERCENTAGE_STEP], 33.3333, rel_tol=1e-3) + assert state.attributes[ATTR_PERCENTAGE_STEP] == pytest.approx(33.3333, rel=1e-3) assert state.attributes[ATTR_PRESET_MODES] == [] @@ -456,7 +455,7 @@ async def test_inovelli_lzw36(hass, client, inovelli_lzw36, integration): # Check static entity properties state = hass.states.get(entity_id) - assert math.isclose(state.attributes[ATTR_PERCENTAGE_STEP], 33.3333, rel_tol=1e-3) + assert state.attributes[ATTR_PERCENTAGE_STEP] == pytest.approx(33.3333, rel=1e-3) assert state.attributes[ATTR_PRESET_MODES] == ["breeze"] # This device has one preset, where a device level of "1" is the @@ -491,6 +490,74 @@ async def test_inovelli_lzw36(hass, client, inovelli_lzw36, integration): assert len(client.async_send_command.call_args_list) == 0 +async def test_leviton_zw4sf_fan(hass, client, leviton_zw4sf, integration): + """Test a Leviton ZW4SF fan with 4 fixed speeds.""" + node = leviton_zw4sf + node_id = 88 + entity_id = "fan.4_speed_fan_controller" + + async def get_zwave_speed_from_percentage(percentage): + """Set the fan to a particular percentage and get the resulting Zwave speed.""" + client.async_send_command.reset_mock() + await hass.services.async_call( + "fan", + "turn_on", + {"entity_id": entity_id, "percentage": percentage}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == node_id + return args["value"] + + async def get_percentage_from_zwave_speed(zwave_speed): + """Set the underlying device speed and get the resulting percentage.""" + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": node_id, + "args": { + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "endpoint": 0, + "property": "currentValue", + "newValue": zwave_speed, + "prevValue": 0, + "propertyName": "currentValue", + }, + }, + ) + node.receive_event(event) + state = hass.states.get(entity_id) + return state.attributes[ATTR_PERCENTAGE] + + # This device has the speeds: + # 1 = 1-25, 2 = 26-49, 3 = 50-74, 4 = 75-99 + percentages_to_zwave_speeds = [ + [[0], [0]], + [range(1, 26), range(1, 26)], + [range(26, 51), range(26, 51)], + [range(51, 76), range(51, 76)], + [range(76, 101), range(76, 100)], + ] + + for percentages, zwave_speeds in percentages_to_zwave_speeds: + for percentage in percentages: + actual_zwave_speed = await get_zwave_speed_from_percentage(percentage) + assert actual_zwave_speed in zwave_speeds + for zwave_speed in zwave_speeds: + actual_percentage = await get_percentage_from_zwave_speed(zwave_speed) + assert actual_percentage in percentages + + state = hass.states.get(entity_id) + assert state.attributes[ATTR_PERCENTAGE_STEP] == pytest.approx(25, rel=1e-3) + assert state.attributes[ATTR_PRESET_MODES] == [] + + async def test_thermostat_fan(hass, client, climate_adc_t3000, integration): """Test the fan entity for a z-wave fan.""" node = climate_adc_t3000 From 8cc0b41daf402d57d9f9b1233f5af76c0ff089ac Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Fri, 9 Sep 2022 16:10:56 -0400 Subject: [PATCH 243/955] Fix zwave_js update entity (#78116) * Test zwave_js update entity progress * Block until firmware update is done * Update homeassistant/components/zwave_js/update.py Co-authored-by: Martin Hjelmare * revert params * unsub finished event listener * fix tests * Add test for returned failure * refactor a little * rename * Remove unnecessary controller logic for mocking * Clear event when resetting * Comments * readability * Fix test * Fix test Co-authored-by: Martin Hjelmare --- homeassistant/components/zwave_js/update.py | 71 +++++-- tests/components/zwave_js/test_update.py | 204 +++++++++++++++++--- 2 files changed, 232 insertions(+), 43 deletions(-) diff --git a/homeassistant/components/zwave_js/update.py b/homeassistant/components/zwave_js/update.py index 4f25d138aea..932ed46a0fc 100644 --- a/homeassistant/components/zwave_js/update.py +++ b/homeassistant/components/zwave_js/update.py @@ -12,7 +12,12 @@ from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.const import NodeStatus from zwave_js_server.exceptions import BaseZwaveJSServerError, FailedZWaveCommand from zwave_js_server.model.driver import Driver -from zwave_js_server.model.firmware import FirmwareUpdateInfo, FirmwareUpdateProgress +from zwave_js_server.model.firmware import ( + FirmwareUpdateFinished, + FirmwareUpdateInfo, + FirmwareUpdateProgress, + FirmwareUpdateStatus, +) from zwave_js_server.model.node import Node as ZwaveNode from homeassistant.components.update import UpdateDeviceClass, UpdateEntity @@ -82,7 +87,10 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): self._status_unsub: Callable[[], None] | None = None self._poll_unsub: Callable[[], None] | None = None self._progress_unsub: Callable[[], None] | None = None + self._finished_unsub: Callable[[], None] | None = None self._num_files_installed: int = 0 + self._finished_event = asyncio.Event() + self._finished_status: FirmwareUpdateStatus | None = None # Entity class attributes self._attr_name = "Firmware" @@ -119,18 +127,38 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): self.async_write_ha_state() @callback - def _reset_progress(self) -> None: - """Reset update install progress.""" + def _update_finished(self, event: dict[str, Any]) -> None: + """Update install progress on event.""" + finished: FirmwareUpdateFinished = event["firmware_update_finished"] + self._finished_status = finished.status + self._finished_event.set() + + @callback + def _unsub_firmware_events_and_reset_progress( + self, write_state: bool = False + ) -> None: + """Unsubscribe from firmware events and reset update install progress.""" if self._progress_unsub: self._progress_unsub() self._progress_unsub = None + + if self._finished_unsub: + self._finished_unsub() + self._finished_unsub = None + + self._finished_status = None + self._finished_event.clear() self._num_files_installed = 0 - self._attr_in_progress = False - self.async_write_ha_state() + self._attr_in_progress = 0 + if write_state: + self.async_write_ha_state() async def _async_update(self, _: HomeAssistant | datetime | None = None) -> None: """Update the entity.""" self._poll_unsub = None + + # If device is asleep/dead, wait for it to wake up/become alive before + # attempting an update for status, event_name in ( (NodeStatus.ASLEEP, "wake up"), (NodeStatus.DEAD, "alive"), @@ -187,19 +215,40 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): """Install an update.""" firmware = self._latest_version_firmware assert firmware - self._attr_in_progress = 0 - self.async_write_ha_state() + self._unsub_firmware_events_and_reset_progress(True) + self._progress_unsub = self.node.on( "firmware update progress", self._update_progress ) + self._finished_unsub = self.node.once( + "firmware update finished", self._update_finished + ) + for file in firmware.files: try: await self.driver.controller.async_begin_ota_firmware_update( self.node, file ) except BaseZwaveJSServerError as err: - self._reset_progress() + self._unsub_firmware_events_and_reset_progress() raise HomeAssistantError(err) from err + + # We need to block until we receive the `firmware update finished` event + await self._finished_event.wait() + assert self._finished_status is not None + + # If status is not OK, we should throw an error to let the user know + if self._finished_status not in ( + FirmwareUpdateStatus.OK_NO_RESTART, + FirmwareUpdateStatus.OK_RESTART_PENDING, + FirmwareUpdateStatus.OK_WAITING_FOR_ACTIVATION, + ): + status = self._finished_status + self._unsub_firmware_events_and_reset_progress() + raise HomeAssistantError(status.name.replace("_", " ").title()) + + # If we get here, the firmware installation was successful and we need to + # update progress accordingly self._num_files_installed += 1 self._attr_in_progress = floor( 100 * self._num_files_installed / len(firmware.files) @@ -208,7 +257,7 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): self._attr_installed_version = self._attr_latest_version = firmware.version self._latest_version_firmware = None - self._reset_progress() + self._unsub_firmware_events_and_reset_progress() async def async_poll_value(self, _: bool) -> None: """Poll a value.""" @@ -255,6 +304,4 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): self._poll_unsub() self._poll_unsub = None - if self._progress_unsub: - self._progress_unsub() - self._progress_unsub = None + self._unsub_firmware_events_and_reset_progress() diff --git a/tests/components/zwave_js/test_update.py b/tests/components/zwave_js/test_update.py index 76fecfdee6d..0b567d93106 100644 --- a/tests/components/zwave_js/test_update.py +++ b/tests/components/zwave_js/test_update.py @@ -1,9 +1,11 @@ """Test the Z-Wave JS update entities.""" +import asyncio from datetime import timedelta import pytest from zwave_js_server.event import Event from zwave_js_server.exceptions import FailedZWaveCommand +from zwave_js_server.model.firmware import FirmwareUpdateStatus from homeassistant.components.update.const import ( ATTR_AUTO_UPDATE, @@ -51,7 +53,7 @@ FIRMWARE_UPDATES = { } -async def test_update_entity_success( +async def test_update_entity_states( hass, client, climate_radio_thermostat_ct100_plus_different_endpoints, @@ -60,7 +62,7 @@ async def test_update_entity_success( caplog, hass_ws_client, ): - """Test update entity.""" + """Test update entity states.""" ws_client = await hass_ws_client(hass) await hass.async_block_till_done() @@ -137,39 +139,14 @@ async def test_update_entity_success( client.async_send_command.reset_mock() - # Test successful install call without a version - await hass.services.async_call( - UPDATE_DOMAIN, - SERVICE_INSTALL, - { - ATTR_ENTITY_ID: UPDATE_ENTITY, - }, - blocking=True, - ) - args = client.async_send_command.call_args_list[0][0][0] - assert args["command"] == "controller.begin_ota_firmware_update" - assert ( - args["nodeId"] - == climate_radio_thermostat_ct100_plus_different_endpoints.node_id - ) - assert args["update"] == { - "target": 0, - "url": "https://example2.com", - "integrity": "sha2", - } - - client.async_send_command.reset_mock() - - -async def test_update_entity_install_failure( +async def test_update_entity_install_raises( hass, client, climate_radio_thermostat_ct100_plus_different_endpoints, - controller_node, integration, ): - """Test update entity failed install.""" + """Test update entity install raises exception.""" client.async_send_command.return_value = FIRMWARE_UPDATES async_fire_time_changed(hass, dt_util.utcnow() + timedelta(days=1)) @@ -287,11 +264,10 @@ async def test_update_entity_ha_not_running( assert args["nodeId"] == zen_31.node_id -async def test_update_entity_failure( +async def test_update_entity_update_failure( hass, client, climate_radio_thermostat_ct100_plus_different_endpoints, - controller_node, integration, ): """Test update entity update failed.""" @@ -311,3 +287,169 @@ async def test_update_entity_failure( args["nodeId"] == climate_radio_thermostat_ct100_plus_different_endpoints.node_id ) + + +async def test_update_entity_progress( + hass, + client, + climate_radio_thermostat_ct100_plus_different_endpoints, + integration, +): + """Test update entity progress.""" + node = climate_radio_thermostat_ct100_plus_different_endpoints + client.async_send_command.return_value = FIRMWARE_UPDATES + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(days=1)) + await hass.async_block_till_done() + + state = hass.states.get(UPDATE_ENTITY) + assert state + assert state.state == STATE_ON + attrs = state.attributes + assert attrs[ATTR_INSTALLED_VERSION] == "10.7" + assert attrs[ATTR_LATEST_VERSION] == "11.2.4" + + client.async_send_command.reset_mock() + client.async_send_command.return_value = None + + # Test successful install call without a version + install_task = hass.async_create_task( + hass.services.async_call( + UPDATE_DOMAIN, + SERVICE_INSTALL, + { + ATTR_ENTITY_ID: UPDATE_ENTITY, + }, + blocking=True, + ) + ) + + # Sleep so that task starts + await asyncio.sleep(0.1) + + event = Event( + type="firmware update progress", + data={ + "source": "node", + "event": "firmware update progress", + "nodeId": node.node_id, + "sentFragments": 1, + "totalFragments": 20, + }, + ) + node.receive_event(event) + + # Validate that the progress is updated + state = hass.states.get(UPDATE_ENTITY) + assert state + attrs = state.attributes + assert attrs[ATTR_IN_PROGRESS] == 5 + + event = Event( + type="firmware update finished", + data={ + "source": "node", + "event": "firmware update finished", + "nodeId": node.node_id, + "status": FirmwareUpdateStatus.OK_NO_RESTART, + }, + ) + + node.receive_event(event) + await hass.async_block_till_done() + + # Validate that progress is reset and entity reflects new version + state = hass.states.get(UPDATE_ENTITY) + assert state + attrs = state.attributes + assert attrs[ATTR_IN_PROGRESS] is False + assert attrs[ATTR_INSTALLED_VERSION] == "11.2.4" + assert attrs[ATTR_LATEST_VERSION] == "11.2.4" + assert state.state == STATE_OFF + + await install_task + + +async def test_update_entity_install_failed( + hass, + client, + climate_radio_thermostat_ct100_plus_different_endpoints, + integration, + caplog, +): + """Test update entity install returns error status.""" + node = climate_radio_thermostat_ct100_plus_different_endpoints + client.async_send_command.return_value = FIRMWARE_UPDATES + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(days=1)) + await hass.async_block_till_done() + + state = hass.states.get(UPDATE_ENTITY) + assert state + assert state.state == STATE_ON + attrs = state.attributes + assert attrs[ATTR_INSTALLED_VERSION] == "10.7" + assert attrs[ATTR_LATEST_VERSION] == "11.2.4" + + client.async_send_command.reset_mock() + client.async_send_command.return_value = None + + async def call_install(): + await hass.services.async_call( + UPDATE_DOMAIN, + SERVICE_INSTALL, + { + ATTR_ENTITY_ID: UPDATE_ENTITY, + }, + blocking=True, + ) + + # Test install call - we expect it to raise + install_task = hass.async_create_task(call_install()) + + # Sleep so that task starts + await asyncio.sleep(0.1) + + event = Event( + type="firmware update progress", + data={ + "source": "node", + "event": "firmware update progress", + "nodeId": node.node_id, + "sentFragments": 1, + "totalFragments": 20, + }, + ) + node.receive_event(event) + + # Validate that the progress is updated + state = hass.states.get(UPDATE_ENTITY) + assert state + attrs = state.attributes + assert attrs[ATTR_IN_PROGRESS] == 5 + + event = Event( + type="firmware update finished", + data={ + "source": "node", + "event": "firmware update finished", + "nodeId": node.node_id, + "status": FirmwareUpdateStatus.ERROR_TIMEOUT, + }, + ) + + node.receive_event(event) + await hass.async_block_till_done() + + # Validate that progress is reset and entity reflects old version + state = hass.states.get(UPDATE_ENTITY) + assert state + attrs = state.attributes + assert attrs[ATTR_IN_PROGRESS] is False + assert attrs[ATTR_INSTALLED_VERSION] == "10.7" + assert attrs[ATTR_LATEST_VERSION] == "11.2.4" + assert state.state == STATE_ON + + # validate that the install task failed + with pytest.raises(HomeAssistantError): + await install_task From fcb6888f87d7688e978c061bb13012ef787f2f33 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 9 Sep 2022 17:16:02 -0500 Subject: [PATCH 244/955] Start logbook stream faster (#77921) Co-authored-by: Paulus Schoutsen --- .../components/logbook/websocket_api.py | 82 +++++------- .../components/logbook/test_websocket_api.py | 117 +++++++++++------- 2 files changed, 106 insertions(+), 93 deletions(-) diff --git a/homeassistant/components/logbook/websocket_api.py b/homeassistant/components/logbook/websocket_api.py index 3af87b26caa..d28666963c8 100644 --- a/homeassistant/components/logbook/websocket_api.py +++ b/homeassistant/components/logbook/websocket_api.py @@ -31,7 +31,6 @@ from .processor import EventProcessor MAX_PENDING_LOGBOOK_EVENTS = 2048 EVENT_COALESCE_TIME = 0.35 -MAX_RECORDER_WAIT = 10 # minimum size that we will split the query BIG_QUERY_HOURS = 25 # how many hours to deliver in the first chunk when we split the query @@ -48,6 +47,7 @@ class LogbookLiveStream: subscriptions: list[CALLBACK_TYPE] end_time_unsub: CALLBACK_TYPE | None = None task: asyncio.Task | None = None + wait_sync_task: asyncio.Task | None = None @callback @@ -57,18 +57,6 @@ def async_setup(hass: HomeAssistant) -> None: websocket_api.async_register_command(hass, ws_event_stream) -async def _async_wait_for_recorder_sync(hass: HomeAssistant) -> None: - """Wait for the recorder to sync.""" - try: - await asyncio.wait_for( - get_instance(hass).async_block_till_done(), MAX_RECORDER_WAIT - ) - except asyncio.TimeoutError: - _LOGGER.debug( - "Recorder is behind more than %s seconds, starting live stream; Some results may be missing" - ) - - @callback def _async_send_empty_response( connection: ActiveConnection, msg_id: int, start_time: dt, end_time: dt | None @@ -347,8 +335,11 @@ async def ws_event_stream( subscriptions.clear() if live_stream.task: live_stream.task.cancel() + if live_stream.wait_sync_task: + live_stream.wait_sync_task.cancel() if live_stream.end_time_unsub: live_stream.end_time_unsub() + live_stream.end_time_unsub = None if end_time: live_stream.end_time_unsub = async_track_point_in_utc_time( @@ -395,43 +386,6 @@ async def ws_event_stream( partial=True, ) - await _async_wait_for_recorder_sync(hass) - if msg_id not in connection.subscriptions: - # Unsubscribe happened while waiting for recorder - return - - # - # Fetch any events from the database that have - # not been committed since the original fetch - # so we can switch over to using the subscriptions - # - # We only want events that happened after the last event - # we had from the last database query or the maximum - # time we allow the recorder to be behind - # - max_recorder_behind = subscriptions_setup_complete_time - timedelta( - seconds=MAX_RECORDER_WAIT - ) - second_fetch_start_time = max( - last_event_time or max_recorder_behind, max_recorder_behind - ) - await _async_send_historical_events( - hass, - connection, - msg_id, - second_fetch_start_time, - subscriptions_setup_complete_time, - messages.event_message, - event_processor, - partial=False, - ) - - if not subscriptions: - # Unsubscribe happened while waiting for formatted events - # or there are no supported entities (all UOM or state class) - # or devices - return - live_stream.task = asyncio.create_task( _async_events_consumer( subscriptions_setup_complete_time, @@ -442,6 +396,34 @@ async def ws_event_stream( ) ) + if msg_id not in connection.subscriptions: + # Unsubscribe happened while sending historical events + return + + live_stream.wait_sync_task = asyncio.create_task( + get_instance(hass).async_block_till_done() + ) + await live_stream.wait_sync_task + + # + # Fetch any events from the database that have + # not been committed since the original fetch + # so we can switch over to using the subscriptions + # + # We only want events that happened after the last event + # we had from the last database query + # + await _async_send_historical_events( + hass, + connection, + msg_id, + last_event_time or start_time, + subscriptions_setup_complete_time, + messages.event_message, + event_processor, + partial=False, + ) + def _ws_formatted_get_events( msg_id: int, diff --git a/tests/components/logbook/test_websocket_api.py b/tests/components/logbook/test_websocket_api.py index ec4f2183a9a..4b2c40f41c0 100644 --- a/tests/components/logbook/test_websocket_api.py +++ b/tests/components/logbook/test_websocket_api.py @@ -11,6 +11,7 @@ from homeassistant import core from homeassistant.components import logbook, recorder from homeassistant.components.automation import ATTR_SOURCE, EVENT_AUTOMATION_TRIGGERED from homeassistant.components.logbook import websocket_api +from homeassistant.components.recorder.util import get_instance from homeassistant.components.script import EVENT_SCRIPT_STARTED from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.const import ( @@ -566,6 +567,15 @@ async def test_subscribe_unsubscribe_logbook_stream_excluded_entities( assert msg["event"]["end_time"] > msg["event"]["start_time"] assert msg["event"]["partial"] is True + await get_instance(hass).async_block_till_done() + await hass.async_block_till_done() + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert "partial" not in msg["event"]["events"] + assert msg["event"]["events"] == [] + hass.states.async_set("light.exc", STATE_ON) hass.states.async_set("light.exc", STATE_OFF) hass.states.async_set("switch.any", STATE_ON) @@ -588,12 +598,8 @@ async def test_subscribe_unsubscribe_logbook_stream_excluded_entities( await hass.async_block_till_done() hass.states.async_set("light.zulu", "on", {"effect": "help", "color": "blue"}) - - msg = await asyncio.wait_for(websocket_client.receive_json(), 2) - assert msg["id"] == 7 - assert msg["type"] == "event" - assert "partial" not in msg["event"]["events"] - assert msg["event"]["events"] == [] + await get_instance(hass).async_block_till_done() + await hass.async_block_till_done() msg = await asyncio.wait_for(websocket_client.receive_json(), 2) assert msg["id"] == 7 @@ -747,6 +753,13 @@ async def test_subscribe_unsubscribe_logbook_stream_included_entities( assert msg["event"]["end_time"] > msg["event"]["start_time"] assert msg["event"]["partial"] is True + await hass.async_block_till_done() + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert "partial" not in msg["event"]["events"] + assert msg["event"]["events"] == [] + for entity_id in test_entities: hass.states.async_set(entity_id, STATE_ON) hass.states.async_set(entity_id, STATE_OFF) @@ -756,12 +769,7 @@ async def test_subscribe_unsubscribe_logbook_stream_included_entities( await hass.async_block_till_done() hass.states.async_set("light.zulu", "on", {"effect": "help", "color": "blue"}) - - msg = await asyncio.wait_for(websocket_client.receive_json(), 2) - assert msg["id"] == 7 - assert msg["type"] == "event" - assert "partial" not in msg["event"]["events"] - assert msg["event"]["events"] == [] + await hass.async_block_till_done() msg = await asyncio.wait_for(websocket_client.receive_json(), 2) assert msg["id"] == 7 @@ -958,6 +966,14 @@ async def test_logbook_stream_excluded_entities_inherits_filters_from_recorder( assert msg["event"]["end_time"] > msg["event"]["start_time"] assert msg["event"]["partial"] is True + await hass.async_block_till_done() + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert "partial" not in msg["event"]["events"] + assert msg["event"]["events"] == [] + hass.states.async_set("light.exc", STATE_ON) hass.states.async_set("light.exc", STATE_OFF) hass.states.async_set("switch.any", STATE_ON) @@ -982,12 +998,7 @@ async def test_logbook_stream_excluded_entities_inherits_filters_from_recorder( await hass.async_block_till_done() hass.states.async_set("light.zulu", "on", {"effect": "help", "color": "blue"}) - - msg = await asyncio.wait_for(websocket_client.receive_json(), 2) - assert msg["id"] == 7 - assert msg["type"] == "event" - assert "partial" not in msg["event"]["events"] - assert msg["event"]["events"] == [] + await hass.async_block_till_done() msg = await asyncio.wait_for(websocket_client.receive_json(), 2) assert msg["id"] == 7 @@ -1121,6 +1132,14 @@ async def test_subscribe_unsubscribe_logbook_stream( assert msg["event"]["end_time"] > msg["event"]["start_time"] assert msg["event"]["partial"] is True + await hass.async_block_till_done() + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert "partial" not in msg["event"]["events"] + assert msg["event"]["events"] == [] + hass.states.async_set("light.alpha", "on") hass.states.async_set("light.alpha", "off") alpha_off_state: State = hass.states.get("light.alpha") @@ -1138,12 +1157,6 @@ async def test_subscribe_unsubscribe_logbook_stream( hass.states.async_set("light.zulu", "on", {"effect": "help", "color": "blue"}) - msg = await asyncio.wait_for(websocket_client.receive_json(), 2) - assert msg["id"] == 7 - assert msg["type"] == "event" - assert "partial" not in msg["event"]["events"] - assert msg["event"]["events"] == [] - msg = await asyncio.wait_for(websocket_client.receive_json(), 2) assert msg["id"] == 7 assert msg["type"] == "event" @@ -1427,12 +1440,8 @@ async def test_subscribe_unsubscribe_logbook_stream_entities( } ] - hass.states.async_set("light.alpha", STATE_ON) - hass.states.async_set("light.alpha", STATE_OFF) - hass.states.async_set("light.small", STATE_OFF, {"effect": "help", "color": "blue"}) - + await get_instance(hass).async_block_till_done() await hass.async_block_till_done() - msg = await asyncio.wait_for(websocket_client.receive_json(), 2) assert msg["id"] == 7 assert msg["type"] == "event" @@ -1441,6 +1450,13 @@ async def test_subscribe_unsubscribe_logbook_stream_entities( assert "partial" not in msg["event"] assert msg["event"]["events"] == [] + hass.states.async_set("light.alpha", STATE_ON) + hass.states.async_set("light.alpha", STATE_OFF) + hass.states.async_set("light.small", STATE_OFF, {"effect": "help", "color": "blue"}) + + await get_instance(hass).async_block_till_done() + await hass.async_block_till_done() + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) assert msg["id"] == 7 assert msg["type"] == "event" @@ -1455,6 +1471,7 @@ async def test_subscribe_unsubscribe_logbook_stream_entities( hass.states.async_remove("light.alpha") hass.states.async_remove("light.small") + await get_instance(hass).async_block_till_done() await hass.async_block_till_done() await websocket_client.send_json( @@ -1520,10 +1537,7 @@ async def test_subscribe_unsubscribe_logbook_stream_entities_with_end_time( } ] - hass.states.async_set("light.alpha", STATE_ON) - hass.states.async_set("light.alpha", STATE_OFF) - hass.states.async_set("light.small", STATE_OFF, {"effect": "help", "color": "blue"}) - + await get_instance(hass).async_block_till_done() await hass.async_block_till_done() msg = await asyncio.wait_for(websocket_client.receive_json(), 2) @@ -1532,6 +1546,12 @@ async def test_subscribe_unsubscribe_logbook_stream_entities_with_end_time( assert "partial" not in msg["event"] assert msg["event"]["events"] == [] + hass.states.async_set("light.alpha", STATE_ON) + hass.states.async_set("light.alpha", STATE_OFF) + hass.states.async_set("light.small", STATE_OFF, {"effect": "help", "color": "blue"}) + + await hass.async_block_till_done() + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) assert msg["id"] == 7 assert msg["type"] == "event" @@ -2176,7 +2196,6 @@ async def test_stream_consumer_stop_processing(hass, recorder_mock, hass_ws_clie @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) -@patch("homeassistant.components.logbook.websocket_api.MAX_RECORDER_WAIT", 0.15) async def test_recorder_is_far_behind(hass, recorder_mock, hass_ws_client, caplog): """Test we still start live streaming if the recorder is far behind.""" now = dt_util.utcnow() @@ -2250,8 +2269,6 @@ async def test_recorder_is_far_behind(hass, recorder_mock, hass_ws_client, caplo assert msg["type"] == TYPE_RESULT assert msg["success"] - assert "Recorder is behind" in caplog.text - @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) async def test_subscribe_all_entities_are_continuous( @@ -2409,16 +2426,28 @@ async def test_subscribe_entities_some_have_uom_multiple( assert msg["type"] == TYPE_RESULT assert msg["success"] - _cycle_entities() + await get_instance(hass).async_block_till_done() + await hass.async_block_till_done() msg = await asyncio.wait_for(websocket_client.receive_json(), 2) assert msg["id"] == 7 assert msg["type"] == "event" - assert msg["event"]["partial"] is True assert msg["event"]["events"] == [ {"entity_id": "sensor.keep", "state": "off", "when": ANY}, {"entity_id": "sensor.keep_two", "state": "off", "when": ANY}, ] + assert msg["event"]["partial"] is True + + await get_instance(hass).async_block_till_done() + await hass.async_block_till_done() + _cycle_entities() + await hass.async_block_till_done() + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert "partial" not in msg["event"] + assert msg["event"]["events"] == [] _cycle_entities() await hass.async_block_till_done() @@ -2426,7 +2455,12 @@ async def test_subscribe_entities_some_have_uom_multiple( msg = await asyncio.wait_for(websocket_client.receive_json(), 2) assert msg["id"] == 7 assert msg["type"] == "event" - assert msg["event"]["events"] == [] + assert msg["event"]["events"] == [ + {"entity_id": "sensor.keep", "state": "on", "when": ANY}, + {"entity_id": "sensor.keep", "state": "off", "when": ANY}, + {"entity_id": "sensor.keep_two", "state": "on", "when": ANY}, + {"entity_id": "sensor.keep_two", "state": "off", "when": ANY}, + ] assert "partial" not in msg["event"] msg = await asyncio.wait_for(websocket_client.receive_json(), 2) @@ -2437,11 +2471,8 @@ async def test_subscribe_entities_some_have_uom_multiple( {"entity_id": "sensor.keep", "state": "off", "when": ANY}, {"entity_id": "sensor.keep_two", "state": "on", "when": ANY}, {"entity_id": "sensor.keep_two", "state": "off", "when": ANY}, - {"entity_id": "sensor.keep", "state": "on", "when": ANY}, - {"entity_id": "sensor.keep", "state": "off", "when": ANY}, - {"entity_id": "sensor.keep_two", "state": "on", "when": ANY}, - {"entity_id": "sensor.keep_two", "state": "off", "when": ANY}, ] + assert "partial" not in msg["event"] await websocket_client.close() From d98ed0384577f5f52342cee4efd922f235a3d8a0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 9 Sep 2022 18:13:27 -0500 Subject: [PATCH 245/955] Fix switchbot writing state too frequently (#78094) --- .../components/switchbot/coordinator.py | 5 ++++- .../components/switchbot/manifest.json | 2 +- homeassistant/components/switchbot/sensor.py | 17 ++++++++++++++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 21 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/switchbot/coordinator.py b/homeassistant/components/switchbot/coordinator.py index 103e9d67c58..94018c1b46b 100644 --- a/homeassistant/components/switchbot/coordinator.py +++ b/homeassistant/components/switchbot/coordinator.py @@ -69,13 +69,16 @@ class SwitchbotDataUpdateCoordinator(PassiveBluetoothDataUpdateCoordinator): change: bluetooth.BluetoothChange, ) -> None: """Handle a Bluetooth event.""" + self.ble_device = service_info.device if adv := switchbot.parse_advertisement_data( service_info.device, service_info.advertisement ): - self.data = flatten_sensors_data(adv.data) if "modelName" in self.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) super()._async_handle_bluetooth_event(service_info, change) diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index f322734ba54..e311295e52c 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.18.27"], + "requirements": ["PySwitchbot==0.19.0"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/homeassistant/components/switchbot/sensor.py b/homeassistant/components/switchbot/sensor.py index 886da1051b7..9658c1ed9c8 100644 --- a/homeassistant/components/switchbot/sensor.py +++ b/homeassistant/components/switchbot/sensor.py @@ -71,14 +71,16 @@ async def async_setup_entry( ) -> None: """Set up Switchbot sensor based on a config entry.""" coordinator: SwitchbotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - async_add_entities( + entities = [ SwitchBotSensor( coordinator, sensor, ) for sensor in coordinator.data["data"] if sensor in SENSOR_TYPES - ) + ] + entities.append(SwitchbotRSSISensor(coordinator, "rssi")) + async_add_entities(entities) class SwitchBotSensor(SwitchbotEntity, SensorEntity): @@ -98,6 +100,15 @@ class SwitchBotSensor(SwitchbotEntity, SensorEntity): self.entity_description = SENSOR_TYPES[sensor] @property - def native_value(self) -> str: + def native_value(self) -> str | int: """Return the state of the sensor.""" return self.data["data"][self._sensor] + + +class SwitchbotRSSISensor(SwitchBotSensor): + """Representation of a Switchbot RSSI sensor.""" + + @property + def native_value(self) -> str | int: + """Return the state of the sensor.""" + return self.coordinator.ble_device.rssi diff --git a/requirements_all.txt b/requirements_all.txt index e516fc66736..6853f91defb 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.18.27 +PySwitchbot==0.19.0 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c182079c026..c58ddd1e686 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.18.27 +PySwitchbot==0.19.0 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From 901031eb252e121a9dc6380c6cb92872452406bd Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 10 Sep 2022 00:29:43 +0000 Subject: [PATCH 246/955] [ci skip] Translation update --- .../amberelectric/translations/el.json | 5 +++++ .../amberelectric/translations/en.json | 10 +++++----- .../amberelectric/translations/fr.json | 5 +++++ .../amberelectric/translations/hu.json | 5 +++++ .../amberelectric/translations/pt-BR.json | 5 +++++ .../amberelectric/translations/zh-Hant.json | 5 +++++ .../automation/translations/id.json | 2 +- .../components/awair/translations/bg.json | 6 ++++++ .../bluemaestro/translations/bg.json | 6 ++++-- .../components/bluetooth/translations/bg.json | 12 ++++++++++- .../fully_kiosk/translations/bg.json | 20 +++++++++++++++++++ .../landisgyr_heat_meter/translations/bg.json | 7 +++++++ .../components/melnor/translations/id.json | 2 +- .../components/nest/translations/id.json | 2 +- .../openexchangerates/translations/id.json | 2 +- .../pure_energie/translations/id.json | 2 +- .../components/qingping/translations/bg.json | 12 +++++++++++ .../components/sensorpro/translations/bg.json | 6 ++++-- .../thermobeacon/translations/bg.json | 3 ++- .../components/thermopro/translations/bg.json | 3 ++- .../components/tilt_ble/translations/bg.json | 6 ++++-- .../components/zha/translations/bg.json | 13 ++++++++++++ 22 files changed, 120 insertions(+), 19 deletions(-) create mode 100644 homeassistant/components/fully_kiosk/translations/bg.json create mode 100644 homeassistant/components/qingping/translations/bg.json diff --git a/homeassistant/components/amberelectric/translations/el.json b/homeassistant/components/amberelectric/translations/el.json index 3dc8fc9dd78..018a4d33bd3 100644 --- a/homeassistant/components/amberelectric/translations/el.json +++ b/homeassistant/components/amberelectric/translations/el.json @@ -1,5 +1,10 @@ { "config": { + "error": { + "invalid_api_token": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API", + "no_site": "\u0394\u03b5\u03bd \u03c0\u03b1\u03c1\u03ad\u03c7\u03b5\u03c4\u03b1\u03b9 \u03c7\u03ce\u03c1\u03bf\u03c2", + "unknown_error": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, "step": { "site": { "data": { diff --git a/homeassistant/components/amberelectric/translations/en.json b/homeassistant/components/amberelectric/translations/en.json index 0a974298134..3798aa77bf6 100644 --- a/homeassistant/components/amberelectric/translations/en.json +++ b/homeassistant/components/amberelectric/translations/en.json @@ -1,5 +1,10 @@ { "config": { + "error": { + "invalid_api_token": "Invalid API key", + "no_site": "No site provided", + "unknown_error": "Unexpected error" + }, "step": { "site": { "data": { @@ -15,11 +20,6 @@ }, "description": "Go to {api_url} to generate an API key" } - }, - "error": { - "invalid_api_token": "Invalid API key", - "no_site": "No site provided", - "unknown_error": "Unexpected error" } } } \ No newline at end of file diff --git a/homeassistant/components/amberelectric/translations/fr.json b/homeassistant/components/amberelectric/translations/fr.json index a5b1164924c..2a23419258d 100644 --- a/homeassistant/components/amberelectric/translations/fr.json +++ b/homeassistant/components/amberelectric/translations/fr.json @@ -1,5 +1,10 @@ { "config": { + "error": { + "invalid_api_token": "Cl\u00e9 d'API non valide", + "no_site": "Aucun site fourni", + "unknown_error": "Erreur inattendue" + }, "step": { "site": { "data": { diff --git a/homeassistant/components/amberelectric/translations/hu.json b/homeassistant/components/amberelectric/translations/hu.json index 2d361e1e76f..3422f278ba8 100644 --- a/homeassistant/components/amberelectric/translations/hu.json +++ b/homeassistant/components/amberelectric/translations/hu.json @@ -1,5 +1,10 @@ { "config": { + "error": { + "invalid_api_token": "\u00c9rv\u00e9nytelen API kulcs", + "no_site": "Nincs megadva a helysz\u00edn", + "unknown_error": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, "step": { "site": { "data": { diff --git a/homeassistant/components/amberelectric/translations/pt-BR.json b/homeassistant/components/amberelectric/translations/pt-BR.json index 35541dbf290..17285172a30 100644 --- a/homeassistant/components/amberelectric/translations/pt-BR.json +++ b/homeassistant/components/amberelectric/translations/pt-BR.json @@ -1,5 +1,10 @@ { "config": { + "error": { + "invalid_api_token": "Chave de API inv\u00e1lida", + "no_site": "Nenhum site fornecido", + "unknown_error": "Erro inesperado" + }, "step": { "site": { "data": { diff --git a/homeassistant/components/amberelectric/translations/zh-Hant.json b/homeassistant/components/amberelectric/translations/zh-Hant.json index 42b671d3530..121db7935fa 100644 --- a/homeassistant/components/amberelectric/translations/zh-Hant.json +++ b/homeassistant/components/amberelectric/translations/zh-Hant.json @@ -1,5 +1,10 @@ { "config": { + "error": { + "invalid_api_token": "API \u91d1\u9470\u7121\u6548", + "no_site": "\u672a\u63d0\u4f9b\u7ad9\u9ede", + "unknown_error": "\u672a\u9810\u671f\u932f\u8aa4" + }, "step": { "site": { "data": { diff --git a/homeassistant/components/automation/translations/id.json b/homeassistant/components/automation/translations/id.json index acf1dfab41b..8bee43fadec 100644 --- a/homeassistant/components/automation/translations/id.json +++ b/homeassistant/components/automation/translations/id.json @@ -4,7 +4,7 @@ "fix_flow": { "step": { "confirm": { - "description": "Otomasi \"{nama}\" (`{entity_id}`) memiliki aksi yang memanggil layanan yang tidak diketahui: `{service}`.\n\nKesalahan ini mencegah otomasi berjalan dengan benar. Mungkin layanan ini tidak lagi tersedia, atau mungkin kesalahan ketik yang menyebabkannya.\n\nUntuk memperbaiki kesalahan ini, [edit otomasi]({edit}) dan hapus aksi yang memanggil layanan ini.\n\nKlik KIRIM di bawah ini untuk mengonfirmasi bahwa Anda telah memperbaiki otomasi ini.", + "description": "Otomasi \"{name}\" (`{entity_id}`) memiliki aksi yang memanggil layanan yang tidak diketahui: `{service}`.\n\nKesalahan ini mencegah otomasi berjalan dengan benar. Mungkin layanan ini tidak lagi tersedia, atau mungkin kesalahan ketik yang menyebabkannya.\n\nUntuk memperbaiki kesalahan ini, [edit otomasi]({edit}) dan hapus aksi yang memanggil layanan ini.\n\nKlik KIRIM di bawah ini untuk mengonfirmasi bahwa Anda telah memperbaiki otomasi ini.", "title": "{name} menggunakan layanan yang tidak dikenal" } } diff --git a/homeassistant/components/awair/translations/bg.json b/homeassistant/components/awair/translations/bg.json index 1d5233cabbf..1fa4dec8b6f 100644 --- a/homeassistant/components/awair/translations/bg.json +++ b/homeassistant/components/awair/translations/bg.json @@ -8,6 +8,12 @@ "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "step": { + "local_pick": { + "data": { + "device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e", + "host": "IP \u0430\u0434\u0440\u0435\u0441" + } + }, "reauth": { "data": { "email": "Email" diff --git a/homeassistant/components/bluemaestro/translations/bg.json b/homeassistant/components/bluemaestro/translations/bg.json index 39900c2a9b2..e3525d4f0de 100644 --- a/homeassistant/components/bluemaestro/translations/bg.json +++ b/homeassistant/components/bluemaestro/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 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u043e" + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u043e", + "no_devices_found": "\u041d\u044f\u043c\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\u0430" }, "flow_title": "{name}", "step": { @@ -11,7 +12,8 @@ "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" } } } diff --git a/homeassistant/components/bluetooth/translations/bg.json b/homeassistant/components/bluetooth/translations/bg.json index 0740fe1b952..9b010d6629d 100644 --- a/homeassistant/components/bluetooth/translations/bg.json +++ b/homeassistant/components/bluetooth/translations/bg.json @@ -4,11 +4,21 @@ "multiple_adapters": { "data": { "adapter": "\u0410\u0434\u0430\u043f\u0442\u0435\u0440" - } + }, + "description": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 Bluetooth \u0430\u0434\u0430\u043f\u0442\u0435\u0440 \u0437\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430" }, "single_adapter": { "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 Bluetooth \u0430\u0434\u0430\u043f\u0442\u0435\u0440\u0430 {name}?" } } + }, + "options": { + "step": { + "init": { + "data": { + "passive": "\u041f\u0430\u0441\u0438\u0432\u043d\u043e \u0441\u043a\u0430\u043d\u0438\u0440\u0430\u043d\u0435" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/fully_kiosk/translations/bg.json b/homeassistant/components/fully_kiosk/translations/bg.json new file mode 100644 index 00000000000..8dbd96c7099 --- /dev/null +++ b/homeassistant/components/fully_kiosk/translations/bg.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d" + }, + "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" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/landisgyr_heat_meter/translations/bg.json b/homeassistant/components/landisgyr_heat_meter/translations/bg.json index 9862b6b3a2a..6120c9152d7 100644 --- a/homeassistant/components/landisgyr_heat_meter/translations/bg.json +++ b/homeassistant/components/landisgyr_heat_meter/translations/bg.json @@ -6,6 +6,13 @@ "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": { + "user": { + "data": { + "device": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/melnor/translations/id.json b/homeassistant/components/melnor/translations/id.json index 551a50ca9e5..32e1b5bdd94 100644 --- a/homeassistant/components/melnor/translations/id.json +++ b/homeassistant/components/melnor/translations/id.json @@ -5,7 +5,7 @@ }, "step": { "bluetooth_confirm": { - "description": "Ingin menambahkan katup Bluetooth Melnor `{nama}` ke Home Assistant?", + "description": "Ingin menambahkan katup Bluetooth Melnor `{name}` ke Home Assistant?", "title": "Katup Bluetooth Melnor yang ditemukan" } } diff --git a/homeassistant/components/nest/translations/id.json b/homeassistant/components/nest/translations/id.json index 1d69f9f48bd..d9ed2039800 100644 --- a/homeassistant/components/nest/translations/id.json +++ b/homeassistant/components/nest/translations/id.json @@ -1,6 +1,6 @@ { "application_credentials": { - "description": "Ikuti [petunjuk]({more_info_url}) untuk mengonfigurasi Konsol Cloud:\n\n1. Buka [Layar persetujuan OAuth]({oauth_consent_url}) dan konfigurasikan\n1. Buka [Kredentsial]({oauth_creds_url}) dan klik **Buat Kredensial**.\n1. Dari daftar pilihan, pilih **ID klien OAuth**.\n1. Pilih **Aplikasi Web** untuk Jenis Aplikasi.\n1. Tambahkan '{redirect_url}' di bawah *URI pengarahan ulang yand diotorisasi*." + "description": "Ikuti [petunjuk]({more_info_url}) untuk mengonfigurasi Konsol Cloud:\n\n1. Buka [Layar persetujuan OAuth]({oauth_consent_url}) dan konfigurasikan\n1. Buka [Kredentsial]({oauth_creds_url}) dan klik **Buat Kredensial**.\n1. Dari daftar pilihan, pilih **ID klien OAuth**.\n1. Pilih **Aplikasi Web** untuk Jenis Aplikasi.\n1. Tambahkan `{redirect_url}` di bawah *URI pengarahan ulang yand diotorisasi*." }, "config": { "abort": { diff --git a/homeassistant/components/openexchangerates/translations/id.json b/homeassistant/components/openexchangerates/translations/id.json index 0b5f0183e2b..29f4a6eaabf 100644 --- a/homeassistant/components/openexchangerates/translations/id.json +++ b/homeassistant/components/openexchangerates/translations/id.json @@ -19,7 +19,7 @@ "base": "Mata uang dasar" }, "data_description": { - "base": "Menggunakan mata uang dasar selain USD memerlukan [paket berbayar]({daftar})." + "base": "Menggunakan mata uang dasar selain USD memerlukan [paket berbayar]({signup})." } } } diff --git a/homeassistant/components/pure_energie/translations/id.json b/homeassistant/components/pure_energie/translations/id.json index ef0e2b69785..55fea559b74 100644 --- a/homeassistant/components/pure_energie/translations/id.json +++ b/homeassistant/components/pure_energie/translations/id.json @@ -18,7 +18,7 @@ } }, "zeroconf_confirm": { - "description": "Ingin menambahkan Pure Energie Meter (`{name}`) ke Home Assistant?", + "description": "Ingin menambahkan Pure Energie Meter (`{model}`) ke Home Assistant?", "title": "Peranti Pure Energie Meter yang ditemukan" } } diff --git a/homeassistant/components/qingping/translations/bg.json b/homeassistant/components/qingping/translations/bg.json new file mode 100644 index 00000000000..bcc9f98e10d --- /dev/null +++ b/homeassistant/components/qingping/translations/bg.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "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/sensorpro/translations/bg.json b/homeassistant/components/sensorpro/translations/bg.json index c79e057d5c0..a61dac839ad 100644 --- a/homeassistant/components/sensorpro/translations/bg.json +++ b/homeassistant/components/sensorpro/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", + "no_devices_found": "\u041d\u044f\u043c\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\u0430" }, "flow_title": "{name}", "step": { @@ -11,7 +12,8 @@ "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" } } } diff --git a/homeassistant/components/thermobeacon/translations/bg.json b/homeassistant/components/thermobeacon/translations/bg.json index 325b3d59a46..d7b7634c9f7 100644 --- a/homeassistant/components/thermobeacon/translations/bg.json +++ b/homeassistant/components/thermobeacon/translations/bg.json @@ -12,7 +12,8 @@ "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" } } } diff --git a/homeassistant/components/thermopro/translations/bg.json b/homeassistant/components/thermopro/translations/bg.json index c79e057d5c0..81695457d85 100644 --- a/homeassistant/components/thermopro/translations/bg.json +++ b/homeassistant/components/thermopro/translations/bg.json @@ -11,7 +11,8 @@ "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" } } } diff --git a/homeassistant/components/tilt_ble/translations/bg.json b/homeassistant/components/tilt_ble/translations/bg.json index 39900c2a9b2..e3525d4f0de 100644 --- a/homeassistant/components/tilt_ble/translations/bg.json +++ b/homeassistant/components/tilt_ble/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 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u043e" + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u043e", + "no_devices_found": "\u041d\u044f\u043c\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\u0430" }, "flow_title": "{name}", "step": { @@ -11,7 +12,8 @@ "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" } } } diff --git a/homeassistant/components/zha/translations/bg.json b/homeassistant/components/zha/translations/bg.json index e3d72388dee..5774bca7124 100644 --- a/homeassistant/components/zha/translations/bg.json +++ b/homeassistant/components/zha/translations/bg.json @@ -14,6 +14,11 @@ "confirm_hardware": { "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 {name}?" }, + "manual_port_config": { + "data": { + "baudrate": "\u0441\u043a\u043e\u0440\u043e\u0441\u0442 \u043d\u0430 \u043f\u043e\u0440\u0442\u0430" + } + }, "pick_radio": { "data": { "radio_type": "\u0422\u0438\u043f \u0440\u0430\u0434\u0438\u043e" @@ -85,8 +90,16 @@ } }, "options": { + "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" + }, "flow_title": "{name}", "step": { + "manual_port_config": { + "data": { + "baudrate": "\u0441\u043a\u043e\u0440\u043e\u0441\u0442 \u043d\u0430 \u043f\u043e\u0440\u0442\u0430" + } + }, "upload_manual_backup": { "data": { "uploaded_backup_file": "\u041a\u0430\u0447\u0432\u0430\u043d\u0435 \u043d\u0430 \u0444\u0430\u0439\u043b" From 0227f2cd67cebc635b270fb0ad7dbdc6f5eb3ed4 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sat, 10 Sep 2022 04:31:10 +0200 Subject: [PATCH 247/955] Bump aioecowitt to 2022.09.1 (#78142) --- homeassistant/components/ecowitt/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ecowitt/manifest.json b/homeassistant/components/ecowitt/manifest.json index 348df17b0cd..080b7d5af72 100644 --- a/homeassistant/components/ecowitt/manifest.json +++ b/homeassistant/components/ecowitt/manifest.json @@ -3,7 +3,7 @@ "name": "Ecowitt", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ecowitt", - "requirements": ["aioecowitt==2022.08.3"], + "requirements": ["aioecowitt==2022.09.1"], "codeowners": ["@pvizeli"], "iot_class": "local_push" } diff --git a/requirements_all.txt b/requirements_all.txt index 6853f91defb..349057ed16c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -147,7 +147,7 @@ aioeafm==0.1.2 aioeagle==1.1.0 # homeassistant.components.ecowitt -aioecowitt==2022.08.3 +aioecowitt==2022.09.1 # homeassistant.components.emonitor aioemonitor==1.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c58ddd1e686..267ac498cdd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -134,7 +134,7 @@ aioeafm==0.1.2 aioeagle==1.1.0 # homeassistant.components.ecowitt -aioecowitt==2022.08.3 +aioecowitt==2022.09.1 # homeassistant.components.emonitor aioemonitor==1.0.5 From 28f4a5b7a22dee270b0117c7fd8a1fc322cb2791 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Sat, 10 Sep 2022 03:31:51 +0100 Subject: [PATCH 248/955] Add missing moisture sensor to xiaomi_ble (#78160) --- .../components/xiaomi_ble/binary_sensor.py | 3 ++ .../xiaomi_ble/test_binary_sensor.py | 45 +++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/homeassistant/components/xiaomi_ble/binary_sensor.py b/homeassistant/components/xiaomi_ble/binary_sensor.py index 448b1f176e5..4de491ab9dd 100644 --- a/homeassistant/components/xiaomi_ble/binary_sensor.py +++ b/homeassistant/components/xiaomi_ble/binary_sensor.py @@ -39,6 +39,9 @@ BINARY_SENSOR_DESCRIPTIONS = { key=XiaomiBinarySensorDeviceClass.SMOKE, device_class=BinarySensorDeviceClass.SMOKE, ), + XiaomiBinarySensorDeviceClass.MOISTURE: BinarySensorEntityDescription( + key=XiaomiBinarySensorDeviceClass.MOISTURE, + ), } diff --git a/tests/components/xiaomi_ble/test_binary_sensor.py b/tests/components/xiaomi_ble/test_binary_sensor.py index 03e3d52d783..390c8d4b579 100644 --- a/tests/components/xiaomi_ble/test_binary_sensor.py +++ b/tests/components/xiaomi_ble/test_binary_sensor.py @@ -52,3 +52,48 @@ async def test_smoke_sensor(hass): assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() + + +async def test_moisture(hass): + """Make sure that formldehyde sensors are correctly mapped.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="C4:7C:8D:6A:3E:7A", + ) + entry.add_to_hass(hass) + + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + + # WARNING: This test data is synthetic, rather than captured from a real device + # obj type is 0x1014, payload len is 0x2 and payload is 0xf400 + saved_callback( + make_advertisement( + "C4:7C:8D:6A:3E:7A", b"q \x5d\x01iz>j\x8d|\xc4\r\x14\x10\x02\xf4\x00" + ), + BluetoothChange.ADVERTISEMENT, + ) + + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 1 + + sensor = hass.states.get("binary_sensor.smart_flower_pot_6a3e7a_moisture") + sensor_attr = sensor.attributes + assert sensor.state == "on" + assert sensor_attr[ATTR_FRIENDLY_NAME] == "Smart Flower Pot 6A3E7A Moisture" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() From a1ec9c61478804d584518af5b9ee37d79754de85 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 9 Sep 2022 21:32:17 -0500 Subject: [PATCH 249/955] Bump pySwitchbot to 0.19.1 (#78168) --- 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 e311295e52c..7c5b0d9bc8d 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.19.0"], + "requirements": ["PySwitchbot==0.19.1"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 349057ed16c..4dc0db398ee 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.19.0 +PySwitchbot==0.19.1 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 267ac498cdd..5a92a8f10ac 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.19.0 +PySwitchbot==0.19.1 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From 39f40011ccebcfa8601b8b6b124a647c7312b12e Mon Sep 17 00:00:00 2001 From: Ernst Klamer Date: Sat, 10 Sep 2022 04:43:25 +0200 Subject: [PATCH 250/955] Add BTHome binary sensors (#78151) --- homeassistant/components/bthome/__init__.py | 2 +- .../components/bthome/binary_sensor.py | 93 ++++++++++++++ homeassistant/components/bthome/manifest.json | 2 +- homeassistant/components/bthome/sensor.py | 4 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/bthome/test_binary_sensor.py | 113 ++++++++++++++++++ 7 files changed, 212 insertions(+), 6 deletions(-) create mode 100644 homeassistant/components/bthome/binary_sensor.py create mode 100644 tests/components/bthome/test_binary_sensor.py diff --git a/homeassistant/components/bthome/__init__.py b/homeassistant/components/bthome/__init__.py index 93ebd7b288f..539aa112a06 100644 --- a/homeassistant/components/bthome/__init__.py +++ b/homeassistant/components/bthome/__init__.py @@ -19,7 +19,7 @@ from homeassistant.core import HomeAssistant from .const import DOMAIN -PLATFORMS: list[Platform] = [Platform.SENSOR] +PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/bthome/binary_sensor.py b/homeassistant/components/bthome/binary_sensor.py new file mode 100644 index 00000000000..9a5ffb94afd --- /dev/null +++ b/homeassistant/components/bthome/binary_sensor.py @@ -0,0 +1,93 @@ +"""Support for BTHome binary sensors.""" +from __future__ import annotations + +from typing import Optional + +from bthome_ble import BTHOME_BINARY_SENSORS, SensorUpdate + +from homeassistant import config_entries +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothDataProcessor, + PassiveBluetoothDataUpdate, + PassiveBluetoothProcessorCoordinator, + PassiveBluetoothProcessorEntity, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .device import device_key_to_bluetooth_entity_key, sensor_device_info_to_hass + +BINARY_SENSOR_DESCRIPTIONS = {} +for key in BTHOME_BINARY_SENSORS: + # Not all BTHome device classes are available in Home Assistant + DEV_CLASS: str | None = key + try: + BinarySensorDeviceClass(key) + except ValueError: + DEV_CLASS = None + BINARY_SENSOR_DESCRIPTIONS[key] = BinarySensorEntityDescription( + key=key, + device_class=DEV_CLASS, + ) + + +def sensor_update_to_bluetooth_data_update( + sensor_update: SensorUpdate, +) -> PassiveBluetoothDataUpdate: + """Convert a binary 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): BINARY_SENSOR_DESCRIPTIONS[ + description.device_key.key + ] + for device_key, description in sensor_update.binary_entity_descriptions.items() + }, + entity_data={ + device_key_to_bluetooth_entity_key(device_key): sensor_values.native_value + for device_key, sensor_values in sensor_update.binary_entity_values.items() + }, + entity_names={ + device_key_to_bluetooth_entity_key(device_key): sensor_values.name + for device_key, sensor_values in sensor_update.binary_entity_values.items() + }, + ) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the BTHome BLE binary 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( + BTHomeBluetoothBinarySensorEntity, async_add_entities + ) + ) + entry.async_on_unload(coordinator.async_register_processor(processor)) + + +class BTHomeBluetoothBinarySensorEntity( + PassiveBluetoothProcessorEntity[PassiveBluetoothDataProcessor[Optional[bool]]], + BinarySensorEntity, +): + """Representation of a BTHome binary sensor.""" + + @property + def is_on(self) -> bool | None: + """Return the native value.""" + return self.processor.entity_data.get(self.entity_key) diff --git a/homeassistant/components/bthome/manifest.json b/homeassistant/components/bthome/manifest.json index 597d52c72e4..5b58581226b 100644 --- a/homeassistant/components/bthome/manifest.json +++ b/homeassistant/components/bthome/manifest.json @@ -13,7 +13,7 @@ "service_data_uuid": "0000181e-0000-1000-8000-00805f9b34fb" } ], - "requirements": ["bthome-ble==1.0.0"], + "requirements": ["bthome-ble==1.2.0"], "dependencies": ["bluetooth"], "codeowners": ["@Ernst79"], "iot_class": "local_push" diff --git a/homeassistant/components/bthome/sensor.py b/homeassistant/components/bthome/sensor.py index a0068596b01..cfb516d07b9 100644 --- a/homeassistant/components/bthome/sensor.py +++ b/homeassistant/components/bthome/sensor.py @@ -148,9 +148,9 @@ SENSOR_DESCRIPTIONS = { state_class=SensorStateClass.MEASUREMENT, ), # Used for moisture sensor - (None, Units.PERCENTAGE,): SensorEntityDescription( + (DeviceClass.MOISTURE, Units.PERCENTAGE): SensorEntityDescription( key=f"{DeviceClass.MOISTURE}_{Units.PERCENTAGE}", - device_class=None, + device_class=SensorDeviceClass.MOISTURE, native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, ), diff --git a/requirements_all.txt b/requirements_all.txt index 4dc0db398ee..2051f1f3009 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -464,7 +464,7 @@ bsblan==0.5.0 bt_proximity==0.2.1 # homeassistant.components.bthome -bthome-ble==1.0.0 +bthome-ble==1.2.0 # homeassistant.components.bt_home_hub_5 bthomehub5-devicelist==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5a92a8f10ac..0be6ec7afed 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -365,7 +365,7 @@ brunt==1.2.0 bsblan==0.5.0 # homeassistant.components.bthome -bthome-ble==1.0.0 +bthome-ble==1.2.0 # homeassistant.components.buienradar buienradar==1.0.5 diff --git a/tests/components/bthome/test_binary_sensor.py b/tests/components/bthome/test_binary_sensor.py new file mode 100644 index 00000000000..fc7b128d124 --- /dev/null +++ b/tests/components/bthome/test_binary_sensor.py @@ -0,0 +1,113 @@ +"""Test BTHome binary sensors.""" + +import logging +from unittest.mock import patch + +import pytest + +from homeassistant.components.bluetooth import BluetoothChange +from homeassistant.components.bthome.const import DOMAIN +from homeassistant.const import ATTR_FRIENDLY_NAME, STATE_OFF, STATE_ON + +from . import make_advertisement + +from tests.common import MockConfigEntry + +_LOGGER = logging.getLogger(__name__) + + +@pytest.mark.parametrize( + "mac_address, advertisement, bind_key, result", + [ + ( + "A4:C1:38:8D:18:B2", + make_advertisement( + "A4:C1:38:8D:18:B2", + b"\x02\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_advertisement( + "A4:C1:38:8D:18:B2", + b"\x02\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_advertisement( + "A4:C1:38:8D:18:B2", + b"\x02\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_binary_sensors( + hass, + mac_address, + advertisement, + bind_key, + result, +): + """Test the different binary sensors.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=mac_address, + data={"bindkey": bind_key}, + ) + entry.add_to_hass(hass) + + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + + saved_callback( + advertisement, + BluetoothChange.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() From 68b511737dafd366a65e3b498cfe9bd6d9e995a3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 9 Sep 2022 22:20:39 -0500 Subject: [PATCH 251/955] Bump aiohomekit to 1.5.3 (#78170) --- 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 08eac050c98..1055779d7cc 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==1.5.2"], + "requirements": ["aiohomekit==1.5.3"], "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 2051f1f3009..97529252410 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==1.5.2 +aiohomekit==1.5.3 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0be6ec7afed..0a4fd0d4349 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==1.5.2 +aiohomekit==1.5.3 # homeassistant.components.emulated_hue # homeassistant.components.http From b2692ecc80b5cafa625064665c273295b1e7d5bc Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 9 Sep 2022 23:32:14 -0400 Subject: [PATCH 252/955] Fix ecowitt typing (#78171) --- homeassistant/components/ecowitt/binary_sensor.py | 2 +- homeassistant/components/ecowitt/diagnostics.py | 4 ++-- homeassistant/components/ecowitt/sensor.py | 5 ++++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/ecowitt/binary_sensor.py b/homeassistant/components/ecowitt/binary_sensor.py index e487009d74b..fbe2e017339 100644 --- a/homeassistant/components/ecowitt/binary_sensor.py +++ b/homeassistant/components/ecowitt/binary_sensor.py @@ -68,4 +68,4 @@ class EcowittBinarySensorEntity(EcowittEntity, BinarySensorEntity): @property def is_on(self) -> bool: """Return true if the binary sensor is on.""" - return self.ecowitt.value > 0 + return bool(self.ecowitt.value) diff --git a/homeassistant/components/ecowitt/diagnostics.py b/homeassistant/components/ecowitt/diagnostics.py index d02a5dadbcc..96fa020667b 100644 --- a/homeassistant/components/ecowitt/diagnostics.py +++ b/homeassistant/components/ecowitt/diagnostics.py @@ -25,13 +25,13 @@ async def async_get_device_diagnostics( "device": { "name": station.station, "model": station.model, - "frequency": station.frequency, + "frequency": station.frequence, "version": station.version, }, "raw": ecowitt.last_values[station_id], "sensors": { sensor.key: sensor.value - for sensor in station.sensors + for sensor in ecowitt.sensors.values() if sensor.station.key == station_id }, } diff --git a/homeassistant/components/ecowitt/sensor.py b/homeassistant/components/ecowitt/sensor.py index 843dc700dc0..bb580b6d4b7 100644 --- a/homeassistant/components/ecowitt/sensor.py +++ b/homeassistant/components/ecowitt/sensor.py @@ -1,5 +1,8 @@ """Support for Ecowitt Weather Stations.""" +from __future__ import annotations + import dataclasses +from datetime import datetime from typing import Final from aioecowitt import EcoWittListener, EcoWittSensor, EcoWittSensorTypes @@ -242,6 +245,6 @@ class EcowittSensorEntity(EcowittEntity, SensorEntity): self.entity_description = description @property - def native_value(self) -> StateType: + def native_value(self) -> StateType | datetime: """Return the state of the sensor.""" return self.ecowitt.value From a877c8030f87d9dc82fc3a3f6c5a17cc88315589 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sat, 10 Sep 2022 17:02:36 +0200 Subject: [PATCH 253/955] Add dependencies to ecowitt (#78187) --- homeassistant/components/ecowitt/manifest.json | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/ecowitt/manifest.json b/homeassistant/components/ecowitt/manifest.json index 080b7d5af72..224b4440e36 100644 --- a/homeassistant/components/ecowitt/manifest.json +++ b/homeassistant/components/ecowitt/manifest.json @@ -3,6 +3,7 @@ "name": "Ecowitt", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ecowitt", + "dependencies": ["webhook"], "requirements": ["aioecowitt==2022.09.1"], "codeowners": ["@pvizeli"], "iot_class": "local_push" From c406e4defe01a139ad57a5dc2ab6ee20b6ec932a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 10 Sep 2022 12:30:30 -0500 Subject: [PATCH 254/955] Bump led-ble to 0.8.3 (#78188) * Bump led-ble to 0.8.0 Fixes setup when the previous shutdown was not clean and the device is still connected * bump again * bump again * bump again --- homeassistant/components/led_ble/__init__.py | 6 ++++-- homeassistant/components/led_ble/light.py | 4 ++-- homeassistant/components/led_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 9 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/led_ble/__init__.py b/homeassistant/components/led_ble/__init__.py index d885b3eb950..5c454c6df7c 100644 --- a/homeassistant/components/led_ble/__init__.py +++ b/homeassistant/components/led_ble/__init__.py @@ -6,7 +6,7 @@ from datetime import timedelta import logging import async_timeout -from led_ble import BLEAK_EXCEPTIONS, LEDBLE +from led_ble import BLEAK_EXCEPTIONS, LEDBLE, get_device from homeassistant.components import bluetooth from homeassistant.components.bluetooth.match import ADDRESS, BluetoothCallbackMatcher @@ -27,7 +27,9 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up LED BLE from a config entry.""" address: str = entry.data[CONF_ADDRESS] - ble_device = bluetooth.async_ble_device_from_address(hass, address.upper(), True) + ble_device = bluetooth.async_ble_device_from_address( + hass, address.upper(), True + ) or await get_device(address) if not ble_device: raise ConfigEntryNotReady( f"Could not find LED BLE device with address {address}" diff --git a/homeassistant/components/led_ble/light.py b/homeassistant/components/led_ble/light.py index a18ab812b19..4a8ff3f01af 100644 --- a/homeassistant/components/led_ble/light.py +++ b/homeassistant/components/led_ble/light.py @@ -48,12 +48,12 @@ class LEDBLEEntity(CoordinatorEntity, LightEntity): """Initialize an ledble light.""" super().__init__(coordinator) self._device = device - self._attr_unique_id = device._address + self._attr_unique_id = device.address self._attr_device_info = DeviceInfo( name=name, model=hex(device.model_num), sw_version=hex(device.version_num), - connections={(dr.CONNECTION_BLUETOOTH, device._address)}, + connections={(dr.CONNECTION_BLUETOOTH, device.address)}, ) self._async_update_attrs() diff --git a/homeassistant/components/led_ble/manifest.json b/homeassistant/components/led_ble/manifest.json index 1dd289daa4d..261d27726e5 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/ble_ble", - "requirements": ["led-ble==0.7.1"], + "requirements": ["led-ble==0.8.3"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "bluetooth": [ diff --git a/requirements_all.txt b/requirements_all.txt index 97529252410..da96aa27157 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -968,7 +968,7 @@ lakeside==0.12 laundrify_aio==1.1.2 # homeassistant.components.led_ble -led-ble==0.7.1 +led-ble==0.8.3 # homeassistant.components.foscam libpyfoscam==1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0a4fd0d4349..0f621a0deca 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -706,7 +706,7 @@ lacrosse-view==0.0.9 laundrify_aio==1.1.2 # homeassistant.components.led_ble -led-ble==0.7.1 +led-ble==0.8.3 # homeassistant.components.foscam libpyfoscam==1.0 From 1e302c12fff5d37f2c460a3ea31e5949a050273a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 10 Sep 2022 12:32:29 -0500 Subject: [PATCH 255/955] Fix Yale Access Bluetooth not setting up when already connected at startup (#78199) --- homeassistant/components/yalexs_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/yalexs_ble/manifest.json b/homeassistant/components/yalexs_ble/manifest.json index de0034c755f..da4bf1cf6d2 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.6.4"], + "requirements": ["yalexs-ble==1.7.1"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "bluetooth": [{ "manufacturer_id": 465 }], diff --git a/requirements_all.txt b/requirements_all.txt index da96aa27157..e536cd7a8e4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2548,7 +2548,7 @@ xs1-api-client==3.0.0 yalesmartalarmclient==0.3.9 # homeassistant.components.yalexs_ble -yalexs-ble==1.6.4 +yalexs-ble==1.7.1 # homeassistant.components.august yalexs==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0f621a0deca..6fb82f56a75 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1752,7 +1752,7 @@ xmltodict==0.13.0 yalesmartalarmclient==0.3.9 # homeassistant.components.yalexs_ble -yalexs-ble==1.6.4 +yalexs-ble==1.7.1 # homeassistant.components.august yalexs==1.2.1 From dbcb269111f6dc5d9e0f0ed4e1c336bfa5c5c549 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 10 Sep 2022 12:32:38 -0500 Subject: [PATCH 256/955] Fix switchbot not setting up when already connected at startup (#78198) --- homeassistant/components/switchbot/__init__.py | 2 +- homeassistant/components/switchbot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/switchbot/__init__.py b/homeassistant/components/switchbot/__init__.py index 345190d8933..d1f68047a83 100644 --- a/homeassistant/components/switchbot/__init__.py +++ b/homeassistant/components/switchbot/__init__.py @@ -84,7 +84,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: address: str = entry.data[CONF_ADDRESS] ble_device = bluetooth.async_ble_device_from_address( hass, address.upper(), connectable - ) + ) or await switchbot.get_device(address) if not ble_device: raise ConfigEntryNotReady( f"Could not find Switchbot {sensor_type} with address {address}" diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index 7c5b0d9bc8d..f35f3cddf72 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.19.1"], + "requirements": ["PySwitchbot==0.19.2"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index e536cd7a8e4..cf17af30bcd 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.19.1 +PySwitchbot==0.19.2 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6fb82f56a75..870f24c1543 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.19.1 +PySwitchbot==0.19.2 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From 17ed090f98c910212803bef28f651abfc6bb9e60 Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Sat, 10 Sep 2022 20:35:26 +0300 Subject: [PATCH 257/955] Fix sending notification to multiple targets in Pushover (#78111) fix sending to mulitple targets --- homeassistant/components/pushover/notify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/pushover/notify.py b/homeassistant/components/pushover/notify.py index dd711c80aaf..bcf47264108 100644 --- a/homeassistant/components/pushover/notify.py +++ b/homeassistant/components/pushover/notify.py @@ -128,7 +128,7 @@ class PushoverNotificationService(BaseNotificationService): self.pushover.send_message( self._user_key, message, - kwargs.get(ATTR_TARGET), + ",".join(kwargs.get(ATTR_TARGET, [])), title, url, url_title, From f19af72895c25557820d75a06e8312e5e146bbae Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Sat, 10 Sep 2022 13:56:01 -0400 Subject: [PATCH 258/955] Bump ZHA dependencies (#78201) --- homeassistant/components/zha/manifest.json | 4 ++-- requirements_all.txt | 4 ++-- requirements_test_all.txt | 4 ++-- tests/components/zha/test_config_flow.py | 4 +++- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 2e35427a70c..419e1c1452b 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -8,8 +8,8 @@ "pyserial==3.5", "pyserial-asyncio==0.6", "zha-quirks==0.0.79", - "zigpy-deconz==0.18.0", - "zigpy==0.50.2", + "zigpy-deconz==0.18.1", + "zigpy==0.50.3", "zigpy-xbee==0.15.0", "zigpy-zigate==0.9.2", "zigpy-znp==0.8.2" diff --git a/requirements_all.txt b/requirements_all.txt index cf17af30bcd..900306d07a6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2584,7 +2584,7 @@ zhong_hong_hvac==1.0.9 ziggo-mediabox-xl==1.1.0 # homeassistant.components.zha -zigpy-deconz==0.18.0 +zigpy-deconz==0.18.1 # homeassistant.components.zha zigpy-xbee==0.15.0 @@ -2596,7 +2596,7 @@ zigpy-zigate==0.9.2 zigpy-znp==0.8.2 # homeassistant.components.zha -zigpy==0.50.2 +zigpy==0.50.3 # homeassistant.components.zoneminder zm-py==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 870f24c1543..cce8935b8e7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1773,7 +1773,7 @@ zeroconf==0.39.1 zha-quirks==0.0.79 # homeassistant.components.zha -zigpy-deconz==0.18.0 +zigpy-deconz==0.18.1 # homeassistant.components.zha zigpy-xbee==0.15.0 @@ -1785,7 +1785,7 @@ zigpy-zigate==0.9.2 zigpy-znp==0.8.2 # homeassistant.components.zha -zigpy==0.50.2 +zigpy==0.50.3 # homeassistant.components.zwave_js zwave-js-server-python==0.41.1 diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index d65732a6ab8..5fc4b232634 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -2,11 +2,12 @@ import copy import json -from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch +from unittest.mock import AsyncMock, MagicMock, PropertyMock, create_autospec, patch import uuid import pytest import serial.tools.list_ports +from zigpy.backups import BackupManager import zigpy.config from zigpy.config import CONF_DEVICE, CONF_DEVICE_PATH from zigpy.exceptions import NetworkNotFormed @@ -49,6 +50,7 @@ def disable_platform_only(): def mock_app(): """Mock zigpy app interface.""" mock_app = AsyncMock() + mock_app.backups = create_autospec(BackupManager, instance=True) mock_app.backups.backups = [] with patch( From a3ec28ea797dd470a3649451fecaf869105c1650 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 10 Sep 2022 13:21:10 -0500 Subject: [PATCH 259/955] Close stale switchbot connections at setup time (#78202) --- homeassistant/components/switchbot/__init__.py | 2 ++ homeassistant/components/switchbot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switchbot/__init__.py b/homeassistant/components/switchbot/__init__.py index d1f68047a83..7307187bf54 100644 --- a/homeassistant/components/switchbot/__init__.py +++ b/homeassistant/components/switchbot/__init__.py @@ -89,6 +89,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryNotReady( f"Could not find Switchbot {sensor_type} with address {address}" ) + + await switchbot.close_stale_connections(ble_device) cls = CLASS_BY_DEVICE.get(sensor_type, switchbot.SwitchbotDevice) device = cls( device=ble_device, diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index f35f3cddf72..c83e575bd49 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.19.2"], + "requirements": ["PySwitchbot==0.19.3"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 900306d07a6..49575f7590f 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.19.2 +PySwitchbot==0.19.3 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cce8935b8e7..d382386ad1b 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.19.2 +PySwitchbot==0.19.3 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From 27e8e40968c3d7ecc189802725a7c26681da3d4b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 10 Sep 2022 13:36:44 -0500 Subject: [PATCH 260/955] Bump aiohomekit to 1.5.4 to handle stale ble connections at startup (#78203) --- 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 1055779d7cc..b71156c9bc7 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==1.5.3"], + "requirements": ["aiohomekit==1.5.4"], "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 49575f7590f..03d2e5af65d 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==1.5.3 +aiohomekit==1.5.4 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d382386ad1b..50c356aca78 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==1.5.3 +aiohomekit==1.5.4 # homeassistant.components.emulated_hue # homeassistant.components.http From a1aac4a2e9c84fedd64215cb90a6e997a75146c2 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 10 Sep 2022 22:12:37 +0200 Subject: [PATCH 261/955] Use new media player enums in esphome (#78099) --- .../components/esphome/media_player.py | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/esphome/media_player.py b/homeassistant/components/esphome/media_player.py index 17635157754..6b72d366754 100644 --- a/homeassistant/components/esphome/media_player.py +++ b/homeassistant/components/esphome/media_player.py @@ -7,21 +7,19 @@ from aioesphomeapi import ( MediaPlayerCommand, MediaPlayerEntityState, MediaPlayerInfo, - MediaPlayerState, + MediaPlayerState as EspMediaPlayerState, ) from homeassistant.components import media_source from homeassistant.components.media_player import ( + BrowseMedia, MediaPlayerDeviceClass, MediaPlayerEntity, -) -from homeassistant.components.media_player.browse_media import ( - BrowseMedia, + MediaPlayerEntityFeature, + MediaPlayerState, async_process_play_media_url, ) -from homeassistant.components.media_player.const import MediaPlayerEntityFeature from homeassistant.config_entries import ConfigEntry -from homeassistant.const import STATE_IDLE, STATE_PAUSED, STATE_PLAYING from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -50,11 +48,11 @@ async def async_setup_entry( ) -_STATES: EsphomeEnumMapper[MediaPlayerState, str] = EsphomeEnumMapper( +_STATES: EsphomeEnumMapper[EspMediaPlayerState, MediaPlayerState] = EsphomeEnumMapper( { - MediaPlayerState.IDLE: STATE_IDLE, - MediaPlayerState.PLAYING: STATE_PLAYING, - MediaPlayerState.PAUSED: STATE_PAUSED, + EspMediaPlayerState.IDLE: MediaPlayerState.IDLE, + EspMediaPlayerState.PLAYING: MediaPlayerState.PLAYING, + EspMediaPlayerState.PAUSED: MediaPlayerState.PAUSED, } ) @@ -68,7 +66,7 @@ class EsphomeMediaPlayer( @property # type: ignore[misc] @esphome_state_property - def state(self) -> str | None: + def state(self) -> MediaPlayerState | None: """Return current state.""" return _STATES.from_esphome(self._state.state) From 64fd84bd881f2535f461222fda3f1e55469b1aa2 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 10 Sep 2022 22:16:04 +0200 Subject: [PATCH 262/955] Use new media player enums in frontier_silicon (#78101) --- .../frontier_silicon/media_player.py | 33 +++++++------------ 1 file changed, 12 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/frontier_silicon/media_player.py b/homeassistant/components/frontier_silicon/media_player.py index 87c7c4036f2..309074c1b26 100644 --- a/homeassistant/components/frontier_silicon/media_player.py +++ b/homeassistant/components/frontier_silicon/media_player.py @@ -15,19 +15,10 @@ from homeassistant.components.media_player import ( PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, + MediaType, ) -from homeassistant.components.media_player.const import MEDIA_TYPE_MUSIC -from homeassistant.const import ( - CONF_HOST, - CONF_NAME, - CONF_PASSWORD, - CONF_PORT, - STATE_IDLE, - STATE_OFF, - STATE_OPENING, - STATE_PAUSED, - STATE_PLAYING, -) +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import DeviceInfo @@ -89,7 +80,7 @@ async def async_setup_platform( class AFSAPIDevice(MediaPlayerEntity): """Representation of a Frontier Silicon device on the network.""" - _attr_media_content_type: str = MEDIA_TYPE_MUSIC + _attr_media_content_type: str = MediaType.MUSIC _attr_supported_features = ( MediaPlayerEntityFeature.PAUSE @@ -132,14 +123,14 @@ class AFSAPIDevice(MediaPlayerEntity): if await afsapi.get_power(): status = await afsapi.get_play_status() self._attr_state = { - PlayState.PLAYING: STATE_PLAYING, - PlayState.PAUSED: STATE_PAUSED, - PlayState.STOPPED: STATE_IDLE, - PlayState.LOADING: STATE_OPENING, - None: STATE_IDLE, + PlayState.PLAYING: MediaPlayerState.PLAYING, + PlayState.PAUSED: MediaPlayerState.PAUSED, + PlayState.STOPPED: MediaPlayerState.IDLE, + PlayState.LOADING: MediaPlayerState.BUFFERING, + None: MediaPlayerState.IDLE, }.get(status) else: - self._attr_state = STATE_OFF + self._attr_state = MediaPlayerState.OFF except FSConnectionError: if self._attr_available: _LOGGER.warning( @@ -186,7 +177,7 @@ class AFSAPIDevice(MediaPlayerEntity): if not self._max_volume: self._max_volume = int(await afsapi.get_volume_steps() or 1) - 1 - if self._attr_state != STATE_OFF: + if self._attr_state != MediaPlayerState.OFF: info_name = await afsapi.get_play_name() info_text = await afsapi.get_play_text() @@ -251,7 +242,7 @@ class AFSAPIDevice(MediaPlayerEntity): async def async_media_play_pause(self) -> None: """Send play/pause command.""" - if self._attr_state == STATE_PLAYING: + if self._attr_state == MediaPlayerState.PLAYING: await self.fs_device.pause() else: await self.fs_device.play() From a2559b48ce67dad166e1274416e8ebd404f8317b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 10 Sep 2022 22:17:38 +0200 Subject: [PATCH 263/955] Use new media player enums in group (#78104) --- homeassistant/components/group/media_player.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/group/media_player.py b/homeassistant/components/group/media_player.py index 60cb37f46ba..cbce44a359a 100644 --- a/homeassistant/components/group/media_player.py +++ b/homeassistant/components/group/media_player.py @@ -18,6 +18,7 @@ from homeassistant.components.media_player import ( SERVICE_PLAY_MEDIA, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -37,8 +38,6 @@ from homeassistant.const import ( SERVICE_TURN_ON, SERVICE_VOLUME_MUTE, SERVICE_VOLUME_SET, - STATE_OFF, - STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN, ) @@ -408,13 +407,13 @@ class MediaPlayerGroup(MediaPlayerEntity): # Set as unknown if all members are unknown or unavailable self._state = None else: - off_values = (STATE_OFF, STATE_UNAVAILABLE, STATE_UNKNOWN) + off_values = {MediaPlayerState.OFF, STATE_UNAVAILABLE, STATE_UNKNOWN} if states.count(states[0]) == len(states): self._state = states[0] elif any(state for state in states if state not in off_values): - self._state = STATE_ON + self._state = MediaPlayerState.ON else: - self._state = STATE_OFF + self._state = MediaPlayerState.OFF supported_features = 0 if self._features[KEY_CLEAR_PLAYLIST]: From d0605d3a59fe5d791f466e65e5b08563e0befbc5 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 10 Sep 2022 22:22:18 +0200 Subject: [PATCH 264/955] Use new media player enums in kodi (#78106) --- homeassistant/components/kodi/media_player.py | 102 ++++++++---------- 1 file changed, 42 insertions(+), 60 deletions(-) diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index a41738eaa3e..bdbac455dd1 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -16,28 +16,14 @@ import voluptuous as vol from homeassistant.components import media_source from homeassistant.components.media_player import ( PLATFORM_SCHEMA, + BrowseError, + BrowseMedia, MediaPlayerEntity, MediaPlayerEntityFeature, -) -from homeassistant.components.media_player.browse_media import ( - BrowseMedia, + MediaPlayerState, + MediaType, async_process_play_media_url, ) -from homeassistant.components.media_player.const import ( - MEDIA_TYPE_ALBUM, - MEDIA_TYPE_ARTIST, - MEDIA_TYPE_CHANNEL, - MEDIA_TYPE_EPISODE, - MEDIA_TYPE_MOVIE, - MEDIA_TYPE_MUSIC, - MEDIA_TYPE_PLAYLIST, - MEDIA_TYPE_SEASON, - MEDIA_TYPE_TRACK, - MEDIA_TYPE_TVSHOW, - MEDIA_TYPE_URL, - MEDIA_TYPE_VIDEO, -) -from homeassistant.components.media_player.errors import BrowseError from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, @@ -50,10 +36,6 @@ from homeassistant.const import ( CONF_TIMEOUT, CONF_USERNAME, EVENT_HOMEASSISTANT_STARTED, - STATE_IDLE, - STATE_OFF, - STATE_PAUSED, - STATE_PLAYING, ) from homeassistant.core import CoreState, HomeAssistant, callback from homeassistant.helpers import ( @@ -112,28 +94,28 @@ WEBSOCKET_WATCHDOG_INTERVAL = timedelta(seconds=10) # https://github.com/xbmc/xbmc/blob/master/xbmc/media/MediaType.h MEDIA_TYPES = { - "music": MEDIA_TYPE_MUSIC, - "artist": MEDIA_TYPE_MUSIC, - "album": MEDIA_TYPE_MUSIC, - "song": MEDIA_TYPE_MUSIC, - "video": MEDIA_TYPE_VIDEO, - "set": MEDIA_TYPE_PLAYLIST, - "musicvideo": MEDIA_TYPE_VIDEO, - "movie": MEDIA_TYPE_MOVIE, - "tvshow": MEDIA_TYPE_TVSHOW, - "season": MEDIA_TYPE_TVSHOW, - "episode": MEDIA_TYPE_TVSHOW, + "music": MediaType.MUSIC, + "artist": MediaType.MUSIC, + "album": MediaType.MUSIC, + "song": MediaType.MUSIC, + "video": MediaType.VIDEO, + "set": MediaType.PLAYLIST, + "musicvideo": MediaType.VIDEO, + "movie": MediaType.MOVIE, + "tvshow": MediaType.TVSHOW, + "season": MediaType.TVSHOW, + "episode": MediaType.TVSHOW, # Type 'channel' is used for radio or tv streams from pvr - "channel": MEDIA_TYPE_CHANNEL, + "channel": MediaType.CHANNEL, # Type 'audio' is used for audio media, that Kodi couldn't scroblle - "audio": MEDIA_TYPE_MUSIC, + "audio": MediaType.MUSIC, } -MAP_KODI_MEDIA_TYPES = { - MEDIA_TYPE_MOVIE: "movieid", - MEDIA_TYPE_EPISODE: "episodeid", - MEDIA_TYPE_SEASON: "seasonid", - MEDIA_TYPE_TVSHOW: "tvshowid", +MAP_KODI_MEDIA_TYPES: dict[MediaType | str, str] = { + MediaType.MOVIE: "movieid", + MediaType.EPISODE: "episodeid", + MediaType.SEASON: "seasonid", + MediaType.TVSHOW: "tvshowid", } @@ -259,7 +241,7 @@ def cmd( await func(obj, *args, **kwargs) except (TransportError, ProtocolError) as exc: # If Kodi is off, we expect calls to fail. - if obj.state == STATE_OFF: + if obj.state == MediaPlayerState.OFF: log_function = _LOGGER.debug else: log_function = _LOGGER.error @@ -380,18 +362,18 @@ class KodiEntity(MediaPlayerEntity): ) @property - def state(self): + def state(self) -> MediaPlayerState: """Return the state of the device.""" if self._kodi_is_off: - return STATE_OFF + return MediaPlayerState.OFF if self._no_active_players: - return STATE_IDLE + return MediaPlayerState.IDLE if self._properties["speed"] == 0: - return STATE_PAUSED + return MediaPlayerState.PAUSED - return STATE_PLAYING + return MediaPlayerState.PLAYING async def async_added_to_hass(self) -> None: """Connect the websocket if needed.""" @@ -703,11 +685,11 @@ class KodiEntity(MediaPlayerEntity): @cmd async def async_play_media( - self, media_type: str, media_id: str, **kwargs: Any + self, media_type: MediaType | str, media_id: str, **kwargs: Any ) -> None: """Send the play_media command to the media player.""" if media_source.is_media_source_id(media_id): - media_type = MEDIA_TYPE_URL + media_type = MediaType.URL play_item = await media_source.async_resolve_media( self.hass, media_id, self.entity_id ) @@ -715,27 +697,27 @@ class KodiEntity(MediaPlayerEntity): media_type_lower = media_type.lower() - if media_type_lower == MEDIA_TYPE_CHANNEL: + if media_type_lower == MediaType.CHANNEL: await self._kodi.play_channel(int(media_id)) - elif media_type_lower == MEDIA_TYPE_PLAYLIST: + elif media_type_lower == MediaType.PLAYLIST: await self._kodi.play_playlist(int(media_id)) elif media_type_lower == "file": await self._kodi.play_file(media_id) elif media_type_lower == "directory": await self._kodi.play_directory(media_id) elif media_type_lower in [ - MEDIA_TYPE_ARTIST, - MEDIA_TYPE_ALBUM, - MEDIA_TYPE_TRACK, + MediaType.ARTIST, + MediaType.ALBUM, + MediaType.TRACK, ]: await self.async_clear_playlist() await self.async_add_to_playlist(media_type_lower, media_id) await self._kodi.play_playlist(0) elif media_type_lower in [ - MEDIA_TYPE_MOVIE, - MEDIA_TYPE_EPISODE, - MEDIA_TYPE_SEASON, - MEDIA_TYPE_TVSHOW, + MediaType.MOVIE, + MediaType.EPISODE, + MediaType.SEASON, + MediaType.TVSHOW, ]: await self._kodi.play_item( {MAP_KODI_MEDIA_TYPES[media_type_lower]: int(media_id)} @@ -796,11 +778,11 @@ class KodiEntity(MediaPlayerEntity): async def async_add_to_playlist(self, media_type, media_id): """Add media item to default playlist (i.e. playlistid=0).""" - if media_type == MEDIA_TYPE_ARTIST: + if media_type == MediaType.ARTIST: await self._kodi.add_artist_to_playlist(int(media_id)) - elif media_type == MEDIA_TYPE_ALBUM: + elif media_type == MediaType.ALBUM: await self._kodi.add_album_to_playlist(int(media_id)) - elif media_type == MEDIA_TYPE_TRACK: + elif media_type == MediaType.TRACK: await self._kodi.add_song_to_playlist(int(media_id)) async def async_add_media_to_playlist( From 9e1cb914b1b378fb8aeeb0d4b89d6d49ff5852a1 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 10 Sep 2022 22:24:01 +0200 Subject: [PATCH 265/955] Use new media player enums in snapcast (#78109) --- .../components/snapcast/media_player.py | 27 +++++++------------ 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/snapcast/media_player.py b/homeassistant/components/snapcast/media_player.py index e7c4c9d8443..de3eca18b45 100644 --- a/homeassistant/components/snapcast/media_player.py +++ b/homeassistant/components/snapcast/media_player.py @@ -12,16 +12,9 @@ from homeassistant.components.media_player import ( PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, ) -from homeassistant.const import ( - CONF_HOST, - CONF_PORT, - STATE_IDLE, - STATE_OFF, - STATE_ON, - STATE_PLAYING, - STATE_UNKNOWN, -) +from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -137,13 +130,13 @@ class SnapcastGroupDevice(MediaPlayerEntity): self._group.set_callback(None) @property - def state(self): + def state(self) -> MediaPlayerState | None: """Return the state of the player.""" return { - "idle": STATE_IDLE, - "playing": STATE_PLAYING, - "unknown": STATE_UNKNOWN, - }.get(self._group.stream_status, STATE_UNKNOWN) + "idle": MediaPlayerState.IDLE, + "playing": MediaPlayerState.PLAYING, + "unknown": None, + }.get(self._group.stream_status) @property def unique_id(self): @@ -271,11 +264,11 @@ class SnapcastClientDevice(MediaPlayerEntity): return list(self._client.group.streams_by_name().keys()) @property - def state(self): + def state(self) -> MediaPlayerState: """Return the state of the player.""" if self._client.connected: - return STATE_ON - return STATE_OFF + return MediaPlayerState.ON + return MediaPlayerState.OFF @property def extra_state_attributes(self): From 0d88567e0e62f2be8b571f7387de3b8151ba820b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 10 Sep 2022 22:26:26 +0200 Subject: [PATCH 266/955] Use new media player enums in soundtouch (#78110) --- .../components/soundtouch/media_player.py | 26 +++++++------------ 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/soundtouch/media_player.py b/homeassistant/components/soundtouch/media_player.py index d1cf6a9fa94..cab2076c750 100644 --- a/homeassistant/components/soundtouch/media_player.py +++ b/homeassistant/components/soundtouch/media_player.py @@ -13,12 +13,11 @@ import voluptuous as vol from homeassistant.components import media_source from homeassistant.components.media_player import ( PLATFORM_SCHEMA, + BrowseMedia, MediaPlayerDeviceClass, MediaPlayerEntity, MediaPlayerEntityFeature, -) -from homeassistant.components.media_player.browse_media import ( - BrowseMedia, + MediaPlayerState, async_process_play_media_url, ) from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry @@ -27,11 +26,6 @@ from homeassistant.const import ( CONF_NAME, CONF_PORT, EVENT_HOMEASSISTANT_START, - STATE_OFF, - STATE_PAUSED, - STATE_PLAYING, - STATE_UNAVAILABLE, - STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv @@ -46,10 +40,10 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) MAP_STATUS = { - "PLAY_STATE": STATE_PLAYING, - "BUFFERING_STATE": STATE_PLAYING, - "PAUSE_STATE": STATE_PAUSED, - "STOP_STATE": STATE_OFF, + "PLAY_STATE": MediaPlayerState.PLAYING, + "BUFFERING_STATE": MediaPlayerState.PLAYING, + "PAUSE_STATE": MediaPlayerState.PAUSED, + "STOP_STATE": MediaPlayerState.OFF, } ATTR_SOUNDTOUCH_GROUP = "soundtouch_group" @@ -168,15 +162,15 @@ class SoundTouchMediaPlayer(MediaPlayerEntity): return self._volume.actual / 100 @property - def state(self): + def state(self) -> MediaPlayerState | None: """Return the state of the device.""" if self._status is None or self._status.source == "STANDBY": - return STATE_OFF + return MediaPlayerState.OFF if self._status.source == "INVALID_SOURCE": - return STATE_UNKNOWN + return None - return MAP_STATUS.get(self._status.play_status, STATE_UNAVAILABLE) + return MAP_STATUS.get(self._status.play_status) @property def source(self): From 7eefaa308f2ba083f37e1841889b05f5aedbf371 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 10 Sep 2022 22:29:04 +0200 Subject: [PATCH 267/955] Use new media player enums in universal (#78112) --- .../components/universal/media_player.py | 39 ++++++++----------- 1 file changed, 16 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/universal/media_player.py b/homeassistant/components/universal/media_player.py index 55adadad845..d26b062de40 100644 --- a/homeassistant/components/universal/media_player.py +++ b/homeassistant/components/universal/media_player.py @@ -7,12 +7,6 @@ from typing import Any import voluptuous as vol from homeassistant.components.media_player import ( - DEVICE_CLASSES_SCHEMA, - PLATFORM_SCHEMA, - MediaPlayerEntity, - MediaPlayerEntityFeature, -) -from homeassistant.components.media_player.const import ( ATTR_APP_ID, ATTR_APP_NAME, ATTR_INPUT_SOURCE, @@ -39,11 +33,16 @@ from homeassistant.components.media_player.const import ( ATTR_MEDIA_VOLUME_MUTED, ATTR_SOUND_MODE, ATTR_SOUND_MODE_LIST, + DEVICE_CLASSES_SCHEMA, DOMAIN, + PLATFORM_SCHEMA, SERVICE_CLEAR_PLAYLIST, SERVICE_PLAY_MEDIA, SERVICE_SELECT_SOUND_MODE, SERVICE_SELECT_SOURCE, + MediaPlayerEntity, + MediaPlayerEntityFeature, + MediaPlayerState, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -71,13 +70,7 @@ from homeassistant.const import ( SERVICE_VOLUME_MUTE, SERVICE_VOLUME_SET, SERVICE_VOLUME_UP, - STATE_BUFFERING, - STATE_IDLE, - STATE_OFF, STATE_ON, - STATE_PAUSED, - STATE_PLAYING, - STATE_STANDBY, STATE_UNAVAILABLE, STATE_UNKNOWN, ) @@ -103,16 +96,16 @@ CONF_COMMANDS = "commands" STATES_ORDER = [ STATE_UNKNOWN, STATE_UNAVAILABLE, - STATE_OFF, - STATE_IDLE, - STATE_STANDBY, - STATE_ON, - STATE_PAUSED, - STATE_BUFFERING, - STATE_PLAYING, + MediaPlayerState.OFF, + MediaPlayerState.IDLE, + MediaPlayerState.STANDBY, + MediaPlayerState.ON, + MediaPlayerState.PAUSED, + MediaPlayerState.BUFFERING, + MediaPlayerState.PLAYING, ] STATES_ORDER_LOOKUP = {state: idx for idx, state in enumerate(STATES_ORDER)} -STATES_ORDER_IDLE = STATES_ORDER_LOOKUP[STATE_IDLE] +STATES_ORDER_IDLE = STATES_ORDER_LOOKUP[MediaPlayerState.IDLE] ATTRS_SCHEMA = cv.schema_with_slug_keys(cv.string) CMD_SCHEMA = cv.schema_with_slug_keys(cv.SERVICE_SCHEMA) @@ -299,7 +292,7 @@ class UniversalMediaPlayer(MediaPlayerEntity): master_state = self._entity_lkp( self._attrs[CONF_STATE][0], self._attrs[CONF_STATE][1] ) - return master_state if master_state else STATE_OFF + return master_state if master_state else MediaPlayerState.OFF return None @@ -317,13 +310,13 @@ class UniversalMediaPlayer(MediaPlayerEntity): else master state or off """ master_state = self.master_state # avoid multiple lookups - if (master_state == STATE_OFF) or (self._state_template is not None): + if (master_state == MediaPlayerState.OFF) or (self._state_template is not None): return master_state if active_child := self._child_state: return active_child.state - return master_state if master_state else STATE_OFF + return master_state if master_state else MediaPlayerState.OFF @property def volume_level(self): From ec532414badf98d5c19f0b7b6d42fede465a7948 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 10 Sep 2022 23:39:52 +0200 Subject: [PATCH 268/955] Import climate constants from root [a-l] (#78177) --- homeassistant/components/advantage_air/climate.py | 4 ++-- homeassistant/components/airtouch4/climate.py | 4 ++-- homeassistant/components/atag/climate.py | 4 ++-- homeassistant/components/balboa/climate.py | 4 ++-- homeassistant/components/balboa/const.py | 2 +- homeassistant/components/bsblan/climate.py | 4 ++-- homeassistant/components/daikin/climate.py | 5 +++-- homeassistant/components/deconz/climate.py | 5 +++-- homeassistant/components/ecobee/climate.py | 4 ++-- homeassistant/components/econet/climate.py | 4 ++-- homeassistant/components/elkm1/climate.py | 4 ++-- homeassistant/components/eq3btsmart/climate.py | 5 +++-- homeassistant/components/escea/climate.py | 4 ++-- homeassistant/components/esphome/climate.py | 4 ++-- homeassistant/components/evohome/climate.py | 4 ++-- homeassistant/components/fibaro/climate.py | 5 +++-- homeassistant/components/fritzbox/climate.py | 4 ++-- homeassistant/components/fritzbox/sensor.py | 2 +- homeassistant/components/generic_thermostat/climate.py | 5 +++-- homeassistant/components/geniushub/climate.py | 4 ++-- homeassistant/components/gree/climate.py | 4 ++-- homeassistant/components/hisense_aehw4a1/climate.py | 4 ++-- homeassistant/components/hive/climate.py | 4 ++-- homeassistant/components/homekit/type_thermostats.py | 4 ++-- homeassistant/components/homekit_controller/climate.py | 8 +++----- homeassistant/components/homematic/climate.py | 4 ++-- homeassistant/components/homematicip_cloud/climate.py | 4 ++-- homeassistant/components/honeywell/climate.py | 4 ++-- homeassistant/components/insteon/climate.py | 4 ++-- homeassistant/components/intesishome/climate.py | 5 +++-- homeassistant/components/isy994/climate.py | 4 ++-- homeassistant/components/isy994/const.py | 2 +- homeassistant/components/izone/climate.py | 4 ++-- homeassistant/components/knx/climate.py | 4 ++-- homeassistant/components/knx/const.py | 2 +- homeassistant/components/lookin/climate.py | 4 ++-- 36 files changed, 75 insertions(+), 71 deletions(-) diff --git a/homeassistant/components/advantage_air/climate.py b/homeassistant/components/advantage_air/climate.py index c11b01f3ace..1925b519a38 100644 --- a/homeassistant/components/advantage_air/climate.py +++ b/homeassistant/components/advantage_air/climate.py @@ -4,12 +4,12 @@ from __future__ import annotations import logging from typing import Any -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( FAN_AUTO, FAN_HIGH, FAN_LOW, FAN_MEDIUM, + ClimateEntity, ClimateEntityFeature, HVACMode, ) diff --git a/homeassistant/components/airtouch4/climate.py b/homeassistant/components/airtouch4/climate.py index dcc107453d5..598b6ecd6e3 100644 --- a/homeassistant/components/airtouch4/climate.py +++ b/homeassistant/components/airtouch4/climate.py @@ -4,14 +4,14 @@ from __future__ import annotations import logging from typing import Any -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( FAN_AUTO, FAN_DIFFUSE, FAN_FOCUS, FAN_HIGH, FAN_LOW, FAN_MEDIUM, + ClimateEntity, ClimateEntityFeature, HVACMode, ) diff --git a/homeassistant/components/atag/climate.py b/homeassistant/components/atag/climate.py index e6e9d8503e8..ce52bd4fd65 100644 --- a/homeassistant/components/atag/climate.py +++ b/homeassistant/components/atag/climate.py @@ -3,10 +3,10 @@ from __future__ import annotations from typing import Any -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( PRESET_AWAY, PRESET_BOOST, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, diff --git a/homeassistant/components/balboa/climate.py b/homeassistant/components/balboa/climate.py index cd10ccf2bc9..1cd93b4fddb 100644 --- a/homeassistant/components/balboa/climate.py +++ b/homeassistant/components/balboa/climate.py @@ -4,12 +4,12 @@ from __future__ import annotations import asyncio from typing import Any -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( FAN_HIGH, FAN_LOW, FAN_MEDIUM, FAN_OFF, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, diff --git a/homeassistant/components/balboa/const.py b/homeassistant/components/balboa/const.py index 9a2d6b704ff..dcd9c05ac91 100644 --- a/homeassistant/components/balboa/const.py +++ b/homeassistant/components/balboa/const.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( FAN_HIGH, FAN_LOW, FAN_MEDIUM, diff --git a/homeassistant/components/bsblan/climate.py b/homeassistant/components/bsblan/climate.py index e83415ebf52..b7fa2bb8010 100644 --- a/homeassistant/components/bsblan/climate.py +++ b/homeassistant/components/bsblan/climate.py @@ -7,12 +7,12 @@ from typing import Any from bsblan import BSBLan, BSBLanError, Info, State -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_HVAC_MODE, ATTR_PRESET_MODE, PRESET_ECO, PRESET_NONE, + ClimateEntity, ClimateEntityFeature, HVACMode, ) diff --git a/homeassistant/components/daikin/climate.py b/homeassistant/components/daikin/climate.py index 2f07c5a0bdc..02107205e70 100644 --- a/homeassistant/components/daikin/climate.py +++ b/homeassistant/components/daikin/climate.py @@ -6,16 +6,17 @@ from typing import Any import voluptuous as vol -from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_FAN_MODE, ATTR_HVAC_MODE, ATTR_PRESET_MODE, ATTR_SWING_MODE, + PLATFORM_SCHEMA, PRESET_AWAY, PRESET_BOOST, PRESET_ECO, PRESET_NONE, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, diff --git a/homeassistant/components/deconz/climate.py b/homeassistant/components/deconz/climate.py index e49918e14f2..24cc6c8d5c8 100644 --- a/homeassistant/components/deconz/climate.py +++ b/homeassistant/components/deconz/climate.py @@ -11,8 +11,8 @@ from pydeconz.models.sensor.thermostat import ( ThermostatPreset, ) -from homeassistant.components.climate import DOMAIN, ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( + DOMAIN, FAN_AUTO, FAN_HIGH, FAN_LOW, @@ -22,6 +22,7 @@ from homeassistant.components.climate.const import ( PRESET_BOOST, PRESET_COMFORT, PRESET_ECO, + ClimateEntity, ClimateEntityFeature, HVACMode, ) diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index 6a96b5418b6..c5490c55850 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -6,14 +6,14 @@ from typing import Any import voluptuous as vol -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, FAN_AUTO, FAN_ON, PRESET_AWAY, PRESET_NONE, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, diff --git a/homeassistant/components/econet/climate.py b/homeassistant/components/econet/climate.py index 9fba4883644..bda462285fc 100644 --- a/homeassistant/components/econet/climate.py +++ b/homeassistant/components/econet/climate.py @@ -4,14 +4,14 @@ from typing import Any from pyeconet.equipment import EquipmentType from pyeconet.equipment.thermostat import ThermostatFanMode, ThermostatOperationMode -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, FAN_AUTO, FAN_HIGH, FAN_LOW, FAN_MEDIUM, + ClimateEntity, ClimateEntityFeature, HVACMode, ) diff --git a/homeassistant/components/elkm1/climate.py b/homeassistant/components/elkm1/climate.py index 6eca3083b3a..570c8567403 100644 --- a/homeassistant/components/elkm1/climate.py +++ b/homeassistant/components/elkm1/climate.py @@ -8,12 +8,12 @@ from elkm1_lib.elements import Element from elkm1_lib.elk import Elk from elkm1_lib.thermostats import Thermostat -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, FAN_AUTO, FAN_ON, + ClimateEntity, ClimateEntityFeature, HVACMode, ) diff --git a/homeassistant/components/eq3btsmart/climate.py b/homeassistant/components/eq3btsmart/climate.py index e469512123b..027366c96ef 100644 --- a/homeassistant/components/eq3btsmart/climate.py +++ b/homeassistant/components/eq3btsmart/climate.py @@ -7,11 +7,12 @@ from typing import Any import eq3bt as eq3 # pylint: disable=import-error import voluptuous as vol -from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( + PLATFORM_SCHEMA, PRESET_AWAY, PRESET_BOOST, PRESET_NONE, + ClimateEntity, ClimateEntityFeature, HVACMode, ) diff --git a/homeassistant/components/escea/climate.py b/homeassistant/components/escea/climate.py index 1ddea9cb026..a764c019c50 100644 --- a/homeassistant/components/escea/climate.py +++ b/homeassistant/components/escea/climate.py @@ -7,11 +7,11 @@ from typing import Any from pescea import Controller -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( FAN_AUTO, FAN_HIGH, FAN_LOW, + ClimateEntity, ClimateEntityFeature, HVACMode, ) diff --git a/homeassistant/components/esphome/climate.py b/homeassistant/components/esphome/climate.py index aba51a47a9d..6dbc9a58ea5 100644 --- a/homeassistant/components/esphome/climate.py +++ b/homeassistant/components/esphome/climate.py @@ -13,8 +13,7 @@ from aioesphomeapi import ( ClimateSwingMode, ) -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_HVAC_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, @@ -39,6 +38,7 @@ from homeassistant.components.climate.const import ( SWING_HORIZONTAL, SWING_OFF, SWING_VERTICAL, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, diff --git a/homeassistant/components/evohome/climate.py b/homeassistant/components/evohome/climate.py index e9eab8c2ae3..0ec64c6b2b1 100644 --- a/homeassistant/components/evohome/climate.py +++ b/homeassistant/components/evohome/climate.py @@ -5,12 +5,12 @@ from datetime import datetime as dt import logging from typing import Any -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( PRESET_AWAY, PRESET_ECO, PRESET_HOME, PRESET_NONE, + ClimateEntity, ClimateEntityFeature, HVACMode, ) diff --git a/homeassistant/components/fibaro/climate.py b/homeassistant/components/fibaro/climate.py index 94dd599698d..90a13fe8988 100644 --- a/homeassistant/components/fibaro/climate.py +++ b/homeassistant/components/fibaro/climate.py @@ -4,10 +4,11 @@ from __future__ import annotations import logging from typing import Any -from homeassistant.components.climate import ENTITY_ID_FORMAT, ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( + ENTITY_ID_FORMAT, PRESET_AWAY, PRESET_BOOST, + ClimateEntity, ClimateEntityFeature, HVACMode, ) diff --git a/homeassistant/components/fritzbox/climate.py b/homeassistant/components/fritzbox/climate.py index 10fd3cf0177..806f8b2303e 100644 --- a/homeassistant/components/fritzbox/climate.py +++ b/homeassistant/components/fritzbox/climate.py @@ -3,11 +3,11 @@ from __future__ import annotations from typing import Any -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_HVAC_MODE, PRESET_COMFORT, PRESET_ECO, + ClimateEntity, ClimateEntityFeature, HVACMode, ) diff --git a/homeassistant/components/fritzbox/sensor.py b/homeassistant/components/fritzbox/sensor.py index 161dfc196d2..4908cfa84a3 100644 --- a/homeassistant/components/fritzbox/sensor.py +++ b/homeassistant/components/fritzbox/sensor.py @@ -8,7 +8,7 @@ from typing import Final from pyfritzhome.fritzhomedevice import FritzhomeDevice -from homeassistant.components.climate.const import PRESET_COMFORT, PRESET_ECO +from homeassistant.components.climate import PRESET_COMFORT, PRESET_ECO from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, diff --git a/homeassistant/components/generic_thermostat/climate.py b/homeassistant/components/generic_thermostat/climate.py index f095037d7f7..dadfc995e03 100644 --- a/homeassistant/components/generic_thermostat/climate.py +++ b/homeassistant/components/generic_thermostat/climate.py @@ -8,15 +8,16 @@ from typing import Any import voluptuous as vol -from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_PRESET_MODE, + PLATFORM_SCHEMA, PRESET_ACTIVITY, PRESET_AWAY, PRESET_COMFORT, PRESET_HOME, PRESET_NONE, PRESET_SLEEP, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, diff --git a/homeassistant/components/geniushub/climate.py b/homeassistant/components/geniushub/climate.py index 429d51c0035..3f8fb0c6805 100644 --- a/homeassistant/components/geniushub/climate.py +++ b/homeassistant/components/geniushub/climate.py @@ -1,10 +1,10 @@ """Support for Genius Hub climate devices.""" from __future__ import annotations -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( PRESET_ACTIVITY, PRESET_BOOST, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, diff --git a/homeassistant/components/gree/climate.py b/homeassistant/components/gree/climate.py index 4096f58e4cb..976cc31761d 100644 --- a/homeassistant/components/gree/climate.py +++ b/homeassistant/components/gree/climate.py @@ -16,8 +16,7 @@ from greeclimate.device import ( VerticalSwing, ) -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( FAN_AUTO, FAN_HIGH, FAN_LOW, @@ -31,6 +30,7 @@ from homeassistant.components.climate.const import ( SWING_HORIZONTAL, SWING_OFF, SWING_VERTICAL, + ClimateEntity, ClimateEntityFeature, HVACMode, ) diff --git a/homeassistant/components/hisense_aehw4a1/climate.py b/homeassistant/components/hisense_aehw4a1/climate.py index 6f6ce2f2366..fc956ec8760 100644 --- a/homeassistant/components/hisense_aehw4a1/climate.py +++ b/homeassistant/components/hisense_aehw4a1/climate.py @@ -7,8 +7,7 @@ from typing import Any from pyaehw4a1.aehw4a1 import AehW4a1 import pyaehw4a1.exceptions -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( FAN_AUTO, FAN_HIGH, FAN_LOW, @@ -21,6 +20,7 @@ from homeassistant.components.climate.const import ( SWING_HORIZONTAL, SWING_OFF, SWING_VERTICAL, + ClimateEntity, ClimateEntityFeature, HVACMode, ) diff --git a/homeassistant/components/hive/climate.py b/homeassistant/components/hive/climate.py index 8e33dda244a..620d679fe1c 100644 --- a/homeassistant/components/hive/climate.py +++ b/homeassistant/components/hive/climate.py @@ -5,10 +5,10 @@ from typing import Any import voluptuous as vol -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( PRESET_BOOST, PRESET_NONE, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index fc27a85850f..a924548816b 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -3,8 +3,7 @@ import logging from pyhap.const import CATEGORY_THERMOSTAT -from homeassistant.components.climate import ClimateEntityFeature -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_CURRENT_HUMIDITY, ATTR_CURRENT_TEMPERATURE, ATTR_FAN_MODE, @@ -41,6 +40,7 @@ from homeassistant.components.climate.const import ( SWING_OFF, SWING_ON, SWING_VERTICAL, + ClimateEntityFeature, HVACAction, HVACMode, ) diff --git a/homeassistant/components/homekit_controller/climate.py b/homeassistant/components/homekit_controller/climate.py index 7254363e835..2c4d2e3871d 100644 --- a/homeassistant/components/homekit_controller/climate.py +++ b/homeassistant/components/homekit_controller/climate.py @@ -17,18 +17,16 @@ from aiohomekit.model.services import Service, ServicesTypes from aiohomekit.utils import clamp_enum_to_char from homeassistant.components.climate import ( - DEFAULT_MAX_TEMP, - DEFAULT_MIN_TEMP, - ClimateEntity, -) -from homeassistant.components.climate.const import ( ATTR_HVAC_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, + DEFAULT_MAX_TEMP, + DEFAULT_MIN_TEMP, FAN_AUTO, FAN_ON, SWING_OFF, SWING_VERTICAL, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, diff --git a/homeassistant/components/homematic/climate.py b/homeassistant/components/homematic/climate.py index 2d106b90072..d597eca30cd 100644 --- a/homeassistant/components/homematic/climate.py +++ b/homeassistant/components/homematic/climate.py @@ -3,12 +3,12 @@ from __future__ import annotations from typing import Any -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( PRESET_BOOST, PRESET_COMFORT, PRESET_ECO, PRESET_NONE, + ClimateEntity, ClimateEntityFeature, HVACMode, ) diff --git a/homeassistant/components/homematicip_cloud/climate.py b/homeassistant/components/homematicip_cloud/climate.py index ae3ecf9dc9d..b3d2236f05a 100644 --- a/homeassistant/components/homematicip_cloud/climate.py +++ b/homeassistant/components/homematicip_cloud/climate.py @@ -9,12 +9,12 @@ from homematicip.base.enums import AbsenceType from homematicip.device import Switch from homematicip.functionalHomes import IndoorClimateHome -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( PRESET_AWAY, PRESET_BOOST, PRESET_ECO, PRESET_NONE, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py index abcd1d6f340..64f87fd19ae 100644 --- a/homeassistant/components/honeywell/climate.py +++ b/homeassistant/components/honeywell/climate.py @@ -6,8 +6,7 @@ from typing import Any import somecomfort -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, DEFAULT_MAX_TEMP, @@ -17,6 +16,7 @@ from homeassistant.components.climate.const import ( FAN_ON, PRESET_AWAY, PRESET_NONE, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, diff --git a/homeassistant/components/insteon/climate.py b/homeassistant/components/insteon/climate.py index 922ef141350..0211b316b96 100644 --- a/homeassistant/components/insteon/climate.py +++ b/homeassistant/components/insteon/climate.py @@ -6,12 +6,12 @@ from typing import Any from pyinsteon.config import CELSIUS from pyinsteon.constants import ThermostatMode -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, DOMAIN as CLIMATE_DOMAIN, FAN_AUTO, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, diff --git a/homeassistant/components/intesishome/climate.py b/homeassistant/components/intesishome/climate.py index b85c976a928..29cb30b81a2 100644 --- a/homeassistant/components/intesishome/climate.py +++ b/homeassistant/components/intesishome/climate.py @@ -8,9 +8,9 @@ from typing import Any, NamedTuple from pyintesishome import IHAuthenticationError, IHConnectionError, IntesisHome import voluptuous as vol -from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_HVAC_MODE, + PLATFORM_SCHEMA, PRESET_BOOST, PRESET_COMFORT, PRESET_ECO, @@ -18,6 +18,7 @@ from homeassistant.components.climate.const import ( SWING_HORIZONTAL, SWING_OFF, SWING_VERTICAL, + ClimateEntity, ClimateEntityFeature, HVACMode, ) diff --git a/homeassistant/components/isy994/climate.py b/homeassistant/components/isy994/climate.py index bc1f0353455..c141f856408 100644 --- a/homeassistant/components/isy994/climate.py +++ b/homeassistant/components/isy994/climate.py @@ -16,14 +16,14 @@ from pyisy.constants import ( ) from pyisy.nodes import Node -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, DOMAIN as CLIMATE, FAN_AUTO, FAN_OFF, FAN_ON, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, diff --git a/homeassistant/components/isy994/const.py b/homeassistant/components/isy994/const.py index ddabe1b9680..21cc23b01ca 100644 --- a/homeassistant/components/isy994/const.py +++ b/homeassistant/components/isy994/const.py @@ -2,7 +2,7 @@ import logging from homeassistant.components.binary_sensor import BinarySensorDeviceClass -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( FAN_AUTO, FAN_HIGH, FAN_MEDIUM, diff --git a/homeassistant/components/izone/climate.py b/homeassistant/components/izone/climate.py index a80549f27fc..155ec7ba210 100644 --- a/homeassistant/components/izone/climate.py +++ b/homeassistant/components/izone/climate.py @@ -7,8 +7,7 @@ from typing import Any from pizone import Controller, Zone import voluptuous as vol -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( FAN_AUTO, FAN_HIGH, FAN_LOW, @@ -16,6 +15,7 @@ from homeassistant.components.climate.const import ( FAN_TOP, PRESET_ECO, PRESET_NONE, + ClimateEntity, ClimateEntityFeature, HVACMode, ) diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py index 2e1fd61ad43..bb2fe7dfbcc 100644 --- a/homeassistant/components/knx/climate.py +++ b/homeassistant/components/knx/climate.py @@ -8,9 +8,9 @@ from xknx.devices import Climate as XknxClimate, ClimateMode as XknxClimateMode from xknx.dpt.dpt_hvac_mode import HVACControllerMode, HVACOperationMode from homeassistant import config_entries -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( PRESET_AWAY, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, diff --git a/homeassistant/components/knx/const.py b/homeassistant/components/knx/const.py index df8f0de3216..3cc73a6dd35 100644 --- a/homeassistant/components/knx/const.py +++ b/homeassistant/components/knx/const.py @@ -4,7 +4,7 @@ from __future__ import annotations from enum import Enum from typing import Final, TypedDict -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( PRESET_AWAY, PRESET_COMFORT, PRESET_ECO, diff --git a/homeassistant/components/lookin/climate.py b/homeassistant/components/lookin/climate.py index b8042cef72a..5b3ecefa4ff 100644 --- a/homeassistant/components/lookin/climate.py +++ b/homeassistant/components/lookin/climate.py @@ -7,8 +7,7 @@ from typing import Any, Final, cast from aiolookin import Climate, MeteoSensor from aiolookin.models import UDPCommandType, UDPEvent -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_HVAC_MODE, FAN_AUTO, FAN_HIGH, @@ -16,6 +15,7 @@ from homeassistant.components.climate.const import ( FAN_MIDDLE, SWING_BOTH, SWING_OFF, + ClimateEntity, ClimateEntityFeature, HVACMode, ) From 6affd9c6fb1ba6f812a174a768613013a4d4b5b0 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 10 Sep 2022 23:42:28 +0200 Subject: [PATCH 269/955] Import climate constants from root [m-z] (#78178) --- homeassistant/components/maxcube/climate.py | 4 ++-- homeassistant/components/melissa/climate.py | 4 ++-- homeassistant/components/mill/climate.py | 4 ++-- homeassistant/components/mqtt/climate.py | 4 ++-- homeassistant/components/nest/climate_sdm.py | 4 ++-- homeassistant/components/nest/legacy/climate.py | 5 +++-- homeassistant/components/netatmo/climate.py | 4 ++-- homeassistant/components/nobo_hub/climate.py | 4 ++-- homeassistant/components/opentherm_gw/climate.py | 5 +++-- .../climate_entities/atlantic_electrical_heater.py | 4 ++-- .../climate_entities/atlantic_electrical_towel_dryer.py | 4 ++-- .../atlantic_heat_recovery_ventilation.py | 4 ++-- .../overkiz/climate_entities/somfy_thermostat.py | 4 ++-- homeassistant/components/radiotherm/climate.py | 5 +++-- homeassistant/components/shelly/climate.py | 5 +++-- homeassistant/components/smarttub/climate.py | 4 ++-- homeassistant/components/stiebel_eltron/climate.py | 4 ++-- homeassistant/components/tado/__init__.py | 2 +- homeassistant/components/tado/climate.py | 4 ++-- homeassistant/components/tado/const.py | 2 +- homeassistant/components/tfiac/climate.py | 5 +++-- homeassistant/components/tolo/climate.py | 4 ++-- homeassistant/components/toon/climate.py | 4 ++-- homeassistant/components/tuya/climate.py | 5 +++-- homeassistant/components/velbus/const.py | 2 +- homeassistant/components/venstar/climate.py | 5 +++-- homeassistant/components/vera/climate.py | 5 +++-- homeassistant/components/vicare/climate.py | 4 ++-- homeassistant/components/whirlpool/climate.py | 5 +++-- homeassistant/components/yolink/climate.py | 5 +++-- homeassistant/components/zha/climate.py | 4 ++-- homeassistant/components/zwave_js/climate.py | 8 +++----- 32 files changed, 72 insertions(+), 64 deletions(-) diff --git a/homeassistant/components/maxcube/climate.py b/homeassistant/components/maxcube/climate.py index 61abde40a37..6361600518b 100644 --- a/homeassistant/components/maxcube/climate.py +++ b/homeassistant/components/maxcube/climate.py @@ -12,13 +12,13 @@ from maxcube.device import ( MAX_DEVICE_MODE_VACATION, ) -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( PRESET_AWAY, PRESET_BOOST, PRESET_COMFORT, PRESET_ECO, PRESET_NONE, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, diff --git a/homeassistant/components/melissa/climate.py b/homeassistant/components/melissa/climate.py index 56278710fd2..bfe1a4929d0 100644 --- a/homeassistant/components/melissa/climate.py +++ b/homeassistant/components/melissa/climate.py @@ -4,12 +4,12 @@ from __future__ import annotations import logging from typing import Any -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( FAN_AUTO, FAN_HIGH, FAN_LOW, FAN_MEDIUM, + ClimateEntity, ClimateEntityFeature, HVACMode, ) diff --git a/homeassistant/components/mill/climate.py b/homeassistant/components/mill/climate.py index a18f9b3bafb..84f87e79a2d 100644 --- a/homeassistant/components/mill/climate.py +++ b/homeassistant/components/mill/climate.py @@ -4,10 +4,10 @@ from typing import Any import mill import voluptuous as vol -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( FAN_OFF, FAN_ON, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index f39d3857ec2..3f0f8a89b3e 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -8,8 +8,7 @@ from typing import Any import voluptuous as vol from homeassistant.components import climate -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_HVAC_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, @@ -22,6 +21,7 @@ from homeassistant.components.climate.const import ( PRESET_NONE, SWING_OFF, SWING_ON, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, diff --git a/homeassistant/components/nest/climate_sdm.py b/homeassistant/components/nest/climate_sdm.py index 87cbf2331f4..e40db60d5ed 100644 --- a/homeassistant/components/nest/climate_sdm.py +++ b/homeassistant/components/nest/climate_sdm.py @@ -15,8 +15,7 @@ from google_nest_sdm.thermostat_traits import ( ThermostatTemperatureSetpointTrait, ) -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_HVAC_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, @@ -24,6 +23,7 @@ from homeassistant.components.climate.const import ( FAN_ON, PRESET_ECO, PRESET_NONE, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, diff --git a/homeassistant/components/nest/legacy/climate.py b/homeassistant/components/nest/legacy/climate.py index 3a735fe44c3..13728585e39 100644 --- a/homeassistant/components/nest/legacy/climate.py +++ b/homeassistant/components/nest/legacy/climate.py @@ -6,15 +6,16 @@ import logging from nest.nest import APIError import voluptuous as vol -from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, FAN_AUTO, FAN_ON, + PLATFORM_SCHEMA, PRESET_AWAY, PRESET_ECO, PRESET_NONE, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index eb7d996eefb..6b30989dd8f 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -7,11 +7,11 @@ from typing import Any import pyatmo import voluptuous as vol -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( DEFAULT_MIN_TEMP, PRESET_AWAY, PRESET_BOOST, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, diff --git a/homeassistant/components/nobo_hub/climate.py b/homeassistant/components/nobo_hub/climate.py index 3b7dc2debd9..a465bfa77ab 100644 --- a/homeassistant/components/nobo_hub/climate.py +++ b/homeassistant/components/nobo_hub/climate.py @@ -6,14 +6,14 @@ from typing import Any from pynobo import nobo -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, PRESET_AWAY, PRESET_COMFORT, PRESET_ECO, PRESET_NONE, + ClimateEntity, ClimateEntityFeature, HVACMode, ) diff --git a/homeassistant/components/opentherm_gw/climate.py b/homeassistant/components/opentherm_gw/climate.py index fba28138f42..08cfbf68ac8 100644 --- a/homeassistant/components/opentherm_gw/climate.py +++ b/homeassistant/components/opentherm_gw/climate.py @@ -6,10 +6,11 @@ from typing import Any from pyotgw import vars as gw_vars -from homeassistant.components.climate import ENTITY_ID_FORMAT, ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( + ENTITY_ID_FORMAT, PRESET_AWAY, PRESET_NONE, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, diff --git a/homeassistant/components/overkiz/climate_entities/atlantic_electrical_heater.py b/homeassistant/components/overkiz/climate_entities/atlantic_electrical_heater.py index 0a397e9f2ad..9195a729ff8 100644 --- a/homeassistant/components/overkiz/climate_entities/atlantic_electrical_heater.py +++ b/homeassistant/components/overkiz/climate_entities/atlantic_electrical_heater.py @@ -5,11 +5,11 @@ from typing import cast from pyoverkiz.enums import OverkizCommand, OverkizCommandParam, OverkizState -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( PRESET_COMFORT, PRESET_ECO, PRESET_NONE, + ClimateEntity, ClimateEntityFeature, HVACMode, ) diff --git a/homeassistant/components/overkiz/climate_entities/atlantic_electrical_towel_dryer.py b/homeassistant/components/overkiz/climate_entities/atlantic_electrical_towel_dryer.py index 9a24a7bf1a9..0e13beae097 100644 --- a/homeassistant/components/overkiz/climate_entities/atlantic_electrical_towel_dryer.py +++ b/homeassistant/components/overkiz/climate_entities/atlantic_electrical_towel_dryer.py @@ -5,10 +5,10 @@ from typing import Any, cast from pyoverkiz.enums import OverkizCommand, OverkizCommandParam, OverkizState -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( PRESET_BOOST, PRESET_NONE, + ClimateEntity, ClimateEntityFeature, HVACMode, ) diff --git a/homeassistant/components/overkiz/climate_entities/atlantic_heat_recovery_ventilation.py b/homeassistant/components/overkiz/climate_entities/atlantic_heat_recovery_ventilation.py index f28db995350..3627aa21c43 100644 --- a/homeassistant/components/overkiz/climate_entities/atlantic_heat_recovery_ventilation.py +++ b/homeassistant/components/overkiz/climate_entities/atlantic_heat_recovery_ventilation.py @@ -5,9 +5,9 @@ from typing import cast from pyoverkiz.enums import OverkizCommand, OverkizCommandParam, OverkizState -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( FAN_AUTO, + ClimateEntity, ClimateEntityFeature, HVACMode, ) diff --git a/homeassistant/components/overkiz/climate_entities/somfy_thermostat.py b/homeassistant/components/overkiz/climate_entities/somfy_thermostat.py index 80859d7561b..608b26b8c9d 100644 --- a/homeassistant/components/overkiz/climate_entities/somfy_thermostat.py +++ b/homeassistant/components/overkiz/climate_entities/somfy_thermostat.py @@ -5,11 +5,11 @@ from typing import Any, cast from pyoverkiz.enums import OverkizCommand, OverkizCommandParam, OverkizState -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( PRESET_AWAY, PRESET_HOME, PRESET_NONE, + ClimateEntity, ClimateEntityFeature, HVACMode, ) diff --git a/homeassistant/components/radiotherm/climate.py b/homeassistant/components/radiotherm/climate.py index cd2bb69ad58..f8ccf068f69 100644 --- a/homeassistant/components/radiotherm/climate.py +++ b/homeassistant/components/radiotherm/climate.py @@ -7,13 +7,14 @@ from typing import Any import radiotherm import voluptuous as vol -from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( FAN_AUTO, FAN_OFF, FAN_ON, + PLATFORM_SCHEMA, PRESET_AWAY, PRESET_HOME, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, diff --git a/homeassistant/components/shelly/climate.py b/homeassistant/components/shelly/climate.py index a3f42c4a928..0bdcb3a9ad9 100644 --- a/homeassistant/components/shelly/climate.py +++ b/homeassistant/components/shelly/climate.py @@ -8,9 +8,10 @@ from typing import Any, cast from aioshelly.block_device import Block import async_timeout -from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN, ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( + DOMAIN as CLIMATE_DOMAIN, PRESET_NONE, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, diff --git a/homeassistant/components/smarttub/climate.py b/homeassistant/components/smarttub/climate.py index a9ff8699008..758cffe7fa8 100644 --- a/homeassistant/components/smarttub/climate.py +++ b/homeassistant/components/smarttub/climate.py @@ -5,10 +5,10 @@ from typing import Any from smarttub import Spa -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( PRESET_ECO, PRESET_NONE, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, diff --git a/homeassistant/components/stiebel_eltron/climate.py b/homeassistant/components/stiebel_eltron/climate.py index 3ce29d1f51a..25c24c50ece 100644 --- a/homeassistant/components/stiebel_eltron/climate.py +++ b/homeassistant/components/stiebel_eltron/climate.py @@ -4,9 +4,9 @@ from __future__ import annotations import logging from typing import Any -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( PRESET_ECO, + ClimateEntity, ClimateEntityFeature, HVACMode, ) diff --git a/homeassistant/components/tado/__init__.py b/homeassistant/components/tado/__init__.py index 7417ac49c96..4b4d46883ce 100644 --- a/homeassistant/components/tado/__init__.py +++ b/homeassistant/components/tado/__init__.py @@ -6,7 +6,7 @@ from PyTado.interface import Tado from requests import RequestException import requests.exceptions -from homeassistant.components.climate.const import PRESET_AWAY, PRESET_HOME +from homeassistant.components.climate import PRESET_AWAY, PRESET_HOME from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant, callback diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py index 879b13310a4..16a433b1d12 100644 --- a/homeassistant/components/tado/climate.py +++ b/homeassistant/components/tado/climate.py @@ -6,11 +6,11 @@ from typing import Any import voluptuous as vol -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( FAN_AUTO, PRESET_AWAY, PRESET_HOME, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, diff --git a/homeassistant/components/tado/const.py b/homeassistant/components/tado/const.py index b6b36444211..c547179f4e9 100644 --- a/homeassistant/components/tado/const.py +++ b/homeassistant/components/tado/const.py @@ -10,7 +10,7 @@ from PyTado.const import ( CONST_HVAC_OFF, ) -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( FAN_AUTO, FAN_HIGH, FAN_LOW, diff --git a/homeassistant/components/tfiac/climate.py b/homeassistant/components/tfiac/climate.py index 46163056948..73e52ca1584 100644 --- a/homeassistant/components/tfiac/climate.py +++ b/homeassistant/components/tfiac/climate.py @@ -9,16 +9,17 @@ from typing import Any from pytfiac import Tfiac import voluptuous as vol -from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( FAN_AUTO, FAN_HIGH, FAN_LOW, FAN_MEDIUM, + PLATFORM_SCHEMA, SWING_BOTH, SWING_HORIZONTAL, SWING_OFF, SWING_VERTICAL, + ClimateEntity, ClimateEntityFeature, HVACMode, ) diff --git a/homeassistant/components/tolo/climate.py b/homeassistant/components/tolo/climate.py index 01955f62a89..e82fe34ab84 100644 --- a/homeassistant/components/tolo/climate.py +++ b/homeassistant/components/tolo/climate.py @@ -5,10 +5,10 @@ from typing import Any from tololib.const import Calefaction -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( FAN_OFF, FAN_ON, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, diff --git a/homeassistant/components/toon/climate.py b/homeassistant/components/toon/climate.py index 2d7cf4c04f6..4216d1c13fa 100644 --- a/homeassistant/components/toon/climate.py +++ b/homeassistant/components/toon/climate.py @@ -10,12 +10,12 @@ from toonapi import ( ACTIVE_STATE_SLEEP, ) -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( PRESET_AWAY, PRESET_COMFORT, PRESET_HOME, PRESET_SLEEP, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, diff --git a/homeassistant/components/tuya/climate.py b/homeassistant/components/tuya/climate.py index e111a1630ad..757701d5382 100644 --- a/homeassistant/components/tuya/climate.py +++ b/homeassistant/components/tuya/climate.py @@ -6,13 +6,14 @@ from typing import Any from tuya_iot import TuyaDevice, TuyaDeviceManager -from homeassistant.components.climate import ClimateEntity, ClimateEntityDescription -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( SWING_BOTH, SWING_HORIZONTAL, SWING_OFF, SWING_ON, SWING_VERTICAL, + ClimateEntity, + ClimateEntityDescription, ClimateEntityFeature, HVACMode, ) diff --git a/homeassistant/components/velbus/const.py b/homeassistant/components/velbus/const.py index d295c725d21..7c41274f11c 100644 --- a/homeassistant/components/velbus/const.py +++ b/homeassistant/components/velbus/const.py @@ -1,7 +1,7 @@ """Const for Velbus.""" from typing import Final -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( PRESET_AWAY, PRESET_COMFORT, PRESET_ECO, diff --git a/homeassistant/components/venstar/climate.py b/homeassistant/components/venstar/climate.py index 11d6ecc1783..2fb0595788f 100644 --- a/homeassistant/components/venstar/climate.py +++ b/homeassistant/components/venstar/climate.py @@ -3,15 +3,16 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_HVAC_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, FAN_AUTO, FAN_ON, + PLATFORM_SCHEMA, PRESET_AWAY, PRESET_NONE, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, diff --git a/homeassistant/components/vera/climate.py b/homeassistant/components/vera/climate.py index 8a3803db821..924acbe6243 100644 --- a/homeassistant/components/vera/climate.py +++ b/homeassistant/components/vera/climate.py @@ -5,10 +5,11 @@ from typing import Any import pyvera as veraApi -from homeassistant.components.climate import ENTITY_ID_FORMAT, ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( + ENTITY_ID_FORMAT, FAN_AUTO, FAN_ON, + ClimateEntity, ClimateEntityFeature, HVACMode, ) diff --git a/homeassistant/components/vicare/climate.py b/homeassistant/components/vicare/climate.py index 44b73ddfbd0..9d507ab9913 100644 --- a/homeassistant/components/vicare/climate.py +++ b/homeassistant/components/vicare/climate.py @@ -13,11 +13,11 @@ from PyViCare.PyViCareUtils import ( import requests import voluptuous as vol -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( PRESET_COMFORT, PRESET_ECO, PRESET_NONE, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, diff --git a/homeassistant/components/whirlpool/climate.py b/homeassistant/components/whirlpool/climate.py index 60de41b46f9..729746a0bcf 100644 --- a/homeassistant/components/whirlpool/climate.py +++ b/homeassistant/components/whirlpool/climate.py @@ -8,8 +8,8 @@ from whirlpool.aircon import Aircon, FanSpeed as AirconFanSpeed, Mode as AirconM from whirlpool.auth import Auth from whirlpool.backendselector import BackendSelector -from homeassistant.components.climate import ENTITY_ID_FORMAT, ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( + ENTITY_ID_FORMAT, FAN_AUTO, FAN_HIGH, FAN_LOW, @@ -17,6 +17,7 @@ from homeassistant.components.climate.const import ( FAN_OFF, SWING_HORIZONTAL, SWING_OFF, + ClimateEntity, ClimateEntityFeature, HVACMode, ) diff --git a/homeassistant/components/yolink/climate.py b/homeassistant/components/yolink/climate.py index 84581c29a8d..d79c7d0fa15 100644 --- a/homeassistant/components/yolink/climate.py +++ b/homeassistant/components/yolink/climate.py @@ -3,14 +3,15 @@ from __future__ import annotations from typing import Any -from homeassistant.components.climate import ClimateEntity, ClimateEntityFeature -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, FAN_AUTO, FAN_ON, PRESET_ECO, PRESET_NONE, + ClimateEntity, + ClimateEntityFeature, HVACAction, HVACMode, ) diff --git a/homeassistant/components/zha/climate.py b/homeassistant/components/zha/climate.py index 4585d41d44d..a0e6a59155a 100644 --- a/homeassistant/components/zha/climate.py +++ b/homeassistant/components/zha/climate.py @@ -13,8 +13,7 @@ from typing import Any from zigpy.zcl.clusters.hvac import Fan as F, Thermostat as T -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_HVAC_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, @@ -25,6 +24,7 @@ from homeassistant.components.climate.const import ( PRESET_COMFORT, PRESET_ECO, PRESET_NONE, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, diff --git a/homeassistant/components/zwave_js/climate.py b/homeassistant/components/zwave_js/climate.py index b0a5cdfe295..264bb5e6ee8 100644 --- a/homeassistant/components/zwave_js/climate.py +++ b/homeassistant/components/zwave_js/climate.py @@ -21,16 +21,14 @@ from zwave_js_server.model.driver import Driver from zwave_js_server.model.value import Value as ZwaveValue from homeassistant.components.climate import ( - DEFAULT_MAX_TEMP, - DEFAULT_MIN_TEMP, - ClimateEntity, -) -from homeassistant.components.climate.const import ( ATTR_HVAC_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, + DEFAULT_MAX_TEMP, + DEFAULT_MIN_TEMP, DOMAIN as CLIMATE_DOMAIN, PRESET_NONE, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, From 0d2465cf0a832c2a6a4e647c5938dd1d80949614 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 10 Sep 2022 23:44:03 +0200 Subject: [PATCH 270/955] Expose logbook constants at the top level (#78184) --- homeassistant/components/logbook/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index 1abfcaba6ff..5b528baabed 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -32,14 +32,17 @@ from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass from . import rest_api, websocket_api -from .const import ( +from .const import ( # noqa: F401 ATTR_MESSAGE, DOMAIN, LOGBOOK_ENTITIES_FILTER, + LOGBOOK_ENTRY_CONTEXT_ID, LOGBOOK_ENTRY_DOMAIN, LOGBOOK_ENTRY_ENTITY_ID, + LOGBOOK_ENTRY_ICON, LOGBOOK_ENTRY_MESSAGE, LOGBOOK_ENTRY_NAME, + LOGBOOK_ENTRY_SOURCE, LOGBOOK_FILTERS, ) from .models import LazyEventPartialState # noqa: F401 From 5f2567cd438382cbdd47ea83e5341f86faecca59 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 10 Sep 2022 23:46:30 +0200 Subject: [PATCH 271/955] Use alphabetical order for platforms in pylint plugin (#78126) --- pylint/plugins/hass_enforce_type_hints.py | 28 +++++++++++------------ 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 1cfb36f9398..80ec054159c 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -1816,6 +1816,20 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ], ), ], + "notify": [ + ClassTypeHintMatch( + base_class="BaseNotificationService", + matches=[ + TypeHintMatch( + function_name="send_message", + arg_types={1: "str"}, + kwargs_type="Any", + return_type=None, + has_async_counterpart=True, + ), + ], + ), + ], "number": [ ClassTypeHintMatch( base_class="Entity", @@ -1882,20 +1896,6 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ], ), ], - "notify": [ - ClassTypeHintMatch( - base_class="BaseNotificationService", - matches=[ - TypeHintMatch( - function_name="send_message", - arg_types={1: "str"}, - kwargs_type="Any", - return_type=None, - has_async_counterpart=True, - ), - ], - ), - ], "remote": [ ClassTypeHintMatch( base_class="Entity", From 051974304a7c663386bf0a5b4d1c0730df9fa21e Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 11 Sep 2022 00:27:01 +0000 Subject: [PATCH 272/955] [ci skip] Translation update --- .../amberelectric/translations/de.json | 5 +++++ .../amberelectric/translations/he.json | 8 ++++++++ .../amberelectric/translations/id.json | 5 +++++ .../amberelectric/translations/it.json | 5 +++++ .../amberelectric/translations/nl.json | 4 ++++ .../amberelectric/translations/no.json | 4 ++++ .../components/bluetooth/translations/he.json | 17 +++++++++++++--- .../components/bthome/translations/he.json | 16 +++++++++++++++ .../components/fibaro/translations/nl.json | 9 ++++++++- .../components/icloud/translations/nl.json | 3 +++ .../components/knx/translations/he.json | 6 ++++-- .../components/mqtt/translations/he.json | 6 ++++++ .../components/mysensors/translations/he.json | 6 ++++++ .../components/owntracks/translations/he.json | 2 +- .../components/qingping/translations/he.json | 16 +++++++++++++++ .../components/sensibo/translations/nl.json | 6 ++++++ .../components/sensorpro/translations/he.json | 16 +++++++++++++++ .../thermobeacon/translations/he.json | 16 +++++++++++++++ .../components/thermopro/translations/he.json | 16 +++++++++++++++ .../components/tilt_ble/translations/nl.json | 20 +++++++++++++++++++ .../components/wled/translations/nl.json | 2 +- .../xiaomi_miio/translations/select.he.json | 10 ++++++++++ 22 files changed, 190 insertions(+), 8 deletions(-) create mode 100644 homeassistant/components/amberelectric/translations/he.json create mode 100644 homeassistant/components/bthome/translations/he.json create mode 100644 homeassistant/components/qingping/translations/he.json create mode 100644 homeassistant/components/sensorpro/translations/he.json create mode 100644 homeassistant/components/thermobeacon/translations/he.json create mode 100644 homeassistant/components/thermopro/translations/he.json create mode 100644 homeassistant/components/tilt_ble/translations/nl.json diff --git a/homeassistant/components/amberelectric/translations/de.json b/homeassistant/components/amberelectric/translations/de.json index 34ce233fed8..333755501b9 100644 --- a/homeassistant/components/amberelectric/translations/de.json +++ b/homeassistant/components/amberelectric/translations/de.json @@ -1,5 +1,10 @@ { "config": { + "error": { + "invalid_api_token": "Ung\u00fcltiger API-Schl\u00fcssel", + "no_site": "Kein Standort vorhanden", + "unknown_error": "Unerwarteter Fehler" + }, "step": { "site": { "data": { diff --git a/homeassistant/components/amberelectric/translations/he.json b/homeassistant/components/amberelectric/translations/he.json new file mode 100644 index 00000000000..8999f497df9 --- /dev/null +++ b/homeassistant/components/amberelectric/translations/he.json @@ -0,0 +1,8 @@ +{ + "config": { + "error": { + "invalid_api_token": "\u05de\u05e4\u05ea\u05d7 API \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown_error": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/amberelectric/translations/id.json b/homeassistant/components/amberelectric/translations/id.json index a88fca7c520..85f96921714 100644 --- a/homeassistant/components/amberelectric/translations/id.json +++ b/homeassistant/components/amberelectric/translations/id.json @@ -1,5 +1,10 @@ { "config": { + "error": { + "invalid_api_token": "Kunci API tidak valid", + "no_site": "Tidak ada situs yang disediakan", + "unknown_error": "Kesalahan yang tidak diharapkan" + }, "step": { "site": { "data": { diff --git a/homeassistant/components/amberelectric/translations/it.json b/homeassistant/components/amberelectric/translations/it.json index f181f549fff..0a247160d92 100644 --- a/homeassistant/components/amberelectric/translations/it.json +++ b/homeassistant/components/amberelectric/translations/it.json @@ -1,5 +1,10 @@ { "config": { + "error": { + "invalid_api_token": "Chiave API non valida", + "no_site": "Nessun sito fornito", + "unknown_error": "Errore imprevisto" + }, "step": { "site": { "data": { diff --git a/homeassistant/components/amberelectric/translations/nl.json b/homeassistant/components/amberelectric/translations/nl.json index 263e94962a0..11f0576496e 100644 --- a/homeassistant/components/amberelectric/translations/nl.json +++ b/homeassistant/components/amberelectric/translations/nl.json @@ -1,5 +1,9 @@ { "config": { + "error": { + "invalid_api_token": "Ongeldige API-sleutel", + "unknown_error": "Onverwachte fout" + }, "step": { "site": { "data": { diff --git a/homeassistant/components/amberelectric/translations/no.json b/homeassistant/components/amberelectric/translations/no.json index 74df9323ce5..27c0111934b 100644 --- a/homeassistant/components/amberelectric/translations/no.json +++ b/homeassistant/components/amberelectric/translations/no.json @@ -1,5 +1,9 @@ { "config": { + "error": { + "invalid_api_token": "Ugyldig API-n\u00f8kkel", + "unknown_error": "Uventet feil" + }, "step": { "site": { "data": { diff --git a/homeassistant/components/bluetooth/translations/he.json b/homeassistant/components/bluetooth/translations/he.json index b5740956a9d..ad9bb727c62 100644 --- a/homeassistant/components/bluetooth/translations/he.json +++ b/homeassistant/components/bluetooth/translations/he.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8", - "no_adapters": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05ea\u05d0\u05de\u05d9 \u05e9\u05df \u05db\u05d7\u05d5\u05dc\u05d4" + "no_adapters": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05ea\u05d0\u05de\u05d9 \u05e9\u05df \u05db\u05d7\u05d5\u05dc\u05d4 \u05e9\u05d0\u05d9\u05e0\u05dd \u05de\u05d5\u05d2\u05d3\u05e8\u05d9\u05dd" }, "flow_title": "{name}", "step": { @@ -12,6 +12,15 @@ "enable_bluetooth": { "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05e9\u05df \u05db\u05d7\u05d5\u05dc\u05d4?" }, + "multiple_adapters": { + "data": { + "adapter": "\u05de\u05ea\u05d0\u05dd" + }, + "description": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05de\u05ea\u05d0\u05dd \u05e9\u05df \u05db\u05d7\u05d5\u05dc\u05d4 \u05dc\u05d4\u05ea\u05e7\u05e0\u05d4" + }, + "single_adapter": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea \u05de\u05ea\u05d0\u05dd \u05e9\u05df \u05db\u05d7\u05d5\u05dc\u05d4 {name}?" + }, "user": { "data": { "address": "\u05d4\u05ea\u05e7\u05df" @@ -24,8 +33,10 @@ "step": { "init": { "data": { - "adapter": "\u05de\u05ea\u05d0\u05dd \u05d4\u05e9\u05df \u05d4\u05db\u05d7\u05d5\u05dc\u05d4 \u05dc\u05e9\u05d9\u05de\u05d5\u05e9 \u05dc\u05e1\u05e8\u05d9\u05e7\u05d4" - } + "adapter": "\u05de\u05ea\u05d0\u05dd \u05d4\u05e9\u05df \u05d4\u05db\u05d7\u05d5\u05dc\u05d4 \u05dc\u05e9\u05d9\u05de\u05d5\u05e9 \u05dc\u05e1\u05e8\u05d9\u05e7\u05d4", + "passive": "\u05e1\u05e8\u05d9\u05e7\u05d4 \u05e4\u05e1\u05d9\u05d1\u05d9\u05ea" + }, + "description": "\u05d4\u05d0\u05d6\u05e0\u05d4 \u05e4\u05e1\u05d9\u05d1\u05d9\u05ea \u05d3\u05d5\u05e8\u05e9\u05ea BlueZ 5.63 \u05d5\u05d0\u05d9\u05dc\u05da \u05e2\u05dd \u05ea\u05db\u05d5\u05e0\u05d5\u05ea \u05e0\u05d9\u05e1\u05d9\u05d5\u05e0\u05d9\u05d5\u05ea \u05de\u05d5\u05e4\u05e2\u05dc\u05d5\u05ea." } } } diff --git a/homeassistant/components/bthome/translations/he.json b/homeassistant/components/bthome/translations/he.json new file mode 100644 index 00000000000..47308062d0d --- /dev/null +++ b/homeassistant/components/bthome/translations/he.json @@ -0,0 +1,16 @@ +{ + "config": { + "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/fibaro/translations/nl.json b/homeassistant/components/fibaro/translations/nl.json index 049168c73d8..49b73f2ac91 100644 --- a/homeassistant/components/fibaro/translations/nl.json +++ b/homeassistant/components/fibaro/translations/nl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Apparaat is al geconfigureerd" + "already_configured": "Apparaat is al geconfigureerd", + "reauth_successful": "Herauthenticatie geslaagd" }, "error": { "cannot_connect": "Kan geen verbinding maken", @@ -9,6 +10,12 @@ "unknown": "Onverwachte fout" }, "step": { + "reauth_confirm": { + "data": { + "password": "Wachtwoord" + }, + "description": "Update je wachtwoord voor {username}" + }, "user": { "data": { "import_plugins": "Entiteiten importeren uit fibaro-plug-ins?", diff --git a/homeassistant/components/icloud/translations/nl.json b/homeassistant/components/icloud/translations/nl.json index 522df78a737..30f0410fd60 100644 --- a/homeassistant/components/icloud/translations/nl.json +++ b/homeassistant/components/icloud/translations/nl.json @@ -18,6 +18,9 @@ "description": "Uw eerder ingevoerde wachtwoord voor {username} werkt niet meer. Update uw wachtwoord om deze integratie te blijven gebruiken.", "title": "Integratie herauthenticeren" }, + "reauth_confirm": { + "description": "Je eerder ingevoerde wachtwoord voor {username} werkt niet meer. Update je wachtwoord om deze integratie te blijven gebruiken." + }, "trusted_device": { "data": { "trusted_device": "Vertrouwd apparaat" diff --git a/homeassistant/components/knx/translations/he.json b/homeassistant/components/knx/translations/he.json index ef11ad342ee..dea454b5f6c 100644 --- a/homeassistant/components/knx/translations/he.json +++ b/homeassistant/components/knx/translations/he.json @@ -16,7 +16,8 @@ }, "routing": { "data": { - "multicast_group": "\u05e7\u05d1\u05d5\u05e6\u05ea \u05e9\u05d9\u05d3\u05d5\u05e8 \u05de\u05e8\u05d5\u05d1\u05d4 \u05d4\u05de\u05e9\u05de\u05e9\u05ea \u05dc\u05e0\u05d9\u05ea\u05d5\u05d1" + "multicast_group": "\u05e7\u05d1\u05d5\u05e6\u05ea \u05e9\u05d9\u05d3\u05d5\u05e8 \u05dc\u05e7\u05d1\u05d5\u05e6\u05d4", + "multicast_port": "\u05d9\u05e6\u05d9\u05d0\u05ea \u05e9\u05d9\u05d3\u05d5\u05e8 \u05dc\u05e7\u05d1\u05d5\u05e6\u05d4" } } } @@ -25,7 +26,8 @@ "step": { "init": { "data": { - "multicast_group": "\u05e7\u05d1\u05d5\u05e6\u05ea \u05e9\u05d9\u05d3\u05d5\u05e8 \u05de\u05e8\u05d5\u05d1\u05d4 \u05d4\u05de\u05e9\u05de\u05e9\u05ea \u05dc\u05e0\u05d9\u05ea\u05d5\u05d1 \u05d5\u05d2\u05d9\u05dc\u05d5\u05d9" + "multicast_group": "\u05e7\u05d1\u05d5\u05e6\u05ea \u05e9\u05d9\u05d3\u05d5\u05e8 \u05dc\u05e7\u05d1\u05d5\u05e6\u05d4", + "multicast_port": "\u05d9\u05e6\u05d9\u05d0\u05ea \u05e9\u05d9\u05d3\u05d5\u05e8 \u05dc\u05e7\u05d1\u05d5\u05e6\u05d4" } }, "tunnel": { diff --git a/homeassistant/components/mqtt/translations/he.json b/homeassistant/components/mqtt/translations/he.json index 7dbc50e246f..f67b83b243e 100644 --- a/homeassistant/components/mqtt/translations/he.json +++ b/homeassistant/components/mqtt/translations/he.json @@ -49,6 +49,12 @@ "button_triple_press": "\"{subtype}\" \u05dc\u05d7\u05d9\u05e6\u05d4 \u05de\u05e9\u05d5\u05dc\u05e9\u05ea" } }, + "issues": { + "deprecated_yaml": { + "description": "\u05de\u05d5\u05d2\u05d3\u05e8 \u05d1\u05d0\u05d5\u05e4\u05df \u05d9\u05d3\u05e0\u05d9 MQTT {platform} \u05e0\u05de\u05e6\u05d0 \u05ea\u05d7\u05ea \u05de\u05e4\u05ea\u05d7 \u05e4\u05dc\u05d8\u05e4\u05d5\u05e8\u05de\u05d4 `{platform}`.\n\n\u05d9\u05e9 \u05dc\u05d4\u05e2\u05d1\u05d9\u05e8 \u05d0\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05dc\u05de\u05e4\u05ea\u05d7 \u05d4\u05d0\u05d9\u05e0\u05d8\u05d2\u05e8\u05e6\u05d9\u05d4 `mqtt`\u05d5\u05dc\u05d4\u05e4\u05e2\u05d9\u05dc \u05de\u05d7\u05d3\u05e9 \u05d0\u05ea Home Assistant \u05db\u05d3\u05d9 \u05dc\u05e4\u05ea\u05d5\u05e8 \u05d1\u05e2\u05d9\u05d4 \u05d6\u05d5. \u05d9\u05e9 \u05dc\u05e2\u05d9\u05d9\u05df \u05d1[\u05ea\u05d9\u05e2\u05d5\u05d3]({more_info_url}), \u05dc\u05e7\u05d1\u05dc\u05ea \u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3.", + "title": "MQTT {platform} \u05d4\u05de\u05d5\u05d2\u05d3\u05e8 \u05d1\u05d0\u05d5\u05e4\u05df \u05d9\u05d3\u05e0\u05d9 \u05d6\u05e7\u05d5\u05e7 \u05dc\u05ea\u05e9\u05d5\u05de\u05ea \u05dc\u05d1" + } + }, "options": { "error": { "bad_birth": "\u05e0\u05d5\u05e9\u05d0 \u05dc\u05d9\u05d3\u05d4 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9.", diff --git a/homeassistant/components/mysensors/translations/he.json b/homeassistant/components/mysensors/translations/he.json index bbe627fd878..9787081e32e 100644 --- a/homeassistant/components/mysensors/translations/he.json +++ b/homeassistant/components/mysensors/translations/he.json @@ -4,6 +4,7 @@ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "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", + "mqtt_required": "\u05e9\u05d9\u05dc\u05d5\u05d1 MQTT \u05d0\u05d9\u05e0\u05d5 \u05de\u05d5\u05d2\u05d3\u05e8", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "error": { @@ -28,6 +29,11 @@ "data": { "tcp_port": "\u05e4\u05ea\u05d7\u05d4" } + }, + "select_gateway_type": { + "menu_options": { + "gw_mqtt": "\u05e7\u05d1\u05d9\u05e2\u05ea \u05ea\u05e6\u05d5\u05e8\u05d4 \u05e9\u05dc \u05e9\u05e2\u05e8 MQTT" + } } } } diff --git a/homeassistant/components/owntracks/translations/he.json b/homeassistant/components/owntracks/translations/he.json index 82dddbc7034..64466e9bce7 100644 --- a/homeassistant/components/owntracks/translations/he.json +++ b/homeassistant/components/owntracks/translations/he.json @@ -5,7 +5,7 @@ "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." }, "create_entry": { - "default": "\n\u05d1\u05d0\u05e0\u05d3\u05e8\u05d5\u05d0\u05d9\u05d3, \u05d9\u05e9 \u05dc\u05e4\u05ea\u05d5\u05d7 \u05d0\u05ea [\u05d9\u05d9\u05e9\u05d5\u05dd OwnTracks]({android_url}), \u05dc\u05e2\u05d1\u05d5\u05e8 \u05d0\u05dc \u05d4\u05e2\u05d3\u05e4\u05d5\u05ea -> \u05d7\u05d9\u05d1\u05d5\u05e8. \u05d5\u05dc\u05e9\u05e0\u05d5\u05ea \u05d0\u05ea \u05d4\u05d4\u05d2\u05d3\u05e8\u05d5\u05ea \u05d4\u05d1\u05d0\u05d5\u05ea:\n - \u05de\u05e6\u05d1: HTTP \u05e4\u05e8\u05d8\u05d9\n - \u05de\u05d0\u05e8\u05d7: {webhook_url}\n - \u05d6\u05d9\u05d4\u05d5\u05d9:\n - \u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9: `'<\u05d4\u05e9\u05dd \u05e9\u05dc\u05da>'`\n - \u05de\u05d6\u05d4\u05d4 \u05d4\u05ea\u05e7\u05df: `'<\u05e9\u05dd \u05d4\u05d4\u05ea\u05e7\u05df \u05e9\u05dc\u05da>'`\n\n\u05d1\u05de\u05d5\u05e6\u05e8\u05d9 \u05d0\u05e4\u05dc, \u05d9\u05e9 \u05dc\u05e4\u05ea\u05d5\u05d7 \u05d0\u05ea [\u05d9\u05d9\u05e9\u05d5\u05dd OwnTracks]({ios_url}), \u05dc\u05d4\u05e7\u05d9\u05e9 \u05e2\u05dc \u05d4\u05e1\u05de\u05dc (i) \u05d1\u05e4\u05d9\u05e0\u05d4 \u05d4\u05d9\u05de\u05e0\u05d9\u05ea \u05d4\u05e2\u05dc\u05d9\u05d5\u05e0\u05d4 -> \u05d4\u05d2\u05d3\u05e8\u05d5\u05ea. \u05d5\u05dc\u05e9\u05e0\u05d5\u05ea \u05d0\u05ea \u05d4\u05d4\u05d2\u05d3\u05e8\u05d5\u05ea \u05d4\u05d1\u05d0\u05d5\u05ea:\n - \u05de\u05e6\u05d1: HTTP\n - \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8: {webhook_url}\n - \u05d4\u05e4\u05e2\u05dc\u05ea \u05d0\u05d9\u05de\u05d5\u05ea\n - \u05de\u05d6\u05d4\u05d4 \u05de\u05e9\u05ea\u05de\u05e9: `'<\u05d4\u05e9\u05dd \u05e9\u05dc\u05da>'`\n\n{secret}\n\n\u05e0\u05d9\u05ea\u05df \u05dc\u05e7\u05e8\u05d5\u05d0 \u05d0\u05ea [\u05d4\u05ea\u05d9\u05e2\u05d5\u05d3]({docs_url}) \u05dc\u05e7\u05d1\u05dc\u05ea \u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3." + "default": "\n\u05d1\u05d0\u05e0\u05d3\u05e8\u05d5\u05d0\u05d9\u05d3, \u05d9\u05e9 \u05dc\u05e4\u05ea\u05d5\u05d7 \u05d0\u05ea [\u05d9\u05d9\u05e9\u05d5\u05dd OwnTracks]({android_url}), \u05dc\u05e2\u05d1\u05d5\u05e8 \u05d0\u05dc \u05d4\u05e2\u05d3\u05e4\u05d5\u05ea -> \u05d7\u05d9\u05d1\u05d5\u05e8. \u05d5\u05dc\u05e9\u05e0\u05d5\u05ea \u05d0\u05ea \u05d4\u05d4\u05d2\u05d3\u05e8\u05d5\u05ea \u05d4\u05d1\u05d0\u05d5\u05ea:\n - \u05de\u05e6\u05d1: HTTP \u05e4\u05e8\u05d8\u05d9\n - \u05de\u05d0\u05e8\u05d7: {webhook_url}\n - \u05d6\u05d9\u05d4\u05d5\u05d9:\n - \u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9: `'<\u05d4\u05e9\u05dd \u05e9\u05dc\u05da>'`\n - \u05de\u05d6\u05d4\u05d4 \u05d4\u05ea\u05e7\u05df: `'<\u05e9\u05dd \u05d4\u05d4\u05ea\u05e7\u05df \u05e9\u05dc\u05da>'`\n\n\u05d1\u05de\u05d5\u05e6\u05e8\u05d9 \u05d0\u05e4\u05dc, \u05d9\u05e9 \u05dc\u05e4\u05ea\u05d5\u05d7 \u05d0\u05ea [\u05d9\u05d9\u05e9\u05d5\u05dd OwnTracks]({ios_url}), \u05dc\u05d4\u05e7\u05d9\u05e9 \u05e2\u05dc \u05d4\u05e1\u05de\u05dc\u05d9\u05dc (i) \u05d1\u05e4\u05d9\u05e0\u05d4 \u05d4\u05d9\u05de\u05e0\u05d9\u05ea \u05d4\u05e2\u05dc\u05d9\u05d5\u05e0\u05d4 -> \u05d4\u05d2\u05d3\u05e8\u05d5\u05ea. \u05d5\u05dc\u05e9\u05e0\u05d5\u05ea \u05d0\u05ea \u05d4\u05d4\u05d2\u05d3\u05e8\u05d5\u05ea \u05d4\u05d1\u05d0\u05d5\u05ea:\n - \u05de\u05e6\u05d1: HTTP\n - \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8: {webhook_url}\n - \u05d4\u05e4\u05e2\u05dc\u05ea \u05d0\u05d9\u05de\u05d5\u05ea\n - \u05de\u05d6\u05d4\u05d4 \u05de\u05e9\u05ea\u05de\u05e9: `'<\u05d4\u05e9\u05dd \u05e9\u05dc\u05da>'`\n\n{secret}\n\n\u05e0\u05d9\u05ea\u05df \u05dc\u05e7\u05e8\u05d5\u05d0 \u05d0\u05ea [\u05d4\u05ea\u05d9\u05e2\u05d5\u05d3]({docs_url}) \u05dc\u05e7\u05d1\u05dc\u05ea \u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3." }, "step": { "user": { diff --git a/homeassistant/components/qingping/translations/he.json b/homeassistant/components/qingping/translations/he.json new file mode 100644 index 00000000000..47308062d0d --- /dev/null +++ b/homeassistant/components/qingping/translations/he.json @@ -0,0 +1,16 @@ +{ + "config": { + "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/sensibo/translations/nl.json b/homeassistant/components/sensibo/translations/nl.json index fe04b6b64a8..f853edc6ac6 100644 --- a/homeassistant/components/sensibo/translations/nl.json +++ b/homeassistant/components/sensibo/translations/nl.json @@ -15,11 +15,17 @@ "reauth_confirm": { "data": { "api_key": "API-sleutel" + }, + "data_description": { + "api_key": "Volg de documentatie om een nieuwe API-sleutel te verkrijgen." } }, "user": { "data": { "api_key": "API-sleutel" + }, + "data_description": { + "api_key": "Volg de documentatie om een API-sleutel te verkrijgen." } } } diff --git a/homeassistant/components/sensorpro/translations/he.json b/homeassistant/components/sensorpro/translations/he.json new file mode 100644 index 00000000000..47308062d0d --- /dev/null +++ b/homeassistant/components/sensorpro/translations/he.json @@ -0,0 +1,16 @@ +{ + "config": { + "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/thermobeacon/translations/he.json b/homeassistant/components/thermobeacon/translations/he.json new file mode 100644 index 00000000000..47308062d0d --- /dev/null +++ b/homeassistant/components/thermobeacon/translations/he.json @@ -0,0 +1,16 @@ +{ + "config": { + "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/thermopro/translations/he.json b/homeassistant/components/thermopro/translations/he.json new file mode 100644 index 00000000000..47308062d0d --- /dev/null +++ b/homeassistant/components/thermopro/translations/he.json @@ -0,0 +1,16 @@ +{ + "config": { + "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/tilt_ble/translations/nl.json b/homeassistant/components/tilt_ble/translations/nl.json new file mode 100644 index 00000000000..10710b9d955 --- /dev/null +++ b/homeassistant/components/tilt_ble/translations/nl.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_in_progress": "De configuratie is momenteel al bezig", + "no_devices_found": "Geen apparaten gevonden op het netwerk" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Wilt u {name} instellen?" + }, + "user": { + "data": { + "address": "Apparaat" + }, + "description": "Kies een apparaat om in te stellen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wled/translations/nl.json b/homeassistant/components/wled/translations/nl.json index d1ba5b75ec3..f732d538489 100644 --- a/homeassistant/components/wled/translations/nl.json +++ b/homeassistant/components/wled/translations/nl.json @@ -14,7 +14,7 @@ "data": { "host": "Host" }, - "description": "Stel uw WLED-integratie in met Home Assistant." + "description": "Stel je WLED-integratie in met Home Assistant." }, "zeroconf_confirm": { "description": "Wil je de WLED genaamd `{name}` toevoegen aan Home Assistant?", diff --git a/homeassistant/components/xiaomi_miio/translations/select.he.json b/homeassistant/components/xiaomi_miio/translations/select.he.json index 2cffbc3b457..315b4bd181d 100644 --- a/homeassistant/components/xiaomi_miio/translations/select.he.json +++ b/homeassistant/components/xiaomi_miio/translations/select.he.json @@ -1,9 +1,19 @@ { "state": { + "xiaomi_miio__display_orientation": { + "forward": "\u05e7\u05d3\u05d9\u05de\u05d4", + "left": "\u05e9\u05de\u05d0\u05dc", + "right": "\u05d9\u05de\u05d9\u05df" + }, "xiaomi_miio__led_brightness": { "bright": "\u05d1\u05d4\u05d9\u05e8", "dim": "\u05de\u05e2\u05d5\u05de\u05e2\u05dd", "off": "\u05db\u05d1\u05d5\u05d9" + }, + "xiaomi_miio__ptc_level": { + "high": "\u05d2\u05d1\u05d5\u05d4", + "low": "\u05e0\u05de\u05d5\u05da", + "medium": "\u05d1\u05d9\u05e0\u05d5\u05e0\u05d9" } } } \ No newline at end of file From e77058b762287beec0688f76fc4dc6e58d867f60 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 11 Sep 2022 03:04:05 -0500 Subject: [PATCH 273/955] Bump led_ble to 0.8.5 (#78215) * Bump led_ble to 0.8.4 Changelog: https://github.com/Bluetooth-Devices/led-ble/compare/v0.8.3...v0.8.4 * bump again --- homeassistant/components/led_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/led_ble/manifest.json b/homeassistant/components/led_ble/manifest.json index 261d27726e5..9bcb3f860e1 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/ble_ble", - "requirements": ["led-ble==0.8.3"], + "requirements": ["led-ble==0.8.5"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "bluetooth": [ diff --git a/requirements_all.txt b/requirements_all.txt index 03d2e5af65d..18a7f14c670 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -968,7 +968,7 @@ lakeside==0.12 laundrify_aio==1.1.2 # homeassistant.components.led_ble -led-ble==0.8.3 +led-ble==0.8.5 # homeassistant.components.foscam libpyfoscam==1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 50c356aca78..20eda669f2d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -706,7 +706,7 @@ lacrosse-view==0.0.9 laundrify_aio==1.1.2 # homeassistant.components.led_ble -led-ble==0.8.3 +led-ble==0.8.5 # homeassistant.components.foscam libpyfoscam==1.0 From d77ca997911fdcad80e0a957d9402a7def9fd55b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 11 Sep 2022 03:06:13 -0500 Subject: [PATCH 274/955] Bump bluetooth-adapters to 0.4.1 (#78205) Switches to dbus-fast which fixes a file descriptor leak --- 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 cda4158086f..3c9ef7a85b7 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -6,7 +6,7 @@ "quality_scale": "internal", "requirements": [ "bleak==0.16.0", - "bluetooth-adapters==0.3.6", + "bluetooth-adapters==0.4.1", "bluetooth-auto-recovery==0.3.2" ], "codeowners": ["@bdraco"], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index bf93aeba6e5..4f41518361f 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ attrs==21.2.0 awesomeversion==22.8.0 bcrypt==3.1.7 bleak==0.16.0 -bluetooth-adapters==0.3.6 +bluetooth-adapters==0.4.1 bluetooth-auto-recovery==0.3.2 certifi>=2021.5.30 ciso8601==2.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 18a7f14c670..426a2627f20 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -430,7 +430,7 @@ bluemaestro-ble==0.2.0 # bluepy==1.3.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.3.6 +bluetooth-adapters==0.4.1 # homeassistant.components.bluetooth bluetooth-auto-recovery==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 20eda669f2d..87380b8c353 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -341,7 +341,7 @@ blinkpy==0.19.0 bluemaestro-ble==0.2.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.3.6 +bluetooth-adapters==0.4.1 # homeassistant.components.bluetooth bluetooth-auto-recovery==0.3.2 From 49222d6bc8e124a9101266386f9bf5b7cf12b309 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 11 Sep 2022 02:17:36 -0600 Subject: [PATCH 275/955] Bump `regenmaschine` to 2022.09.1 (#78210) --- homeassistant/components/rainmachine/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rainmachine/manifest.json b/homeassistant/components/rainmachine/manifest.json index a6cc86e5055..6a87110f6f6 100644 --- a/homeassistant/components/rainmachine/manifest.json +++ b/homeassistant/components/rainmachine/manifest.json @@ -3,7 +3,7 @@ "name": "RainMachine", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/rainmachine", - "requirements": ["regenmaschine==2022.09.0"], + "requirements": ["regenmaschine==2022.09.1"], "codeowners": ["@bachya"], "iot_class": "local_polling", "homekit": { diff --git a/requirements_all.txt b/requirements_all.txt index 426a2627f20..07449f3285f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2121,7 +2121,7 @@ raincloudy==0.0.7 raspyrfm-client==1.2.8 # homeassistant.components.rainmachine -regenmaschine==2022.09.0 +regenmaschine==2022.09.1 # homeassistant.components.renault renault-api==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 87380b8c353..5c908a0640a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1454,7 +1454,7 @@ radios==0.1.1 radiotherm==2.1.0 # homeassistant.components.rainmachine -regenmaschine==2022.09.0 +regenmaschine==2022.09.1 # homeassistant.components.renault renault-api==0.1.11 From 9a61cc07c7309ae735ccf5beee7d298c41b4ddf5 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sun, 11 Sep 2022 16:36:03 +0200 Subject: [PATCH 276/955] Use new media player enums in emby (#78098) --- homeassistant/components/emby/media_player.py | 38 ++++++++----------- 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/emby/media_player.py b/homeassistant/components/emby/media_player.py index 014f9e2ac1d..b573aef65ea 100644 --- a/homeassistant/components/emby/media_player.py +++ b/homeassistant/components/emby/media_player.py @@ -10,12 +10,8 @@ from homeassistant.components.media_player import ( PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, -) -from homeassistant.components.media_player.const import ( - MEDIA_TYPE_CHANNEL, - MEDIA_TYPE_MOVIE, - MEDIA_TYPE_MUSIC, - MEDIA_TYPE_TVSHOW, + MediaPlayerState, + MediaType, ) from homeassistant.const import ( CONF_API_KEY, @@ -25,10 +21,6 @@ from homeassistant.const import ( DEVICE_DEFAULT_NAME, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, - STATE_IDLE, - STATE_OFF, - STATE_PAUSED, - STATE_PLAYING, ) from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv @@ -39,7 +31,6 @@ import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) MEDIA_TYPE_TRAILER = "trailer" -MEDIA_TYPE_GENERIC_VIDEO = "video" DEFAULT_HOST = "localhost" DEFAULT_PORT = 8096 @@ -189,17 +180,18 @@ class EmbyDevice(MediaPlayerEntity): return f"Emby {self.device.name}" or DEVICE_DEFAULT_NAME @property - def state(self): + def state(self) -> MediaPlayerState | None: """Return the state of the device.""" state = self.device.state if state == "Paused": - return STATE_PAUSED + return MediaPlayerState.PAUSED if state == "Playing": - return STATE_PLAYING + return MediaPlayerState.PLAYING if state == "Idle": - return STATE_IDLE + return MediaPlayerState.IDLE if state == "Off": - return STATE_OFF + return MediaPlayerState.OFF + return None @property def app_name(self): @@ -213,23 +205,23 @@ class EmbyDevice(MediaPlayerEntity): return self.device.media_id @property - def media_content_type(self): + def media_content_type(self) -> MediaType | str | None: """Content type of current playing media.""" media_type = self.device.media_type if media_type == "Episode": - return MEDIA_TYPE_TVSHOW + return MediaType.TVSHOW if media_type == "Movie": - return MEDIA_TYPE_MOVIE + return MediaType.MOVIE if media_type == "Trailer": return MEDIA_TYPE_TRAILER if media_type == "Music": - return MEDIA_TYPE_MUSIC + return MediaType.MUSIC if media_type == "Video": - return MEDIA_TYPE_GENERIC_VIDEO + return MediaType.VIDEO if media_type == "Audio": - return MEDIA_TYPE_MUSIC + return MediaType.MUSIC if media_type == "TvChannel": - return MEDIA_TYPE_CHANNEL + return MediaType.CHANNEL return None @property From b0777e6280ee68b2ff5b8fa9a06d33ea2451b876 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sun, 11 Sep 2022 16:48:18 +0200 Subject: [PATCH 277/955] Use new media player enums in demo (#78114) * Use new media player enums in demo * Adjust import location --- homeassistant/components/demo/media_player.py | 54 +++++++------------ 1 file changed, 20 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/demo/media_player.py b/homeassistant/components/demo/media_player.py index d5098dc4586..8dbf059c2ca 100644 --- a/homeassistant/components/demo/media_player.py +++ b/homeassistant/components/demo/media_player.py @@ -7,16 +7,12 @@ from typing import Any from homeassistant.components.media_player import ( MediaPlayerDeviceClass, MediaPlayerEntity, -) -from homeassistant.components.media_player.const import ( - MEDIA_TYPE_MOVIE, - MEDIA_TYPE_MUSIC, - MEDIA_TYPE_TVSHOW, - REPEAT_MODE_OFF, MediaPlayerEntityFeature, + MediaPlayerState, + MediaType, + RepeatMode, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import STATE_OFF, STATE_PAUSED, STATE_PLAYING from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -118,7 +114,7 @@ class AbstractDemoPlayer(MediaPlayerEntity): ) -> None: """Initialize the demo device.""" self._attr_name = name - self._player_state = STATE_PLAYING + self._player_state = MediaPlayerState.PLAYING self._volume_level = 1.0 self._volume_muted = False self._shuffle = False @@ -163,12 +159,12 @@ class AbstractDemoPlayer(MediaPlayerEntity): def turn_on(self) -> None: """Turn the media player on.""" - self._player_state = STATE_PLAYING + self._player_state = MediaPlayerState.PLAYING self.schedule_update_ha_state() def turn_off(self) -> None: """Turn the media player off.""" - self._player_state = STATE_OFF + self._player_state = MediaPlayerState.OFF self.schedule_update_ha_state() def mute_volume(self, mute: bool) -> None: @@ -193,17 +189,17 @@ class AbstractDemoPlayer(MediaPlayerEntity): def media_play(self) -> None: """Send play command.""" - self._player_state = STATE_PLAYING + self._player_state = MediaPlayerState.PLAYING self.schedule_update_ha_state() def media_pause(self) -> None: """Send pause command.""" - self._player_state = STATE_PAUSED + self._player_state = MediaPlayerState.PAUSED self.schedule_update_ha_state() def media_stop(self) -> None: """Send stop command.""" - self._player_state = STATE_OFF + self._player_state = MediaPlayerState.OFF self.schedule_update_ha_state() def set_shuffle(self, shuffle: bool) -> None: @@ -222,6 +218,8 @@ class DemoYoutubePlayer(AbstractDemoPlayer): # We only implement the methods that we support + _attr_media_content_type = MediaType.MOVIE + def __init__( self, name: str, youtube_id: str, media_title: str, duration: int ) -> None: @@ -238,11 +236,6 @@ class DemoYoutubePlayer(AbstractDemoPlayer): """Return the content ID of current playing media.""" return self.youtube_id - @property - def media_content_type(self) -> str: - """Return the content type of current playing media.""" - return MEDIA_TYPE_MOVIE - @property def media_duration(self) -> int: """Return the duration of current playing media in seconds.""" @@ -276,7 +269,7 @@ class DemoYoutubePlayer(AbstractDemoPlayer): position = self._progress - if self._player_state == STATE_PLAYING: + if self._player_state == MediaPlayerState.PLAYING: position += int( (dt_util.utcnow() - self._progress_updated_at).total_seconds() ) @@ -289,7 +282,7 @@ class DemoYoutubePlayer(AbstractDemoPlayer): Returns value from homeassistant.util.dt.utcnow(). """ - if self._player_state == STATE_PLAYING: + if self._player_state == MediaPlayerState.PLAYING: return self._progress_updated_at return None @@ -310,6 +303,8 @@ class DemoMusicPlayer(AbstractDemoPlayer): # We only implement the methods that we support + _attr_media_content_type = MediaType.MUSIC + tracks = [ ("Technohead", "I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)"), ("Paul Elstak", "Luv U More"), @@ -338,7 +333,7 @@ class DemoMusicPlayer(AbstractDemoPlayer): super().__init__(name) self._cur_track = 0 self._group_members: list[str] = [] - self._repeat = REPEAT_MODE_OFF + self._repeat = RepeatMode.OFF @property def group_members(self) -> list[str]: @@ -350,11 +345,6 @@ class DemoMusicPlayer(AbstractDemoPlayer): """Return the content ID of current playing media.""" return "bounzz-1" - @property - def media_content_type(self) -> str: - """Return the content type of current playing media.""" - return MEDIA_TYPE_MUSIC - @property def media_duration(self) -> int: """Return the duration of current playing media in seconds.""" @@ -386,7 +376,7 @@ class DemoMusicPlayer(AbstractDemoPlayer): return self._cur_track + 1 @property - def repeat(self) -> str: + def repeat(self) -> RepeatMode: """Return current repeat mode.""" return self._repeat @@ -411,10 +401,10 @@ class DemoMusicPlayer(AbstractDemoPlayer): """Clear players playlist.""" self.tracks = [] self._cur_track = 0 - self._player_state = STATE_OFF + self._player_state = MediaPlayerState.OFF self.schedule_update_ha_state() - def set_repeat(self, repeat: str) -> None: + def set_repeat(self, repeat: RepeatMode) -> None: """Enable/disable repeat mode.""" self._repeat = repeat self.schedule_update_ha_state() @@ -438,6 +428,7 @@ class DemoTVShowPlayer(AbstractDemoPlayer): # We only implement the methods that we support _attr_device_class = MediaPlayerDeviceClass.TV + _attr_media_content_type = MediaType.TVSHOW def __init__(self) -> None: """Initialize the demo device.""" @@ -452,11 +443,6 @@ class DemoTVShowPlayer(AbstractDemoPlayer): """Return the content ID of current playing media.""" return "house-of-cards-1" - @property - def media_content_type(self) -> str: - """Return the content type of current playing media.""" - return MEDIA_TYPE_TVSHOW - @property def media_duration(self) -> int: """Return the duration of current playing media in seconds.""" From 29be6d17b02b4b2362f9fe1a27e31be56ebefa8d Mon Sep 17 00:00:00 2001 From: Artem Draft Date: Sun, 11 Sep 2022 19:12:04 +0300 Subject: [PATCH 278/955] Add is_host_valid util (#76589) --- .../components/braviatv/config_flow.py | 15 ++---------- .../components/brother/config_flow.py | 16 ++----------- .../components/dunehd/config_flow.py | 18 ++------------ homeassistant/components/vilfo/config_flow.py | 15 ++---------- homeassistant/util/network.py | 15 ++++++++++++ tests/util/test_network.py | 24 +++++++++++++++++++ 6 files changed, 47 insertions(+), 56 deletions(-) diff --git a/homeassistant/components/braviatv/config_flow.py b/homeassistant/components/braviatv/config_flow.py index f89880caf89..75a8d5873ef 100644 --- a/homeassistant/components/braviatv/config_flow.py +++ b/homeassistant/components/braviatv/config_flow.py @@ -1,9 +1,6 @@ """Config flow to configure the Bravia TV integration.""" from __future__ import annotations -from contextlib import suppress -import ipaddress -import re from typing import Any from aiohttp import CookieJar @@ -17,6 +14,7 @@ from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_create_clientsession import homeassistant.helpers.config_validation as cv +from homeassistant.util.network import is_host_valid from . import BraviaTVCoordinator from .const import ( @@ -30,15 +28,6 @@ from .const import ( ) -def host_valid(host: str) -> bool: - """Return True if hostname or IP address is valid.""" - with suppress(ValueError): - if ipaddress.ip_address(host).version in [4, 6]: - return True - disallowed = re.compile(r"[^a-zA-Z\d\-]") - return all(x and not disallowed.search(x) for x in host.split(".")) - - class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Bravia TV integration.""" @@ -82,7 +71,7 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: host = user_input[CONF_HOST] - if host_valid(host): + if is_host_valid(host): session = async_create_clientsession( self.hass, cookie_jar=CookieJar(unsafe=True, quote_cookie=False), diff --git a/homeassistant/components/brother/config_flow.py b/homeassistant/components/brother/config_flow.py index bcedc65d7ff..27f3c73cd63 100644 --- a/homeassistant/components/brother/config_flow.py +++ b/homeassistant/components/brother/config_flow.py @@ -1,8 +1,6 @@ """Adds config flow for Brother Printer.""" from __future__ import annotations -import ipaddress -import re from typing import Any from brother import Brother, SnmpError, UnsupportedModel @@ -12,6 +10,7 @@ from homeassistant import config_entries, exceptions from homeassistant.components import zeroconf from homeassistant.const import CONF_HOST, CONF_TYPE from homeassistant.data_entry_flow import FlowResult +from homeassistant.util.network import is_host_valid from .const import DOMAIN, PRINTER_TYPES from .utils import get_snmp_engine @@ -24,17 +23,6 @@ DATA_SCHEMA = vol.Schema( ) -def host_valid(host: str) -> bool: - """Return True if hostname or IP address is valid.""" - try: - if ipaddress.ip_address(host).version in [4, 6]: - return True - except ValueError: - pass - disallowed = re.compile(r"[^a-zA-Z\d\-]") - return all(x and not disallowed.search(x) for x in host.split(".")) - - class BrotherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Brother Printer.""" @@ -53,7 +41,7 @@ class BrotherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: try: - if not host_valid(user_input[CONF_HOST]): + if not is_host_valid(user_input[CONF_HOST]): raise InvalidHost() snmp_engine = get_snmp_engine(self.hass) diff --git a/homeassistant/components/dunehd/config_flow.py b/homeassistant/components/dunehd/config_flow.py index b5a656716b0..fac2e245633 100644 --- a/homeassistant/components/dunehd/config_flow.py +++ b/homeassistant/components/dunehd/config_flow.py @@ -1,8 +1,6 @@ """Adds config flow for Dune HD integration.""" from __future__ import annotations -import ipaddress -import re from typing import Any from pdunehd import DuneHDPlayer @@ -11,23 +9,11 @@ import voluptuous as vol from homeassistant import config_entries, exceptions from homeassistant.const import CONF_HOST from homeassistant.data_entry_flow import FlowResult +from homeassistant.util.network import is_host_valid from .const import DOMAIN -def host_valid(host: str) -> bool: - """Return True if hostname or IP address is valid.""" - try: - if ipaddress.ip_address(host).version in (4, 6): - return True - except ValueError: - pass - if len(host) > 253: - return False - allowed = re.compile(r"(?!-)[A-Z\d\-\_]{1,63}(? bool: return True +def is_host_valid(host: str) -> bool: + """Check if a given string is an IP address or valid hostname.""" + if is_ip_address(host): + return True + if len(host) > 255: + return False + if re.match(r"^[0-9\.]+$", host): # reject invalid IPv4 + return False + if host.endswith("."): # dot at the end is correct + host = host[:-1] + allowed = re.compile(r"(?!-)[A-Z\d\-]{1,63}(? str: """Normalize a given URL.""" url = yarl.URL(address.rstrip("/")) diff --git a/tests/util/test_network.py b/tests/util/test_network.py index 7339b6dc51d..43c50ac674f 100644 --- a/tests/util/test_network.py +++ b/tests/util/test_network.py @@ -80,6 +80,30 @@ def test_is_ipv6_address(): assert network_util.is_ipv6_address("8.8.8.8") is False +def test_is_valid_host(): + """Test if strings are IPv6 addresses.""" + assert network_util.is_host_valid("::1") + assert network_util.is_host_valid("::ffff:127.0.0.0") + assert network_util.is_host_valid("2001:0db8:85a3:0000:0000:8a2e:0370:7334") + assert network_util.is_host_valid("8.8.8.8") + assert network_util.is_host_valid("local") + assert network_util.is_host_valid("host-host") + assert network_util.is_host_valid("example.com") + assert network_util.is_host_valid("example.com.") + assert network_util.is_host_valid("Example123.com") + assert not network_util.is_host_valid("") + assert not network_util.is_host_valid("192.168.0.1:8080") + assert not network_util.is_host_valid("192.168.0.999") + assert not network_util.is_host_valid("2001:hb8::1:0:0:1") + assert not network_util.is_host_valid("-host-host") + assert not network_util.is_host_valid("host-host-") + assert not network_util.is_host_valid("host_host") + assert not network_util.is_host_valid("example.com/path") + assert not network_util.is_host_valid("example.com:8080") + assert not network_util.is_host_valid("verylonghostname" * 4) + assert not network_util.is_host_valid("verydeepdomain." * 18) + + def test_normalize_url(): """Test the normalizing of URLs.""" assert network_util.normalize_url("http://example.com") == "http://example.com" From 93b7f604d5e94c38964dca47daa2f84c9bc253f0 Mon Sep 17 00:00:00 2001 From: Vincent Knoop Pathuis <48653141+vpathuis@users.noreply.github.com> Date: Sun, 11 Sep 2022 18:18:01 +0200 Subject: [PATCH 279/955] Landis+Gyr integration: increase timeout and add debug logging (#78025) --- homeassistant/components/landisgyr_heat_meter/__init__.py | 4 +--- .../components/landisgyr_heat_meter/config_flow.py | 6 ++++-- homeassistant/components/landisgyr_heat_meter/const.py | 1 + 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/landisgyr_heat_meter/__init__.py b/homeassistant/components/landisgyr_heat_meter/__init__.py index 4321f53bed6..3ef235ff8af 100644 --- a/homeassistant/components/landisgyr_heat_meter/__init__.py +++ b/homeassistant/components/landisgyr_heat_meter/__init__.py @@ -31,9 +31,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.info("Polling on %s", entry.data[CONF_DEVICE]) return await hass.async_add_executor_job(api.read) - # No automatic polling and no initial refresh of data is being done at this point, - # to prevent battery drain. The user will have to do it manually. - + # Polling is only daily to prevent battery drain. coordinator = DataUpdateCoordinator( hass, _LOGGER, diff --git a/homeassistant/components/landisgyr_heat_meter/config_flow.py b/homeassistant/components/landisgyr_heat_meter/config_flow.py index e3dbbb7433b..2e244a9a65f 100644 --- a/homeassistant/components/landisgyr_heat_meter/config_flow.py +++ b/homeassistant/components/landisgyr_heat_meter/config_flow.py @@ -14,7 +14,7 @@ from homeassistant import config_entries from homeassistant.const import CONF_DEVICE from homeassistant.exceptions import HomeAssistantError -from .const import DOMAIN +from .const import DOMAIN, ULTRAHEAT_TIMEOUT _LOGGER = logging.getLogger(__name__) @@ -43,6 +43,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): dev_path = await self.hass.async_add_executor_job( get_serial_by_id, user_input[CONF_DEVICE] ) + _LOGGER.debug("Using this path : %s", dev_path) try: return await self.validate_and_create_entry(dev_path) @@ -76,6 +77,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Try to connect to the device path and return an entry.""" 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) self._abort_if_unique_id_configured() data = { @@ -94,7 +96,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): reader = UltraheatReader(port) heat_meter = HeatMeterService(reader) try: - async with async_timeout.timeout(10): + 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) diff --git a/homeassistant/components/landisgyr_heat_meter/const.py b/homeassistant/components/landisgyr_heat_meter/const.py index 70008890d1f..55a6c65892c 100644 --- a/homeassistant/components/landisgyr_heat_meter/const.py +++ b/homeassistant/components/landisgyr_heat_meter/const.py @@ -11,6 +11,7 @@ from homeassistant.helpers.entity import EntityCategory DOMAIN = "landisgyr_heat_meter" GJ_TO_MWH = 0.277778 # conversion factor +ULTRAHEAT_TIMEOUT = 30 # reading the IR port can take some time HEAT_METER_SENSOR_TYPES = ( SensorEntityDescription( From f0053066ca1dbc622dec6696d094af436ea52837 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 11 Sep 2022 18:18:38 +0200 Subject: [PATCH 280/955] Bump pysensibo to 1.0.20 (#78222) --- homeassistant/components/sensibo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensibo/manifest.json b/homeassistant/components/sensibo/manifest.json index a2a7cbe3bd0..4f89148a21c 100644 --- a/homeassistant/components/sensibo/manifest.json +++ b/homeassistant/components/sensibo/manifest.json @@ -2,7 +2,7 @@ "domain": "sensibo", "name": "Sensibo", "documentation": "https://www.home-assistant.io/integrations/sensibo", - "requirements": ["pysensibo==1.0.19"], + "requirements": ["pysensibo==1.0.20"], "config_flow": true, "codeowners": ["@andrey-git", "@gjohansson-ST"], "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 07449f3285f..8b6f039fc44 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1841,7 +1841,7 @@ pysaj==0.0.16 pysdcp==1 # homeassistant.components.sensibo -pysensibo==1.0.19 +pysensibo==1.0.20 # homeassistant.components.serial # homeassistant.components.zha diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5c908a0640a..178cd3bc452 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1288,7 +1288,7 @@ pyruckus==0.16 pysabnzbd==1.1.1 # homeassistant.components.sensibo -pysensibo==1.0.19 +pysensibo==1.0.20 # homeassistant.components.serial # homeassistant.components.zha From 4da10a50ad5b75ff06f80f935f8fb8ac8da1907d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 11 Sep 2022 11:18:52 -0500 Subject: [PATCH 281/955] Bump yalexs-ble to 1.8.1 (#78225) --- homeassistant/components/yalexs_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/yalexs_ble/manifest.json b/homeassistant/components/yalexs_ble/manifest.json index da4bf1cf6d2..a685d750077 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.7.1"], + "requirements": ["yalexs-ble==1.8.1"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "bluetooth": [{ "manufacturer_id": 465 }], diff --git a/requirements_all.txt b/requirements_all.txt index 8b6f039fc44..66e36322959 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2548,7 +2548,7 @@ xs1-api-client==3.0.0 yalesmartalarmclient==0.3.9 # homeassistant.components.yalexs_ble -yalexs-ble==1.7.1 +yalexs-ble==1.8.1 # homeassistant.components.august yalexs==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 178cd3bc452..d0513431318 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1752,7 +1752,7 @@ xmltodict==0.13.0 yalesmartalarmclient==0.3.9 # homeassistant.components.yalexs_ble -yalexs-ble==1.7.1 +yalexs-ble==1.8.1 # homeassistant.components.august yalexs==1.2.1 From 7e0652f80ea0b9b9601de4c433809449e192be60 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 11 Sep 2022 11:19:01 -0500 Subject: [PATCH 282/955] Bump led-ble to 0.9.1 (#78226) --- homeassistant/components/led_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/led_ble/manifest.json b/homeassistant/components/led_ble/manifest.json index 9bcb3f860e1..1f27837b89e 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/ble_ble", - "requirements": ["led-ble==0.8.5"], + "requirements": ["led-ble==0.9.1"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "bluetooth": [ diff --git a/requirements_all.txt b/requirements_all.txt index 66e36322959..1937890b8ca 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -968,7 +968,7 @@ lakeside==0.12 laundrify_aio==1.1.2 # homeassistant.components.led_ble -led-ble==0.8.5 +led-ble==0.9.1 # homeassistant.components.foscam libpyfoscam==1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d0513431318..38fe3412f3a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -706,7 +706,7 @@ lacrosse-view==0.0.9 laundrify_aio==1.1.2 # homeassistant.components.led_ble -led-ble==0.8.5 +led-ble==0.9.1 # homeassistant.components.foscam libpyfoscam==1.0 From 5a9dfa9df96592a4685c41a4f32c8da9fea4323a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 11 Sep 2022 11:19:12 -0500 Subject: [PATCH 283/955] Bump aiohomekit to 1.5.6 (#78228) --- 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 b71156c9bc7..a4cbbc227d9 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==1.5.4"], + "requirements": ["aiohomekit==1.5.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 1937890b8ca..50b5efd26b5 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==1.5.4 +aiohomekit==1.5.6 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 38fe3412f3a..6a6bdbbe031 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==1.5.4 +aiohomekit==1.5.6 # homeassistant.components.emulated_hue # homeassistant.components.http From c0374a9434d930e9ebf95fcf9ad7d4f20b630d6c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 11 Sep 2022 11:19:31 -0500 Subject: [PATCH 284/955] Bump PySwitchbot to 0.19.5 (#78224) --- 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 c83e575bd49..e1e832f854a 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.19.3"], + "requirements": ["PySwitchbot==0.19.5"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 50b5efd26b5..33c57882be9 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.19.3 +PySwitchbot==0.19.5 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6a6bdbbe031..4111d5f9534 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.19.3 +PySwitchbot==0.19.5 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From 7eb5e6d62378c89282a4651324aa9d5a076df755 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sun, 11 Sep 2022 21:11:51 +0200 Subject: [PATCH 285/955] Import automation constants from root (#78238) --- homeassistant/components/analytics/analytics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/analytics/analytics.py b/homeassistant/components/analytics/analytics.py index 1a696b0c206..5bb1836928c 100644 --- a/homeassistant/components/analytics/analytics.py +++ b/homeassistant/components/analytics/analytics.py @@ -8,7 +8,7 @@ import async_timeout from homeassistant.components import hassio from homeassistant.components.api import ATTR_INSTALLATION_TYPE -from homeassistant.components.automation.const import DOMAIN as AUTOMATION_DOMAIN +from homeassistant.components.automation import DOMAIN as AUTOMATION_DOMAIN from homeassistant.components.energy import ( DOMAIN as ENERGY_DOMAIN, is_configured as energy_is_configured, From b56f54745a9134ff3447eb57ad96ed51884bb9f9 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 12 Sep 2022 00:26:16 +0000 Subject: [PATCH 286/955] [ci skip] Translation update --- .../amberelectric/translations/es.json | 5 +++++ .../amberelectric/translations/ru.json | 5 +++++ .../ambiclimate/translations/fr.json | 2 +- .../components/awair/translations/fr.json | 2 +- .../binary_sensor/translations/pt-BR.json | 2 +- .../components/demo/translations/fr.json | 2 +- .../components/freebox/translations/fr.json | 2 +- .../homematicip_cloud/translations/fr.json | 2 +- .../logi_circle/translations/fr.json | 2 +- .../components/point/translations/fr.json | 2 +- .../components/ps4/translations/fr.json | 2 +- .../tellduslive/translations/fr.json | 2 +- .../components/tilt_ble/translations/ru.json | 21 +++++++++++++++++++ 13 files changed, 41 insertions(+), 10 deletions(-) create mode 100644 homeassistant/components/tilt_ble/translations/ru.json diff --git a/homeassistant/components/amberelectric/translations/es.json b/homeassistant/components/amberelectric/translations/es.json index e8c40903e27..4e34117641b 100644 --- a/homeassistant/components/amberelectric/translations/es.json +++ b/homeassistant/components/amberelectric/translations/es.json @@ -1,5 +1,10 @@ { "config": { + "error": { + "invalid_api_token": "Clave API no v\u00e1lida", + "no_site": "No se proporciona ning\u00fan sitio", + "unknown_error": "Error inesperado" + }, "step": { "site": { "data": { diff --git a/homeassistant/components/amberelectric/translations/ru.json b/homeassistant/components/amberelectric/translations/ru.json index 793f8bcae9d..f81b4db835c 100644 --- a/homeassistant/components/amberelectric/translations/ru.json +++ b/homeassistant/components/amberelectric/translations/ru.json @@ -1,5 +1,10 @@ { "config": { + "error": { + "invalid_api_token": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API.", + "no_site": "\u0421\u0430\u0439\u0442 \u043d\u0435 \u0443\u043a\u0430\u0437\u0430\u043d.", + "unknown_error": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, "step": { "site": { "data": { diff --git a/homeassistant/components/ambiclimate/translations/fr.json b/homeassistant/components/ambiclimate/translations/fr.json index b6464b58244..2453bc16d07 100644 --- a/homeassistant/components/ambiclimate/translations/fr.json +++ b/homeassistant/components/ambiclimate/translations/fr.json @@ -9,7 +9,7 @@ "default": "Authentification r\u00e9ussie" }, "error": { - "follow_link": "Veuillez suivre le lien et vous authentifier avant d'appuyer sur Soumettre.", + "follow_link": "Veuillez suivre le lien et vous authentifier avant d'appuyer sur \u00ab\u00a0Valider\u00a0\u00bb", "no_token": "Non authentifi\u00e9 avec Ambiclimate" }, "step": { diff --git a/homeassistant/components/awair/translations/fr.json b/homeassistant/components/awair/translations/fr.json index 79101fd9128..5d9636eccee 100644 --- a/homeassistant/components/awair/translations/fr.json +++ b/homeassistant/components/awair/translations/fr.json @@ -29,7 +29,7 @@ "data": { "host": "Adresse IP" }, - "description": "Suivez [ces instructions]({url}) pour activer l\u2019API locale Awair.\n\nCliquez sur Envoyer apr\u00e8s avoir termin\u00e9." + "description": "Suivez [ces instructions]({url}) pour activer l\u2019API locale Awair.\n\nCliquez sur \u00ab\u00a0Valider\u00a0\u00bb apr\u00e8s avoir termin\u00e9." }, "local_pick": { "data": { diff --git a/homeassistant/components/binary_sensor/translations/pt-BR.json b/homeassistant/components/binary_sensor/translations/pt-BR.json index 5bd166f9367..63b45557623 100644 --- a/homeassistant/components/binary_sensor/translations/pt-BR.json +++ b/homeassistant/components/binary_sensor/translations/pt-BR.json @@ -94,7 +94,7 @@ "powered": "{entity_name} alimentado", "present": "{entity_name} presente", "problem": "{entity_name} come\u00e7ou a detectar problema", - "running": "{entity_name} come\u00e7ar a correr", + "running": "{nome_da_entidade} come\u00e7ou a funcionar", "smoke": "{entity_name} come\u00e7ou a detectar fuma\u00e7a", "sound": "{entity_name} come\u00e7ou a detectar som", "tampered": "{entity_name} come\u00e7ou a detectar adultera\u00e7\u00e3o", diff --git a/homeassistant/components/demo/translations/fr.json b/homeassistant/components/demo/translations/fr.json index a09a7a1fd2f..4fa207692f3 100644 --- a/homeassistant/components/demo/translations/fr.json +++ b/homeassistant/components/demo/translations/fr.json @@ -14,7 +14,7 @@ "fix_flow": { "step": { "confirm": { - "description": "Appuyez sur OK une fois le liquide de clignotant rempli", + "description": "Appuyez sur VALIDER une fois le liquide de clignotant rempli", "title": "Le liquide de clignotant doit \u00eatre rempli" } } diff --git a/homeassistant/components/freebox/translations/fr.json b/homeassistant/components/freebox/translations/fr.json index 60289791408..83d6a030d5f 100644 --- a/homeassistant/components/freebox/translations/fr.json +++ b/homeassistant/components/freebox/translations/fr.json @@ -10,7 +10,7 @@ }, "step": { "link": { - "description": "Cliquez sur \u00abSoumettre\u00bb, puis appuyez sur la fl\u00e8che droite du routeur pour enregistrer Freebox avec Home Assistant. \n\n ! [Emplacement du bouton sur le routeur](/static/images/config_freebox.png)", + "description": "Cliquez sur \u00ab\u00a0Valider\u00a0\u00bb puis appuyez sur la fl\u00e8che droite du routeur pour enregistrer la Freebox aupr\u00e8s de Home Assistant. \n\n![Emplacement du bouton sur le routeur](/static/images/config_freebox.png)", "title": "Lien routeur Freebox" }, "user": { diff --git a/homeassistant/components/homematicip_cloud/translations/fr.json b/homeassistant/components/homematicip_cloud/translations/fr.json index dd85878991f..a54b26acb71 100644 --- a/homeassistant/components/homematicip_cloud/translations/fr.json +++ b/homeassistant/components/homematicip_cloud/translations/fr.json @@ -21,7 +21,7 @@ "title": "Choisissez le point d'acc\u00e8s HomematicIP" }, "link": { - "description": "Appuyez sur le bouton bleu du point d'acc\u00e8s et sur le bouton Envoyer pour enregistrer HomematicIP avec Home Assistant. \n\n ![Emplacement du bouton sur le pont](/static/images/config_flows/config_homematicip_cloud.png)", + "description": "Appuyez sur le bouton bleu du point d'acc\u00e8s et sur le bouton \u00ab\u00a0Valider\u00a0\u00bb pour enregistrer HomematicIP aupr\u00e8s de Home Assistant. \n\n![Emplacement du bouton sur le pont](/static/images/config_flows/config_homematicip_cloud.png)", "title": "Lier le point d'acc\u00e8s" } } diff --git a/homeassistant/components/logi_circle/translations/fr.json b/homeassistant/components/logi_circle/translations/fr.json index 5c4bb6fab9d..94c48c6fdd6 100644 --- a/homeassistant/components/logi_circle/translations/fr.json +++ b/homeassistant/components/logi_circle/translations/fr.json @@ -8,7 +8,7 @@ }, "error": { "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification expir\u00e9.", - "follow_link": "Veuillez suivre le lien et vous authentifier avant d'appuyer sur Soumettre.", + "follow_link": "Veuillez suivre le lien et vous authentifier avant d'appuyer sur \u00ab\u00a0Valider\u00a0\u00bb.", "invalid_auth": "Authentification non valide" }, "step": { diff --git a/homeassistant/components/point/translations/fr.json b/homeassistant/components/point/translations/fr.json index 440bd076bef..347313ba8b7 100644 --- a/homeassistant/components/point/translations/fr.json +++ b/homeassistant/components/point/translations/fr.json @@ -11,7 +11,7 @@ "default": "Authentification r\u00e9ussie" }, "error": { - "follow_link": "Veuillez suivre le lien et vous authentifier avant d'appuyer sur Soumettre.", + "follow_link": "Veuillez suivre le lien et vous authentifier avant d'appuyer sur \u00ab\u00a0Valider\u00a0\u00bb", "no_token": "Jeton d'acc\u00e8s non valide" }, "step": { diff --git a/homeassistant/components/ps4/translations/fr.json b/homeassistant/components/ps4/translations/fr.json index 73e2e18e22a..cc2332af9b0 100644 --- a/homeassistant/components/ps4/translations/fr.json +++ b/homeassistant/components/ps4/translations/fr.json @@ -9,7 +9,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "credential_timeout": "Le service d'informations d'identification a expir\u00e9. Appuyez sur soumettre pour red\u00e9marrer.", + "credential_timeout": "Le service d'informations d'identification a expir\u00e9. Appuyez sur \u00ab\u00a0Valider\u00a0\u00bb pour red\u00e9marrer.", "login_failed": "\u00c9chec de l'association \u00e0 la PlayStation 4. V\u00e9rifiez que le code PIN est correct.", "no_ipaddress": "Entrez l'adresse IP de la PlayStation 4 que vous souhaitez configurer." }, diff --git a/homeassistant/components/tellduslive/translations/fr.json b/homeassistant/components/tellduslive/translations/fr.json index adcef0c1fd4..57a919be189 100644 --- a/homeassistant/components/tellduslive/translations/fr.json +++ b/homeassistant/components/tellduslive/translations/fr.json @@ -11,7 +11,7 @@ }, "step": { "auth": { - "description": "Pour lier votre compte TelldusLive: \n 1. Cliquez sur le lien ci-dessous \n 2. Connectez-vous \u00e0 Telldus Live \n 3. Autorisez ** {app_name} ** (cliquez sur ** Oui **). \n 4. Revenez ici et cliquez sur ** ENVOYER **. \n\n [Lien compte TelldusLive] ( {auth_url} )", + "description": "Pour lier votre compte TelldusLive\u00a0:\n 1. Cliquez sur le lien ci-dessous\n 2. Connectez-vous sur Telldus Live\n 3. Autorisez **{app_name}** (cliquez sur **Oui**)\n 4. Revenez ici et cliquez sur **VALIDER**\n\n [Lien compte TelldusLive]({auth_url})", "title": "S\u2019authentifier sur TelldusLive" }, "user": { diff --git a/homeassistant/components/tilt_ble/translations/ru.json b/homeassistant/components/tilt_ble/translations/ru.json new file mode 100644 index 00000000000..c912fc120e4 --- /dev/null +++ b/homeassistant/components/tilt_ble/translations/ru.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", + "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438." + }, + "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 From abebf3c06755f82deb88f299fbae22bbb46fa50e Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Mon, 12 Sep 2022 03:14:59 -0400 Subject: [PATCH 287/955] Switch to new entity naming schema across zwave_js (#77434) --- .../components/zwave_js/binary_sensor.py | 4 +- homeassistant/components/zwave_js/button.py | 7 ++-- homeassistant/components/zwave_js/entity.py | 38 ++++++++++--------- homeassistant/components/zwave_js/number.py | 4 +- homeassistant/components/zwave_js/select.py | 4 +- homeassistant/components/zwave_js/sensor.py | 21 +++------- tests/components/zwave_js/common.py | 10 ++--- .../components/zwave_js/test_binary_sensor.py | 2 +- tests/components/zwave_js/test_init.py | 14 +++---- 9 files changed, 43 insertions(+), 61 deletions(-) diff --git a/homeassistant/components/zwave_js/binary_sensor.py b/homeassistant/components/zwave_js/binary_sensor.py index 7fa20b2b1f5..dfb412ca6fa 100644 --- a/homeassistant/components/zwave_js/binary_sensor.py +++ b/homeassistant/components/zwave_js/binary_sensor.py @@ -376,9 +376,7 @@ class ZWaveNotificationBinarySensor(ZWaveBaseEntity, BinarySensorEntity): # Entity class attributes self._attr_name = self.generate_name( - include_value_name=True, - alternate_value_name=self.info.primary_value.property_name, - additional_info=[self.info.primary_value.metadata.states[self.state_key]], + alternate_value_name=self.info.primary_value.metadata.states[self.state_key] ) self._attr_unique_id = f"{self._attr_unique_id}.{self.state_key}" diff --git a/homeassistant/components/zwave_js/button.py b/homeassistant/components/zwave_js/button.py index 1d97ed05da5..c759471670b 100644 --- a/homeassistant/components/zwave_js/button.py +++ b/homeassistant/components/zwave_js/button.py @@ -47,15 +47,14 @@ class ZWaveNodePingButton(ButtonEntity): _attr_should_poll = False _attr_entity_category = EntityCategory.CONFIG + _attr_has_entity_name = True def __init__(self, driver: Driver, node: ZwaveNode) -> None: """Initialize a ping Z-Wave device button entity.""" self.node = node - name: str = ( - node.name or node.device_config.description or f"Node {node.node_id}" - ) + # Entity class attributes - self._attr_name = f"{name}: Ping" + self._attr_name = "Ping" self._base_unique_id = get_valueless_base_unique_id(driver, node) self._attr_unique_id = f"{self._base_unique_id}.ping" # device may not be precreated in main handler yet diff --git a/homeassistant/components/zwave_js/entity.py b/homeassistant/components/zwave_js/entity.py index 65f00b5022a..621316a166f 100644 --- a/homeassistant/components/zwave_js/entity.py +++ b/homeassistant/components/zwave_js/entity.py @@ -24,6 +24,7 @@ class ZWaveBaseEntity(Entity): """Generic Entity Class for a Z-Wave Device.""" _attr_should_poll = False + _attr_has_entity_name = True def __init__( self, config_entry: ConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo @@ -126,29 +127,32 @@ class ZWaveBaseEntity(Entity): include_value_name: bool = False, alternate_value_name: str | None = None, additional_info: list[str] | None = None, - name_suffix: str | None = None, + name_prefix: str | None = None, ) -> str: """Generate entity name.""" - if additional_info is None: - additional_info = [] - name: str = ( - self.info.node.name - or self.info.node.device_config.description - or f"Node {self.info.node.node_id}" - ) - if name_suffix: - name = f"{name} {name_suffix}" - if include_value_name: + name = "" + if ( + hasattr(self, "entity_description") + and self.entity_description + and self.entity_description.name + ): + name = self.entity_description.name + + if name_prefix: + name = f"{name_prefix} {name}".strip() + + value_name = "" + if alternate_value_name: + value_name = alternate_value_name + elif include_value_name: value_name = ( - alternate_value_name - or self.info.primary_value.metadata.label + self.info.primary_value.metadata.label or self.info.primary_value.property_key_name or self.info.primary_value.property_name + or "" ) - name = f"{name}: {value_name}" - for item in additional_info: - if item: - name += f" - {item}" + name = f"{name} {value_name}".strip() + name = f"{name} {' '.join(additional_info or [])}".strip() # append endpoint if > 1 if ( self.info.primary_value.endpoint is not None diff --git a/homeassistant/components/zwave_js/number.py b/homeassistant/components/zwave_js/number.py index 1b17fa35024..f898170e308 100644 --- a/homeassistant/components/zwave_js/number.py +++ b/homeassistant/components/zwave_js/number.py @@ -66,9 +66,7 @@ class ZwaveNumberEntity(ZWaveBaseEntity, NumberEntity): self._target_value = self.get_zwave_value(TARGET_VALUE_PROPERTY) # Entity class attributes - self._attr_name = self.generate_name( - include_value_name=True, alternate_value_name=info.platform_hint - ) + self._attr_name = self.generate_name(alternate_value_name=info.platform_hint) @property def native_min_value(self) -> float: diff --git a/homeassistant/components/zwave_js/select.py b/homeassistant/components/zwave_js/select.py index 81e60582764..0360b968173 100644 --- a/homeassistant/components/zwave_js/select.py +++ b/homeassistant/components/zwave_js/select.py @@ -107,9 +107,7 @@ class ZwaveDefaultToneSelectEntity(ZWaveBaseEntity, SelectEntity): ) # Entity class attributes - self._attr_name = self.generate_name( - include_value_name=True, alternate_value_name=info.platform_hint - ) + self._attr_name = self.generate_name(alternate_value_name=info.platform_hint) @property def options(self) -> list[str]: diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index 75d8066d595..4592a6518b8 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -386,13 +386,8 @@ class ZWaveListSensor(ZwaveSensorBase): config_entry, driver, info, entity_description, unit_of_measurement ) - property_key_name = self.info.primary_value.property_key_name # Entity class attributes - self._attr_name = self.generate_name( - include_value_name=True, - alternate_value_name=self.info.primary_value.property_name, - additional_info=[property_key_name] if property_key_name else None, - ) + self._attr_name = self.generate_name(include_value_name=True) @property def native_value(self) -> str | None: @@ -437,10 +432,9 @@ class ZWaveConfigParameterSensor(ZwaveSensorBase): property_key_name = self.info.primary_value.property_key_name # Entity class attributes self._attr_name = self.generate_name( - include_value_name=True, alternate_value_name=self.info.primary_value.property_name, additional_info=[property_key_name] if property_key_name else None, - name_suffix="Config Parameter", + name_prefix="Config parameter", ) @property @@ -477,6 +471,7 @@ class ZWaveNodeStatusSensor(SensorEntity): _attr_should_poll = False _attr_entity_category = EntityCategory.DIAGNOSTIC + _attr_has_entity_name = True def __init__( self, config_entry: ConfigEntry, driver: Driver, node: ZwaveNode @@ -484,18 +479,13 @@ class ZWaveNodeStatusSensor(SensorEntity): """Initialize a generic Z-Wave device entity.""" self.config_entry = config_entry self.node = node - name: str = ( - self.node.name - or self.node.device_config.description - or f"Node {self.node.node_id}" - ) + # Entity class attributes - self._attr_name = f"{name}: Node Status" + self._attr_name = "Node status" self._base_unique_id = get_valueless_base_unique_id(driver, node) self._attr_unique_id = f"{self._base_unique_id}.node_status" # device may not be precreated in main handler yet self._attr_device_info = get_device_info(driver, node) - self._attr_native_value: str = node.status.name.lower() async def async_poll_value(self, _: bool) -> None: """Poll a value.""" @@ -534,4 +524,5 @@ class ZWaveNodeStatusSensor(SensorEntity): self.async_remove, ) ) + self._attr_native_value: str = self.node.status.name.lower() self.async_write_ha_state() diff --git a/tests/components/zwave_js/common.py b/tests/components/zwave_js/common.py index aea895d03bb..c2079564dcf 100644 --- a/tests/components/zwave_js/common.py +++ b/tests/components/zwave_js/common.py @@ -1,9 +1,7 @@ """Provide common test tools for Z-Wave JS.""" AIR_TEMPERATURE_SENSOR = "sensor.multisensor_6_air_temperature" BATTERY_SENSOR = "sensor.multisensor_6_battery_level" -TAMPER_SENSOR = ( - "binary_sensor.multisensor_6_home_security_tampering_product_cover_removed" -) +TAMPER_SENSOR = "binary_sensor.multisensor_6_tampering_product_cover_removed" HUMIDITY_SENSOR = "sensor.multisensor_6_humidity" POWER_SENSOR = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed" ENERGY_SENSOR = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed_2" @@ -13,10 +11,8 @@ SWITCH_ENTITY = "switch.smart_plug_with_two_usb_ports" LOW_BATTERY_BINARY_SENSOR = "binary_sensor.multisensor_6_low_battery_level" ENABLED_LEGACY_BINARY_SENSOR = "binary_sensor.z_wave_door_window_sensor_any" DISABLED_LEGACY_BINARY_SENSOR = "binary_sensor.multisensor_6_any" -NOTIFICATION_MOTION_BINARY_SENSOR = ( - "binary_sensor.multisensor_6_home_security_motion_detection" -) -NOTIFICATION_MOTION_SENSOR = "sensor.multisensor_6_home_security_motion_sensor_status" +NOTIFICATION_MOTION_BINARY_SENSOR = "binary_sensor.multisensor_6_motion_detection" +NOTIFICATION_MOTION_SENSOR = "sensor.multisensor_6_motion_sensor_status" INDICATOR_SENSOR = "sensor.z_wave_thermostat_indicator_value" BASIC_NUMBER_ENTITY = "number.livingroomlight_basic" PROPERTY_DOOR_STATUS_BINARY_SENSOR = ( diff --git a/tests/components/zwave_js/test_binary_sensor.py b/tests/components/zwave_js/test_binary_sensor.py index 292bbc2da37..2a1c13b0db2 100644 --- a/tests/components/zwave_js/test_binary_sensor.py +++ b/tests/components/zwave_js/test_binary_sensor.py @@ -141,7 +141,7 @@ async def test_notification_off_state( state = door_states[0] assert state - assert state.entity_id == "binary_sensor.node_62_access_control_window_door_is_open" + assert state.entity_id == "binary_sensor.node_62_window_door_is_open" async def test_property_sensor_door_status(hass, lock_august_pro, integration): diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index 63cbc090e7d..5929657f595 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -347,10 +347,10 @@ async def test_existing_node_not_replaced_when_not_ready( assert not device.area_id assert device == dev_reg.async_get_device(identifiers={(DOMAIN, device_id_ext)}) - motion_entity = "binary_sensor.4_in_1_sensor_home_security_motion_detection" + motion_entity = "binary_sensor.4_in_1_sensor_motion_detection" state = hass.states.get(motion_entity) assert state - assert state.name == "4-in-1 Sensor: Home Security - Motion detection" + assert state.name == "4-in-1 Sensor Motion detection" dev_reg.async_update_device( device.id, name_by_user="Custom Device Name", area_id=kitchen_area.id @@ -1179,10 +1179,10 @@ async def test_node_model_change(hass, zp3111, client, integration): dev_id = device.id - motion_entity = "binary_sensor.4_in_1_sensor_home_security_motion_detection" + motion_entity = "binary_sensor.4_in_1_sensor_motion_detection" state = hass.states.get(motion_entity) assert state - assert state.name == "4-in-1 Sensor: Home Security - Motion detection" + assert state.name == "4-in-1 Sensor Motion detection" # Customize device and entity names/ids dev_reg.async_update_device(device.id, name_by_user="Custom Device Name") @@ -1267,7 +1267,7 @@ async def test_disabled_entity_on_value_removed(hass, zp3111, client, integratio er_reg = er.async_get(hass) # re-enable this default-disabled entity - sensor_cover_entity = "sensor.4_in_1_sensor_home_security_cover_status" + sensor_cover_entity = "sensor.4_in_1_sensor_cover_status" er_reg.async_update_entity(entity_id=sensor_cover_entity, disabled_by=None) await hass.async_block_till_done() @@ -1285,9 +1285,7 @@ async def test_disabled_entity_on_value_removed(hass, zp3111, client, integratio assert state.state != STATE_UNAVAILABLE # check for expected entities - binary_cover_entity = ( - "binary_sensor.4_in_1_sensor_home_security_tampering_product_cover_removed" - ) + binary_cover_entity = "binary_sensor.4_in_1_sensor_tampering_product_cover_removed" state = hass.states.get(binary_cover_entity) assert state assert state.state != STATE_UNAVAILABLE From e83594a179b6dd12355e99a7049656c857e9b7d9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 12 Sep 2022 05:19:45 -0500 Subject: [PATCH 288/955] Bump aiodiscover to 1.4.13 (#78253) --- homeassistant/components/dhcp/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/dhcp/manifest.json b/homeassistant/components/dhcp/manifest.json index fea7772f72d..f3f44f6dc9b 100644 --- a/homeassistant/components/dhcp/manifest.json +++ b/homeassistant/components/dhcp/manifest.json @@ -2,7 +2,7 @@ "domain": "dhcp", "name": "DHCP Discovery", "documentation": "https://www.home-assistant.io/integrations/dhcp", - "requirements": ["scapy==2.4.5", "aiodiscover==1.4.11"], + "requirements": ["scapy==2.4.5", "aiodiscover==1.4.13"], "codeowners": ["@bdraco"], "quality_scale": "internal", "iot_class": "local_push", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 4f41518361f..f0f2a2eb8c3 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,6 +1,6 @@ PyJWT==2.4.0 PyNaCl==1.5.0 -aiodiscover==1.4.11 +aiodiscover==1.4.13 aiohttp==3.8.1 aiohttp_cors==0.7.0 astral==2.2 diff --git a/requirements_all.txt b/requirements_all.txt index 33c57882be9..104d0f70854 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -134,7 +134,7 @@ aiobafi6==0.7.2 aiobotocore==2.1.0 # homeassistant.components.dhcp -aiodiscover==1.4.11 +aiodiscover==1.4.13 # homeassistant.components.dnsip # homeassistant.components.minecraft_server diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4111d5f9534..d7c89984ffb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -121,7 +121,7 @@ aiobafi6==0.7.2 aiobotocore==2.1.0 # homeassistant.components.dhcp -aiodiscover==1.4.11 +aiodiscover==1.4.13 # homeassistant.components.dnsip # homeassistant.components.minecraft_server From 253d3555265060aa5cf1c8f227b2ab3d86d433b0 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 12 Sep 2022 15:25:11 +0200 Subject: [PATCH 289/955] Remove unused mypy ignore statements (#78292) --- homeassistant/components/binary_sensor/device_trigger.py | 2 -- homeassistant/components/camera/__init__.py | 2 -- homeassistant/components/derivative/sensor.py | 2 -- homeassistant/components/device_automation/entity.py | 2 -- homeassistant/components/device_automation/toggle_entity.py | 2 -- homeassistant/components/geo_location/__init__.py | 2 -- .../components/homeassistant/triggers/homeassistant.py | 2 -- .../components/homeassistant/triggers/numeric_state.py | 3 --- homeassistant/components/homeassistant/triggers/state.py | 3 --- homeassistant/components/homeassistant/triggers/time.py | 2 -- .../components/homeassistant/triggers/time_pattern.py | 2 -- homeassistant/components/recorder/history.py | 2 -- homeassistant/components/remote/__init__.py | 2 -- homeassistant/components/sensor/device_condition.py | 2 -- homeassistant/components/sensor/device_trigger.py | 2 -- homeassistant/components/stt/__init__.py | 2 -- homeassistant/components/template/trigger.py | 2 -- homeassistant/components/trace/websocket_api.py | 2 -- homeassistant/components/vacuum/__init__.py | 2 -- homeassistant/components/water_heater/__init__.py | 2 -- homeassistant/components/webhook/trigger.py | 2 -- 21 files changed, 44 deletions(-) diff --git a/homeassistant/components/binary_sensor/device_trigger.py b/homeassistant/components/binary_sensor/device_trigger.py index a6dfc762d45..66daa687f38 100644 --- a/homeassistant/components/binary_sensor/device_trigger.py +++ b/homeassistant/components/binary_sensor/device_trigger.py @@ -16,8 +16,6 @@ from homeassistant.helpers.typing import ConfigType from . import DOMAIN, BinarySensorDeviceClass -# mypy: allow-untyped-defs, no-check-untyped-defs - DEVICE_CLASS_NONE = "none" CONF_BAT_LOW = "bat_low" diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index e5ccb433975..8d7557ad81d 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -75,8 +75,6 @@ from .const import ( # noqa: F401 from .img_util import scale_jpeg_camera_image from .prefs import CameraPreferences -# mypy: allow-untyped-calls - _LOGGER = logging.getLogger(__name__) SERVICE_ENABLE_MOTION: Final = "enable_motion_detection" diff --git a/homeassistant/components/derivative/sensor.py b/homeassistant/components/derivative/sensor.py index 8e1934dcecf..59e661fce0b 100644 --- a/homeassistant/components/derivative/sensor.py +++ b/homeassistant/components/derivative/sensor.py @@ -35,8 +35,6 @@ from .const import ( CONF_UNIT_TIME, ) -# mypy: allow-untyped-defs, no-check-untyped-defs - _LOGGER = logging.getLogger(__name__) ATTR_SOURCE_ID = "source" diff --git a/homeassistant/components/device_automation/entity.py b/homeassistant/components/device_automation/entity.py index c7716f38712..f38daf2dae6 100644 --- a/homeassistant/components/device_automation/entity.py +++ b/homeassistant/components/device_automation/entity.py @@ -13,8 +13,6 @@ from homeassistant.helpers.typing import ConfigType from . import DEVICE_TRIGGER_BASE_SCHEMA from .const import CONF_CHANGED_STATES -# mypy: allow-untyped-calls, allow-untyped-defs - ENTITY_TRIGGERS = [ { # Trigger when entity is turned on or off diff --git a/homeassistant/components/device_automation/toggle_entity.py b/homeassistant/components/device_automation/toggle_entity.py index f14c4de5c2f..e5061cb691e 100644 --- a/homeassistant/components/device_automation/toggle_entity.py +++ b/homeassistant/components/device_automation/toggle_entity.py @@ -33,8 +33,6 @@ from .const import ( CONF_TURNED_ON, ) -# mypy: allow-untyped-calls, allow-untyped-defs - ENTITY_ACTIONS = [ { # Turn entity off diff --git a/homeassistant/components/geo_location/__init__.py b/homeassistant/components/geo_location/__init__.py index 54542fa8503..cb737b896eb 100644 --- a/homeassistant/components/geo_location/__init__.py +++ b/homeassistant/components/geo_location/__init__.py @@ -16,8 +16,6 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType -# mypy: allow-untyped-defs, no-check-untyped-defs - _LOGGER = logging.getLogger(__name__) ATTR_DISTANCE = "distance" diff --git a/homeassistant/components/homeassistant/triggers/homeassistant.py b/homeassistant/components/homeassistant/triggers/homeassistant.py index 8749c47861a..e3dc93a9788 100644 --- a/homeassistant/components/homeassistant/triggers/homeassistant.py +++ b/homeassistant/components/homeassistant/triggers/homeassistant.py @@ -7,8 +7,6 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType -# mypy: allow-untyped-defs - EVENT_START = "start" EVENT_SHUTDOWN = "shutdown" diff --git a/homeassistant/components/homeassistant/triggers/numeric_state.py b/homeassistant/components/homeassistant/triggers/numeric_state.py index 10971a03781..b8f548adb16 100644 --- a/homeassistant/components/homeassistant/triggers/numeric_state.py +++ b/homeassistant/components/homeassistant/triggers/numeric_state.py @@ -27,9 +27,6 @@ from homeassistant.helpers.event import ( from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType -# mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs -# mypy: no-check-untyped-defs - def validate_above_below(value): """Validate that above and below can co-exist.""" diff --git a/homeassistant/components/homeassistant/triggers/state.py b/homeassistant/components/homeassistant/triggers/state.py index 8514000de07..25622e0a3c6 100644 --- a/homeassistant/components/homeassistant/triggers/state.py +++ b/homeassistant/components/homeassistant/triggers/state.py @@ -29,9 +29,6 @@ from homeassistant.helpers.event import ( from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType -# mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs -# mypy: no-check-untyped-defs - _LOGGER = logging.getLogger(__name__) CONF_ENTITY_ID = "entity_id" diff --git a/homeassistant/components/homeassistant/triggers/time.py b/homeassistant/components/homeassistant/triggers/time.py index a81afa1323a..a51eff004e5 100644 --- a/homeassistant/components/homeassistant/triggers/time.py +++ b/homeassistant/components/homeassistant/triggers/time.py @@ -23,8 +23,6 @@ from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType import homeassistant.util.dt as dt_util -# mypy: allow-untyped-defs, no-check-untyped-defs - _TIME_TRIGGER_SCHEMA = vol.Any( cv.time, vol.All(str, cv.entity_domain(["input_datetime", "sensor"])), diff --git a/homeassistant/components/homeassistant/triggers/time_pattern.py b/homeassistant/components/homeassistant/triggers/time_pattern.py index 3c2cf58bca8..2a5022bebf3 100644 --- a/homeassistant/components/homeassistant/triggers/time_pattern.py +++ b/homeassistant/components/homeassistant/triggers/time_pattern.py @@ -8,8 +8,6 @@ from homeassistant.helpers.event import async_track_time_change from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType -# mypy: allow-untyped-defs, no-check-untyped-defs - CONF_HOURS = "hours" CONF_MINUTES = "minutes" CONF_SECONDS = "seconds" diff --git a/homeassistant/components/recorder/history.py b/homeassistant/components/recorder/history.py index 7e875a5ff93..cbcbdd2c75b 100644 --- a/homeassistant/components/recorder/history.py +++ b/homeassistant/components/recorder/history.py @@ -36,8 +36,6 @@ from .models import ( ) from .util import execute_stmt_lambda_element, session_scope -# mypy: allow-untyped-defs, no-check-untyped-defs - _LOGGER = logging.getLogger(__name__) STATE_KEY = "state" diff --git a/homeassistant/components/remote/__init__.py b/homeassistant/components/remote/__init__.py index 1a739a2a476..b1b856cfa29 100644 --- a/homeassistant/components/remote/__init__.py +++ b/homeassistant/components/remote/__init__.py @@ -31,8 +31,6 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass -# mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs - _LOGGER = logging.getLogger(__name__) ATTR_ACTIVITY = "activity" diff --git a/homeassistant/components/sensor/device_condition.py b/homeassistant/components/sensor/device_condition.py index 01be2f37172..437671a7e30 100644 --- a/homeassistant/components/sensor/device_condition.py +++ b/homeassistant/components/sensor/device_condition.py @@ -29,8 +29,6 @@ from homeassistant.helpers.typing import ConfigType from . import ATTR_STATE_CLASS, DOMAIN, SensorDeviceClass -# mypy: allow-untyped-defs, no-check-untyped-defs - DEVICE_CLASS_NONE = "none" CONF_IS_APPARENT_POWER = "is_apparent_power" diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py index 90d0a4c3d0e..3cce6b74a81 100644 --- a/homeassistant/components/sensor/device_trigger.py +++ b/homeassistant/components/sensor/device_trigger.py @@ -28,8 +28,6 @@ from homeassistant.helpers.typing import ConfigType from . import ATTR_STATE_CLASS, DOMAIN, SensorDeviceClass -# mypy: allow-untyped-defs, no-check-untyped-defs - DEVICE_CLASS_NONE = "none" CONF_APPARENT_POWER = "apparent_power" diff --git a/homeassistant/components/stt/__init__.py b/homeassistant/components/stt/__init__.py index b4e22f1cdd2..94acf155968 100644 --- a/homeassistant/components/stt/__init__.py +++ b/homeassistant/components/stt/__init__.py @@ -31,8 +31,6 @@ from .const import ( SpeechResultState, ) -# mypy: allow-untyped-defs, no-check-untyped-defs - _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/template/trigger.py b/homeassistant/components/template/trigger.py index 7c25e7090a6..e6266a07077 100644 --- a/homeassistant/components/template/trigger.py +++ b/homeassistant/components/template/trigger.py @@ -16,8 +16,6 @@ from homeassistant.helpers.template import Template, result_as_boolean from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType -# mypy: allow-untyped-defs, no-check-untyped-defs - _LOGGER = logging.getLogger(__name__) TRIGGER_SCHEMA = IF_ACTION_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( diff --git a/homeassistant/components/trace/websocket_api.py b/homeassistant/components/trace/websocket_api.py index fb9357cc067..2ea58db895a 100644 --- a/homeassistant/components/trace/websocket_api.py +++ b/homeassistant/components/trace/websocket_api.py @@ -26,8 +26,6 @@ from homeassistant.helpers.script import ( from .. import trace -# mypy: allow-untyped-calls, allow-untyped-defs - TRACE_DOMAINS = ("automation", "script") diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index 24d4718540e..dc5c9cc225c 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -40,8 +40,6 @@ from homeassistant.helpers.icon import icon_for_battery_level from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass -# mypy: allow-untyped-defs, no-check-untyped-defs - _LOGGER = logging.getLogger(__name__) DOMAIN = "vacuum" diff --git a/homeassistant/components/water_heater/__init__.py b/homeassistant/components/water_heater/__init__.py index 50ba1d15f2e..c7ece01a93d 100644 --- a/homeassistant/components/water_heater/__init__.py +++ b/homeassistant/components/water_heater/__init__.py @@ -36,8 +36,6 @@ from homeassistant.helpers.temperature import display_temp as show_temp from homeassistant.helpers.typing import ConfigType from homeassistant.util.temperature import convert as convert_temperature -# mypy: allow-untyped-defs, no-check-untyped-defs - DEFAULT_MIN_TEMP = 110 DEFAULT_MAX_TEMP = 140 diff --git a/homeassistant/components/webhook/trigger.py b/homeassistant/components/webhook/trigger.py index 262d77e61f7..cb1a6cb4eb6 100644 --- a/homeassistant/components/webhook/trigger.py +++ b/homeassistant/components/webhook/trigger.py @@ -14,8 +14,6 @@ from homeassistant.helpers.typing import ConfigType from . import DOMAIN, async_register, async_unregister -# mypy: allow-untyped-defs - DEPENDENCIES = ("webhook",) TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( From 844074c3a9b1bb376a291fa1ba1ad53d9d951ad6 Mon Sep 17 00:00:00 2001 From: alakdae Date: Mon, 12 Sep 2022 15:53:03 +0200 Subject: [PATCH 290/955] Add extra precision to ADC voltage (from 1 decimal to 2 decimals) (#77889) --- 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 c37bbbd5ff8..bc55aa3e865 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -281,7 +281,7 @@ SENSORS: Final = { key="adc|adc", name="ADC", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - value=lambda value: round(value, 1), + value=lambda value: round(value, 2), device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, ), From 6a5678154a3500186740b4842dcc1a72088352e6 Mon Sep 17 00:00:00 2001 From: Radu Date: Mon, 12 Sep 2022 15:26:23 +0100 Subject: [PATCH 291/955] Add ZigStar ZeroConf (#78237) --- homeassistant/components/zha/manifest.json | 4 ++++ homeassistant/generated/zeroconf.py | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 419e1c1452b..78d76d6556f 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -91,6 +91,10 @@ { "type": "_zigate-zigbee-gateway._tcp.local.", "name": "*zigate*" + }, + { + "type": "_zigstar_gw._tcp.local.", + "name": "*zigstar*" } ], "dependencies": ["file_upload"], diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index b6237a36cd7..61d6fdae63c 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -411,6 +411,12 @@ ZEROCONF = { "name": "*zigate*" } ], + "_zigstar_gw._tcp.local.": [ + { + "domain": "zha", + "name": "*zigstar*" + } + ], "_zwave-js-server._tcp.local.": [ { "domain": "zwave_js" From 0ce526efe1ab780b0dae88430378ee206f178f22 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 12 Sep 2022 17:53:06 +0200 Subject: [PATCH 292/955] Import logbook constants from root (#78236) --- homeassistant/components/automation/logbook.py | 4 ++-- homeassistant/components/homeassistant/logbook.py | 2 +- homeassistant/components/mobile_app/logbook.py | 2 +- homeassistant/components/script/logbook.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/automation/logbook.py b/homeassistant/components/automation/logbook.py index 529fed80d26..e5ab351b0be 100644 --- a/homeassistant/components/automation/logbook.py +++ b/homeassistant/components/automation/logbook.py @@ -1,11 +1,11 @@ """Describe logbook events.""" -from homeassistant.components.logbook import LazyEventPartialState -from homeassistant.components.logbook.const import ( +from homeassistant.components.logbook import ( LOGBOOK_ENTRY_CONTEXT_ID, LOGBOOK_ENTRY_ENTITY_ID, LOGBOOK_ENTRY_MESSAGE, LOGBOOK_ENTRY_NAME, LOGBOOK_ENTRY_SOURCE, + LazyEventPartialState, ) from homeassistant.const import ATTR_ENTITY_ID, ATTR_NAME from homeassistant.core import HomeAssistant, callback diff --git a/homeassistant/components/homeassistant/logbook.py b/homeassistant/components/homeassistant/logbook.py index 548753982a8..229fb24cb27 100644 --- a/homeassistant/components/homeassistant/logbook.py +++ b/homeassistant/components/homeassistant/logbook.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Callable -from homeassistant.components.logbook.const import ( +from homeassistant.components.logbook import ( LOGBOOK_ENTRY_ICON, LOGBOOK_ENTRY_MESSAGE, LOGBOOK_ENTRY_NAME, diff --git a/homeassistant/components/mobile_app/logbook.py b/homeassistant/components/mobile_app/logbook.py index 6f7c2e4e99c..083db294a1c 100644 --- a/homeassistant/components/mobile_app/logbook.py +++ b/homeassistant/components/mobile_app/logbook.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Callable -from homeassistant.components.logbook.const import ( +from homeassistant.components.logbook import ( LOGBOOK_ENTRY_ENTITY_ID, LOGBOOK_ENTRY_ICON, LOGBOOK_ENTRY_MESSAGE, diff --git a/homeassistant/components/script/logbook.py b/homeassistant/components/script/logbook.py index 7fcbad07479..ce23f083ee0 100644 --- a/homeassistant/components/script/logbook.py +++ b/homeassistant/components/script/logbook.py @@ -1,5 +1,5 @@ """Describe logbook events.""" -from homeassistant.components.logbook.const import ( +from homeassistant.components.logbook import ( LOGBOOK_ENTRY_CONTEXT_ID, LOGBOOK_ENTRY_ENTITY_ID, LOGBOOK_ENTRY_MESSAGE, From 1fb5800bdf700f47bc5a2ae17c364d0cb3c3da7c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 12 Sep 2022 18:06:03 +0200 Subject: [PATCH 293/955] Import trace constants from root (#78243) --- homeassistant/components/automation/trace.py | 7 +++++-- homeassistant/components/script/trace.py | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/automation/trace.py b/homeassistant/components/automation/trace.py index f76dd57e4ed..b302f99d036 100644 --- a/homeassistant/components/automation/trace.py +++ b/homeassistant/components/automation/trace.py @@ -4,8 +4,11 @@ from __future__ import annotations from contextlib import contextmanager from typing import Any -from homeassistant.components.trace import ActionTrace, async_store_trace -from homeassistant.components.trace.const import CONF_STORED_TRACES +from homeassistant.components.trace import ( + CONF_STORED_TRACES, + ActionTrace, + async_store_trace, +) from homeassistant.core import Context from .const import DOMAIN diff --git a/homeassistant/components/script/trace.py b/homeassistant/components/script/trace.py index 27cb1514448..c63d50b1041 100644 --- a/homeassistant/components/script/trace.py +++ b/homeassistant/components/script/trace.py @@ -5,8 +5,11 @@ from collections.abc import Iterator from contextlib import contextmanager from typing import Any -from homeassistant.components.trace import ActionTrace, async_store_trace -from homeassistant.components.trace.const import CONF_STORED_TRACES +from homeassistant.components.trace import ( + CONF_STORED_TRACES, + ActionTrace, + async_store_trace, +) from homeassistant.core import Context, HomeAssistant from .const import DOMAIN From 4dcbe3e608716544dcf66179606929b4776cc8d6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 12 Sep 2022 18:07:26 +0200 Subject: [PATCH 294/955] Import notify constants from root (#78244) --- homeassistant/components/aws/notify.py | 2 +- homeassistant/components/simplepush/notify.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/aws/notify.py b/homeassistant/components/aws/notify.py index 37b49d44a88..c4e450a4aab 100644 --- a/homeassistant/components/aws/notify.py +++ b/homeassistant/components/aws/notify.py @@ -7,12 +7,12 @@ import logging from aiobotocore.session import AioSession from homeassistant.components.notify import ( + ATTR_DATA, ATTR_TARGET, ATTR_TITLE, ATTR_TITLE_DEFAULT, BaseNotificationService, ) -from homeassistant.components.notify.const import ATTR_DATA from homeassistant.const import ( CONF_NAME, CONF_PLATFORM, diff --git a/homeassistant/components/simplepush/notify.py b/homeassistant/components/simplepush/notify.py index 36abf31fcb7..108aaf3cbf6 100644 --- a/homeassistant/components/simplepush/notify.py +++ b/homeassistant/components/simplepush/notify.py @@ -7,12 +7,12 @@ from typing import Any from simplepush import BadRequest, UnknownError, send, send_encrypted from homeassistant.components.notify import ( + ATTR_DATA, ATTR_TITLE, ATTR_TITLE_DEFAULT, PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA, BaseNotificationService, ) -from homeassistant.components.notify.const import ATTR_DATA from homeassistant.const import CONF_EVENT, CONF_PASSWORD from homeassistant.core import HomeAssistant from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue From 45a69090f024a8e57deb8b21cf4dec53d3bff5aa Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 12 Sep 2022 18:10:33 +0200 Subject: [PATCH 295/955] Expose and use lovelace constants from root (#78246) --- homeassistant/components/lovelace/__init__.py | 3 ++- homeassistant/components/websocket_api/permissions.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lovelace/__init__.py b/homeassistant/components/lovelace/__init__.py index cf74a3a588c..c2e709aeb40 100644 --- a/homeassistant/components/lovelace/__init__.py +++ b/homeassistant/components/lovelace/__init__.py @@ -14,7 +14,7 @@ from homeassistant.helpers.typing import ConfigType from homeassistant.loader import async_get_integration from . import dashboard, resources, websocket -from .const import ( +from .const import ( # noqa: F401 CONF_ICON, CONF_REQUIRE_ADMIN, CONF_SHOW_IN_SIDEBAR, @@ -23,6 +23,7 @@ from .const import ( DASHBOARD_BASE_CREATE_FIELDS, DEFAULT_ICON, DOMAIN, + EVENT_LOVELACE_UPDATED, MODE_STORAGE, MODE_YAML, RESOURCE_CREATE_FIELDS, diff --git a/homeassistant/components/websocket_api/permissions.py b/homeassistant/components/websocket_api/permissions.py index 280594580e8..6100c2ea13c 100644 --- a/homeassistant/components/websocket_api/permissions.py +++ b/homeassistant/components/websocket_api/permissions.py @@ -7,7 +7,7 @@ from __future__ import annotations from typing import Final from homeassistant.components.frontend import EVENT_PANELS_UPDATED -from homeassistant.components.lovelace.const import EVENT_LOVELACE_UPDATED +from homeassistant.components.lovelace import EVENT_LOVELACE_UPDATED from homeassistant.components.persistent_notification import ( EVENT_PERSISTENT_NOTIFICATIONS_UPDATED, ) From 9c8e9f044b1f3125c0aec343f90302dc00c75555 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 12 Sep 2022 18:12:06 +0200 Subject: [PATCH 296/955] Import stt constants from root (#78247) --- homeassistant/components/cloud/stt.py | 6 ++++-- homeassistant/components/demo/stt.py | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/cloud/stt.py b/homeassistant/components/cloud/stt.py index 80578a8d721..b1798b2f3be 100644 --- a/homeassistant/components/cloud/stt.py +++ b/homeassistant/components/cloud/stt.py @@ -5,13 +5,15 @@ from aiohttp import StreamReader from hass_nabucasa import Cloud from hass_nabucasa.voice import VoiceError -from homeassistant.components.stt import Provider, SpeechMetadata, SpeechResult -from homeassistant.components.stt.const import ( +from homeassistant.components.stt import ( AudioBitRates, AudioChannels, AudioCodecs, AudioFormats, AudioSampleRates, + Provider, + SpeechMetadata, + SpeechResult, SpeechResultState, ) diff --git a/homeassistant/components/demo/stt.py b/homeassistant/components/demo/stt.py index c035021b5ee..9c3cf89d80e 100644 --- a/homeassistant/components/demo/stt.py +++ b/homeassistant/components/demo/stt.py @@ -3,13 +3,15 @@ from __future__ import annotations from aiohttp import StreamReader -from homeassistant.components.stt import Provider, SpeechMetadata, SpeechResult -from homeassistant.components.stt.const import ( +from homeassistant.components.stt import ( AudioBitRates, AudioChannels, AudioCodecs, AudioFormats, AudioSampleRates, + Provider, + SpeechMetadata, + SpeechResult, SpeechResultState, ) from homeassistant.core import HomeAssistant From ac3534cba120688b1d40d1b6bbc947322aa8c776 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 12 Sep 2022 18:14:49 +0200 Subject: [PATCH 297/955] Import number constants from root (#78248) --- homeassistant/components/homekit_controller/number.py | 5 +++-- homeassistant/components/juicenet/number.py | 7 +++++-- homeassistant/components/template/number.py | 4 ++-- homeassistant/components/xiaomi_miio/number.py | 7 +++++-- 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/homekit_controller/number.py b/homeassistant/components/homekit_controller/number.py index 2987c82e829..29cad299902 100644 --- a/homeassistant/components/homekit_controller/number.py +++ b/homeassistant/components/homekit_controller/number.py @@ -8,11 +8,12 @@ from __future__ import annotations from aiohomekit.model.characteristics import Characteristic, CharacteristicsTypes -from homeassistant.components.number import NumberEntity, NumberEntityDescription -from homeassistant.components.number.const import ( +from homeassistant.components.number import ( DEFAULT_MAX_VALUE, DEFAULT_MIN_VALUE, DEFAULT_STEP, + NumberEntity, + NumberEntityDescription, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback diff --git a/homeassistant/components/juicenet/number.py b/homeassistant/components/juicenet/number.py index c7f444b83e2..45be1dd9004 100644 --- a/homeassistant/components/juicenet/number.py +++ b/homeassistant/components/juicenet/number.py @@ -5,8 +5,11 @@ from dataclasses import dataclass from pyjuicenet import Api, Charger -from homeassistant.components.number import NumberEntity, NumberEntityDescription -from homeassistant.components.number.const import DEFAULT_MAX_VALUE +from homeassistant.components.number import ( + DEFAULT_MAX_VALUE, + NumberEntity, + NumberEntityDescription, +) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback diff --git a/homeassistant/components/template/number.py b/homeassistant/components/template/number.py index 89f4d22b957..4e74b469984 100644 --- a/homeassistant/components/template/number.py +++ b/homeassistant/components/template/number.py @@ -6,8 +6,7 @@ from typing import Any import voluptuous as vol -from homeassistant.components.number import NumberEntity -from homeassistant.components.number.const import ( +from homeassistant.components.number import ( ATTR_MAX, ATTR_MIN, ATTR_STEP, @@ -16,6 +15,7 @@ from homeassistant.components.number.const import ( DEFAULT_MIN_VALUE, DEFAULT_STEP, DOMAIN as NUMBER_DOMAIN, + NumberEntity, ) from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC, CONF_STATE, CONF_UNIQUE_ID from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/xiaomi_miio/number.py b/homeassistant/components/xiaomi_miio/number.py index 1bc843463e8..b324ab318a0 100644 --- a/homeassistant/components/xiaomi_miio/number.py +++ b/homeassistant/components/xiaomi_miio/number.py @@ -6,8 +6,11 @@ from dataclasses import dataclass from miio import Device -from homeassistant.components.number import NumberEntity, NumberEntityDescription -from homeassistant.components.number.const import DOMAIN as PLATFORM_DOMAIN +from homeassistant.components.number import ( + DOMAIN as PLATFORM_DOMAIN, + NumberEntity, + NumberEntityDescription, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_MODEL, DEGREE, TIME_MINUTES from homeassistant.core import HomeAssistant, callback From d8c5d08f90c75ad1b6f342c13f43ffa7fa8af5f8 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 12 Sep 2022 18:18:37 +0200 Subject: [PATCH 298/955] Expose websocket_api constants in root (#78249) --- homeassistant/components/websocket_api/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/websocket_api/__init__.py b/homeassistant/components/websocket_api/__init__.py index c98ca54d25a..043eb42c12a 100644 --- a/homeassistant/components/websocket_api/__init__.py +++ b/homeassistant/components/websocket_api/__init__.py @@ -12,6 +12,10 @@ from homeassistant.loader import bind_hass from . import commands, connection, const, decorators, http, messages # noqa: F401 from .connection import ActiveConnection, current_connection # noqa: F401 from .const import ( # noqa: F401 + COMPRESSED_STATE_ATTRIBUTES, + COMPRESSED_STATE_LAST_CHANGED, + COMPRESSED_STATE_LAST_UPDATED, + COMPRESSED_STATE_STATE, ERR_HOME_ASSISTANT_ERROR, ERR_INVALID_FORMAT, ERR_NOT_FOUND, From 2f3091122b31d940189523d31287c58435706224 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 12 Sep 2022 18:22:22 +0200 Subject: [PATCH 299/955] Import update constants from root (#78251) --- homeassistant/components/demo/update.py | 7 +++++-- homeassistant/components/zwave_js/update.py | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/demo/update.py b/homeassistant/components/demo/update.py index 648ad59bb55..15e67ffa0a8 100644 --- a/homeassistant/components/demo/update.py +++ b/homeassistant/components/demo/update.py @@ -4,8 +4,11 @@ from __future__ import annotations import asyncio from typing import Any -from homeassistant.components.update import UpdateDeviceClass, UpdateEntity -from homeassistant.components.update.const import UpdateEntityFeature +from homeassistant.components.update import ( + UpdateDeviceClass, + UpdateEntity, + UpdateEntityFeature, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import DEVICE_DEFAULT_NAME from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/zwave_js/update.py b/homeassistant/components/zwave_js/update.py index 932ed46a0fc..022c4dc3f3c 100644 --- a/homeassistant/components/zwave_js/update.py +++ b/homeassistant/components/zwave_js/update.py @@ -20,8 +20,11 @@ from zwave_js_server.model.firmware import ( ) from zwave_js_server.model.node import Node as ZwaveNode -from homeassistant.components.update import UpdateDeviceClass, UpdateEntity -from homeassistant.components.update.const import UpdateEntityFeature +from homeassistant.components.update import ( + UpdateDeviceClass, + UpdateEntity, + UpdateEntityFeature, +) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError From 17e217269fe34167414d0b5c6ab3bec4e772e28a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 12 Sep 2022 18:39:07 +0200 Subject: [PATCH 300/955] Expose device_automation constants in root (#78266) --- homeassistant/components/device_automation/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index 93119d1b4a0..3b75f4fff4c 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -30,6 +30,12 @@ from homeassistant.helpers.typing import ConfigType from homeassistant.loader import IntegrationNotFound from homeassistant.requirements import async_get_integration_with_requirements +from .const import ( # noqa: F401 + CONF_IS_OFF, + CONF_IS_ON, + CONF_TURNED_OFF, + CONF_TURNED_ON, +) from .exceptions import DeviceNotFound, InvalidDeviceAutomationConfig if TYPE_CHECKING: From 454bdcc00d9a986335910d3a025436a33d8d7213 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 12 Sep 2022 18:41:18 +0200 Subject: [PATCH 301/955] Expose http constants in root (#78267) --- homeassistant/components/http/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 7c8594bdd90..9e022ab0444 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -33,7 +33,12 @@ from homeassistant.util import ssl as ssl_util from .auth import async_setup_auth from .ban import setup_bans -from .const import KEY_AUTHENTICATED, KEY_HASS, KEY_HASS_USER # noqa: F401 +from .const import ( # noqa: F401 + KEY_AUTHENTICATED, + KEY_HASS, + KEY_HASS_REFRESH_TOKEN_ID, + KEY_HASS_USER, +) from .cors import setup_cors from .forwarded import async_setup_forwarded from .request_context import current_request, setup_request_context From 1e6e10ec937b43196abe09bea3dc28ad0db37d69 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 12 Sep 2022 18:43:50 +0200 Subject: [PATCH 302/955] Expose constants in device_tracker root (#78240) --- homeassistant/components/device_tracker/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 9e58c5bbc92..2d3353a1110 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -13,6 +13,7 @@ from .const import ( # noqa: F401 ATTR_DEV_ID, ATTR_GPS, ATTR_HOST_NAME, + ATTR_IP, ATTR_LOCATION_NAME, ATTR_MAC, ATTR_SOURCE_TYPE, @@ -20,8 +21,12 @@ from .const import ( # noqa: F401 CONF_NEW_DEVICE_DEFAULTS, CONF_SCAN_INTERVAL, CONF_TRACK_NEW, + CONNECTED_DEVICE_REGISTERED, + DEFAULT_CONSIDER_HOME, + DEFAULT_TRACK_NEW, DOMAIN, ENTITY_ID_FORMAT, + SCAN_INTERVAL, SOURCE_TYPE_BLUETOOTH, SOURCE_TYPE_BLUETOOTH_LE, SOURCE_TYPE_GPS, From 1fc8a570b06acee7990ebcdf9b97d84e9caf9a12 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 12 Sep 2022 18:46:25 +0200 Subject: [PATCH 303/955] Expose media-source constants in root (#78268) --- homeassistant/components/media_source/__init__.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/media_source/__init__.py b/homeassistant/components/media_source/__init__.py index 4818934d1dd..a882798687e 100644 --- a/homeassistant/components/media_source/__init__.py +++ b/homeassistant/components/media_source/__init__.py @@ -26,7 +26,13 @@ from homeassistant.helpers.typing import UNDEFINED, ConfigType, UndefinedType from homeassistant.loader import bind_hass from . import local_source -from .const import DOMAIN, URI_SCHEME, URI_SCHEME_REGEX +from .const import ( + DOMAIN, + MEDIA_CLASS_MAP, + MEDIA_MIME_TYPES, + URI_SCHEME, + URI_SCHEME_REGEX, +) from .error import MediaSourceError, Unresolvable from .models import BrowseMediaSource, MediaSourceItem, PlayMedia @@ -41,6 +47,8 @@ __all__ = [ "MediaSourceItem", "Unresolvable", "MediaSourceError", + "MEDIA_CLASS_MAP", + "MEDIA_MIME_TYPES", ] From 7aa7458a229e9f56ef7247ae4685540db2a8da8d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 12 Sep 2022 18:48:10 +0200 Subject: [PATCH 304/955] Expose modbus constants in root (#78269) --- homeassistant/components/modbus/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index 17a4acc1742..2e855c7af8d 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -50,11 +50,12 @@ from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType -from .const import ( +from .const import ( # noqa: F401 CALL_TYPE_COIL, CALL_TYPE_DISCRETE, CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_REGISTER_INPUT, + CALL_TYPE_WRITE_REGISTER, CALL_TYPE_X_COILS, CALL_TYPE_X_REGISTER_HOLDINGS, CONF_BAUDRATE, @@ -63,6 +64,7 @@ from .const import ( CONF_CLOSE_COMM_ON_ERROR, CONF_DATA_TYPE, CONF_FANS, + CONF_HUB, CONF_INPUT_TYPE, CONF_LAZY_ERROR, CONF_MAX_TEMP, From 7871a517a80e4981e37b80262d235ee38095850e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 12 Sep 2022 18:53:05 +0200 Subject: [PATCH 305/955] Import constants from root (#78271) --- homeassistant/components/camera/__init__.py | 2 +- homeassistant/components/cloud/google_config.py | 2 +- homeassistant/components/emulated_hue/hue_api.py | 2 +- homeassistant/components/fully_kiosk/const.py | 2 +- homeassistant/components/google_assistant/diagnostics.py | 3 +-- homeassistant/components/hassio/auth.py | 3 +-- homeassistant/components/homekit/__init__.py | 2 +- homeassistant/components/logbook/__init__.py | 2 +- homeassistant/components/lovelace/cast.py | 2 +- homeassistant/components/manual/alarm_control_panel.py | 4 +--- homeassistant/components/media_extractor/__init__.py | 4 ++-- homeassistant/components/mobile_app/webhook.py | 2 +- homeassistant/components/motioneye/__init__.py | 2 +- homeassistant/components/nest/__init__.py | 2 +- homeassistant/components/onkyo/media_player.py | 2 +- homeassistant/components/onvif/camera.py | 2 +- homeassistant/components/plex/cast.py | 2 +- homeassistant/components/roku/media_player.py | 2 +- homeassistant/components/sensor/recorder.py | 2 +- homeassistant/components/sonos/media_player.py | 2 +- homeassistant/components/switch_as_x/cover.py | 2 +- homeassistant/components/switch_as_x/entity.py | 2 +- homeassistant/components/switch_as_x/lock.py | 2 +- homeassistant/components/template/select.py | 4 ++-- homeassistant/components/zeroconf/__init__.py | 3 +-- homeassistant/components/zwave_js/diagnostics.py | 2 +- 26 files changed, 28 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 8d7557ad81d..6e2b36070ae 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -21,7 +21,7 @@ import voluptuous as vol from homeassistant.components import websocket_api from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView -from homeassistant.components.media_player.const import ( +from homeassistant.components.media_player import ( ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, DOMAIN as DOMAIN_MP, diff --git a/homeassistant/components/cloud/google_config.py b/homeassistant/components/cloud/google_config.py index 9bb2e405dca..42570dfff6e 100644 --- a/homeassistant/components/cloud/google_config.py +++ b/homeassistant/components/cloud/google_config.py @@ -6,7 +6,7 @@ import logging from hass_nabucasa import Cloud, cloud_api from hass_nabucasa.google_report_state import ErrorResponse -from homeassistant.components.google_assistant.const import DOMAIN as GOOGLE_DOMAIN +from homeassistant.components.google_assistant import DOMAIN as GOOGLE_DOMAIN from homeassistant.components.google_assistant.helpers import AbstractConfig from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES from homeassistant.core import CoreState, Event, callback, split_entity_id diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index aa43fbad910..687a7d5fbe6 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -43,7 +43,7 @@ from homeassistant.components.light import ( ATTR_XY_COLOR, LightEntityFeature, ) -from homeassistant.components.media_player.const import ( +from homeassistant.components.media_player import ( ATTR_MEDIA_VOLUME_LEVEL, MediaPlayerEntityFeature, ) diff --git a/homeassistant/components/fully_kiosk/const.py b/homeassistant/components/fully_kiosk/const.py index 56248544b81..4af7628ed63 100644 --- a/homeassistant/components/fully_kiosk/const.py +++ b/homeassistant/components/fully_kiosk/const.py @@ -5,7 +5,7 @@ from datetime import timedelta import logging from typing import Final -from homeassistant.components.media_player.const import MediaPlayerEntityFeature +from homeassistant.components.media_player import MediaPlayerEntityFeature DOMAIN: Final = "fully_kiosk" diff --git a/homeassistant/components/google_assistant/diagnostics.py b/homeassistant/components/google_assistant/diagnostics.py index 01e17e0bcf8..6cfaec99f6e 100644 --- a/homeassistant/components/google_assistant/diagnostics.py +++ b/homeassistant/components/google_assistant/diagnostics.py @@ -3,8 +3,7 @@ from __future__ import annotations from typing import Any -from homeassistant.components.diagnostics import async_redact_data -from homeassistant.components.diagnostics.const import REDACTED +from homeassistant.components.diagnostics import REDACTED, async_redact_data from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/hassio/auth.py b/homeassistant/components/hassio/auth.py index 37687ee70df..afe944d03bc 100644 --- a/homeassistant/components/hassio/auth.py +++ b/homeassistant/components/hassio/auth.py @@ -10,8 +10,7 @@ import voluptuous as vol from homeassistant.auth.models import User from homeassistant.auth.providers import homeassistant as auth_ha -from homeassistant.components.http import HomeAssistantView -from homeassistant.components.http.const import KEY_HASS_USER +from homeassistant.components.http import KEY_HASS_USER, HomeAssistantView from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 6852ff4fa9f..ee53c6da665 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -23,7 +23,7 @@ from homeassistant.components.binary_sensor import ( from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN from homeassistant.components.http import HomeAssistantView from homeassistant.components.humidifier import DOMAIN as HUMIDIFIER_DOMAIN -from homeassistant.components.network.const import MDNS_TARGET_IP +from homeassistant.components.network import MDNS_TARGET_IP from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index 5b528baabed..fb1b9d78b89 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -7,7 +7,7 @@ from typing import Any import voluptuous as vol from homeassistant.components import frontend -from homeassistant.components.recorder.const import DOMAIN as RECORDER_DOMAIN +from homeassistant.components.recorder import DOMAIN as RECORDER_DOMAIN from homeassistant.components.recorder.filters import ( extract_include_exclude_filter_conf, merge_include_exclude_filters, diff --git a/homeassistant/components/lovelace/cast.py b/homeassistant/components/lovelace/cast.py index 73645e66ddb..02f5d0c0478 100644 --- a/homeassistant/components/lovelace/cast.py +++ b/homeassistant/components/lovelace/cast.py @@ -5,7 +5,7 @@ from __future__ import annotations from pychromecast import Chromecast from pychromecast.const import CAST_TYPE_CHROMECAST -from homeassistant.components.cast.const import DOMAIN as CAST_DOMAIN +from homeassistant.components.cast import DOMAIN as CAST_DOMAIN from homeassistant.components.cast.home_assistant_cast import ( ATTR_URL_PATH, ATTR_VIEW_PATH, diff --git a/homeassistant/components/manual/alarm_control_panel.py b/homeassistant/components/manual/alarm_control_panel.py index 9a5d84f5997..d96ada6e139 100644 --- a/homeassistant/components/manual/alarm_control_panel.py +++ b/homeassistant/components/manual/alarm_control_panel.py @@ -10,9 +10,7 @@ from typing import Any import voluptuous as vol import homeassistant.components.alarm_control_panel as alarm -from homeassistant.components.alarm_control_panel.const import ( - AlarmControlPanelEntityFeature, -) +from homeassistant.components.alarm_control_panel import AlarmControlPanelEntityFeature from homeassistant.const import ( CONF_ARMING_TIME, CONF_CODE, diff --git a/homeassistant/components/media_extractor/__init__.py b/homeassistant/components/media_extractor/__init__.py index c6f9c666649..081375e8e08 100644 --- a/homeassistant/components/media_extractor/__init__.py +++ b/homeassistant/components/media_extractor/__init__.py @@ -5,11 +5,11 @@ import voluptuous as vol from youtube_dl import YoutubeDL from youtube_dl.utils import DownloadError, ExtractorError -from homeassistant.components.media_player import MEDIA_PLAYER_PLAY_MEDIA_SCHEMA -from homeassistant.components.media_player.const import ( +from homeassistant.components.media_player import ( ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, DOMAIN as MEDIA_PLAYER_DOMAIN, + MEDIA_PLAYER_PLAY_MEDIA_SCHEMA, SERVICE_PLAY_MEDIA, ) from homeassistant.const import ATTR_ENTITY_ID diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index 61b69ebad5c..7c6bcc58db9 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -27,7 +27,7 @@ from homeassistant.components.sensor import ( DEVICE_CLASSES as SENSOR_CLASSES, STATE_CLASSES as SENSOSR_STATE_CLASSES, ) -from homeassistant.components.zone.const import DOMAIN as ZONE_DOMAIN +from homeassistant.components.zone import DOMAIN as ZONE_DOMAIN from homeassistant.const import ( ATTR_DEVICE_ID, ATTR_DOMAIN, diff --git a/homeassistant/components/motioneye/__init__.py b/homeassistant/components/motioneye/__init__.py index 37562d7d15f..c7aa8edc6c9 100644 --- a/homeassistant/components/motioneye/__init__.py +++ b/homeassistant/components/motioneye/__init__.py @@ -36,7 +36,7 @@ from motioneye_client.const import ( ) from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN -from homeassistant.components.media_source.const import URI_SCHEME +from homeassistant.components.media_source import URI_SCHEME from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.components.webhook import ( diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index aa50b999c1c..99760d4be39 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -27,7 +27,7 @@ from homeassistant.components.application_credentials import ( async_import_client_credential, ) from homeassistant.components.camera import Image, img_util -from homeassistant.components.http.const import KEY_HASS_USER +from homeassistant.components.http import KEY_HASS_USER from homeassistant.components.http.view import HomeAssistantView from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( diff --git a/homeassistant/components/onkyo/media_player.py b/homeassistant/components/onkyo/media_player.py index 5465bee9ecf..c1d242c840c 100644 --- a/homeassistant/components/onkyo/media_player.py +++ b/homeassistant/components/onkyo/media_player.py @@ -9,12 +9,12 @@ from eiscp import eISCP import voluptuous as vol from homeassistant.components.media_player import ( + DOMAIN, PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, MediaPlayerState, ) -from homeassistant.components.media_player.const import DOMAIN from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant, ServiceCall import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index 758631e6e29..6c76f98a8da 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -12,8 +12,8 @@ from homeassistant.components.ffmpeg import CONF_EXTRA_ARGUMENTS, get_ffmpeg_man from homeassistant.components.stream import ( CONF_RTSP_TRANSPORT, CONF_USE_WALLCLOCK_AS_TIMESTAMPS, + RTSP_TRANSPORTS, ) -from homeassistant.components.stream.const import RTSP_TRANSPORTS from homeassistant.config_entries import ConfigEntry from homeassistant.const import HTTP_BASIC_AUTHENTICATION from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/plex/cast.py b/homeassistant/components/plex/cast.py index 720875bec6b..7dc112b72de 100644 --- a/homeassistant/components/plex/cast.py +++ b/homeassistant/components/plex/cast.py @@ -4,7 +4,7 @@ from __future__ import annotations from pychromecast import Chromecast from pychromecast.controllers.plex import PlexController -from homeassistant.components.cast.const import DOMAIN as CAST_DOMAIN +from homeassistant.components.cast import DOMAIN as CAST_DOMAIN from homeassistant.components.media_player import BrowseMedia, MediaClass, MediaType from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/roku/media_player.py b/homeassistant/components/roku/media_player.py index 4df38ca874a..7b495247c4d 100644 --- a/homeassistant/components/roku/media_player.py +++ b/homeassistant/components/roku/media_player.py @@ -21,7 +21,7 @@ from homeassistant.components.media_player import ( MediaType, async_process_play_media_url, ) -from homeassistant.components.stream.const import FORMAT_CONTENT_TYPE, HLS_PROVIDER +from homeassistant.components.stream import FORMAT_CONTENT_TYPE, HLS_PROVIDER from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_NAME from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 8c70a2c3ffe..b2542d98738 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -12,12 +12,12 @@ from typing import Any from sqlalchemy.orm.session import Session from homeassistant.components.recorder import ( + DOMAIN as RECORDER_DOMAIN, history, is_entity_recorded, statistics, util as recorder_util, ) -from homeassistant.components.recorder.const import DOMAIN as RECORDER_DOMAIN from homeassistant.components.recorder.models import ( StatisticData, StatisticMetaData, diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 8d541bd8f0d..1b0d8dc6ed1 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -29,7 +29,7 @@ from homeassistant.components.media_player import ( RepeatMode, async_process_play_media_url, ) -from homeassistant.components.plex.const import PLEX_URI_SCHEME +from homeassistant.components.plex import PLEX_URI_SCHEME from homeassistant.components.plex.services import process_plex_payload from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TIME diff --git a/homeassistant/components/switch_as_x/cover.py b/homeassistant/components/switch_as_x/cover.py index 4aea836517d..e480151a946 100644 --- a/homeassistant/components/switch_as_x/cover.py +++ b/homeassistant/components/switch_as_x/cover.py @@ -4,7 +4,7 @@ from __future__ import annotations from typing import Any from homeassistant.components.cover import CoverEntity, CoverEntityFeature -from homeassistant.components.switch.const import DOMAIN as SWITCH_DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, diff --git a/homeassistant/components/switch_as_x/entity.py b/homeassistant/components/switch_as_x/entity.py index 040a5d35232..4d478154763 100644 --- a/homeassistant/components/switch_as_x/entity.py +++ b/homeassistant/components/switch_as_x/entity.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import Any -from homeassistant.components.switch.const import DOMAIN as SWITCH_DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TURN_OFF, diff --git a/homeassistant/components/switch_as_x/lock.py b/homeassistant/components/switch_as_x/lock.py index 0eaabb03770..a4e3b2ec180 100644 --- a/homeassistant/components/switch_as_x/lock.py +++ b/homeassistant/components/switch_as_x/lock.py @@ -4,7 +4,7 @@ from __future__ import annotations from typing import Any from homeassistant.components.lock import LockEntity -from homeassistant.components.switch.const import DOMAIN as SWITCH_DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, diff --git a/homeassistant/components/template/select.py b/homeassistant/components/template/select.py index 4aa36a378ab..7871410a694 100644 --- a/homeassistant/components/template/select.py +++ b/homeassistant/components/template/select.py @@ -6,11 +6,11 @@ from typing import Any import voluptuous as vol -from homeassistant.components.select import SelectEntity -from homeassistant.components.select.const import ( +from homeassistant.components.select import ( ATTR_OPTION, ATTR_OPTIONS, DOMAIN as SELECT_DOMAIN, + SelectEntity, ) from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC, CONF_STATE, CONF_UNIQUE_ID from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 476cbd82cf8..e6c635dc308 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -20,8 +20,7 @@ from zeroconf.asyncio import AsyncServiceInfo from homeassistant import config_entries from homeassistant.components import network -from homeassistant.components.network import async_get_source_ip -from homeassistant.components.network.const import MDNS_TARGET_IP +from homeassistant.components.network import MDNS_TARGET_IP, async_get_source_ip from homeassistant.components.network.models import Adapter from homeassistant.const import ( EVENT_HOMEASSISTANT_START, diff --git a/homeassistant/components/zwave_js/diagnostics.py b/homeassistant/components/zwave_js/diagnostics.py index 33d32e96fe0..2b3d72078b9 100644 --- a/homeassistant/components/zwave_js/diagnostics.py +++ b/homeassistant/components/zwave_js/diagnostics.py @@ -11,7 +11,7 @@ from zwave_js_server.dump import dump_msgs from zwave_js_server.model.node import Node, NodeDataType from zwave_js_server.model.value import ValueDataType -from homeassistant.components.diagnostics.const import REDACTED +from homeassistant.components.diagnostics import REDACTED from homeassistant.components.diagnostics.util import async_redact_data from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_URL From 5e9c0399eb7d4d0b2de50793b830c3c0cc7c1b74 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 12 Sep 2022 18:58:06 +0200 Subject: [PATCH 306/955] Add STT checks to pylint plugin (#78284) --- pylint/plugins/hass_enforce_type_hints.py | 36 +++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 80ec054159c..2b99dde8c0d 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -2072,6 +2072,42 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ], ), ], + "stt": [ + ClassTypeHintMatch( + base_class="Provider", + matches=[ + TypeHintMatch( + function_name="supported_languages", + return_type="list[str]", + ), + TypeHintMatch( + function_name="supported_formats", + return_type="list[AudioFormats]", + ), + TypeHintMatch( + function_name="supported_codecs", + return_type="list[AudioCodecs]", + ), + TypeHintMatch( + function_name="supported_bit_rates", + return_type="list[AudioBitRates]", + ), + TypeHintMatch( + function_name="supported_sample_rates", + return_type="list[AudioSampleRates]", + ), + TypeHintMatch( + function_name="supported_channels", + return_type="list[AudioChannels]", + ), + TypeHintMatch( + function_name="async_process_audio_stream", + arg_types={1: "SpeechMetadata", 2: "StreamReader"}, + return_type="SpeechResult", + ), + ], + ), + ], "switch": [ ClassTypeHintMatch( base_class="Entity", From 5c8e8e48607f9e3ab13a17f6566fd50d09c448c7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 12 Sep 2022 20:06:27 +0200 Subject: [PATCH 307/955] Use new media player enums (#78264) --- .../components/camera/media_source.py | 12 +- homeassistant/components/dlna_dms/dms.py | 5 +- .../components/dlna_dms/media_source.py | 18 +-- .../components/google_assistant/trait.py | 4 +- .../components/jellyfin/media_source.py | 35 +++--- homeassistant/components/kodi/browse_media.py | 107 +++++++--------- .../components/media_source/const.py | 12 +- .../components/media_source/local_source.py | 11 +- .../components/media_source/models.py | 17 +-- .../components/motioneye/media_source.py | 38 +++--- homeassistant/components/nest/media_source.py | 29 ++--- .../components/netatmo/media_source.py | 11 +- .../components/plex/media_browser.py | 59 ++++----- homeassistant/components/plex/models.py | 15 +-- homeassistant/components/plex/server.py | 5 +- homeassistant/components/ps4/__init__.py | 6 +- .../components/radio_browser/media_source.py | 40 +++--- homeassistant/components/roku/browse_media.py | 61 +++++---- .../components/roon/media_browser.py | 17 +-- homeassistant/components/sonos/const.py | 119 ++++++++---------- .../components/sonos/media_browser.py | 25 ++-- .../components/spotify/browse_media.py | 115 ++++++++--------- homeassistant/components/spotify/const.py | 18 +-- .../components/squeezebox/browse_media.py | 78 ++++++------ .../components/system_bridge/media_source.py | 22 ++-- homeassistant/components/tts/__init__.py | 6 +- homeassistant/components/tts/media_source.py | 9 +- .../components/unifiprotect/media_source.py | 39 +++--- .../components/volumio/browse_media.py | 59 ++++----- homeassistant/components/xbox/browse_media.py | 31 ++--- homeassistant/components/xbox/media_source.py | 31 ++--- 31 files changed, 445 insertions(+), 609 deletions(-) diff --git a/homeassistant/components/camera/media_source.py b/homeassistant/components/camera/media_source.py index 733efb3a430..117f65edb07 100644 --- a/homeassistant/components/camera/media_source.py +++ b/homeassistant/components/camera/media_source.py @@ -3,11 +3,7 @@ from __future__ import annotations from typing import Optional, cast -from homeassistant.components.media_player.const import ( - MEDIA_CLASS_APP, - MEDIA_CLASS_VIDEO, -) -from homeassistant.components.media_player.errors import BrowseError +from homeassistant.components.media_player import BrowseError, MediaClass from homeassistant.components.media_source.error import Unresolvable from homeassistant.components.media_source.models import ( BrowseMediaSource, @@ -97,7 +93,7 @@ class CameraMediaSource(MediaSource): BrowseMediaSource( domain=DOMAIN, identifier=camera.entity_id, - media_class=MEDIA_CLASS_VIDEO, + media_class=MediaClass.VIDEO, media_content_type=content_type, title=camera.name, thumbnail=f"/api/camera_proxy/{camera.entity_id}", @@ -109,12 +105,12 @@ class CameraMediaSource(MediaSource): return BrowseMediaSource( domain=DOMAIN, identifier=None, - media_class=MEDIA_CLASS_APP, + media_class=MediaClass.APP, media_content_type="", title="Camera", can_play=False, can_expand=True, - children_media_class=MEDIA_CLASS_VIDEO, + children_media_class=MediaClass.VIDEO, children=children, not_shown=not_shown, ) diff --git a/homeassistant/components/dlna_dms/dms.py b/homeassistant/components/dlna_dms/dms.py index d47f480132b..2fd1a85ebae 100644 --- a/homeassistant/components/dlna_dms/dms.py +++ b/homeassistant/components/dlna_dms/dms.py @@ -17,8 +17,7 @@ from didl_lite import didl_lite from homeassistant.backports.enum import StrEnum from homeassistant.components import ssdp -from homeassistant.components.media_player.const import MEDIA_CLASS_DIRECTORY -from homeassistant.components.media_player.errors import BrowseError +from homeassistant.components.media_player import BrowseError, MediaClass from homeassistant.components.media_source.error import Unresolvable from homeassistant.components.media_source.models import BrowseMediaSource, PlayMedia from homeassistant.config_entries import ConfigEntry @@ -518,7 +517,7 @@ class DmsDeviceSource: media_source = BrowseMediaSource( domain=DOMAIN, identifier=self._make_identifier(Action.SEARCH, query), - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_type="", title="Search results", can_play=False, diff --git a/homeassistant/components/dlna_dms/media_source.py b/homeassistant/components/dlna_dms/media_source.py index 84910b7ff67..c1245997c7a 100644 --- a/homeassistant/components/dlna_dms/media_source.py +++ b/homeassistant/components/dlna_dms/media_source.py @@ -12,13 +12,7 @@ Media identifiers can look like: from __future__ import annotations -from homeassistant.components.media_player.const import ( - MEDIA_CLASS_CHANNEL, - MEDIA_CLASS_DIRECTORY, - MEDIA_TYPE_CHANNEL, - MEDIA_TYPE_CHANNELS, -) -from homeassistant.components.media_player.errors import BrowseError +from homeassistant.components.media_player import BrowseError, MediaClass, MediaType from homeassistant.components.media_source.error import Unresolvable from homeassistant.components.media_source.models import ( BrowseMediaSource, @@ -82,20 +76,20 @@ class DmsMediaSource(MediaSource): base = BrowseMediaSource( domain=DOMAIN, identifier="", - media_class=MEDIA_CLASS_DIRECTORY, - media_content_type=MEDIA_TYPE_CHANNELS, + media_class=MediaClass.DIRECTORY, + media_content_type=MediaType.CHANNELS, title=self.name, can_play=False, can_expand=True, - children_media_class=MEDIA_CLASS_CHANNEL, + children_media_class=MediaClass.CHANNEL, ) base.children = [ BrowseMediaSource( domain=DOMAIN, identifier=f"{source_id}/{PATH_OBJECT_ID_FLAG}{ROOT_OBJECT_ID}", - media_class=MEDIA_CLASS_CHANNEL, - media_content_type=MEDIA_TYPE_CHANNEL, + media_class=MediaClass.CHANNEL, + media_content_type=MediaType.CHANNEL, title=source.name, can_play=False, can_expand=True, diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 4f2971c01fa..8f3f20d177e 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -28,7 +28,7 @@ from homeassistant.components import ( from homeassistant.components.climate import const as climate from homeassistant.components.humidifier import const as humidifier from homeassistant.components.lock import STATE_JAMMED, STATE_UNLOCKING -from homeassistant.components.media_player.const import MEDIA_TYPE_CHANNEL +from homeassistant.components.media_player import MediaType from homeassistant.const import ( ATTR_ASSUMED_STATE, ATTR_BATTERY_LEVEL, @@ -2347,7 +2347,7 @@ class ChannelTrait(_Trait): { ATTR_ENTITY_ID: self.state.entity_id, media_player.ATTR_MEDIA_CONTENT_ID: channel_number, - media_player.ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_CHANNEL, + media_player.ATTR_MEDIA_CONTENT_TYPE: MediaType.CHANNEL, }, blocking=not self.config.should_report_state, context=data.context, diff --git a/homeassistant/components/jellyfin/media_source.py b/homeassistant/components/jellyfin/media_source.py index 662c0f0040a..2cb211acb9b 100644 --- a/homeassistant/components/jellyfin/media_source.py +++ b/homeassistant/components/jellyfin/media_source.py @@ -8,14 +8,7 @@ from typing import Any from jellyfin_apiclient_python.api import jellyfin_url from jellyfin_apiclient_python.client import JellyfinClient -from homeassistant.components.media_player.const import ( - MEDIA_CLASS_ALBUM, - MEDIA_CLASS_ARTIST, - MEDIA_CLASS_DIRECTORY, - MEDIA_CLASS_MOVIE, - MEDIA_CLASS_TRACK, -) -from homeassistant.components.media_player.errors import BrowseError +from homeassistant.components.media_player import BrowseError, MediaClass from homeassistant.components.media_source.models import ( BrowseMediaSource, MediaSource, @@ -113,12 +106,12 @@ class JellyfinSource(MediaSource): base = BrowseMediaSource( domain=DOMAIN, identifier=None, - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_type=MEDIA_TYPE_NONE, title=self.name, can_play=False, can_expand=True, - children_media_class=MEDIA_CLASS_DIRECTORY, + children_media_class=MediaClass.DIRECTORY, ) libraries = await self._get_libraries() @@ -164,7 +157,7 @@ class JellyfinSource(MediaSource): result = BrowseMediaSource( domain=DOMAIN, identifier=library_id, - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_type=MEDIA_TYPE_NONE, title=library_name, can_play=False, @@ -172,10 +165,10 @@ class JellyfinSource(MediaSource): ) if include_children: - result.children_media_class = MEDIA_CLASS_ARTIST + result.children_media_class = MediaClass.ARTIST result.children = await self._build_artists(library_id) if not result.children: - result.children_media_class = MEDIA_CLASS_ALBUM + result.children_media_class = MediaClass.ALBUM result.children = await self._build_albums(library_id) return result @@ -197,7 +190,7 @@ class JellyfinSource(MediaSource): result = BrowseMediaSource( domain=DOMAIN, identifier=artist_id, - media_class=MEDIA_CLASS_ARTIST, + media_class=MediaClass.ARTIST, media_content_type=MEDIA_TYPE_NONE, title=artist_name, can_play=False, @@ -206,7 +199,7 @@ class JellyfinSource(MediaSource): ) if include_children: - result.children_media_class = MEDIA_CLASS_ALBUM + result.children_media_class = MediaClass.ALBUM result.children = await self._build_albums(artist_id) return result @@ -228,7 +221,7 @@ class JellyfinSource(MediaSource): result = BrowseMediaSource( domain=DOMAIN, identifier=album_id, - media_class=MEDIA_CLASS_ALBUM, + media_class=MediaClass.ALBUM, media_content_type=MEDIA_TYPE_NONE, title=album_title, can_play=False, @@ -237,7 +230,7 @@ class JellyfinSource(MediaSource): ) if include_children: - result.children_media_class = MEDIA_CLASS_TRACK + result.children_media_class = MediaClass.TRACK result.children = await self._build_tracks(album_id) return result @@ -264,7 +257,7 @@ class JellyfinSource(MediaSource): result = BrowseMediaSource( domain=DOMAIN, identifier=track_id, - media_class=MEDIA_CLASS_TRACK, + media_class=MediaClass.TRACK, media_content_type=mime_type, title=track_title, can_play=True, @@ -284,7 +277,7 @@ class JellyfinSource(MediaSource): result = BrowseMediaSource( domain=DOMAIN, identifier=library_id, - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_type=MEDIA_TYPE_NONE, title=library_name, can_play=False, @@ -292,7 +285,7 @@ class JellyfinSource(MediaSource): ) if include_children: - result.children_media_class = MEDIA_CLASS_MOVIE + result.children_media_class = MediaClass.MOVIE result.children = await self._build_movies(library_id) return result @@ -313,7 +306,7 @@ class JellyfinSource(MediaSource): result = BrowseMediaSource( domain=DOMAIN, identifier=movie_id, - media_class=MEDIA_CLASS_MOVIE, + media_class=MediaClass.MOVIE, media_content_type=mime_type, title=movie_title, can_play=True, diff --git a/homeassistant/components/kodi/browse_media.py b/homeassistant/components/kodi/browse_media.py index 73247d23a9d..cfb4fa3caa6 100644 --- a/homeassistant/components/kodi/browse_media.py +++ b/homeassistant/components/kodi/browse_media.py @@ -4,54 +4,37 @@ import contextlib import logging from homeassistant.components import media_source -from homeassistant.components.media_player import BrowseError, BrowseMedia -from homeassistant.components.media_player.const import ( - MEDIA_CLASS_ALBUM, - MEDIA_CLASS_ARTIST, - MEDIA_CLASS_CHANNEL, - MEDIA_CLASS_DIRECTORY, - MEDIA_CLASS_EPISODE, - MEDIA_CLASS_MOVIE, - MEDIA_CLASS_MUSIC, - MEDIA_CLASS_PLAYLIST, - MEDIA_CLASS_SEASON, - MEDIA_CLASS_TRACK, - MEDIA_CLASS_TV_SHOW, - MEDIA_TYPE_ALBUM, - MEDIA_TYPE_ARTIST, - MEDIA_TYPE_CHANNEL, - MEDIA_TYPE_EPISODE, - MEDIA_TYPE_MOVIE, - MEDIA_TYPE_PLAYLIST, - MEDIA_TYPE_SEASON, - MEDIA_TYPE_TRACK, - MEDIA_TYPE_TVSHOW, +from homeassistant.components.media_player import ( + BrowseError, + BrowseMedia, + MediaClass, + MediaType, ) PLAYABLE_MEDIA_TYPES = [ - MEDIA_TYPE_ALBUM, - MEDIA_TYPE_ARTIST, - MEDIA_TYPE_TRACK, + MediaType.ALBUM, + MediaType.ARTIST, + MediaType.TRACK, ] CONTAINER_TYPES_SPECIFIC_MEDIA_CLASS = { - MEDIA_TYPE_ALBUM: MEDIA_CLASS_ALBUM, - MEDIA_TYPE_ARTIST: MEDIA_CLASS_ARTIST, - MEDIA_TYPE_PLAYLIST: MEDIA_CLASS_PLAYLIST, - MEDIA_TYPE_SEASON: MEDIA_CLASS_SEASON, - MEDIA_TYPE_TVSHOW: MEDIA_CLASS_TV_SHOW, + MediaType.ALBUM: MediaClass.ALBUM, + MediaType.ARTIST: MediaClass.ARTIST, + MediaType.PLAYLIST: MediaClass.PLAYLIST, + MediaType.SEASON: MediaClass.SEASON, + MediaType.TVSHOW: MediaClass.TV_SHOW, } CHILD_TYPE_MEDIA_CLASS = { - MEDIA_TYPE_SEASON: MEDIA_CLASS_SEASON, - MEDIA_TYPE_ALBUM: MEDIA_CLASS_ALBUM, - MEDIA_TYPE_ARTIST: MEDIA_CLASS_ARTIST, - MEDIA_TYPE_MOVIE: MEDIA_CLASS_MOVIE, - MEDIA_TYPE_PLAYLIST: MEDIA_CLASS_PLAYLIST, - MEDIA_TYPE_TRACK: MEDIA_CLASS_TRACK, - MEDIA_TYPE_TVSHOW: MEDIA_CLASS_TV_SHOW, - MEDIA_TYPE_CHANNEL: MEDIA_CLASS_CHANNEL, - MEDIA_TYPE_EPISODE: MEDIA_CLASS_EPISODE, + MediaType.SEASON: MediaClass.SEASON, + MediaType.ALBUM: MediaClass.ALBUM, + MediaType.ARTIST: MediaClass.ARTIST, + MediaType.MOVIE: MediaClass.MOVIE, + MediaType.PLAYLIST: MediaClass.PLAYLIST, + MediaType.TRACK: MediaClass.TRACK, + MediaType.TVSHOW: MediaClass.TV_SHOW, + MediaType.CHANNEL: MediaClass.CHANNEL, + MediaType.EPISODE: MediaClass.EPISODE, } _LOGGER = logging.getLogger(__name__) @@ -76,12 +59,12 @@ async def build_item_response(media_library, payload, get_thumbnail_url=None): *(item_payload(item, get_thumbnail_url) for item in media) ) - if search_type in (MEDIA_TYPE_TVSHOW, MEDIA_TYPE_MOVIE) and search_id == "": + if search_type in (MediaType.TVSHOW, MediaType.MOVIE) and search_id == "": children.sort(key=lambda x: x.title.replace("The ", "", 1), reverse=False) response = BrowseMedia( media_class=CONTAINER_TYPES_SPECIFIC_MEDIA_CLASS.get( - search_type, MEDIA_CLASS_DIRECTORY + search_type, MediaClass.DIRECTORY ), media_content_id=search_id, media_content_type=search_type, @@ -93,7 +76,7 @@ async def build_item_response(media_library, payload, get_thumbnail_url=None): ) if search_type == "library_music": - response.children_media_class = MEDIA_CLASS_MUSIC + response.children_media_class = MediaClass.MUSIC else: response.calculate_children_class() @@ -111,42 +94,42 @@ async def item_payload(item, get_thumbnail_url=None): media_class = None if "songid" in item: - media_content_type = MEDIA_TYPE_TRACK + media_content_type = MediaType.TRACK media_content_id = f"{item['songid']}" can_play = True can_expand = False elif "albumid" in item: - media_content_type = MEDIA_TYPE_ALBUM + media_content_type = MediaType.ALBUM media_content_id = f"{item['albumid']}" can_play = True can_expand = True elif "artistid" in item: - media_content_type = MEDIA_TYPE_ARTIST + media_content_type = MediaType.ARTIST media_content_id = f"{item['artistid']}" can_play = True can_expand = True elif "movieid" in item: - media_content_type = MEDIA_TYPE_MOVIE + media_content_type = MediaType.MOVIE media_content_id = f"{item['movieid']}" can_play = True can_expand = False elif "episodeid" in item: - media_content_type = MEDIA_TYPE_EPISODE + media_content_type = MediaType.EPISODE media_content_id = f"{item['episodeid']}" can_play = True can_expand = False elif "seasonid" in item: - media_content_type = MEDIA_TYPE_SEASON + media_content_type = MediaType.SEASON media_content_id = f"{item['tvshowid']}/{item['season']}" can_play = False can_expand = True elif "tvshowid" in item: - media_content_type = MEDIA_TYPE_TVSHOW + media_content_type = MediaType.TVSHOW media_content_id = f"{item['tvshowid']}" can_play = False can_expand = True elif "channelid" in item: - media_content_type = MEDIA_TYPE_CHANNEL + media_content_type = MediaType.CHANNEL media_content_id = f"{item['channelid']}" if broadcasting := item.get("broadcastnow"): show = broadcasting.get("title") @@ -156,7 +139,7 @@ async def item_payload(item, get_thumbnail_url=None): else: # this case is for the top folder of each type # possible content types: album, artist, movie, library_music, tvshow, channel - media_class = MEDIA_CLASS_DIRECTORY + media_class = MediaClass.DIRECTORY media_content_type = item["type"] media_content_id = "" can_play = False @@ -202,7 +185,7 @@ async def library_payload(hass): Used by async_browse_media. """ library_info = BrowseMedia( - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_id="library", media_content_type="library", title="Media Library", @@ -213,9 +196,9 @@ async def library_payload(hass): library = { "library_music": "Music", - MEDIA_TYPE_MOVIE: "Movies", - MEDIA_TYPE_TVSHOW: "TV shows", - MEDIA_TYPE_CHANNEL: "Channels", + MediaType.MOVIE: "Movies", + MediaType.TVSHOW: "TV shows", + MediaType.CHANNEL: "Channels", } library_info.children = await asyncio.gather( @@ -256,7 +239,7 @@ async def get_media_info(media_library, search_id, search_type): media = None properties = ["thumbnail"] - if search_type == MEDIA_TYPE_ALBUM: + if search_type == MediaType.ALBUM: if search_id: album = await media_library.get_album_details( album_id=int(search_id), properties=properties @@ -282,7 +265,7 @@ async def get_media_info(media_library, search_id, search_type): media = media.get("albums") title = "Albums" - elif search_type == MEDIA_TYPE_ARTIST: + elif search_type == MediaType.ARTIST: if search_id: media = await media_library.get_albums( artist_id=int(search_id), properties=properties @@ -301,11 +284,11 @@ async def get_media_info(media_library, search_id, search_type): title = "Artists" elif search_type == "library_music": - library = {MEDIA_TYPE_ALBUM: "Albums", MEDIA_TYPE_ARTIST: "Artists"} + library = {MediaType.ALBUM: "Albums", MediaType.ARTIST: "Artists"} media = [{"label": name, "type": type_} for type_, name in library.items()] title = "Music Library" - elif search_type == MEDIA_TYPE_MOVIE: + elif search_type == MediaType.MOVIE: if search_id: movie = await media_library.get_movie_details( movie_id=int(search_id), properties=properties @@ -319,7 +302,7 @@ async def get_media_info(media_library, search_id, search_type): media = media.get("movies") title = "Movies" - elif search_type == MEDIA_TYPE_TVSHOW: + elif search_type == MediaType.TVSHOW: if search_id: media = await media_library.get_seasons( tv_show_id=int(search_id), @@ -338,7 +321,7 @@ async def get_media_info(media_library, search_id, search_type): media = media.get("tvshows") title = "TV Shows" - elif search_type == MEDIA_TYPE_SEASON: + elif search_type == MediaType.SEASON: tv_show_id, season_id = search_id.split("/", 1) media = await media_library.get_episodes( tv_show_id=int(tv_show_id), @@ -355,7 +338,7 @@ async def get_media_info(media_library, search_id, search_type): ) title = season["seasondetails"]["label"] - elif search_type == MEDIA_TYPE_CHANNEL: + elif search_type == MediaType.CHANNEL: media = await media_library.get_channels( channel_group_id="alltv", properties=["thumbnail", "channeltype", "channel", "broadcastnow"], diff --git a/homeassistant/components/media_source/const.py b/homeassistant/components/media_source/const.py index d146cd953f8..73599efb6c3 100644 --- a/homeassistant/components/media_source/const.py +++ b/homeassistant/components/media_source/const.py @@ -1,18 +1,14 @@ """Constants for the media_source integration.""" import re -from homeassistant.components.media_player.const import ( - MEDIA_CLASS_IMAGE, - MEDIA_CLASS_MUSIC, - MEDIA_CLASS_VIDEO, -) +from homeassistant.components.media_player import MediaClass DOMAIN = "media_source" MEDIA_MIME_TYPES = ("audio", "video", "image") MEDIA_CLASS_MAP = { - "audio": MEDIA_CLASS_MUSIC, - "video": MEDIA_CLASS_VIDEO, - "image": MEDIA_CLASS_IMAGE, + "audio": MediaClass.MUSIC, + "video": MediaClass.VIDEO, + "image": MediaClass.IMAGE, } URI_SCHEME = "media-source://" URI_SCHEME_REGEX = re.compile( diff --git a/homeassistant/components/media_source/local_source.py b/homeassistant/components/media_source/local_source.py index 863380b7600..1ec3d6f9462 100644 --- a/homeassistant/components/media_source/local_source.py +++ b/homeassistant/components/media_source/local_source.py @@ -11,8 +11,7 @@ from aiohttp.web_request import FileField import voluptuous as vol from homeassistant.components import http, websocket_api -from homeassistant.components.media_player.const import MEDIA_CLASS_DIRECTORY -from homeassistant.components.media_player.errors import BrowseError +from homeassistant.components.media_player import BrowseError, MediaClass from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import Unauthorized from homeassistant.util import raise_if_invalid_filename, raise_if_invalid_path @@ -109,12 +108,12 @@ class LocalSource(MediaSource): base = BrowseMediaSource( domain=DOMAIN, identifier="", - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_type=None, title=self.name, can_play=False, can_expand=True, - children_media_class=MEDIA_CLASS_DIRECTORY, + children_media_class=MediaClass.DIRECTORY, ) base.children = [ @@ -158,10 +157,10 @@ class LocalSource(MediaSource): title = path.name - media_class = MEDIA_CLASS_DIRECTORY + media_class = MediaClass.DIRECTORY if mime_type: media_class = MEDIA_CLASS_MAP.get( - mime_type.split("/")[0], MEDIA_CLASS_DIRECTORY + mime_type.split("/")[0], MediaClass.DIRECTORY ) media = BrowseMediaSource( diff --git a/homeassistant/components/media_source/models.py b/homeassistant/components/media_source/models.py index f6772bc6ad9..3bf77daf691 100644 --- a/homeassistant/components/media_source/models.py +++ b/homeassistant/components/media_source/models.py @@ -5,12 +5,7 @@ from abc import ABC from dataclasses import dataclass from typing import Any, cast -from homeassistant.components.media_player import BrowseMedia -from homeassistant.components.media_player.const import ( - MEDIA_CLASS_APP, - MEDIA_TYPE_APP, - MEDIA_TYPE_APPS, -) +from homeassistant.components.media_player import BrowseMedia, MediaClass, MediaType from homeassistant.core import HomeAssistant, callback from .const import DOMAIN, URI_SCHEME, URI_SCHEME_REGEX @@ -56,20 +51,20 @@ class MediaSourceItem: base = BrowseMediaSource( domain=None, identifier=None, - media_class=MEDIA_CLASS_APP, - media_content_type=MEDIA_TYPE_APPS, + media_class=MediaClass.APP, + media_content_type=MediaType.APPS, title="Media Sources", can_play=False, can_expand=True, - children_media_class=MEDIA_CLASS_APP, + children_media_class=MediaClass.APP, ) base.children = sorted( ( BrowseMediaSource( domain=source.domain, identifier=None, - media_class=MEDIA_CLASS_APP, - media_content_type=MEDIA_TYPE_APP, + media_class=MediaClass.APP, + media_content_type=MediaType.APP, thumbnail=f"https://brands.home-assistant.io/_/{source.domain}/logo.png", title=source.name, can_play=False, diff --git a/homeassistant/components/motioneye/media_source.py b/homeassistant/components/motioneye/media_source.py index 915cc30897e..85fe3985b93 100644 --- a/homeassistant/components/motioneye/media_source.py +++ b/homeassistant/components/motioneye/media_source.py @@ -7,13 +7,7 @@ from typing import Optional, cast from motioneye_client.const import KEY_MEDIA_LIST, KEY_MIME_TYPE, KEY_PATH -from homeassistant.components.media_player.const import ( - MEDIA_CLASS_DIRECTORY, - MEDIA_CLASS_IMAGE, - MEDIA_CLASS_VIDEO, - MEDIA_TYPE_IMAGE, - MEDIA_TYPE_VIDEO, -) +from homeassistant.components.media_player import MediaClass, MediaType from homeassistant.components.media_source.error import MediaSourceError, Unresolvable from homeassistant.components.media_source.models import ( BrowseMediaSource, @@ -34,8 +28,8 @@ MIME_TYPE_MAP = { } MEDIA_CLASS_MAP = { - "movies": MEDIA_CLASS_VIDEO, - "images": MEDIA_CLASS_IMAGE, + "movies": MediaClass.VIDEO, + "images": MediaClass.IMAGE, } _LOGGER = logging.getLogger(__name__) @@ -172,12 +166,12 @@ class MotionEyeMediaSource(MediaSource): return BrowseMediaSource( domain=DOMAIN, identifier=config.entry_id, - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_type="", title=config.title, can_play=False, can_expand=True, - children_media_class=MEDIA_CLASS_DIRECTORY, + children_media_class=MediaClass.DIRECTORY, ) def _build_media_configs(self) -> BrowseMediaSource: @@ -185,7 +179,7 @@ class MotionEyeMediaSource(MediaSource): return BrowseMediaSource( domain=DOMAIN, identifier="", - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_type="", title="motionEye Media", can_play=False, @@ -194,7 +188,7 @@ class MotionEyeMediaSource(MediaSource): self._build_media_config(entry) for entry in self.hass.config_entries.async_entries(DOMAIN) ], - children_media_class=MEDIA_CLASS_DIRECTORY, + children_media_class=MediaClass.DIRECTORY, ) @classmethod @@ -207,12 +201,12 @@ class MotionEyeMediaSource(MediaSource): return BrowseMediaSource( domain=DOMAIN, identifier=f"{config.entry_id}#{device.id}", - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_type="", title=f"{config.title} {device.name}" if full_title else device.name, can_play=False, can_expand=True, - children_media_class=MEDIA_CLASS_DIRECTORY, + children_media_class=MediaClass.DIRECTORY, ) def _build_media_devices(self, config: ConfigEntry) -> BrowseMediaSource: @@ -238,9 +232,9 @@ class MotionEyeMediaSource(MediaSource): return BrowseMediaSource( domain=DOMAIN, identifier=f"{config.entry_id}#{device.id}#{kind}", - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_type=( - MEDIA_TYPE_VIDEO if kind == "movies" else MEDIA_TYPE_IMAGE + MediaType.VIDEO if kind == "movies" else MediaType.IMAGE ), title=( f"{config.title} {device.name} {kind.title()}" @@ -250,7 +244,7 @@ class MotionEyeMediaSource(MediaSource): can_play=False, can_expand=True, children_media_class=( - MEDIA_CLASS_VIDEO if kind == "movies" else MEDIA_CLASS_IMAGE + MediaClass.VIDEO if kind == "movies" else MediaClass.IMAGE ), ) @@ -340,16 +334,16 @@ class MotionEyeMediaSource(MediaSource): f"{config.entry_id}#{device.id}" f"#{kind}#{full_child_path}" ), - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_type=( - MEDIA_TYPE_VIDEO + MediaType.VIDEO if kind == "movies" - else MEDIA_TYPE_IMAGE + else MediaType.IMAGE ), title=display_child_path, can_play=False, can_expand=True, - children_media_class=MEDIA_CLASS_DIRECTORY, + children_media_class=MediaClass.DIRECTORY, ) ) return base diff --git a/homeassistant/components/nest/media_source.py b/homeassistant/components/nest/media_source.py index 7b5b96b7145..d9478a99316 100644 --- a/homeassistant/components/nest/media_source.py +++ b/homeassistant/components/nest/media_source.py @@ -36,14 +36,7 @@ from google_nest_sdm.google_nest_subscriber import GoogleNestSubscriber from google_nest_sdm.transcoder import Transcoder from homeassistant.components.ffmpeg import get_ffmpeg_manager -from homeassistant.components.media_player.const import ( - MEDIA_CLASS_DIRECTORY, - MEDIA_CLASS_IMAGE, - MEDIA_CLASS_VIDEO, - MEDIA_TYPE_IMAGE, - MEDIA_TYPE_VIDEO, -) -from homeassistant.components.media_player.errors import BrowseError +from homeassistant.components.media_player import BrowseError, MediaClass, MediaType from homeassistant.components.media_source.error import Unresolvable from homeassistant.components.media_source.models import ( BrowseMediaSource, @@ -450,9 +443,9 @@ def _browse_root() -> BrowseMediaSource: return BrowseMediaSource( domain=DOMAIN, identifier="", - media_class=MEDIA_CLASS_DIRECTORY, - media_content_type=MEDIA_TYPE_VIDEO, - children_media_class=MEDIA_CLASS_VIDEO, + media_class=MediaClass.DIRECTORY, + media_content_type=MediaType.VIDEO, + children_media_class=MediaClass.VIDEO, title=MEDIA_SOURCE_TITLE, can_play=False, can_expand=True, @@ -482,9 +475,9 @@ def _browse_device(device_id: MediaId, device: Device) -> BrowseMediaSource: return BrowseMediaSource( domain=DOMAIN, identifier=device_id.identifier, - media_class=MEDIA_CLASS_DIRECTORY, - media_content_type=MEDIA_TYPE_VIDEO, - children_media_class=MEDIA_CLASS_VIDEO, + media_class=MediaClass.DIRECTORY, + media_content_type=MediaType.VIDEO, + children_media_class=MediaClass.VIDEO, title=DEVICE_TITLE_FORMAT.format(device_name=device_info.device_name), can_play=False, can_expand=True, @@ -503,8 +496,8 @@ def _browse_clip_preview( return BrowseMediaSource( domain=DOMAIN, identifier=event_id.identifier, - media_class=MEDIA_CLASS_IMAGE, - media_content_type=MEDIA_TYPE_IMAGE, + media_class=MediaClass.IMAGE, + media_content_type=MediaType.IMAGE, title=CLIP_TITLE_FORMAT.format( event_name=", ".join(types), event_time=dt_util.as_local(event.timestamp).strftime(DATE_STR_FORMAT), @@ -525,8 +518,8 @@ def _browse_image_event( return BrowseMediaSource( domain=DOMAIN, identifier=event_id.identifier, - media_class=MEDIA_CLASS_IMAGE, - media_content_type=MEDIA_TYPE_IMAGE, + media_class=MediaClass.IMAGE, + media_content_type=MediaType.IMAGE, title=CLIP_TITLE_FORMAT.format( event_name=MEDIA_SOURCE_EVENT_TITLE_MAP.get(event.event_type, "Event"), event_time=dt_util.as_local(event.timestamp).strftime(DATE_STR_FORMAT), diff --git a/homeassistant/components/netatmo/media_source.py b/homeassistant/components/netatmo/media_source.py index f753a1163a5..948d162a613 100644 --- a/homeassistant/components/netatmo/media_source.py +++ b/homeassistant/components/netatmo/media_source.py @@ -5,12 +5,7 @@ import datetime as dt import logging import re -from homeassistant.components.media_player.const import ( - MEDIA_CLASS_DIRECTORY, - MEDIA_CLASS_VIDEO, - MEDIA_TYPE_VIDEO, -) -from homeassistant.components.media_player.errors import BrowseError +from homeassistant.components.media_player import BrowseError, MediaClass, MediaType from homeassistant.components.media_source.error import MediaSourceError, Unresolvable from homeassistant.components.media_source.models import ( BrowseMediaSource, @@ -102,13 +97,13 @@ class NetatmoSource(MediaSource): else: path = f"{source}/{camera_id}" - media_class = MEDIA_CLASS_DIRECTORY if event_id is None else MEDIA_CLASS_VIDEO + media_class = MediaClass.DIRECTORY if event_id is None else MediaClass.VIDEO media = BrowseMediaSource( domain=DOMAIN, identifier=path, media_class=media_class, - media_content_type=MEDIA_TYPE_VIDEO, + media_content_type=MediaType.VIDEO, title=title, can_play=bool( event_id and self.events[camera_id][event_id].get("media_url") diff --git a/homeassistant/components/plex/media_browser.py b/homeassistant/components/plex/media_browser.py index d65c01410c7..95ad3f39c70 100644 --- a/homeassistant/components/plex/media_browser.py +++ b/homeassistant/components/plex/media_browser.py @@ -3,20 +3,7 @@ from __future__ import annotations from yarl import URL -from homeassistant.components.media_player import BrowseMedia -from homeassistant.components.media_player.const import ( - MEDIA_CLASS_ALBUM, - MEDIA_CLASS_ARTIST, - MEDIA_CLASS_DIRECTORY, - MEDIA_CLASS_EPISODE, - MEDIA_CLASS_MOVIE, - MEDIA_CLASS_PLAYLIST, - MEDIA_CLASS_SEASON, - MEDIA_CLASS_TRACK, - MEDIA_CLASS_TV_SHOW, - MEDIA_CLASS_VIDEO, -) -from homeassistant.components.media_player.errors import BrowseError +from homeassistant.components.media_player import BrowseError, BrowseMedia, MediaClass from .const import DOMAIN, SERVERS from .errors import MediaNotFound @@ -29,18 +16,18 @@ class UnknownMediaType(BrowseError): EXPANDABLES = ["album", "artist", "playlist", "season", "show"] ITEM_TYPE_MEDIA_CLASS = { - "album": MEDIA_CLASS_ALBUM, - "artist": MEDIA_CLASS_ARTIST, - "clip": MEDIA_CLASS_VIDEO, - "episode": MEDIA_CLASS_EPISODE, - "mixed": MEDIA_CLASS_DIRECTORY, - "movie": MEDIA_CLASS_MOVIE, - "playlist": MEDIA_CLASS_PLAYLIST, - "season": MEDIA_CLASS_SEASON, - "show": MEDIA_CLASS_TV_SHOW, - "station": MEDIA_CLASS_ARTIST, - "track": MEDIA_CLASS_TRACK, - "video": MEDIA_CLASS_VIDEO, + "album": MediaClass.ALBUM, + "artist": MediaClass.ARTIST, + "clip": MediaClass.VIDEO, + "episode": MediaClass.EPISODE, + "mixed": MediaClass.DIRECTORY, + "movie": MediaClass.MOVIE, + "playlist": MediaClass.PLAYLIST, + "season": MediaClass.SEASON, + "show": MediaClass.TV_SHOW, + "station": MediaClass.ARTIST, + "track": MediaClass.TRACK, + "video": MediaClass.VIDEO, } @@ -99,13 +86,13 @@ def browse_media( # noqa: C901 """Create response payload to describe libraries of the Plex server.""" server_info = BrowseMedia( title=plex_server.friendly_name, - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_id=generate_plex_uri(server_id, "server"), media_content_type="server", can_play=False, can_expand=True, children=[], - children_media_class=MEDIA_CLASS_DIRECTORY, + children_media_class=MediaClass.DIRECTORY, thumbnail="https://brands.home-assistant.io/_/plex/logo.png", ) if platform != "sonos": @@ -136,7 +123,7 @@ def browse_media( # noqa: C901 """Create response payload for all available playlists.""" playlists_info = { "title": "Playlists", - "media_class": MEDIA_CLASS_DIRECTORY, + "media_class": MediaClass.DIRECTORY, "media_content_id": generate_plex_uri(server_id, "all"), "media_content_type": "playlists", "can_play": False, @@ -151,7 +138,7 @@ def browse_media( # noqa: C901 except UnknownMediaType: continue response = BrowseMedia(**playlists_info) - response.children_media_class = MEDIA_CLASS_PLAYLIST + response.children_media_class = MediaClass.PLAYLIST return response def build_item_response(payload): @@ -197,7 +184,7 @@ def browse_media( # noqa: C901 raise UnknownMediaType(f"Unknown type received: {hub.type}") from err payload = { "title": hub.title, - "media_class": MEDIA_CLASS_DIRECTORY, + "media_class": MediaClass.DIRECTORY, "media_content_id": generate_plex_uri(server_id, media_content_id), "media_content_type": "hub", "can_play": False, @@ -223,7 +210,7 @@ def browse_media( # noqa: C901 if special_folder: if media_content_type == "server": library_or_section = plex_server.library - children_media_class = MEDIA_CLASS_DIRECTORY + children_media_class = MediaClass.DIRECTORY title = plex_server.friendly_name elif media_content_type == "library": library_or_section = plex_server.library.sectionByID(int(media_content_id)) @@ -241,7 +228,7 @@ def browse_media( # noqa: C901 payload = { "title": title, - "media_class": MEDIA_CLASS_DIRECTORY, + "media_class": MediaClass.DIRECTORY, "media_content_id": generate_plex_uri( server_id, f"{media_content_id}/{special_folder}" ), @@ -323,7 +310,7 @@ def root_payload(hass, is_internal, platform=None): return BrowseMedia( title="Plex", - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_id="", media_content_type="plex_root", can_play=False, @@ -341,7 +328,7 @@ def library_section_payload(section): server_id = section._server.machineIdentifier # pylint: disable=protected-access return BrowseMedia( title=section.title, - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_id=generate_plex_uri(server_id, section.key), media_content_type="library", can_play=False, @@ -374,7 +361,7 @@ def hub_payload(hub): server_id = hub._server.machineIdentifier # pylint: disable=protected-access payload = { "title": hub.title, - "media_class": MEDIA_CLASS_DIRECTORY, + "media_class": MediaClass.DIRECTORY, "media_content_id": generate_plex_uri(server_id, media_content_id), "media_content_type": "hub", "can_play": False, diff --git a/homeassistant/components/plex/models.py b/homeassistant/components/plex/models.py index 48eee9d988d..d834012d287 100644 --- a/homeassistant/components/plex/models.py +++ b/homeassistant/components/plex/models.py @@ -1,12 +1,7 @@ """Models to represent various Plex objects used in the integration.""" import logging -from homeassistant.components.media_player.const import ( - MEDIA_TYPE_MOVIE, - MEDIA_TYPE_MUSIC, - MEDIA_TYPE_TVSHOW, - MEDIA_TYPE_VIDEO, -) +from homeassistant.components.media_player import MediaType from homeassistant.helpers.template import result_as_boolean from homeassistant.util import dt as dt_util @@ -92,19 +87,19 @@ class PlexSession: ) if media.type == "episode": - self.media_content_type = MEDIA_TYPE_TVSHOW + self.media_content_type = MediaType.TVSHOW self.media_season = media.seasonNumber self.media_series_title = media.grandparentTitle if media.index is not None: self.media_episode = media.index self.sensor_title = f"{self.media_series_title} - {media.seasonEpisode} - {self.media_title}" elif media.type == "movie": - self.media_content_type = MEDIA_TYPE_MOVIE + self.media_content_type = MediaType.MOVIE if media.year is not None and media.title is not None: self.media_title += f" ({media.year!s})" self.sensor_title = self.media_title elif media.type == "track": - self.media_content_type = MEDIA_TYPE_MUSIC + self.media_content_type = MediaType.MUSIC self.media_album_name = media.parentTitle self.media_album_artist = media.grandparentTitle self.media_track = media.index @@ -113,7 +108,7 @@ class PlexSession: f"{self.media_artist} - {self.media_album_name} - {self.media_title}" ) elif media.type == "clip": - self.media_content_type = MEDIA_TYPE_VIDEO + self.media_content_type = MediaType.VIDEO self.sensor_title = media.title else: self.sensor_title = "Unknown" diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index 058e8abbecd..8bcb0192cd2 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -12,8 +12,7 @@ import plexapi.server from requests import Session import requests.exceptions -from homeassistant.components.media_player import DOMAIN as MP_DOMAIN -from homeassistant.components.media_player.const import MEDIA_TYPE_PLAYLIST +from homeassistant.components.media_player import DOMAIN as MP_DOMAIN, MediaType from homeassistant.const import CONF_CLIENT_ID, CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL from homeassistant.core import callback from homeassistant.helpers.debounce import Debouncer @@ -627,7 +626,7 @@ class PlexServer: except NotFound as err: raise MediaNotFound(f"Media for key {key} not found") from err - if media_type == MEDIA_TYPE_PLAYLIST: + if media_type == MediaType.PLAYLIST: try: playlist_name = kwargs["playlist_name"] return self.playlist(playlist_name) diff --git a/homeassistant/components/ps4/__init__.py b/homeassistant/components/ps4/__init__.py index e480396e6a2..9a5744de2fd 100644 --- a/homeassistant/components/ps4/__init__.py +++ b/homeassistant/components/ps4/__init__.py @@ -7,10 +7,10 @@ from pyps4_2ndscreen.media_art import COUNTRIES import voluptuous as vol from homeassistant.components import persistent_notification -from homeassistant.components.media_player.const import ( +from homeassistant.components.media_player import ( ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_TITLE, - MEDIA_TYPE_GAME, + MediaType, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -206,7 +206,7 @@ def _reformat_data(hass: HomeAssistant, games: dict, unique_id: str) -> dict: ATTR_LOCKED: False, ATTR_MEDIA_TITLE: data, ATTR_MEDIA_IMAGE_URL: None, - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_GAME, + ATTR_MEDIA_CONTENT_TYPE: MediaType.GAME, } data_reformatted = True diff --git a/homeassistant/components/radio_browser/media_source.py b/homeassistant/components/radio_browser/media_source.py index 6ba1b7b2b9a..e49f670d371 100644 --- a/homeassistant/components/radio_browser/media_source.py +++ b/homeassistant/components/radio_browser/media_source.py @@ -5,13 +5,7 @@ import mimetypes from radios import FilterBy, Order, RadioBrowser, Station -from homeassistant.components.media_player.const import ( - MEDIA_CLASS_CHANNEL, - MEDIA_CLASS_DIRECTORY, - MEDIA_CLASS_MUSIC, - MEDIA_TYPE_MUSIC, -) -from homeassistant.components.media_player.errors import BrowseError +from homeassistant.components.media_player import BrowseError, MediaClass, MediaType from homeassistant.components.media_source.error import Unresolvable from homeassistant.components.media_source.models import ( BrowseMediaSource, @@ -88,12 +82,12 @@ class RadioMediaSource(MediaSource): return BrowseMediaSource( domain=DOMAIN, identifier=None, - media_class=MEDIA_CLASS_CHANNEL, - media_content_type=MEDIA_TYPE_MUSIC, + media_class=MediaClass.CHANNEL, + media_content_type=MediaType.MUSIC, title=self.entry.title, can_play=False, can_expand=True, - children_media_class=MEDIA_CLASS_DIRECTORY, + children_media_class=MediaClass.DIRECTORY, children=[ *await self._async_build_popular(radios, item), *await self._async_build_by_tag(radios, item), @@ -128,7 +122,7 @@ class RadioMediaSource(MediaSource): BrowseMediaSource( domain=DOMAIN, identifier=station.uuid, - media_class=MEDIA_CLASS_MUSIC, + media_class=MediaClass.MUSIC, media_content_type=mime_type, title=station.name, can_play=True, @@ -161,8 +155,8 @@ class RadioMediaSource(MediaSource): BrowseMediaSource( domain=DOMAIN, identifier=f"country/{country.code}", - media_class=MEDIA_CLASS_DIRECTORY, - media_content_type=MEDIA_TYPE_MUSIC, + media_class=MediaClass.DIRECTORY, + media_content_type=MediaType.MUSIC, title=country.name, can_play=False, can_expand=True, @@ -194,8 +188,8 @@ class RadioMediaSource(MediaSource): BrowseMediaSource( domain=DOMAIN, identifier=f"language/{language.code}", - media_class=MEDIA_CLASS_DIRECTORY, - media_content_type=MEDIA_TYPE_MUSIC, + media_class=MediaClass.DIRECTORY, + media_content_type=MediaType.MUSIC, title=language.name, can_play=False, can_expand=True, @@ -209,8 +203,8 @@ class RadioMediaSource(MediaSource): BrowseMediaSource( domain=DOMAIN, identifier="language", - media_class=MEDIA_CLASS_DIRECTORY, - media_content_type=MEDIA_TYPE_MUSIC, + media_class=MediaClass.DIRECTORY, + media_content_type=MediaType.MUSIC, title="By Language", can_play=False, can_expand=True, @@ -237,8 +231,8 @@ class RadioMediaSource(MediaSource): BrowseMediaSource( domain=DOMAIN, identifier="popular", - media_class=MEDIA_CLASS_DIRECTORY, - media_content_type=MEDIA_TYPE_MUSIC, + media_class=MediaClass.DIRECTORY, + media_content_type=MediaType.MUSIC, title="Popular", can_play=False, can_expand=True, @@ -277,8 +271,8 @@ class RadioMediaSource(MediaSource): BrowseMediaSource( domain=DOMAIN, identifier=f"tag/{tag.name}", - media_class=MEDIA_CLASS_DIRECTORY, - media_content_type=MEDIA_TYPE_MUSIC, + media_class=MediaClass.DIRECTORY, + media_content_type=MediaType.MUSIC, title=tag.name.title(), can_play=False, can_expand=True, @@ -291,8 +285,8 @@ class RadioMediaSource(MediaSource): BrowseMediaSource( domain=DOMAIN, identifier="tag", - media_class=MEDIA_CLASS_DIRECTORY, - media_content_type=MEDIA_TYPE_MUSIC, + media_class=MediaClass.DIRECTORY, + media_content_type=MediaType.MUSIC, title="By Category", can_play=False, can_expand=True, diff --git a/homeassistant/components/roku/browse_media.py b/homeassistant/components/roku/browse_media.py index 495ff910fd9..07e65ed3891 100644 --- a/homeassistant/components/roku/browse_media.py +++ b/homeassistant/components/roku/browse_media.py @@ -5,17 +5,12 @@ from collections.abc import Callable from functools import partial from homeassistant.components import media_source -from homeassistant.components.media_player import BrowseMedia -from homeassistant.components.media_player.const import ( - MEDIA_CLASS_APP, - MEDIA_CLASS_CHANNEL, - MEDIA_CLASS_DIRECTORY, - MEDIA_TYPE_APP, - MEDIA_TYPE_APPS, - MEDIA_TYPE_CHANNEL, - MEDIA_TYPE_CHANNELS, +from homeassistant.components.media_player import ( + BrowseError, + BrowseMedia, + MediaClass, + MediaType, ) -from homeassistant.components.media_player.errors import BrowseError from homeassistant.core import HomeAssistant from homeassistant.helpers.network import is_internal_request @@ -23,25 +18,25 @@ from .coordinator import RokuDataUpdateCoordinator from .helpers import format_channel_name CONTENT_TYPE_MEDIA_CLASS = { - MEDIA_TYPE_APP: MEDIA_CLASS_APP, - MEDIA_TYPE_APPS: MEDIA_CLASS_APP, - MEDIA_TYPE_CHANNEL: MEDIA_CLASS_CHANNEL, - MEDIA_TYPE_CHANNELS: MEDIA_CLASS_CHANNEL, + MediaType.APP: MediaClass.APP, + MediaType.APPS: MediaClass.APP, + MediaType.CHANNEL: MediaClass.CHANNEL, + MediaType.CHANNELS: MediaClass.CHANNEL, } CONTAINER_TYPES_SPECIFIC_MEDIA_CLASS = { - MEDIA_TYPE_APPS: MEDIA_CLASS_DIRECTORY, - MEDIA_TYPE_CHANNELS: MEDIA_CLASS_DIRECTORY, + MediaType.APPS: MediaClass.DIRECTORY, + MediaType.CHANNELS: MediaClass.DIRECTORY, } PLAYABLE_MEDIA_TYPES = [ - MEDIA_TYPE_APP, - MEDIA_TYPE_CHANNEL, + MediaType.APP, + MediaType.CHANNEL, ] EXPANDABLE_MEDIA_TYPES = [ - MEDIA_TYPE_APPS, - MEDIA_TYPE_CHANNELS, + MediaType.APPS, + MediaType.CHANNELS, ] GetBrowseImageUrlType = Callable[[str, str, "str | None"], str] @@ -57,7 +52,7 @@ def get_thumbnail_url_full( ) -> str | None: """Get thumbnail URL.""" if is_internal: - if media_content_type == MEDIA_TYPE_APP and media_content_id: + if media_content_type == MediaType.APP and media_content_id: return coordinator.roku.app_icon_url(media_content_id) return None @@ -119,7 +114,7 @@ async def root_payload( children = [ item_payload( - {"title": "Apps", "type": MEDIA_TYPE_APPS}, + {"title": "Apps", "type": MediaType.APPS}, coordinator, get_browse_image_url, ) @@ -128,7 +123,7 @@ async def root_payload( if device.info.device_type == "tv" and len(device.channels) > 0: children.append( item_payload( - {"title": "TV Channels", "type": MEDIA_TYPE_CHANNELS}, + {"title": "TV Channels", "type": MediaType.CHANNELS}, coordinator, get_browse_image_url, ) @@ -160,7 +155,7 @@ async def root_payload( return BrowseMedia( title="Roku", - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_id="", media_content_type="root", can_play=False, @@ -183,31 +178,31 @@ def build_item_response( media = None children_media_class = None - if search_type == MEDIA_TYPE_APPS: + if search_type == MediaType.APPS: title = "Apps" media = [ - {"app_id": item.app_id, "title": item.name, "type": MEDIA_TYPE_APP} + {"app_id": item.app_id, "title": item.name, "type": MediaType.APP} for item in coordinator.data.apps ] - children_media_class = MEDIA_CLASS_APP - elif search_type == MEDIA_TYPE_CHANNELS: + children_media_class = MediaClass.APP + elif search_type == MediaType.CHANNELS: title = "TV Channels" media = [ { "channel_number": channel.number, "title": format_channel_name(channel.number, channel.name), - "type": MEDIA_TYPE_CHANNEL, + "type": MediaType.CHANNEL, } for channel in coordinator.data.channels ] - children_media_class = MEDIA_CLASS_CHANNEL + children_media_class = MediaClass.CHANNEL if title is None or media is None: return None return BrowseMedia( media_class=CONTAINER_TYPES_SPECIFIC_MEDIA_CLASS.get( - search_type, MEDIA_CLASS_DIRECTORY + search_type, MediaClass.DIRECTORY ), media_content_id=search_id, media_content_type=search_type, @@ -235,11 +230,11 @@ def item_payload( thumbnail = None if "app_id" in item: - media_content_type = MEDIA_TYPE_APP + media_content_type = MediaType.APP media_content_id = item["app_id"] thumbnail = get_browse_image_url(media_content_type, media_content_id, None) elif "channel_number" in item: - media_content_type = MEDIA_TYPE_CHANNEL + media_content_type = MediaType.CHANNEL media_content_id = item["channel_number"] else: media_content_type = item["type"] diff --git a/homeassistant/components/roon/media_browser.py b/homeassistant/components/roon/media_browser.py index 2f132ee9d23..dd7a2a1faa3 100644 --- a/homeassistant/components/roon/media_browser.py +++ b/homeassistant/components/roon/media_browser.py @@ -1,12 +1,7 @@ """Support to interface with the Roon API.""" import logging -from homeassistant.components.media_player import BrowseMedia -from homeassistant.components.media_player.const import ( - MEDIA_CLASS_DIRECTORY, - MEDIA_CLASS_PLAYLIST, - MEDIA_CLASS_TRACK, -) +from homeassistant.components.media_player import BrowseMedia, MediaClass from homeassistant.components.media_player.errors import BrowseError @@ -71,18 +66,18 @@ def item_payload(roon_server, item, list_image_id): hint = item.get("hint") if hint == "list": - media_class = MEDIA_CLASS_DIRECTORY + media_class = MediaClass.DIRECTORY can_expand = True elif hint == "action_list": - media_class = MEDIA_CLASS_PLAYLIST + media_class = MediaClass.PLAYLIST can_expand = False elif hint == "action": media_content_type = "track" - media_class = MEDIA_CLASS_TRACK + media_class = MediaClass.TRACK can_expand = False else: # Roon API says to treat unknown as a list - media_class = MEDIA_CLASS_DIRECTORY + media_class = MediaClass.DIRECTORY can_expand = True _LOGGER.warning("Unknown hint %s - %s", title, hint) @@ -135,7 +130,7 @@ def library_payload(roon_server, zone_id, media_content_id): title=list_title, media_content_id=content_id, media_content_type="library", - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, can_play=False, can_expand=True, children=[], diff --git a/homeassistant/components/sonos/const.py b/homeassistant/components/sonos/const.py index 4c10bb113c7..9476b361ae7 100644 --- a/homeassistant/components/sonos/const.py +++ b/homeassistant/components/sonos/const.py @@ -1,22 +1,9 @@ """Const for Sonos.""" +from __future__ import annotations + import datetime -from homeassistant.components.media_player.const import ( - MEDIA_CLASS_ALBUM, - MEDIA_CLASS_ARTIST, - MEDIA_CLASS_COMPOSER, - MEDIA_CLASS_CONTRIBUTING_ARTIST, - MEDIA_CLASS_GENRE, - MEDIA_CLASS_PLAYLIST, - MEDIA_CLASS_TRACK, - MEDIA_TYPE_ALBUM, - MEDIA_TYPE_ARTIST, - MEDIA_TYPE_COMPOSER, - MEDIA_TYPE_CONTRIBUTING_ARTIST, - MEDIA_TYPE_GENRE, - MEDIA_TYPE_PLAYLIST, - MEDIA_TYPE_TRACK, -) +from homeassistant.components.media_player import MediaClass, MediaType from homeassistant.const import Platform UPNP_ST = "urn:schemas-upnp-org:device:ZonePlayer:1" @@ -46,11 +33,11 @@ SONOS_STATE_PLAYING = "PLAYING" SONOS_STATE_TRANSITIONING = "TRANSITIONING" EXPANDABLE_MEDIA_TYPES = [ - MEDIA_TYPE_ALBUM, - MEDIA_TYPE_ARTIST, - MEDIA_TYPE_COMPOSER, - MEDIA_TYPE_GENRE, - MEDIA_TYPE_PLAYLIST, + MediaType.ALBUM, + MediaType.ARTIST, + MediaType.COMPOSER, + MediaType.GENRE, + MediaType.PLAYLIST, SONOS_ALBUM, SONOS_ALBUM_ARTIST, SONOS_ARTIST, @@ -60,49 +47,49 @@ EXPANDABLE_MEDIA_TYPES = [ ] SONOS_TO_MEDIA_CLASSES = { - SONOS_ALBUM: MEDIA_CLASS_ALBUM, - SONOS_ALBUM_ARTIST: MEDIA_CLASS_ARTIST, - SONOS_ARTIST: MEDIA_CLASS_CONTRIBUTING_ARTIST, - SONOS_COMPOSER: MEDIA_CLASS_COMPOSER, - SONOS_GENRE: MEDIA_CLASS_GENRE, - SONOS_PLAYLISTS: MEDIA_CLASS_PLAYLIST, - SONOS_TRACKS: MEDIA_CLASS_TRACK, - "object.container.album.musicAlbum": MEDIA_CLASS_ALBUM, - "object.container.genre.musicGenre": MEDIA_CLASS_PLAYLIST, - "object.container.person.composer": MEDIA_CLASS_PLAYLIST, - "object.container.person.musicArtist": MEDIA_CLASS_ARTIST, - "object.container.playlistContainer.sameArtist": MEDIA_CLASS_ARTIST, - "object.container.playlistContainer": MEDIA_CLASS_PLAYLIST, - "object.item": MEDIA_CLASS_TRACK, - "object.item.audioItem.musicTrack": MEDIA_CLASS_TRACK, - "object.item.audioItem.audioBroadcast": MEDIA_CLASS_GENRE, + SONOS_ALBUM: MediaClass.ALBUM, + SONOS_ALBUM_ARTIST: MediaClass.ARTIST, + SONOS_ARTIST: MediaClass.CONTRIBUTING_ARTIST, + SONOS_COMPOSER: MediaClass.COMPOSER, + SONOS_GENRE: MediaClass.GENRE, + SONOS_PLAYLISTS: MediaClass.PLAYLIST, + SONOS_TRACKS: MediaClass.TRACK, + "object.container.album.musicAlbum": MediaClass.ALBUM, + "object.container.genre.musicGenre": MediaClass.PLAYLIST, + "object.container.person.composer": MediaClass.PLAYLIST, + "object.container.person.musicArtist": MediaClass.ARTIST, + "object.container.playlistContainer.sameArtist": MediaClass.ARTIST, + "object.container.playlistContainer": MediaClass.PLAYLIST, + "object.item": MediaClass.TRACK, + "object.item.audioItem.musicTrack": MediaClass.TRACK, + "object.item.audioItem.audioBroadcast": MediaClass.GENRE, } SONOS_TO_MEDIA_TYPES = { - SONOS_ALBUM: MEDIA_TYPE_ALBUM, - SONOS_ALBUM_ARTIST: MEDIA_TYPE_ARTIST, - SONOS_ARTIST: MEDIA_TYPE_CONTRIBUTING_ARTIST, - SONOS_COMPOSER: MEDIA_TYPE_COMPOSER, - SONOS_GENRE: MEDIA_TYPE_GENRE, - SONOS_PLAYLISTS: MEDIA_TYPE_PLAYLIST, - SONOS_TRACKS: MEDIA_TYPE_TRACK, - "object.container.album.musicAlbum": MEDIA_TYPE_ALBUM, - "object.container.genre.musicGenre": MEDIA_TYPE_PLAYLIST, - "object.container.person.composer": MEDIA_TYPE_PLAYLIST, - "object.container.person.musicArtist": MEDIA_TYPE_ARTIST, - "object.container.playlistContainer.sameArtist": MEDIA_TYPE_ARTIST, - "object.container.playlistContainer": MEDIA_TYPE_PLAYLIST, - "object.item.audioItem.musicTrack": MEDIA_TYPE_TRACK, + SONOS_ALBUM: MediaType.ALBUM, + SONOS_ALBUM_ARTIST: MediaType.ARTIST, + SONOS_ARTIST: MediaType.CONTRIBUTING_ARTIST, + SONOS_COMPOSER: MediaType.COMPOSER, + SONOS_GENRE: MediaType.GENRE, + SONOS_PLAYLISTS: MediaType.PLAYLIST, + SONOS_TRACKS: MediaType.TRACK, + "object.container.album.musicAlbum": MediaType.ALBUM, + "object.container.genre.musicGenre": MediaType.PLAYLIST, + "object.container.person.composer": MediaType.PLAYLIST, + "object.container.person.musicArtist": MediaType.ARTIST, + "object.container.playlistContainer.sameArtist": MediaType.ARTIST, + "object.container.playlistContainer": MediaType.PLAYLIST, + "object.item.audioItem.musicTrack": MediaType.TRACK, } -MEDIA_TYPES_TO_SONOS = { - MEDIA_TYPE_ALBUM: SONOS_ALBUM, - MEDIA_TYPE_ARTIST: SONOS_ALBUM_ARTIST, - MEDIA_TYPE_CONTRIBUTING_ARTIST: SONOS_ARTIST, - MEDIA_TYPE_COMPOSER: SONOS_COMPOSER, - MEDIA_TYPE_GENRE: SONOS_GENRE, - MEDIA_TYPE_PLAYLIST: SONOS_PLAYLISTS, - MEDIA_TYPE_TRACK: SONOS_TRACKS, +MEDIA_TYPES_TO_SONOS: dict[MediaType | str, str] = { + MediaType.ALBUM: SONOS_ALBUM, + MediaType.ARTIST: SONOS_ALBUM_ARTIST, + MediaType.CONTRIBUTING_ARTIST: SONOS_ARTIST, + MediaType.COMPOSER: SONOS_COMPOSER, + MediaType.GENRE: SONOS_GENRE, + MediaType.PLAYLIST: SONOS_PLAYLISTS, + MediaType.TRACK: SONOS_TRACKS, } SONOS_TYPES_MAPPING = { @@ -135,13 +122,13 @@ LIBRARY_TITLES_MAPPING = { } PLAYABLE_MEDIA_TYPES = [ - MEDIA_TYPE_ALBUM, - MEDIA_TYPE_ARTIST, - MEDIA_TYPE_COMPOSER, - MEDIA_TYPE_CONTRIBUTING_ARTIST, - MEDIA_TYPE_GENRE, - MEDIA_TYPE_PLAYLIST, - MEDIA_TYPE_TRACK, + MediaType.ALBUM, + MediaType.ARTIST, + MediaType.COMPOSER, + MediaType.CONTRIBUTING_ARTIST, + MediaType.GENRE, + MediaType.PLAYLIST, + MediaType.TRACK, ] SONOS_CHECK_ACTIVITY = "sonos_check_activity" diff --git a/homeassistant/components/sonos/media_browser.py b/homeassistant/components/sonos/media_browser.py index 3859f691179..187c0dbef55 100644 --- a/homeassistant/components/sonos/media_browser.py +++ b/homeassistant/components/sonos/media_browser.py @@ -12,12 +12,7 @@ from soco.ms_data_structures import MusicServiceItem from soco.music_library import MusicLibrary from homeassistant.components import media_source, plex, spotify -from homeassistant.components.media_player import BrowseMedia -from homeassistant.components.media_player.const import ( - MEDIA_CLASS_APP, - MEDIA_CLASS_DIRECTORY, - MEDIA_TYPE_ALBUM, -) +from homeassistant.components.media_player import BrowseMedia, MediaClass, MediaType from homeassistant.components.media_player.errors import BrowseError from homeassistant.core import HomeAssistant from homeassistant.helpers.network import is_internal_request @@ -159,7 +154,7 @@ def build_item_response( media_library: MusicLibrary, payload: dict[str, str], get_thumbnail_url=None ) -> BrowseMedia | None: """Create response payload for the provided media query.""" - if payload["search_type"] == MEDIA_TYPE_ALBUM and payload["idstring"].startswith( + if payload["search_type"] == MediaType.ALBUM and payload["idstring"].startswith( ("A:GENRE", "A:COMPOSER") ): payload["idstring"] = "A:ALBUMARTIST/" + "/".join( @@ -191,7 +186,7 @@ def build_item_response( # Fetch album info for titles and thumbnails # Can't be extracted from track info if ( - payload["search_type"] == MEDIA_TYPE_ALBUM + payload["search_type"] == MediaType.ALBUM and media[0].item_class == "object.item.audioItem.musicTrack" ): item = get_media(media_library, payload["idstring"], SONOS_ALBUM_ARTIST) @@ -271,7 +266,7 @@ async def root_payload( children.append( BrowseMedia( title="Favorites", - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_id="", media_content_type="favorites", thumbnail="https://brands.home-assistant.io/_/sonos/logo.png", @@ -286,7 +281,7 @@ async def root_payload( children.append( BrowseMedia( title="Music Library", - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_id="", media_content_type="library", thumbnail="https://brands.home-assistant.io/_/sonos/logo.png", @@ -299,7 +294,7 @@ async def root_payload( children.append( BrowseMedia( title="Plex", - media_class=MEDIA_CLASS_APP, + media_class=MediaClass.APP, media_content_id="", media_content_type="plex", thumbnail="https://brands.home-assistant.io/_/plex/logo.png", @@ -337,7 +332,7 @@ async def root_payload( return BrowseMedia( title="Sonos", - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_id="", media_content_type="root", can_play=False, @@ -359,7 +354,7 @@ def library_payload(media_library: MusicLibrary, get_thumbnail_url=None) -> Brow return BrowseMedia( title="Music Library", - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_id="library", media_content_type="library", can_play=False, @@ -397,7 +392,7 @@ def favorites_payload(favorites: list[DidlFavorite]) -> BrowseMedia: return BrowseMedia( title="Favorites", - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_id="", media_content_type="favorites", can_play=False, @@ -433,7 +428,7 @@ def favorites_folder_payload( return BrowseMedia( title=content_type.title(), - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_id="", media_content_type="favorites", can_play=False, diff --git a/homeassistant/components/spotify/browse_media.py b/homeassistant/components/spotify/browse_media.py index db2379a57fc..12268c8ab56 100644 --- a/homeassistant/components/spotify/browse_media.py +++ b/homeassistant/components/spotify/browse_media.py @@ -9,22 +9,11 @@ from spotipy import Spotify import yarl from homeassistant.backports.enum import StrEnum -from homeassistant.components.media_player import BrowseError, BrowseMedia -from homeassistant.components.media_player.const import ( - MEDIA_CLASS_ALBUM, - MEDIA_CLASS_APP, - MEDIA_CLASS_ARTIST, - MEDIA_CLASS_DIRECTORY, - MEDIA_CLASS_EPISODE, - MEDIA_CLASS_GENRE, - MEDIA_CLASS_PLAYLIST, - MEDIA_CLASS_PODCAST, - MEDIA_CLASS_TRACK, - MEDIA_TYPE_ALBUM, - MEDIA_TYPE_ARTIST, - MEDIA_TYPE_EPISODE, - MEDIA_TYPE_PLAYLIST, - MEDIA_TYPE_TRACK, +from homeassistant.components.media_player import ( + BrowseError, + BrowseMedia, + MediaClass, + MediaType, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session @@ -70,62 +59,62 @@ LIBRARY_MAP = { CONTENT_TYPE_MEDIA_CLASS: dict[str, Any] = { BrowsableMedia.CURRENT_USER_PLAYLISTS.value: { - "parent": MEDIA_CLASS_DIRECTORY, - "children": MEDIA_CLASS_PLAYLIST, + "parent": MediaClass.DIRECTORY, + "children": MediaClass.PLAYLIST, }, BrowsableMedia.CURRENT_USER_FOLLOWED_ARTISTS.value: { - "parent": MEDIA_CLASS_DIRECTORY, - "children": MEDIA_CLASS_ARTIST, + "parent": MediaClass.DIRECTORY, + "children": MediaClass.ARTIST, }, BrowsableMedia.CURRENT_USER_SAVED_ALBUMS.value: { - "parent": MEDIA_CLASS_DIRECTORY, - "children": MEDIA_CLASS_ALBUM, + "parent": MediaClass.DIRECTORY, + "children": MediaClass.ALBUM, }, BrowsableMedia.CURRENT_USER_SAVED_TRACKS.value: { - "parent": MEDIA_CLASS_DIRECTORY, - "children": MEDIA_CLASS_TRACK, + "parent": MediaClass.DIRECTORY, + "children": MediaClass.TRACK, }, BrowsableMedia.CURRENT_USER_SAVED_SHOWS.value: { - "parent": MEDIA_CLASS_DIRECTORY, - "children": MEDIA_CLASS_PODCAST, + "parent": MediaClass.DIRECTORY, + "children": MediaClass.PODCAST, }, BrowsableMedia.CURRENT_USER_RECENTLY_PLAYED.value: { - "parent": MEDIA_CLASS_DIRECTORY, - "children": MEDIA_CLASS_TRACK, + "parent": MediaClass.DIRECTORY, + "children": MediaClass.TRACK, }, BrowsableMedia.CURRENT_USER_TOP_ARTISTS.value: { - "parent": MEDIA_CLASS_DIRECTORY, - "children": MEDIA_CLASS_ARTIST, + "parent": MediaClass.DIRECTORY, + "children": MediaClass.ARTIST, }, BrowsableMedia.CURRENT_USER_TOP_TRACKS.value: { - "parent": MEDIA_CLASS_DIRECTORY, - "children": MEDIA_CLASS_TRACK, + "parent": MediaClass.DIRECTORY, + "children": MediaClass.TRACK, }, BrowsableMedia.FEATURED_PLAYLISTS.value: { - "parent": MEDIA_CLASS_DIRECTORY, - "children": MEDIA_CLASS_PLAYLIST, + "parent": MediaClass.DIRECTORY, + "children": MediaClass.PLAYLIST, }, BrowsableMedia.CATEGORIES.value: { - "parent": MEDIA_CLASS_DIRECTORY, - "children": MEDIA_CLASS_GENRE, + "parent": MediaClass.DIRECTORY, + "children": MediaClass.GENRE, }, "category_playlists": { - "parent": MEDIA_CLASS_DIRECTORY, - "children": MEDIA_CLASS_PLAYLIST, + "parent": MediaClass.DIRECTORY, + "children": MediaClass.PLAYLIST, }, BrowsableMedia.NEW_RELEASES.value: { - "parent": MEDIA_CLASS_DIRECTORY, - "children": MEDIA_CLASS_ALBUM, + "parent": MediaClass.DIRECTORY, + "children": MediaClass.ALBUM, }, - MEDIA_TYPE_PLAYLIST: { - "parent": MEDIA_CLASS_PLAYLIST, - "children": MEDIA_CLASS_TRACK, + MediaType.PLAYLIST: { + "parent": MediaClass.PLAYLIST, + "children": MediaClass.TRACK, }, - MEDIA_TYPE_ALBUM: {"parent": MEDIA_CLASS_ALBUM, "children": MEDIA_CLASS_TRACK}, - MEDIA_TYPE_ARTIST: {"parent": MEDIA_CLASS_ARTIST, "children": MEDIA_CLASS_ALBUM}, - MEDIA_TYPE_EPISODE: {"parent": MEDIA_CLASS_EPISODE, "children": None}, - MEDIA_TYPE_SHOW: {"parent": MEDIA_CLASS_PODCAST, "children": MEDIA_CLASS_EPISODE}, - MEDIA_TYPE_TRACK: {"parent": MEDIA_CLASS_TRACK, "children": None}, + MediaType.ALBUM: {"parent": MediaClass.ALBUM, "children": MediaClass.TRACK}, + MediaType.ARTIST: {"parent": MediaClass.ARTIST, "children": MediaClass.ALBUM}, + MediaType.EPISODE: {"parent": MediaClass.EPISODE, "children": None}, + MEDIA_TYPE_SHOW: {"parent": MediaClass.PODCAST, "children": MediaClass.EPISODE}, + MediaType.TRACK: {"parent": MediaClass.TRACK, "children": None}, } @@ -157,7 +146,7 @@ async def async_browse_media( children.append( BrowseMedia( title=config_entry.title, - media_class=MEDIA_CLASS_APP, + media_class=MediaClass.APP, media_content_id=f"{MEDIA_PLAYER_PREFIX}{config_entry_id}", media_content_type=f"{MEDIA_PLAYER_PREFIX}library", thumbnail="https://brands.home-assistant.io/_/spotify/logo.png", @@ -167,7 +156,7 @@ async def async_browse_media( ) return BrowseMedia( title="Spotify", - media_class=MEDIA_CLASS_APP, + media_class=MediaClass.APP, media_content_id=MEDIA_PLAYER_PREFIX, media_content_type="spotify", thumbnail="https://brands.home-assistant.io/_/spotify/logo.png", @@ -312,13 +301,13 @@ def build_item_response( # noqa: C901 elif media_content_type == BrowsableMedia.NEW_RELEASES: if media := spotify.new_releases(country=user["country"], limit=BROWSE_LIMIT): items = media.get("albums", {}).get("items", []) - elif media_content_type == MEDIA_TYPE_PLAYLIST: + elif media_content_type == MediaType.PLAYLIST: if media := spotify.playlist(media_content_id): items = [item["track"] for item in media.get("tracks", {}).get("items", [])] - elif media_content_type == MEDIA_TYPE_ALBUM: + elif media_content_type == MediaType.ALBUM: if media := spotify.album(media_content_id): items = media.get("tracks", {}).get("items", []) - elif media_content_type == MEDIA_TYPE_ARTIST: + elif media_content_type == MediaType.ARTIST: if (media := spotify.artist_albums(media_content_id, limit=BROWSE_LIMIT)) and ( artist := spotify.artist(media_content_id) ): @@ -364,8 +353,8 @@ def build_item_response( # noqa: C901 BrowseMedia( can_expand=True, can_play=False, - children_media_class=MEDIA_CLASS_TRACK, - media_class=MEDIA_CLASS_PLAYLIST, + children_media_class=MediaClass.TRACK, + media_class=MediaClass.PLAYLIST, media_content_id=item_id, media_content_type=f"{MEDIA_PLAYER_PREFIX}category_playlists", thumbnail=fetch_image_url(item, key="icons"), @@ -380,7 +369,7 @@ def build_item_response( # noqa: C901 title = media["name"] can_play = media_content_type in PLAYABLE_MEDIA_TYPES and ( - media_content_type != MEDIA_TYPE_ARTIST or can_play_artist + media_content_type != MediaType.ARTIST or can_play_artist ) browse_media = BrowseMedia( @@ -429,12 +418,12 @@ def item_payload(item: dict[str, Any], *, can_play_artist: bool) -> BrowseMedia: raise UnknownMediaType from err can_expand = media_type not in [ - MEDIA_TYPE_TRACK, - MEDIA_TYPE_EPISODE, + MediaType.TRACK, + MediaType.EPISODE, ] can_play = media_type in PLAYABLE_MEDIA_TYPES and ( - media_type != MEDIA_TYPE_ARTIST or can_play_artist + media_type != MediaType.ARTIST or can_play_artist ) browse_media = BrowseMedia( @@ -449,8 +438,8 @@ def item_payload(item: dict[str, Any], *, can_play_artist: bool) -> BrowseMedia: if "images" in item: browse_media.thumbnail = fetch_image_url(item) - elif MEDIA_TYPE_ALBUM in item: - browse_media.thumbnail = fetch_image_url(item[MEDIA_TYPE_ALBUM]) + elif MediaType.ALBUM in item: + browse_media.thumbnail = fetch_image_url(item[MediaType.ALBUM]) return browse_media @@ -464,8 +453,8 @@ def library_payload(*, can_play_artist: bool) -> BrowseMedia: browse_media = BrowseMedia( can_expand=True, can_play=False, - children_media_class=MEDIA_CLASS_DIRECTORY, - media_class=MEDIA_CLASS_DIRECTORY, + children_media_class=MediaClass.DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_id="library", media_content_type=f"{MEDIA_PLAYER_PREFIX}library", title="Media Library", diff --git a/homeassistant/components/spotify/const.py b/homeassistant/components/spotify/const.py index ad73262921b..a89d35b7edf 100644 --- a/homeassistant/components/spotify/const.py +++ b/homeassistant/components/spotify/const.py @@ -2,13 +2,7 @@ import logging -from homeassistant.components.media_player.const import ( - MEDIA_TYPE_ALBUM, - MEDIA_TYPE_ARTIST, - MEDIA_TYPE_EPISODE, - MEDIA_TYPE_PLAYLIST, - MEDIA_TYPE_TRACK, -) +from homeassistant.components.media_player import MediaType DOMAIN = "spotify" @@ -35,10 +29,10 @@ MEDIA_PLAYER_PREFIX = "spotify://" MEDIA_TYPE_SHOW = "show" PLAYABLE_MEDIA_TYPES = [ - MEDIA_TYPE_PLAYLIST, - MEDIA_TYPE_ALBUM, - MEDIA_TYPE_ARTIST, - MEDIA_TYPE_EPISODE, + MediaType.PLAYLIST, + MediaType.ALBUM, + MediaType.ARTIST, + MediaType.EPISODE, MEDIA_TYPE_SHOW, - MEDIA_TYPE_TRACK, + MediaType.TRACK, ] diff --git a/homeassistant/components/squeezebox/browse_media.py b/homeassistant/components/squeezebox/browse_media.py index 76627119b85..979b4c36a98 100644 --- a/homeassistant/components/squeezebox/browse_media.py +++ b/homeassistant/components/squeezebox/browse_media.py @@ -2,19 +2,11 @@ import contextlib from homeassistant.components import media_source -from homeassistant.components.media_player import BrowseError, BrowseMedia -from homeassistant.components.media_player.const import ( - MEDIA_CLASS_ALBUM, - MEDIA_CLASS_ARTIST, - MEDIA_CLASS_DIRECTORY, - MEDIA_CLASS_GENRE, - MEDIA_CLASS_PLAYLIST, - MEDIA_CLASS_TRACK, - MEDIA_TYPE_ALBUM, - MEDIA_TYPE_ARTIST, - MEDIA_TYPE_GENRE, - MEDIA_TYPE_PLAYLIST, - MEDIA_TYPE_TRACK, +from homeassistant.components.media_player import ( + BrowseError, + BrowseMedia, + MediaClass, + MediaType, ) from homeassistant.helpers.network import is_internal_request @@ -26,44 +18,44 @@ MEDIA_TYPE_TO_SQUEEZEBOX = { "Tracks": "titles", "Playlists": "playlists", "Genres": "genres", - MEDIA_TYPE_ALBUM: "album", - MEDIA_TYPE_ARTIST: "artist", - MEDIA_TYPE_TRACK: "title", - MEDIA_TYPE_PLAYLIST: "playlist", - MEDIA_TYPE_GENRE: "genre", + MediaType.ALBUM: "album", + MediaType.ARTIST: "artist", + MediaType.TRACK: "title", + MediaType.PLAYLIST: "playlist", + MediaType.GENRE: "genre", } SQUEEZEBOX_ID_BY_TYPE = { - MEDIA_TYPE_ALBUM: "album_id", - MEDIA_TYPE_ARTIST: "artist_id", - MEDIA_TYPE_TRACK: "track_id", - MEDIA_TYPE_PLAYLIST: "playlist_id", - MEDIA_TYPE_GENRE: "genre_id", + MediaType.ALBUM: "album_id", + MediaType.ARTIST: "artist_id", + MediaType.TRACK: "track_id", + MediaType.PLAYLIST: "playlist_id", + MediaType.GENRE: "genre_id", } CONTENT_TYPE_MEDIA_CLASS = { - "Artists": {"item": MEDIA_CLASS_DIRECTORY, "children": MEDIA_CLASS_ARTIST}, - "Albums": {"item": MEDIA_CLASS_DIRECTORY, "children": MEDIA_CLASS_ALBUM}, - "Tracks": {"item": MEDIA_CLASS_DIRECTORY, "children": MEDIA_CLASS_TRACK}, - "Playlists": {"item": MEDIA_CLASS_DIRECTORY, "children": MEDIA_CLASS_PLAYLIST}, - "Genres": {"item": MEDIA_CLASS_DIRECTORY, "children": MEDIA_CLASS_GENRE}, - MEDIA_TYPE_ALBUM: {"item": MEDIA_CLASS_ALBUM, "children": MEDIA_CLASS_TRACK}, - MEDIA_TYPE_ARTIST: {"item": MEDIA_CLASS_ARTIST, "children": MEDIA_CLASS_ALBUM}, - MEDIA_TYPE_TRACK: {"item": MEDIA_CLASS_TRACK, "children": None}, - MEDIA_TYPE_GENRE: {"item": MEDIA_CLASS_GENRE, "children": MEDIA_CLASS_ARTIST}, - MEDIA_TYPE_PLAYLIST: {"item": MEDIA_CLASS_PLAYLIST, "children": MEDIA_CLASS_TRACK}, + "Artists": {"item": MediaClass.DIRECTORY, "children": MediaClass.ARTIST}, + "Albums": {"item": MediaClass.DIRECTORY, "children": MediaClass.ALBUM}, + "Tracks": {"item": MediaClass.DIRECTORY, "children": MediaClass.TRACK}, + "Playlists": {"item": MediaClass.DIRECTORY, "children": MediaClass.PLAYLIST}, + "Genres": {"item": MediaClass.DIRECTORY, "children": MediaClass.GENRE}, + MediaType.ALBUM: {"item": MediaClass.ALBUM, "children": MediaClass.TRACK}, + MediaType.ARTIST: {"item": MediaClass.ARTIST, "children": MediaClass.ALBUM}, + MediaType.TRACK: {"item": MediaClass.TRACK, "children": None}, + MediaType.GENRE: {"item": MediaClass.GENRE, "children": MediaClass.ARTIST}, + MediaType.PLAYLIST: {"item": MediaClass.PLAYLIST, "children": MediaClass.TRACK}, } CONTENT_TYPE_TO_CHILD_TYPE = { - MEDIA_TYPE_ALBUM: MEDIA_TYPE_TRACK, - MEDIA_TYPE_PLAYLIST: MEDIA_TYPE_PLAYLIST, - MEDIA_TYPE_ARTIST: MEDIA_TYPE_ALBUM, - MEDIA_TYPE_GENRE: MEDIA_TYPE_ARTIST, - "Artists": MEDIA_TYPE_ARTIST, - "Albums": MEDIA_TYPE_ALBUM, - "Tracks": MEDIA_TYPE_TRACK, - "Playlists": MEDIA_TYPE_PLAYLIST, - "Genres": MEDIA_TYPE_GENRE, + MediaType.ALBUM: MediaType.TRACK, + MediaType.PLAYLIST: MediaType.PLAYLIST, + MediaType.ARTIST: MediaType.ALBUM, + MediaType.GENRE: MediaType.ARTIST, + "Artists": MediaType.ARTIST, + "Albums": MediaType.ALBUM, + "Tracks": MediaType.TRACK, + "Playlists": MediaType.PLAYLIST, + "Genres": MediaType.GENRE, } BROWSE_LIMIT = 1000 @@ -141,7 +133,7 @@ async def library_payload(hass, player): """Create response payload to describe contents of library.""" library_info = { "title": "Music Library", - "media_class": MEDIA_CLASS_DIRECTORY, + "media_class": MediaClass.DIRECTORY, "media_content_id": "library", "media_content_type": "library", "can_play": False, diff --git a/homeassistant/components/system_bridge/media_source.py b/homeassistant/components/system_bridge/media_source.py index dc6bc1bbbdc..ab0d4f28319 100644 --- a/homeassistant/components/system_bridge/media_source.py +++ b/homeassistant/components/system_bridge/media_source.py @@ -4,7 +4,7 @@ from __future__ import annotations from systembridgeconnector.models.media_directories import MediaDirectories from systembridgeconnector.models.media_files import File as MediaFile, MediaFiles -from homeassistant.components.media_player.const import MEDIA_CLASS_DIRECTORY +from homeassistant.components.media_player import MediaClass from homeassistant.components.media_source.const import ( MEDIA_CLASS_MAP, MEDIA_MIME_TYPES, @@ -97,26 +97,26 @@ class SystemBridgeSource(MediaSource): BrowseMediaSource( domain=DOMAIN, identifier=entry.entry_id, - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_type="", title=entry.title, can_play=False, can_expand=True, children=[], - children_media_class=MEDIA_CLASS_DIRECTORY, + children_media_class=MediaClass.DIRECTORY, ) ) return BrowseMediaSource( domain=DOMAIN, identifier="", - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_type="", title=self.name, can_play=False, can_expand=True, children=children, - children_media_class=MEDIA_CLASS_DIRECTORY, + children_media_class=MediaClass.DIRECTORY, ) @@ -138,7 +138,7 @@ def _build_root_paths( return BrowseMediaSource( domain=DOMAIN, identifier="", - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_type="", title=entry.title, can_play=False, @@ -147,17 +147,17 @@ def _build_root_paths( BrowseMediaSource( domain=DOMAIN, identifier=f"{entry.entry_id}~~{directory.key}", - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_type="", title=f"{directory.key[:1].capitalize()}{directory.key[1:]}", can_play=False, can_expand=True, children=[], - children_media_class=MEDIA_CLASS_DIRECTORY, + children_media_class=MediaClass.DIRECTORY, ) for directory in media_directories.directories ], - children_media_class=MEDIA_CLASS_DIRECTORY, + children_media_class=MediaClass.DIRECTORY, ) @@ -171,7 +171,7 @@ def _build_media_items( return BrowseMediaSource( domain=DOMAIN, identifier=identifier, - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_type="", title=f"{entry.title} - {path}", can_play=False, @@ -199,7 +199,7 @@ def _build_media_item( ext = f"~~{media_file.mime_type}" if media_file.is_directory or media_file.mime_type is None: - media_class = MEDIA_CLASS_DIRECTORY + media_class = MediaClass.DIRECTORY else: media_class = MEDIA_CLASS_MAP[media_file.mime_type.split("/", 1)[0]] diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index 706122c174c..8263408c4bb 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -20,13 +20,13 @@ import voluptuous as vol import yarl from homeassistant.components.http import HomeAssistantView -from homeassistant.components.media_player.const import ( +from homeassistant.components.media_player import ( ATTR_MEDIA_ANNOUNCE, ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, DOMAIN as DOMAIN_MP, - MEDIA_TYPE_MUSIC, SERVICE_PLAY_MEDIA, + MediaType, ) from homeassistant.components.media_source import generate_media_source_id from homeassistant.const import ( @@ -224,7 +224,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: DOMAIN, str(yarl.URL.build(path=p_type, query=params)), ), - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, + ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC, ATTR_MEDIA_ANNOUNCE: True, }, blocking=True, diff --git a/homeassistant/components/tts/media_source.py b/homeassistant/components/tts/media_source.py index 99ef3dd85a0..eda64c804b8 100644 --- a/homeassistant/components/tts/media_source.py +++ b/homeassistant/components/tts/media_source.py @@ -6,8 +6,7 @@ from typing import TYPE_CHECKING, Any from yarl import URL -from homeassistant.components.media_player.const import MEDIA_CLASS_APP -from homeassistant.components.media_player.errors import BrowseError +from homeassistant.components.media_player import BrowseError, MediaClass from homeassistant.components.media_source.error import Unresolvable from homeassistant.components.media_source.models import ( BrowseMediaSource, @@ -85,12 +84,12 @@ class TTSMediaSource(MediaSource): return BrowseMediaSource( domain=DOMAIN, identifier=None, - media_class=MEDIA_CLASS_APP, + media_class=MediaClass.APP, media_content_type="", title=self.name, can_play=False, can_expand=True, - children_media_class=MEDIA_CLASS_APP, + children_media_class=MediaClass.APP, children=children, ) @@ -111,7 +110,7 @@ class TTSMediaSource(MediaSource): return BrowseMediaSource( domain=DOMAIN, identifier=f"{provider_domain}{params}", - media_class=MEDIA_CLASS_APP, + media_class=MediaClass.APP, media_content_type="provider", title=provider.name, thumbnail=f"https://brands.home-assistant.io/_/{provider_domain}/logo.png", diff --git a/homeassistant/components/unifiprotect/media_source.py b/homeassistant/components/unifiprotect/media_source.py index 4910c18cf5f..6ebb36c11c5 100644 --- a/homeassistant/components/unifiprotect/media_source.py +++ b/homeassistant/components/unifiprotect/media_source.py @@ -19,12 +19,7 @@ from pyunifiprotect.utils import from_js_time from yarl import URL from homeassistant.components.camera import CameraImageView -from homeassistant.components.media_player.const import ( - MEDIA_CLASS_DIRECTORY, - MEDIA_CLASS_IMAGE, - MEDIA_CLASS_VIDEO, -) -from homeassistant.components.media_player.errors import BrowseError +from homeassistant.components.media_player import BrowseError, MediaClass from homeassistant.components.media_source.models import ( BrowseMediaSource, MediaSource, @@ -422,7 +417,7 @@ class ProtectMediaSource(MediaSource): return BrowseMediaSource( domain=DOMAIN, identifier=f"{nvr.id}:eventthumb:{event_id}", - media_class=MEDIA_CLASS_IMAGE, + media_class=MediaClass.IMAGE, media_content_type="image/jpeg", title=title, can_play=True, @@ -435,7 +430,7 @@ class ProtectMediaSource(MediaSource): return BrowseMediaSource( domain=DOMAIN, identifier=f"{nvr.id}:event:{event_id}", - media_class=MEDIA_CLASS_VIDEO, + media_class=MediaClass.VIDEO, media_content_type="video/mp4", title=title, can_play=True, @@ -506,12 +501,12 @@ class ProtectMediaSource(MediaSource): source = BrowseMediaSource( domain=DOMAIN, identifier=f"{base_id}:recent:{days}", - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_type="video/mp4", title=title, can_play=False, can_expand=True, - children_media_class=MEDIA_CLASS_VIDEO, + children_media_class=MediaClass.VIDEO, ) if not build_children: @@ -560,12 +555,12 @@ class ProtectMediaSource(MediaSource): source = BrowseMediaSource( domain=DOMAIN, identifier=f"{base_id}:range:{start.year}:{start.month}", - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_type=VIDEO_FORMAT, title=title, can_play=False, can_expand=True, - children_media_class=MEDIA_CLASS_VIDEO, + children_media_class=MediaClass.VIDEO, ) if not build_children: @@ -622,12 +617,12 @@ class ProtectMediaSource(MediaSource): source = BrowseMediaSource( domain=DOMAIN, identifier=identifier, - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_type=VIDEO_FORMAT, title=title, can_play=False, can_expand=True, - children_media_class=MEDIA_CLASS_VIDEO, + children_media_class=MediaClass.VIDEO, ) if not build_children: @@ -692,12 +687,12 @@ class ProtectMediaSource(MediaSource): source = BrowseMediaSource( domain=DOMAIN, identifier=base_id, - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_type=VIDEO_FORMAT, title=title, can_play=False, can_expand=True, - children_media_class=MEDIA_CLASS_VIDEO, + children_media_class=MediaClass.VIDEO, ) if not build_children or data.api.bootstrap.recording_start is None: @@ -781,13 +776,13 @@ class ProtectMediaSource(MediaSource): source = BrowseMediaSource( domain=DOMAIN, identifier=f"{data.api.bootstrap.nvr.id}:browse:{camera_id}", - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_type=VIDEO_FORMAT, title=name, can_play=False, can_expand=True, thumbnail=thumbnail_url, - children_media_class=MEDIA_CLASS_VIDEO, + children_media_class=MediaClass.VIDEO, ) if not build_children: @@ -837,12 +832,12 @@ class ProtectMediaSource(MediaSource): base = BrowseMediaSource( domain=DOMAIN, identifier=f"{data.api.bootstrap.nvr.id}:browse", - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_type=VIDEO_FORMAT, title=data.api.bootstrap.nvr.name, can_play=False, can_expand=True, - children_media_class=MEDIA_CLASS_VIDEO, + children_media_class=MediaClass.VIDEO, children=await self._build_cameras(data), ) @@ -864,11 +859,11 @@ class ProtectMediaSource(MediaSource): return BrowseMediaSource( domain=DOMAIN, identifier=None, - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_type=VIDEO_FORMAT, title=self.name, can_play=False, can_expand=True, - children_media_class=MEDIA_CLASS_VIDEO, + children_media_class=MediaClass.VIDEO, children=consoles, ) diff --git a/homeassistant/components/volumio/browse_media.py b/homeassistant/components/volumio/browse_media.py index ad634392989..019b4e33666 100644 --- a/homeassistant/components/volumio/browse_media.py +++ b/homeassistant/components/volumio/browse_media.py @@ -1,16 +1,11 @@ """Support for media browsing.""" import json -from homeassistant.components.media_player import BrowseError, BrowseMedia -from homeassistant.components.media_player.const import ( - MEDIA_CLASS_ALBUM, - MEDIA_CLASS_ARTIST, - MEDIA_CLASS_CHANNEL, - MEDIA_CLASS_DIRECTORY, - MEDIA_CLASS_GENRE, - MEDIA_CLASS_PLAYLIST, - MEDIA_CLASS_TRACK, - MEDIA_TYPE_MUSIC, +from homeassistant.components.media_player import ( + BrowseError, + BrowseMedia, + MediaClass, + MediaType, ) PLAYABLE_ITEM_TYPES = [ @@ -48,52 +43,52 @@ FAVOURITES_URI = "favourites" def _item_to_children_media_class(item, info=None): if info and "album" in info and "artist" in info: - return MEDIA_CLASS_TRACK + return MediaClass.TRACK if item["uri"].startswith(PLAYLISTS_URI_PREFIX): - return MEDIA_CLASS_PLAYLIST + return MediaClass.PLAYLIST if item["uri"].startswith(ARTISTS_URI_PREFIX): if len(item["uri"]) > len(ARTISTS_URI_PREFIX): - return MEDIA_CLASS_ALBUM - return MEDIA_CLASS_ARTIST + return MediaClass.ALBUM + return MediaClass.ARTIST if item["uri"].startswith(ALBUMS_URI_PREFIX): if len(item["uri"]) > len(ALBUMS_URI_PREFIX): - return MEDIA_CLASS_TRACK - return MEDIA_CLASS_ALBUM + return MediaClass.TRACK + return MediaClass.ALBUM if item["uri"].startswith(GENRES_URI_PREFIX): if len(item["uri"]) > len(GENRES_URI_PREFIX): - return MEDIA_CLASS_ALBUM - return MEDIA_CLASS_GENRE + return MediaClass.ALBUM + return MediaClass.GENRE if item["uri"].startswith(LAST_100_URI_PREFIX) or item["uri"] == FAVOURITES_URI: - return MEDIA_CLASS_TRACK + return MediaClass.TRACK if item["uri"].startswith(RADIO_URI_PREFIX): - return MEDIA_CLASS_CHANNEL - return MEDIA_CLASS_DIRECTORY + return MediaClass.CHANNEL + return MediaClass.DIRECTORY def _item_to_media_class(item, parent_item=None): if "type" not in item: - return MEDIA_CLASS_DIRECTORY + return MediaClass.DIRECTORY if item["type"] in ("webradio", "mywebradio"): - return MEDIA_CLASS_CHANNEL + return MediaClass.CHANNEL if item["type"] in ("song", "cuesong"): - return MEDIA_CLASS_TRACK + return MediaClass.TRACK if item.get("artist"): - return MEDIA_CLASS_ALBUM + return MediaClass.ALBUM if item["uri"].startswith(ARTISTS_URI_PREFIX) and len(item["uri"]) > len( ARTISTS_URI_PREFIX ): - return MEDIA_CLASS_ARTIST + return MediaClass.ARTIST if parent_item: return _item_to_children_media_class(parent_item) - return MEDIA_CLASS_DIRECTORY + return MediaClass.DIRECTORY def _list_payload(item, children=None): return BrowseMedia( title=item["name"], - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, children_media_class=_item_to_children_media_class(item), - media_content_type=MEDIA_TYPE_MUSIC, + media_content_type=MediaType.MUSIC, media_content_id=json.dumps(item), can_play=False, can_expand=True, @@ -105,7 +100,7 @@ def _raw_item_payload(entity, item, parent_item=None, title=None, info=None): if thumbnail := item.get("albumart"): item_hash = str(hash(thumbnail)) entity.thumbnail_cache.setdefault(item_hash, thumbnail) - thumbnail = entity.get_browse_image_url(MEDIA_TYPE_MUSIC, item_hash) + thumbnail = entity.get_browse_image_url(MediaType.MUSIC, item_hash) else: # don't use the built-in volumio white-on-white icons thumbnail = None @@ -114,7 +109,7 @@ def _raw_item_payload(entity, item, parent_item=None, title=None, info=None): "title": title or item.get("title"), "media_class": _item_to_media_class(item, parent_item), "children_media_class": _item_to_children_media_class(item, info), - "media_content_type": MEDIA_TYPE_MUSIC, + "media_content_type": MediaType.MUSIC, "media_content_id": json.dumps(item), "can_play": item.get("type") in PLAYABLE_ITEM_TYPES, "can_expand": item.get("type") not in NON_EXPANDABLE_ITEM_TYPES, @@ -131,7 +126,7 @@ async def browse_top_level(media_library): navigation = await media_library.browse() children = [_list_payload(item) for item in navigation["lists"]] return BrowseMedia( - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_id="library", media_content_type="library", title="Media Library", diff --git a/homeassistant/components/xbox/browse_media.py b/homeassistant/components/xbox/browse_media.py index e4c268bd6b8..138b0bd612c 100644 --- a/homeassistant/components/xbox/browse_media.py +++ b/homeassistant/components/xbox/browse_media.py @@ -16,14 +16,7 @@ from xbox.webapi.api.provider.smartglass.models import ( InstalledPackagesList, ) -from homeassistant.components.media_player import BrowseMedia -from homeassistant.components.media_player.const import ( - MEDIA_CLASS_APP, - MEDIA_CLASS_DIRECTORY, - MEDIA_CLASS_GAME, - MEDIA_TYPE_APP, - MEDIA_TYPE_GAME, -) +from homeassistant.components.media_player import BrowseMedia, MediaClass, MediaType class MediaTypeDetails(NamedTuple): @@ -35,12 +28,12 @@ class MediaTypeDetails(NamedTuple): TYPE_MAP = { "App": MediaTypeDetails( - type=MEDIA_TYPE_APP, - cls=MEDIA_CLASS_APP, + type=MediaType.APP, + cls=MediaClass.APP, ), "Game": MediaTypeDetails( - type=MEDIA_TYPE_GAME, - cls=MEDIA_CLASS_GAME, + type=MediaType.GAME, + cls=MediaClass.GAME, ), } @@ -58,7 +51,7 @@ async def build_item_response( if media_content_type in (None, "library"): children: list[BrowseMedia] = [] library_info = BrowseMedia( - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_id="library", media_content_type="library", title="Installed Applications", @@ -79,9 +72,9 @@ async def build_item_response( ) children.append( BrowseMedia( - media_class=MEDIA_CLASS_APP, + media_class=MediaClass.APP, media_content_id="Home", - media_content_type=MEDIA_TYPE_APP, + media_content_type=MediaType.APP, title="Home", can_play=True, can_expand=False, @@ -102,9 +95,9 @@ async def build_item_response( ) children.append( BrowseMedia( - media_class=MEDIA_CLASS_APP, + media_class=MediaClass.APP, media_content_id="TV", - media_content_type=MEDIA_TYPE_APP, + media_content_type=MediaType.APP, title="Live TV", can_play=True, can_expand=False, @@ -118,7 +111,7 @@ async def build_item_response( for c_type in content_types: children.append( BrowseMedia( - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_id=c_type, media_content_type=TYPE_MAP[c_type].type, title=f"{c_type}s", @@ -145,7 +138,7 @@ async def build_item_response( } return BrowseMedia( - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_id=media_content_id, media_content_type=media_content_type, title=f"{media_content_id}s", diff --git a/homeassistant/components/xbox/media_source.py b/homeassistant/components/xbox/media_source.py index 21b9b25ce2d..6a2def82a96 100644 --- a/homeassistant/components/xbox/media_source.py +++ b/homeassistant/components/xbox/media_source.py @@ -11,12 +11,7 @@ from xbox.webapi.api.provider.gameclips.models import GameclipsResponse from xbox.webapi.api.provider.screenshots.models import ScreenshotResponse from xbox.webapi.api.provider.smartglass.models import InstalledPackage -from homeassistant.components.media_player.const import ( - MEDIA_CLASS_DIRECTORY, - MEDIA_CLASS_GAME, - MEDIA_CLASS_IMAGE, - MEDIA_CLASS_VIDEO, -) +from homeassistant.components.media_player import MediaClass from homeassistant.components.media_source.models import ( BrowseMediaSource, MediaSource, @@ -35,8 +30,8 @@ MIME_TYPE_MAP = { } MEDIA_CLASS_MAP = { - "gameclips": MEDIA_CLASS_VIDEO, - "screenshots": MEDIA_CLASS_IMAGE, + "gameclips": MediaClass.VIDEO, + "screenshots": MediaClass.IMAGE, } @@ -120,13 +115,13 @@ class XboxSource(MediaSource): return BrowseMediaSource( domain=DOMAIN, identifier="", - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_type="", title="Xbox Game Media", can_play=False, can_expand=True, children=[_build_game_item(game, images) for game in games.values()], - children_media_class=MEDIA_CLASS_GAME, + children_media_class=MediaClass.GAME, ) async def _build_media_items(self, title, category): @@ -157,7 +152,7 @@ class XboxSource(MediaSource): ).strftime("%b. %d, %Y %I:%M %p"), item.thumbnails[0].uri, item.game_clip_uris[0].uri, - MEDIA_CLASS_VIDEO, + MediaClass.VIDEO, ) for item in response.game_clips ] @@ -182,7 +177,7 @@ class XboxSource(MediaSource): ), item.thumbnails[0].uri, item.screenshot_uris[0].uri, - MEDIA_CLASS_IMAGE, + MediaClass.IMAGE, ) for item in response.screenshots ] @@ -190,7 +185,7 @@ class XboxSource(MediaSource): return BrowseMediaSource( domain=DOMAIN, identifier=f"{title}~~{category}", - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_type="", title=f"{owner.title()} {kind.title()}", can_play=False, @@ -213,12 +208,12 @@ def _build_game_item(item: InstalledPackage, images: dict[str, list[Image]]): return BrowseMediaSource( domain=DOMAIN, identifier=f"{item.title_id}#{item.name}#{thumbnail}", - media_class=MEDIA_CLASS_GAME, + media_class=MediaClass.GAME, media_content_type="", title=item.name, can_play=False, can_expand=True, - children_media_class=MEDIA_CLASS_DIRECTORY, + children_media_class=MediaClass.DIRECTORY, thumbnail=thumbnail, ) @@ -229,13 +224,13 @@ def _build_categories(title): base = BrowseMediaSource( domain=DOMAIN, identifier=f"{title}", - media_class=MEDIA_CLASS_GAME, + media_class=MediaClass.GAME, media_content_type="", title=name, can_play=False, can_expand=True, children=[], - children_media_class=MEDIA_CLASS_DIRECTORY, + children_media_class=MediaClass.DIRECTORY, thumbnail=thumbnail, ) @@ -247,7 +242,7 @@ def _build_categories(title): BrowseMediaSource( domain=DOMAIN, identifier=f"{title}~~{owner}#{kind}", - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_type="", title=f"{owner.title()} {kind.title()}", can_play=False, From 3be9bee61e826338cf9da5e6954c2c6196b7be29 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 12 Sep 2022 14:18:26 -0500 Subject: [PATCH 308/955] Bump pySwitchbot to 0.19.6 (#78304) No longer swallows exceptions from bleak connection errors which was hiding the root cause of problems. This was the original behavior from a long time ago which does not make sense anymore since we retry a few times anyways https://github.com/Danielhiversen/pySwitchbot/compare/0.19.5...0.19.6 --- 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 e1e832f854a..b2a5b68deae 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.19.5"], + "requirements": ["PySwitchbot==0.19.6"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 104d0f70854..f56119f9845 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.19.5 +PySwitchbot==0.19.6 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d7c89984ffb..9f2925e8637 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.19.5 +PySwitchbot==0.19.6 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From 19bee11a018a26117f567bec76f7af0f075d7eb3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 12 Sep 2022 21:34:35 +0200 Subject: [PATCH 309/955] Improve sun typing (#78298) --- homeassistant/components/sun/__init__.py | 69 +++++++++++++----------- homeassistant/components/sun/trigger.py | 4 +- 2 files changed, 40 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/sun/__init__.py b/homeassistant/components/sun/__init__.py index a30b18befd5..256df2f8971 100644 --- a/homeassistant/components/sun/__init__.py +++ b/homeassistant/components/sun/__init__.py @@ -1,6 +1,11 @@ """Support for functionality to keep track of the sun.""" -from datetime import timedelta +from __future__ import annotations + +from datetime import datetime, timedelta import logging +from typing import Any + +from astral.location import Elevation, Location from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( @@ -9,7 +14,7 @@ from homeassistant.const import ( SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback from homeassistant.helpers import event from homeassistant.helpers.entity import Entity from homeassistant.helpers.integration_platform import ( @@ -24,8 +29,6 @@ from homeassistant.util import dt as dt_util from .const import DOMAIN -# mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs - _LOGGER = logging.getLogger(__name__) ENTITY_ID = "sun.sun" @@ -114,32 +117,40 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class Sun(Entity): """Representation of the Sun.""" + _attr_name = "Sun" entity_id = ENTITY_ID - def __init__(self, hass): + location: Location + elevation: Elevation + next_rising: datetime + next_setting: datetime + next_dawn: datetime + next_dusk: datetime + next_midnight: datetime + next_noon: datetime + solar_elevation: float + solar_azimuth: float + rising: bool + _next_change: datetime + + def __init__(self, hass: HomeAssistant) -> None: """Initialize the sun.""" self.hass = hass - self.location = None - self.elevation = 0.0 - self._state = self.next_rising = self.next_setting = None - self.next_dawn = self.next_dusk = None - self.next_midnight = self.next_noon = None - self.solar_elevation = self.solar_azimuth = None - self.rising = self.phase = None - self._next_change = None - self._config_listener = None - self._update_events_listener = None - self._update_sun_position_listener = None + self.phase: str | None = None + + self._config_listener: CALLBACK_TYPE | None = None + self._update_events_listener: CALLBACK_TYPE | None = None + self._update_sun_position_listener: CALLBACK_TYPE | None = None self._config_listener = self.hass.bus.async_listen( EVENT_CORE_CONFIG_UPDATE, self.update_location ) - self.update_location() + self.update_location(initial=True) @callback - def update_location(self, *_): + def update_location(self, _: Event | None = None, initial: bool = False) -> None: """Update location.""" location, elevation = get_astral_location(self.hass) - if location == self.location: + if not initial and location == self.location: return self.location = location self.elevation = elevation @@ -148,7 +159,7 @@ class Sun(Entity): self.update_events() @callback - def remove_listeners(self): + def remove_listeners(self) -> None: """Remove listeners.""" if self._config_listener: self._config_listener() @@ -158,12 +169,7 @@ class Sun(Entity): self._update_sun_position_listener() @property - def name(self): - """Return the name.""" - return "Sun" - - @property - def state(self): + def state(self) -> str: """Return the state of the sun.""" # 0.8333 is the same value as astral uses if self.solar_elevation > -0.833: @@ -172,7 +178,7 @@ class Sun(Entity): return STATE_BELOW_HORIZON @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes of the sun.""" return { STATE_ATTR_NEXT_DAWN: self.next_dawn.isoformat(), @@ -186,7 +192,9 @@ class Sun(Entity): STATE_ATTR_RISING: self.rising, } - def _check_event(self, utc_point_in_time, sun_event, before): + def _check_event( + self, utc_point_in_time: datetime, sun_event: str, before: str | None + ) -> datetime: next_utc = get_location_astral_event_next( self.location, self.elevation, sun_event, utc_point_in_time ) @@ -196,7 +204,7 @@ class Sun(Entity): return next_utc @callback - def update_events(self, now=None): + def update_events(self, now: datetime | None = None) -> None: """Update the attributes containing solar events.""" # Grab current time in case system clock changed since last time we ran. utc_point_in_time = dt_util.utcnow() @@ -266,7 +274,7 @@ class Sun(Entity): _LOGGER.debug("next time: %s", self._next_change.isoformat()) @callback - def update_sun_position(self, now=None): + def update_sun_position(self, now: datetime | None = None) -> None: """Calculate the position of the sun.""" # Grab current time in case system clock changed since last time we ran. utc_point_in_time = dt_util.utcnow() @@ -286,6 +294,7 @@ class Sun(Entity): self.async_write_ha_state() # Next update as per the current phase + assert self.phase delta = _PHASE_UPDATES[self.phase] # if the next update is within 1.25 of the next # position update just drop it diff --git a/homeassistant/components/sun/trigger.py b/homeassistant/components/sun/trigger.py index 86af51f0283..3cc3cabfbd3 100644 --- a/homeassistant/components/sun/trigger.py +++ b/homeassistant/components/sun/trigger.py @@ -15,8 +15,6 @@ from homeassistant.helpers.event import async_track_sunrise, async_track_sunset from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType -# mypy: allow-untyped-defs, no-check-untyped-defs - TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( { vol.Required(CONF_PLATFORM): "sun", @@ -42,7 +40,7 @@ async def async_attach_trigger( job = HassJob(action) @callback - def call_action(): + def call_action() -> None: """Call action with right context.""" hass.async_run_hass_job( job, From 9d47160e6823c6c357780a45ebc857adf4e145fd Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 12 Sep 2022 15:37:11 -0400 Subject: [PATCH 310/955] Fix sengled bulbs in ZHA (#78315) * Fix sengled bulbs in ZHA * fix tests * update discovery data --- homeassistant/components/zha/light.py | 2 +- tests/components/zha/test_light.py | 2 +- tests/components/zha/zha_devices_list.py | 18 +++++++++--------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 528833608b3..2953ab3b99a 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -78,7 +78,7 @@ PARALLEL_UPDATES = 0 SIGNAL_LIGHT_GROUP_STATE_CHANGED = "zha_light_group_state_changed" SIGNAL_LIGHT_GROUP_TRANSITION_START = "zha_light_group_transition_start" SIGNAL_LIGHT_GROUP_TRANSITION_FINISHED = "zha_light_group_transition_finished" -DEFAULT_MIN_TRANSITION_MANUFACTURERS = {"Sengled"} +DEFAULT_MIN_TRANSITION_MANUFACTURERS = {"sengled"} COLOR_MODES_GROUP_LIGHT = {ColorMode.COLOR_TEMP, ColorMode.XY} SUPPORT_GROUP_LIGHT = ( diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index 018ac68a25f..f3779c4841e 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -176,7 +176,7 @@ async def device_light_2(hass, zigpy_device_mock, zha_device_joined): } }, ieee=IEEE_GROUPABLE_DEVICE2, - manufacturer="Sengled", + manufacturer="sengled", nwk=0xC79E, ) color_cluster = zigpy_device.endpoints[1].light_color diff --git a/tests/components/zha/zha_devices_list.py b/tests/components/zha/zha_devices_list.py index 4cea8eb8f66..2d15f9335db 100644 --- a/tests/components/zha/zha_devices_list.py +++ b/tests/components/zha/zha_devices_list.py @@ -5493,7 +5493,7 @@ DEVICES = [ DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ "button.sengled_e11_g13_identifybutton", - "light.sengled_e11_g13_light", + "light.sengled_e11_g13_mintransitionlight", "sensor.sengled_e11_g13_smartenergymetering", "sensor.sengled_e11_g13_smartenergysummation", "sensor.sengled_e11_g13_rssi", @@ -5502,8 +5502,8 @@ DEVICES = [ DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off", "level"], - DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.sengled_e11_g13_light", + DEV_SIG_ENT_MAP_CLASS: "MinTransitionLight", + DEV_SIG_ENT_MAP_ID: "light.sengled_e11_g13_mintransitionlight", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], @@ -5549,7 +5549,7 @@ DEVICES = [ DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ "button.sengled_e12_n14_identifybutton", - "light.sengled_e12_n14_light", + "light.sengled_e12_n14_mintransitionlight", "sensor.sengled_e12_n14_smartenergymetering", "sensor.sengled_e12_n14_smartenergysummation", "sensor.sengled_e12_n14_rssi", @@ -5558,8 +5558,8 @@ DEVICES = [ DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off", "level"], - DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.sengled_e12_n14_light", + DEV_SIG_ENT_MAP_CLASS: "MinTransitionLight", + DEV_SIG_ENT_MAP_ID: "light.sengled_e12_n14_mintransitionlight", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], @@ -5605,7 +5605,7 @@ DEVICES = [ DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ "button.sengled_z01_a19nae26_identifybutton", - "light.sengled_z01_a19nae26_light", + "light.sengled_z01_a19nae26_mintransitionlight", "sensor.sengled_z01_a19nae26_smartenergymetering", "sensor.sengled_z01_a19nae26_smartenergysummation", "sensor.sengled_z01_a19nae26_rssi", @@ -5614,8 +5614,8 @@ DEVICES = [ DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off", "level", "light_color"], - DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.sengled_z01_a19nae26_light", + DEV_SIG_ENT_MAP_CLASS: "MinTransitionLight", + DEV_SIG_ENT_MAP_ID: "light.sengled_z01_a19nae26_mintransitionlight", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], From 55e59b778cd8783aab66467c0a76dc54462474ba Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 12 Sep 2022 23:29:55 +0200 Subject: [PATCH 311/955] Add type hints to TTS provider (#78285) Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> --- homeassistant/components/tts/__init__.py | 32 +++++++++++++---------- homeassistant/components/tts/notify.py | 18 ++++++++----- pylint/plugins/hass_enforce_type_hints.py | 29 ++++++++++++++++++++ 3 files changed, 59 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index 8263408c4bb..7def2c84bc0 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -11,7 +11,7 @@ import mimetypes import os from pathlib import Path import re -from typing import TYPE_CHECKING, Optional, cast +from typing import TYPE_CHECKING, Any, Optional, cast from aiohttp import web import mutagen @@ -49,8 +49,6 @@ from homeassistant.util.yaml import load_yaml from .const import DOMAIN -# mypy: allow-untyped-defs, no-check-untyped-defs - _LOGGER = logging.getLogger(__name__) TtsAudioType = tuple[Optional[str], Optional[bytes]] @@ -86,7 +84,7 @@ _RE_VOICE_FILE = re.compile(r"([a-f0-9]{40})_([^_]+)_([^_]+)_([a-z_]+)\.[a-z0-9] KEY_PATTERN = "{0}_{1}_{2}_{3}" -def _deprecated_platform(value): +def _deprecated_platform(value: str) -> str: """Validate if platform is deprecated.""" if value == "google": raise vol.Invalid( @@ -253,7 +251,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if setup_tasks: await asyncio.wait(setup_tasks) - async def async_platform_discovered(platform, info): + async def async_platform_discovered( + platform: str, info: dict[str, Any] | None + ) -> None: """Handle for discovered platform.""" await async_setup_platform(platform, discovery_info=info) @@ -327,7 +327,7 @@ class SpeechManager: """Read file cache and delete files.""" self.mem_cache = {} - def remove_files(): + def remove_files() -> None: """Remove files from filesystem.""" for filename in self.file_cache.values(): try: @@ -365,7 +365,11 @@ class SpeechManager: # Languages language = language or provider.default_language - if language is None or language not in provider.supported_languages: + if ( + language is None + or provider.supported_languages is None + or language not in provider.supported_languages + ): raise HomeAssistantError(f"Not supported language {language}") # Options @@ -583,33 +587,33 @@ class Provider: name: str | None = None @property - def default_language(self): + def default_language(self) -> str | None: """Return the default language.""" return None @property - def supported_languages(self): + def supported_languages(self) -> list[str] | None: """Return a list of supported languages.""" return None @property - def supported_options(self): - """Return a list of supported options like voice, emotionen.""" + def supported_options(self) -> list[str] | None: + """Return a list of supported options like voice, emotions.""" return None @property - def default_options(self): + def default_options(self) -> dict[str, Any] | None: """Return a dict include default options.""" return None def get_tts_audio( - self, message: str, language: str, options: dict | None = None + self, message: str, language: str, options: dict[str, Any] | None = None ) -> TtsAudioType: """Load tts audio file from provider.""" raise NotImplementedError() async def async_get_tts_audio( - self, message: str, language: str, options: dict | None = None + self, message: str, language: str, options: dict[str, Any] | None = None ) -> TtsAudioType: """Load tts audio file from provider. diff --git a/homeassistant/components/tts/notify.py b/homeassistant/components/tts/notify.py index c53788f89b8..041638f830f 100644 --- a/homeassistant/components/tts/notify.py +++ b/homeassistant/components/tts/notify.py @@ -1,20 +1,22 @@ """Support notifications through TTS service.""" +from __future__ import annotations + import logging +from typing import Any import voluptuous as vol from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService from homeassistant.const import ATTR_ENTITY_ID, CONF_NAME -from homeassistant.core import split_entity_id +from homeassistant.core import HomeAssistant, split_entity_id import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import ATTR_LANGUAGE, ATTR_MESSAGE, DOMAIN CONF_MEDIA_PLAYER = "media_player" CONF_TTS_SERVICE = "tts_service" -# mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs - _LOGGER = logging.getLogger(__name__) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -27,7 +29,11 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -async def async_get_service(hass, config, discovery_info=None): +async def async_get_service( + hass: HomeAssistant, + config: ConfigType, + discovery_info: DiscoveryInfoType | None = None, +) -> TTSNotificationService: """Return the notify service.""" return TTSNotificationService(config) @@ -36,13 +42,13 @@ async def async_get_service(hass, config, discovery_info=None): class TTSNotificationService(BaseNotificationService): """The TTS Notification Service.""" - def __init__(self, config): + def __init__(self, config: ConfigType) -> None: """Initialize the service.""" _, self._tts_service = split_entity_id(config[CONF_TTS_SERVICE]) self._media_player = config[CONF_MEDIA_PLAYER] self._language = config.get(ATTR_LANGUAGE) - async def async_send_message(self, message="", **kwargs): + async def async_send_message(self, message: str = "", **kwargs: Any) -> None: """Call TTS service to speak the notification.""" _LOGGER.debug("%s '%s' on %s", self._tts_service, message, self._media_player) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 2b99dde8c0d..f7c4d66f2e2 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -2127,6 +2127,35 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ], ), ], + "tts": [ + ClassTypeHintMatch( + base_class="Provider", + matches=[ + TypeHintMatch( + function_name="default_language", + return_type=["str", None], + ), + TypeHintMatch( + function_name="supported_languages", + return_type=["list[str]", None], + ), + TypeHintMatch( + function_name="supported_options", + return_type=["list[str]", None], + ), + TypeHintMatch( + function_name="default_options", + return_type=["dict[str, Any]", None], + ), + TypeHintMatch( + function_name="get_tts_audio", + arg_types={1: "str", 2: "str", 3: "dict[str, Any] | None"}, + return_type="TtsAudioType", + has_async_counterpart=True, + ), + ], + ), + ], "update": [ ClassTypeHintMatch( base_class="Entity", From 9cb8d910cebbf2952d632635699dac162b25e565 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 12 Sep 2022 23:32:07 +0200 Subject: [PATCH 312/955] Improve media-player typing (#78300) Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> --- .../components/media_player/__init__.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 177d7f5ad74..37cc7fa230c 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -135,8 +135,6 @@ from .const import ( # noqa: F401 ) from .errors import BrowseError -# mypy: allow-untyped-defs, no-check-untyped-defs - _LOGGER = logging.getLogger(__name__) ENTITY_ID_FORMAT = DOMAIN + ".{}" @@ -221,7 +219,7 @@ ATTR_TO_PROPERTY = [ @bind_hass -def is_on(hass, entity_id=None): +def is_on(hass: HomeAssistant, entity_id: str | None = None) -> bool: """ Return true if specified media player entity_id is on. @@ -372,7 +370,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ) # Remove in Home Assistant 2022.9 - def _rewrite_enqueue(value): + def _rewrite_enqueue(value: dict[str, Any]) -> dict[str, Any]: """Rewrite the enqueue value.""" if ATTR_MEDIA_ENQUEUE not in value: pass @@ -1186,7 +1184,11 @@ class MediaPlayerImageView(HomeAssistantView): } ) @websocket_api.async_response -async def websocket_browse_media(hass, connection, msg): +async def websocket_browse_media( + hass: HomeAssistant, + connection: websocket_api.connection.ActiveConnection, + msg: dict[str, Any], +) -> None: """ Browse media available to the media_player entity. @@ -1213,6 +1215,7 @@ async def websocket_browse_media(hass, connection, msg): try: payload = await player.async_browse_media(media_content_type, media_content_id) except NotImplementedError: + assert player.platform _LOGGER.error( "%s allows media browsing but its integration (%s) does not", player.entity_id, @@ -1234,11 +1237,12 @@ async def websocket_browse_media(hass, connection, msg): # For backwards compat if isinstance(payload, BrowseMedia): - payload = payload.as_dict() + result = payload.as_dict() else: + result = payload # type: ignore[unreachable] _LOGGER.warning("Browse Media should use new BrowseMedia class") - connection.send_result(msg["id"], payload) + connection.send_result(msg["id"], result) async def async_fetch_image( From dbc6dda41e769d1cca153ccf881c7536a29f695d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 12 Sep 2022 23:33:21 +0200 Subject: [PATCH 313/955] Adjust components to use relative imports (#78279) --- .../components/insteon/api/__init__.py | 2 +- .../atlantic_electrical_heater.py | 3 ++- .../atlantic_electrical_towel_dryer.py | 5 +++-- .../atlantic_pass_apc_zone_control.py | 3 ++- .../recorder/system_health/__init__.py | 4 ++-- .../components/zwave_js/triggers/event.py | 16 +++++++------- .../components/zwave_js/triggers/helpers.py | 3 ++- .../zwave_js/triggers/value_updated.py | 21 ++++++++----------- 8 files changed, 29 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/insteon/api/__init__.py b/homeassistant/components/insteon/api/__init__.py index 71dd1a0463e..e56d4dab07e 100644 --- a/homeassistant/components/insteon/api/__init__.py +++ b/homeassistant/components/insteon/api/__init__.py @@ -3,9 +3,9 @@ from insteon_frontend import get_build_id, locate_dir from homeassistant.components import panel_custom, websocket_api -from homeassistant.components.insteon.const import CONF_DEV_PATH, DOMAIN from homeassistant.core import HomeAssistant, callback +from ..const import CONF_DEV_PATH, DOMAIN from .aldb import ( websocket_add_default_links, websocket_change_aldb_record, diff --git a/homeassistant/components/overkiz/climate_entities/atlantic_electrical_heater.py b/homeassistant/components/overkiz/climate_entities/atlantic_electrical_heater.py index 9195a729ff8..c0d1dd04663 100644 --- a/homeassistant/components/overkiz/climate_entities/atlantic_electrical_heater.py +++ b/homeassistant/components/overkiz/climate_entities/atlantic_electrical_heater.py @@ -13,9 +13,10 @@ from homeassistant.components.climate import ( ClimateEntityFeature, HVACMode, ) -from homeassistant.components.overkiz.entity import OverkizEntity from homeassistant.const import TEMP_CELSIUS +from ..entity import OverkizEntity + PRESET_FROST_PROTECTION = "frost_protection" OVERKIZ_TO_HVAC_MODES: dict[str, HVACMode] = { diff --git a/homeassistant/components/overkiz/climate_entities/atlantic_electrical_towel_dryer.py b/homeassistant/components/overkiz/climate_entities/atlantic_electrical_towel_dryer.py index 0e13beae097..7ab59a47a34 100644 --- a/homeassistant/components/overkiz/climate_entities/atlantic_electrical_towel_dryer.py +++ b/homeassistant/components/overkiz/climate_entities/atlantic_electrical_towel_dryer.py @@ -12,10 +12,11 @@ from homeassistant.components.climate import ( ClimateEntityFeature, HVACMode, ) -from homeassistant.components.overkiz.coordinator import OverkizDataUpdateCoordinator -from homeassistant.components.overkiz.entity import OverkizEntity from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS +from ..coordinator import OverkizDataUpdateCoordinator +from ..entity import OverkizEntity + PRESET_DRYING = "drying" OVERKIZ_TO_HVAC_MODE: dict[str, str] = { diff --git a/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_zone_control.py b/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_zone_control.py index bdb204d4ba7..ba95785fbc7 100644 --- a/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_zone_control.py +++ b/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_zone_control.py @@ -4,9 +4,10 @@ from typing import cast from pyoverkiz.enums import OverkizCommand, OverkizCommandParam, OverkizState from homeassistant.components.climate import ClimateEntity, HVACMode -from homeassistant.components.overkiz.entity import OverkizEntity from homeassistant.const import TEMP_CELSIUS +from ..entity import OverkizEntity + OVERKIZ_TO_HVAC_MODE: dict[str, str] = { OverkizCommandParam.HEATING: HVACMode.HEAT, OverkizCommandParam.DRYING: HVACMode.DRY, diff --git a/homeassistant/components/recorder/system_health/__init__.py b/homeassistant/components/recorder/system_health/__init__.py index c4bf2c3bb89..b79f526db2b 100644 --- a/homeassistant/components/recorder/system_health/__init__.py +++ b/homeassistant/components/recorder/system_health/__init__.py @@ -5,12 +5,12 @@ from typing import Any from urllib.parse import urlparse from homeassistant.components import system_health -from homeassistant.components.recorder.core import Recorder -from homeassistant.components.recorder.util import session_scope from homeassistant.core import HomeAssistant, callback from .. import get_instance from ..const import SupportedDialect +from ..core import Recorder +from ..util import session_scope from .mysql import db_size_bytes as mysql_db_size_bytes from .postgresql import db_size_bytes as postgresql_db_size_bytes from .sqlite import db_size_bytes as sqlite_db_size_bytes diff --git a/homeassistant/components/zwave_js/triggers/event.py b/homeassistant/components/zwave_js/triggers/event.py index eecd685cc1b..a6d6c967504 100644 --- a/homeassistant/components/zwave_js/triggers/event.py +++ b/homeassistant/components/zwave_js/triggers/event.py @@ -10,7 +10,13 @@ from zwave_js_server.model.controller import CONTROLLER_EVENT_MODEL_MAP from zwave_js_server.model.driver import DRIVER_EVENT_MODEL_MAP from zwave_js_server.model.node import NODE_EVENT_MODEL_MAP -from homeassistant.components.zwave_js.const import ( +from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID, CONF_PLATFORM +from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback +from homeassistant.helpers import config_validation as cv, device_registry as dr +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo +from homeassistant.helpers.typing import ConfigType + +from ..const import ( ATTR_CONFIG_ENTRY_ID, ATTR_EVENT, ATTR_EVENT_DATA, @@ -20,17 +26,11 @@ from homeassistant.components.zwave_js.const import ( DATA_CLIENT, DOMAIN, ) -from homeassistant.components.zwave_js.helpers import ( +from ..helpers import ( async_get_nodes_from_targets, get_device_id, get_home_and_node_id_from_device_entry, ) -from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID, CONF_PLATFORM -from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback -from homeassistant.helpers import config_validation as cv, device_registry as dr -from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo -from homeassistant.helpers.typing import ConfigType - from .helpers import async_bypass_dynamic_config_validation # Platform type should be . diff --git a/homeassistant/components/zwave_js/triggers/helpers.py b/homeassistant/components/zwave_js/triggers/helpers.py index 2fbc585c887..706c4fc0aca 100644 --- a/homeassistant/components/zwave_js/triggers/helpers.py +++ b/homeassistant/components/zwave_js/triggers/helpers.py @@ -1,11 +1,12 @@ """Helpers for Z-Wave JS custom triggers.""" -from homeassistant.components.zwave_js.const import ATTR_CONFIG_ENTRY_ID, DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.typing import ConfigType +from ..const import ATTR_CONFIG_ENTRY_ID, DOMAIN + @callback def async_bypass_dynamic_config_validation( diff --git a/homeassistant/components/zwave_js/triggers/value_updated.py b/homeassistant/components/zwave_js/triggers/value_updated.py index 6a94ab0577b..9478daf2aa2 100644 --- a/homeassistant/components/zwave_js/triggers/value_updated.py +++ b/homeassistant/components/zwave_js/triggers/value_updated.py @@ -7,8 +7,14 @@ import voluptuous as vol from zwave_js_server.const import CommandClass from zwave_js_server.model.value import Value, get_value_id -from homeassistant.components.zwave_js.config_validation import VALUE_SCHEMA -from homeassistant.components.zwave_js.const import ( +from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID, CONF_PLATFORM, MATCH_ALL +from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback +from homeassistant.helpers import config_validation as cv, device_registry as dr +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo +from homeassistant.helpers.typing import ConfigType + +from ..config_validation import VALUE_SCHEMA +from ..const import ( ATTR_COMMAND_CLASS, ATTR_COMMAND_CLASS_NAME, ATTR_CURRENT_VALUE, @@ -23,16 +29,7 @@ from homeassistant.components.zwave_js.const import ( ATTR_PROPERTY_NAME, DOMAIN, ) -from homeassistant.components.zwave_js.helpers import ( - async_get_nodes_from_targets, - get_device_id, -) -from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID, CONF_PLATFORM, MATCH_ALL -from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback -from homeassistant.helpers import config_validation as cv, device_registry as dr -from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo -from homeassistant.helpers.typing import ConfigType - +from ..helpers import async_get_nodes_from_targets, get_device_id from .helpers import async_bypass_dynamic_config_validation # Platform type should be . From 7db1f8186c2718c617ccbb6eff62c7f4f25800b4 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 12 Sep 2022 23:43:21 +0200 Subject: [PATCH 314/955] Improve zone typing (#78294) Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> --- homeassistant/components/zone/trigger.py | 29 +++++++++++++++--------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/zone/trigger.py b/homeassistant/components/zone/trigger.py index 4958ec102d1..9fdfc9dcc90 100644 --- a/homeassistant/components/zone/trigger.py +++ b/homeassistant/components/zone/trigger.py @@ -1,4 +1,6 @@ """Offer zone automation rules.""" +from __future__ import annotations + import logging import voluptuous as vol @@ -10,7 +12,14 @@ from homeassistant.const import ( CONF_PLATFORM, CONF_ZONE, ) -from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback +from homeassistant.core import ( + CALLBACK_TYPE, + Event, + HassJob, + HomeAssistant, + State, + callback, +) from homeassistant.helpers import ( condition, config_validation as cv, @@ -21,9 +30,6 @@ from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType -# mypy: allow-incomplete-defs, allow-untyped-defs -# mypy: no-check-untyped-defs - EVENT_ENTER = "enter" EVENT_LEAVE = "leave" DEFAULT_EVENT = EVENT_ENTER @@ -67,21 +73,22 @@ async def async_attach_trigger( """Listen for state changes based on configuration.""" trigger_data = trigger_info["trigger_data"] entity_id: list[str] = config[CONF_ENTITY_ID] - zone_entity_id = config.get(CONF_ZONE) - event = config.get(CONF_EVENT) + zone_entity_id: str = config[CONF_ZONE] + event: str = config[CONF_EVENT] job = HassJob(action) @callback - def zone_automation_listener(zone_event): + def zone_automation_listener(zone_event: Event) -> None: """Listen for state changes and calls action.""" entity = zone_event.data.get("entity_id") - from_s = zone_event.data.get("old_state") - to_s = zone_event.data.get("new_state") + from_s: State | None = zone_event.data.get("old_state") + to_s: State | None = zone_event.data.get("new_state") if ( from_s and not location.has_location(from_s) - or not location.has_location(to_s) + or to_s + and not location.has_location(to_s) ): return @@ -119,7 +126,7 @@ async def async_attach_trigger( "description": description, } }, - to_s.context, + to_s.context if to_s else None, ) return async_track_state_change_event(hass, entity_id, zone_automation_listener) From 2857a15cbcbafde2f8e0c4c8f4990563b44cefd0 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 12 Sep 2022 23:43:59 +0200 Subject: [PATCH 315/955] Import http constants from root (#78274) --- homeassistant/components/onboarding/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/onboarding/views.py b/homeassistant/components/onboarding/views.py index c29fb7edf3a..43d942c8912 100644 --- a/homeassistant/components/onboarding/views.py +++ b/homeassistant/components/onboarding/views.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.auth.const import GROUP_ID_ADMIN from homeassistant.components import person from homeassistant.components.auth import indieauth -from homeassistant.components.http.const import KEY_HASS_REFRESH_TOKEN_ID +from homeassistant.components.http import KEY_HASS_REFRESH_TOKEN_ID from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.components.http.view import HomeAssistantView from homeassistant.core import callback From c6872731804b76ade460d3bea7fb5f88772729f9 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 12 Sep 2022 23:45:19 +0200 Subject: [PATCH 316/955] Import media source constants from root (#78275) --- homeassistant/components/system_bridge/media_source.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/homeassistant/components/system_bridge/media_source.py b/homeassistant/components/system_bridge/media_source.py index ab0d4f28319..3186d74b15a 100644 --- a/homeassistant/components/system_bridge/media_source.py +++ b/homeassistant/components/system_bridge/media_source.py @@ -5,10 +5,7 @@ from systembridgeconnector.models.media_directories import MediaDirectories from systembridgeconnector.models.media_files import File as MediaFile, MediaFiles from homeassistant.components.media_player import MediaClass -from homeassistant.components.media_source.const import ( - MEDIA_CLASS_MAP, - MEDIA_MIME_TYPES, -) +from homeassistant.components.media_source import MEDIA_CLASS_MAP, MEDIA_MIME_TYPES from homeassistant.components.media_source.models import ( BrowseMediaSource, MediaSource, From e32adfc801e5cc0bce782ef39bc423607927b568 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 12 Sep 2022 23:46:24 +0200 Subject: [PATCH 317/955] Import modbus constants from root (#78273) --- homeassistant/components/flexit/climate.py | 6 +++--- homeassistant/components/stiebel_eltron/__init__.py | 6 +++++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/flexit/climate.py b/homeassistant/components/flexit/climate.py index a26febd5a47..27f2314c1f2 100644 --- a/homeassistant/components/flexit/climate.py +++ b/homeassistant/components/flexit/climate.py @@ -13,15 +13,15 @@ from homeassistant.components.climate import ( HVACAction, HVACMode, ) -from homeassistant.components.modbus import get_hub -from homeassistant.components.modbus.const import ( +from homeassistant.components.modbus import ( CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_REGISTER_INPUT, CALL_TYPE_WRITE_REGISTER, CONF_HUB, DEFAULT_HUB, + ModbusHub, + get_hub, ) -from homeassistant.components.modbus.modbus import ModbusHub from homeassistant.const import ( ATTR_TEMPERATURE, CONF_NAME, diff --git a/homeassistant/components/stiebel_eltron/__init__.py b/homeassistant/components/stiebel_eltron/__init__.py index e7a5c8830db..84a39e3c875 100644 --- a/homeassistant/components/stiebel_eltron/__init__.py +++ b/homeassistant/components/stiebel_eltron/__init__.py @@ -5,7 +5,11 @@ import logging from pystiebeleltron import pystiebeleltron import voluptuous as vol -from homeassistant.components.modbus.const import CONF_HUB, DEFAULT_HUB, MODBUS_DOMAIN +from homeassistant.components.modbus import ( + CONF_HUB, + DEFAULT_HUB, + DOMAIN as MODBUS_DOMAIN, +) from homeassistant.const import CONF_NAME, DEVICE_DEFAULT_NAME, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import discovery From 5f1979dbc3fd2f6f4351d223afa57d69f6088353 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 12 Sep 2022 16:54:20 -0500 Subject: [PATCH 318/955] Bump xiaomi-ble to 0.9.3 (#78301) --- .../components/xiaomi_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../xiaomi_ble/test_binary_sensor.py | 8 +- .../components/xiaomi_ble/test_config_flow.py | 38 ++++---- tests/components/xiaomi_ble/test_sensor.py | 95 ++++++++----------- 6 files changed, 66 insertions(+), 81 deletions(-) diff --git a/homeassistant/components/xiaomi_ble/manifest.json b/homeassistant/components/xiaomi_ble/manifest.json index c01f0846234..de8e61ad8ce 100644 --- a/homeassistant/components/xiaomi_ble/manifest.json +++ b/homeassistant/components/xiaomi_ble/manifest.json @@ -9,7 +9,7 @@ "service_data_uuid": "0000fe95-0000-1000-8000-00805f9b34fb" } ], - "requirements": ["xiaomi-ble==0.9.0"], + "requirements": ["xiaomi-ble==0.9.3"], "dependencies": ["bluetooth"], "codeowners": ["@Jc2k", "@Ernst79"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index f56119f9845..d1ff0c957f7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2528,7 +2528,7 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.9.0 +xiaomi-ble==0.9.3 # homeassistant.components.knx xknx==1.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9f2925e8637..116498440a4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1735,7 +1735,7 @@ wolf_smartset==0.1.11 xbox-webapi==2.0.11 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.9.0 +xiaomi-ble==0.9.3 # homeassistant.components.knx xknx==1.0.2 diff --git a/tests/components/xiaomi_ble/test_binary_sensor.py b/tests/components/xiaomi_ble/test_binary_sensor.py index 390c8d4b579..dd49b4d181d 100644 --- a/tests/components/xiaomi_ble/test_binary_sensor.py +++ b/tests/components/xiaomi_ble/test_binary_sensor.py @@ -45,10 +45,10 @@ async def test_smoke_sensor(hass): await hass.async_block_till_done() assert len(hass.states.async_all()) == 1 - smoke_sensor = hass.states.get("binary_sensor.thermometer_e39cbc_smoke") + smoke_sensor = hass.states.get("binary_sensor.thermometer_9cbc_smoke") smoke_sensor_attribtes = smoke_sensor.attributes assert smoke_sensor.state == "on" - assert smoke_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Thermometer E39CBC Smoke" + assert smoke_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Thermometer 9CBC Smoke" assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() @@ -90,10 +90,10 @@ async def test_moisture(hass): await hass.async_block_till_done() assert len(hass.states.async_all()) == 1 - sensor = hass.states.get("binary_sensor.smart_flower_pot_6a3e7a_moisture") + sensor = hass.states.get("binary_sensor.smart_flower_pot_3e7a_moisture") sensor_attr = sensor.attributes assert sensor.state == "on" - assert sensor_attr[ATTR_FRIENDLY_NAME] == "Smart Flower Pot 6A3E7A Moisture" + assert sensor_attr[ATTR_FRIENDLY_NAME] == "Smart Flower Pot 3E7A Moisture" assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/xiaomi_ble/test_config_flow.py b/tests/components/xiaomi_ble/test_config_flow.py index 6a1c9c8e435..a884e5cbefa 100644 --- a/tests/components/xiaomi_ble/test_config_flow.py +++ b/tests/components/xiaomi_ble/test_config_flow.py @@ -39,7 +39,7 @@ async def test_async_step_bluetooth_valid_device(hass): result["flow_id"], user_input={} ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "Baby Thermometer DD6FC1 (MMC-T201-1)" + assert result2["title"] == "Baby Thermometer 6FC1 (MMC-T201-1)" assert result2["data"] == {} assert result2["result"].unique_id == "00:81:F9:DD:6F:C1" @@ -65,7 +65,7 @@ async def test_async_step_bluetooth_valid_device_but_missing_payload(hass): result["flow_id"], user_input={} ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "Temperature/Humidity Sensor 565384 (LYWSD03MMC)" + assert result2["title"] == "Temperature/Humidity Sensor 5384 (LYWSD03MMC)" assert result2["data"] == {} assert result2["result"].unique_id == "A4:C1:38:56:53:84" @@ -123,7 +123,7 @@ async def test_async_step_bluetooth_during_onboarding(hass): ) assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == "Baby Thermometer DD6FC1 (MMC-T201-1)" + assert result["title"] == "Baby Thermometer 6FC1 (MMC-T201-1)" assert result["data"] == {} assert result["result"].unique_id == "00:81:F9:DD:6F:C1" assert len(mock_setup_entry.mock_calls) == 1 @@ -148,7 +148,7 @@ async def test_async_step_bluetooth_valid_device_legacy_encryption(hass): user_input={"bindkey": "b853075158487ca39a5b5ea9"}, ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "Dimmer Switch C5988B (YLKG07YL/YLKG08YL)" + assert result2["title"] == "Dimmer Switch 988B (YLKG07YL/YLKG08YL)" assert result2["data"] == {"bindkey": "b853075158487ca39a5b5ea9"} assert result2["result"].unique_id == "F8:24:41:C5:98:8B" @@ -180,7 +180,7 @@ async def test_async_step_bluetooth_valid_device_legacy_encryption_wrong_key(has user_input={"bindkey": "b853075158487ca39a5b5ea9"}, ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "Dimmer Switch C5988B (YLKG07YL/YLKG08YL)" + assert result2["title"] == "Dimmer Switch 988B (YLKG07YL/YLKG08YL)" assert result2["data"] == {"bindkey": "b853075158487ca39a5b5ea9"} assert result2["result"].unique_id == "F8:24:41:C5:98:8B" @@ -214,7 +214,7 @@ async def test_async_step_bluetooth_valid_device_legacy_encryption_wrong_key_len user_input={"bindkey": "b853075158487ca39a5b5ea9"}, ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "Dimmer Switch C5988B (YLKG07YL/YLKG08YL)" + assert result2["title"] == "Dimmer Switch 988B (YLKG07YL/YLKG08YL)" assert result2["data"] == {"bindkey": "b853075158487ca39a5b5ea9"} assert result2["result"].unique_id == "F8:24:41:C5:98:8B" @@ -238,7 +238,7 @@ async def test_async_step_bluetooth_valid_device_v4_encryption(hass): ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "Thermometer E39CBC (JTYJGD03MI)" + assert result2["title"] == "Thermometer 9CBC (JTYJGD03MI)" assert result2["data"] == {"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"} assert result2["result"].unique_id == "54:EF:44:E3:9C:BC" @@ -272,7 +272,7 @@ async def test_async_step_bluetooth_valid_device_v4_encryption_wrong_key(hass): ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "Thermometer E39CBC (JTYJGD03MI)" + assert result2["title"] == "Thermometer 9CBC (JTYJGD03MI)" assert result2["data"] == {"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"} assert result2["result"].unique_id == "54:EF:44:E3:9C:BC" @@ -306,7 +306,7 @@ async def test_async_step_bluetooth_valid_device_v4_encryption_wrong_key_length( ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "Thermometer E39CBC (JTYJGD03MI)" + assert result2["title"] == "Thermometer 9CBC (JTYJGD03MI)" assert result2["data"] == {"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"} assert result2["result"].unique_id == "54:EF:44:E3:9C:BC" @@ -370,7 +370,7 @@ async def test_async_step_user_with_found_devices(hass): user_input={"address": "58:2D:34:35:93:21"}, ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "Temperature/Humidity Sensor 359321 (LYWSDCGQ)" + assert result2["title"] == "Temperature/Humidity Sensor 9321 (LYWSDCGQ)" assert result2["data"] == {} assert result2["result"].unique_id == "58:2D:34:35:93:21" @@ -405,7 +405,7 @@ async def test_async_step_user_short_payload(hass): result["flow_id"], user_input={} ) assert result3["type"] == FlowResultType.CREATE_ENTRY - assert result3["title"] == "Temperature/Humidity Sensor 565384 (LYWSD03MMC)" + assert result3["title"] == "Temperature/Humidity Sensor 5384 (LYWSD03MMC)" assert result3["data"] == {} assert result3["result"].unique_id == "A4:C1:38:56:53:84" @@ -453,7 +453,7 @@ async def test_async_step_user_short_payload_then_full(hass): ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "Temperature/Humidity Sensor 565384 (LYWSD03MMC)" + assert result2["title"] == "Temperature/Humidity Sensor 5384 (LYWSD03MMC)" assert result2["data"] == {"bindkey": "a115210eed7a88e50ad52662e732a9fb"} @@ -486,7 +486,7 @@ async def test_async_step_user_with_found_devices_v4_encryption(hass): ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "Thermometer E39CBC (JTYJGD03MI)" + assert result2["title"] == "Thermometer 9CBC (JTYJGD03MI)" assert result2["data"] == {"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"} assert result2["result"].unique_id == "54:EF:44:E3:9C:BC" @@ -532,7 +532,7 @@ async def test_async_step_user_with_found_devices_v4_encryption_wrong_key(hass): ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "Thermometer E39CBC (JTYJGD03MI)" + assert result2["title"] == "Thermometer 9CBC (JTYJGD03MI)" assert result2["data"] == {"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"} assert result2["result"].unique_id == "54:EF:44:E3:9C:BC" @@ -580,7 +580,7 @@ async def test_async_step_user_with_found_devices_v4_encryption_wrong_key_length ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "Thermometer E39CBC (JTYJGD03MI)" + assert result2["title"] == "Thermometer 9CBC (JTYJGD03MI)" assert result2["data"] == {"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"} assert result2["result"].unique_id == "54:EF:44:E3:9C:BC" @@ -613,7 +613,7 @@ async def test_async_step_user_with_found_devices_legacy_encryption(hass): user_input={"bindkey": "b853075158487ca39a5b5ea9"}, ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "Dimmer Switch C5988B (YLKG07YL/YLKG08YL)" + assert result2["title"] == "Dimmer Switch 988B (YLKG07YL/YLKG08YL)" assert result2["data"] == {"bindkey": "b853075158487ca39a5b5ea9"} assert result2["result"].unique_id == "F8:24:41:C5:98:8B" @@ -658,7 +658,7 @@ async def test_async_step_user_with_found_devices_legacy_encryption_wrong_key( user_input={"bindkey": "b853075158487ca39a5b5ea9"}, ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "Dimmer Switch C5988B (YLKG07YL/YLKG08YL)" + assert result2["title"] == "Dimmer Switch 988B (YLKG07YL/YLKG08YL)" assert result2["data"] == {"bindkey": "b853075158487ca39a5b5ea9"} assert result2["result"].unique_id == "F8:24:41:C5:98:8B" @@ -703,7 +703,7 @@ async def test_async_step_user_with_found_devices_legacy_encryption_wrong_key_le user_input={"bindkey": "b853075158487ca39a5b5ea9"}, ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "Dimmer Switch C5988B (YLKG07YL/YLKG08YL)" + assert result2["title"] == "Dimmer Switch 988B (YLKG07YL/YLKG08YL)" assert result2["data"] == {"bindkey": "b853075158487ca39a5b5ea9"} assert result2["result"].unique_id == "F8:24:41:C5:98:8B" @@ -822,7 +822,7 @@ async def test_async_step_user_takes_precedence_over_discovery(hass): user_input={"address": "00:81:F9:DD:6F:C1"}, ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "Baby Thermometer DD6FC1 (MMC-T201-1)" + assert result2["title"] == "Baby Thermometer 6FC1 (MMC-T201-1)" assert result2["data"] == {} assert result2["result"].unique_id == "00:81:F9:DD:6F:C1" diff --git a/tests/components/xiaomi_ble/test_sensor.py b/tests/components/xiaomi_ble/test_sensor.py index c4052d36bf4..25c118bed49 100644 --- a/tests/components/xiaomi_ble/test_sensor.py +++ b/tests/components/xiaomi_ble/test_sensor.py @@ -42,12 +42,11 @@ async def test_sensors(hass): await hass.async_block_till_done() assert len(hass.states.async_all()) == 2 - temp_sensor = hass.states.get("sensor.baby_thermometer_dd6fc1_temperature") + temp_sensor = hass.states.get("sensor.baby_thermometer_6fc1_temperature") temp_sensor_attribtes = temp_sensor.attributes assert temp_sensor.state == "36.8719980616822" assert ( - temp_sensor_attribtes[ATTR_FRIENDLY_NAME] - == "Baby Thermometer DD6FC1 Temperature" + temp_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Baby Thermometer 6FC1 Temperature" ) assert temp_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "°C" assert temp_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" @@ -92,10 +91,10 @@ async def test_xiaomi_formaldeyhde(hass): await hass.async_block_till_done() assert len(hass.states.async_all()) == 1 - sensor = hass.states.get("sensor.smart_flower_pot_6a3e7a_formaldehyde") + sensor = hass.states.get("sensor.smart_flower_pot_3e7a_formaldehyde") sensor_attr = sensor.attributes assert sensor.state == "2.44" - assert sensor_attr[ATTR_FRIENDLY_NAME] == "Smart Flower Pot 6A3E7A Formaldehyde" + assert sensor_attr[ATTR_FRIENDLY_NAME] == "Smart Flower Pot 3E7A Formaldehyde" assert sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == "mg/m³" assert sensor_attr[ATTR_STATE_CLASS] == "measurement" @@ -139,10 +138,10 @@ async def test_xiaomi_consumable(hass): await hass.async_block_till_done() assert len(hass.states.async_all()) == 1 - sensor = hass.states.get("sensor.smart_flower_pot_6a3e7a_consumable") + sensor = hass.states.get("sensor.smart_flower_pot_3e7a_consumable") sensor_attr = sensor.attributes assert sensor.state == "96" - assert sensor_attr[ATTR_FRIENDLY_NAME] == "Smart Flower Pot 6A3E7A Consumable" + assert sensor_attr[ATTR_FRIENDLY_NAME] == "Smart Flower Pot 3E7A Consumable" assert sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == "%" assert sensor_attr[ATTR_STATE_CLASS] == "measurement" @@ -186,17 +185,17 @@ async def test_xiaomi_battery_voltage(hass): await hass.async_block_till_done() assert len(hass.states.async_all()) == 2 - volt_sensor = hass.states.get("sensor.smart_flower_pot_6a3e7a_voltage") + volt_sensor = hass.states.get("sensor.smart_flower_pot_3e7a_voltage") volt_sensor_attr = volt_sensor.attributes assert volt_sensor.state == "3.1" - assert volt_sensor_attr[ATTR_FRIENDLY_NAME] == "Smart Flower Pot 6A3E7A Voltage" + assert volt_sensor_attr[ATTR_FRIENDLY_NAME] == "Smart Flower Pot 3E7A Voltage" assert volt_sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == "V" assert volt_sensor_attr[ATTR_STATE_CLASS] == "measurement" - bat_sensor = hass.states.get("sensor.smart_flower_pot_6a3e7a_battery") + bat_sensor = hass.states.get("sensor.smart_flower_pot_3e7a_battery") bat_sensor_attr = bat_sensor.attributes assert bat_sensor.state == "100" - assert bat_sensor_attr[ATTR_FRIENDLY_NAME] == "Smart Flower Pot 6A3E7A Battery" + assert bat_sensor_attr[ATTR_FRIENDLY_NAME] == "Smart Flower Pot 3E7A Battery" assert bat_sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == "%" assert bat_sensor_attr[ATTR_STATE_CLASS] == "measurement" @@ -254,42 +253,38 @@ async def test_xiaomi_HHCCJCY01(hass): await hass.async_block_till_done() assert len(hass.states.async_all()) == 5 - illum_sensor = hass.states.get("sensor.plant_sensor_6a3e7a_illuminance") + illum_sensor = hass.states.get("sensor.plant_sensor_3e7a_illuminance") illum_sensor_attr = illum_sensor.attributes assert illum_sensor.state == "0" - assert illum_sensor_attr[ATTR_FRIENDLY_NAME] == "Plant Sensor 6A3E7A Illuminance" + assert illum_sensor_attr[ATTR_FRIENDLY_NAME] == "Plant Sensor 3E7A Illuminance" assert illum_sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == "lx" assert illum_sensor_attr[ATTR_STATE_CLASS] == "measurement" - cond_sensor = hass.states.get("sensor.plant_sensor_6a3e7a_conductivity") + cond_sensor = hass.states.get("sensor.plant_sensor_3e7a_conductivity") cond_sensor_attribtes = cond_sensor.attributes assert cond_sensor.state == "599" - assert ( - cond_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Plant Sensor 6A3E7A Conductivity" - ) + assert cond_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Plant Sensor 3E7A Conductivity" assert cond_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "µS/cm" assert cond_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" - moist_sensor = hass.states.get("sensor.plant_sensor_6a3e7a_moisture") + moist_sensor = hass.states.get("sensor.plant_sensor_3e7a_moisture") moist_sensor_attribtes = moist_sensor.attributes assert moist_sensor.state == "64" - assert moist_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Plant Sensor 6A3E7A Moisture" + assert moist_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Plant Sensor 3E7A Moisture" assert moist_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "%" assert moist_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" - temp_sensor = hass.states.get("sensor.plant_sensor_6a3e7a_temperature") + temp_sensor = hass.states.get("sensor.plant_sensor_3e7a_temperature") temp_sensor_attribtes = temp_sensor.attributes assert temp_sensor.state == "24.4" - assert ( - temp_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Plant Sensor 6A3E7A Temperature" - ) + assert temp_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Plant Sensor 3E7A Temperature" assert temp_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "°C" assert temp_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" - batt_sensor = hass.states.get("sensor.plant_sensor_6a3e7a_battery") + batt_sensor = hass.states.get("sensor.plant_sensor_3e7a_battery") batt_sensor_attribtes = batt_sensor.attributes assert batt_sensor.state == "5" - assert batt_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Plant Sensor 6A3E7A Battery" + assert batt_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Plant Sensor 3E7A Battery" assert batt_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "%" assert batt_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" @@ -355,35 +350,31 @@ async def test_xiaomi_HHCCJCY01_not_connectable(hass): await hass.async_block_till_done() assert len(hass.states.async_all()) == 4 - illum_sensor = hass.states.get("sensor.plant_sensor_6a3e7a_illuminance") + illum_sensor = hass.states.get("sensor.plant_sensor_3e7a_illuminance") illum_sensor_attr = illum_sensor.attributes assert illum_sensor.state == "0" - assert illum_sensor_attr[ATTR_FRIENDLY_NAME] == "Plant Sensor 6A3E7A Illuminance" + assert illum_sensor_attr[ATTR_FRIENDLY_NAME] == "Plant Sensor 3E7A Illuminance" assert illum_sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == "lx" assert illum_sensor_attr[ATTR_STATE_CLASS] == "measurement" - cond_sensor = hass.states.get("sensor.plant_sensor_6a3e7a_conductivity") + cond_sensor = hass.states.get("sensor.plant_sensor_3e7a_conductivity") cond_sensor_attribtes = cond_sensor.attributes assert cond_sensor.state == "599" - assert ( - cond_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Plant Sensor 6A3E7A Conductivity" - ) + assert cond_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Plant Sensor 3E7A Conductivity" assert cond_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "µS/cm" assert cond_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" - moist_sensor = hass.states.get("sensor.plant_sensor_6a3e7a_moisture") + moist_sensor = hass.states.get("sensor.plant_sensor_3e7a_moisture") moist_sensor_attribtes = moist_sensor.attributes assert moist_sensor.state == "64" - assert moist_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Plant Sensor 6A3E7A Moisture" + assert moist_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Plant Sensor 3E7A Moisture" assert moist_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "%" assert moist_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" - temp_sensor = hass.states.get("sensor.plant_sensor_6a3e7a_temperature") + temp_sensor = hass.states.get("sensor.plant_sensor_3e7a_temperature") temp_sensor_attribtes = temp_sensor.attributes assert temp_sensor.state == "24.4" - assert ( - temp_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Plant Sensor 6A3E7A Temperature" - ) + assert temp_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Plant Sensor 3E7A Temperature" assert temp_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "°C" assert temp_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" @@ -438,42 +429,38 @@ async def test_xiaomi_HHCCJCY01_only_some_sources_connectable(hass): await hass.async_block_till_done() assert len(hass.states.async_all()) == 5 - illum_sensor = hass.states.get("sensor.plant_sensor_6a3e7a_illuminance") + illum_sensor = hass.states.get("sensor.plant_sensor_3e7a_illuminance") illum_sensor_attr = illum_sensor.attributes assert illum_sensor.state == "0" - assert illum_sensor_attr[ATTR_FRIENDLY_NAME] == "Plant Sensor 6A3E7A Illuminance" + assert illum_sensor_attr[ATTR_FRIENDLY_NAME] == "Plant Sensor 3E7A Illuminance" assert illum_sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == "lx" assert illum_sensor_attr[ATTR_STATE_CLASS] == "measurement" - cond_sensor = hass.states.get("sensor.plant_sensor_6a3e7a_conductivity") + cond_sensor = hass.states.get("sensor.plant_sensor_3e7a_conductivity") cond_sensor_attribtes = cond_sensor.attributes assert cond_sensor.state == "599" - assert ( - cond_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Plant Sensor 6A3E7A Conductivity" - ) + assert cond_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Plant Sensor 3E7A Conductivity" assert cond_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "µS/cm" assert cond_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" - moist_sensor = hass.states.get("sensor.plant_sensor_6a3e7a_moisture") + moist_sensor = hass.states.get("sensor.plant_sensor_3e7a_moisture") moist_sensor_attribtes = moist_sensor.attributes assert moist_sensor.state == "64" - assert moist_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Plant Sensor 6A3E7A Moisture" + assert moist_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Plant Sensor 3E7A Moisture" assert moist_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "%" assert moist_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" - temp_sensor = hass.states.get("sensor.plant_sensor_6a3e7a_temperature") + temp_sensor = hass.states.get("sensor.plant_sensor_3e7a_temperature") temp_sensor_attribtes = temp_sensor.attributes assert temp_sensor.state == "24.4" - assert ( - temp_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Plant Sensor 6A3E7A Temperature" - ) + assert temp_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Plant Sensor 3E7A Temperature" assert temp_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "°C" assert temp_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" - batt_sensor = hass.states.get("sensor.plant_sensor_6a3e7a_battery") + batt_sensor = hass.states.get("sensor.plant_sensor_3e7a_battery") batt_sensor_attribtes = batt_sensor.attributes assert batt_sensor.state == "5" - assert batt_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Plant Sensor 6A3E7A Battery" + assert batt_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Plant Sensor 3E7A Battery" assert batt_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "%" assert batt_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" @@ -515,14 +502,12 @@ async def test_xiaomi_CGDK2(hass): await hass.async_block_till_done() assert len(hass.states.async_all()) == 1 - temp_sensor = hass.states.get( - "sensor.temperature_humidity_sensor_122089_temperature" - ) + temp_sensor = hass.states.get("sensor.temperature_humidity_sensor_2089_temperature") temp_sensor_attribtes = temp_sensor.attributes assert temp_sensor.state == "22.6" assert ( temp_sensor_attribtes[ATTR_FRIENDLY_NAME] - == "Temperature/Humidity Sensor 122089 Temperature" + == "Temperature/Humidity Sensor 2089 Temperature" ) assert temp_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "°C" assert temp_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" From 9b0602a8b6588c8f4ac4f67dd6e7494356111d0d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 13 Sep 2022 00:19:57 +0200 Subject: [PATCH 319/955] Import device automation constants from root (#78272) --- homeassistant/components/binary_sensor/device_condition.py | 2 +- homeassistant/components/binary_sensor/device_trigger.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/binary_sensor/device_condition.py b/homeassistant/components/binary_sensor/device_condition.py index 4b3aa70d716..4da9bd45670 100644 --- a/homeassistant/components/binary_sensor/device_condition.py +++ b/homeassistant/components/binary_sensor/device_condition.py @@ -3,7 +3,7 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.device_automation.const import CONF_IS_OFF, CONF_IS_ON +from homeassistant.components.device_automation import CONF_IS_OFF, CONF_IS_ON from homeassistant.const import ( CONF_CONDITION, CONF_ENTITY_ID, diff --git a/homeassistant/components/binary_sensor/device_trigger.py b/homeassistant/components/binary_sensor/device_trigger.py index 66daa687f38..969a52d1514 100644 --- a/homeassistant/components/binary_sensor/device_trigger.py +++ b/homeassistant/components/binary_sensor/device_trigger.py @@ -1,10 +1,10 @@ """Provides device triggers for binary sensors.""" import voluptuous as vol -from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA -from homeassistant.components.device_automation.const import ( +from homeassistant.components.device_automation import ( CONF_TURNED_OFF, CONF_TURNED_ON, + DEVICE_TRIGGER_BASE_SCHEMA, ) from homeassistant.components.homeassistant.triggers import state as state_trigger from homeassistant.const import CONF_ENTITY_ID, CONF_FOR, CONF_TYPE From 93a5b99191880ac84eb3d95adf4a402bbb536c1e Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 13 Sep 2022 00:36:05 +0200 Subject: [PATCH 320/955] Rename zwave_js trigger helper to avoid confusion (#78331) * Rename zwave_js trigger helper to avoid confusion * Fix test --- homeassistant/components/zwave_js/triggers/event.py | 2 +- .../zwave_js/triggers/{helpers.py => trigger_helpers.py} | 0 homeassistant/components/zwave_js/triggers/value_updated.py | 2 +- tests/components/zwave_js/test_trigger.py | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename homeassistant/components/zwave_js/triggers/{helpers.py => trigger_helpers.py} (100%) diff --git a/homeassistant/components/zwave_js/triggers/event.py b/homeassistant/components/zwave_js/triggers/event.py index a6d6c967504..12c9d267ca6 100644 --- a/homeassistant/components/zwave_js/triggers/event.py +++ b/homeassistant/components/zwave_js/triggers/event.py @@ -31,7 +31,7 @@ from ..helpers import ( get_device_id, get_home_and_node_id_from_device_entry, ) -from .helpers import async_bypass_dynamic_config_validation +from .trigger_helpers import async_bypass_dynamic_config_validation # Platform type should be . PLATFORM_TYPE = f"{DOMAIN}.{__name__.rsplit('.', maxsplit=1)[-1]}" diff --git a/homeassistant/components/zwave_js/triggers/helpers.py b/homeassistant/components/zwave_js/triggers/trigger_helpers.py similarity index 100% rename from homeassistant/components/zwave_js/triggers/helpers.py rename to homeassistant/components/zwave_js/triggers/trigger_helpers.py diff --git a/homeassistant/components/zwave_js/triggers/value_updated.py b/homeassistant/components/zwave_js/triggers/value_updated.py index 9478daf2aa2..780d1251911 100644 --- a/homeassistant/components/zwave_js/triggers/value_updated.py +++ b/homeassistant/components/zwave_js/triggers/value_updated.py @@ -30,7 +30,7 @@ from ..const import ( DOMAIN, ) from ..helpers import async_get_nodes_from_targets, get_device_id -from .helpers import async_bypass_dynamic_config_validation +from .trigger_helpers import async_bypass_dynamic_config_validation # Platform type should be . PLATFORM_TYPE = f"{DOMAIN}.{__name__.rsplit('.', maxsplit=1)[-1]}" diff --git a/tests/components/zwave_js/test_trigger.py b/tests/components/zwave_js/test_trigger.py index 48439eede0f..a5c226057d4 100644 --- a/tests/components/zwave_js/test_trigger.py +++ b/tests/components/zwave_js/test_trigger.py @@ -10,7 +10,7 @@ from zwave_js_server.model.node import Node from homeassistant.components import automation from homeassistant.components.zwave_js import DOMAIN from homeassistant.components.zwave_js.trigger import async_validate_trigger_config -from homeassistant.components.zwave_js.triggers.helpers import ( +from homeassistant.components.zwave_js.triggers.trigger_helpers import ( async_bypass_dynamic_config_validation, ) from homeassistant.const import SERVICE_RELOAD From 0b2c3cfb99ed339b4c58a52389d50fc3b9e03bf4 Mon Sep 17 00:00:00 2001 From: d-walsh Date: Mon, 12 Sep 2022 19:03:07 -0400 Subject: [PATCH 321/955] Fix missing dependency for dbus_next (#78235) --- homeassistant/components/bluetooth/manifest.json | 3 ++- homeassistant/package_constraints.txt | 1 + requirements_all.txt | 3 +++ requirements_test_all.txt | 3 +++ 4 files changed, 9 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 3c9ef7a85b7..55a7e610e76 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -7,7 +7,8 @@ "requirements": [ "bleak==0.16.0", "bluetooth-adapters==0.4.1", - "bluetooth-auto-recovery==0.3.2" + "bluetooth-auto-recovery==0.3.2", + "dbus_next==0.2.3" ], "codeowners": ["@bdraco"], "config_flow": true, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index f0f2a2eb8c3..f2af1efde94 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,6 +16,7 @@ bluetooth-auto-recovery==0.3.2 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==37.0.4 +dbus_next==0.2.3 fnvhash==0.1.0 hass-nabucasa==0.55.0 home-assistant-bluetooth==1.3.0 diff --git a/requirements_all.txt b/requirements_all.txt index d1ff0c957f7..851784ca92f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -534,6 +534,9 @@ datadog==0.15.0 # homeassistant.components.metoffice datapoint==0.9.8 +# homeassistant.components.bluetooth +dbus_next==0.2.3 + # homeassistant.components.debugpy debugpy==1.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 116498440a4..5096087b44d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -411,6 +411,9 @@ datadog==0.15.0 # homeassistant.components.metoffice datapoint==0.9.8 +# homeassistant.components.bluetooth +dbus_next==0.2.3 + # homeassistant.components.debugpy debugpy==1.6.3 From 05d4ece4de682f2f4a9367a1e8759343e53faaba Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 12 Sep 2022 19:16:46 -0500 Subject: [PATCH 322/955] Bump bluetooth-auto-recovery to 0.3.3 (#78245) Downgrades a few more loggers to debug since the only reason we check them is to give a better error message when the bluetooth adapter is blocked by rfkill. https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/compare/v0.3.2...v0.3.3 closes #78211 --- 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 55a7e610e76..434b4c1127e 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -7,7 +7,7 @@ "requirements": [ "bleak==0.16.0", "bluetooth-adapters==0.4.1", - "bluetooth-auto-recovery==0.3.2", + "bluetooth-auto-recovery==0.3.3", "dbus_next==0.2.3" ], "codeowners": ["@bdraco"], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index f2af1efde94..dbbfda9d6db 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,7 +12,7 @@ awesomeversion==22.8.0 bcrypt==3.1.7 bleak==0.16.0 bluetooth-adapters==0.4.1 -bluetooth-auto-recovery==0.3.2 +bluetooth-auto-recovery==0.3.3 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==37.0.4 diff --git a/requirements_all.txt b/requirements_all.txt index 851784ca92f..6c310e55e55 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -433,7 +433,7 @@ bluemaestro-ble==0.2.0 bluetooth-adapters==0.4.1 # homeassistant.components.bluetooth -bluetooth-auto-recovery==0.3.2 +bluetooth-auto-recovery==0.3.3 # homeassistant.components.bond bond-async==0.1.22 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5096087b44d..769c5ffcaa9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -344,7 +344,7 @@ bluemaestro-ble==0.2.0 bluetooth-adapters==0.4.1 # homeassistant.components.bluetooth -bluetooth-auto-recovery==0.3.2 +bluetooth-auto-recovery==0.3.3 # homeassistant.components.bond bond-async==0.1.22 From 955f3b7083a59b906afa92836e40a77c69363ec5 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 13 Sep 2022 00:29:19 +0000 Subject: [PATCH 323/955] [ci skip] Translation update --- .../amberelectric/translations/bg.json | 4 + .../amberelectric/translations/ca.json | 5 + .../amberelectric/translations/et.json | 5 + .../amberelectric/translations/pt.json | 7 + .../amberelectric/translations/tr.json | 5 + .../automation/translations/pt.json | 13 ++ .../automation/translations/tr.json | 13 ++ .../components/awair/translations/bg.json | 22 ++- .../components/awair/translations/pt.json | 14 ++ .../components/awair/translations/tr.json | 2 +- .../binary_sensor/translations/pt-BR.json | 2 +- .../bluemaestro/translations/bg.json | 3 +- .../bluemaestro/translations/nl.json | 22 +++ .../bluemaestro/translations/pt.json | 7 + .../bluemaestro/translations/tr.json | 22 +++ .../components/bluetooth/translations/tr.json | 8 +- .../components/bthome/translations/pt.json | 16 +++ .../components/bthome/translations/tr.json | 32 +++++ .../components/demo/translations/pt.json | 13 ++ .../components/ecowitt/translations/bg.json | 5 + .../components/ecowitt/translations/pt.json | 19 +++ .../components/ecowitt/translations/tr.json | 20 +++ .../components/escea/translations/pt.json | 9 ++ .../components/fibaro/translations/et.json | 10 +- .../components/fibaro/translations/nl.json | 3 +- .../components/fibaro/translations/pt.json | 5 + .../components/fibaro/translations/tr.json | 10 +- .../components/guardian/translations/pt.json | 13 ++ .../translations/sensor.pt.json | 10 ++ .../components/hue/translations/tr.json | 14 +- .../components/icloud/translations/pt.json | 3 + .../components/icloud/translations/tr.json | 7 + .../justnimbus/translations/pt.json | 11 ++ .../components/lametric/translations/pt.json | 36 +++++ .../components/led_ble/translations/pt.json | 15 ++ .../components/led_ble/translations/tr.json | 23 ++++ .../litterrobot/translations/tr.json | 10 +- .../components/melnor/translations/pt.json | 13 ++ .../components/melnor/translations/tr.json | 13 ++ .../components/mqtt/translations/tr.json | 6 + .../components/mysensors/translations/pt.json | 11 ++ .../nam/translations/sensor.pt.json | 7 + .../nam/translations/sensor.tr.json | 11 ++ .../components/nest/translations/pt.json | 10 ++ .../components/nobo_hub/translations/nl.json | 18 +++ .../components/nobo_hub/translations/pt.json | 39 ++++++ .../components/nobo_hub/translations/tr.json | 44 ++++++ .../openexchangerates/translations/pt.json | 20 +++ .../components/overkiz/translations/pt.json | 3 +- .../components/overkiz/translations/tr.json | 3 +- .../p1_monitor/translations/pt.json | 3 + .../p1_monitor/translations/tr.json | 3 + .../components/prusalink/translations/pt.json | 7 + .../prusalink/translations/sensor.pt.json | 11 ++ .../prusalink/translations/sensor.tr.json | 11 ++ .../components/prusalink/translations/tr.json | 18 +++ .../pure_energie/translations/pt.json | 3 + .../pure_energie/translations/tr.json | 3 + .../components/pushover/translations/pt.json | 17 +++ .../components/qingping/translations/bg.json | 8 ++ .../components/risco/translations/pt.json | 4 + .../components/risco/translations/tr.json | 18 +++ .../components/schedule/translations/pt.json | 3 + .../components/sensibo/translations/pt.json | 6 + .../components/sensibo/translations/tr.json | 6 + .../components/sensor/translations/pt.json | 2 + .../components/sensor/translations/tr.json | 2 + .../components/sensorpro/translations/bg.json | 3 +- .../components/sensorpro/translations/pt.json | 7 + .../components/sensorpro/translations/tr.json | 22 +++ .../simplepush/translations/pt.json | 6 + .../components/skybell/translations/pt.json | 6 + .../components/skybell/translations/tr.json | 7 + .../speedtestdotnet/translations/pt.json | 13 ++ .../speedtestdotnet/translations/tr.json | 13 ++ .../thermobeacon/translations/pt.json | 7 + .../thermobeacon/translations/tr.json | 22 +++ .../components/thermopro/translations/tr.json | 21 +++ .../components/tilt_ble/translations/et.json | 21 +++ .../components/tilt_ble/translations/nl.json | 1 + .../components/tilt_ble/translations/tr.json | 21 +++ .../unifiprotect/translations/pt.json | 9 ++ .../unifiprotect/translations/tr.json | 4 + .../volvooncall/translations/nl.json | 3 + .../volvooncall/translations/pt-BR.json | 2 +- .../volvooncall/translations/pt.json | 19 +++ .../volvooncall/translations/tr.json | 29 ++++ .../xiaomi_ble/translations/pt.json | 17 +++ .../xiaomi_miio/translations/select.pt.json | 10 ++ .../yalexs_ble/translations/bg.json | 12 ++ .../yalexs_ble/translations/pt.json | 22 +++ .../components/zha/translations/bg.json | 9 +- .../components/zha/translations/et.json | 45 ++++++ .../components/zha/translations/pt.json | 68 ++++++++- .../components/zha/translations/tr.json | 129 +++++++++++++++++- 95 files changed, 1283 insertions(+), 26 deletions(-) create mode 100644 homeassistant/components/amberelectric/translations/pt.json create mode 100644 homeassistant/components/bluemaestro/translations/nl.json create mode 100644 homeassistant/components/bluemaestro/translations/pt.json create mode 100644 homeassistant/components/bluemaestro/translations/tr.json create mode 100644 homeassistant/components/bthome/translations/pt.json create mode 100644 homeassistant/components/bthome/translations/tr.json create mode 100644 homeassistant/components/ecowitt/translations/pt.json create mode 100644 homeassistant/components/ecowitt/translations/tr.json create mode 100644 homeassistant/components/escea/translations/pt.json create mode 100644 homeassistant/components/homekit_controller/translations/sensor.pt.json create mode 100644 homeassistant/components/justnimbus/translations/pt.json create mode 100644 homeassistant/components/lametric/translations/pt.json create mode 100644 homeassistant/components/led_ble/translations/pt.json create mode 100644 homeassistant/components/led_ble/translations/tr.json create mode 100644 homeassistant/components/melnor/translations/pt.json create mode 100644 homeassistant/components/melnor/translations/tr.json create mode 100644 homeassistant/components/nam/translations/sensor.pt.json create mode 100644 homeassistant/components/nam/translations/sensor.tr.json create mode 100644 homeassistant/components/nobo_hub/translations/nl.json create mode 100644 homeassistant/components/nobo_hub/translations/pt.json create mode 100644 homeassistant/components/nobo_hub/translations/tr.json create mode 100644 homeassistant/components/openexchangerates/translations/pt.json create mode 100644 homeassistant/components/prusalink/translations/pt.json create mode 100644 homeassistant/components/prusalink/translations/sensor.pt.json create mode 100644 homeassistant/components/prusalink/translations/sensor.tr.json create mode 100644 homeassistant/components/prusalink/translations/tr.json create mode 100644 homeassistant/components/pushover/translations/pt.json create mode 100644 homeassistant/components/schedule/translations/pt.json create mode 100644 homeassistant/components/sensorpro/translations/pt.json create mode 100644 homeassistant/components/sensorpro/translations/tr.json create mode 100644 homeassistant/components/thermobeacon/translations/pt.json create mode 100644 homeassistant/components/thermobeacon/translations/tr.json create mode 100644 homeassistant/components/thermopro/translations/tr.json create mode 100644 homeassistant/components/tilt_ble/translations/et.json create mode 100644 homeassistant/components/tilt_ble/translations/tr.json create mode 100644 homeassistant/components/volvooncall/translations/pt.json create mode 100644 homeassistant/components/volvooncall/translations/tr.json create mode 100644 homeassistant/components/xiaomi_ble/translations/pt.json create mode 100644 homeassistant/components/yalexs_ble/translations/bg.json create mode 100644 homeassistant/components/yalexs_ble/translations/pt.json diff --git a/homeassistant/components/amberelectric/translations/bg.json b/homeassistant/components/amberelectric/translations/bg.json index 6f035fae4e6..f7765e3461f 100644 --- a/homeassistant/components/amberelectric/translations/bg.json +++ b/homeassistant/components/amberelectric/translations/bg.json @@ -1,5 +1,9 @@ { "config": { + "error": { + "invalid_api_token": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d API \u043a\u043b\u044e\u0447", + "unknown_error": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, "step": { "user": { "description": "\u041e\u0442\u0438\u0434\u0435\u0442\u0435 \u043d\u0430 {api_url}, \u0437\u0430 \u0434\u0430 \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0430\u0442\u0435 API \u043a\u043b\u044e\u0447" diff --git a/homeassistant/components/amberelectric/translations/ca.json b/homeassistant/components/amberelectric/translations/ca.json index 208c7fb9f7c..678a70f6db7 100644 --- a/homeassistant/components/amberelectric/translations/ca.json +++ b/homeassistant/components/amberelectric/translations/ca.json @@ -1,5 +1,10 @@ { "config": { + "error": { + "invalid_api_token": "Clau API inv\u00e0lida", + "no_site": "No s'ha proporcionat cap lloc", + "unknown_error": "Error inesperat" + }, "step": { "site": { "data": { diff --git a/homeassistant/components/amberelectric/translations/et.json b/homeassistant/components/amberelectric/translations/et.json index e48f6f2a749..0bab3d8661d 100644 --- a/homeassistant/components/amberelectric/translations/et.json +++ b/homeassistant/components/amberelectric/translations/et.json @@ -1,5 +1,10 @@ { "config": { + "error": { + "invalid_api_token": "Vigane API v\u00f5ti", + "no_site": "Saiti pole pakutud", + "unknown_error": "Ootamatu t\u00f5rge" + }, "step": { "site": { "data": { diff --git a/homeassistant/components/amberelectric/translations/pt.json b/homeassistant/components/amberelectric/translations/pt.json new file mode 100644 index 00000000000..8320b3662d3 --- /dev/null +++ b/homeassistant/components/amberelectric/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "no_site": "Nenhum site fornecido" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/amberelectric/translations/tr.json b/homeassistant/components/amberelectric/translations/tr.json index 393b2cf08ee..ff8c80d8f51 100644 --- a/homeassistant/components/amberelectric/translations/tr.json +++ b/homeassistant/components/amberelectric/translations/tr.json @@ -1,5 +1,10 @@ { "config": { + "error": { + "invalid_api_token": "Ge\u00e7ersiz API anahtar\u0131", + "no_site": "Site sa\u011flanmad\u0131", + "unknown_error": "Beklenmeyen hata" + }, "step": { "site": { "data": { diff --git a/homeassistant/components/automation/translations/pt.json b/homeassistant/components/automation/translations/pt.json index 447658433e5..b406550ac45 100644 --- a/homeassistant/components/automation/translations/pt.json +++ b/homeassistant/components/automation/translations/pt.json @@ -1,4 +1,17 @@ { + "issues": { + "service_not_found": { + "fix_flow": { + "step": { + "confirm": { + "description": "A automa\u00e7\u00e3o \" {name} \" (` {entity_id} `) tem uma a\u00e7\u00e3o que chama um servi\u00e7o desconhecido: ` {service} `. \n\n Este erro impede que a automa\u00e7\u00e3o seja executada corretamente. Talvez este servi\u00e7o n\u00e3o esteja mais dispon\u00edvel, ou talvez um erro de digita\u00e7\u00e3o o tenha causado. \n\n Para corrigir esse erro, [edite a automa\u00e7\u00e3o]( {edit} ) e remova a a\u00e7\u00e3o que chama este servi\u00e7o. \n\n Clique em ENVIAR abaixo para confirmar que voc\u00ea corrigiu essa automa\u00e7\u00e3o.", + "title": "{name} usa um servi\u00e7o desconhecido" + } + } + }, + "title": "{name} usa um servi\u00e7o desconhecido" + } + }, "state": { "_": { "off": "Desligado", diff --git a/homeassistant/components/automation/translations/tr.json b/homeassistant/components/automation/translations/tr.json index 804b616bfae..9d324a000a1 100644 --- a/homeassistant/components/automation/translations/tr.json +++ b/homeassistant/components/automation/translations/tr.json @@ -1,4 +1,17 @@ { + "issues": { + "service_not_found": { + "fix_flow": { + "step": { + "confirm": { + "description": "\" {name} \" (` {entity_id} `) otomasyonunun bilinmeyen bir hizmeti \u00e7a\u011f\u0131ran bir eylemi var: ` {service} `. \n\n Bu hata, otomasyonun do\u011fru \u015fekilde \u00e7al\u0131\u015fmas\u0131n\u0131 engeller. Belki bu hizmet art\u0131k mevcut de\u011fildir veya belki de bir yaz\u0131m hatas\u0131 buna neden olmu\u015ftur. \n\n Bu hatay\u0131 d\u00fczeltmek i\u00e7in [otomasyonu d\u00fczenleyin]( {edit} ) ve bu hizmeti \u00e7a\u011f\u0131ran eylemi kald\u0131r\u0131n. \n\n Bu otomasyonu d\u00fczeltti\u011finizi onaylamak i\u00e7in a\u015fa\u011f\u0131daki G\u00d6NDER'e t\u0131klay\u0131n.", + "title": "{name} bilinmeyen bir hizmet kullan\u0131yor" + } + } + }, + "title": "{name} bilinmeyen bir hizmet kullan\u0131yor" + } + }, "state": { "_": { "off": "Kapal\u0131", diff --git a/homeassistant/components/awair/translations/bg.json b/homeassistant/components/awair/translations/bg.json index 1fa4dec8b6f..2c2bf0cdfd0 100644 --- a/homeassistant/components/awair/translations/bg.json +++ b/homeassistant/components/awair/translations/bg.json @@ -2,12 +2,30 @@ "config": { "abort": { "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \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" + "already_configured_account": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", + "already_configured_device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\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", + "unreachable": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, "error": { - "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430", + "unreachable": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, + "flow_title": "{model} ({device_id})", "step": { + "cloud": { + "data": { + "email": "Email" + } + }, + "discovery_confirm": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 {model} ({device_id})?" + }, + "local": { + "data": { + "host": "IP \u0430\u0434\u0440\u0435\u0441" + } + }, "local_pick": { "data": { "device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e", diff --git a/homeassistant/components/awair/translations/pt.json b/homeassistant/components/awair/translations/pt.json index c906e6f380e..7357334a827 100644 --- a/homeassistant/components/awair/translations/pt.json +++ b/homeassistant/components/awair/translations/pt.json @@ -9,7 +9,17 @@ "invalid_access_token": "Token de acesso inv\u00e1lido", "unknown": "Erro inesperado" }, + "flow_title": "{model} ( {device_id} )", "step": { + "cloud": { + "description": "Voc\u00ea deve se registrar para um token de acesso de desenvolvedor Awair em: {url}" + }, + "discovery_confirm": { + "description": "Deseja configurar {model} ( {device_id} )?" + }, + "local": { + "description": "Siga [estas instru\u00e7\u00f5es]( {url} ) sobre como ativar a API local Awair. \n\n Clique em enviar quando terminar." + }, "reauth": { "data": { "access_token": "Token de Acesso", @@ -26,6 +36,10 @@ "data": { "access_token": "Token de Acesso", "email": "Email" + }, + "menu_options": { + "cloud": "Conecte-se pela nuvem", + "local": "Conecte-se localmente (preferencial)" } } } diff --git a/homeassistant/components/awair/translations/tr.json b/homeassistant/components/awair/translations/tr.json index 8d49f985eae..87f90a564bc 100644 --- a/homeassistant/components/awair/translations/tr.json +++ b/homeassistant/components/awair/translations/tr.json @@ -56,7 +56,7 @@ "access_token": "Eri\u015fim Anahtar\u0131", "email": "E-posta" }, - "description": "Awair geli\u015ftirici eri\u015fim belirteci i\u00e7in \u015fu adresten kaydolmal\u0131s\u0131n\u0131z: https://developer.getawair.com/onboard/login", + "description": "En iyi deneyim i\u00e7in yerel se\u00e7in. Bulutu yaln\u0131zca cihaz Home Assistant ile ayn\u0131 a\u011fa ba\u011fl\u0131 de\u011filse veya eski bir cihaz\u0131n\u0131z varsa kullan\u0131n.", "menu_options": { "cloud": "Bulut \u00fczerinden ba\u011flan\u0131n", "local": "Yerel olarak ba\u011flan (tercih edilen)" diff --git a/homeassistant/components/binary_sensor/translations/pt-BR.json b/homeassistant/components/binary_sensor/translations/pt-BR.json index 63b45557623..5fd4939a015 100644 --- a/homeassistant/components/binary_sensor/translations/pt-BR.json +++ b/homeassistant/components/binary_sensor/translations/pt-BR.json @@ -94,7 +94,7 @@ "powered": "{entity_name} alimentado", "present": "{entity_name} presente", "problem": "{entity_name} come\u00e7ou a detectar problema", - "running": "{nome_da_entidade} come\u00e7ou a funcionar", + "running": "{nome_da_entidade} come\u00e7ou a executar", "smoke": "{entity_name} come\u00e7ou a detectar fuma\u00e7a", "sound": "{entity_name} come\u00e7ou a detectar som", "tampered": "{entity_name} come\u00e7ou a detectar adultera\u00e7\u00e3o", diff --git a/homeassistant/components/bluemaestro/translations/bg.json b/homeassistant/components/bluemaestro/translations/bg.json index e3525d4f0de..2ddd9134286 100644 --- a/homeassistant/components/bluemaestro/translations/bg.json +++ b/homeassistant/components/bluemaestro/translations/bg.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u043e", - "no_devices_found": "\u041d\u044f\u043c\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\u0430" + "no_devices_found": "\u041d\u044f\u043c\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\u0430", + "not_supported": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u043d\u0435 \u0441\u0435 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/bluemaestro/translations/nl.json b/homeassistant/components/bluemaestro/translations/nl.json new file mode 100644 index 00000000000..281d6feff46 --- /dev/null +++ b/homeassistant/components/bluemaestro/translations/nl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "already_in_progress": "De configuratie is momenteel al bezig", + "no_devices_found": "Geen apparaten gevonden op het netwerk", + "not_supported": "Apparaat is niet ondersteund" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Wilt u {name} instellen?" + }, + "user": { + "data": { + "address": "Apparaat" + }, + "description": "Kies een apparaat om in te stellen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluemaestro/translations/pt.json b/homeassistant/components/bluemaestro/translations/pt.json new file mode 100644 index 00000000000..5a10362e52b --- /dev/null +++ b/homeassistant/components/bluemaestro/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "not_supported": "Dispositivo n\u00e3o suportado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluemaestro/translations/tr.json b/homeassistant/components/bluemaestro/translations/tr.json new file mode 100644 index 00000000000..f0ddbc274c9 --- /dev/null +++ b/homeassistant/components/bluemaestro/translations/tr.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "no_devices_found": "A\u011fda cihaz bulunamad\u0131", + "not_supported": "Cihaz desteklenmiyor" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "{name} kurulumunu yapmak istiyor musunuz?" + }, + "user": { + "data": { + "address": "Cihaz" + }, + "description": "Kurulum i\u00e7in bir cihaz se\u00e7in" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/tr.json b/homeassistant/components/bluetooth/translations/tr.json index e2286fcd122..2ffd8c80814 100644 --- a/homeassistant/components/bluetooth/translations/tr.json +++ b/homeassistant/components/bluetooth/translations/tr.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", - "no_adapters": "Bluetooth adapt\u00f6r\u00fc bulunamad\u0131" + "no_adapters": "Yap\u0131land\u0131r\u0131lmam\u0131\u015f Bluetooth adapt\u00f6r\u00fc bulunamad\u0131" }, "flow_title": "{name}", "step": { @@ -33,8 +33,10 @@ "step": { "init": { "data": { - "adapter": "Tarama i\u00e7in kullan\u0131lacak Bluetooth Adapt\u00f6r\u00fc" - } + "adapter": "Tarama i\u00e7in kullan\u0131lacak Bluetooth Adapt\u00f6r\u00fc", + "passive": "Pasif tarama" + }, + "description": "Pasif dinleme, BlueZ 5.63 veya daha yenisini ve deneysel \u00f6zelliklerin etkinle\u015ftirilmesini gerektirir." } } } diff --git a/homeassistant/components/bthome/translations/pt.json b/homeassistant/components/bthome/translations/pt.json new file mode 100644 index 00000000000..ee54701d78c --- /dev/null +++ b/homeassistant/components/bthome/translations/pt.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "decryption_failed": "A chave de liga\u00e7\u00e3o fornecida n\u00e3o funcionou, os dados do sensor n\u00e3o puderam ser descriptografados. Por favor verifique e tente novamente.", + "expected_32_characters": "Esperava-se uma chave de liga\u00e7\u00e3o hexadecimal de 32 caracteres." + }, + "step": { + "get_encryption_key": { + "data": { + "bindkey": "Chave de liga\u00e7\u00e3o" + }, + "description": "Os dados do sensor transmitidos pelo sensor s\u00e3o criptografados. Para decifr\u00e1-lo, precisamos de uma chave de liga\u00e7\u00e3o hexadecimal de 32 caracteres." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bthome/translations/tr.json b/homeassistant/components/bthome/translations/tr.json new file mode 100644 index 00000000000..48b91fe6932 --- /dev/null +++ b/homeassistant/components/bthome/translations/tr.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "no_devices_found": "A\u011fda cihaz bulunamad\u0131", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + }, + "error": { + "decryption_failed": "Sa\u011flanan ba\u011flama anahtar\u0131 \u00e7al\u0131\u015fmad\u0131, sens\u00f6r verilerinin \u015fifresi \u00e7\u00f6z\u00fclemedi. L\u00fctfen kontrol edin ve tekrar deneyin.", + "expected_32_characters": "32 karakterlik onalt\u0131l\u0131k bir ba\u011flama anahtar\u0131 bekleniyor." + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "{name} kurulumunu yapmak istiyor musunuz?" + }, + "get_encryption_key": { + "data": { + "bindkey": "Bindkey" + }, + "description": "Sens\u00f6r taraf\u0131ndan yay\u0131nlanan sens\u00f6r verileri \u015fifrelenmi\u015ftir. \u015eifreyi \u00e7\u00f6zmek i\u00e7in 32 karakterlik onalt\u0131l\u0131k bir ba\u011flama anahtar\u0131na ihtiyac\u0131m\u0131z var." + }, + "user": { + "data": { + "address": "Cihaz" + }, + "description": "Kurulum i\u00e7in bir cihaz se\u00e7in" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/translations/pt.json b/homeassistant/components/demo/translations/pt.json index 7d9ee992b39..b0c39f31567 100644 --- a/homeassistant/components/demo/translations/pt.json +++ b/homeassistant/components/demo/translations/pt.json @@ -1,4 +1,17 @@ { + "issues": { + "bad_psu": { + "fix_flow": { + "step": { + "confirm": { + "description": "Pressione ENVIAR para confirmar que a fonte de alimenta\u00e7\u00e3o foi substitu\u00edda", + "title": "A fonte de alimenta\u00e7\u00e3o precisa ser substitu\u00edda" + } + } + }, + "title": "A fonte de alimenta\u00e7\u00e3o n\u00e3o \u00e9 est\u00e1vel" + } + }, "options": { "step": { "options_1": { diff --git a/homeassistant/components/ecowitt/translations/bg.json b/homeassistant/components/ecowitt/translations/bg.json index 5d274ec2b73..eb32d7e9e6e 100644 --- a/homeassistant/components/ecowitt/translations/bg.json +++ b/homeassistant/components/ecowitt/translations/bg.json @@ -2,6 +2,11 @@ "config": { "error": { "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "description": "\u0421\u0438\u0433\u0443\u0440\u043d\u0438 \u043b\u0438 \u0441\u0442\u0435, \u0447\u0435 \u0438\u0441\u043a\u0430\u0442\u0435 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 Ecowitt?" + } } } } \ No newline at end of file diff --git a/homeassistant/components/ecowitt/translations/pt.json b/homeassistant/components/ecowitt/translations/pt.json new file mode 100644 index 00000000000..71a66816a83 --- /dev/null +++ b/homeassistant/components/ecowitt/translations/pt.json @@ -0,0 +1,19 @@ +{ + "config": { + "create_entry": { + "default": "Para finalizar a configura\u00e7\u00e3o da integra\u00e7\u00e3o, use o Ecowitt App (no seu telefone) ou acesse o Ecowitt WebUI em um navegador no endere\u00e7o IP da esta\u00e7\u00e3o. \n\n Escolha sua esta\u00e7\u00e3o - > Menu Outros - > Servidores de Upload DIY. Clique em pr\u00f3ximo e selecione 'Personalizado' \n\n - IP do servidor: ` {server} `\n - Caminho: ` {path} `\n - Porta: ` {port} ` \n\n Clique em 'Salvar'." + }, + "error": { + "invalid_port": "A porta j\u00e1 \u00e9 usada." + }, + "step": { + "user": { + "data": { + "path": "Caminho com token de seguran\u00e7a", + "port": "Porta de escuta" + }, + "description": "Tem certeza de que deseja configurar o Ecowitt?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecowitt/translations/tr.json b/homeassistant/components/ecowitt/translations/tr.json new file mode 100644 index 00000000000..8e4d6906e4b --- /dev/null +++ b/homeassistant/components/ecowitt/translations/tr.json @@ -0,0 +1,20 @@ +{ + "config": { + "create_entry": { + "default": "Entegrasyon kurulumunu tamamlamak i\u00e7in Ecowitt Uygulamas\u0131n\u0131 (telefonunuzda) kullan\u0131n veya istasyonun IP adresindeki bir taray\u0131c\u0131da Ecowitt WebUI'ye eri\u015fin. \n\n \u0130stasyonunuzu se\u00e7in - > Men\u00fc Di\u011ferleri - > Kendin Yap Y\u00fckleme Sunucular\u0131. \u0130leri'ye bas\u0131n ve '\u00d6zelle\u015ftirilmi\u015f'i se\u00e7in \n\n - Sunucu IP'si: ` {server} `\n - Yol: ` {path} `\n - Ba\u011flant\u0131 noktas\u0131: ` {port} ` \n\n 'Kaydet'e t\u0131klay\u0131n." + }, + "error": { + "invalid_port": "Ba\u011flant\u0131 noktas\u0131 zaten kullan\u0131l\u0131yor.", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "path": "G\u00fcvenlik anahtar\u0131 i\u00e7eren yol", + "port": "Dinleme ba\u011flant\u0131 noktas\u0131" + }, + "description": "Ecowitt'i kurmak istedi\u011finizden emin misiniz?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/escea/translations/pt.json b/homeassistant/components/escea/translations/pt.json new file mode 100644 index 00000000000..3650c239009 --- /dev/null +++ b/homeassistant/components/escea/translations/pt.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "confirm": { + "description": "Quer montar uma lareira Escea?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fibaro/translations/et.json b/homeassistant/components/fibaro/translations/et.json index d9f140f8380..fa24f26df33 100644 --- a/homeassistant/components/fibaro/translations/et.json +++ b/homeassistant/components/fibaro/translations/et.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "error": { "cannot_connect": "\u00dchendamine nurjus", @@ -9,6 +10,13 @@ "unknown": "Ootamatu t\u00f5rge" }, "step": { + "reauth_confirm": { + "data": { + "password": "Salas\u00f5na" + }, + "description": "Uuenda kasutaja {username} salas\u00f5na", + "title": "Taastuvasta sidumine" + }, "user": { "data": { "import_plugins": "Kas importida olemid fibaro pistikprogrammidest?", diff --git a/homeassistant/components/fibaro/translations/nl.json b/homeassistant/components/fibaro/translations/nl.json index 49b73f2ac91..74cdb76596c 100644 --- a/homeassistant/components/fibaro/translations/nl.json +++ b/homeassistant/components/fibaro/translations/nl.json @@ -14,7 +14,8 @@ "data": { "password": "Wachtwoord" }, - "description": "Update je wachtwoord voor {username}" + "description": "Update je wachtwoord voor {username}", + "title": "Integratie herauthenticeren" }, "user": { "data": { diff --git a/homeassistant/components/fibaro/translations/pt.json b/homeassistant/components/fibaro/translations/pt.json index db0e0c2a137..f98b7b7b7b3 100644 --- a/homeassistant/components/fibaro/translations/pt.json +++ b/homeassistant/components/fibaro/translations/pt.json @@ -5,6 +5,11 @@ }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "reauth_confirm": { + "description": "Atualize sua senha para {username}" + } } } } \ No newline at end of file diff --git a/homeassistant/components/fibaro/translations/tr.json b/homeassistant/components/fibaro/translations/tr.json index c873e0dafd3..c4ca357b4bb 100644 --- a/homeassistant/components/fibaro/translations/tr.json +++ b/homeassistant/components/fibaro/translations/tr.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", @@ -9,6 +10,13 @@ "unknown": "Beklenmeyen hata" }, "step": { + "reauth_confirm": { + "data": { + "password": "Parola" + }, + "description": "L\u00fctfen {username} i\u00e7in \u015fifrenizi g\u00fcncelleyin", + "title": "Entegrasyonu Yeniden Do\u011frula" + }, "user": { "data": { "import_plugins": "Varl\u0131klar\u0131 fibaro eklentilerinden i\u00e7e aktar\u0131ls\u0131n m\u0131?", diff --git a/homeassistant/components/guardian/translations/pt.json b/homeassistant/components/guardian/translations/pt.json index 91def9afb9d..04a5519b895 100644 --- a/homeassistant/components/guardian/translations/pt.json +++ b/homeassistant/components/guardian/translations/pt.json @@ -13,5 +13,18 @@ } } } + }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Atualize quaisquer automa\u00e7\u00f5es ou scripts que usam este servi\u00e7o para usar o servi\u00e7o ` {alternate_service} ` com um ID de entidade de destino de ` {alternate_target} `. Em seguida, clique em ENVIAR abaixo para marcar este problema como resolvido.", + "title": "O servi\u00e7o {deprecated_service} est\u00e1 sendo removido" + } + } + }, + "title": "O servi\u00e7o {deprecated_service} est\u00e1 sendo removido" + } } } \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/sensor.pt.json b/homeassistant/components/homekit_controller/translations/sensor.pt.json new file mode 100644 index 00000000000..077ff565388 --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/sensor.pt.json @@ -0,0 +1,10 @@ +{ + "state": { + "homekit_controller__thread_node_capabilities": { + "border_router_capable": "Capacidade de roteador de borda", + "full": "Dispositivo final completo", + "minimal": "Dispositivo final m\u00ednimo", + "none": "Nenhum" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/translations/tr.json b/homeassistant/components/hue/translations/tr.json index df6d4e247a9..9e423bd3815 100644 --- a/homeassistant/components/hue/translations/tr.json +++ b/homeassistant/components/hue/translations/tr.json @@ -55,15 +55,15 @@ }, "trigger_type": { "double_short_release": "Her iki \"{subtype}\" de b\u0131rak\u0131ld\u0131", - "initial_press": "Ba\u015flang\u0131\u00e7ta \" {subtype} \" d\u00fc\u011fmesine bas\u0131ld\u0131", - "long_release": "\" {subtype} \" d\u00fc\u011fmesi uzun bas\u0131ld\u0131ktan sonra b\u0131rak\u0131ld\u0131", - "remote_button_long_release": "\" {subtype} \" d\u00fc\u011fmesi uzun bas\u0131ld\u0131ktan sonra b\u0131rak\u0131ld\u0131", - "remote_button_short_press": "\" {subtype} \" d\u00fc\u011fmesine bas\u0131ld\u0131", - "remote_button_short_release": "\" {subtype} \" d\u00fc\u011fmesi b\u0131rak\u0131ld\u0131", + "initial_press": "\" {subtype} \" ba\u015flang\u0131\u00e7ta bas\u0131ld\u0131", + "long_release": "\"{subtype}\" uzun s\u00fcren bask\u0131lar\u0131n ard\u0131ndan yay\u0131nland\u0131", + "remote_button_long_release": "\" {subtype} \" uzun bas\u0131\u015ftan sonra \u00e7\u0131kt\u0131", + "remote_button_short_press": "\" {subtype} \" bas\u0131ld\u0131", + "remote_button_short_release": "\" {subtype} \" yay\u0131nland\u0131", "remote_double_button_long_press": "Her iki \" {subtype} \" uzun bas\u0131\u015ftan sonra b\u0131rak\u0131ld\u0131", "remote_double_button_short_press": "Her iki \"{subtype}\" de b\u0131rak\u0131ld\u0131", - "repeat": "\" {subtype} \" d\u00fc\u011fmesi bas\u0131l\u0131 tutuldu", - "short_release": "K\u0131sa bas\u0131ld\u0131ktan sonra \"{subtype}\" d\u00fc\u011fmesi b\u0131rak\u0131ld\u0131", + "repeat": "\" {subtype} \" bas\u0131l\u0131 tutuldu", + "short_release": "\" {subtype} \" k\u0131sa bas\u0131\u015ftan sonra yay\u0131nland\u0131", "start": "\" {subtype} \" ba\u015flang\u0131\u00e7ta bas\u0131ld\u0131" } }, diff --git a/homeassistant/components/icloud/translations/pt.json b/homeassistant/components/icloud/translations/pt.json index 3e8e4cce2b8..da7711298fc 100644 --- a/homeassistant/components/icloud/translations/pt.json +++ b/homeassistant/components/icloud/translations/pt.json @@ -15,6 +15,9 @@ "description": "A sua palavra-passe anteriormente introduzida para {username} j\u00e1 n\u00e3o \u00e9 v\u00e1lida. Atualize sua palavra-passe para continuar a utilizar esta integra\u00e7\u00e3o.", "title": "Reautenticar integra\u00e7\u00e3o" }, + "reauth_confirm": { + "description": "Sua senha inserida anteriormente para {username} n\u00e3o est\u00e1 mais funcionando. Atualize sua senha para continuar usando esta integra\u00e7\u00e3o." + }, "trusted_device": { "data": { "trusted_device": "Dispositivo confi\u00e1vel" diff --git a/homeassistant/components/icloud/translations/tr.json b/homeassistant/components/icloud/translations/tr.json index 0f917b132f4..e220141f24d 100644 --- a/homeassistant/components/icloud/translations/tr.json +++ b/homeassistant/components/icloud/translations/tr.json @@ -18,6 +18,13 @@ "description": "{username} i\u00e7in \u00f6nceden girdi\u011finiz \u015fifreniz art\u0131k \u00e7al\u0131\u015fm\u0131yor. Bu entegrasyonu kullanmaya devam etmek i\u00e7in \u015fifrenizi g\u00fcncelleyin.", "title": "Entegrasyonu Yeniden Do\u011frula" }, + "reauth_confirm": { + "data": { + "password": "Parola" + }, + "description": "{username} i\u00e7in \u00f6nceden girdi\u011finiz \u015fifre art\u0131k \u00e7al\u0131\u015fm\u0131yor. Bu entegrasyonu kullanmaya devam etmek i\u00e7in \u015fifrenizi g\u00fcncelleyin.", + "title": "Entegrasyonu Yeniden Do\u011frula" + }, "trusted_device": { "data": { "trusted_device": "G\u00fcvenilir ayg\u0131t" diff --git a/homeassistant/components/justnimbus/translations/pt.json b/homeassistant/components/justnimbus/translations/pt.json new file mode 100644 index 00000000000..8df0a5b68e3 --- /dev/null +++ b/homeassistant/components/justnimbus/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "client_id": "ID do Cliente" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lametric/translations/pt.json b/homeassistant/components/lametric/translations/pt.json new file mode 100644 index 00000000000..ca715b2e6e2 --- /dev/null +++ b/homeassistant/components/lametric/translations/pt.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "invalid_discovery_info": "Informa\u00e7\u00f5es de descoberta inv\u00e1lidas recebidas", + "link_local_address": "Endere\u00e7os locais de link n\u00e3o s\u00e3o suportados", + "missing_configuration": "A integra\u00e7\u00e3o LaMetric n\u00e3o est\u00e1 configurada. Por favor, siga a documenta\u00e7\u00e3o.", + "no_devices": "O usu\u00e1rio autorizado n\u00e3o possui dispositivos LaMetric" + }, + "step": { + "choice_enter_manual_or_fetch_cloud": { + "description": "Um dispositivo LaMetric pode ser configurado no Home Assistant de duas maneiras diferentes. \n\n Voc\u00ea mesmo pode inserir todas as informa\u00e7\u00f5es do dispositivo e tokens de API, ou o Home Assistant pode import\u00e1-los de sua conta LaMetric.com.", + "menu_options": { + "manual_entry": "Entrar manualmente", + "pick_implementation": "Importar do LaMetric.com (recomendado)" + } + }, + "manual_entry": { + "data_description": { + "api_key": "Voc\u00ea pode encontrar essa chave de API em [p\u00e1gina de dispositivos em sua conta de desenvolvedor LaMetric](https://developer.lametric.com/user/devices).", + "host": "O endere\u00e7o IP ou nome de host do seu LaMetric TIME em sua rede." + } + }, + "user_cloud_select_device": { + "data": { + "device": "Selecione o dispositivo LaMetric para adicionar" + } + } + } + }, + "issues": { + "manual_migration": { + "description": "A integra\u00e7\u00e3o LaMetric foi modernizada: agora est\u00e1 configurada e configurada atrav\u00e9s da interface do usu\u00e1rio e as comunica\u00e7\u00f5es agora s\u00e3o locais. \n\n Infelizmente, n\u00e3o h\u00e1 caminho de migra\u00e7\u00e3o autom\u00e1tica poss\u00edvel e, portanto, exige que voc\u00ea reconfigure seu LaMetric com o Home Assistant. Consulte a documenta\u00e7\u00e3o de integra\u00e7\u00e3o do Home Assistant LaMetric sobre como configur\u00e1-lo. \n\n Remova a configura\u00e7\u00e3o antiga do LaMetric YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "Migra\u00e7\u00e3o manual necess\u00e1ria para LaMetric" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/led_ble/translations/pt.json b/homeassistant/components/led_ble/translations/pt.json new file mode 100644 index 00000000000..c7268106706 --- /dev/null +++ b/homeassistant/components/led_ble/translations/pt.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_unconfigured_devices": "Nenhum dispositivo n\u00e3o configurado encontrado.", + "not_supported": "Dispositivo n\u00e3o suportado" + }, + "step": { + "user": { + "data": { + "address": "Endere\u00e7o Bluetooth" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/led_ble/translations/tr.json b/homeassistant/components/led_ble/translations/tr.json new file mode 100644 index 00000000000..f9755124974 --- /dev/null +++ b/homeassistant/components/led_ble/translations/tr.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "no_devices_found": "A\u011fda cihaz bulunamad\u0131", + "no_unconfigured_devices": "Yap\u0131land\u0131r\u0131lmam\u0131\u015f cihaz bulunamad\u0131.", + "not_supported": "Cihaz desteklenmiyor" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unknown": "Beklenmeyen hata" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "Bluetooth adresi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/tr.json b/homeassistant/components/litterrobot/translations/tr.json index a83e1936fb4..193413280eb 100644 --- a/homeassistant/components/litterrobot/translations/tr.json +++ b/homeassistant/components/litterrobot/translations/tr.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", @@ -9,6 +10,13 @@ "unknown": "Beklenmeyen hata" }, "step": { + "reauth_confirm": { + "data": { + "password": "Parola" + }, + "description": "L\u00fctfen {username} i\u00e7in \u015fifrenizi g\u00fcncelleyin", + "title": "Entegrasyonu Yeniden Do\u011frula" + }, "user": { "data": { "password": "Parola", diff --git a/homeassistant/components/melnor/translations/pt.json b/homeassistant/components/melnor/translations/pt.json new file mode 100644 index 00000000000..2c44c3380bc --- /dev/null +++ b/homeassistant/components/melnor/translations/pt.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "N\u00e3o existem dispositivos Bluetooth Melnor nas proximidades." + }, + "step": { + "bluetooth_confirm": { + "description": "Deseja adicionar a v\u00e1lvula Melnor Bluetooth ` {name} ` ao Home Assistant?", + "title": "V\u00e1lvula Bluetooth Melnor descoberta" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/melnor/translations/tr.json b/homeassistant/components/melnor/translations/tr.json new file mode 100644 index 00000000000..7c99d8ab655 --- /dev/null +++ b/homeassistant/components/melnor/translations/tr.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Yak\u0131nlarda Melnor Bluetooth cihaz\u0131 yok." + }, + "step": { + "bluetooth_confirm": { + "description": "Home Assistant'a Melnor Bluetooth valfi ` {name} ` eklemek ister misiniz?", + "title": "Melnor Bluetooth valfi ke\u015ffedildi" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/tr.json b/homeassistant/components/mqtt/translations/tr.json index d00aaf1c06b..eda134d8220 100644 --- a/homeassistant/components/mqtt/translations/tr.json +++ b/homeassistant/components/mqtt/translations/tr.json @@ -49,6 +49,12 @@ "button_triple_press": "\" {subtype} \" \u00fc\u00e7 kez t\u0131kland\u0131" } }, + "issues": { + "deprecated_yaml": { + "description": "El ile yap\u0131land\u0131r\u0131lm\u0131\u015f MQTT {platform} (lar) ` {platform} ` platform anahtar\u0131 alt\u0131nda bulundu. \n\n Bu sorunu gidermek i\u00e7in l\u00fctfen yap\u0131land\u0131rmay\u0131 `mqtt` entegrasyon anahtar\u0131na ta\u015f\u0131y\u0131n ve Home Assistant'\u0131 yeniden ba\u015flat\u0131n. Daha fazla bilgi i\u00e7in [belgelere]( {more_info_url} ) bak\u0131n.", + "title": "Manuel olarak yap\u0131land\u0131r\u0131lan MQTT {platform} (lar)\u0131n\u0131zla ilgilenilmesi gerekiyor" + } + }, "options": { "error": { "bad_birth": "Ge\u00e7ersiz do\u011fum konusu.", diff --git a/homeassistant/components/mysensors/translations/pt.json b/homeassistant/components/mysensors/translations/pt.json index 3ace45dd942..2eb65a87447 100644 --- a/homeassistant/components/mysensors/translations/pt.json +++ b/homeassistant/components/mysensors/translations/pt.json @@ -2,7 +2,18 @@ "config": { "abort": { "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "mqtt_required": "A integra\u00e7\u00e3o do MQTT n\u00e3o est\u00e1 configurada", "unknown": "Erro inesperado" + }, + "step": { + "select_gateway_type": { + "description": "Selecione qual gateway configurar.", + "menu_options": { + "gw_mqtt": "Configurar um gateway MQTT", + "gw_serial": "Configurar um gateway serial", + "gw_tcp": "Configurar um gateway TCP" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/nam/translations/sensor.pt.json b/homeassistant/components/nam/translations/sensor.pt.json new file mode 100644 index 00000000000..a46b60abec3 --- /dev/null +++ b/homeassistant/components/nam/translations/sensor.pt.json @@ -0,0 +1,7 @@ +{ + "state": { + "nam__caqi_level": { + "high": "Alto" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nam/translations/sensor.tr.json b/homeassistant/components/nam/translations/sensor.tr.json new file mode 100644 index 00000000000..d271db62efe --- /dev/null +++ b/homeassistant/components/nam/translations/sensor.tr.json @@ -0,0 +1,11 @@ +{ + "state": { + "nam__caqi_level": { + "high": "Y\u00fcksek", + "low": "D\u00fc\u015f\u00fck", + "medium": "Orta", + "very high": "\u00c7ok y\u00fcksek", + "very low": "\u00c7ok d\u00fc\u015f\u00fck" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/translations/pt.json b/homeassistant/components/nest/translations/pt.json index 40b70a2c67f..4e5c4c2c34a 100644 --- a/homeassistant/components/nest/translations/pt.json +++ b/homeassistant/components/nest/translations/pt.json @@ -50,5 +50,15 @@ "trigger_type": { "camera_motion": "Movimento detectado" } + }, + "issues": { + "deprecated_yaml": { + "description": "A configura\u00e7\u00e3o do Nest em configuration.yaml est\u00e1 sendo removida no Home Assistant 2022.10. \n\n Suas credenciais de aplicativo OAuth e configura\u00e7\u00f5es de acesso existentes foram importadas para a interface do usu\u00e1rio automaticamente. Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o Nest YAML est\u00e1 sendo removida" + }, + "removed_app_auth": { + "description": "Para melhorar a seguran\u00e7a e reduzir o risco de phishing, o Google desativou o m\u00e9todo de autentica\u00e7\u00e3o usado pelo Home Assistant. \n\n **Isso requer uma a\u00e7\u00e3o sua para resolver** ([mais informa\u00e7\u00f5es]( {more_info_url} )) \n\n 1. Visite a p\u00e1gina de integra\u00e7\u00f5es\n 1. Clique em Reconfigurar na integra\u00e7\u00e3o Nest.\n 1. O Home Assistant o guiar\u00e1 pelas etapas para atualizar para a autentica\u00e7\u00e3o da Web. \n\n Consulte as [instru\u00e7\u00f5es de integra\u00e7\u00e3o]( {documentation_url} ) do Nest para obter informa\u00e7\u00f5es sobre solu\u00e7\u00e3o de problemas.", + "title": "As credenciais de autentica\u00e7\u00e3o Nest precisam ser atualizadas" + } } } \ No newline at end of file diff --git a/homeassistant/components/nobo_hub/translations/nl.json b/homeassistant/components/nobo_hub/translations/nl.json new file mode 100644 index 00000000000..dde5dfc5ad1 --- /dev/null +++ b/homeassistant/components/nobo_hub/translations/nl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "unknown": "Onverwachte fout" + }, + "step": { + "manual": { + "data": { + "ip_address": "IP-adres", + "serial": "Serienummer (12 cijfers)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nobo_hub/translations/pt.json b/homeassistant/components/nobo_hub/translations/pt.json new file mode 100644 index 00000000000..0749b1fe831 --- /dev/null +++ b/homeassistant/components/nobo_hub/translations/pt.json @@ -0,0 +1,39 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha ao conectar - verifique o n\u00famero de s\u00e9rie", + "invalid_ip": "Endere\u00e7o IP inv\u00e1lido", + "invalid_serial": "N\u00famero de s\u00e9rie inv\u00e1lido" + }, + "step": { + "manual": { + "data": { + "serial": "N\u00famero de s\u00e9rie (12 d\u00edgitos)" + }, + "description": "Configure um Nob\u00f8 Ecohub n\u00e3o descoberto em sua rede local. Se o seu hub estiver em outra rede, voc\u00ea ainda poder\u00e1 se conectar a ele digitando o n\u00famero de s\u00e9rie completo (12 d\u00edgitos) e seu endere\u00e7o IP." + }, + "selected": { + "data": { + "serial_suffix": "Sufixo do n\u00famero de s\u00e9rie (3 d\u00edgitos)" + }, + "description": "Configurando {hub} . Para se conectar ao hub, voc\u00ea precisa inserir os 3 \u00faltimos d\u00edgitos do n\u00famero de s\u00e9rie do hub." + }, + "user": { + "data": { + "device": "Hubs descobertos" + }, + "description": "Selecione Nob\u00f8 Ecohub para configurar." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "override_type": "Tipo de substitui\u00e7\u00e3o" + }, + "description": "Selecione o tipo de substitui\u00e7\u00e3o \"Agora\" para encerrar a substitui\u00e7\u00e3o na pr\u00f3xima semana de altera\u00e7\u00e3o de perfil." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nobo_hub/translations/tr.json b/homeassistant/components/nobo_hub/translations/tr.json new file mode 100644 index 00000000000..8dda9b88c76 --- /dev/null +++ b/homeassistant/components/nobo_hub/translations/tr.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flant\u0131 kurulamad\u0131 - seri numaras\u0131n\u0131 kontrol edin", + "invalid_ip": "Ge\u00e7ersiz IP adresi", + "invalid_serial": "Ge\u00e7ersiz seri numaras\u0131", + "unknown": "Beklenmeyen hata" + }, + "step": { + "manual": { + "data": { + "ip_address": "IP Adresi", + "serial": "Seri numaras\u0131 (12 haneli)" + }, + "description": "Yerel a\u011f\u0131n\u0131zda ke\u015ffedilmemi\u015f bir Nob\u00f8 Ecohub yap\u0131land\u0131r\u0131n. Hub'\u0131n\u0131z ba\u015fka bir a\u011fdaysa, tam seri numaras\u0131n\u0131 (12 haneli) ve IP adresini girerek yine de hub'a ba\u011flanabilirsiniz." + }, + "selected": { + "data": { + "serial_suffix": "Seri numaras\u0131 son eki (3 basamak)" + }, + "description": "{hub} yap\u0131land\u0131r\u0131l\u0131yor. Hub'a ba\u011flanmak i\u00e7in hub'\u0131n seri numaras\u0131n\u0131n son 3 hanesini girmeniz gerekir." + }, + "user": { + "data": { + "device": "Ke\u015ffedilen hub'lar" + }, + "description": "Yap\u0131land\u0131rmak i\u00e7in Nob\u00f8 Ecohub \u00f6\u011fesini se\u00e7in." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "override_type": "Ge\u00e7ersiz k\u0131lma t\u00fcr\u00fc" + }, + "description": "Sonraki hafta profil de\u011fi\u015fikli\u011finde ge\u00e7ersiz k\u0131lmay\u0131 sonland\u0131rmak i\u00e7in \"\u015eimdi\" ge\u00e7ersiz k\u0131lma t\u00fcr\u00fcn\u00fc se\u00e7in." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openexchangerates/translations/pt.json b/homeassistant/components/openexchangerates/translations/pt.json new file mode 100644 index 00000000000..1da8a0cc5ab --- /dev/null +++ b/homeassistant/components/openexchangerates/translations/pt.json @@ -0,0 +1,20 @@ +{ + "config": { + "step": { + "user": { + "data": { + "base": "Moeda base" + }, + "data_description": { + "base": "Usar outra moeda base que n\u00e3o seja USD requer um [plano pago]( {signup} )." + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "A configura\u00e7\u00e3o de taxas de c\u00e2mbio abertas 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 Open Exchange Rates YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o de YAML de taxas de c\u00e2mbio abertas est\u00e1 sendo removida" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/pt.json b/homeassistant/components/overkiz/translations/pt.json index 1e3d9138c84..5b2dd940959 100644 --- a/homeassistant/components/overkiz/translations/pt.json +++ b/homeassistant/components/overkiz/translations/pt.json @@ -1,7 +1,8 @@ { "config": { "error": { - "unknown": "Erro inesperado" + "unknown": "Erro inesperado", + "unknown_user": "Usu\u00e1rio desconhecido. As contas Somfy Protect n\u00e3o s\u00e3o suportadas por esta integra\u00e7\u00e3o." }, "step": { "user": { diff --git a/homeassistant/components/overkiz/translations/tr.json b/homeassistant/components/overkiz/translations/tr.json index 1d04fbbd3cc..3981f7dbc8c 100644 --- a/homeassistant/components/overkiz/translations/tr.json +++ b/homeassistant/components/overkiz/translations/tr.json @@ -11,7 +11,8 @@ "server_in_maintenance": "Sunucu bak\u0131m nedeniyle kapal\u0131", "too_many_attempts": "Ge\u00e7ersiz anahtarla \u00e7ok fazla deneme, ge\u00e7ici olarak yasakland\u0131", "too_many_requests": "\u00c7ok fazla istek var, daha sonra tekrar deneyin", - "unknown": "Beklenmeyen hata" + "unknown": "Beklenmeyen hata", + "unknown_user": "Bilinmeyen kullan\u0131c\u0131. Somfy Protect hesaplar\u0131 bu entegrasyon taraf\u0131ndan desteklenmez." }, "flow_title": "A\u011f ge\u00e7idi: {gateway_id}", "step": { diff --git a/homeassistant/components/p1_monitor/translations/pt.json b/homeassistant/components/p1_monitor/translations/pt.json index 38336a1d5de..ab627843537 100644 --- a/homeassistant/components/p1_monitor/translations/pt.json +++ b/homeassistant/components/p1_monitor/translations/pt.json @@ -8,6 +8,9 @@ "data": { "host": "Servidor", "name": "Nome" + }, + "data_description": { + "host": "O endere\u00e7o IP ou nome de host da instala\u00e7\u00e3o do Monitor P1." } } } diff --git a/homeassistant/components/p1_monitor/translations/tr.json b/homeassistant/components/p1_monitor/translations/tr.json index f00060462fd..1ee8c351c8d 100644 --- a/homeassistant/components/p1_monitor/translations/tr.json +++ b/homeassistant/components/p1_monitor/translations/tr.json @@ -9,6 +9,9 @@ "host": "Sunucu", "name": "Ad" }, + "data_description": { + "host": "P1 Monitor kurulumunuzun IP adresi veya ana bilgisayar ad\u0131." + }, "description": "Home Assistant ile entegre etmek i\u00e7in P1 Monitor'\u00fc kurun." } } diff --git a/homeassistant/components/prusalink/translations/pt.json b/homeassistant/components/prusalink/translations/pt.json new file mode 100644 index 00000000000..5003d44e3d9 --- /dev/null +++ b/homeassistant/components/prusalink/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "not_supported": "Apenas a API PrusaLink v2 \u00e9 suportada" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/sensor.pt.json b/homeassistant/components/prusalink/translations/sensor.pt.json new file mode 100644 index 00000000000..462b35d9722 --- /dev/null +++ b/homeassistant/components/prusalink/translations/sensor.pt.json @@ -0,0 +1,11 @@ +{ + "state": { + "prusalink__printer_state": { + "cancelling": "Cancelando", + "idle": "Ocioso", + "paused": "Pausado", + "pausing": "Pausando", + "printing": "Impress\u00e3o" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/sensor.tr.json b/homeassistant/components/prusalink/translations/sensor.tr.json new file mode 100644 index 00000000000..32dab7904bc --- /dev/null +++ b/homeassistant/components/prusalink/translations/sensor.tr.json @@ -0,0 +1,11 @@ +{ + "state": { + "prusalink__printer_state": { + "cancelling": "\u0130ptal", + "idle": "Bo\u015fta", + "paused": "Durduruldu", + "pausing": "Duraklat\u0131l\u0131yor", + "printing": "Yazd\u0131r\u0131l\u0131yor" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/tr.json b/homeassistant/components/prusalink/translations/tr.json new file mode 100644 index 00000000000..4658856d2fa --- /dev/null +++ b/homeassistant/components/prusalink/translations/tr.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "not_supported": "Yaln\u0131zca PrusaLink API v2 desteklenir", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "api_key": "API Anahtar\u0131", + "host": "Sunucu" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pure_energie/translations/pt.json b/homeassistant/components/pure_energie/translations/pt.json index ce7cbc3f548..aad646d8e8c 100644 --- a/homeassistant/components/pure_energie/translations/pt.json +++ b/homeassistant/components/pure_energie/translations/pt.json @@ -4,6 +4,9 @@ "user": { "data": { "host": "Servidor" + }, + "data_description": { + "host": "O endere\u00e7o IP ou nome de host do seu Medidor Pure Energie." } } } diff --git a/homeassistant/components/pure_energie/translations/tr.json b/homeassistant/components/pure_energie/translations/tr.json index 8c2a8402124..7af9d18c26e 100644 --- a/homeassistant/components/pure_energie/translations/tr.json +++ b/homeassistant/components/pure_energie/translations/tr.json @@ -12,6 +12,9 @@ "user": { "data": { "host": "Sunucu" + }, + "data_description": { + "host": "Pure Energie Meter cihaz\u0131n\u0131z\u0131n IP adresi veya ana bilgisayar ad\u0131." } }, "zeroconf_confirm": { diff --git a/homeassistant/components/pushover/translations/pt.json b/homeassistant/components/pushover/translations/pt.json new file mode 100644 index 00000000000..519ac06a01f --- /dev/null +++ b/homeassistant/components/pushover/translations/pt.json @@ -0,0 +1,17 @@ +{ + "config": { + "step": { + "user": { + "data": { + "user_key": "Chave do usu\u00e1rio" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "A configura\u00e7\u00e3o do Pushover 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 Pushover YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o do Pushover YAML est\u00e1 sendo removida" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qingping/translations/bg.json b/homeassistant/components/qingping/translations/bg.json index bcc9f98e10d..d7b7634c9f7 100644 --- a/homeassistant/components/qingping/translations/bg.json +++ b/homeassistant/components/qingping/translations/bg.json @@ -1,6 +1,14 @@ { "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", + "not_supported": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u043d\u0435 \u0441\u0435 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\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" diff --git a/homeassistant/components/risco/translations/pt.json b/homeassistant/components/risco/translations/pt.json index 7b98c6234c5..30b87b0a0da 100644 --- a/homeassistant/components/risco/translations/pt.json +++ b/homeassistant/components/risco/translations/pt.json @@ -14,6 +14,10 @@ "password": "Palavra-passe", "pin": "C\u00f3digo PIN", "username": "Nome de Utilizador" + }, + "menu_options": { + "cloud": "Risco Cloud (recomendado)", + "local": "Painel de Risco Local (avan\u00e7ado)" } } } diff --git a/homeassistant/components/risco/translations/tr.json b/homeassistant/components/risco/translations/tr.json index 168fc621e05..6572e50de4f 100644 --- a/homeassistant/components/risco/translations/tr.json +++ b/homeassistant/components/risco/translations/tr.json @@ -9,11 +9,29 @@ "unknown": "Beklenmeyen hata" }, "step": { + "cloud": { + "data": { + "password": "Parola", + "pin": "PIN Kodu", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + }, + "local": { + "data": { + "host": "Sunucu", + "pin": "PIN Kodu", + "port": "Port" + } + }, "user": { "data": { "password": "Parola", "pin": "PIN Kodu", "username": "Kullan\u0131c\u0131 Ad\u0131" + }, + "menu_options": { + "cloud": "Risco Cloud (\u00f6nerilir)", + "local": "Yerel Risco Paneli (geli\u015fmi\u015f)" } } } diff --git a/homeassistant/components/schedule/translations/pt.json b/homeassistant/components/schedule/translations/pt.json new file mode 100644 index 00000000000..b3fab8f544d --- /dev/null +++ b/homeassistant/components/schedule/translations/pt.json @@ -0,0 +1,3 @@ +{ + "title": "Hor\u00e1rio" +} \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/pt.json b/homeassistant/components/sensibo/translations/pt.json index 80f65d0a06d..56abfd08b05 100644 --- a/homeassistant/components/sensibo/translations/pt.json +++ b/homeassistant/components/sensibo/translations/pt.json @@ -7,11 +7,17 @@ "reauth_confirm": { "data": { "api_key": "Chave da API" + }, + "data_description": { + "api_key": "Siga a documenta\u00e7\u00e3o para obter uma nova chave de API." } }, "user": { "data": { "api_key": "Chave da API" + }, + "data_description": { + "api_key": "Siga a documenta\u00e7\u00e3o para obter sua chave de API." } } } diff --git a/homeassistant/components/sensibo/translations/tr.json b/homeassistant/components/sensibo/translations/tr.json index fbe466cfd8d..47270a1dece 100644 --- a/homeassistant/components/sensibo/translations/tr.json +++ b/homeassistant/components/sensibo/translations/tr.json @@ -15,11 +15,17 @@ "reauth_confirm": { "data": { "api_key": "API Anahtar\u0131" + }, + "data_description": { + "api_key": "Yeni bir api anahtar\u0131 almak i\u00e7in belgeleri izleyin." } }, "user": { "data": { "api_key": "API Anahtar\u0131" + }, + "data_description": { + "api_key": "API anahtar\u0131n\u0131z\u0131 almak i\u00e7in belgeleri izleyin." } } } diff --git a/homeassistant/components/sensor/translations/pt.json b/homeassistant/components/sensor/translations/pt.json index 864d49f2373..e1d91ddfcd3 100644 --- a/homeassistant/components/sensor/translations/pt.json +++ b/homeassistant/components/sensor/translations/pt.json @@ -5,6 +5,7 @@ "is_energy": "Energia atual de {entity_name}", "is_humidity": "humidade {entity_name}", "is_illuminance": "Luminancia atual de {entity_name}", + "is_moisture": "Umidade atual {entity_name}", "is_power": "Pot\u00eancia atual de {entity_name}", "is_pressure": "Press\u00e3o atual de {entity_name}", "is_signal_strength": "Intensidade atual do sinal de {entity_name}", @@ -16,6 +17,7 @@ "energy": "Mudan\u00e7as de energia de {entity_name}", "humidity": "humidade {entity_name}", "illuminance": "ilumin\u00e2ncia {entity_name}", + "moisture": "Mudan\u00e7as de umidade {entity_name}", "power": "pot\u00eancia {entity_name}", "pressure": "press\u00e3o {entity_name}", "signal_strength": "Altera\u00e7\u00e3o da intensidade do sinal de {entity_name}", diff --git a/homeassistant/components/sensor/translations/tr.json b/homeassistant/components/sensor/translations/tr.json index 1a6e54d8e81..cc7cf3f39fa 100644 --- a/homeassistant/components/sensor/translations/tr.json +++ b/homeassistant/components/sensor/translations/tr.json @@ -11,6 +11,7 @@ "is_gas": "Mevcut {entity_name} gaz\u0131", "is_humidity": "Ge\u00e7erli {entity_name} nem oran\u0131", "is_illuminance": "Mevcut {entity_name} ayd\u0131nlatma d\u00fczeyi", + "is_moisture": "Mevcut {entity_name} nemi", "is_nitrogen_dioxide": "Mevcut {entity_name} nitrojen dioksit konsantrasyon seviyesi", "is_nitrogen_monoxide": "Mevcut {entity_name} nitrojen monoksit konsantrasyon seviyesi", "is_nitrous_oxide": "Ge\u00e7erli {entity_name} azot oksit konsantrasyon seviyesi", @@ -40,6 +41,7 @@ "gas": "{entity_name} gaz de\u011fi\u015fiklikleri", "humidity": "{entity_name} nem de\u011fi\u015fiklikleri", "illuminance": "{entity_name} ayd\u0131nlatma de\u011fi\u015fiklikleri", + "moisture": "{entity_name} nem de\u011fi\u015fimleri", "nitrogen_dioxide": "{entity_name} nitrojen dioksit konsantrasyonu de\u011fi\u015fiklikleri", "nitrogen_monoxide": "{entity_name} nitrojen monoksit konsantrasyonu de\u011fi\u015fiklikleri", "nitrous_oxide": "{entity_name} nitr\u00f6z oksit konsantrasyonu de\u011fi\u015fiklikleri", diff --git a/homeassistant/components/sensorpro/translations/bg.json b/homeassistant/components/sensorpro/translations/bg.json index a61dac839ad..af9a13197df 100644 --- a/homeassistant/components/sensorpro/translations/bg.json +++ b/homeassistant/components/sensorpro/translations/bg.json @@ -2,7 +2,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", - "no_devices_found": "\u041d\u044f\u043c\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\u0430" + "no_devices_found": "\u041d\u044f\u043c\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\u0430", + "not_supported": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u043d\u0435 \u0441\u0435 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/sensorpro/translations/pt.json b/homeassistant/components/sensorpro/translations/pt.json new file mode 100644 index 00000000000..5a10362e52b --- /dev/null +++ b/homeassistant/components/sensorpro/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "not_supported": "Dispositivo n\u00e3o suportado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensorpro/translations/tr.json b/homeassistant/components/sensorpro/translations/tr.json new file mode 100644 index 00000000000..f0ddbc274c9 --- /dev/null +++ b/homeassistant/components/sensorpro/translations/tr.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "no_devices_found": "A\u011fda cihaz bulunamad\u0131", + "not_supported": "Cihaz desteklenmiyor" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "{name} kurulumunu yapmak istiyor musunuz?" + }, + "user": { + "data": { + "address": "Cihaz" + }, + "description": "Kurulum i\u00e7in bir cihaz se\u00e7in" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/pt.json b/homeassistant/components/simplepush/translations/pt.json index d7e598b33e4..67316f99a70 100644 --- a/homeassistant/components/simplepush/translations/pt.json +++ b/homeassistant/components/simplepush/translations/pt.json @@ -13,5 +13,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "A configura\u00e7\u00e3o do Simplepush 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 Simplepush YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o Simplepush YAML est\u00e1 sendo removida" + } } } \ No newline at end of file diff --git a/homeassistant/components/skybell/translations/pt.json b/homeassistant/components/skybell/translations/pt.json index 8487c8869b6..0818ea34699 100644 --- a/homeassistant/components/skybell/translations/pt.json +++ b/homeassistant/components/skybell/translations/pt.json @@ -11,5 +11,11 @@ } } } + }, + "issues": { + "removed_yaml": { + "description": "A configura\u00e7\u00e3o do Skybell usando YAML foi removida. \n\n Sua configura\u00e7\u00e3o YAML existente n\u00e3o \u00e9 usada pelo Home Assistant. \n\n Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o YAML do Skybell foi removida" + } } } \ No newline at end of file diff --git a/homeassistant/components/skybell/translations/tr.json b/homeassistant/components/skybell/translations/tr.json index 23e8cb87551..6609af82219 100644 --- a/homeassistant/components/skybell/translations/tr.json +++ b/homeassistant/components/skybell/translations/tr.json @@ -10,6 +10,13 @@ "unknown": "Beklenmeyen hata" }, "step": { + "reauth_confirm": { + "data": { + "password": "Parola" + }, + "description": "L\u00fctfen {email} \u015fifrenizi g\u00fcncelleyin", + "title": "Entegrasyonu Yeniden Do\u011frula" + }, "user": { "data": { "email": "E-posta", diff --git a/homeassistant/components/speedtestdotnet/translations/pt.json b/homeassistant/components/speedtestdotnet/translations/pt.json index c299020ce9a..b0c105531f2 100644 --- a/homeassistant/components/speedtestdotnet/translations/pt.json +++ b/homeassistant/components/speedtestdotnet/translations/pt.json @@ -8,5 +8,18 @@ "description": "Quer dar inicio \u00e0 configura\u00e7\u00e3o?" } } + }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Atualize quaisquer automa\u00e7\u00f5es ou scripts que usam este servi\u00e7o para usar o servi\u00e7o `homeassistant.update_entity` com um ID de entidade do Speedtest de destino. Em seguida, clique em ENVIAR abaixo para marcar este problema como resolvido.", + "title": "O servi\u00e7o speedtest est\u00e1 sendo removido" + } + } + }, + "title": "O servi\u00e7o speedtest est\u00e1 sendo removido" + } } } \ No newline at end of file diff --git a/homeassistant/components/speedtestdotnet/translations/tr.json b/homeassistant/components/speedtestdotnet/translations/tr.json index 5becafdf153..d096c2c264d 100644 --- a/homeassistant/components/speedtestdotnet/translations/tr.json +++ b/homeassistant/components/speedtestdotnet/translations/tr.json @@ -9,6 +9,19 @@ } } }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Bu hizmeti kullanan t\u00fcm otomasyonlar\u0131 veya komut dosyalar\u0131n\u0131, bunun yerine \"homeassistant.update_entity\" hizmetini bir hedef Speedtest entity_id ile kullanacak \u015fekilde g\u00fcncelleyin. Ard\u0131ndan, bu sorunu \u00e7\u00f6z\u00fcld\u00fc olarak i\u015faretlemek i\u00e7in a\u015fa\u011f\u0131daki G\u00d6NDER'i t\u0131klay\u0131n.", + "title": "Speedtest hizmeti kald\u0131r\u0131l\u0131yor" + } + } + }, + "title": "Speedtest hizmeti kald\u0131r\u0131l\u0131yor" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/thermobeacon/translations/pt.json b/homeassistant/components/thermobeacon/translations/pt.json new file mode 100644 index 00000000000..5a10362e52b --- /dev/null +++ b/homeassistant/components/thermobeacon/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "not_supported": "Dispositivo n\u00e3o suportado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/thermobeacon/translations/tr.json b/homeassistant/components/thermobeacon/translations/tr.json new file mode 100644 index 00000000000..f0ddbc274c9 --- /dev/null +++ b/homeassistant/components/thermobeacon/translations/tr.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "no_devices_found": "A\u011fda cihaz bulunamad\u0131", + "not_supported": "Cihaz desteklenmiyor" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "{name} kurulumunu yapmak istiyor musunuz?" + }, + "user": { + "data": { + "address": "Cihaz" + }, + "description": "Kurulum i\u00e7in bir cihaz se\u00e7in" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/thermopro/translations/tr.json b/homeassistant/components/thermopro/translations/tr.json new file mode 100644 index 00000000000..f63cee3493c --- /dev/null +++ b/homeassistant/components/thermopro/translations/tr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "no_devices_found": "A\u011fda cihaz bulunamad\u0131" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "{name} kurulumunu yapmak istiyor musunuz?" + }, + "user": { + "data": { + "address": "Cihaz" + }, + "description": "Kurulum i\u00e7in bir cihaz se\u00e7in" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tilt_ble/translations/et.json b/homeassistant/components/tilt_ble/translations/et.json new file mode 100644 index 00000000000..506a823dd6e --- /dev/null +++ b/homeassistant/components/tilt_ble/translations/et.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "already_in_progress": "H\u00e4\u00e4lestamine on juba k\u00e4imas", + "no_devices_found": "V\u00f5rgust seadmeid ei leitud" + }, + "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/tilt_ble/translations/nl.json b/homeassistant/components/tilt_ble/translations/nl.json index 10710b9d955..a46f954fe5f 100644 --- a/homeassistant/components/tilt_ble/translations/nl.json +++ b/homeassistant/components/tilt_ble/translations/nl.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Apparaat is al geconfigureerd", "already_in_progress": "De configuratie is momenteel al bezig", "no_devices_found": "Geen apparaten gevonden op het netwerk" }, diff --git a/homeassistant/components/tilt_ble/translations/tr.json b/homeassistant/components/tilt_ble/translations/tr.json new file mode 100644 index 00000000000..f63cee3493c --- /dev/null +++ b/homeassistant/components/tilt_ble/translations/tr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "no_devices_found": "A\u011fda cihaz bulunamad\u0131" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "{name} kurulumunu yapmak istiyor musunuz?" + }, + "user": { + "data": { + "address": "Cihaz" + }, + "description": "Kurulum i\u00e7in bir cihaz se\u00e7in" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifiprotect/translations/pt.json b/homeassistant/components/unifiprotect/translations/pt.json index 9f781c2de55..bd20918e494 100644 --- a/homeassistant/components/unifiprotect/translations/pt.json +++ b/homeassistant/components/unifiprotect/translations/pt.json @@ -22,5 +22,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "max_media": "N\u00famero m\u00e1ximo de eventos a serem carregados para o Media Browser (aumenta o uso de RAM)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/unifiprotect/translations/tr.json b/homeassistant/components/unifiprotect/translations/tr.json index d26f6af41ce..65a8c52f368 100644 --- a/homeassistant/components/unifiprotect/translations/tr.json +++ b/homeassistant/components/unifiprotect/translations/tr.json @@ -42,11 +42,15 @@ } }, "options": { + "error": { + "invalid_mac_list": "Virg\u00fclle ayr\u0131lm\u0131\u015f bir MAC adresleri listesi olmal\u0131d\u0131r" + }, "step": { "init": { "data": { "all_updates": "Ger\u00e7ek zamanl\u0131 \u00f6l\u00e7\u00fcmler (UYARI: CPU kullan\u0131m\u0131n\u0131 b\u00fcy\u00fck \u00f6l\u00e7\u00fcde art\u0131r\u0131r)", "disable_rtsp": "RTSP ak\u0131\u015f\u0131n\u0131 devre d\u0131\u015f\u0131 b\u0131rak\u0131n", + "ignored_devices": "Yok say\u0131lacak ayg\u0131tlar\u0131n MAC adreslerinin virg\u00fclle ayr\u0131lm\u0131\u015f listesi", "max_media": "Medya Taray\u0131c\u0131 i\u00e7in y\u00fcklenecek maksimum olay say\u0131s\u0131 (RAM kullan\u0131m\u0131n\u0131 art\u0131r\u0131r)", "override_connection_host": "Ba\u011flant\u0131 Ana Bilgisayar\u0131n\u0131 Ge\u00e7ersiz K\u0131l" }, diff --git a/homeassistant/components/volvooncall/translations/nl.json b/homeassistant/components/volvooncall/translations/nl.json index f0b4ddf59a9..1e942c8694c 100644 --- a/homeassistant/components/volvooncall/translations/nl.json +++ b/homeassistant/components/volvooncall/translations/nl.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "reauth_successful": "Herauthenticatie geslaagd" + }, "error": { "invalid_auth": "Ongeldige authenticatie", "unknown": "Onverwachte fout" diff --git a/homeassistant/components/volvooncall/translations/pt-BR.json b/homeassistant/components/volvooncall/translations/pt-BR.json index 515ff7d3f31..be90fa861e4 100644 --- a/homeassistant/components/volvooncall/translations/pt-BR.json +++ b/homeassistant/components/volvooncall/translations/pt-BR.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "A conta j\u00e1 est\u00e1 configurada", + "already_configured": "A conta j\u00e1 foi configurada", "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" }, "error": { diff --git a/homeassistant/components/volvooncall/translations/pt.json b/homeassistant/components/volvooncall/translations/pt.json new file mode 100644 index 00000000000..232da9d130d --- /dev/null +++ b/homeassistant/components/volvooncall/translations/pt.json @@ -0,0 +1,19 @@ +{ + "config": { + "step": { + "user": { + "data": { + "mutable": "Permitir Partida/Bloqueio Remoto/etc.", + "region": "Regi\u00e3o", + "scandinavian_miles": "Use milhas escandinavas" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "A configura\u00e7\u00e3o da plataforma Volvo On Call usando YAML est\u00e1 sendo removida em uma vers\u00e3o futura do Home Assistant. \n\n Sua configura\u00e7\u00e3o existente foi importada para a interface do usu\u00e1rio automaticamente. Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o YAML do Volvo On Call est\u00e1 sendo removida" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/volvooncall/translations/tr.json b/homeassistant/components/volvooncall/translations/tr.json new file mode 100644 index 00000000000..0b56c9b67b6 --- /dev/null +++ b/homeassistant/components/volvooncall/translations/tr.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + }, + "error": { + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "mutable": "Uzaktan \u00c7al\u0131\u015ft\u0131rmaya / Kilitlemeye / vb. izin verin.", + "password": "Parola", + "region": "B\u00f6lge", + "scandinavian_miles": "\u0130skandinav Millerini Kullan\u0131n", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Volvo On Call platformunun YAML kullan\u0131larak yap\u0131land\u0131r\u0131lmas\u0131, Home Assistant'\u0131n gelecekteki bir s\u00fcr\u00fcm\u00fcnde kald\u0131r\u0131lmaktad\u0131r. \n\n Mevcut yap\u0131land\u0131rman\u0131z otomatik olarak kullan\u0131c\u0131 aray\u00fcz\u00fcne aktar\u0131ld\u0131. YAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu gidermek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "Volvo On Call YAML yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131l\u0131yor" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/translations/pt.json b/homeassistant/components/xiaomi_ble/translations/pt.json new file mode 100644 index 00000000000..9c78f327cb9 --- /dev/null +++ b/homeassistant/components/xiaomi_ble/translations/pt.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "decryption_failed": "A chave de liga\u00e7\u00e3o fornecida n\u00e3o funcionou, os dados do sensor n\u00e3o puderam ser descriptografados. Por favor verifique e tente novamente.", + "expected_24_characters": "Espera-se uma chave de liga\u00e7\u00e3o hexadecimal de 24 caracteres.", + "expected_32_characters": "Esperava-se uma chave de liga\u00e7\u00e3o hexadecimal de 32 caracteres." + }, + "step": { + "confirm_slow": { + "description": "N\u00e3o houve uma transmiss\u00e3o deste dispositivo no \u00faltimo minuto, por isso n\u00e3o temos certeza se este dispositivo usa criptografia ou n\u00e3o. Isso pode ocorrer porque o dispositivo usa um intervalo de transmiss\u00e3o lento. Confirme para adicionar este dispositivo de qualquer maneira e, na pr\u00f3xima vez que uma transmiss\u00e3o for recebida, voc\u00ea ser\u00e1 solicitado a inserir sua chave de liga\u00e7\u00e3o, se necess\u00e1rio." + }, + "slow_confirm": { + "description": "N\u00e3o houve uma transmiss\u00e3o deste dispositivo no \u00faltimo minuto, por isso n\u00e3o temos certeza se este dispositivo usa criptografia ou n\u00e3o. Isso pode ocorrer porque o dispositivo usa um intervalo de transmiss\u00e3o lento. Confirme para adicionar este dispositivo de qualquer maneira e, na pr\u00f3xima vez que uma transmiss\u00e3o for recebida, voc\u00ea ser\u00e1 solicitado a inserir sua chave de liga\u00e7\u00e3o, se necess\u00e1rio." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/select.pt.json b/homeassistant/components/xiaomi_miio/translations/select.pt.json index 24ed8a3e752..ad9d007e81e 100644 --- a/homeassistant/components/xiaomi_miio/translations/select.pt.json +++ b/homeassistant/components/xiaomi_miio/translations/select.pt.json @@ -1,7 +1,17 @@ { "state": { + "xiaomi_miio__display_orientation": { + "forward": "Encaminhar", + "left": "Esquerda", + "right": "Direita" + }, "xiaomi_miio__led_brightness": { "dim": "Escurecer" + }, + "xiaomi_miio__ptc_level": { + "high": "Alto", + "low": "Baixo", + "medium": "M\u00e9dio" } } } \ No newline at end of file diff --git a/homeassistant/components/yalexs_ble/translations/bg.json b/homeassistant/components/yalexs_ble/translations/bg.json new file mode 100644 index 00000000000..fce4c934f7e --- /dev/null +++ b/homeassistant/components/yalexs_ble/translations/bg.json @@ -0,0 +1,12 @@ +{ + "config": { + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "Bluetooth \u0430\u0434\u0440\u0435\u0441" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yalexs_ble/translations/pt.json b/homeassistant/components/yalexs_ble/translations/pt.json new file mode 100644 index 00000000000..3cbc65bda52 --- /dev/null +++ b/homeassistant/components/yalexs_ble/translations/pt.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "invalid_key_format": "A chave offline deve ser uma string hexadecimal de 32 bytes.", + "invalid_key_index": "O slot de chave offline deve ser um n\u00famero inteiro entre 0 e 255." + }, + "flow_title": "{name}", + "step": { + "integration_discovery_confirm": { + "description": "Deseja configurar {name} por Bluetooth com o endere\u00e7o {address} ?" + }, + "user": { + "data": { + "address": "Endere\u00e7o Bluetooth", + "key": "Chave offline (sequ\u00eancia hexadecimal de 32 bytes)", + "slot": "Slot de chave offline (inteiro entre 0 e 255)" + }, + "description": "Verifique a documenta\u00e7\u00e3o para saber como encontrar a chave offline." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/translations/bg.json b/homeassistant/components/zha/translations/bg.json index 5774bca7124..cc58b42b11a 100644 --- a/homeassistant/components/zha/translations/bg.json +++ b/homeassistant/components/zha/translations/bg.json @@ -8,6 +8,9 @@ "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" }, "step": { + "choose_serial_port": { + "title": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0441\u0435\u0440\u0438\u0435\u043d \u043f\u043e\u0440\u0442" + }, "confirm": { "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 {name}?" }, @@ -17,7 +20,8 @@ "manual_port_config": { "data": { "baudrate": "\u0441\u043a\u043e\u0440\u043e\u0441\u0442 \u043d\u0430 \u043f\u043e\u0440\u0442\u0430" - } + }, + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043d\u0430 \u0441\u0435\u0440\u0438\u0439\u043d\u0438\u044f \u043f\u043e\u0440\u0442" }, "pick_radio": { "data": { @@ -95,6 +99,9 @@ }, "flow_title": "{name}", "step": { + "choose_serial_port": { + "title": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0441\u0435\u0440\u0438\u0435\u043d \u043f\u043e\u0440\u0442" + }, "manual_port_config": { "data": { "baudrate": "\u0441\u043a\u043e\u0440\u043e\u0441\u0442 \u043d\u0430 \u043f\u043e\u0440\u0442\u0430" diff --git a/homeassistant/components/zha/translations/et.json b/homeassistant/components/zha/translations/et.json index 9363efb6b53..6d39f4b6539 100644 --- a/homeassistant/components/zha/translations/et.json +++ b/homeassistant/components/zha/translations/et.json @@ -117,11 +117,56 @@ }, "options": { "step": { + "choose_automatic_backup": { + "title": "Taasta automaatvarundusest" + }, + "choose_formation_strategy": { + "description": "Vali raadiov\u00f5rgu s\u00e4tted", + "menu_options": { + "choose_automatic_backup": "Taasta automaatvarundusest", + "form_new_network": "Kustuta vana v\u00f5rk ja loo uus", + "reuse_settings": "S\u00e4ilita raadiov\u00f5rgu s\u00e4tted", + "upload_manual_backup": "Lae varukoopia \u00fcles" + }, + "title": "V\u00f5rgu \u00fclesehitus" + }, + "choose_serial_port": { + "data": { + "path": "Jadapordi seadme rada" + }, + "description": "Vali Zigbee raadio jadaport", + "title": "Vali jadaport" + }, "init": { "description": "ZHA peatatakse. Kas soovid j\u00e4tkata?", "title": "Seadista ZHA uuesti" }, + "manual_pick_radio_type": { + "data": { + "radio_type": "Raadio t\u00fc\u00fcp" + }, + "description": "Vali Zigbee raadio t\u00fc\u00fcp", + "title": "Raadioseadme t\u00fc\u00fcp" + }, + "manual_port_config": { + "data": { + "baudrate": "Pordi kiirus", + "flow_control": "Andmevoo kontroll", + "path": "Jadapordi seadme aadress" + }, + "description": "Sisesta jadapordi s\u00e4tted", + "title": "Jadapordi s\u00e4tted" + }, + "maybe_confirm_ezsp_restore": { + "data": { + "overwrite_coordinator_ieee": "Asenda IEEE aadress j\u00e4\u00e4davalt" + }, + "title": "Kirjuta IEEE aadress \u00fcle" + }, "upload_manual_backup": { + "data": { + "uploaded_backup_file": "Lae kirje \u00fcles" + }, "title": "Lae k\u00e4sitsi loodud varukoopia \u00fcles" } } diff --git a/homeassistant/components/zha/translations/pt.json b/homeassistant/components/zha/translations/pt.json index 435e80b8b76..075cde5efb7 100644 --- a/homeassistant/components/zha/translations/pt.json +++ b/homeassistant/components/zha/translations/pt.json @@ -4,9 +4,67 @@ "single_instance_allowed": "Apenas uma configura\u00e7\u00e3o do ZHA \u00e9 permitida." }, "error": { - "cannot_connect": "N\u00e3o \u00e9 poss\u00edvel conectar-se ao dispositivo ZHA." + "cannot_connect": "N\u00e3o \u00e9 poss\u00edvel conectar-se ao dispositivo ZHA.", + "invalid_backup_json": "JSON de backup inv\u00e1lido" }, "step": { + "choose_automatic_backup": { + "data": { + "choose_automatic_backup": "Escolha um backup autom\u00e1tico" + }, + "description": "Restaure suas configura\u00e7\u00f5es de rede a partir de um backup autom\u00e1tico", + "title": "Restaurar Backup Autom\u00e1tico" + }, + "choose_formation_strategy": { + "description": "Escolha as configura\u00e7\u00f5es de rede para o seu r\u00e1dio.", + "menu_options": { + "choose_automatic_backup": "Restaurar um backup autom\u00e1tico", + "form_new_network": "Apague as configura\u00e7\u00f5es de rede e forme uma nova rede", + "reuse_settings": "Manter as configura\u00e7\u00f5es de rede de r\u00e1dio", + "upload_manual_backup": "Carregar um backup manual" + }, + "title": "Forma\u00e7\u00e3o de rede" + }, + "choose_serial_port": { + "data": { + "path": "Caminho do dispositivo serial" + }, + "description": "Selecione a porta serial para o seu r\u00e1dio Zigbee", + "title": "Selecione uma porta serial" + }, + "confirm_hardware": { + "description": "Deseja configurar {name} ?" + }, + "manual_pick_radio_type": { + "data": { + "radio_type": "Tipo de r\u00e1dio" + }, + "description": "Escolha seu tipo de r\u00e1dio Zigbee", + "title": "Tipo de r\u00e1dio" + }, + "manual_port_config": { + "data": { + "baudrate": "velocidade da porta", + "flow_control": "controle de fluxo de dados", + "path": "Caminho do dispositivo serial" + }, + "description": "Digite as configura\u00e7\u00f5es da porta serial", + "title": "Configura\u00e7\u00f5es da porta serial" + }, + "maybe_confirm_ezsp_restore": { + "data": { + "overwrite_coordinator_ieee": "Substituir permanentemente o endere\u00e7o IEEE do r\u00e1dio" + }, + "description": "Seu backup tem um endere\u00e7o IEEE diferente do seu r\u00e1dio. Para que sua rede funcione corretamente, o endere\u00e7o IEEE do seu r\u00e1dio tamb\u00e9m deve ser alterado. \n\n Esta \u00e9 uma opera\u00e7\u00e3o permanente.", + "title": "Sobrescrever o endere\u00e7o IEEE do r\u00e1dio" + }, + "upload_manual_backup": { + "data": { + "uploaded_backup_file": "Enviar um arquivo" + }, + "description": "Restaure suas configura\u00e7\u00f5es de rede de um arquivo JSON de backup carregado. Voc\u00ea pode baixar um de uma instala\u00e7\u00e3o ZHA diferente em **Network Settings**, ou usar um arquivo Zigbee2MQTT `coordinator_backup.json`.", + "title": "Carregar um backup manual" + }, "user": { "title": "ZHA" } @@ -42,5 +100,13 @@ "device_dropped": "Dispositivo caiu", "device_shaken": "Dispositivo abanado" } + }, + "options": { + "step": { + "init": { + "description": "ZHA ser\u00e1 interrompido. Voc\u00ea deseja continuar?", + "title": "Reconfigurar ZHA" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/translations/tr.json b/homeassistant/components/zha/translations/tr.json index 82eb9b11b68..3d3859f745c 100644 --- a/homeassistant/components/zha/translations/tr.json +++ b/homeassistant/components/zha/translations/tr.json @@ -6,16 +6,64 @@ "usb_probe_failed": "USB ayg\u0131t\u0131 ara\u015ft\u0131r\u0131lamad\u0131" }, "error": { - "cannot_connect": "Ba\u011flanma hatas\u0131" + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_backup_json": "Ge\u00e7ersiz yedek JSON" }, "flow_title": "{name}", "step": { + "choose_automatic_backup": { + "data": { + "choose_automatic_backup": "Otomatik bir yedekleme se\u00e7in" + }, + "description": "Otomatik bir yedeklemeden a\u011f ayarlar\u0131n\u0131z\u0131 geri y\u00fckleyin", + "title": "Otomatik Yedeklemeyi Geri Y\u00fckle" + }, + "choose_formation_strategy": { + "description": "Radyonuz i\u00e7in a\u011f ayarlar\u0131n\u0131 se\u00e7in.", + "menu_options": { + "choose_automatic_backup": "Otomatik yedeklemeyi geri y\u00fckleyin", + "form_new_network": "A\u011f ayarlar\u0131n\u0131 silin ve yeni bir a\u011f olu\u015fturun", + "reuse_settings": "Radyo a\u011f\u0131 ayarlar\u0131n\u0131 koru", + "upload_manual_backup": "Manuel bir yedekleme y\u00fckleyin" + }, + "title": "A\u011f Olu\u015fumu" + }, + "choose_serial_port": { + "data": { + "path": "Seri Cihaz Yolu" + }, + "description": "Zigbee radyonuz i\u00e7in seri ba\u011flant\u0131 noktas\u0131n\u0131 se\u00e7in", + "title": "Seri Ba\u011flant\u0131 Noktas\u0131 Se\u00e7in" + }, "confirm": { "description": "{name} kurulumunu yapmak istiyor musunuz?" }, "confirm_hardware": { "description": "{name} kurulumunu yapmak istiyor musunuz?" }, + "manual_pick_radio_type": { + "data": { + "radio_type": "Radyo Tipi" + }, + "description": "Zigbee radyo tipinizi se\u00e7in", + "title": "Radyo Tipi" + }, + "manual_port_config": { + "data": { + "baudrate": "ba\u011flant\u0131 noktas\u0131 h\u0131z\u0131", + "flow_control": "veri ak\u0131\u015f\u0131 denetimi", + "path": "Seri cihaz yolu" + }, + "description": "Seri ba\u011flant\u0131 noktas\u0131 ayarlar\u0131n\u0131 girin", + "title": "Seri Ba\u011flant\u0131 Noktas\u0131 Ayarlar\u0131" + }, + "maybe_confirm_ezsp_restore": { + "data": { + "overwrite_coordinator_ieee": "Radyo IEEE adresini kal\u0131c\u0131 olarak de\u011fi\u015ftirin" + }, + "description": "Yedeklemenizin, telsizinizden farkl\u0131 bir IEEE adresi var. A\u011f\u0131n\u0131z\u0131n d\u00fczg\u00fcn \u00e7al\u0131\u015fmas\u0131 i\u00e7in telsizinizin IEEE adresinin de de\u011fi\u015ftirilmesi gerekir. \n\n Bu kal\u0131c\u0131 bir operasyondur.", + "title": "Radyo IEEE Adresinin \u00dczerine Yaz" + }, "pick_radio": { "data": { "radio_type": "Radyo Tipi" @@ -32,6 +80,13 @@ "description": "Ba\u011flant\u0131 noktas\u0131na \u00f6zel ayarlar\u0131 girin", "title": "Ayarlar" }, + "upload_manual_backup": { + "data": { + "uploaded_backup_file": "Bir dosya y\u00fckleyin" + }, + "description": "Y\u00fcklenen bir yedek JSON dosyas\u0131ndan a\u011f ayarlar\u0131n\u0131z\u0131 geri y\u00fckleyin. **A\u011f Ayarlar\u0131**'ndan farkl\u0131 bir ZHA kurulumundan bir tane indirebilir veya bir Zigbee2MQTT `coordinator_backup.json` dosyas\u0131 kullanabilirsiniz.", + "title": "Manuel Yedekleme Y\u00fckleyin" + }, "user": { "data": { "path": "Seri Cihaz Yolu" @@ -114,5 +169,77 @@ "remote_button_short_release": "\" {subtype} \" d\u00fc\u011fmesi b\u0131rak\u0131ld\u0131", "remote_button_triple_press": "\" {subtype} \" d\u00fc\u011fmesine \u00fc\u00e7 kez t\u0131kland\u0131" } + }, + "options": { + "abort": { + "not_zha_device": "Bu cihaz bir zha cihaz\u0131 de\u011fil", + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.", + "usb_probe_failed": "USB ayg\u0131t\u0131 ara\u015ft\u0131r\u0131lamad\u0131" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_backup_json": "Ge\u00e7ersiz yedek JSON" + }, + "flow_title": "{name}", + "step": { + "choose_automatic_backup": { + "data": { + "choose_automatic_backup": "Otomatik bir yedekleme se\u00e7in" + }, + "description": "Otomatik bir yedeklemeden a\u011f ayarlar\u0131n\u0131z\u0131 geri y\u00fckleyin", + "title": "Otomatik Yedeklemeyi Geri Y\u00fckle" + }, + "choose_formation_strategy": { + "description": "Radyonuz i\u00e7in a\u011f ayarlar\u0131n\u0131 se\u00e7in.", + "menu_options": { + "choose_automatic_backup": "Otomatik yedeklemeyi geri y\u00fckleyin", + "form_new_network": "A\u011f ayarlar\u0131n\u0131 silin ve yeni bir a\u011f olu\u015fturun", + "reuse_settings": "Radyo a\u011f\u0131 ayarlar\u0131n\u0131 koru", + "upload_manual_backup": "Manuel bir yedekleme y\u00fckleyin" + }, + "title": "A\u011f Olu\u015fumu" + }, + "choose_serial_port": { + "data": { + "path": "Seri Cihaz Yolu" + }, + "description": "Zigbee radyonuz i\u00e7in seri ba\u011flant\u0131 noktas\u0131n\u0131 se\u00e7in", + "title": "Seri Ba\u011flant\u0131 Noktas\u0131 Se\u00e7in" + }, + "init": { + "description": "ZHA durdurulacak. Devam etmek istiyor musunuz?", + "title": "ZHA'y\u0131 yeniden yap\u0131land\u0131r\u0131n" + }, + "manual_pick_radio_type": { + "data": { + "radio_type": "Radyo Tipi" + }, + "description": "Zigbee radyo tipinizi se\u00e7in", + "title": "Radyo Tipi" + }, + "manual_port_config": { + "data": { + "baudrate": "ba\u011flant\u0131 noktas\u0131 h\u0131z\u0131", + "flow_control": "veri ak\u0131\u015f\u0131 denetimi", + "path": "Seri cihaz yolu" + }, + "description": "Seri ba\u011flant\u0131 noktas\u0131 ayarlar\u0131n\u0131 girin", + "title": "Seri Ba\u011flant\u0131 Noktas\u0131 Ayarlar\u0131" + }, + "maybe_confirm_ezsp_restore": { + "data": { + "overwrite_coordinator_ieee": "Radyo IEEE adresini kal\u0131c\u0131 olarak de\u011fi\u015ftirin" + }, + "description": "Yedeklemenizin, telsizinizden farkl\u0131 bir IEEE adresi var. A\u011f\u0131n\u0131z\u0131n d\u00fczg\u00fcn \u00e7al\u0131\u015fmas\u0131 i\u00e7in telsizinizin IEEE adresinin de de\u011fi\u015ftirilmesi gerekir. \n\n Bu kal\u0131c\u0131 bir operasyondur.", + "title": "Radyo IEEE Adresinin \u00dczerine Yaz" + }, + "upload_manual_backup": { + "data": { + "uploaded_backup_file": "Bir dosya y\u00fckleyin" + }, + "description": "Y\u00fcklenen bir yedek JSON dosyas\u0131ndan a\u011f ayarlar\u0131n\u0131z\u0131 geri y\u00fckleyin. **A\u011f Ayarlar\u0131**'ndan farkl\u0131 bir ZHA kurulumundan bir tane indirebilir veya bir Zigbee2MQTT `coordinator_backup.json` dosyas\u0131 kullanabilirsiniz.", + "title": "Manuel Yedekleme Y\u00fckleyin" + } + } } } \ No newline at end of file From b5402f9b57cf5e5fa37f0dd7fe0f9122df416a46 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 13 Sep 2022 02:50:44 +0200 Subject: [PATCH 324/955] Import device tracker constants from root (#78242) --- homeassistant/components/asuswrt/config_flow.py | 2 +- homeassistant/components/asuswrt/router.py | 2 +- .../components/bluetooth_le_tracker/device_tracker.py | 4 +--- .../components/bluetooth_tracker/device_tracker.py | 4 +--- homeassistant/components/dhcp/__init__.py | 2 +- homeassistant/components/fritz/common.py | 4 ++-- homeassistant/components/fritz/config_flow.py | 2 +- homeassistant/components/huawei_lte/device_tracker.py | 4 ++-- homeassistant/components/keenetic_ndms2/const.py | 2 +- homeassistant/components/mikrotik/device_tracker.py | 5 +---- homeassistant/components/mobile_app/device_tracker.py | 2 +- homeassistant/components/nmap_tracker/__init__.py | 2 +- homeassistant/components/nmap_tracker/config_flow.py | 2 +- homeassistant/components/owntracks/device_tracker.py | 6 +----- homeassistant/components/ping/device_tracker.py | 6 ++---- homeassistant/components/starline/device_tracker.py | 2 +- homeassistant/components/tile/device_tracker.py | 3 +-- homeassistant/components/unifi/device_tracker.py | 3 +-- 18 files changed, 21 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/asuswrt/config_flow.py b/homeassistant/components/asuswrt/config_flow.py index ec5cccb9a71..94843a4c07c 100644 --- a/homeassistant/components/asuswrt/config_flow.py +++ b/homeassistant/components/asuswrt/config_flow.py @@ -9,7 +9,7 @@ from typing import Any import voluptuous as vol -from homeassistant.components.device_tracker.const import ( +from homeassistant.components.device_tracker import ( CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME, ) diff --git a/homeassistant/components/asuswrt/router.py b/homeassistant/components/asuswrt/router.py index f68c77a2a66..a48e1374b6d 100644 --- a/homeassistant/components/asuswrt/router.py +++ b/homeassistant/components/asuswrt/router.py @@ -8,7 +8,7 @@ from typing import Any from aioasuswrt.asuswrt import AsusWrt, Device as WrtDevice -from homeassistant.components.device_tracker.const import ( +from homeassistant.components.device_tracker import ( CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME, DOMAIN as TRACKER_DOMAIN, diff --git a/homeassistant/components/bluetooth_le_tracker/device_tracker.py b/homeassistant/components/bluetooth_le_tracker/device_tracker.py index 85908cc1d55..179d038e96e 100644 --- a/homeassistant/components/bluetooth_le_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_le_tracker/device_tracker.py @@ -12,10 +12,8 @@ import voluptuous as vol from homeassistant.components import bluetooth from homeassistant.components.bluetooth.match import BluetoothCallbackMatcher from homeassistant.components.device_tracker import ( - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, -) -from homeassistant.components.device_tracker.const import ( CONF_TRACK_NEW, + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, SCAN_INTERVAL, SourceType, ) diff --git a/homeassistant/components/bluetooth_tracker/device_tracker.py b/homeassistant/components/bluetooth_tracker/device_tracker.py index d266ba5d542..ce8f6ca8006 100644 --- a/homeassistant/components/bluetooth_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_tracker/device_tracker.py @@ -12,12 +12,10 @@ from bt_proximity import BluetoothRSSI import voluptuous as vol from homeassistant.components.device_tracker import ( - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, -) -from homeassistant.components.device_tracker.const import ( CONF_SCAN_INTERVAL, CONF_TRACK_NEW, DEFAULT_TRACK_NEW, + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, SCAN_INTERVAL, SourceType, ) diff --git a/homeassistant/components/dhcp/__init__.py b/homeassistant/components/dhcp/__init__.py index 7a5854fc53e..ab8787f4bff 100644 --- a/homeassistant/components/dhcp/__init__.py +++ b/homeassistant/components/dhcp/__init__.py @@ -26,7 +26,7 @@ from scapy.config import conf from scapy.error import Scapy_Exception from homeassistant import config_entries -from homeassistant.components.device_tracker.const import ( +from homeassistant.components.device_tracker import ( ATTR_HOST_NAME, ATTR_IP, ATTR_MAC, diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index 610dcab6aaa..d424b38ec24 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -20,10 +20,10 @@ from fritzconnection.lib.fritzhosts import FritzHosts from fritzconnection.lib.fritzstatus import FritzStatus from fritzconnection.lib.fritzwlan import DEFAULT_PASSWORD_LENGTH, FritzGuestWLAN -from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN -from homeassistant.components.device_tracker.const import ( +from homeassistant.components.device_tracker import ( CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME, + DOMAIN as DEVICE_TRACKER_DOMAIN, ) from homeassistant.components.switch import DOMAIN as DEVICE_SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/fritz/config_flow.py b/homeassistant/components/fritz/config_flow.py index ea6461cef32..df0277494ec 100644 --- a/homeassistant/components/fritz/config_flow.py +++ b/homeassistant/components/fritz/config_flow.py @@ -13,7 +13,7 @@ from fritzconnection.core.exceptions import FritzConnectionException, FritzSecur import voluptuous as vol from homeassistant.components import ssdp -from homeassistant.components.device_tracker.const import ( +from homeassistant.components.device_tracker import ( CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME, ) diff --git a/homeassistant/components/huawei_lte/device_tracker.py b/homeassistant/components/huawei_lte/device_tracker.py index 18d797aefc6..f97bda7481e 100644 --- a/homeassistant/components/huawei_lte/device_tracker.py +++ b/homeassistant/components/huawei_lte/device_tracker.py @@ -8,11 +8,11 @@ from typing import Any, cast from stringcase import snakecase -from homeassistant.components.device_tracker.config_entry import ScannerEntity -from homeassistant.components.device_tracker.const import ( +from homeassistant.components.device_tracker import ( DOMAIN as DEVICE_TRACKER_DOMAIN, SourceType, ) +from homeassistant.components.device_tracker.config_entry import ScannerEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry diff --git a/homeassistant/components/keenetic_ndms2/const.py b/homeassistant/components/keenetic_ndms2/const.py index c07fb0a0d15..0b415a9502f 100644 --- a/homeassistant/components/keenetic_ndms2/const.py +++ b/homeassistant/components/keenetic_ndms2/const.py @@ -1,6 +1,6 @@ """Constants used in the Keenetic NDMS2 components.""" -from homeassistant.components.device_tracker.const import ( +from homeassistant.components.device_tracker import ( DEFAULT_CONSIDER_HOME as _DEFAULT_CONSIDER_HOME, ) diff --git a/homeassistant/components/mikrotik/device_tracker.py b/homeassistant/components/mikrotik/device_tracker.py index f50c49d5ab6..8e112e2ee03 100644 --- a/homeassistant/components/mikrotik/device_tracker.py +++ b/homeassistant/components/mikrotik/device_tracker.py @@ -3,11 +3,8 @@ from __future__ import annotations from typing import Any +from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER, SourceType from homeassistant.components.device_tracker.config_entry import ScannerEntity -from homeassistant.components.device_tracker.const import ( - DOMAIN as DEVICE_TRACKER, - SourceType, -) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry diff --git a/homeassistant/components/mobile_app/device_tracker.py b/homeassistant/components/mobile_app/device_tracker.py index ef1acdaf32d..6f9c68224ec 100644 --- a/homeassistant/components/mobile_app/device_tracker.py +++ b/homeassistant/components/mobile_app/device_tracker.py @@ -4,9 +4,9 @@ from homeassistant.components.device_tracker import ( ATTR_GPS, ATTR_GPS_ACCURACY, ATTR_LOCATION_NAME, + SourceType, ) from homeassistant.components.device_tracker.config_entry import TrackerEntity -from homeassistant.components.device_tracker.const import SourceType from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_BATTERY_LEVEL, diff --git a/homeassistant/components/nmap_tracker/__init__.py b/homeassistant/components/nmap_tracker/__init__.py index 5f3333ec750..e3b83f8abc2 100644 --- a/homeassistant/components/nmap_tracker/__init__.py +++ b/homeassistant/components/nmap_tracker/__init__.py @@ -14,7 +14,7 @@ from getmac import get_mac_address from mac_vendor_lookup import AsyncMacLookup from nmap import PortScanner, PortScannerError -from homeassistant.components.device_tracker.const import ( +from homeassistant.components.device_tracker import ( CONF_CONSIDER_HOME, CONF_SCAN_INTERVAL, DEFAULT_CONSIDER_HOME, diff --git a/homeassistant/components/nmap_tracker/config_flow.py b/homeassistant/components/nmap_tracker/config_flow.py index 42e84af2eaa..f3008a8c6d7 100644 --- a/homeassistant/components/nmap_tracker/config_flow.py +++ b/homeassistant/components/nmap_tracker/config_flow.py @@ -8,7 +8,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components import network -from homeassistant.components.device_tracker.const import ( +from homeassistant.components.device_tracker import ( CONF_CONSIDER_HOME, CONF_SCAN_INTERVAL, DEFAULT_CONSIDER_HOME, diff --git a/homeassistant/components/owntracks/device_tracker.py b/homeassistant/components/owntracks/device_tracker.py index 70b8bde6de1..79efee6bd2c 100644 --- a/homeassistant/components/owntracks/device_tracker.py +++ b/homeassistant/components/owntracks/device_tracker.py @@ -1,10 +1,6 @@ """Device tracker platform that adds support for OwnTracks over MQTT.""" +from homeassistant.components.device_tracker import ATTR_SOURCE_TYPE, DOMAIN, SourceType from homeassistant.components.device_tracker.config_entry import TrackerEntity -from homeassistant.components.device_tracker.const import ( - ATTR_SOURCE_TYPE, - DOMAIN, - SourceType, -) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_BATTERY_LEVEL, diff --git a/homeassistant/components/ping/device_tracker.py b/homeassistant/components/ping/device_tracker.py index 52285350cd4..b4266c8e9a7 100644 --- a/homeassistant/components/ping/device_tracker.py +++ b/homeassistant/components/ping/device_tracker.py @@ -11,12 +11,10 @@ import voluptuous as vol from homeassistant import const, util from homeassistant.components.device_tracker import ( - PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA, - AsyncSeeCallback, -) -from homeassistant.components.device_tracker.const import ( CONF_SCAN_INTERVAL, + PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA, SCAN_INTERVAL, + AsyncSeeCallback, SourceType, ) from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/starline/device_tracker.py b/homeassistant/components/starline/device_tracker.py index 35adbd38a4a..e01807ba702 100644 --- a/homeassistant/components/starline/device_tracker.py +++ b/homeassistant/components/starline/device_tracker.py @@ -1,6 +1,6 @@ """StarLine device tracker.""" +from homeassistant.components.device_tracker import SourceType from homeassistant.components.device_tracker.config_entry import TrackerEntity -from homeassistant.components.device_tracker.const import SourceType from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback diff --git a/homeassistant/components/tile/device_tracker.py b/homeassistant/components/tile/device_tracker.py index f749d500b46..86afee18505 100644 --- a/homeassistant/components/tile/device_tracker.py +++ b/homeassistant/components/tile/device_tracker.py @@ -5,9 +5,8 @@ import logging from pytile.tile import Tile -from homeassistant.components.device_tracker import AsyncSeeCallback +from homeassistant.components.device_tracker import AsyncSeeCallback, SourceType from homeassistant.components.device_tracker.config_entry import TrackerEntity -from homeassistant.components.device_tracker.const import SourceType from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant, callback diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index 5484bcf2d5b..fc9c41ce184 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -17,9 +17,8 @@ from aiounifi.events import ( WIRELESS_GUEST_ROAMRADIO, ) -from homeassistant.components.device_tracker import DOMAIN +from homeassistant.components.device_tracker import DOMAIN, SourceType from homeassistant.components.device_tracker.config_entry import ScannerEntity -from homeassistant.components.device_tracker.const import SourceType from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect From 8a9edea2c126f6dad12f93ffbdb898ab0cf5a2aa Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 13 Sep 2022 03:24:49 +0200 Subject: [PATCH 325/955] Fix calculating gas cost for gas measured in ft3 (#78327) --- homeassistant/components/energy/sensor.py | 3 ++- tests/components/energy/test_sensor.py | 8 ++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/energy/sensor.py b/homeassistant/components/energy/sensor.py index 4002f6c416d..602fc09f602 100644 --- a/homeassistant/components/energy/sensor.py +++ b/homeassistant/components/energy/sensor.py @@ -20,6 +20,7 @@ from homeassistant.const import ( ENERGY_KILO_WATT_HOUR, ENERGY_MEGA_WATT_HOUR, ENERGY_WATT_HOUR, + VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS, ) from homeassistant.core import ( @@ -44,7 +45,7 @@ SUPPORTED_STATE_CLASSES = [ SensorStateClass.TOTAL_INCREASING, ] VALID_ENERGY_UNITS = [ENERGY_WATT_HOUR, ENERGY_KILO_WATT_HOUR, ENERGY_MEGA_WATT_HOUR] -VALID_ENERGY_UNITS_GAS = [VOLUME_CUBIC_METERS] + VALID_ENERGY_UNITS +VALID_ENERGY_UNITS_GAS = [VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS] + VALID_ENERGY_UNITS _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/energy/test_sensor.py b/tests/components/energy/test_sensor.py index 997f60a8899..9fa82ead2a1 100644 --- a/tests/components/energy/test_sensor.py +++ b/tests/components/energy/test_sensor.py @@ -20,6 +20,7 @@ from homeassistant.const import ( ENERGY_MEGA_WATT_HOUR, ENERGY_WATT_HOUR, STATE_UNKNOWN, + VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS, ) from homeassistant.helpers import entity_registry as er @@ -841,10 +842,13 @@ async def test_cost_sensor_handle_price_units( assert state.state == "20.0" -async def test_cost_sensor_handle_gas(hass, hass_storage, setup_integration) -> None: +@pytest.mark.parametrize("unit", (VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS)) +async def test_cost_sensor_handle_gas( + hass, hass_storage, setup_integration, unit +) -> None: """Test gas cost price from sensor entity.""" energy_attributes = { - ATTR_UNIT_OF_MEASUREMENT: VOLUME_CUBIC_METERS, + ATTR_UNIT_OF_MEASUREMENT: unit, ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, } energy_data = data.EnergyManager.default_preferences() From 13f250319da80d78848cc9cbf60759a6d82e6fa6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 13 Sep 2022 04:07:58 +0200 Subject: [PATCH 326/955] Import websocket api constants from root (#78250) --- homeassistant/components/config/entity_registry.py | 2 +- homeassistant/components/media_player/__init__.py | 5 +---- homeassistant/components/recorder/history.py | 2 +- homeassistant/components/recorder/models.py | 2 +- homeassistant/components/zwave_js/api.py | 4 ++-- 5 files changed, 6 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/config/entity_registry.py b/homeassistant/components/config/entity_registry.py index cbfd092bc0c..ea75ac4d043 100644 --- a/homeassistant/components/config/entity_registry.py +++ b/homeassistant/components/config/entity_registry.py @@ -7,7 +7,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components import websocket_api -from homeassistant.components.websocket_api.const import ERR_NOT_FOUND +from homeassistant.components.websocket_api import ERR_NOT_FOUND from homeassistant.components.websocket_api.decorators import require_admin from homeassistant.components.websocket_api.messages import ( IDEN_JSON_TEMPLATE, diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 37cc7fa230c..408f33bbecc 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -25,10 +25,7 @@ from yarl import URL from homeassistant.backports.enum import StrEnum from homeassistant.components import websocket_api from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView -from homeassistant.components.websocket_api.const import ( - ERR_NOT_SUPPORTED, - ERR_UNKNOWN_ERROR, -) +from homeassistant.components.websocket_api import ERR_NOT_SUPPORTED, ERR_UNKNOWN_ERROR from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( # noqa: F401 SERVICE_MEDIA_NEXT_TRACK, diff --git a/homeassistant/components/recorder/history.py b/homeassistant/components/recorder/history.py index cbcbdd2c75b..5c3f47c02ed 100644 --- a/homeassistant/components/recorder/history.py +++ b/homeassistant/components/recorder/history.py @@ -17,7 +17,7 @@ from sqlalchemy.sql.expression import literal from sqlalchemy.sql.lambdas import StatementLambdaElement from sqlalchemy.sql.selectable import Subquery -from homeassistant.components.websocket_api.const import ( +from homeassistant.components.websocket_api import ( COMPRESSED_STATE_LAST_UPDATED, COMPRESSED_STATE_STATE, ) diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index ff53d9be3d1..2004c3ec30d 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -7,7 +7,7 @@ from typing import Any, TypedDict, overload from sqlalchemy.engine.row import Row -from homeassistant.components.websocket_api.const import ( +from homeassistant.components.websocket_api import ( COMPRESSED_STATE_ATTRIBUTES, COMPRESSED_STATE_LAST_CHANGED, COMPRESSED_STATE_LAST_UPDATED, diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 2e565a3be7c..98078c8457f 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -47,12 +47,12 @@ from zwave_js_server.util.node import async_set_config_parameter from homeassistant.components import websocket_api from homeassistant.components.http.view import HomeAssistantView -from homeassistant.components.websocket_api.connection import ActiveConnection -from homeassistant.components.websocket_api.const import ( +from homeassistant.components.websocket_api import ( ERR_INVALID_FORMAT, ERR_NOT_FOUND, ERR_NOT_SUPPORTED, ERR_UNKNOWN_ERROR, + ActiveConnection, ) from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.core import HomeAssistant, callback From 4e32bf2ac910e38695a1b9bf9b610c717db97998 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 13 Sep 2022 04:28:05 +0200 Subject: [PATCH 327/955] Drop old migration code from entity registry (#78278) --- homeassistant/helpers/entity_registry.py | 159 +++++++++-------------- tests/helpers/test_entity_registry.py | 98 +------------- 2 files changed, 61 insertions(+), 196 deletions(-) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 701b0816d1c..f40c6347af7 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -42,7 +42,6 @@ from homeassistant.core import ( from homeassistant.exceptions import MaxLengthExceeded from homeassistant.loader import bind_hass from homeassistant.util import slugify, uuid as uuid_util -from homeassistant.util.yaml import load_yaml from . import device_registry as dr, storage from .device_registry import EVENT_DEVICE_REGISTRY_UPDATED @@ -56,7 +55,6 @@ if TYPE_CHECKING: T = TypeVar("T") -PATH_REGISTRY = "entity_registry.yaml" DATA_REGISTRY = "entity_registry" EVENT_ENTITY_REGISTRY_UPDATED = "entity_registry_updated" SAVE_DELAY = 10 @@ -177,7 +175,65 @@ class EntityRegistryStore(storage.Store): self, old_major_version: int, old_minor_version: int, old_data: dict ) -> dict: """Migrate to the new version.""" - return await _async_migrate(old_major_version, old_minor_version, old_data) + data = old_data + if old_major_version == 1 and old_minor_version < 2: + # From version 1.1 + for entity in data["entities"]: + # Populate all keys + entity["area_id"] = entity.get("area_id") + entity["capabilities"] = entity.get("capabilities") or {} + entity["config_entry_id"] = entity.get("config_entry_id") + entity["device_class"] = entity.get("device_class") + entity["device_id"] = entity.get("device_id") + entity["disabled_by"] = entity.get("disabled_by") + entity["entity_category"] = entity.get("entity_category") + entity["icon"] = entity.get("icon") + entity["name"] = entity.get("name") + entity["original_icon"] = entity.get("original_icon") + entity["original_name"] = entity.get("original_name") + entity["platform"] = entity["platform"] + entity["supported_features"] = entity.get("supported_features", 0) + entity["unit_of_measurement"] = entity.get("unit_of_measurement") + + if old_major_version == 1 and old_minor_version < 3: + # Version 1.3 adds original_device_class + for entity in data["entities"]: + # Move device_class to original_device_class + entity["original_device_class"] = entity["device_class"] + entity["device_class"] = None + + if old_major_version == 1 and old_minor_version < 4: + # Version 1.4 adds id + for entity in data["entities"]: + entity["id"] = uuid_util.random_uuid_hex() + + if old_major_version == 1 and old_minor_version < 5: + # Version 1.5 adds entity options + for entity in data["entities"]: + entity["options"] = {} + + if old_major_version == 1 and old_minor_version < 6: + # Version 1.6 adds hidden_by + for entity in data["entities"]: + entity["hidden_by"] = None + + if old_major_version == 1 and old_minor_version < 7: + # Version 1.7 adds has_entity_name + for entity in data["entities"]: + entity["has_entity_name"] = False + + if old_major_version == 1 and old_minor_version < 8: + # Cleanup after frontend bug which incorrectly updated device_class + # Fixed by frontend PR #13551 + for entity in data["entities"]: + domain = split_entity_id(entity["entity_id"])[0] + if domain in [Platform.BINARY_SENSOR, Platform.COVER]: + continue + entity["device_class"] = None + + if old_major_version > 1: + raise NotImplementedError + return data class EntityRegistryItems(UserDict[str, "RegistryEntry"]): @@ -362,13 +418,6 @@ class EntityRegistry: original_name=original_name, supported_features=supported_features, unit_of_measurement=unit_of_measurement, - # When we changed our slugify algorithm, we invalidated some - # stored entity IDs with either a __ or ending in _. - # Fix introduced in 0.86 (Jan 23, 2019). Next line can be - # removed when we release 1.0 or in 2020. - new_entity_id=".".join( - slugify(part) for part in entity_id.split(".", 1) - ), ) entity_id = self.async_generate_entity_id( @@ -722,25 +771,13 @@ class EntityRegistry: """Load the entity registry.""" async_setup_entity_restore(self.hass, self) - data = await storage.async_migrator( - self.hass, - self.hass.config.path(PATH_REGISTRY), - self._store, - old_conf_load_func=load_yaml, - old_conf_migrate_func=_async_migrate_yaml_to_json, - ) + data = await self._store.async_load() entities = EntityRegistryItems() from .entity import EntityCategory # pylint: disable=import-outside-toplevel if data is not None: for entity in data["entities"]: - # Some old installations can have some bad entities. - # Filter them out as they cause errors down the line. - # Can be removed in Jan 2021 - if not valid_entity_id(entity["entity_id"]): - continue - # We removed this in 2022.5. Remove this check in 2023.1. if entity["entity_category"] == "system": entity["entity_category"] = None @@ -922,82 +959,6 @@ def async_config_entry_disabled_by_changed( ) -async def _async_migrate( - old_major_version: int, old_minor_version: int, data: dict -) -> dict: - """Migrate to the new version.""" - if old_major_version == 1 and old_minor_version < 2: - # From version 1.1 - for entity in data["entities"]: - # Populate all keys - entity["area_id"] = entity.get("area_id") - entity["capabilities"] = entity.get("capabilities") or {} - entity["config_entry_id"] = entity.get("config_entry_id") - entity["device_class"] = entity.get("device_class") - entity["device_id"] = entity.get("device_id") - entity["disabled_by"] = entity.get("disabled_by") - entity["entity_category"] = entity.get("entity_category") - entity["icon"] = entity.get("icon") - entity["name"] = entity.get("name") - entity["original_icon"] = entity.get("original_icon") - entity["original_name"] = entity.get("original_name") - entity["platform"] = entity["platform"] - entity["supported_features"] = entity.get("supported_features", 0) - entity["unit_of_measurement"] = entity.get("unit_of_measurement") - - if old_major_version == 1 and old_minor_version < 3: - # Version 1.3 adds original_device_class - for entity in data["entities"]: - # Move device_class to original_device_class - entity["original_device_class"] = entity["device_class"] - entity["device_class"] = None - - if old_major_version == 1 and old_minor_version < 4: - # Version 1.4 adds id - for entity in data["entities"]: - entity["id"] = uuid_util.random_uuid_hex() - - if old_major_version == 1 and old_minor_version < 5: - # Version 1.5 adds entity options - for entity in data["entities"]: - entity["options"] = {} - - if old_major_version == 1 and old_minor_version < 6: - # Version 1.6 adds hidden_by - for entity in data["entities"]: - entity["hidden_by"] = None - - if old_major_version == 1 and old_minor_version < 7: - # Version 1.7 adds has_entity_name - for entity in data["entities"]: - entity["has_entity_name"] = False - - if old_major_version == 1 and old_minor_version < 8: - # Cleanup after frontend bug which incorrectly updated device_class - # Fixed by frontend PR #13551 - for entity in data["entities"]: - domain = split_entity_id(entity["entity_id"])[0] - if domain in [Platform.BINARY_SENSOR, Platform.COVER]: - continue - entity["device_class"] = None - - if old_major_version > 1: - raise NotImplementedError - return data - - -async def _async_migrate_yaml_to_json( - entities: dict[str, Any] -) -> dict[str, list[dict[str, Any]]]: - """Migrate the YAML config file to storage helper format.""" - entities_1_1 = { - "entities": [ - {"entity_id": entity_id, **info} for entity_id, info in entities.items() - ] - } - return await _async_migrate(1, 1, entities_1_1) - - @callback def async_setup_entity_restore(hass: HomeAssistant, registry: EntityRegistry) -> None: """Set up the entity restore mechanism.""" diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index 8ccba2c7ecc..64579c25766 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -6,7 +6,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import EVENT_HOMEASSISTANT_START, STATE_UNAVAILABLE -from homeassistant.core import CoreState, callback, valid_entity_id +from homeassistant.core import CoreState, callback from homeassistant.exceptions import MaxLengthExceeded from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity import EntityCategory @@ -342,13 +342,6 @@ async def test_filter_on_load(hass, hass_storage): "unique_id": "disabled-hass", "disabled_by": "hass", # We store the string representation }, - # This entry should not be loaded because the entity_id is invalid - { - "entity_id": "test.invalid__entity", - "platform": "super_platform", - "unique_id": "invalid-hass", - "disabled_by": "hass", # We store the string representation - }, # This entry should have the entity_category reset to None { "entity_id": "test.system_entity", @@ -465,38 +458,6 @@ async def test_removing_area_id(registry): assert entry_w_area != entry_wo_area -@pytest.mark.parametrize("load_registries", [False]) -async def test_migration_yaml_to_json(hass): - """Test migration from old (yaml) data to new.""" - mock_config = MockConfigEntry(domain="test-platform", entry_id="test-config-id") - - old_conf = { - "light.kitchen": { - "config_entry_id": "test-config-id", - "unique_id": "test-unique", - "platform": "test-platform", - "name": "Test Name", - "disabled_by": er.RegistryEntryDisabler.HASS, - } - } - with patch("os.path.isfile", return_value=True), patch("os.remove"), patch( - "homeassistant.helpers.entity_registry.load_yaml", return_value=old_conf - ): - await er.async_load(hass) - registry = er.async_get(hass) - - assert registry.async_is_registered("light.kitchen") - entry = registry.async_get_or_create( - domain="light", - platform="test-platform", - unique_id="test-unique", - config_entry=mock_config, - ) - assert entry.name == "Test Name" - assert entry.disabled_by is er.RegistryEntryDisabler.HASS - assert entry.config_entry_id == "test-config-id" - - @pytest.mark.parametrize("load_registries", [False]) async def test_migration_1_1(hass, hass_storage): """Test migration from version 1.1.""" @@ -596,63 +557,6 @@ async def test_migration_1_7(hass, hass_storage): assert entry.original_device_class == "class_by_integration" -@pytest.mark.parametrize("load_registries", [False]) -async def test_loading_invalid_entity_id(hass, hass_storage): - """Test we skip entities with invalid entity IDs.""" - hass_storage[er.STORAGE_KEY] = { - "version": er.STORAGE_VERSION_MAJOR, - "minor_version": er.STORAGE_VERSION_MINOR, - "data": { - "entities": [ - { - "entity_id": "test.invalid__middle", - "platform": "super_platform", - "unique_id": "id-invalid-middle", - "name": "registry override 1", - }, - { - "entity_id": "test.invalid_end_", - "platform": "super_platform", - "unique_id": "id-invalid-end", - "name": "registry override 2", - }, - { - "entity_id": "test._invalid_start", - "platform": "super_platform", - "unique_id": "id-invalid-start", - "name": "registry override 3", - }, - ] - }, - } - - await er.async_load(hass) - registry = er.async_get(hass) - assert len(registry.entities) == 0 - - entity_invalid_middle = registry.async_get_or_create( - "test", "super_platform", "id-invalid-middle" - ) - - assert valid_entity_id(entity_invalid_middle.entity_id) - # Check name to make sure we created a new entity - assert entity_invalid_middle.name is None - - entity_invalid_end = registry.async_get_or_create( - "test", "super_platform", "id-invalid-end" - ) - - assert valid_entity_id(entity_invalid_end.entity_id) - assert entity_invalid_end.name is None - - entity_invalid_start = registry.async_get_or_create( - "test", "super_platform", "id-invalid-start" - ) - - assert valid_entity_id(entity_invalid_start.entity_id) - assert entity_invalid_start.name is None - - async def test_update_entity_unique_id(registry): """Test entity's unique_id is updated.""" mock_config = MockConfigEntry(domain="light", entry_id="mock-id-1") From 24266f142615fe1bbbf06217cc897ea0657b3993 Mon Sep 17 00:00:00 2001 From: jafar-atili Date: Tue, 13 Sep 2022 10:01:29 +0300 Subject: [PATCH 328/955] Add SwitchBee Integration (#70201) * Add SwitchBee Integration * fixes * improved API and more logs * fixed test_config_flow code * removed light and cover * Fixed CR comments, updated pylib, improved response time and lowered the scan interval for lower latency * CR fixes, added advanced setup form to let the user choose the following: - scan interval in seconds: default 5 - whether to expose scenarios and group switches from the CU or not * used SCAN_INTERVAL_SEC instead of typing just the number * Fixed PR comments, added unit tests * fixes * Improved the pypi and updated the code accordingly * Add SwitchBee Integration * fixes * improved API and more logs * fixed test_config_flow code * removed light and cover * Fixed CR comments, updated pylib, improved response time and lowered the scan interval for lower latency * CR fixes, added advanced setup form to let the user choose the following: - scan interval in seconds: default 5 - whether to expose scenarios and group switches from the CU or not * used SCAN_INTERVAL_SEC instead of typing just the number * Fixed PR comments, added unit tests * fixes * Improved the pypi and updated the code accordingly * fixes * restored new line in .coveragerc * test: increased config_flow test coverage * removed two way type * Updated CODEOWNERS * fix: code review comments * fixed review comments * added device_info * moved device info to attribute --- .coveragerc | 3 + CODEOWNERS | 2 + .../components/switchbee/__init__.py | 156 ++++++++++++++ .../components/switchbee/config_flow.py | 133 ++++++++++++ homeassistant/components/switchbee/const.py | 14 ++ .../components/switchbee/manifest.json | 9 + .../components/switchbee/strings.json | 32 +++ homeassistant/components/switchbee/switch.py | 141 +++++++++++++ .../components/switchbee/translations/en.json | 32 +++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/switchbee/__init__.py | 141 +++++++++++++ .../components/switchbee/test_config_flow.py | 198 ++++++++++++++++++ 14 files changed, 868 insertions(+) create mode 100644 homeassistant/components/switchbee/__init__.py create mode 100644 homeassistant/components/switchbee/config_flow.py create mode 100644 homeassistant/components/switchbee/const.py create mode 100644 homeassistant/components/switchbee/manifest.json create mode 100644 homeassistant/components/switchbee/strings.json create mode 100644 homeassistant/components/switchbee/switch.py create mode 100644 homeassistant/components/switchbee/translations/en.json create mode 100644 tests/components/switchbee/__init__.py create mode 100644 tests/components/switchbee/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 23531c159fd..6f4fa46864c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1587,6 +1587,9 @@ omit = homeassistant/components/zwave_me/sensor.py homeassistant/components/zwave_me/siren.py homeassistant/components/zwave_me/switch.py + homeassistant/components/switchbee/__init__.py + homeassistant/components/switchbee/const.py + homeassistant/components/switchbee/switch.py [report] # Regexes for lines to exclude from consideration diff --git a/CODEOWNERS b/CODEOWNERS index 7203e4705f1..c8e4de87818 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1087,6 +1087,8 @@ build.json @home-assistant/supervisor /tests/components/switch/ @home-assistant/core /homeassistant/components/switch_as_x/ @home-assistant/core /tests/components/switch_as_x/ @home-assistant/core +/homeassistant/components/switchbee/ @jafar-atili +/tests/components/switchbee/ @jafar-atili /homeassistant/components/switchbot/ @bdraco @danielhiversen @RenierM26 @murtas @Eloston /tests/components/switchbot/ @bdraco @danielhiversen @RenierM26 @murtas @Eloston /homeassistant/components/switcher_kis/ @tomerfi @thecode diff --git a/homeassistant/components/switchbee/__init__.py b/homeassistant/components/switchbee/__init__.py new file mode 100644 index 00000000000..ca8d792111b --- /dev/null +++ b/homeassistant/components/switchbee/__init__.py @@ -0,0 +1,156 @@ +"""The SwitchBee Smart Home integration.""" + +from __future__ import annotations + +from datetime import timedelta +import logging + +from switchbee.api import CentralUnitAPI, SwitchBeeError +from switchbee.device import DeviceType + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.device_registry import format_mac +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import ( + CONF_DEFUALT_ALLOWED, + CONF_DEVICES, + CONF_SWITCHES_AS_LIGHTS, + DOMAIN, + SCAN_INTERVAL_SEC, +) + +_LOGGER = logging.getLogger(__name__) + + +PLATFORMS: list[Platform] = [Platform.SWITCH] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up SwitchBee Smart Home from a config entry.""" + hass.data.setdefault(DOMAIN, {}) + central_unit = entry.data[CONF_HOST] + user = entry.data[CONF_USERNAME] + password = entry.data[CONF_PASSWORD] + devices_map: dict[str, DeviceType] = {s.display: s for s in DeviceType} + allowed_devices = [ + devices_map[device] + for device in entry.options.get(CONF_DEVICES, CONF_DEFUALT_ALLOWED) + ] + websession = async_get_clientsession(hass, verify_ssl=False) + api = CentralUnitAPI(central_unit, user, password, websession) + try: + await api.connect() + except SwitchBeeError: + return False + + coordinator = SwitchBeeCoordinator( + hass, + api, + SCAN_INTERVAL_SEC, + allowed_devices, + entry.data[CONF_SWITCHES_AS_LIGHTS], + ) + await coordinator.async_config_entry_first_refresh() + entry.async_on_unload(entry.add_update_listener(update_listener)) + hass.data[DOMAIN][entry.entry_id] = coordinator + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> 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 + + +async def update_listener(hass: HomeAssistant, config_entry: ConfigEntry) -> None: + """Update listener.""" + await hass.config_entries.async_reload(config_entry.entry_id) + + +class SwitchBeeCoordinator(DataUpdateCoordinator): + """Class to manage fetching Freedompro data API.""" + + def __init__( + self, + hass, + swb_api, + scan_interval, + devices: list[DeviceType], + switch_as_light: bool, + ): + """Initialize.""" + self._api: CentralUnitAPI = swb_api + self._reconnect_counts: int = 0 + self._devices_to_include: list[DeviceType] = devices + self._prev_devices_to_include_to_include: list[DeviceType] = [] + self._mac_addr_fmt: str = format_mac(swb_api.mac) + self._switch_as_light = switch_as_light + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=timedelta(seconds=scan_interval), + ) + + @property + def api(self) -> CentralUnitAPI: + """Return SwitchBee API object.""" + return self._api + + @property + def mac_formated(self) -> str: + """Return formatted MAC address.""" + return self._mac_addr_fmt + + @property + def switch_as_light(self) -> bool: + """Return switch_as_ligh config.""" + return self._switch_as_light + + async def _async_update_data(self): + + if self._reconnect_counts != self._api.reconnect_count: + self._reconnect_counts = self._api.reconnect_count + _LOGGER.debug( + "Central Unit re-connected again due to invalid token, total %i", + self._reconnect_counts, + ) + + config_changed = False + + if set(self._prev_devices_to_include_to_include) != set( + self._devices_to_include + ): + self._prev_devices_to_include_to_include = self._devices_to_include + config_changed = True + + # The devices are loaded once during the config_entry + if not self._api.devices or config_changed: + # Try to load the devices from the CU for the first time + try: + await self._api.fetch_configuration(self._devices_to_include) + except SwitchBeeError as exp: + raise UpdateFailed( + f"Error communicating with API: {exp}" + ) from SwitchBeeError + else: + _LOGGER.debug("Loaded devices") + + # Get the state of the devices + try: + await self._api.fetch_states() + except SwitchBeeError as exp: + raise UpdateFailed( + f"Error communicating with API: {exp}" + ) from SwitchBeeError + else: + return self._api.devices diff --git a/homeassistant/components/switchbee/config_flow.py b/homeassistant/components/switchbee/config_flow.py new file mode 100644 index 00000000000..38d33bd4981 --- /dev/null +++ b/homeassistant/components/switchbee/config_flow.py @@ -0,0 +1,133 @@ +"""Config flow for SwitchBee Smart Home integration.""" +from __future__ import annotations + +import logging +from typing import Any + +from switchbee.api import CentralUnitAPI, SwitchBeeError +from switchbee.device import DeviceType +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResult +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import format_mac + +from .const import CONF_DEFUALT_ALLOWED, CONF_DEVICES, CONF_SWITCHES_AS_LIGHTS, DOMAIN + +_LOGGER = logging.getLogger(__name__) + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_SWITCHES_AS_LIGHTS, default=False): cv.boolean, + } +) + + +async def validate_input(hass: HomeAssistant, data: dict[str, Any]): + """Validate the user input allows us to connect.""" + + websession = async_get_clientsession(hass, verify_ssl=False) + api = CentralUnitAPI( + data[CONF_HOST], data[CONF_USERNAME], data[CONF_PASSWORD], websession + ) + try: + await api.connect() + except SwitchBeeError as exp: + _LOGGER.error(exp) + if "LOGIN_FAILED" in str(exp): + raise InvalidAuth from SwitchBeeError + + raise CannotConnect from SwitchBeeError + + return format_mac(api.mac) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for SwitchBee Smart Home.""" + + VERSION = 1 + + async def async_step_user(self, user_input=None) -> FlowResult: + """Show the setup form to the user.""" + errors: dict[str, str] = {} + + if user_input is None: + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) + + try: + mac_formated = await validate_input(self.hass, user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + + else: + await self.async_set_unique_id(mac_formated) + self._abort_if_unique_id_configured() + return self.async_create_entry(title=user_input[CONF_HOST], data=user_input) + + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) + + @staticmethod + @callback + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> OptionsFlowHandler: + """Get the options flow for this handler.""" + return OptionsFlowHandler(config_entry) + + +class OptionsFlowHandler(config_entries.OptionsFlow): + """Handle a option flow for AEMET.""" + + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + """Initialize options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None) -> FlowResult: + """Handle options flow.""" + + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + all_devices = [ + DeviceType.Switch, + DeviceType.TimedSwitch, + DeviceType.GroupSwitch, + DeviceType.TimedPowerSwitch, + ] + + data_schema = { + vol.Required( + CONF_DEVICES, + default=self.config_entry.options.get( + CONF_DEVICES, + CONF_DEFUALT_ALLOWED, + ), + ): cv.multi_select([device.display for device in all_devices]), + } + + return self.async_show_form(step_id="init", data_schema=vol.Schema(data_schema)) + + +class CannotConnect(HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class InvalidAuth(HomeAssistantError): + """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/switchbee/const.py b/homeassistant/components/switchbee/const.py new file mode 100644 index 00000000000..be818346589 --- /dev/null +++ b/homeassistant/components/switchbee/const.py @@ -0,0 +1,14 @@ +"""Constants for the SwitchBee Smart Home integration.""" + +from switchbee.device import DeviceType + +DOMAIN = "switchbee" +SCAN_INTERVAL_SEC = 5 +CONF_SCAN_INTERVAL = "scan_interval" +CONF_SWITCHES_AS_LIGHTS = "switch_as_light" +CONF_DEVICES = "devices" +CONF_DEFUALT_ALLOWED = [ + DeviceType.Switch.display, + DeviceType.TimedPowerSwitch.display, + DeviceType.TimedSwitch.display, +] diff --git a/homeassistant/components/switchbee/manifest.json b/homeassistant/components/switchbee/manifest.json new file mode 100644 index 00000000000..ba0e4a454ce --- /dev/null +++ b/homeassistant/components/switchbee/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "switchbee", + "name": "SwitchBee", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/switchbee", + "requirements": ["pyswitchbee==1.4.7"], + "codeowners": ["@jafar-atili"], + "iot_class": "local_polling" +} diff --git a/homeassistant/components/switchbee/strings.json b/homeassistant/components/switchbee/strings.json new file mode 100644 index 00000000000..531e19fda00 --- /dev/null +++ b/homeassistant/components/switchbee/strings.json @@ -0,0 +1,32 @@ +{ + "config": { + "step": { + "user": { + "description": "Setup SwitchBee integration with Home Assistant.", + "data": { + "host": "[%key:common::config_flow::data::host%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", + "switch_as_light": "Initialize switches as light entities" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + }, + "options": { + "step": { + "init": { + "data": { + "devices": "Devices to include" + } + } + } + } +} diff --git a/homeassistant/components/switchbee/switch.py b/homeassistant/components/switchbee/switch.py new file mode 100644 index 00000000000..de648d4232c --- /dev/null +++ b/homeassistant/components/switchbee/switch.py @@ -0,0 +1,141 @@ +"""Support for SwitchBee switch.""" +import logging +from typing import Any + +from switchbee import SWITCHBEE_BRAND +from switchbee.api import SwitchBeeDeviceOfflineError, SwitchBeeError +from switchbee.device import ApiStateCommand, DeviceType, SwitchBeeBaseDevice + +from homeassistant.components.switch import SwitchEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import SwitchBeeCoordinator +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up Switchbee switch.""" + coordinator: SwitchBeeCoordinator = hass.data[DOMAIN][entry.entry_id] + device_types = ( + [DeviceType.TimedPowerSwitch] + if coordinator.switch_as_light + else [ + DeviceType.TimedPowerSwitch, + DeviceType.GroupSwitch, + DeviceType.Switch, + DeviceType.TimedSwitch, + DeviceType.TwoWay, + ] + ) + + async_add_entities( + Device(hass, device, coordinator) + for device in coordinator.data.values() + if device.type in device_types + ) + + +class Device(CoordinatorEntity, SwitchEntity): + """Representation of an Switchbee switch.""" + + def __init__(self, hass, device: SwitchBeeBaseDevice, coordinator): + """Initialize the Switchbee switch.""" + super().__init__(coordinator) + self._session = aiohttp_client.async_get_clientsession(hass) + self._attr_name = f"{device.name}" + self._device_id = device.id + self._attr_unique_id = f"{coordinator.mac_formated}-{device.id}" + self._attr_is_on = False + self._attr_available = True + self._attr_has_entity_name = True + self._device = device + self._attr_device_info = DeviceInfo( + name=f"SwitchBee_{str(device.unit_id)}", + identifiers={ + ( + DOMAIN, + f"{str(device.unit_id)}-{coordinator.mac_formated}", + ) + }, + manufacturer=SWITCHBEE_BRAND, + model=coordinator.api.module_display(device.unit_id), + suggested_area=device.zone, + via_device=( + DOMAIN, + f"{coordinator.api.name} ({coordinator.api.mac})", + ), + ) + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + + async def async_refresh_state(): + + try: + await self.coordinator.api.set_state(self._device_id, "dummy") + except SwitchBeeDeviceOfflineError: + return + except SwitchBeeError: + return + + if self.coordinator.data[self._device_id].state == -1: + # This specific call will refresh the state of the device in the CU + self.hass.async_create_task(async_refresh_state()) + + if self.available: + _LOGGER.error( + "%s switch is not responding, check the status in the SwitchBee mobile app", + self.name, + ) + self._attr_available = False + self.async_write_ha_state() + return None + + if not self.available: + _LOGGER.info( + "%s switch is now responding", + self.name, + ) + self._attr_available = True + + # timed power switch state will represent a number of minutes until it goes off + # regulare switches state is ON/OFF + self._attr_is_on = ( + self.coordinator.data[self._device_id].state != ApiStateCommand.OFF + ) + + super()._handle_coordinator_update() + + async def async_added_to_hass(self) -> None: + """When entity is added to hass.""" + await super().async_added_to_hass() + self._handle_coordinator_update() + + async def async_turn_on(self, **kwargs: Any) -> None: + """Async function to set on to switch.""" + return await self._async_set_state(ApiStateCommand.ON) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Async function to set off to switch.""" + return await self._async_set_state(ApiStateCommand.OFF) + + async def _async_set_state(self, state): + try: + await self.coordinator.api.set_state(self._device_id, state) + except (SwitchBeeError, SwitchBeeDeviceOfflineError) as exp: + _LOGGER.error( + "Failed to set %s state %s, error: %s", self._attr_name, state, exp + ) + self._async_write_ha_state() + else: + await self.coordinator.async_refresh() diff --git a/homeassistant/components/switchbee/translations/en.json b/homeassistant/components/switchbee/translations/en.json new file mode 100644 index 00000000000..8fee54f3fba --- /dev/null +++ b/homeassistant/components/switchbee/translations/en.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured_device": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Failed to Authenticate with the Central Unit", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "description": "Setup SwitchBee integration with Home Assistant.", + "data": { + "host": "Central Unit IP address", + "username": "User (e-mail)", + "password": "Password", + "switch_as_light": "Initialize switches as light entities" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "devices": "Devices to include" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 2b78c6888d4..22425231288 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -372,6 +372,7 @@ FLOWS = { "subaru", "sun", "surepetcare", + "switchbee", "switchbot", "switcher_kis", "syncthing", diff --git a/requirements_all.txt b/requirements_all.txt index 6c310e55e55..1c7129153d7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1908,6 +1908,9 @@ pystiebeleltron==0.0.1.dev2 # homeassistant.components.suez_water pysuez==0.1.19 +# homeassistant.components.switchbee +pyswitchbee==1.4.7 + # homeassistant.components.syncthru pysyncthru==0.7.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 769c5ffcaa9..ddac1964828 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1334,6 +1334,9 @@ pyspcwebgw==0.4.0 # homeassistant.components.squeezebox pysqueezebox==0.6.0 +# homeassistant.components.switchbee +pyswitchbee==1.4.7 + # homeassistant.components.syncthru pysyncthru==0.7.10 diff --git a/tests/components/switchbee/__init__.py b/tests/components/switchbee/__init__.py new file mode 100644 index 00000000000..5043a70c35c --- /dev/null +++ b/tests/components/switchbee/__init__.py @@ -0,0 +1,141 @@ +"""Tests for the SwitchBee Smart Home integration.""" + +MOCK_GET_CONFIGURATION = { + "status": "OK", + "data": { + "mac": "A8-21-08-E7-67-B6", + "name": "Residence", + "version": "1.4.4(4)", + "lastConfChange": 1661856874511, + "zones": [ + { + "name": "Sensor Setting", + "items": [ + { + "id": 200000, + "name": "home", + "hw": "VIRTUAL", + "type": "ALARM_SYSTEM", + }, + { + "id": 200010, + "name": "away", + "hw": "VIRTUAL", + "type": "ALARM_SYSTEM", + }, + ], + }, + { + "name": "General", + "items": [ + { + "operations": [113], + "id": 100080, + "name": "All Lights", + "hw": "VIRTUAL", + "type": "GROUP_SWITCH", + }, + { + "operations": [ + {"itemId": 21, "value": 100}, + {"itemId": 333, "value": 100}, + ], + "id": 100160, + "name": "Sunrise", + "hw": "VIRTUAL", + "type": "SCENARIO", + }, + ], + }, + { + "name": "Entrance", + "items": [ + { + "id": 113, + "name": "Staircase Lights", + "hw": "DIMMABLE_SWITCH", + "type": "TIMED_SWITCH", + }, + { + "id": 222, + "name": "Front Door", + "hw": "REGULAR_SWITCH", + "type": "TIMED_SWITCH", + }, + ], + }, + { + "name": "Kitchen", + "items": [ + {"id": 21, "name": "Shutter ", "hw": "SHUTTER", "type": "SHUTTER"}, + { + "operations": [593, 581, 171], + "id": 481, + "name": "Leds", + "hw": "DIMMABLE_SWITCH", + "type": "GROUP_SWITCH", + }, + { + "id": 12, + "name": "Walls", + "hw": "DIMMABLE_SWITCH", + "type": "DIMMER", + }, + ], + }, + { + "name": "Two Way Zone", + "items": [ + { + "operations": [113], + "id": 72, + "name": "Staircase Lights", + "hw": "DIMMABLE_SWITCH", + "type": "TWO_WAY", + } + ], + }, + { + "name": "Facilities ", + "items": [ + { + "id": 321, + "name": "Boiler", + "hw": "TIMED_POWER_SWITCH", + "type": "TIMED_POWER", + }, + { + "modes": ["COOL", "HEAT", "FAN"], + "temperatureUnits": "CELSIUS", + "id": 271, + "name": "HVAC", + "hw": "THERMOSTAT", + "type": "THERMOSTAT", + }, + { + "id": 571, + "name": "Repeater", + "hw": "REPEATER", + "type": "REPEATER", + }, + ], + }, + { + "name": "Alarm", + "items": [ + { + "operations": [{"itemId": 113, "value": 100}], + "id": 81, + "name": "Open Home", + "hw": "STIKER_SWITCH", + "type": "SCENARIO", + } + ], + }, + ], + }, +} +MOCK_FAILED_TO_LOGIN_MSG = ( + "Central Unit replied with failure: {'status': 'LOGIN_FAILED'}" +) +MOCK_INVALID_TOKEN_MGS = "Error fetching switchbee data: Error communicating with API: data Request failed due to INVALID_TOKEN, trying to re-login" diff --git a/tests/components/switchbee/test_config_flow.py b/tests/components/switchbee/test_config_flow.py new file mode 100644 index 00000000000..541259853a3 --- /dev/null +++ b/tests/components/switchbee/test_config_flow.py @@ -0,0 +1,198 @@ +"""Test the SwitchBee Smart Home config flow.""" +from unittest.mock import patch + +from homeassistant import config_entries +from homeassistant.components.switchbee.config_flow import DeviceType, SwitchBeeError +from homeassistant.components.switchbee.const import CONF_SWITCHES_AS_LIGHTS, DOMAIN +from homeassistant.const import CONF_DEVICES, CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import RESULT_TYPE_FORM, FlowResultType + +from . import MOCK_FAILED_TO_LOGIN_MSG, MOCK_GET_CONFIGURATION, MOCK_INVALID_TOKEN_MGS + +from tests.common import MockConfigEntry + + +async def test_form(hass): + """Test we get the form.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch( + "switchbee.api.CentralUnitAPI.get_configuration", + return_value=MOCK_GET_CONFIGURATION, + ), patch( + "homeassistant.components.switchbee.async_setup_entry", + return_value=True, + ), patch( + "switchbee.api.CentralUnitAPI.fetch_states", return_value=None + ), patch( + "switchbee.api.CentralUnitAPI._login", return_value=None + ): + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "1.1.1.1", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + CONF_SWITCHES_AS_LIGHTS: False, + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "1.1.1.1" + assert result2["data"] == { + CONF_HOST: "1.1.1.1", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + CONF_SWITCHES_AS_LIGHTS: False, + } + + +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( + "switchbee.api.CentralUnitAPI._login", + side_effect=SwitchBeeError(MOCK_FAILED_TO_LOGIN_MSG), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "1.1.1.1", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + CONF_SWITCHES_AS_LIGHTS: False, + }, + ) + + assert result2["type"] == RESULT_TYPE_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( + "switchbee.api.CentralUnitAPI._login", + side_effect=SwitchBeeError(MOCK_INVALID_TOKEN_MGS), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "1.1.1.1", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + CONF_SWITCHES_AS_LIGHTS: False, + }, + ) + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_unknown_error(hass): + """Test we handle an unknown error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "switchbee.api.CentralUnitAPI._login", + side_effect=Exception, + ): + form_result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "1.1.1.1", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + CONF_SWITCHES_AS_LIGHTS: False, + }, + ) + + assert form_result["type"] == RESULT_TYPE_FORM + assert form_result["errors"] == {"base": "unknown"} + + +async def test_form_entry_exists(hass): + """Test we handle an already existing entry.""" + MockConfigEntry( + unique_id="a8:21:08:e7:67:b6", + domain=DOMAIN, + data={ + CONF_HOST: "1.1.1.1", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + CONF_SWITCHES_AS_LIGHTS: False, + }, + title="1.1.1.1", + ).add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch("switchbee.api.CentralUnitAPI._login", return_value=None), patch( + "homeassistant.components.switchbee.async_setup_entry", + return_value=True, + ), patch( + "switchbee.api.CentralUnitAPI.get_configuration", + return_value=MOCK_GET_CONFIGURATION, + ), patch( + "switchbee.api.CentralUnitAPI.fetch_states", return_value=None + ): + form_result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "1.2.2.2", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + CONF_SWITCHES_AS_LIGHTS: False, + }, + ) + + assert form_result["type"] == FlowResultType.ABORT + assert form_result["reason"] == "already_configured" + + +async def test_option_flow(hass): + """Test config flow options.""" + entry = MockConfigEntry( + unique_id="a8:21:08:e7:67:b6", + domain=DOMAIN, + data={ + CONF_HOST: "1.1.1.1", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + CONF_SWITCHES_AS_LIGHTS: False, + }, + title="1.1.1.1", + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.options.async_init(entry.entry_id) + assert result["type"] == "form" + assert result["step_id"] == "init" + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_DEVICES: [DeviceType.Switch.display, DeviceType.GroupSwitch.display], + }, + ) + assert result["type"] == "create_entry" + assert result["data"] == { + CONF_DEVICES: [DeviceType.Switch.display, DeviceType.GroupSwitch.display], + } From b0249e6aa9c4f051d56f9a4dc09cf0dd5ceba554 Mon Sep 17 00:00:00 2001 From: Robin Wohlers-Reichel Date: Tue, 13 Sep 2022 17:37:38 +0930 Subject: [PATCH 329/955] Update solax to 0.3.0 (#78219) --- homeassistant/components/solax/config_flow.py | 2 +- homeassistant/components/solax/manifest.json | 2 +- homeassistant/components/solax/sensor.py | 117 +++++++++++++----- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/solax/test_config_flow.py | 5 +- 6 files changed, 95 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/solax/config_flow.py b/homeassistant/components/solax/config_flow.py index e3255a8e377..2334fd0def2 100644 --- a/homeassistant/components/solax/config_flow.py +++ b/homeassistant/components/solax/config_flow.py @@ -5,7 +5,7 @@ import logging from typing import Any from solax import real_time_api -from solax.inverter import DiscoveryError +from solax.discovery import DiscoveryError import voluptuous as vol from homeassistant import config_entries diff --git a/homeassistant/components/solax/manifest.json b/homeassistant/components/solax/manifest.json index 17ae6db0232..a41285277da 100644 --- a/homeassistant/components/solax/manifest.json +++ b/homeassistant/components/solax/manifest.json @@ -2,7 +2,7 @@ "domain": "solax", "name": "SolaX Power", "documentation": "https://www.home-assistant.io/integrations/solax", - "requirements": ["solax==0.2.9"], + "requirements": ["solax==0.3.0"], "codeowners": ["@squishykid"], "iot_class": "local_polling", "config_flow": true, diff --git a/homeassistant/components/solax/sensor.py b/homeassistant/components/solax/sensor.py index 7f9d81ac9b0..307d3c4c373 100644 --- a/homeassistant/components/solax/sensor.py +++ b/homeassistant/components/solax/sensor.py @@ -4,15 +4,26 @@ from __future__ import annotations import asyncio from datetime import timedelta -from solax.inverter import InverterError +from solax import RealTimeAPI +from solax.discovery import InverterError +from solax.units import Units from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, + SensorEntityDescription, SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import ( + ELECTRIC_CURRENT_AMPERE, + ELECTRIC_POTENTIAL_VOLT, + ENERGY_KILO_WATT_HOUR, + FREQUENCY_HERTZ, + PERCENTAGE, + POWER_WATT, + TEMP_CELSIUS, +) from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.entity import DeviceInfo @@ -25,13 +36,69 @@ DEFAULT_PORT = 80 SCAN_INTERVAL = timedelta(seconds=30) +SENSOR_DESCRIPTIONS: dict[tuple[Units, bool], SensorEntityDescription] = { + (Units.C, False): SensorEntityDescription( + key=f"{Units.C}_{False}", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=TEMP_CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + ), + (Units.KWH, False): SensorEntityDescription( + key=f"{Units.KWH}_{False}", + device_class=SensorDeviceClass.ENERGY, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + state_class=SensorStateClass.MEASUREMENT, + ), + (Units.KWH, True): SensorEntityDescription( + key=f"{Units.KWH}_{True}", + device_class=SensorDeviceClass.ENERGY, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + (Units.V, False): SensorEntityDescription( + key=f"{Units.V}_{False}", + device_class=SensorDeviceClass.VOLTAGE, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + state_class=SensorStateClass.MEASUREMENT, + ), + (Units.A, False): SensorEntityDescription( + key=f"{Units.A}_{False}", + device_class=SensorDeviceClass.CURRENT, + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + state_class=SensorStateClass.MEASUREMENT, + ), + (Units.W, False): SensorEntityDescription( + key=f"{Units.W}_{False}", + device_class=SensorDeviceClass.POWER, + native_unit_of_measurement=POWER_WATT, + state_class=SensorStateClass.MEASUREMENT, + ), + (Units.PERCENT, False): SensorEntityDescription( + key=f"{Units.PERCENT}_{False}", + device_class=SensorDeviceClass.BATTERY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + (Units.HZ, False): SensorEntityDescription( + key=f"{Units.HZ}_{False}", + device_class=SensorDeviceClass.FREQUENCY, + native_unit_of_measurement=FREQUENCY_HERTZ, + state_class=SensorStateClass.MEASUREMENT, + ), + (Units.NONE, False): SensorEntityDescription( + key=f"{Units.NONE}_{False}", + state_class=SensorStateClass.MEASUREMENT, + ), +} + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Entry setup.""" - api = hass.data[DOMAIN][entry.entry_id] + api: RealTimeAPI = hass.data[DOMAIN][entry.entry_id] resp = await api.get_data() serial = resp.serial_number version = resp.version @@ -39,30 +106,21 @@ async def async_setup_entry( hass.async_add_job(endpoint.async_refresh) async_track_time_interval(hass, endpoint.async_refresh, SCAN_INTERVAL) devices = [] - for sensor, (idx, unit) in api.inverter.sensor_map().items(): - device_class = state_class = None - if unit == "C": - device_class = SensorDeviceClass.TEMPERATURE - state_class = SensorStateClass.MEASUREMENT - unit = TEMP_CELSIUS - elif unit == "kWh": - device_class = SensorDeviceClass.ENERGY - state_class = SensorStateClass.TOTAL_INCREASING - elif unit == "V": - device_class = SensorDeviceClass.VOLTAGE - state_class = SensorStateClass.MEASUREMENT - elif unit == "A": - device_class = SensorDeviceClass.CURRENT - state_class = SensorStateClass.MEASUREMENT - elif unit == "W": - device_class = SensorDeviceClass.POWER - state_class = SensorStateClass.MEASUREMENT - elif unit == "%": - device_class = SensorDeviceClass.BATTERY - state_class = SensorStateClass.MEASUREMENT + for sensor, (idx, measurement) in api.inverter.sensor_map().items(): + description = SENSOR_DESCRIPTIONS[(measurement.unit, measurement.is_monotonic)] + uid = f"{serial}-{idx}" devices.append( - Inverter(uid, serial, version, sensor, unit, state_class, device_class) + Inverter( + api.inverter.manufacturer, + uid, + serial, + version, + sensor, + description.native_unit_of_measurement, + description.state_class, + description.device_class, + ) ) endpoint.sensors = devices async_add_entities(devices) @@ -71,12 +129,12 @@ async def async_setup_entry( class RealTimeDataEndpoint: """Representation of a Sensor.""" - def __init__(self, hass, api): + def __init__(self, hass: HomeAssistant, api: RealTimeAPI) -> None: """Initialize the sensor.""" self.hass = hass self.api = api self.ready = asyncio.Event() - self.sensors = [] + self.sensors: list[Inverter] = [] async def async_refresh(self, now=None): """Fetch new state data for the sensor. @@ -105,6 +163,7 @@ class Inverter(SensorEntity): def __init__( self, + manufacturer, uid, serial, version, @@ -115,14 +174,14 @@ class Inverter(SensorEntity): ): """Initialize an inverter sensor.""" self._attr_unique_id = uid - self._attr_name = f"Solax {serial} {key}" + self._attr_name = f"{manufacturer} {serial} {key}" self._attr_native_unit_of_measurement = unit self._attr_state_class = state_class self._attr_device_class = device_class self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, serial)}, manufacturer=MANUFACTURER, - name=f"Solax {serial}", + name=f"{manufacturer} {serial}", sw_version=version, ) self.key = key diff --git a/requirements_all.txt b/requirements_all.txt index 1c7129153d7..f5d3058bb27 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2266,7 +2266,7 @@ solaredge-local==0.2.0 solaredge==0.0.2 # homeassistant.components.solax -solax==0.2.9 +solax==0.3.0 # homeassistant.components.honeywell somecomfort==0.8.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ddac1964828..3d49e63ff71 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1548,7 +1548,7 @@ soco==0.28.0 solaredge==0.0.2 # homeassistant.components.solax -solax==0.2.9 +solax==0.3.0 # homeassistant.components.honeywell somecomfort==0.8.0 diff --git a/tests/components/solax/test_config_flow.py b/tests/components/solax/test_config_flow.py index cb658405860..68a14e9133f 100644 --- a/tests/components/solax/test_config_flow.py +++ b/tests/components/solax/test_config_flow.py @@ -1,8 +1,9 @@ """Tests for the solax config flow.""" from unittest.mock import patch -from solax import RealTimeAPI, inverter +from solax import RealTimeAPI from solax.inverter import InverterResponse +from solax.inverters import X1MiniV34 from homeassistant import config_entries from homeassistant.components.solax.const import DOMAIN @@ -10,7 +11,7 @@ from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_PORT def __mock_real_time_api_success(): - return RealTimeAPI(inverter.X1MiniV34) + return RealTimeAPI(X1MiniV34) def __mock_get_data(): From d88334b2b29e8ee2da2ab653fb902557a0cc7af1 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 13 Sep 2022 11:28:08 +0200 Subject: [PATCH 330/955] Expose humidifier constants at the top level (#78033) --- homeassistant/components/humidifier/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/humidifier/__init__.py b/homeassistant/components/humidifier/__init__.py index b5f02a6a5d3..051e326fa53 100644 --- a/homeassistant/components/humidifier/__init__.py +++ b/homeassistant/components/humidifier/__init__.py @@ -38,6 +38,9 @@ from .const import ( # noqa: F401 DEVICE_CLASS_DEHUMIDIFIER, DEVICE_CLASS_HUMIDIFIER, DOMAIN, + MODE_AUTO, + MODE_AWAY, + MODE_NORMAL, SERVICE_SET_HUMIDITY, SERVICE_SET_MODE, SUPPORT_MODES, From 52ea9998bbc47f53f493d22e747e43c3c3bf3deb Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 13 Sep 2022 11:55:24 +0200 Subject: [PATCH 331/955] Use new media player enums in forked_daapd (#78100) --- .../components/forked_daapd/media_player.py | 52 ++++++++----------- 1 file changed, 23 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/forked_daapd/media_player.py b/homeassistant/components/forked_daapd/media_player.py index 953461c1019..429c0d7ab60 100644 --- a/homeassistant/components/forked_daapd/media_player.py +++ b/homeassistant/components/forked_daapd/media_player.py @@ -10,22 +10,15 @@ from pyforked_daapd import ForkedDaapdAPI from pylibrespot_java import LibrespotJavaAPI from homeassistant.components import media_source -from homeassistant.components.media_player import BrowseMedia, MediaPlayerEntity -from homeassistant.components.media_player.browse_media import ( +from homeassistant.components.media_player import ( + BrowseMedia, + MediaPlayerEntity, + MediaPlayerState, + MediaType, async_process_play_media_url, ) -from homeassistant.components.media_player.const import MEDIA_TYPE_MUSIC from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - CONF_HOST, - CONF_PASSWORD, - CONF_PORT, - STATE_IDLE, - STATE_OFF, - STATE_ON, - STATE_PAUSED, - STATE_PLAYING, -) +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.dispatcher import ( @@ -171,7 +164,7 @@ class ForkedDaapdZone(MediaPlayerEntity): async def async_toggle(self) -> None: """Toggle the power on the zone.""" - if self.state == STATE_OFF: + if self.state == MediaPlayerState.OFF: await self.async_turn_on() else: await self.async_turn_off() @@ -195,11 +188,11 @@ class ForkedDaapdZone(MediaPlayerEntity): return f"{FD_NAME} output ({self._output['name']})" @property - def state(self) -> str: + def state(self) -> MediaPlayerState: """State of the zone.""" if self._output["selected"]: - return STATE_ON - return STATE_OFF + return MediaPlayerState.ON + return MediaPlayerState.OFF @property def volume_level(self): @@ -452,7 +445,7 @@ class ForkedDaapdMaster(MediaPlayerEntity): Default media player component method counts idle as off. We consider idle to be on but just not playing. """ - if self.state == STATE_OFF: + if self.state == MediaPlayerState.OFF: await self.async_turn_on() else: await self.async_turn_off() @@ -463,16 +456,17 @@ class ForkedDaapdMaster(MediaPlayerEntity): return f"{FD_NAME} server" @property - def state(self): + def state(self) -> MediaPlayerState | None: """State of the player.""" if self._player["state"] == "play": - return STATE_PLAYING + return MediaPlayerState.PLAYING if self._player["state"] == "pause": - return STATE_PAUSED + return MediaPlayerState.PAUSED if not any(output["selected"] for output in self._outputs): - return STATE_OFF + return MediaPlayerState.OFF if self._player["state"] == "stop": # this should catch all remaining cases - return STATE_IDLE + return MediaPlayerState.IDLE + return None @property def volume_level(self): @@ -661,17 +655,17 @@ class ForkedDaapdMaster(MediaPlayerEntity): self._paused_event.clear() async def async_play_media( - self, media_type: str, media_id: str, **kwargs: Any + self, media_type: MediaType | str, media_id: str, **kwargs: Any ) -> None: """Play a URI.""" if media_source.is_media_source_id(media_id): - media_type = MEDIA_TYPE_MUSIC + media_type = MediaType.MUSIC play_item = await media_source.async_resolve_media( self.hass, media_id, self.entity_id ) media_id = play_item.url - if media_type == MEDIA_TYPE_MUSIC: + if media_type == MediaType.MUSIC: media_id = async_process_play_media_url(self.hass, media_id) saved_state = self.state # save play state @@ -717,7 +711,7 @@ class ForkedDaapdMaster(MediaPlayerEntity): await self._api.add_to_queue( uris=self._sources_uris[self._source], clear=True ) - if saved_state == STATE_PLAYING: + if saved_state == MediaPlayerState.PLAYING: await self.async_media_play() else: # restore stashed queue if saved_queue: @@ -731,9 +725,9 @@ class ForkedDaapdMaster(MediaPlayerEntity): clear=True, ) await self._api.seek(position_ms=saved_song_position) - if saved_state == STATE_PAUSED: + if saved_state == MediaPlayerState.PAUSED: await self.async_media_pause() - elif saved_state != STATE_PLAYING: + elif saved_state != MediaPlayerState.PLAYING: await self.async_media_stop() else: _LOGGER.debug("Media type '%s' not supported", media_type) From 131512f7fddbcc41a1ba4a6c8365b9b80ccd224a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 13 Sep 2022 05:38:01 -0500 Subject: [PATCH 332/955] Make yalexs_ble matcher more specific (#78307) --- homeassistant/components/yalexs_ble/manifest.json | 7 ++++++- homeassistant/generated/bluetooth.py | 3 ++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/yalexs_ble/manifest.json b/homeassistant/components/yalexs_ble/manifest.json index a685d750077..f3a9cc798e6 100644 --- a/homeassistant/components/yalexs_ble/manifest.json +++ b/homeassistant/components/yalexs_ble/manifest.json @@ -6,7 +6,12 @@ "requirements": ["yalexs-ble==1.8.1"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], - "bluetooth": [{ "manufacturer_id": 465 }], + "bluetooth": [ + { + "manufacturer_id": 465, + "service_uuid": "0000fe24-0000-1000-8000-00805f9b34fb" + } + ], "iot_class": "local_push", "supported_brands": { "august_ble": "August Bluetooth" diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 5368d9a745d..5aa128cd7f5 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -285,6 +285,7 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ }, { "domain": "yalexs_ble", - "manufacturer_id": 465 + "manufacturer_id": 465, + "service_uuid": "0000fe24-0000-1000-8000-00805f9b34fb" } ] From b5935e5a4f0912d7e4e0fad8d976751e8dd341af Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Tue, 13 Sep 2022 13:01:13 +0200 Subject: [PATCH 333/955] Bump PyViCare==2.17.0 (#78232) --- homeassistant/components/vicare/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/vicare/manifest.json b/homeassistant/components/vicare/manifest.json index 575ca35729b..c770f1b2382 100644 --- a/homeassistant/components/vicare/manifest.json +++ b/homeassistant/components/vicare/manifest.json @@ -3,7 +3,7 @@ "name": "Viessmann ViCare", "documentation": "https://www.home-assistant.io/integrations/vicare", "codeowners": ["@oischinger"], - "requirements": ["PyViCare==2.16.2"], + "requirements": ["PyViCare==2.17.0"], "iot_class": "cloud_polling", "config_flow": true, "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index f5d3058bb27..864cf67b947 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -47,7 +47,7 @@ PyTransportNSW==0.1.1 PyTurboJPEG==1.6.7 # homeassistant.components.vicare -PyViCare==2.16.2 +PyViCare==2.17.0 # homeassistant.components.xiaomi_aqara PyXiaomiGateway==0.13.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3d49e63ff71..97c7b7ab25e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -43,7 +43,7 @@ PyTransportNSW==0.1.1 PyTurboJPEG==1.6.7 # homeassistant.components.vicare -PyViCare==2.16.2 +PyViCare==2.17.0 # homeassistant.components.xiaomi_aqara PyXiaomiGateway==0.13.4 From 0d380738171a8bf0d78fe8107d8e2057c280ae4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Tue, 13 Sep 2022 13:59:46 +0200 Subject: [PATCH 334/955] Bump mill-local to 0.2.0 (#78302) --- homeassistant/components/mill/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mill/manifest.json b/homeassistant/components/mill/manifest.json index 9a1026e4dfd..432c77601d1 100644 --- a/homeassistant/components/mill/manifest.json +++ b/homeassistant/components/mill/manifest.json @@ -2,7 +2,7 @@ "domain": "mill", "name": "Mill", "documentation": "https://www.home-assistant.io/integrations/mill", - "requirements": ["millheater==0.10.0", "mill-local==0.1.1"], + "requirements": ["millheater==0.10.0", "mill-local==0.2.0"], "codeowners": ["@danielhiversen"], "config_flow": true, "iot_class": "local_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 864cf67b947..9a49b3a55b3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1061,7 +1061,7 @@ mficlient==0.3.0 micloud==0.5 # homeassistant.components.mill -mill-local==0.1.1 +mill-local==0.2.0 # homeassistant.components.mill millheater==0.10.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 97c7b7ab25e..440cd42a360 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -760,7 +760,7 @@ mficlient==0.3.0 micloud==0.5 # homeassistant.components.mill -mill-local==0.1.1 +mill-local==0.2.0 # homeassistant.components.mill millheater==0.10.0 From 7c3258fbecfe51c1a990c9b1a099c6b358d60c1b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 13 Sep 2022 14:04:02 +0200 Subject: [PATCH 335/955] Import network constants from root (#78342) --- homeassistant/components/nmap_tracker/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/nmap_tracker/config_flow.py b/homeassistant/components/nmap_tracker/config_flow.py index f3008a8c6d7..a1afa1b1bba 100644 --- a/homeassistant/components/nmap_tracker/config_flow.py +++ b/homeassistant/components/nmap_tracker/config_flow.py @@ -13,7 +13,7 @@ from homeassistant.components.device_tracker import ( CONF_SCAN_INTERVAL, DEFAULT_CONSIDER_HOME, ) -from homeassistant.components.network.const import MDNS_TARGET_IP +from homeassistant.components.network import MDNS_TARGET_IP from homeassistant.config_entries import ConfigEntry, OptionsFlow from homeassistant.const import CONF_EXCLUDE, CONF_HOSTS from homeassistant.core import HomeAssistant, callback From 1dff0075b04623564ef7828d9dcab4411bca4425 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 13 Sep 2022 14:05:36 +0200 Subject: [PATCH 336/955] Import humidifier constants from root (#78343) --- homeassistant/components/ecobee/humidifier.py | 8 +++----- homeassistant/components/generic_hygrostat/humidifier.py | 8 +++----- homeassistant/components/homekit_controller/humidifier.py | 8 +++----- 3 files changed, 9 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/ecobee/humidifier.py b/homeassistant/components/ecobee/humidifier.py index 230e456dc60..93d658a9ddc 100644 --- a/homeassistant/components/ecobee/humidifier.py +++ b/homeassistant/components/ecobee/humidifier.py @@ -4,14 +4,12 @@ from __future__ import annotations from datetime import timedelta from homeassistant.components.humidifier import ( - HumidifierDeviceClass, - HumidifierEntity, - HumidifierEntityFeature, -) -from homeassistant.components.humidifier.const import ( DEFAULT_MAX_HUMIDITY, DEFAULT_MIN_HUMIDITY, MODE_AUTO, + HumidifierDeviceClass, + HumidifierEntity, + HumidifierEntityFeature, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/generic_hygrostat/humidifier.py b/homeassistant/components/generic_hygrostat/humidifier.py index 07d2d2a36f0..0f111ca3d87 100644 --- a/homeassistant/components/generic_hygrostat/humidifier.py +++ b/homeassistant/components/generic_hygrostat/humidifier.py @@ -5,16 +5,14 @@ import asyncio import logging from homeassistant.components.humidifier import ( + ATTR_HUMIDITY, + MODE_AWAY, + MODE_NORMAL, PLATFORM_SCHEMA, HumidifierDeviceClass, HumidifierEntity, HumidifierEntityFeature, ) -from homeassistant.components.humidifier.const import ( - ATTR_HUMIDITY, - MODE_AWAY, - MODE_NORMAL, -) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_MODE, diff --git a/homeassistant/components/homekit_controller/humidifier.py b/homeassistant/components/homekit_controller/humidifier.py index ebba525e0c9..adc1b1c7935 100644 --- a/homeassistant/components/homekit_controller/humidifier.py +++ b/homeassistant/components/homekit_controller/humidifier.py @@ -7,15 +7,13 @@ from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import Service, ServicesTypes from homeassistant.components.humidifier import ( - HumidifierDeviceClass, - HumidifierEntity, - HumidifierEntityFeature, -) -from homeassistant.components.humidifier.const import ( DEFAULT_MAX_HUMIDITY, DEFAULT_MIN_HUMIDITY, MODE_AUTO, MODE_NORMAL, + HumidifierDeviceClass, + HumidifierEntity, + HumidifierEntityFeature, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback From bad81c1bc931290b0baa9c9f3a79c70b6156fb8d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 13 Sep 2022 14:20:15 +0200 Subject: [PATCH 337/955] Prevent use of deprecated media-player constants (#77937) --- pylint/plugins/hass_imports.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/pylint/plugins/hass_imports.py b/pylint/plugins/hass_imports.py index 0a0d9c8c7b1..34c7d87c53a 100644 --- a/pylint/plugins/hass_imports.py +++ b/pylint/plugins/hass_imports.py @@ -175,12 +175,36 @@ _OBSOLETE_IMPORT: dict[str, list[ObsoleteImportMatch]] = { reason="replaced by MediaPlayerEntityFeature enum", constant=re.compile(r"^SUPPORT_(\w*)$"), ), + ObsoleteImportMatch( + reason="replaced by MediaClass enum", + constant=re.compile(r"^MEDIA_CLASS_(\w*)$"), + ), + ObsoleteImportMatch( + reason="replaced by MediaType enum", + constant=re.compile(r"^MEDIA_TYPE_(\w*)$"), + ), + ObsoleteImportMatch( + reason="replaced by RepeatMode enum", + constant=re.compile(r"^REPEAT_MODE(\w*)$"), + ), ], "homeassistant.components.media_player.const": [ ObsoleteImportMatch( reason="replaced by MediaPlayerEntityFeature enum", constant=re.compile(r"^SUPPORT_(\w*)$"), ), + ObsoleteImportMatch( + reason="replaced by MediaClass enum", + constant=re.compile(r"^MEDIA_CLASS_(\w*)$"), + ), + ObsoleteImportMatch( + reason="replaced by MediaType enum", + constant=re.compile(r"^MEDIA_TYPE_(\w*)$"), + ), + ObsoleteImportMatch( + reason="replaced by RepeatMode enum", + constant=re.compile(r"^REPEAT_MODE(\w*)$"), + ), ], "homeassistant.components.remote": [ ObsoleteImportMatch( From 392548fe6e4212b4a3497479240a415f3e565ace Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 13 Sep 2022 14:31:34 +0200 Subject: [PATCH 338/955] Bump bleak to 0.17.0 (#78333) --- homeassistant/components/bluetooth/manifest.json | 5 +++-- homeassistant/components/bluetooth/scanner.py | 2 +- homeassistant/package_constraints.txt | 5 +++-- requirements_all.txt | 7 +++++-- requirements_test_all.txt | 7 +++++-- tests/components/bluetooth/test_scanner.py | 2 +- 6 files changed, 18 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 434b4c1127e..acc8a6977e3 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -5,10 +5,11 @@ "dependencies": ["usb"], "quality_scale": "internal", "requirements": [ - "bleak==0.16.0", + "bleak==0.17.0", + "bleak-retry-connector==1.15.1", "bluetooth-adapters==0.4.1", "bluetooth-auto-recovery==0.3.3", - "dbus_next==0.2.3" + "dbus-fast==1.4.0" ], "codeowners": ["@bdraco"], "config_flow": true, diff --git a/homeassistant/components/bluetooth/scanner.py b/homeassistant/components/bluetooth/scanner.py index 78979198e5c..184e8775f07 100644 --- a/homeassistant/components/bluetooth/scanner.py +++ b/homeassistant/components/bluetooth/scanner.py @@ -17,7 +17,7 @@ from bleak.backends.bluezdbus.advertisement_monitor import OrPattern from bleak.backends.bluezdbus.scanner import BlueZScannerArgs from bleak.backends.device import BLEDevice from bleak.backends.scanner import AdvertisementData -from dbus_next import InvalidMessageError +from dbus_fast import InvalidMessageError from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import ( diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index dbbfda9d6db..b511248a602 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,13 +10,14 @@ atomicwrites-homeassistant==1.4.1 attrs==21.2.0 awesomeversion==22.8.0 bcrypt==3.1.7 -bleak==0.16.0 +bleak-retry-connector==1.15.1 +bleak==0.17.0 bluetooth-adapters==0.4.1 bluetooth-auto-recovery==0.3.3 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==37.0.4 -dbus_next==0.2.3 +dbus-fast==1.4.0 fnvhash==0.1.0 hass-nabucasa==0.55.0 home-assistant-bluetooth==1.3.0 diff --git a/requirements_all.txt b/requirements_all.txt index 9a49b3a55b3..9b87b010056 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -408,7 +408,10 @@ bimmer_connected==0.10.2 bizkaibus==0.1.1 # homeassistant.components.bluetooth -bleak==0.16.0 +bleak-retry-connector==1.15.1 + +# homeassistant.components.bluetooth +bleak==0.17.0 # homeassistant.components.blebox blebox_uniapi==2.0.2 @@ -535,7 +538,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus_next==0.2.3 +dbus-fast==1.4.0 # homeassistant.components.debugpy debugpy==1.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 440cd42a360..22e515f3205 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -329,7 +329,10 @@ bellows==0.33.1 bimmer_connected==0.10.2 # homeassistant.components.bluetooth -bleak==0.16.0 +bleak-retry-connector==1.15.1 + +# homeassistant.components.bluetooth +bleak==0.17.0 # homeassistant.components.blebox blebox_uniapi==2.0.2 @@ -412,7 +415,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus_next==0.2.3 +dbus-fast==1.4.0 # homeassistant.components.debugpy debugpy==1.6.3 diff --git a/tests/components/bluetooth/test_scanner.py b/tests/components/bluetooth/test_scanner.py index 26e949ad2e3..2c1810d8338 100644 --- a/tests/components/bluetooth/test_scanner.py +++ b/tests/components/bluetooth/test_scanner.py @@ -7,7 +7,7 @@ from bleak.backends.scanner import ( AdvertisementDataCallback, BLEDevice, ) -from dbus_next import InvalidMessageError +from dbus_fast import InvalidMessageError import pytest from homeassistant.components import bluetooth From a5fa34b4bbbe27549ddd2b39b01529151fc621a1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Sep 2022 14:32:37 +0200 Subject: [PATCH 339/955] Bump home-assistant/builder from 2022.07.0 to 2022.09.0 (#78103) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/builder.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index ac30becb128..5516af1ab4d 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -159,7 +159,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build base image - uses: home-assistant/builder@2022.07.0 + uses: home-assistant/builder@2022.09.0 with: args: | $BUILD_ARGS \ @@ -225,7 +225,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build base image - uses: home-assistant/builder@2022.07.0 + uses: home-assistant/builder@2022.09.0 with: args: | $BUILD_ARGS \ From 6256d07255a791b1adc209fa99a2c666f8451d7b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 13 Sep 2022 14:39:05 +0200 Subject: [PATCH 340/955] Drop initial when loading input_number from storage (#78354) --- .../components/input_number/__init__.py | 16 ++++++++++++++++ tests/components/input_number/test_init.py | 4 ++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/input_number/__init__.py b/homeassistant/components/input_number/__init__.py index d5fffeba3f9..affad6ca30f 100644 --- a/homeassistant/components/input_number/__init__.py +++ b/homeassistant/components/input_number/__init__.py @@ -196,6 +196,22 @@ class NumberStorageCollection(collection.StorageCollection): """Suggest an ID based on the config.""" return info[CONF_NAME] + async def _async_load_data(self) -> dict | None: + """Load the data. + + A past bug caused frontend to add initial value to all input numbers. + This drops that. + """ + data = await super()._async_load_data() + + if data is None: + return data + + for number in data["items"]: + number.pop(CONF_INITIAL, None) + + return data + async def _update_data(self, data: dict, update_data: dict) -> dict: """Return a new updated data object.""" update_data = self.UPDATE_SCHEMA(update_data) diff --git a/tests/components/input_number/test_init.py b/tests/components/input_number/test_init.py index 4149627720b..bec05d3f344 100644 --- a/tests/components/input_number/test_init.py +++ b/tests/components/input_number/test_init.py @@ -416,7 +416,7 @@ async def test_load_from_storage(hass, storage_setup): """Test set up from storage.""" assert await storage_setup() state = hass.states.get(f"{DOMAIN}.from_storage") - assert float(state.state) == 10 + assert float(state.state) == 0 # initial is not supported when loading from storage assert state.attributes.get(ATTR_FRIENDLY_NAME) == "from storage" assert state.attributes.get(ATTR_EDITABLE) @@ -438,7 +438,7 @@ async def test_editable_state_attribute(hass, storage_setup): ) state = hass.states.get(f"{DOMAIN}.from_storage") - assert float(state.state) == 10 + assert float(state.state) == 0 assert state.attributes.get(ATTR_FRIENDLY_NAME) == "from storage" assert state.attributes.get(ATTR_EDITABLE) From 458ddb6f4b528831ce4e0cc647ce4cacee873e11 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 13 Sep 2022 14:39:39 +0200 Subject: [PATCH 341/955] Improve type hints in image-processing (#78351) --- .../components/image_processing/__init__.py | 29 +++++++------- .../openalpr_local/image_processing.py | 2 +- pylint/plugins/hass_enforce_type_hints.py | 39 +++++++++++++++++++ 3 files changed, 55 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/image_processing/__init__.py b/homeassistant/components/image_processing/__init__.py index 115d32c5d5b..de90f7dbf81 100644 --- a/homeassistant/components/image_processing/__init__.py +++ b/homeassistant/components/image_processing/__init__.py @@ -1,11 +1,14 @@ """Provides functionality to interact with image processing services.""" +from __future__ import annotations + import asyncio from datetime import timedelta import logging -from typing import Final, TypedDict, final +from typing import Any, Final, TypedDict, final import voluptuous as vol +from homeassistant.components.camera import Image from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_NAME, @@ -22,8 +25,6 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType from homeassistant.util.async_ import run_callback_threadsafe -# mypy: allow-untyped-defs, no-check-untyped-defs - _LOGGER = logging.getLogger(__name__) DOMAIN = "image_processing" @@ -44,7 +45,7 @@ ATTR_CONFIDENCE: Final = "confidence" ATTR_FACES = "faces" ATTR_GENDER = "gender" ATTR_GLASSES = "glasses" -ATTR_MOTION = "motion" +ATTR_MOTION: Final = "motion" ATTR_TOTAL_FACES = "total_faces" CONF_CONFIDENCE = "confidence" @@ -113,24 +114,24 @@ class ImageProcessingEntity(Entity): timeout = DEFAULT_TIMEOUT @property - def camera_entity(self): + def camera_entity(self) -> str | None: """Return camera entity id from process pictures.""" return None @property - def confidence(self): + def confidence(self) -> float | None: """Return minimum confidence for do some things.""" return None - def process_image(self, image): + def process_image(self, image: Image) -> None: """Process image.""" raise NotImplementedError() - async def async_process_image(self, image): + async def async_process_image(self, image: Image) -> None: """Process image.""" return await self.hass.async_add_executor_job(self.process_image, image) - async def async_update(self): + async def async_update(self) -> None: """Update image and process it. This method is a coroutine. @@ -160,9 +161,9 @@ class ImageProcessingFaceEntity(ImageProcessingEntity): self.total_faces = 0 @property - def state(self): + def state(self) -> str | int | None: """Return the state of the entity.""" - confidence = 0 + confidence: float = 0 state = None # No confidence support @@ -178,19 +179,19 @@ class ImageProcessingFaceEntity(ImageProcessingEntity): confidence = f_co for attr in (ATTR_NAME, ATTR_MOTION): if attr in face: - state = face[attr] + state = face[attr] # type: ignore[literal-required] break return state @property - def device_class(self): + def device_class(self) -> str: """Return the class of this device, from component DEVICE_CLASSES.""" return "face" @final @property - def state_attributes(self): + def state_attributes(self) -> dict[str, Any]: """Return device specific state attributes.""" return {ATTR_FACES: self.faces, ATTR_TOTAL_FACES: self.total_faces} diff --git a/homeassistant/components/openalpr_local/image_processing.py b/homeassistant/components/openalpr_local/image_processing.py index a2c0bc99287..87d189fdbd8 100644 --- a/homeassistant/components/openalpr_local/image_processing.py +++ b/homeassistant/components/openalpr_local/image_processing.py @@ -148,7 +148,7 @@ class ImageProcessingAlprEntity(ImageProcessingEntity): plates = { plate: confidence for plate, confidence in plates.items() - if confidence >= self.confidence + if self.confidence is None or confidence >= self.confidence } new_plates = set(plates) - set(self.plates) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index f7c4d66f2e2..cb0c6ecc2e7 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -1348,6 +1348,45 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ], ), ], + "image_processing": [ + ClassTypeHintMatch( + base_class="Entity", + matches=_ENTITY_MATCH, + ), + ClassTypeHintMatch( + base_class="ImageProcessingEntity", + matches=[ + TypeHintMatch( + function_name="camera_entity", + return_type=["str", None], + ), + TypeHintMatch( + function_name="confidence", + return_type=["float", None], + ), + TypeHintMatch( + function_name="process_image", + arg_types={1: "Image"}, + return_type=None, + has_async_counterpart=True, + ), + ], + ), + ClassTypeHintMatch( + base_class="ImageProcessingFaceEntity", + matches=[ + TypeHintMatch( + function_name="process_faces", + arg_types={ + 1: "list[FaceInformation]", + 2: "int", + }, + return_type=None, + has_async_counterpart=True, + ), + ], + ), + ], "light": [ ClassTypeHintMatch( base_class="Entity", From 69b59c9d597f022e14f576580a28f43af23d8b35 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 13 Sep 2022 15:09:38 +0200 Subject: [PATCH 342/955] Improve type hints in trace (#78366) --- homeassistant/components/trace/__init__.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/trace/__init__.py b/homeassistant/components/trace/__init__.py index 3761ff155b4..e6a10746a38 100644 --- a/homeassistant/components/trace/__init__.py +++ b/homeassistant/components/trace/__init__.py @@ -133,7 +133,9 @@ async def async_list_traces(hass, wanted_domain, wanted_key): return traces -def async_store_trace(hass, trace, stored_traces): +def async_store_trace( + hass: HomeAssistant, trace: ActionTrace, stored_traces: int +) -> None: """Store a trace if its key is valid.""" if key := trace.key: traces = hass.data[DATA_TRACE] @@ -144,7 +146,7 @@ def async_store_trace(hass, trace, stored_traces): traces[key][trace.run_id] = trace -def _async_store_restored_trace(hass, trace): +def _async_store_restored_trace(hass: HomeAssistant, trace: RestoredTrace) -> None: """Store a restored trace and move it to the end of the LimitedSizeDict.""" key = trace.key traces = hass.data[DATA_TRACE] @@ -154,7 +156,7 @@ def _async_store_restored_trace(hass, trace): traces[key].move_to_end(trace.run_id, last=False) -async def async_restore_traces(hass): +async def async_restore_traces(hass: HomeAssistant) -> None: """Restore saved traces.""" if DATA_TRACES_RESTORED in hass.data: return @@ -216,15 +218,15 @@ class ActionTrace(BaseTrace): def __init__( self, - item_id: str, - config: dict[str, Any], - blueprint_inputs: dict[str, Any], + item_id: str | None, + config: dict[str, Any] | None, + blueprint_inputs: dict[str, Any] | None, context: Context, ) -> None: """Container for script trace.""" self._trace: dict[str, deque[TraceElement]] | None = None - self._config: dict[str, Any] = config - self._blueprint_inputs: dict[str, Any] = blueprint_inputs + self._config = config + self._blueprint_inputs = blueprint_inputs self.context: Context = context self._error: Exception | None = None self._state: str = "running" @@ -239,7 +241,7 @@ class ActionTrace(BaseTrace): trace_set_child_id(self.key, self.run_id) trace_id_set((self.key, self.run_id)) - def set_trace(self, trace: dict[str, deque[TraceElement]]) -> None: + def set_trace(self, trace: dict[str, deque[TraceElement]] | None) -> None: """Set action trace.""" self._trace = trace From fd7c257a900ee08a438f4a5639dbaf5caef29dd0 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Tue, 13 Sep 2022 16:44:22 +0200 Subject: [PATCH 343/955] Fix Sensibo Pure sensitivity sensor text (#78313) --- homeassistant/components/sensibo/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/sensibo/sensor.py b/homeassistant/components/sensibo/sensor.py index e9d9447ad81..3fa764a8757 100644 --- a/homeassistant/components/sensibo/sensor.py +++ b/homeassistant/components/sensibo/sensor.py @@ -132,6 +132,7 @@ PURE_SENSOR_TYPES: tuple[SensiboDeviceSensorEntityDescription, ...] = ( icon="mdi:air-filter", value_fn=lambda data: data.pure_sensitivity, extra_fn=None, + device_class="sensibo__sensitivity", ), FILTER_LAST_RESET_DESCRIPTION, ) From 328530479da6b6b75f2c68447083a300693ed21b Mon Sep 17 00:00:00 2001 From: Yevhenii Vaskivskyi Date: Tue, 13 Sep 2022 16:46:31 +0200 Subject: [PATCH 344/955] Bump blinkpy to 0.19.2 (#78097) --- homeassistant/components/blink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/blink/manifest.json b/homeassistant/components/blink/manifest.json index c302f2dab6c..aa3cad317ac 100644 --- a/homeassistant/components/blink/manifest.json +++ b/homeassistant/components/blink/manifest.json @@ -2,7 +2,7 @@ "domain": "blink", "name": "Blink", "documentation": "https://www.home-assistant.io/integrations/blink", - "requirements": ["blinkpy==0.19.0"], + "requirements": ["blinkpy==0.19.2"], "codeowners": ["@fronzbot"], "dhcp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index 9b87b010056..dc151959d0c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -417,7 +417,7 @@ bleak==0.17.0 blebox_uniapi==2.0.2 # homeassistant.components.blink -blinkpy==0.19.0 +blinkpy==0.19.2 # homeassistant.components.blinksticklight blinkstick==1.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 22e515f3205..37b0a90762c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -338,7 +338,7 @@ bleak==0.17.0 blebox_uniapi==2.0.2 # homeassistant.components.blink -blinkpy==0.19.0 +blinkpy==0.19.2 # homeassistant.components.bluemaestro bluemaestro-ble==0.2.0 From be34fdc3447c75dc637a0aa5678122a80ae2e674 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 13 Sep 2022 17:00:18 +0200 Subject: [PATCH 345/955] Bump pylutron-caseta to 0.15.1 (#78209) --- homeassistant/components/lutron_caseta/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lutron_caseta/manifest.json b/homeassistant/components/lutron_caseta/manifest.json index c80d0deb794..531ecb2a086 100644 --- a/homeassistant/components/lutron_caseta/manifest.json +++ b/homeassistant/components/lutron_caseta/manifest.json @@ -2,7 +2,7 @@ "domain": "lutron_caseta", "name": "Lutron Cas\u00e9ta", "documentation": "https://www.home-assistant.io/integrations/lutron_caseta", - "requirements": ["pylutron-caseta==0.13.1"], + "requirements": ["pylutron-caseta==0.15.1"], "config_flow": true, "zeroconf": ["_leap._tcp.local."], "homekit": { diff --git a/requirements_all.txt b/requirements_all.txt index dc151959d0c..cacd04f2c76 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1677,7 +1677,7 @@ pylitejet==0.3.0 pylitterbot==2022.9.1 # homeassistant.components.lutron_caseta -pylutron-caseta==0.13.1 +pylutron-caseta==0.15.1 # homeassistant.components.lutron pylutron==0.2.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 37b0a90762c..1a48725a130 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1175,7 +1175,7 @@ pylitejet==0.3.0 pylitterbot==2022.9.1 # homeassistant.components.lutron_caseta -pylutron-caseta==0.13.1 +pylutron-caseta==0.15.1 # homeassistant.components.mailgun pymailgunner==1.4 From 0e7c81288fcd161091f609aa1b58310f0ca1fe23 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 13 Sep 2022 20:51:04 +0200 Subject: [PATCH 346/955] Unregister EcoWitt webhook at unload (#78388) --- homeassistant/components/ecowitt/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/ecowitt/__init__.py b/homeassistant/components/ecowitt/__init__.py index ebd861c1377..567e21b4d87 100644 --- a/homeassistant/components/ecowitt/__init__.py +++ b/homeassistant/components/ecowitt/__init__.py @@ -44,6 +44,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" + webhook.async_unregister(hass, entry.data[CONF_WEBHOOK_ID]) + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): hass.data[DOMAIN].pop(entry.entry_id) From 15f104911a1cc89d206edf6f68ea59907ccc2493 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 13 Sep 2022 20:54:52 +0200 Subject: [PATCH 347/955] Don't allow partial update of input_number settings (#78356) --- .../components/input_number/__init__.py | 24 ++++---------- tests/components/input_number/test_init.py | 32 +++++++++++-------- 2 files changed, 25 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/input_number/__init__.py b/homeassistant/components/input_number/__init__.py index affad6ca30f..3a7f7b29f13 100644 --- a/homeassistant/components/input_number/__init__.py +++ b/homeassistant/components/input_number/__init__.py @@ -65,7 +65,7 @@ def _cv_input_number(cfg): return cfg -CREATE_FIELDS = { +STORAGE_FIELDS = { vol.Required(CONF_NAME): vol.All(str, vol.Length(min=1)), vol.Required(CONF_MIN): vol.Coerce(float), vol.Required(CONF_MAX): vol.Coerce(float), @@ -76,17 +76,6 @@ CREATE_FIELDS = { vol.Optional(CONF_MODE, default=MODE_SLIDER): vol.In([MODE_BOX, MODE_SLIDER]), } -UPDATE_FIELDS = { - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_MIN): vol.Coerce(float), - vol.Optional(CONF_MAX): vol.Coerce(float), - vol.Optional(CONF_INITIAL): vol.Coerce(float), - vol.Optional(CONF_STEP): vol.All(vol.Coerce(float), vol.Range(min=1e-9)), - vol.Optional(CONF_ICON): cv.icon, - vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, - vol.Optional(CONF_MODE): vol.In([MODE_BOX, MODE_SLIDER]), -} - CONFIG_SCHEMA = vol.Schema( { DOMAIN: cv.schema_with_slug_keys( @@ -148,7 +137,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: await storage_collection.async_load() collection.StorageCollectionWebsocket( - storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS + storage_collection, DOMAIN, DOMAIN, STORAGE_FIELDS, STORAGE_FIELDS ).async_setup(hass) async def reload_service_handler(service_call: ServiceCall) -> None: @@ -184,12 +173,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: class NumberStorageCollection(collection.StorageCollection): """Input storage based collection.""" - CREATE_SCHEMA = vol.Schema(vol.All(CREATE_FIELDS, _cv_input_number)) - UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS) + SCHEMA = vol.Schema(vol.All(STORAGE_FIELDS, _cv_input_number)) async def _process_create_data(self, data: dict) -> dict: """Validate the config is valid.""" - return self.CREATE_SCHEMA(data) + return self.SCHEMA(data) @callback def _get_suggested_id(self, info: dict) -> str: @@ -214,8 +202,8 @@ class NumberStorageCollection(collection.StorageCollection): async def _update_data(self, data: dict, update_data: dict) -> dict: """Return a new updated data object.""" - update_data = self.UPDATE_SCHEMA(update_data) - return _cv_input_number({**data, **update_data}) + update_data = self.SCHEMA(update_data) + return {CONF_ID: data[CONF_ID]} | update_data class InputNumber(collection.CollectionEntity, RestoreEntity): diff --git a/tests/components/input_number/test_init.py b/tests/components/input_number/test_init.py index bec05d3f344..7ba7489f644 100644 --- a/tests/components/input_number/test_init.py +++ b/tests/components/input_number/test_init.py @@ -507,16 +507,14 @@ async def test_ws_delete(hass, hass_ws_client, storage_setup): async def test_update_min_max(hass, hass_ws_client, storage_setup): """Test updating min/max updates the state.""" - items = [ - { - "id": "from_storage", - "name": "from storage", - "max": 100, - "min": 0, - "step": 1, - "mode": "slider", - } - ] + settings = { + "name": "from storage", + "max": 100, + "min": 0, + "step": 1, + "mode": "slider", + } + items = [{"id": "from_storage"} | settings] assert await storage_setup(items) input_id = "from_storage" @@ -530,26 +528,34 @@ async def test_update_min_max(hass, hass_ws_client, storage_setup): client = await hass_ws_client(hass) + updated_settings = settings | {"min": 9} await client.send_json( - {"id": 6, "type": f"{DOMAIN}/update", f"{DOMAIN}_id": f"{input_id}", "min": 9} + { + "id": 6, + "type": f"{DOMAIN}/update", + f"{DOMAIN}_id": f"{input_id}", + **updated_settings, + } ) resp = await client.receive_json() assert resp["success"] + assert resp["result"] == {"id": "from_storage"} | updated_settings state = hass.states.get(input_entity_id) assert float(state.state) == 9 + updated_settings = settings | {"max": 5} await client.send_json( { "id": 7, "type": f"{DOMAIN}/update", f"{DOMAIN}_id": f"{input_id}", - "max": 5, - "min": 0, + **updated_settings, } ) resp = await client.receive_json() assert resp["success"] + assert resp["result"] == {"id": "from_storage"} | updated_settings state = hass.states.get(input_entity_id) assert float(state.state) == 5 From 47da1c456b9d6ee964040b14a3a8b79f6be0a255 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 13 Sep 2022 20:55:06 +0200 Subject: [PATCH 348/955] Don't allow partial update of counter settings (#78371) --- homeassistant/components/counter/__init__.py | 23 ++++--------- tests/components/counter/test_init.py | 36 ++++++++++---------- 2 files changed, 24 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/counter/__init__.py b/homeassistant/components/counter/__init__.py index 61ec384ae50..113826c2291 100644 --- a/homeassistant/components/counter/__init__.py +++ b/homeassistant/components/counter/__init__.py @@ -47,7 +47,7 @@ SERVICE_CONFIGURE = "configure" STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 -CREATE_FIELDS = { +STORAGE_FIELDS = { vol.Optional(CONF_ICON): cv.icon, vol.Optional(CONF_INITIAL, default=DEFAULT_INITIAL): cv.positive_int, vol.Required(CONF_NAME): vol.All(cv.string, vol.Length(min=1)), @@ -57,16 +57,6 @@ CREATE_FIELDS = { vol.Optional(CONF_STEP, default=DEFAULT_STEP): cv.positive_int, } -UPDATE_FIELDS = { - vol.Optional(CONF_ICON): cv.icon, - vol.Optional(CONF_INITIAL): cv.positive_int, - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_MAXIMUM): vol.Any(None, vol.Coerce(int)), - vol.Optional(CONF_MINIMUM): vol.Any(None, vol.Coerce(int)), - vol.Optional(CONF_RESTORE): cv.boolean, - vol.Optional(CONF_STEP): cv.positive_int, -} - def _none_to_empty_dict(value): if value is None: @@ -128,7 +118,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: await storage_collection.async_load() collection.StorageCollectionWebsocket( - storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS + storage_collection, DOMAIN, DOMAIN, STORAGE_FIELDS, STORAGE_FIELDS ).async_setup(hass) component.async_register_entity_service(SERVICE_INCREMENT, {}, "async_increment") @@ -152,12 +142,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: class CounterStorageCollection(collection.StorageCollection): """Input storage based collection.""" - CREATE_SCHEMA = vol.Schema(CREATE_FIELDS) - UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS) + CREATE_UPDATE_SCHEMA = vol.Schema(STORAGE_FIELDS) async def _process_create_data(self, data: dict) -> dict: """Validate the config is valid.""" - return self.CREATE_SCHEMA(data) + return self.CREATE_UPDATE_SCHEMA(data) @callback def _get_suggested_id(self, info: dict) -> str: @@ -166,8 +155,8 @@ class CounterStorageCollection(collection.StorageCollection): async def _update_data(self, data: dict, update_data: dict) -> dict: """Return a new updated data object.""" - update_data = self.UPDATE_SCHEMA(update_data) - return {**data, **update_data} + update_data = self.CREATE_UPDATE_SCHEMA(update_data) + return {CONF_ID: data[CONF_ID]} | update_data class Counter(collection.CollectionEntity, RestoreEntity): diff --git a/tests/components/counter/test_init.py b/tests/components/counter/test_init.py index 107dd97924d..90885be770d 100644 --- a/tests/components/counter/test_init.py +++ b/tests/components/counter/test_init.py @@ -591,17 +591,15 @@ async def test_ws_delete(hass, hass_ws_client, storage_setup): async def test_update_min_max(hass, hass_ws_client, storage_setup): """Test updating min/max updates the state.""" - items = [ - { - "id": "from_storage", - "initial": 15, - "name": "from storage", - "maximum": 100, - "minimum": 10, - "step": 3, - "restore": True, - } - ] + settings = { + "initial": 15, + "name": "from storage", + "maximum": 100, + "minimum": 10, + "step": 3, + "restore": True, + } + items = [{"id": "from_storage"} | settings] assert await storage_setup(items) input_id = "from_storage" @@ -618,16 +616,18 @@ async def test_update_min_max(hass, hass_ws_client, storage_setup): client = await hass_ws_client(hass) + updated_settings = settings | {"minimum": 19} await client.send_json( { "id": 6, "type": f"{DOMAIN}/update", f"{DOMAIN}_id": f"{input_id}", - "minimum": 19, + **updated_settings, } ) resp = await client.receive_json() assert resp["success"] + assert resp["result"] == {"id": "from_storage"} | updated_settings state = hass.states.get(input_entity_id) assert int(state.state) == 19 @@ -635,18 +635,18 @@ async def test_update_min_max(hass, hass_ws_client, storage_setup): assert state.attributes[ATTR_MAXIMUM] == 100 assert state.attributes[ATTR_STEP] == 3 + updated_settings = settings | {"maximum": 5, "minimum": 2, "step": 5} await client.send_json( { "id": 7, "type": f"{DOMAIN}/update", f"{DOMAIN}_id": f"{input_id}", - "maximum": 5, - "minimum": 2, - "step": 5, + **updated_settings, } ) resp = await client.receive_json() assert resp["success"] + assert resp["result"] == {"id": "from_storage"} | updated_settings state = hass.states.get(input_entity_id) assert int(state.state) == 5 @@ -654,18 +654,18 @@ async def test_update_min_max(hass, hass_ws_client, storage_setup): assert state.attributes[ATTR_MAXIMUM] == 5 assert state.attributes[ATTR_STEP] == 5 + updated_settings = settings | {"maximum": None, "minimum": None, "step": 6} await client.send_json( { "id": 8, "type": f"{DOMAIN}/update", f"{DOMAIN}_id": f"{input_id}", - "maximum": None, - "minimum": None, - "step": 6, + **updated_settings, } ) resp = await client.receive_json() assert resp["success"] + assert resp["result"] == {"id": "from_storage"} | updated_settings state = hass.states.get(input_entity_id) assert int(state.state) == 5 From e2a0dd99555acff1414b18a92bae764e2eca56e2 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 13 Sep 2022 20:55:24 +0200 Subject: [PATCH 349/955] Don't allow partial update of input_boolean settings (#78372) --- .../components/input_boolean/__init__.py | 32 ++++--- tests/components/input_boolean/test_init.py | 89 ++++++++++++++++++- 2 files changed, 106 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/input_boolean/__init__.py b/homeassistant/components/input_boolean/__init__.py index e6e99037afa..f1cdb145a7c 100644 --- a/homeassistant/components/input_boolean/__init__.py +++ b/homeassistant/components/input_boolean/__init__.py @@ -37,20 +37,25 @@ _LOGGER = logging.getLogger(__name__) CONF_INITIAL = "initial" -CREATE_FIELDS = { +STORAGE_FIELDS = { vol.Required(CONF_NAME): vol.All(str, vol.Length(min=1)), vol.Optional(CONF_INITIAL): cv.boolean, vol.Optional(CONF_ICON): cv.icon, } -UPDATE_FIELDS = { - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_INITIAL): cv.boolean, - vol.Optional(CONF_ICON): cv.icon, -} - CONFIG_SCHEMA = vol.Schema( - {DOMAIN: cv.schema_with_slug_keys(vol.Any(UPDATE_FIELDS, None))}, + { + DOMAIN: cv.schema_with_slug_keys( + vol.Any( + { + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_INITIAL): cv.boolean, + vol.Optional(CONF_ICON): cv.icon, + }, + None, + ) + ) + }, extra=vol.ALLOW_EXTRA, ) @@ -62,12 +67,11 @@ STORAGE_VERSION = 1 class InputBooleanStorageCollection(collection.StorageCollection): """Input boolean collection stored in storage.""" - CREATE_SCHEMA = vol.Schema(CREATE_FIELDS) - UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS) + CREATE_UPDATE_SCHEMA = vol.Schema(STORAGE_FIELDS) async def _process_create_data(self, data: dict) -> dict: """Validate the config is valid.""" - return self.CREATE_SCHEMA(data) + return self.CREATE_UPDATE_SCHEMA(data) @callback def _get_suggested_id(self, info: dict) -> str: @@ -76,8 +80,8 @@ class InputBooleanStorageCollection(collection.StorageCollection): async def _update_data(self, data: dict, update_data: dict) -> dict: """Return a new updated data object.""" - update_data = self.UPDATE_SCHEMA(update_data) - return {**data, **update_data} + update_data = self.CREATE_UPDATE_SCHEMA(update_data) + return {CONF_ID: data[CONF_ID]} | update_data @bind_hass @@ -118,7 +122,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: await storage_collection.async_load() collection.StorageCollectionWebsocket( - storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS + storage_collection, DOMAIN, DOMAIN, STORAGE_FIELDS, STORAGE_FIELDS ).async_setup(hass) async def reload_service_handler(service_call: ServiceCall) -> None: diff --git a/tests/components/input_boolean/test_init.py b/tests/components/input_boolean/test_init.py index 2b7a1f88ef1..2e044c7a90f 100644 --- a/tests/components/input_boolean/test_init.py +++ b/tests/components/input_boolean/test_init.py @@ -40,7 +40,11 @@ def storage_setup(hass, hass_storage): "data": {"items": [{"id": "from_storage", "name": "from storage"}]}, } else: - hass_storage[DOMAIN] = items + hass_storage[DOMAIN] = { + "key": DOMAIN, + "version": 1, + "data": {"items": items}, + } if config is None: config = {DOMAIN: {}} return await async_setup_component(hass, DOMAIN, config) @@ -332,6 +336,89 @@ async def test_ws_delete(hass, hass_ws_client, storage_setup): assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, input_id) is None +async def test_ws_update(hass, hass_ws_client, storage_setup): + """Test update WS.""" + + settings = { + "name": "from storage", + } + items = [{"id": "from_storage"} | settings] + assert await storage_setup(items) + + input_id = "from_storage" + input_entity_id = f"{DOMAIN}.{input_id}" + ent_reg = er.async_get(hass) + + state = hass.states.get(input_entity_id) + assert state is not None + assert state.state + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, input_id) is not None + + client = await hass_ws_client(hass) + + updated_settings = settings | {"name": "new_name", "icon": "mdi:blah"} + await client.send_json( + { + "id": 6, + "type": f"{DOMAIN}/update", + f"{DOMAIN}_id": f"{input_id}", + **updated_settings, + } + ) + resp = await client.receive_json() + assert resp["success"] + assert resp["result"] == {"id": "from_storage"} | updated_settings + + state = hass.states.get(input_entity_id) + assert state.attributes["icon"] == "mdi:blah" + assert state.attributes["friendly_name"] == "new_name" + + updated_settings = settings | {"name": "new_name_2"} + await client.send_json( + { + "id": 7, + "type": f"{DOMAIN}/update", + f"{DOMAIN}_id": f"{input_id}", + **updated_settings, + } + ) + resp = await client.receive_json() + assert resp["success"] + assert resp["result"] == {"id": "from_storage"} | updated_settings + + state = hass.states.get(input_entity_id) + assert "icon" not in state.attributes + assert state.attributes["friendly_name"] == "new_name_2" + + +async def test_ws_create(hass, hass_ws_client, storage_setup): + """Test create WS.""" + assert await storage_setup(items=[]) + + input_id = "new_input" + input_entity_id = f"{DOMAIN}.{input_id}" + ent_reg = er.async_get(hass) + + state = hass.states.get(input_entity_id) + assert state is None + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, input_id) is None + + client = await hass_ws_client(hass) + + await client.send_json( + { + "id": 6, + "type": f"{DOMAIN}/create", + "name": "New Input", + } + ) + resp = await client.receive_json() + assert resp["success"] + + state = hass.states.get(input_entity_id) + assert state.state + + async def test_setup_no_config(hass, hass_admin_user): """Test component setup with no config.""" count_start = len(hass.states.async_entity_ids()) From 33fa4ec8b257934088d7a9595864a1f8a061025c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 13 Sep 2022 20:56:40 +0200 Subject: [PATCH 350/955] Don't allow partial update of input_datetime settings (#78373) --- .../components/input_datetime/__init__.py | 20 ++++++------------- tests/components/input_datetime/test_init.py | 10 ++++++++-- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/input_datetime/__init__.py b/homeassistant/components/input_datetime/__init__.py index 8b7e81f2c77..afd94ea60f4 100644 --- a/homeassistant/components/input_datetime/__init__.py +++ b/homeassistant/components/input_datetime/__init__.py @@ -61,20 +61,13 @@ def validate_set_datetime_attrs(config): STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 -CREATE_FIELDS = { +STORAGE_FIELDS = { vol.Required(CONF_NAME): vol.All(str, vol.Length(min=1)), vol.Optional(CONF_HAS_DATE, default=False): cv.boolean, vol.Optional(CONF_HAS_TIME, default=False): cv.boolean, vol.Optional(CONF_ICON): cv.icon, vol.Optional(CONF_INITIAL): cv.string, } -UPDATE_FIELDS = { - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_HAS_DATE): cv.boolean, - vol.Optional(CONF_HAS_TIME): cv.boolean, - vol.Optional(CONF_ICON): cv.icon, - vol.Optional(CONF_INITIAL): cv.string, -} def has_date_or_time(conf): @@ -167,7 +160,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: await storage_collection.async_load() collection.StorageCollectionWebsocket( - storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS + storage_collection, DOMAIN, DOMAIN, STORAGE_FIELDS, STORAGE_FIELDS ).async_setup(hass) async def reload_service_handler(service_call: ServiceCall) -> None: @@ -213,12 +206,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: class DateTimeStorageCollection(collection.StorageCollection): """Input storage based collection.""" - CREATE_SCHEMA = vol.Schema(vol.All(CREATE_FIELDS, has_date_or_time)) - UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS) + CREATE_UPDATE_SCHEMA = vol.Schema(vol.All(STORAGE_FIELDS, has_date_or_time)) async def _process_create_data(self, data: dict) -> dict: """Validate the config is valid.""" - return self.CREATE_SCHEMA(data) + return self.CREATE_UPDATE_SCHEMA(data) @callback def _get_suggested_id(self, info: dict) -> str: @@ -227,8 +219,8 @@ class DateTimeStorageCollection(collection.StorageCollection): async def _update_data(self, data: dict, update_data: dict) -> dict: """Return a new updated data object.""" - update_data = self.UPDATE_SCHEMA(update_data) - return has_date_or_time({**data, **update_data}) + update_data = self.CREATE_UPDATE_SCHEMA(update_data) + return {CONF_ID: data[CONF_ID]} | update_data class InputDatetime(collection.CollectionEntity, RestoreEntity): diff --git a/tests/components/input_datetime/test_init.py b/tests/components/input_datetime/test_init.py index 28ca2ab02bd..9e694488797 100644 --- a/tests/components/input_datetime/test_init.py +++ b/tests/components/input_datetime/test_init.py @@ -583,17 +583,23 @@ async def test_update(hass, hass_ws_client, storage_setup): client = await hass_ws_client(hass) + updated_settings = { + CONF_NAME: "even newer name", + CONF_HAS_DATE: False, + CONF_HAS_TIME: True, + CONF_INITIAL: INITIAL_DATETIME, + } await client.send_json( { "id": 6, "type": f"{DOMAIN}/update", f"{DOMAIN}_id": f"{input_id}", - ATTR_NAME: "even newer name", - CONF_HAS_DATE: False, + **updated_settings, } ) resp = await client.receive_json() assert resp["success"] + assert resp["result"] == {"id": "from_storage"} | updated_settings state = hass.states.get(input_entity_id) assert state.state == INITIAL_TIME From abf8b59831607bc886a7cc81a39292e838fdb043 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 13 Sep 2022 20:56:46 +0200 Subject: [PATCH 351/955] Don't allow partial update of input_button settings (#78374) --- .../components/input_button/__init__.py | 30 +++++++++++-------- tests/components/input_button/test_init.py | 1 + 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/input_button/__init__.py b/homeassistant/components/input_button/__init__.py index d59142fb915..14ff940ff64 100644 --- a/homeassistant/components/input_button/__init__.py +++ b/homeassistant/components/input_button/__init__.py @@ -30,18 +30,23 @@ DOMAIN = "input_button" _LOGGER = logging.getLogger(__name__) -CREATE_FIELDS = { +STORAGE_FIELDS = { vol.Required(CONF_NAME): vol.All(str, vol.Length(min=1)), vol.Optional(CONF_ICON): cv.icon, } -UPDATE_FIELDS = { - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_ICON): cv.icon, -} - CONFIG_SCHEMA = vol.Schema( - {DOMAIN: cv.schema_with_slug_keys(vol.Any(UPDATE_FIELDS, None))}, + { + DOMAIN: cv.schema_with_slug_keys( + vol.Any( + { + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_ICON): cv.icon, + }, + None, + ) + ) + }, extra=vol.ALLOW_EXTRA, ) @@ -53,12 +58,11 @@ STORAGE_VERSION = 1 class InputButtonStorageCollection(collection.StorageCollection): """Input button collection stored in storage.""" - CREATE_SCHEMA = vol.Schema(CREATE_FIELDS) - UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS) + CREATE_UPDATE_SCHEMA = vol.Schema(STORAGE_FIELDS) async def _process_create_data(self, data: dict) -> vol.Schema: """Validate the config is valid.""" - return self.CREATE_SCHEMA(data) + return self.CREATE_UPDATE_SCHEMA(data) @callback def _get_suggested_id(self, info: dict) -> str: @@ -67,8 +71,8 @@ class InputButtonStorageCollection(collection.StorageCollection): async def _update_data(self, data: dict, update_data: dict) -> dict: """Return a new updated data object.""" - update_data = self.UPDATE_SCHEMA(update_data) - return {**data, **update_data} + update_data = self.CREATE_UPDATE_SCHEMA(update_data) + return {CONF_ID: data[CONF_ID]} | update_data async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @@ -103,7 +107,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: await storage_collection.async_load() collection.StorageCollectionWebsocket( - storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS + storage_collection, DOMAIN, DOMAIN, STORAGE_FIELDS, STORAGE_FIELDS ).async_setup(hass) async def reload_service_handler(service_call: ServiceCall) -> None: diff --git a/tests/components/input_button/test_init.py b/tests/components/input_button/test_init.py index 33342455147..eb27f277884 100644 --- a/tests/components/input_button/test_init.py +++ b/tests/components/input_button/test_init.py @@ -305,6 +305,7 @@ async def test_ws_create_update(hass, hass_ws_client, storage_setup): ) resp = await client.receive_json() assert resp["success"] + assert resp["result"] == {"id": "new", "name": "newer"} state = hass.states.get(f"{DOMAIN}.new") assert state is not None From 19e853dbb0aad1501d253908fc5a6d8ce9c70e35 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 13 Sep 2022 20:56:59 +0200 Subject: [PATCH 352/955] Don't allow partial update of input_select settings (#78376) --- .../components/input_select/__init__.py | 21 +++------ tests/components/input_select/test_init.py | 45 +++++++++++-------- 2 files changed, 32 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/input_select/__init__.py b/homeassistant/components/input_select/__init__.py index fa582f22cd5..41b079f0888 100644 --- a/homeassistant/components/input_select/__init__.py +++ b/homeassistant/components/input_select/__init__.py @@ -56,7 +56,7 @@ def _unique(options: Any) -> Any: raise HomeAssistantError("Duplicate options are not allowed") from exc -CREATE_FIELDS = { +STORAGE_FIELDS = { vol.Required(CONF_NAME): vol.All(str, vol.Length(min=1)), vol.Required(CONF_OPTIONS): vol.All( cv.ensure_list, vol.Length(min=1), _unique, [cv.string] @@ -64,14 +64,6 @@ CREATE_FIELDS = { vol.Optional(CONF_INITIAL): cv.string, vol.Optional(CONF_ICON): cv.icon, } -UPDATE_FIELDS = { - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_OPTIONS): vol.All( - cv.ensure_list, vol.Length(min=1), _unique, [cv.string] - ), - vol.Optional(CONF_INITIAL): cv.string, - vol.Optional(CONF_ICON): cv.icon, -} def _remove_duplicates(options: list[str], name: str | None) -> list[str]: @@ -172,7 +164,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: await storage_collection.async_load() collection.StorageCollectionWebsocket( - storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS + storage_collection, DOMAIN, DOMAIN, STORAGE_FIELDS, STORAGE_FIELDS ).async_setup(hass) async def reload_service_handler(service_call: ServiceCall) -> None: @@ -238,12 +230,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: class InputSelectStorageCollection(collection.StorageCollection): """Input storage based collection.""" - CREATE_SCHEMA = vol.Schema(vol.All(CREATE_FIELDS, _cv_input_select)) - UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS) + CREATE_UPDATE_SCHEMA = vol.Schema(vol.All(STORAGE_FIELDS, _cv_input_select)) async def _process_create_data(self, data: dict[str, Any]) -> dict[str, Any]: """Validate the config is valid.""" - return cast(dict[str, Any], self.CREATE_SCHEMA(data)) + return cast(dict[str, Any], self.CREATE_UPDATE_SCHEMA(data)) @callback def _get_suggested_id(self, info: dict[str, Any]) -> str: @@ -254,8 +245,8 @@ class InputSelectStorageCollection(collection.StorageCollection): self, data: dict[str, Any], update_data: dict[str, Any] ) -> dict[str, Any]: """Return a new updated data object.""" - update_data = self.UPDATE_SCHEMA(update_data) - return _cv_input_select({**data, **update_data}) + update_data = self.CREATE_UPDATE_SCHEMA(update_data) + return {CONF_ID: data[CONF_ID]} | update_data class InputSelect(collection.CollectionEntity, SelectEntity, RestoreEntity): diff --git a/tests/components/input_select/test_init.py b/tests/components/input_select/test_init.py index d65140dcbf9..1a1618d7805 100644 --- a/tests/components/input_select/test_init.py +++ b/tests/components/input_select/test_init.py @@ -628,13 +628,11 @@ async def test_ws_delete(hass, hass_ws_client, storage_setup): async def test_update(hass, hass_ws_client, storage_setup): """Test updating options updates the state.""" - items = [ - { - "id": "from_storage", - "name": "from storage", - "options": ["yaml update 1", "yaml update 2"], - } - ] + settings = { + "name": "from storage", + "options": ["yaml update 1", "yaml update 2"], + } + items = [{"id": "from_storage"} | settings] assert await storage_setup(items) input_id = "from_storage" @@ -647,28 +645,36 @@ async def test_update(hass, hass_ws_client, storage_setup): client = await hass_ws_client(hass) + updated_settings = settings | { + "options": ["new option", "newer option"], + CONF_INITIAL: "newer option", + } await client.send_json( { "id": 6, "type": f"{DOMAIN}/update", f"{DOMAIN}_id": f"{input_id}", - "options": ["new option", "newer option"], - CONF_INITIAL: "newer option", + **updated_settings, } ) resp = await client.receive_json() assert resp["success"] + assert resp["result"] == {"id": "from_storage"} | updated_settings state = hass.states.get(input_entity_id) assert state.attributes[ATTR_OPTIONS] == ["new option", "newer option"] # Should fail because the initial state is now invalid + updated_settings = settings | { + "options": ["new option", "no newer option"], + CONF_INITIAL: "newer option", + } await client.send_json( { "id": 7, "type": f"{DOMAIN}/update", f"{DOMAIN}_id": f"{input_id}", - "options": ["new option", "no newer option"], + **updated_settings, } ) resp = await client.receive_json() @@ -678,13 +684,11 @@ async def test_update(hass, hass_ws_client, storage_setup): async def test_update_duplicates(hass, hass_ws_client, storage_setup, caplog): """Test updating options updates the state.""" - items = [ - { - "id": "from_storage", - "name": "from storage", - "options": ["yaml update 1", "yaml update 2"], - } - ] + settings = { + "name": "from storage", + "options": ["yaml update 1", "yaml update 2"], + } + items = [{"id": "from_storage"} | settings] assert await storage_setup(items) input_id = "from_storage" @@ -697,13 +701,16 @@ async def test_update_duplicates(hass, hass_ws_client, storage_setup, caplog): client = await hass_ws_client(hass) + updated_settings = settings | { + "options": ["new option", "newer option", "newer option"], + CONF_INITIAL: "newer option", + } await client.send_json( { "id": 6, "type": f"{DOMAIN}/update", f"{DOMAIN}_id": f"{input_id}", - "options": ["new option", "newer option", "newer option"], - CONF_INITIAL: "newer option", + **updated_settings, } ) resp = await client.receive_json() From 925a4b028693c681554bb402e328c2a3ddfb77fd Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 13 Sep 2022 20:57:14 +0200 Subject: [PATCH 353/955] Don't allow partial update of input_text settings (#78377) --- .../components/input_text/__init__.py | 23 +++++-------------- tests/components/input_text/test_init.py | 13 +++++++---- 2 files changed, 15 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/input_text/__init__.py b/homeassistant/components/input_text/__init__.py index ac6557dad91..072f17c72a3 100644 --- a/homeassistant/components/input_text/__init__.py +++ b/homeassistant/components/input_text/__init__.py @@ -51,7 +51,7 @@ SERVICE_SET_VALUE = "set_value" STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 -CREATE_FIELDS = { +STORAGE_FIELDS = { vol.Required(CONF_NAME): vol.All(str, vol.Length(min=1)), vol.Optional(CONF_MIN, default=CONF_MIN_VALUE): vol.Coerce(int), vol.Optional(CONF_MAX, default=CONF_MAX_VALUE): vol.Coerce(int), @@ -61,16 +61,6 @@ CREATE_FIELDS = { vol.Optional(CONF_PATTERN): cv.string, vol.Optional(CONF_MODE, default=MODE_TEXT): vol.In([MODE_TEXT, MODE_PASSWORD]), } -UPDATE_FIELDS = { - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_MIN): vol.Coerce(int), - vol.Optional(CONF_MAX): vol.Coerce(int), - vol.Optional(CONF_INITIAL): cv.string, - vol.Optional(CONF_ICON): cv.icon, - vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, - vol.Optional(CONF_PATTERN): cv.string, - vol.Optional(CONF_MODE): vol.In([MODE_TEXT, MODE_PASSWORD]), -} def _cv_input_text(cfg): @@ -147,7 +137,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: await storage_collection.async_load() collection.StorageCollectionWebsocket( - storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS + storage_collection, DOMAIN, DOMAIN, STORAGE_FIELDS, STORAGE_FIELDS ).async_setup(hass) async def reload_service_handler(service_call: ServiceCall) -> None: @@ -177,12 +167,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: class InputTextStorageCollection(collection.StorageCollection): """Input storage based collection.""" - CREATE_SCHEMA = vol.Schema(vol.All(CREATE_FIELDS, _cv_input_text)) - UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS) + CREATE_UPDATE_SCHEMA = vol.Schema(vol.All(STORAGE_FIELDS, _cv_input_text)) async def _process_create_data(self, data: dict) -> dict: """Validate the config is valid.""" - return self.CREATE_SCHEMA(data) + return self.CREATE_UPDATE_SCHEMA(data) @callback def _get_suggested_id(self, info: dict) -> str: @@ -191,8 +180,8 @@ class InputTextStorageCollection(collection.StorageCollection): async def _update_data(self, data: dict, update_data: dict) -> dict: """Return a new updated data object.""" - update_data = self.UPDATE_SCHEMA(update_data) - return _cv_input_text({**data, **update_data}) + update_data = self.CREATE_UPDATE_SCHEMA(update_data) + return {CONF_ID: data[CONF_ID]} | update_data class InputText(collection.CollectionEntity, RestoreEntity): diff --git a/tests/components/input_text/test_init.py b/tests/components/input_text/test_init.py index 48f9551a65b..8256d9d351f 100644 --- a/tests/components/input_text/test_init.py +++ b/tests/components/input_text/test_init.py @@ -432,19 +432,24 @@ async def test_update(hass, hass_ws_client, storage_setup): client = await hass_ws_client(hass) + updated_settings = { + ATTR_NAME: "even newer name", + CONF_INITIAL: "newer option", + ATTR_MAX: TEST_VAL_MAX, + ATTR_MIN: 6, + ATTR_MODE: "password", + } await client.send_json( { "id": 6, "type": f"{DOMAIN}/update", f"{DOMAIN}_id": f"{input_id}", - ATTR_NAME: "even newer name", - CONF_INITIAL: "newer option", - ATTR_MIN: 6, - ATTR_MODE: "password", + **updated_settings, } ) resp = await client.receive_json() assert resp["success"] + assert resp["result"] == {"id": "from_storage"} | updated_settings state = hass.states.get(input_entity_id) assert state.state == "loaded from storage" From 4898a41dcf12b9201e1da35e025d68f1899ce196 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 13 Sep 2022 20:58:12 +0200 Subject: [PATCH 354/955] Don't allow partial update of timer settings (#78378) --- homeassistant/components/timer/__init__.py | 17 +++++------------ tests/components/timer/test_init.py | 14 ++++++++++++-- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/timer/__init__.py b/homeassistant/components/timer/__init__.py index 53912a4dec8..ff50e96a18c 100644 --- a/homeassistant/components/timer/__init__.py +++ b/homeassistant/components/timer/__init__.py @@ -61,18 +61,12 @@ SERVICE_FINISH = "finish" STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 -CREATE_FIELDS = { +STORAGE_FIELDS = { vol.Required(CONF_NAME): cv.string, vol.Optional(CONF_ICON): cv.icon, vol.Optional(CONF_DURATION, default=DEFAULT_DURATION): cv.time_period, vol.Optional(CONF_RESTORE, default=DEFAULT_RESTORE): cv.boolean, } -UPDATE_FIELDS = { - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_ICON): cv.icon, - vol.Optional(CONF_DURATION): cv.time_period, - vol.Optional(CONF_RESTORE): cv.boolean, -} def _format_timedelta(delta: timedelta): @@ -137,7 +131,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: await storage_collection.async_load() collection.StorageCollectionWebsocket( - storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS + storage_collection, DOMAIN, DOMAIN, STORAGE_FIELDS, STORAGE_FIELDS ).async_setup(hass) async def reload_service_handler(service_call: ServiceCall) -> None: @@ -171,12 +165,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: class TimerStorageCollection(collection.StorageCollection): """Timer storage based collection.""" - CREATE_SCHEMA = vol.Schema(CREATE_FIELDS) - UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS) + CREATE_UPDATE_SCHEMA = vol.Schema(STORAGE_FIELDS) async def _process_create_data(self, data: dict) -> dict: """Validate the config is valid.""" - data = self.CREATE_SCHEMA(data) + data = self.CREATE_UPDATE_SCHEMA(data) # make duration JSON serializeable data[CONF_DURATION] = _format_timedelta(data[CONF_DURATION]) return data @@ -188,7 +181,7 @@ class TimerStorageCollection(collection.StorageCollection): async def _update_data(self, data: dict, update_data: dict) -> dict: """Return a new updated data object.""" - data = {**data, **self.UPDATE_SCHEMA(update_data)} + data = {CONF_ID: data[CONF_ID]} | self.CREATE_UPDATE_SCHEMA(update_data) # make duration JSON serializeable if CONF_DURATION in update_data: data[CONF_DURATION] = _format_timedelta(data[CONF_DURATION]) diff --git a/tests/components/timer/test_init.py b/tests/components/timer/test_init.py index b51200c6ad0..ac2dde57c8d 100644 --- a/tests/components/timer/test_init.py +++ b/tests/components/timer/test_init.py @@ -585,17 +585,27 @@ async def test_update(hass, hass_ws_client, storage_setup): client = await hass_ws_client(hass) + updated_settings = { + CONF_NAME: "timer from storage", + CONF_DURATION: 33, + CONF_RESTORE: True, + } await client.send_json( { "id": 6, "type": f"{DOMAIN}/update", f"{DOMAIN}_id": f"{timer_id}", - CONF_DURATION: 33, - CONF_RESTORE: True, + **updated_settings, } ) resp = await client.receive_json() assert resp["success"] + assert resp["result"] == { + "id": "from_storage", + CONF_DURATION: "0:00:33", + CONF_NAME: "timer from storage", + CONF_RESTORE: True, + } state = hass.states.get(timer_entity_id) assert state.attributes[ATTR_DURATION] == _format_timedelta(cv.time_period(33)) From be52b66f6cfc87f89d04e53d26c1d740d41ef87b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 13 Sep 2022 21:00:45 +0200 Subject: [PATCH 355/955] Bump aiohomekit to 1.5.7 (#78369) --- 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 a4cbbc227d9..0b67f80bac5 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==1.5.6"], + "requirements": ["aiohomekit==1.5.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 cacd04f2c76..b5e596c8fa6 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==1.5.6 +aiohomekit==1.5.7 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1a48725a130..9e1792ce63d 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==1.5.6 +aiohomekit==1.5.7 # homeassistant.components.emulated_hue # homeassistant.components.http From 4da08ee1e9689ed68d58598a3dfc25cf8a97296f Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 13 Sep 2022 21:01:46 +0200 Subject: [PATCH 356/955] Fix CI workflow caching (#78398) --- .github/workflows/ci.yaml | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 02e50ddcc65..8c507ef1c9e 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -169,7 +169,6 @@ jobs: uses: actions/setup-python@v4.1.0 with: python-version: ${{ env.DEFAULT_PYTHON }} - cache: "pip" - name: Restore base Python virtual environment id: cache-venv uses: actions/cache@v3.0.8 @@ -484,7 +483,7 @@ jobs: with: path: venv key: >- - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + ${{ runner.os }}-${{ matrix.python-version }}-${{ needs.info.outputs.python_cache_key }} - name: Restore pip wheel cache if: steps.cache-venv.outputs.cache-hit != 'true' @@ -492,10 +491,10 @@ jobs: with: path: ${{ env.PIP_CACHE }} key: >- - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + ${{ runner.os }}-${{ matrix.python-version }}-${{ steps.generate-pip-key.outputs.key }} restore-keys: | - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-pip-${{ env.PIP_CACHE_VERSION }}-${{ env.HA_SHORT_VERSION }}- + ${{ runner.os }}-${{ matrix.python-version }}-pip-${{ env.PIP_CACHE_VERSION }}-${{ env.HA_SHORT_VERSION }}- - name: Install additional OS dependencies if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -542,7 +541,7 @@ jobs: with: path: venv key: >- - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-${{ needs.info.outputs.python_cache_key }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' @@ -574,7 +573,7 @@ jobs: with: path: venv key: >- - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-${{ needs.info.outputs.python_cache_key }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' @@ -607,7 +606,7 @@ jobs: with: path: venv key: >- - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-${{ needs.info.outputs.python_cache_key }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' @@ -651,7 +650,7 @@ jobs: with: path: venv key: >- - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-${{ needs.info.outputs.python_cache_key }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' @@ -699,7 +698,7 @@ jobs: with: path: venv key: >- - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + ${{ runner.os }}-${{ matrix.python-version }}-${{ needs.info.outputs.python_cache_key }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' @@ -752,7 +751,7 @@ jobs: uses: actions/cache@v3.0.8 with: path: venv - key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + key: ${{ runner.os }}-${{ matrix.python-version }}-${{ needs.info.outputs.python_cache_key }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' From dc3c4a2b54f6baec735a18e6a659135f31be3ba8 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 13 Sep 2022 21:05:55 +0200 Subject: [PATCH 357/955] Expose SOURCE_CLOUD in google-assistant root (#78394) --- homeassistant/components/google_assistant/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/google_assistant/__init__.py b/homeassistant/components/google_assistant/__init__.py index 638ccfd9133..644179858a3 100644 --- a/homeassistant/components/google_assistant/__init__.py +++ b/homeassistant/components/google_assistant/__init__.py @@ -11,7 +11,7 @@ from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers.typing import ConfigType -from .const import ( +from .const import ( # noqa: F401 CONF_ALIASES, CONF_CLIENT_EMAIL, CONF_ENTITY_CONFIG, @@ -29,6 +29,7 @@ from .const import ( DEFAULT_EXPOSED_DOMAINS, DOMAIN, SERVICE_REQUEST_SYNC, + SOURCE_CLOUD, ) from .const import EVENT_QUERY_RECEIVED # noqa: F401 from .http import GoogleAssistantView, GoogleConfig From 9c2601036d24e9576d361cdfc1f9b84bc1510976 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 13 Sep 2022 21:06:17 +0200 Subject: [PATCH 358/955] Bump xiaomi-ble to 0.10.0 (#78365) --- homeassistant/components/xiaomi_ble/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_ble/manifest.json b/homeassistant/components/xiaomi_ble/manifest.json index de8e61ad8ce..56efd9e966a 100644 --- a/homeassistant/components/xiaomi_ble/manifest.json +++ b/homeassistant/components/xiaomi_ble/manifest.json @@ -9,7 +9,7 @@ "service_data_uuid": "0000fe95-0000-1000-8000-00805f9b34fb" } ], - "requirements": ["xiaomi-ble==0.9.3"], + "requirements": ["xiaomi-ble==0.10.0"], "dependencies": ["bluetooth"], "codeowners": ["@Jc2k", "@Ernst79"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index b5e596c8fa6..792c763881b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2537,7 +2537,7 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.9.3 +xiaomi-ble==0.10.0 # homeassistant.components.knx xknx==1.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9e1792ce63d..49733260953 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1744,7 +1744,7 @@ wolf_smartset==0.1.11 xbox-webapi==2.0.11 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.9.3 +xiaomi-ble==0.10.0 # homeassistant.components.knx xknx==1.0.2 From 0ab19fe6f6349f1af9e9eea17c8275a6d9756d38 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 13 Sep 2022 21:11:57 +0200 Subject: [PATCH 359/955] Bump aioecowitt 2022.09.2 (#78287) * Bump aioecowitt 2022.09.2 * add percentage type --- homeassistant/components/ecowitt/manifest.json | 2 +- homeassistant/components/ecowitt/sensor.py | 5 +++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ecowitt/manifest.json b/homeassistant/components/ecowitt/manifest.json index 224b4440e36..4306311d2ec 100644 --- a/homeassistant/components/ecowitt/manifest.json +++ b/homeassistant/components/ecowitt/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ecowitt", "dependencies": ["webhook"], - "requirements": ["aioecowitt==2022.09.1"], + "requirements": ["aioecowitt==2022.09.2"], "codeowners": ["@pvizeli"], "iot_class": "local_push" } diff --git a/homeassistant/components/ecowitt/sensor.py b/homeassistant/components/ecowitt/sensor.py index bb580b6d4b7..a644cd3ca7a 100644 --- a/homeassistant/components/ecowitt/sensor.py +++ b/homeassistant/components/ecowitt/sensor.py @@ -196,6 +196,11 @@ ECOWITT_SENSORS_MAPPING: Final = { native_unit_of_measurement=PRESSURE_INHG, state_class=SensorStateClass.MEASUREMENT, ), + EcoWittSensorTypes.PERCENTAGE: SensorEntityDescription( + key="PERCENTAGE", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), } diff --git a/requirements_all.txt b/requirements_all.txt index 792c763881b..d25968e471e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -147,7 +147,7 @@ aioeafm==0.1.2 aioeagle==1.1.0 # homeassistant.components.ecowitt -aioecowitt==2022.09.1 +aioecowitt==2022.09.2 # homeassistant.components.emonitor aioemonitor==1.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 49733260953..9f30f2418fd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -134,7 +134,7 @@ aioeafm==0.1.2 aioeagle==1.1.0 # homeassistant.components.ecowitt -aioecowitt==2022.09.1 +aioecowitt==2022.09.2 # homeassistant.components.emonitor aioemonitor==1.0.5 From 49ab5cfc9ca76696751d1451959e1db3892eb80c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 13 Sep 2022 21:55:13 +0200 Subject: [PATCH 360/955] Improve type hints in geo-location (#78352) --- .../components/geo_location/trigger.py | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/geo_location/trigger.py b/homeassistant/components/geo_location/trigger.py index bc04490e76c..24632e78454 100644 --- a/homeassistant/components/geo_location/trigger.py +++ b/homeassistant/components/geo_location/trigger.py @@ -1,10 +1,20 @@ """Offer geolocation automation rules.""" +from __future__ import annotations + import logging +from typing import Final import voluptuous as vol from homeassistant.const import CONF_EVENT, CONF_PLATFORM, CONF_SOURCE, CONF_ZONE -from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback +from homeassistant.core import ( + CALLBACK_TYPE, + Event, + HassJob, + HomeAssistant, + State, + callback, +) from homeassistant.helpers import condition, config_validation as cv from homeassistant.helpers.config_validation import entity_domain from homeassistant.helpers.event import TrackStates, async_track_state_change_filtered @@ -13,13 +23,11 @@ from homeassistant.helpers.typing import ConfigType from . import DOMAIN -# mypy: allow-untyped-defs, no-check-untyped-defs - _LOGGER = logging.getLogger(__name__) -EVENT_ENTER = "enter" -EVENT_LEAVE = "leave" -DEFAULT_EVENT = EVENT_ENTER +EVENT_ENTER: Final = "enter" +EVENT_LEAVE: Final = "leave" +DEFAULT_EVENT: Final = EVENT_ENTER TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( { @@ -33,9 +41,9 @@ TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( ) -def source_match(state, source): +def source_match(state: State | None, source: str) -> bool: """Check if the state matches the provided source.""" - return state and state.attributes.get("source") == source + return state is not None and state.attributes.get("source") == source async def async_attach_trigger( @@ -47,12 +55,12 @@ async def async_attach_trigger( """Listen for state changes based on configuration.""" trigger_data = trigger_info["trigger_data"] source: str = config[CONF_SOURCE].lower() - zone_entity_id = config.get(CONF_ZONE) - trigger_event = config.get(CONF_EVENT) + zone_entity_id: str = config[CONF_ZONE] + trigger_event: str = config[CONF_EVENT] job = HassJob(action) @callback - def state_change_listener(event): + def state_change_listener(event: Event) -> None: """Handle specific state changes.""" # Skip if the event's source does not match the trigger's source. from_state = event.data.get("old_state") From 02c95418622ffe5a221fa5e75a56d16f102f4efd Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 13 Sep 2022 21:55:50 +0200 Subject: [PATCH 361/955] Improve type hints in mailbox (#78353) --- .../components/asterisk_mbox/mailbox.py | 28 +++++----- homeassistant/components/mailbox/__init__.py | 51 ++++++++++--------- pylint/plugins/hass_enforce_type_hints.py | 33 ++++++++++++ 3 files changed, 76 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/asterisk_mbox/mailbox.py b/homeassistant/components/asterisk_mbox/mailbox.py index 04d2be70704..edf95cb3787 100644 --- a/homeassistant/components/asterisk_mbox/mailbox.py +++ b/homeassistant/components/asterisk_mbox/mailbox.py @@ -3,6 +3,7 @@ from __future__ import annotations from functools import partial import logging +from typing import Any from asterisk_mbox import ServerError @@ -11,7 +12,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import DOMAIN as ASTERISK_DOMAIN +from . import DOMAIN as ASTERISK_DOMAIN, AsteriskData _LOGGER = logging.getLogger(__name__) @@ -31,7 +32,7 @@ async def async_get_handler( class AsteriskMailbox(Mailbox): """Asterisk VM Sensor.""" - def __init__(self, hass, name): + def __init__(self, hass: HomeAssistant, name: str) -> None: """Initialize Asterisk mailbox.""" super().__init__(hass, name) async_dispatcher_connect( @@ -39,29 +40,30 @@ class AsteriskMailbox(Mailbox): ) @callback - def _update_callback(self, msg): + def _update_callback(self, msg: str) -> None: """Update the message count in HA, if needed.""" self.async_update() @property - def media_type(self): + def media_type(self) -> str: """Return the supported media type.""" return CONTENT_TYPE_MPEG @property - def can_delete(self): + def can_delete(self) -> bool: """Return if messages can be deleted.""" return True @property - def has_media(self): + def has_media(self) -> bool: """Return if messages have attached media files.""" return True - async def async_get_media(self, msgid): + async def async_get_media(self, msgid: str) -> bytes: """Return the media blob for the msgid.""" - client = self.hass.data[ASTERISK_DOMAIN].client + data: AsteriskData = self.hass.data[ASTERISK_DOMAIN] + client = data.client try: return await self.hass.async_add_executor_job( partial(client.mp3, msgid, sync=True) @@ -69,13 +71,15 @@ class AsteriskMailbox(Mailbox): except ServerError as err: raise StreamError(err) from err - async def async_get_messages(self): + async def async_get_messages(self) -> list[dict[str, Any]]: """Return a list of the current messages.""" - return self.hass.data[ASTERISK_DOMAIN].messages + data: AsteriskData = self.hass.data[ASTERISK_DOMAIN] + return data.messages - async def async_delete(self, msgid): + async def async_delete(self, msgid: str) -> bool: """Delete the specified messages.""" - client = self.hass.data[ASTERISK_DOMAIN].client + data: AsteriskData = self.hass.data[ASTERISK_DOMAIN] + client = data.client _LOGGER.info("Deleting: %s", msgid) await self.hass.async_add_executor_job(client.delete, msgid) return True diff --git a/homeassistant/components/mailbox/__init__.py b/homeassistant/components/mailbox/__init__.py index b6a727083c9..4e65d989b98 100644 --- a/homeassistant/components/mailbox/__init__.py +++ b/homeassistant/components/mailbox/__init__.py @@ -6,6 +6,7 @@ from contextlib import suppress from datetime import timedelta from http import HTTPStatus import logging +from typing import Any, Final from aiohttp import web from aiohttp.web_exceptions import HTTPNotFound @@ -13,7 +14,7 @@ import async_timeout from homeassistant.components import frontend from homeassistant.components.http import HomeAssistantView -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_per_platform, discovery from homeassistant.helpers.entity import Entity @@ -21,15 +22,13 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.setup import async_prepare_setup_platform -# mypy: allow-untyped-defs, no-check-untyped-defs - _LOGGER = logging.getLogger(__name__) -DOMAIN = "mailbox" +DOMAIN: Final = "mailbox" -EVENT = "mailbox_updated" -CONTENT_TYPE_MPEG = "audio/mpeg" -CONTENT_TYPE_NONE = "none" +EVENT: Final = "mailbox_updated" +CONTENT_TYPE_MPEG: Final = "audio/mpeg" +CONTENT_TYPE_NONE: Final = "none" SCAN_INTERVAL = timedelta(seconds=30) @@ -98,7 +97,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if setup_tasks: await asyncio.wait(setup_tasks) - async def async_platform_discovered(platform, info): + async def async_platform_discovered( + platform: str, info: DiscoveryInfoType | None + ) -> None: """Handle for discovered platform.""" await async_setup_platform(platform, discovery_info=info) @@ -115,27 +116,27 @@ class MailboxEntity(Entity): self.mailbox = mailbox self.message_count = 0 - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Complete entity initialization.""" @callback - def _mailbox_updated(event): + def _mailbox_updated(event: Event) -> None: self.async_schedule_update_ha_state(True) self.hass.bus.async_listen(EVENT, _mailbox_updated) self.async_schedule_update_ha_state(True) @property - def state(self): + def state(self) -> str: """Return the state of the binary sensor.""" return str(self.message_count) @property - def name(self): + def name(self) -> str: """Return the name of the entity.""" return self.mailbox.name - async def async_update(self): + async def async_update(self) -> None: """Retrieve messages from platform.""" messages = await self.mailbox.async_get_messages() self.message_count = len(messages) @@ -155,29 +156,29 @@ class Mailbox: self.hass.bus.async_fire(EVENT) @property - def media_type(self): + def media_type(self) -> str: """Return the supported media type.""" raise NotImplementedError() @property - def can_delete(self): + def can_delete(self) -> bool: """Return if messages can be deleted.""" return False @property - def has_media(self): + def has_media(self) -> bool: """Return if messages have attached media files.""" return False - async def async_get_media(self, msgid): + async def async_get_media(self, msgid: str) -> bytes: """Return the media blob for the msgid.""" raise NotImplementedError() - async def async_get_messages(self): + async def async_get_messages(self) -> list[dict[str, Any]]: """Return a list of the current messages.""" raise NotImplementedError() - async def async_delete(self, msgid): + async def async_delete(self, msgid: str) -> bool: """Delete the specified messages.""" raise NotImplementedError() @@ -193,7 +194,7 @@ class MailboxView(HomeAssistantView): """Initialize a basic mailbox view.""" self.mailboxes = mailboxes - def get_mailbox(self, platform): + def get_mailbox(self, platform: str) -> Mailbox: """Retrieve the specified mailbox.""" for mailbox in self.mailboxes: if mailbox.name == platform: @@ -209,7 +210,7 @@ class MailboxPlatformsView(MailboxView): async def get(self, request: web.Request) -> web.Response: """Retrieve list of platforms.""" - platforms = [] + platforms: list[dict[str, Any]] = [] for mailbox in self.mailboxes: platforms.append( { @@ -227,7 +228,7 @@ class MailboxMessageView(MailboxView): url = "/api/mailbox/messages/{platform}" name = "api:mailbox:messages" - async def get(self, request, platform): + async def get(self, request: web.Request, platform: str) -> web.Response: """Retrieve messages.""" mailbox = self.get_mailbox(platform) messages = await mailbox.async_get_messages() @@ -240,7 +241,7 @@ class MailboxDeleteView(MailboxView): url = "/api/mailbox/delete/{platform}/{msgid}" name = "api:mailbox:delete" - async def delete(self, request, platform, msgid): + async def delete(self, request: web.Request, platform: str, msgid: str) -> None: """Delete items.""" mailbox = self.get_mailbox(platform) await mailbox.async_delete(msgid) @@ -252,7 +253,9 @@ class MailboxMediaView(MailboxView): url = r"/api/mailbox/media/{platform}/{msgid}" name = "api:asteriskmbox:media" - async def get(self, request, platform, msgid): + async def get( + self, request: web.Request, platform: str, msgid: str + ) -> web.Response: """Retrieve media.""" mailbox = self.get_mailbox(platform) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index cb0c6ecc2e7..7cd94b3181c 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -1539,6 +1539,39 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ], ), ], + "mailbox": [ + ClassTypeHintMatch( + base_class="Mailbox", + matches=[ + TypeHintMatch( + function_name="media_type", + return_type="str", + ), + TypeHintMatch( + function_name="can_delete", + return_type="bool", + ), + TypeHintMatch( + function_name="has_media", + return_type="bool", + ), + TypeHintMatch( + function_name="async_get_media", + arg_types={1: "str"}, + return_type="bytes", + ), + TypeHintMatch( + function_name="async_get_messages", + return_type="list[dict[str, Any]]", + ), + TypeHintMatch( + function_name="async_delete", + arg_types={1: "str"}, + return_type="bool", + ), + ], + ), + ], "media_player": [ ClassTypeHintMatch( base_class="Entity", From bf852812bcfc18927a76ccbb300a3d6ee88b29f0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 13 Sep 2022 22:10:50 +0200 Subject: [PATCH 362/955] Fix flapping system log test (#78391) Since we run tests with asyncio debug on, there is a chance we will get an asyncio log message instead of the one we want Fixes https://github.com/home-assistant/core/actions/runs/3045080236/jobs/4906717578 --- tests/components/system_log/test_init.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/system_log/test_init.py b/tests/components/system_log/test_init.py index 6304e0ea7cf..96e5480acb5 100644 --- a/tests/components/system_log/test_init.py +++ b/tests/components/system_log/test_init.py @@ -41,7 +41,7 @@ def find_log(logs, level): if not isinstance(level, tuple): level = (level,) log = next( - (log for log in logs if log["level"] in level), + (log for log in logs if log["level"] in level and log["name"] != "asyncio"), None, ) assert log is not None From 32c6f8aaef309433e625a36f341a913c77585302 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 13 Sep 2022 22:11:44 +0200 Subject: [PATCH 363/955] Bump PySwitchbot to 0.19.8 (#78361) * Bump PySwitchbot to 0.19.7 Changes for bleak 0.17 https://github.com/Danielhiversen/pySwitchbot/compare/0.19.6...0.19.7 * bump again to fix some more stale state bugs --- 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 b2a5b68deae..41b3f5aa61b 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.19.6"], + "requirements": ["PySwitchbot==0.19.8"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index d25968e471e..8e8d06a408d 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.19.6 +PySwitchbot==0.19.8 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9f30f2418fd..eca64c75b72 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.19.6 +PySwitchbot==0.19.8 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From e2c563c79d460342f89d883b7dd51be5c75a7ba4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 13 Sep 2022 22:14:53 +0200 Subject: [PATCH 364/955] Bump yalexs-ble to 1.9.0 (#78362) --- homeassistant/components/yalexs_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/yalexs_ble/manifest.json b/homeassistant/components/yalexs_ble/manifest.json index f3a9cc798e6..673521f9e06 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.8.1"], + "requirements": ["yalexs-ble==1.9.0"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "bluetooth": [ diff --git a/requirements_all.txt b/requirements_all.txt index 8e8d06a408d..491f7674367 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2557,7 +2557,7 @@ xs1-api-client==3.0.0 yalesmartalarmclient==0.3.9 # homeassistant.components.yalexs_ble -yalexs-ble==1.8.1 +yalexs-ble==1.9.0 # homeassistant.components.august yalexs==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index eca64c75b72..7c977ba034b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1761,7 +1761,7 @@ xmltodict==0.13.0 yalesmartalarmclient==0.3.9 # homeassistant.components.yalexs_ble -yalexs-ble==1.8.1 +yalexs-ble==1.9.0 # homeassistant.components.august yalexs==1.2.1 From 8189af0e7e569738992e8e935535e73fa24aee36 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 13 Sep 2022 22:15:30 +0200 Subject: [PATCH 365/955] Bump led-ble to 0.10.0 (#78367) --- homeassistant/components/led_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/led_ble/manifest.json b/homeassistant/components/led_ble/manifest.json index 1f27837b89e..89b4fdb26af 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/ble_ble", - "requirements": ["led-ble==0.9.1"], + "requirements": ["led-ble==0.10.0"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "bluetooth": [ diff --git a/requirements_all.txt b/requirements_all.txt index 491f7674367..25bd0d13480 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -974,7 +974,7 @@ lakeside==0.12 laundrify_aio==1.1.2 # homeassistant.components.led_ble -led-ble==0.9.1 +led-ble==0.10.0 # homeassistant.components.foscam libpyfoscam==1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7c977ba034b..268f83428c8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -712,7 +712,7 @@ lacrosse-view==0.0.9 laundrify_aio==1.1.2 # homeassistant.components.led_ble -led-ble==0.9.1 +led-ble==0.10.0 # homeassistant.components.foscam libpyfoscam==1.0 From 4c164cc48defbcaed60295a1846daa366e749aab Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 13 Sep 2022 22:15:45 +0200 Subject: [PATCH 366/955] Update frontend to 20220907.1 (#78404) --- 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 07822979683..42d7dd4b0fa 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==20220907.0"], + "requirements": ["home-assistant-frontend==20220907.1"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b511248a602..ef5ff53c4a4 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -21,7 +21,7 @@ dbus-fast==1.4.0 fnvhash==0.1.0 hass-nabucasa==0.55.0 home-assistant-bluetooth==1.3.0 -home-assistant-frontend==20220907.0 +home-assistant-frontend==20220907.1 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index 25bd0d13480..7dc944052c7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -857,7 +857,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220907.0 +home-assistant-frontend==20220907.1 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 268f83428c8..32bda788cfa 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -634,7 +634,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220907.0 +home-assistant-frontend==20220907.1 # homeassistant.components.home_connect homeconnect==0.7.2 From 13e8bae43282d0709ad9925a746a5afcbd548088 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 13 Sep 2022 22:17:09 +0200 Subject: [PATCH 367/955] Bump govee-ble to 0.17.3 (#78405) --- homeassistant/components/govee_ble/manifest.json | 7 ++++++- homeassistant/generated/bluetooth.py | 6 ++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/govee_ble/manifest.json b/homeassistant/components/govee_ble/manifest.json index 2ce68498968..537ae9c7ed5 100644 --- a/homeassistant/components/govee_ble/manifest.json +++ b/homeassistant/components/govee_ble/manifest.json @@ -17,6 +17,11 @@ "service_uuid": "00008351-0000-1000-8000-00805f9b34fb", "connectable": false }, + { + "manufacturer_id": 57391, + "service_uuid": "00008351-0000-1000-8000-00805f9b34fb", + "connectable": false + }, { "manufacturer_id": 18994, "service_uuid": "00008551-0000-1000-8000-00805f9b34fb", @@ -53,7 +58,7 @@ "connectable": false } ], - "requirements": ["govee-ble==0.17.2"], + "requirements": ["govee-ble==0.17.3"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 5aa128cd7f5..f883e507163 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -62,6 +62,12 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ "service_uuid": "00008351-0000-1000-8000-00805f9b34fb", "connectable": False }, + { + "domain": "govee_ble", + "manufacturer_id": 57391, + "service_uuid": "00008351-0000-1000-8000-00805f9b34fb", + "connectable": False + }, { "domain": "govee_ble", "manufacturer_id": 18994, diff --git a/requirements_all.txt b/requirements_all.txt index 7dc944052c7..eb745712dfa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -778,7 +778,7 @@ googlemaps==2.5.1 goslide-api==0.5.1 # homeassistant.components.govee_ble -govee-ble==0.17.2 +govee-ble==0.17.3 # homeassistant.components.remote_rpi_gpio gpiozero==1.6.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 32bda788cfa..3d9aa174186 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -579,7 +579,7 @@ google-nest-sdm==2.0.0 googlemaps==2.5.1 # homeassistant.components.govee_ble -govee-ble==0.17.2 +govee-ble==0.17.3 # homeassistant.components.gree greeclimate==1.3.0 From 6c0ad54a84393181e9a4e2af7f984306f2c38d08 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 13 Sep 2022 22:27:04 +0200 Subject: [PATCH 368/955] Use media player enums in dlna_dms (#78393) Co-authored-by: Shay Levy --- homeassistant/components/dlna_dms/const.py | 74 +++++++++++----------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/dlna_dms/const.py b/homeassistant/components/dlna_dms/const.py index 5d1c887fd49..c38ab64e112 100644 --- a/homeassistant/components/dlna_dms/const.py +++ b/homeassistant/components/dlna_dms/const.py @@ -5,7 +5,7 @@ from collections.abc import Mapping import logging from typing import Final -from homeassistant.components.media_player import const as _mp_const +from homeassistant.components.media_player import MediaClass, MediaType LOGGER = logging.getLogger(__package__) @@ -42,40 +42,40 @@ STREAMABLE_PROTOCOLS: Final = [PROTOCOL_HTTP, PROTOCOL_RTSP, PROTOCOL_ANY] # Map UPnP object class to media_player media class MEDIA_CLASS_MAP: Mapping[str, str] = { - "object": _mp_const.MEDIA_CLASS_URL, - "object.item": _mp_const.MEDIA_CLASS_URL, - "object.item.imageItem": _mp_const.MEDIA_CLASS_IMAGE, - "object.item.imageItem.photo": _mp_const.MEDIA_CLASS_IMAGE, - "object.item.audioItem": _mp_const.MEDIA_CLASS_MUSIC, - "object.item.audioItem.musicTrack": _mp_const.MEDIA_CLASS_MUSIC, - "object.item.audioItem.audioBroadcast": _mp_const.MEDIA_CLASS_MUSIC, - "object.item.audioItem.audioBook": _mp_const.MEDIA_CLASS_PODCAST, - "object.item.videoItem": _mp_const.MEDIA_CLASS_VIDEO, - "object.item.videoItem.movie": _mp_const.MEDIA_CLASS_MOVIE, - "object.item.videoItem.videoBroadcast": _mp_const.MEDIA_CLASS_TV_SHOW, - "object.item.videoItem.musicVideoClip": _mp_const.MEDIA_CLASS_VIDEO, - "object.item.playlistItem": _mp_const.MEDIA_CLASS_TRACK, - "object.item.textItem": _mp_const.MEDIA_CLASS_URL, - "object.item.bookmarkItem": _mp_const.MEDIA_CLASS_URL, - "object.item.epgItem": _mp_const.MEDIA_CLASS_EPISODE, - "object.item.epgItem.audioProgram": _mp_const.MEDIA_CLASS_MUSIC, - "object.item.epgItem.videoProgram": _mp_const.MEDIA_CLASS_VIDEO, - "object.container": _mp_const.MEDIA_CLASS_DIRECTORY, - "object.container.person": _mp_const.MEDIA_CLASS_ARTIST, - "object.container.person.musicArtist": _mp_const.MEDIA_CLASS_ARTIST, - "object.container.playlistContainer": _mp_const.MEDIA_CLASS_PLAYLIST, - "object.container.album": _mp_const.MEDIA_CLASS_ALBUM, - "object.container.album.musicAlbum": _mp_const.MEDIA_CLASS_ALBUM, - "object.container.album.photoAlbum": _mp_const.MEDIA_CLASS_ALBUM, - "object.container.genre": _mp_const.MEDIA_CLASS_GENRE, - "object.container.genre.musicGenre": _mp_const.MEDIA_CLASS_GENRE, - "object.container.genre.movieGenre": _mp_const.MEDIA_CLASS_GENRE, - "object.container.channelGroup": _mp_const.MEDIA_CLASS_CHANNEL, - "object.container.channelGroup.audioChannelGroup": _mp_const.MEDIA_TYPE_CHANNELS, - "object.container.channelGroup.videoChannelGroup": _mp_const.MEDIA_TYPE_CHANNELS, - "object.container.epgContainer": _mp_const.MEDIA_CLASS_DIRECTORY, - "object.container.storageSystem": _mp_const.MEDIA_CLASS_DIRECTORY, - "object.container.storageVolume": _mp_const.MEDIA_CLASS_DIRECTORY, - "object.container.storageFolder": _mp_const.MEDIA_CLASS_DIRECTORY, - "object.container.bookmarkFolder": _mp_const.MEDIA_CLASS_DIRECTORY, + "object": MediaClass.URL, + "object.item": MediaClass.URL, + "object.item.imageItem": MediaClass.IMAGE, + "object.item.imageItem.photo": MediaClass.IMAGE, + "object.item.audioItem": MediaClass.MUSIC, + "object.item.audioItem.musicTrack": MediaClass.MUSIC, + "object.item.audioItem.audioBroadcast": MediaClass.MUSIC, + "object.item.audioItem.audioBook": MediaClass.PODCAST, + "object.item.videoItem": MediaClass.VIDEO, + "object.item.videoItem.movie": MediaClass.MOVIE, + "object.item.videoItem.videoBroadcast": MediaClass.TV_SHOW, + "object.item.videoItem.musicVideoClip": MediaClass.VIDEO, + "object.item.playlistItem": MediaClass.TRACK, + "object.item.textItem": MediaClass.URL, + "object.item.bookmarkItem": MediaClass.URL, + "object.item.epgItem": MediaClass.EPISODE, + "object.item.epgItem.audioProgram": MediaClass.MUSIC, + "object.item.epgItem.videoProgram": MediaClass.VIDEO, + "object.container": MediaClass.DIRECTORY, + "object.container.person": MediaClass.ARTIST, + "object.container.person.musicArtist": MediaClass.ARTIST, + "object.container.playlistContainer": MediaClass.PLAYLIST, + "object.container.album": MediaClass.ALBUM, + "object.container.album.musicAlbum": MediaClass.ALBUM, + "object.container.album.photoAlbum": MediaClass.ALBUM, + "object.container.genre": MediaClass.GENRE, + "object.container.genre.musicGenre": MediaClass.GENRE, + "object.container.genre.movieGenre": MediaClass.GENRE, + "object.container.channelGroup": MediaClass.CHANNEL, + "object.container.channelGroup.audioChannelGroup": MediaType.CHANNELS, + "object.container.channelGroup.videoChannelGroup": MediaType.CHANNELS, + "object.container.epgContainer": MediaClass.DIRECTORY, + "object.container.storageSystem": MediaClass.DIRECTORY, + "object.container.storageVolume": MediaClass.DIRECTORY, + "object.container.storageFolder": MediaClass.DIRECTORY, + "object.container.bookmarkFolder": MediaClass.DIRECTORY, } From 14611f9b5ccf54c76aa6cdb67fd97f03f45a7877 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 13 Sep 2022 22:40:08 +0200 Subject: [PATCH 369/955] Fix race in logbook websocket test (#78390) --- tests/components/logbook/test_websocket_api.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/components/logbook/test_websocket_api.py b/tests/components/logbook/test_websocket_api.py index 4b2c40f41c0..a7bd28f0e4d 100644 --- a/tests/components/logbook/test_websocket_api.py +++ b/tests/components/logbook/test_websocket_api.py @@ -2440,8 +2440,6 @@ async def test_subscribe_entities_some_have_uom_multiple( await get_instance(hass).async_block_till_done() await hass.async_block_till_done() - _cycle_entities() - await hass.async_block_till_done() msg = await asyncio.wait_for(websocket_client.receive_json(), 2) assert msg["id"] == 7 @@ -2450,6 +2448,10 @@ async def test_subscribe_entities_some_have_uom_multiple( assert msg["event"]["events"] == [] _cycle_entities() + await get_instance(hass).async_block_till_done() + await hass.async_block_till_done() + _cycle_entities() + await get_instance(hass).async_block_till_done() await hass.async_block_till_done() msg = await asyncio.wait_for(websocket_client.receive_json(), 2) From 4f963cfc6427b28d9c12970479791f6865b010f8 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 13 Sep 2022 22:45:25 +0200 Subject: [PATCH 370/955] Improve type hints in integration (#78345) --- .../components/integration/sensor.py | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index b1b666af9aa..d08d04e094d 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -3,6 +3,7 @@ from __future__ import annotations from decimal import Decimal, DecimalException import logging +from typing import Final import voluptuous as vol @@ -26,7 +27,7 @@ from homeassistant.const import ( TIME_MINUTES, TIME_SECONDS, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, State, callback from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event @@ -45,11 +46,9 @@ from .const import ( METHOD_TRAPEZOIDAL, ) -# mypy: allow-untyped-defs, no-check-untyped-defs - _LOGGER = logging.getLogger(__name__) -ATTR_SOURCE_ID = "source" +ATTR_SOURCE_ID: Final = "source" # SI Metric prefixes UNIT_PREFIXES = {None: 1, "k": 10**3, "M": 10**6, "G": 10**9, "T": 10**12} @@ -135,6 +134,9 @@ async def async_setup_platform( class IntegrationSensor(RestoreEntity, SensorEntity): """Representation of an integration sensor.""" + _attr_state_class = SensorStateClass.TOTAL + _attr_should_poll = False + def __init__( self, *, @@ -155,13 +157,11 @@ class IntegrationSensor(RestoreEntity, SensorEntity): self._attr_name = name if name is not None else f"{source_entity} integral" self._unit_template = f"{'' if unit_prefix is None else unit_prefix}{{}}" - self._unit_of_measurement = None + self._unit_of_measurement: str | None = None self._unit_prefix = UNIT_PREFIXES[unit_prefix] self._unit_time = UNIT_TIME[unit_time] self._unit_time_str = unit_time - self._attr_state_class = SensorStateClass.TOTAL self._attr_icon = "mdi:chart-histogram" - self._attr_should_poll = False self._attr_extra_state_attributes = {ATTR_SOURCE_ID: source_entity} def _unit(self, source_unit: str) -> str: @@ -195,10 +195,10 @@ class IntegrationSensor(RestoreEntity, SensorEntity): ) @callback - def calc_integration(event): + def calc_integration(event: Event) -> None: """Handle the sensor state changes.""" - old_state = event.data.get("old_state") - new_state = event.data.get("new_state") + old_state: State | None = event.data.get("old_state") + new_state: State | None = event.data.get("new_state") if new_state is None or new_state.state in ( STATE_UNKNOWN, @@ -237,7 +237,7 @@ class IntegrationSensor(RestoreEntity, SensorEntity): try: # integration as the Riemann integral of previous measures. - area = 0 + area = Decimal(0) elapsed_time = ( new_state.last_updated - old_state.last_updated ).total_seconds() @@ -277,13 +277,13 @@ class IntegrationSensor(RestoreEntity, SensorEntity): ) @property - def native_value(self): + def native_value(self) -> Decimal | None: """Return the state of the sensor.""" if isinstance(self._state, Decimal): return round(self._state, self._round_digits) return self._state @property - def native_unit_of_measurement(self): + def native_unit_of_measurement(self) -> str | None: """Return the unit the value is expressed in.""" return self._unit_of_measurement From d3be06906bec9ec61c01958893c02ca1b17b4099 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 13 Sep 2022 23:11:29 +0200 Subject: [PATCH 371/955] Improve type hints in script helpers (#78364) * Improve type hints in script helpers * Import CONF_SERVICE_DATA from homeassistant.const * Make data optional --- homeassistant/helpers/script.py | 41 +++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 54ae4f456ab..e472934fc76 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -2,7 +2,7 @@ from __future__ import annotations import asyncio -from collections.abc import Callable, Sequence +from collections.abc import Callable, Mapping, Sequence from contextlib import asynccontextmanager, suppress from contextvars import ContextVar from copy import copy @@ -49,6 +49,7 @@ from homeassistant.const import ( CONF_SCENE, CONF_SEQUENCE, CONF_SERVICE, + CONF_SERVICE_DATA, CONF_STOP, CONF_TARGET, CONF_THEN, @@ -218,7 +219,9 @@ async def trace_action(hass, script_run, stop, variables): trace_stack_pop(trace_stack_cv) -def make_script_schema(schema, default_script_mode, extra=vol.PREVENT_EXTRA): +def make_script_schema( + schema: Mapping[Any, Any], default_script_mode: str, extra: int = vol.PREVENT_EXTRA +) -> vol.Schema: """Make a schema for a component that uses the script helper.""" return vol.Schema( { @@ -1109,7 +1112,9 @@ async def _async_stop_scripts_at_shutdown(hass, event): _VarsType = Union[dict[str, Any], MappingProxyType] -def _referenced_extract_ids(data: dict[str, Any], key: str, found: set[str]) -> None: +def _referenced_extract_ids( + data: dict[str, Any] | None, key: str, found: set[str] +) -> None: """Extract referenced IDs.""" if not data: return @@ -1275,24 +1280,26 @@ class Script: return self.script_mode in (SCRIPT_MODE_PARALLEL, SCRIPT_MODE_QUEUED) @property - def referenced_areas(self): + def referenced_areas(self) -> set[str]: """Return a set of referenced areas.""" if self._referenced_areas is not None: return self._referenced_areas - self._referenced_areas: set[str] = set() + self._referenced_areas = set() Script._find_referenced_areas(self._referenced_areas, self.sequence) return self._referenced_areas @staticmethod - def _find_referenced_areas(referenced, sequence): + def _find_referenced_areas( + referenced: set[str], sequence: Sequence[dict[str, Any]] + ) -> None: for step in sequence: action = cv.determine_script_action(step) if action == cv.SCRIPT_ACTION_CALL_SERVICE: for data in ( step.get(CONF_TARGET), - step.get(service.CONF_SERVICE_DATA), + step.get(CONF_SERVICE_DATA), step.get(service.CONF_SERVICE_DATA_TEMPLATE), ): _referenced_extract_ids(data, ATTR_AREA_ID, referenced) @@ -1313,24 +1320,26 @@ class Script: Script._find_referenced_areas(referenced, script[CONF_SEQUENCE]) @property - def referenced_devices(self): + def referenced_devices(self) -> set[str]: """Return a set of referenced devices.""" if self._referenced_devices is not None: return self._referenced_devices - self._referenced_devices: set[str] = set() + self._referenced_devices = set() Script._find_referenced_devices(self._referenced_devices, self.sequence) return self._referenced_devices @staticmethod - def _find_referenced_devices(referenced, sequence): + def _find_referenced_devices( + referenced: set[str], sequence: Sequence[dict[str, Any]] + ) -> None: for step in sequence: action = cv.determine_script_action(step) if action == cv.SCRIPT_ACTION_CALL_SERVICE: for data in ( step.get(CONF_TARGET), - step.get(service.CONF_SERVICE_DATA), + step.get(CONF_SERVICE_DATA), step.get(service.CONF_SERVICE_DATA_TEMPLATE), ): _referenced_extract_ids(data, ATTR_DEVICE_ID, referenced) @@ -1361,17 +1370,19 @@ class Script: Script._find_referenced_devices(referenced, script[CONF_SEQUENCE]) @property - def referenced_entities(self): + def referenced_entities(self) -> set[str]: """Return a set of referenced entities.""" if self._referenced_entities is not None: return self._referenced_entities - self._referenced_entities: set[str] = set() + self._referenced_entities = set() Script._find_referenced_entities(self._referenced_entities, self.sequence) return self._referenced_entities @staticmethod - def _find_referenced_entities(referenced, sequence): + def _find_referenced_entities( + referenced: set[str], sequence: Sequence[dict[str, Any]] + ) -> None: for step in sequence: action = cv.determine_script_action(step) @@ -1379,7 +1390,7 @@ class Script: for data in ( step, step.get(CONF_TARGET), - step.get(service.CONF_SERVICE_DATA), + step.get(CONF_SERVICE_DATA), step.get(service.CONF_SERVICE_DATA_TEMPLATE), ): _referenced_extract_ids(data, ATTR_ENTITY_ID, referenced) From f1c7fb78667892ae4cb9e350dcdfa1d3d0153703 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 Sep 2022 00:11:57 +0200 Subject: [PATCH 372/955] Adjust pylint plugin for relative imports (#78277) --- pylint/plugins/hass_imports.py | 20 ++++++++++++-------- tests/pylint/test_imports.py | 11 +++++++++++ 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/pylint/plugins/hass_imports.py b/pylint/plugins/hass_imports.py index 34c7d87c53a..de26cfef982 100644 --- a/pylint/plugins/hass_imports.py +++ b/pylint/plugins/hass_imports.py @@ -362,14 +362,18 @@ class HassImportsFormatChecker(BaseChecker): # type: ignore[misc] ): self.add_message("hass-relative-import", node=node) return - if ( - self.current_package.startswith("homeassistant.components") - and node.modname == "homeassistant.components" - ): - for name in node.names: - if name[0] == self.current_package.split(".")[2]: - self.add_message("hass-relative-import", node=node) - return + if self.current_package.startswith("homeassistant.components."): + current_component = self.current_package.split(".")[2] + if node.modname == "homeassistant.components": + for name in node.names: + if name[0] == current_component: + self.add_message("hass-relative-import", node=node) + return + if node.modname.startswith( + f"homeassistant.components.{current_component}." + ): + self.add_message("hass-relative-import", node=node) + return if obsolete_imports := _OBSOLETE_IMPORT.get(node.modname): for name_tuple in node.names: for obsolete_import in obsolete_imports: diff --git a/tests/pylint/test_imports.py b/tests/pylint/test_imports.py index d1ba7fe4a7f..f535e34e8de 100644 --- a/tests/pylint/test_imports.py +++ b/tests/pylint/test_imports.py @@ -19,6 +19,11 @@ from . import assert_adds_messages, assert_no_messages "homeassistant.const", "CONSTANT", ), + ( + "homeassistant.components.pylint_test.sensor", + "homeassistant.components.pylint_testing", + "CONSTANT", + ), ("homeassistant.components.pylint_test.sensor", ".const", "CONSTANT"), ("homeassistant.components.pylint_test.sensor", ".", "CONSTANT"), ("homeassistant.components.pylint_test.sensor", "..", "pylint_test"), @@ -90,6 +95,12 @@ def test_good_import( "pylint_test", "hass-relative-import", ), + ( + "homeassistant.components.pylint_test.api.hub", + "homeassistant.components.pylint_test.const", + "CONSTANT", + "hass-relative-import", + ), ], ) def test_bad_import( From 416a5cb279f70fc020dc257d804d5b37df393aa0 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 Sep 2022 00:12:54 +0200 Subject: [PATCH 373/955] Import constants from component root (#78395) --- homeassistant/components/alexa/const.py | 2 +- homeassistant/components/alexa/entities.py | 2 +- homeassistant/components/alexa/handlers.py | 2 +- homeassistant/components/cloud/__init__.py | 7 +++---- homeassistant/components/cloud/client.py | 6 +++--- homeassistant/components/cloud/http_api.py | 7 ++++--- homeassistant/components/google_assistant/trait.py | 4 ++-- 7 files changed, 15 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/alexa/const.py b/homeassistant/components/alexa/const.py index d51409a5a1c..d1061720718 100644 --- a/homeassistant/components/alexa/const.py +++ b/homeassistant/components/alexa/const.py @@ -1,7 +1,7 @@ """Constants for the Alexa integration.""" from collections import OrderedDict -from homeassistant.components.climate import const as climate +from homeassistant.components import climate from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT DOMAIN = "alexa" diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index ac78dbeed5e..8319e146d9f 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -11,6 +11,7 @@ from homeassistant.components import ( binary_sensor, button, camera, + climate, cover, fan, group, @@ -28,7 +29,6 @@ from homeassistant.components import ( timer, vacuum, ) -from homeassistant.components.climate import const as climate from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_SUPPORTED_FEATURES, diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index ba3892a62f2..c10f7a50369 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -10,6 +10,7 @@ from homeassistant import core as ha from homeassistant.components import ( button, camera, + climate, cover, fan, group, @@ -20,7 +21,6 @@ from homeassistant.components import ( timer, vacuum, ) -from homeassistant.components.climate import const as climate from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE, diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index aa31f796491..6a948c0ad15 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -8,8 +8,7 @@ from enum import Enum from hass_nabucasa import Cloud import voluptuous as vol -from homeassistant.components.alexa import const as alexa_const -from homeassistant.components.google_assistant import const as ga_c +from homeassistant.components import alexa, google_assistant from homeassistant.const import ( CONF_DESCRIPTION, CONF_MODE, @@ -68,7 +67,7 @@ SIGNAL_CLOUD_CONNECTION_STATE = "CLOUD_CONNECTION_STATE" ALEXA_ENTITY_SCHEMA = vol.Schema( { vol.Optional(CONF_DESCRIPTION): cv.string, - vol.Optional(alexa_const.CONF_DISPLAY_CATEGORIES): cv.string, + vol.Optional(alexa.CONF_DISPLAY_CATEGORIES): cv.string, vol.Optional(CONF_NAME): cv.string, } ) @@ -77,7 +76,7 @@ GOOGLE_ENTITY_SCHEMA = vol.Schema( { vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_ALIASES): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(ga_c.CONF_ROOM_HINT): cv.string, + vol.Optional(google_assistant.CONF_ROOM_HINT): cv.string, } ) diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py index 6011e9bf551..04b9a9aab97 100644 --- a/homeassistant/components/cloud/client.py +++ b/homeassistant/components/cloud/client.py @@ -10,12 +10,12 @@ from typing import Any import aiohttp from hass_nabucasa.client import CloudClient as Interface -from homeassistant.components import persistent_notification, webhook +from homeassistant.components import google_assistant, persistent_notification, webhook from homeassistant.components.alexa import ( errors as alexa_errors, smart_home as alexa_smart_home, ) -from homeassistant.components.google_assistant import const as gc, smart_home as ga +from homeassistant.components.google_assistant import smart_home as ga from homeassistant.core import Context, HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_call_later @@ -216,7 +216,7 @@ class CloudClient(Interface): return ga.api_disabled_response(payload, gconf.agent_user_id) return await ga.async_handle_message( - self._hass, gconf, gconf.cloud_user, payload, gc.SOURCE_CLOUD + self._hass, gconf, gconf.cloud_user, payload, google_assistant.SOURCE_CLOUD ) async def async_webhook_message(self, payload: dict[Any, Any]) -> dict[Any, Any]: diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index 8e5c214b388..ebeb79dcd2a 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -21,7 +21,6 @@ from homeassistant.components.alexa import ( from homeassistant.components.google_assistant import helpers as google_helpers from homeassistant.components.http import HomeAssistantView from homeassistant.components.http.data_validator import RequestDataValidator -from homeassistant.components.websocket_api import const as ws_const from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util.location import async_detect_location_info @@ -616,7 +615,9 @@ async def alexa_sync(hass, connection, msg): if success: connection.send_result(msg["id"]) else: - connection.send_error(msg["id"], ws_const.ERR_UNKNOWN_ERROR, "Unknown error") + connection.send_error( + msg["id"], websocket_api.ERR_UNKNOWN_ERROR, "Unknown error" + ) @websocket_api.websocket_command({"type": "cloud/thingtalk/convert", "query": str}) @@ -631,7 +632,7 @@ async def thingtalk_convert(hass, connection, msg): msg["id"], await thingtalk.async_convert(cloud, msg["query"]) ) except thingtalk.ThingTalkConversionError as err: - connection.send_error(msg["id"], ws_const.ERR_UNKNOWN_ERROR, str(err)) + connection.send_error(msg["id"], websocket_api.ERR_UNKNOWN_ERROR, str(err)) @websocket_api.websocket_command({"type": "cloud/tts/info"}) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 8f3f20d177e..8c253523561 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -9,9 +9,11 @@ from homeassistant.components import ( binary_sensor, button, camera, + climate, cover, fan, group, + humidifier, input_boolean, input_button, input_select, @@ -25,8 +27,6 @@ from homeassistant.components import ( switch, vacuum, ) -from homeassistant.components.climate import const as climate -from homeassistant.components.humidifier import const as humidifier from homeassistant.components.lock import STATE_JAMMED, STATE_UNLOCKING from homeassistant.components.media_player import MediaType from homeassistant.const import ( From 5501b7e710bed9ec32b2afc195a75f37eab1bf69 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 13 Sep 2022 16:16:21 -0600 Subject: [PATCH 374/955] Fix bug with RainMachine update entity (#78411) * Fix bug with RainMachine update entity * Comment --- homeassistant/components/rainmachine/update.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/rainmachine/update.py b/homeassistant/components/rainmachine/update.py index b191f2695a0..a811894a0c2 100644 --- a/homeassistant/components/rainmachine/update.py +++ b/homeassistant/components/rainmachine/update.py @@ -99,4 +99,11 @@ class RainMachineUpdateEntity(RainMachineEntity, UpdateEntity): UpdateStates.UPGRADING, UpdateStates.REBOOT, ) - self._attr_latest_version = data["packageDetails"]["newVersion"] + + # The RainMachine API docs say that multiple "packages" can be updated, but + # don't give details on what types exist (which makes it impossible to have + # update entities per update type); so, we use the first one (with the idea that + # after it succeeds, the entity will show the next update): + package_details = data["packageDetails"][0] + self._attr_latest_version = package_details["newVersion"] + self._attr_title = package_details["packageName"] From 23faf8024ee5427e1f0da15df36c6ce5dc89677b Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 14 Sep 2022 00:28:24 +0000 Subject: [PATCH 375/955] [ci skip] Translation update --- .../aladdin_connect/translations/cs.json | 4 ++ .../components/ambee/translations/cs.json | 4 ++ .../amberelectric/translations/cs.json | 8 +++ .../android_ip_webcam/translations/cs.json | 20 +++++++ .../components/anthemav/translations/cs.json | 17 ++++++ .../aurora_abb_powerone/translations/cs.json | 7 +++ .../aussie_broadband/translations/cs.json | 6 +++ .../components/awair/translations/cs.json | 10 +++- .../components/baf/translations/cs.json | 3 ++ .../components/balboa/translations/cs.json | 11 ++++ .../bluemaestro/translations/cs.json | 22 ++++++++ .../components/bluetooth/translations/cs.json | 22 ++++++++ .../components/bosch_shc/translations/cs.json | 11 ++++ .../components/brunt/translations/cs.json | 15 ++++++ .../components/bthome/translations/cs.json | 22 ++++++++ .../components/climate/translations/cs.json | 4 +- .../coolmaster/translations/cs.json | 2 +- .../components/daikin/translations/cs.json | 3 +- .../devolo_home_network/translations/cs.json | 4 ++ .../components/discord/translations/cs.json | 12 +++++ .../components/dlna_dmr/translations/cs.json | 11 +++- .../components/dsmr/translations/cs.json | 8 ++- .../components/ecowitt/translations/cs.json | 1 + .../components/elkm1/translations/cs.json | 4 +- .../components/elmax/translations/cs.json | 3 ++ .../components/escea/translations/cs.json | 7 +++ .../evil_genius_labs/translations/cs.json | 8 +++ .../components/fibaro/translations/cs.json | 21 ++++++++ .../components/flume/translations/cs.json | 5 ++ .../components/fritz/translations/cs.json | 6 ++- .../components/fronius/translations/cs.json | 11 +++- .../fully_kiosk/translations/cs.json | 17 ++++++ .../garages_amsterdam/translations/cs.json | 1 + .../components/generic/translations/cs.json | 6 +++ .../geocaching/translations/cs.json | 13 +++++ .../components/goalzero/translations/cs.json | 2 +- .../components/google/translations/cs.json | 1 + .../components/govee_ble/translations/cs.json | 21 ++++++++ .../growatt_server/translations/cs.json | 4 +- .../here_travel_time/translations/cs.json | 11 ++++ .../components/hlk_sw16/translations/cs.json | 2 +- .../homewizard/translations/cs.json | 1 + .../components/inkbird/translations/cs.json | 21 ++++++++ .../components/insteon/translations/cs.json | 3 ++ .../intellifire/translations/cs.json | 10 +++- .../components/isy994/translations/cs.json | 3 +- .../components/jellyfin/translations/cs.json | 8 +++ .../justnimbus/translations/cs.json | 12 +++++ .../kaleidescape/translations/cs.json | 9 +++- .../components/knx/translations/cs.json | 5 ++ .../components/konnected/translations/cs.json | 1 + .../components/kraken/translations/tr.json | 2 +- .../lacrosse_view/translations/cs.json | 20 +++++++ .../components/lametric/translations/cs.json | 18 +++++++ .../landisgyr_heat_meter/translations/cs.json | 11 ++++ .../components/laundrify/translations/cs.json | 14 +++++ .../components/led_ble/translations/cs.json | 13 +++++ .../lg_soundbar/translations/cs.json | 17 ++++++ .../components/life360/translations/cs.json | 8 +++ .../components/lifx/translations/cs.json | 13 +++++ .../litterrobot/translations/cs.json | 9 +++- .../components/lookin/translations/cs.json | 7 ++- .../components/meater/translations/cs.json | 5 ++ .../meteoclimatic/translations/cs.json | 4 ++ .../components/mill/translations/cs.json | 8 +++ .../components/mjpeg/translations/cs.json | 10 +++- .../components/moat/translations/cs.json | 21 ++++++++ .../modern_forms/translations/cs.json | 18 +++++++ .../components/motioneye/translations/cs.json | 4 ++ .../components/nam/translations/cs.json | 20 +++++++ .../components/nextdns/translations/cs.json | 9 ++++ .../components/nina/translations/cs.json | 6 +++ .../components/nobo_hub/translations/cs.json | 20 +++++++ .../components/octoprint/translations/cs.json | 5 ++ .../components/onewire/translations/cs.json | 3 ++ .../openexchangerates/translations/cs.json | 13 +++++ .../plum_lightpad/translations/cs.json | 2 +- .../components/pushover/translations/cs.json | 16 ++++++ .../components/qingping/translations/cs.json | 21 ++++++++ .../components/qnap_qsw/translations/cs.json | 18 +++++++ .../radiotherm/translations/cs.json | 21 ++++++++ .../components/rdw/translations/cs.json | 7 +++ .../components/rhasspy/translations/cs.json | 9 ++++ .../components/ridwell/translations/cs.json | 14 +++++ .../components/risco/translations/cs.json | 11 ++++ .../components/roon/translations/cs.json | 5 ++ .../components/roon/translations/tr.json | 4 ++ .../components/sabnzbd/translations/cs.json | 8 +++ .../components/scrape/translations/cs.json | 4 +- .../components/senseme/translations/cs.json | 8 ++- .../components/sensibo/translations/cs.json | 14 ++++- .../components/sensorpro/translations/cs.json | 22 ++++++++ .../sensorpush/translations/cs.json | 21 ++++++++ .../components/senz/translations/cs.json | 7 +++ .../simplepush/translations/cs.json | 10 ++++ .../components/skybell/translations/cs.json | 18 +++++++ .../components/slack/translations/cs.json | 2 + .../components/sleepiq/translations/cs.json | 18 +++++++ .../components/solax/translations/cs.json | 2 + .../soundtouch/translations/cs.json | 17 ++++++ .../steam_online/translations/cs.json | 11 +++- .../components/switchbee/translations/cs.json | 30 +++++++++++ .../components/switchbee/translations/de.json | 32 +++++++++++ .../components/switchbee/translations/el.json | 32 +++++++++++ .../components/switchbee/translations/en.json | 54 +++++++++---------- .../components/switchbee/translations/es.json | 32 +++++++++++ .../components/switchbee/translations/et.json | 32 +++++++++++ .../components/switchbee/translations/fr.json | 32 +++++++++++ .../switchbee/translations/pt-BR.json | 32 +++++++++++ .../switchbee/translations/zh-Hant.json | 32 +++++++++++ .../components/switchbot/translations/cs.json | 8 +++ .../synology_dsm/translations/cs.json | 3 +- .../system_bridge/translations/cs.json | 2 + .../components/tailscale/translations/cs.json | 1 + .../tankerkoenig/translations/cs.json | 10 ++++ .../components/tautulli/translations/cs.json | 12 +++++ .../tesla_wall_connector/translations/cs.json | 11 ++++ .../thermobeacon/translations/cs.json | 21 ++++++++ .../components/thermopro/translations/cs.json | 21 ++++++++ .../components/tilt_ble/translations/cs.json | 21 ++++++++ .../components/tolo/translations/cs.json | 17 ++++++ .../tomorrowio/translations/cs.json | 4 ++ .../trafikverket_ferry/translations/cs.json | 11 ++++ .../trafikverket_train/translations/cs.json | 11 ++++ .../transmission/translations/cs.json | 9 +++- .../ukraine_alarm/translations/cs.json | 8 +++ .../components/vallox/translations/cs.json | 8 +++ .../components/venstar/translations/cs.json | 13 +++++ .../volvooncall/translations/cs.json | 19 +++++++ .../components/wallbox/translations/cs.json | 9 ++++ .../components/withings/translations/cs.json | 3 ++ .../wolflink/translations/sensor.cs.json | 4 +- .../components/ws66i/translations/cs.json | 11 ++++ .../xiaomi_ble/translations/cs.json | 22 ++++++++ .../yalexs_ble/translations/cs.json | 14 +++++ .../yamaha_musiccast/translations/cs.json | 14 +++++ .../components/yolink/translations/cs.json | 13 +++++ .../components/zha/translations/cs.json | 16 +++++- .../components/zha/translations/el.json | 1 + .../components/zwave_js/translations/cs.json | 8 +++ 140 files changed, 1583 insertions(+), 61 deletions(-) create mode 100644 homeassistant/components/amberelectric/translations/cs.json create mode 100644 homeassistant/components/android_ip_webcam/translations/cs.json create mode 100644 homeassistant/components/anthemav/translations/cs.json create mode 100644 homeassistant/components/aurora_abb_powerone/translations/cs.json create mode 100644 homeassistant/components/bluemaestro/translations/cs.json create mode 100644 homeassistant/components/bluetooth/translations/cs.json create mode 100644 homeassistant/components/bthome/translations/cs.json create mode 100644 homeassistant/components/discord/translations/cs.json create mode 100644 homeassistant/components/escea/translations/cs.json create mode 100644 homeassistant/components/fully_kiosk/translations/cs.json create mode 100644 homeassistant/components/geocaching/translations/cs.json create mode 100644 homeassistant/components/govee_ble/translations/cs.json create mode 100644 homeassistant/components/here_travel_time/translations/cs.json create mode 100644 homeassistant/components/inkbird/translations/cs.json create mode 100644 homeassistant/components/justnimbus/translations/cs.json create mode 100644 homeassistant/components/lacrosse_view/translations/cs.json create mode 100644 homeassistant/components/lametric/translations/cs.json create mode 100644 homeassistant/components/landisgyr_heat_meter/translations/cs.json create mode 100644 homeassistant/components/laundrify/translations/cs.json create mode 100644 homeassistant/components/led_ble/translations/cs.json create mode 100644 homeassistant/components/lg_soundbar/translations/cs.json create mode 100644 homeassistant/components/moat/translations/cs.json create mode 100644 homeassistant/components/modern_forms/translations/cs.json create mode 100644 homeassistant/components/nextdns/translations/cs.json create mode 100644 homeassistant/components/nobo_hub/translations/cs.json create mode 100644 homeassistant/components/openexchangerates/translations/cs.json create mode 100644 homeassistant/components/pushover/translations/cs.json create mode 100644 homeassistant/components/qingping/translations/cs.json create mode 100644 homeassistant/components/radiotherm/translations/cs.json create mode 100644 homeassistant/components/rdw/translations/cs.json create mode 100644 homeassistant/components/rhasspy/translations/cs.json create mode 100644 homeassistant/components/sabnzbd/translations/cs.json create mode 100644 homeassistant/components/sensorpro/translations/cs.json create mode 100644 homeassistant/components/sensorpush/translations/cs.json create mode 100644 homeassistant/components/senz/translations/cs.json create mode 100644 homeassistant/components/simplepush/translations/cs.json create mode 100644 homeassistant/components/soundtouch/translations/cs.json create mode 100644 homeassistant/components/switchbee/translations/cs.json create mode 100644 homeassistant/components/switchbee/translations/de.json create mode 100644 homeassistant/components/switchbee/translations/el.json create mode 100644 homeassistant/components/switchbee/translations/es.json create mode 100644 homeassistant/components/switchbee/translations/et.json create mode 100644 homeassistant/components/switchbee/translations/fr.json create mode 100644 homeassistant/components/switchbee/translations/pt-BR.json create mode 100644 homeassistant/components/switchbee/translations/zh-Hant.json create mode 100644 homeassistant/components/tankerkoenig/translations/cs.json create mode 100644 homeassistant/components/tautulli/translations/cs.json create mode 100644 homeassistant/components/thermobeacon/translations/cs.json create mode 100644 homeassistant/components/thermopro/translations/cs.json create mode 100644 homeassistant/components/tilt_ble/translations/cs.json create mode 100644 homeassistant/components/tolo/translations/cs.json create mode 100644 homeassistant/components/trafikverket_ferry/translations/cs.json create mode 100644 homeassistant/components/trafikverket_train/translations/cs.json create mode 100644 homeassistant/components/ukraine_alarm/translations/cs.json create mode 100644 homeassistant/components/volvooncall/translations/cs.json create mode 100644 homeassistant/components/ws66i/translations/cs.json create mode 100644 homeassistant/components/xiaomi_ble/translations/cs.json create mode 100644 homeassistant/components/yalexs_ble/translations/cs.json create mode 100644 homeassistant/components/yamaha_musiccast/translations/cs.json create mode 100644 homeassistant/components/yolink/translations/cs.json diff --git a/homeassistant/components/aladdin_connect/translations/cs.json b/homeassistant/components/aladdin_connect/translations/cs.json index a3144ba2f55..007d1eb6704 100644 --- a/homeassistant/components/aladdin_connect/translations/cs.json +++ b/homeassistant/components/aladdin_connect/translations/cs.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + }, "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" diff --git a/homeassistant/components/ambee/translations/cs.json b/homeassistant/components/ambee/translations/cs.json index 6459ddb3ba0..88a6b354852 100644 --- a/homeassistant/components/ambee/translations/cs.json +++ b/homeassistant/components/ambee/translations/cs.json @@ -3,6 +3,10 @@ "abort": { "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_api_key": "Neplatn\u00fd kl\u00ed\u010d API" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/amberelectric/translations/cs.json b/homeassistant/components/amberelectric/translations/cs.json new file mode 100644 index 00000000000..e2986983838 --- /dev/null +++ b/homeassistant/components/amberelectric/translations/cs.json @@ -0,0 +1,8 @@ +{ + "config": { + "error": { + "invalid_api_token": "Neplatn\u00fd kl\u00ed\u010d API", + "unknown_error": "Neo\u010dek\u00e1van\u00e1 chyba" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/android_ip_webcam/translations/cs.json b/homeassistant/components/android_ip_webcam/translations/cs.json new file mode 100644 index 00000000000..20f0a9bf4fb --- /dev/null +++ b/homeassistant/components/android_ip_webcam/translations/cs.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" + }, + "step": { + "user": { + "data": { + "host": "Hostitel", + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/cs.json b/homeassistant/components/anthemav/translations/cs.json new file mode 100644 index 00000000000..6fabc170b6e --- /dev/null +++ b/homeassistant/components/anthemav/translations/cs.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, + "step": { + "user": { + "data": { + "host": "Hostitel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aurora_abb_powerone/translations/cs.json b/homeassistant/components/aurora_abb_powerone/translations/cs.json new file mode 100644 index 00000000000..33006d6761b --- /dev/null +++ b/homeassistant/components/aurora_abb_powerone/translations/cs.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aussie_broadband/translations/cs.json b/homeassistant/components/aussie_broadband/translations/cs.json index 131dddca93a..7f734257985 100644 --- a/homeassistant/components/aussie_broadband/translations/cs.json +++ b/homeassistant/components/aussie_broadband/translations/cs.json @@ -10,6 +10,12 @@ "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { + "reauth_confirm": { + "data": { + "password": "Heslo" + }, + "title": "Znovu ov\u011b\u0159it integraci" + }, "user": { "data": { "password": "Heslo", diff --git a/homeassistant/components/awair/translations/cs.json b/homeassistant/components/awair/translations/cs.json index dfc83778bf9..3b0110001b3 100644 --- a/homeassistant/components/awair/translations/cs.json +++ b/homeassistant/components/awair/translations/cs.json @@ -2,14 +2,20 @@ "config": { "abort": { "already_configured": "\u00da\u010det je ji\u017e nastaven", + "already_configured_device": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed", - "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9", + "unreachable": "Nepoda\u0159ilo se p\u0159ipojit" }, "error": { "invalid_access_token": "Neplatn\u00fd p\u0159\u00edstupov\u00fd token", - "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba", + "unreachable": "Nepoda\u0159ilo se p\u0159ipojit" }, "step": { + "discovery_confirm": { + "description": "Chcete nastavit {model} ({device_id})?" + }, "reauth": { "data": { "access_token": "P\u0159\u00edstupov\u00fd token", diff --git a/homeassistant/components/baf/translations/cs.json b/homeassistant/components/baf/translations/cs.json index 04f18366eaf..e33c65701c1 100644 --- a/homeassistant/components/baf/translations/cs.json +++ b/homeassistant/components/baf/translations/cs.json @@ -8,6 +8,9 @@ "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { + "discovery_confirm": { + "description": "Chcete nastavit {name} - {model} ({ip_address})?" + }, "user": { "data": { "ip_address": "IP adresa" diff --git a/homeassistant/components/balboa/translations/cs.json b/homeassistant/components/balboa/translations/cs.json index e1bf8e7f45f..5eac883adf0 100644 --- a/homeassistant/components/balboa/translations/cs.json +++ b/homeassistant/components/balboa/translations/cs.json @@ -1,7 +1,18 @@ { "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "host": "Hostitel" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/bluemaestro/translations/cs.json b/homeassistant/components/bluemaestro/translations/cs.json new file mode 100644 index 00000000000..1163b27775a --- /dev/null +++ b/homeassistant/components/bluemaestro/translations/cs.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", + "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed", + "not_supported": "Za\u0159\u00edzen\u00ed nen\u00ed podporov\u00e1no" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Chcete nastavit {name}?" + }, + "user": { + "data": { + "address": "Za\u0159\u00edzen\u00ed" + }, + "description": "Zvolte za\u0159\u00edzen\u00ed, kter\u00e9 chcete nastavit" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/cs.json b/homeassistant/components/bluetooth/translations/cs.json new file mode 100644 index 00000000000..e53690d3458 --- /dev/null +++ b/homeassistant/components/bluetooth/translations/cs.json @@ -0,0 +1,22 @@ +{ + "config": { + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Chcete nastavit {name}?" + }, + "enable_bluetooth": { + "description": "Chcete nastavit Bluetooth?" + }, + "single_adapter": { + "description": "Chcete nastavit Bluetooth adapt\u00e9r {name}?" + }, + "user": { + "data": { + "address": "Za\u0159\u00edzen\u00ed" + }, + "description": "Zvolte za\u0159\u00edzen\u00ed, kter\u00e9 chcete nastavit" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bosch_shc/translations/cs.json b/homeassistant/components/bosch_shc/translations/cs.json index 45e02001105..ff70a713b06 100644 --- a/homeassistant/components/bosch_shc/translations/cs.json +++ b/homeassistant/components/bosch_shc/translations/cs.json @@ -1,12 +1,23 @@ { "config": { "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "reauth_confirm": { + "title": "Znovu ov\u011b\u0159it integraci" + }, + "user": { + "data": { + "host": "Hostitel" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/brunt/translations/cs.json b/homeassistant/components/brunt/translations/cs.json index 72df4a96818..5ac20c2bde8 100644 --- a/homeassistant/components/brunt/translations/cs.json +++ b/homeassistant/components/brunt/translations/cs.json @@ -4,8 +4,23 @@ "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Heslo" + }, + "title": "Znovu ov\u011b\u0159it integraci" + }, + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/bthome/translations/cs.json b/homeassistant/components/bthome/translations/cs.json new file mode 100644 index 00000000000..76d1e332913 --- /dev/null +++ b/homeassistant/components/bthome/translations/cs.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", + "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Chcete nastavit {name}?" + }, + "user": { + "data": { + "address": "Za\u0159\u00edzen\u00ed" + }, + "description": "Zvolte za\u0159\u00edzen\u00ed, kter\u00e9 chcete nastavit" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climate/translations/cs.json b/homeassistant/components/climate/translations/cs.json index 3740a7b423e..276ef1a7d70 100644 --- a/homeassistant/components/climate/translations/cs.json +++ b/homeassistant/components/climate/translations/cs.json @@ -20,8 +20,8 @@ "cool": "Chlazen\u00ed", "dry": "Vysou\u0161en\u00ed", "fan_only": "Pouze ventil\u00e1tor", - "heat": "Topen\u00ed", - "heat_cool": "Topen\u00ed/Chlazen\u00ed", + "heat": "Vyt\u00e1p\u011bn\u00ed", + "heat_cool": "Vyt\u00e1p\u011bn\u00ed/Chlazen\u00ed", "off": "Vypnuto" } }, diff --git a/homeassistant/components/coolmaster/translations/cs.json b/homeassistant/components/coolmaster/translations/cs.json index 9e820808f0c..2d459900450 100644 --- a/homeassistant/components/coolmaster/translations/cs.json +++ b/homeassistant/components/coolmaster/translations/cs.json @@ -10,7 +10,7 @@ "cool": "Podpora re\u017eimu chlazen\u00ed", "dry": "Podpora re\u017eimu vysou\u0161en\u00ed", "fan_only": "Podpora re\u017eimu pouze ventil\u00e1tor", - "heat": "Podpora re\u017eimu topen\u00ed", + "heat": "Podpora re\u017eimu vyt\u00e1p\u011bn\u00ed", "heat_cool": "Podpora automatick\u00e9ho oh\u0159\u00edv\u00e1n\u00ed/chlazen\u00ed", "host": "Hostitel", "off": "Lze vypnout" diff --git a/homeassistant/components/daikin/translations/cs.json b/homeassistant/components/daikin/translations/cs.json index 86625365a30..e7906d0af45 100644 --- a/homeassistant/components/daikin/translations/cs.json +++ b/homeassistant/components/daikin/translations/cs.json @@ -5,6 +5,7 @@ "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" }, "error": { + "api_password": "Neplatn\u00e9 ov\u011b\u0159en\u00ed, pou\u017eijte kl\u00ed\u010d API nebo heslo.", "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" @@ -16,7 +17,7 @@ "host": "Hostitel", "password": "Heslo" }, - "description": "Zadejte IP adresu va\u0161eho Daikin AC. \n\nV\u0161imn\u011bte si, \u017ee Kl\u00ed\u010d API a Heslo jsou pou\u017eit\u00e9 za\u0159\u00edzen\u00edm BRP072Cxx, respektive SKYFi.", + "description": "Zadejte IP adresu va\u0161eho Daikin AC. \n\nVezm\u011bte na v\u011bdom\u00ed, \u017ee Kl\u00ed\u010d API a Heslo pou\u017e\u00edvaj\u00ed pouze za\u0159\u00edzen\u00ed BRP072Cxx a SKYFi.", "title": "Nastaven\u00ed Daikin AC" } } diff --git a/homeassistant/components/devolo_home_network/translations/cs.json b/homeassistant/components/devolo_home_network/translations/cs.json index e1bf8e7f45f..0887542d784 100644 --- a/homeassistant/components/devolo_home_network/translations/cs.json +++ b/homeassistant/components/devolo_home_network/translations/cs.json @@ -1,6 +1,10 @@ { "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" } } diff --git a/homeassistant/components/discord/translations/cs.json b/homeassistant/components/discord/translations/cs.json new file mode 100644 index 00000000000..45e02001105 --- /dev/null +++ b/homeassistant/components/discord/translations/cs.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dmr/translations/cs.json b/homeassistant/components/dlna_dmr/translations/cs.json index c9087b82ab7..edbffb7c6ee 100644 --- a/homeassistant/components/dlna_dmr/translations/cs.json +++ b/homeassistant/components/dlna_dmr/translations/cs.json @@ -1,12 +1,21 @@ { "config": { "abort": { - "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" }, "flow_title": "{name}", "step": { "confirm": { "description": "Chcete za\u010d\u00edt nastavovat?" + }, + "user": { + "data": { + "host": "Hostitel" + } } } } diff --git a/homeassistant/components/dsmr/translations/cs.json b/homeassistant/components/dsmr/translations/cs.json index 9ab3eefa6a6..d51fa24c335 100644 --- a/homeassistant/components/dsmr/translations/cs.json +++ b/homeassistant/components/dsmr/translations/cs.json @@ -1,11 +1,17 @@ { "config": { "abort": { - "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, + "error": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" }, "step": { "setup_network": { "data": { + "host": "Hostitel", "port": "Port" } }, diff --git a/homeassistant/components/ecowitt/translations/cs.json b/homeassistant/components/ecowitt/translations/cs.json index e1bf8e7f45f..b9301d0099d 100644 --- a/homeassistant/components/ecowitt/translations/cs.json +++ b/homeassistant/components/ecowitt/translations/cs.json @@ -1,6 +1,7 @@ { "config": { "error": { + "invalid_port": "Port je ji\u017e pou\u017e\u00edv\u00e1n", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" } } diff --git a/homeassistant/components/elkm1/translations/cs.json b/homeassistant/components/elkm1/translations/cs.json index 9ff8da3067d..e371fb4ea98 100644 --- a/homeassistant/components/elkm1/translations/cs.json +++ b/homeassistant/components/elkm1/translations/cs.json @@ -4,7 +4,9 @@ "address_already_configured": "ElkM1 s touto adresou je ji\u017e nastaven", "already_configured": "ElkM1 s t\u00edmto prefixem je ji\u017e nastaven", "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", - "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", diff --git a/homeassistant/components/elmax/translations/cs.json b/homeassistant/components/elmax/translations/cs.json index 6c534c1a8a6..593ffb5f43c 100644 --- a/homeassistant/components/elmax/translations/cs.json +++ b/homeassistant/components/elmax/translations/cs.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, "error": { "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", "invalid_pin": "Poskytnut\u00fd k\u00f3d PIN je neplatn\u00fd", diff --git a/homeassistant/components/escea/translations/cs.json b/homeassistant/components/escea/translations/cs.json new file mode 100644 index 00000000000..3f0012e00d2 --- /dev/null +++ b/homeassistant/components/escea/translations/cs.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/evil_genius_labs/translations/cs.json b/homeassistant/components/evil_genius_labs/translations/cs.json index 7a929c1286f..6633920519c 100644 --- a/homeassistant/components/evil_genius_labs/translations/cs.json +++ b/homeassistant/components/evil_genius_labs/translations/cs.json @@ -1,8 +1,16 @@ { "config": { "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "timeout": "Vypr\u0161el \u010dasov\u00fd limit pro nav\u00e1z\u00e1n\u00ed spojen\u00ed", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "host": "Hostitel" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/fibaro/translations/cs.json b/homeassistant/components/fibaro/translations/cs.json index e1bf8e7f45f..7cc900ee748 100644 --- a/homeassistant/components/fibaro/translations/cs.json +++ b/homeassistant/components/fibaro/translations/cs.json @@ -1,7 +1,28 @@ { "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + }, "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Heslo" + }, + "description": "Pros\u00edm aktualizujte sv\u00e9 heslo k \u00fa\u010dtu {username}", + "title": "Znovu ov\u011b\u0159it integraci" + }, + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/flume/translations/cs.json b/homeassistant/components/flume/translations/cs.json index e3f6cf1d39e..52118306733 100644 --- a/homeassistant/components/flume/translations/cs.json +++ b/homeassistant/components/flume/translations/cs.json @@ -10,6 +10,11 @@ "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { + "reauth_confirm": { + "data": { + "password": "Heslo" + } + }, "user": { "data": { "client_id": "ID klienta", diff --git a/homeassistant/components/fritz/translations/cs.json b/homeassistant/components/fritz/translations/cs.json index 8f114045caf..c4ca8595f52 100644 --- a/homeassistant/components/fritz/translations/cs.json +++ b/homeassistant/components/fritz/translations/cs.json @@ -8,6 +8,7 @@ "error": { "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" }, "flow_title": "{name}", @@ -26,7 +27,10 @@ }, "user": { "data": { - "port": "Port" + "host": "Hostitel", + "password": "Heslo", + "port": "Port", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" } } } diff --git a/homeassistant/components/fronius/translations/cs.json b/homeassistant/components/fronius/translations/cs.json index 773ee67a7cf..2b83abc6792 100644 --- a/homeassistant/components/fronius/translations/cs.json +++ b/homeassistant/components/fronius/translations/cs.json @@ -1,11 +1,20 @@ { "config": { "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", "invalid_host": "Neplatn\u00fd hostitel nebo IP adresa" }, "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, - "flow_title": "{device}" + "flow_title": "{device}", + "step": { + "user": { + "data": { + "host": "Hostitel" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/fully_kiosk/translations/cs.json b/homeassistant/components/fully_kiosk/translations/cs.json new file mode 100644 index 00000000000..dd68d899002 --- /dev/null +++ b/homeassistant/components/fully_kiosk/translations/cs.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "host": "Hostitel", + "password": "Heslo" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/garages_amsterdam/translations/cs.json b/homeassistant/components/garages_amsterdam/translations/cs.json index 5073c9248e0..6cb98e7ad68 100644 --- a/homeassistant/components/garages_amsterdam/translations/cs.json +++ b/homeassistant/components/garages_amsterdam/translations/cs.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" } diff --git a/homeassistant/components/generic/translations/cs.json b/homeassistant/components/generic/translations/cs.json index 73c3e470129..f6756ac66a9 100644 --- a/homeassistant/components/generic/translations/cs.json +++ b/homeassistant/components/generic/translations/cs.json @@ -3,6 +3,9 @@ "abort": { "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed" }, + "error": { + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, "step": { "content_type": { "data": { @@ -19,6 +22,9 @@ } }, "options": { + "error": { + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, "step": { "content_type": { "data": { diff --git a/homeassistant/components/geocaching/translations/cs.json b/homeassistant/components/geocaching/translations/cs.json new file mode 100644 index 00000000000..0ae00fc3605 --- /dev/null +++ b/homeassistant/components/geocaching/translations/cs.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + }, + "step": { + "reauth_confirm": { + "title": "Znovu ov\u011b\u0159it integraci" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/goalzero/translations/cs.json b/homeassistant/components/goalzero/translations/cs.json index c8d0ab45fd6..9097c3351dd 100644 --- a/homeassistant/components/goalzero/translations/cs.json +++ b/homeassistant/components/goalzero/translations/cs.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u00da\u010det je ji\u017e nastaven", + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "error": { diff --git a/homeassistant/components/google/translations/cs.json b/homeassistant/components/google/translations/cs.json index 0c11a65f69b..f1d2cc4c85a 100644 --- a/homeassistant/components/google/translations/cs.json +++ b/homeassistant/components/google/translations/cs.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "\u00da\u010det je ji\u017e nastaven", "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "invalid_access_token": "Neplatn\u00fd p\u0159\u00edstupov\u00fd token", "missing_configuration": "Komponenta nen\u00ed nastavena. Postupujte podle dokumentace.", "oauth_error": "P\u0159ijata neplatn\u00e1 data tokenu.", diff --git a/homeassistant/components/govee_ble/translations/cs.json b/homeassistant/components/govee_ble/translations/cs.json new file mode 100644 index 00000000000..925c1cbd337 --- /dev/null +++ b/homeassistant/components/govee_ble/translations/cs.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", + "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Chcete nastavit {name}?" + }, + "user": { + "data": { + "address": "Za\u0159\u00edzen\u00ed" + }, + "description": "Zvolte za\u0159\u00edzen\u00ed, kter\u00e9 chcete nastavit" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/growatt_server/translations/cs.json b/homeassistant/components/growatt_server/translations/cs.json index 31a4cdfdf03..2dcdd1e7c37 100644 --- a/homeassistant/components/growatt_server/translations/cs.json +++ b/homeassistant/components/growatt_server/translations/cs.json @@ -7,7 +7,9 @@ "user": { "data": { "name": "Jm\u00e9no", - "url": "URL" + "password": "Heslo", + "url": "URL", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" } } } diff --git a/homeassistant/components/here_travel_time/translations/cs.json b/homeassistant/components/here_travel_time/translations/cs.json new file mode 100644 index 00000000000..d0cbf3d50c3 --- /dev/null +++ b/homeassistant/components/here_travel_time/translations/cs.json @@ -0,0 +1,11 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hlk_sw16/translations/cs.json b/homeassistant/components/hlk_sw16/translations/cs.json index a4bad4b7c9f..0f02cd974c2 100644 --- a/homeassistant/components/hlk_sw16/translations/cs.json +++ b/homeassistant/components/hlk_sw16/translations/cs.json @@ -4,7 +4,7 @@ "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" }, "error": { - "cannot_connect": "Nelze se p\u0159ipojit", + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, diff --git a/homeassistant/components/homewizard/translations/cs.json b/homeassistant/components/homewizard/translations/cs.json index dc14c52ea9c..9e3a3155670 100644 --- a/homeassistant/components/homewizard/translations/cs.json +++ b/homeassistant/components/homewizard/translations/cs.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", "unknown_error": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { diff --git a/homeassistant/components/inkbird/translations/cs.json b/homeassistant/components/inkbird/translations/cs.json new file mode 100644 index 00000000000..925c1cbd337 --- /dev/null +++ b/homeassistant/components/inkbird/translations/cs.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", + "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Chcete nastavit {name}?" + }, + "user": { + "data": { + "address": "Za\u0159\u00edzen\u00ed" + }, + "description": "Zvolte za\u0159\u00edzen\u00ed, kter\u00e9 chcete nastavit" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/insteon/translations/cs.json b/homeassistant/components/insteon/translations/cs.json index e10e25e6361..a80e657efef 100644 --- a/homeassistant/components/insteon/translations/cs.json +++ b/homeassistant/components/insteon/translations/cs.json @@ -9,6 +9,9 @@ "select_single": "Vyberte jednu mo\u017enost." }, "step": { + "confirm_usb": { + "description": "Chcete nastavit {name}?" + }, "hubv1": { "data": { "host": "IP adresa", diff --git a/homeassistant/components/intellifire/translations/cs.json b/homeassistant/components/intellifire/translations/cs.json index 8684c426280..9ac5e9d9099 100644 --- a/homeassistant/components/intellifire/translations/cs.json +++ b/homeassistant/components/intellifire/translations/cs.json @@ -1,16 +1,22 @@ { "config": { "abort": { - "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" }, "flow_title": "{serial} ({host})", "step": { + "api_config": { + "data": { + "password": "Heslo" + } + }, "manual_device_entry": { "data": { - "host": "Hostitel" + "host": "Hostitel (IP adresa)" } }, "pick_device": { diff --git a/homeassistant/components/isy994/translations/cs.json b/homeassistant/components/isy994/translations/cs.json index d165050bf77..45cd90895c7 100644 --- a/homeassistant/components/isy994/translations/cs.json +++ b/homeassistant/components/isy994/translations/cs.json @@ -13,7 +13,8 @@ "step": { "reauth_confirm": { "data": { - "password": "Heslo" + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" } }, "user": { diff --git a/homeassistant/components/jellyfin/translations/cs.json b/homeassistant/components/jellyfin/translations/cs.json index 5d03904568e..c0841233cb7 100644 --- a/homeassistant/components/jellyfin/translations/cs.json +++ b/homeassistant/components/jellyfin/translations/cs.json @@ -4,6 +4,14 @@ "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/justnimbus/translations/cs.json b/homeassistant/components/justnimbus/translations/cs.json new file mode 100644 index 00000000000..19a31a3f9cb --- /dev/null +++ b/homeassistant/components/justnimbus/translations/cs.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kaleidescape/translations/cs.json b/homeassistant/components/kaleidescape/translations/cs.json index deb0693b00d..9044ce69fb1 100644 --- a/homeassistant/components/kaleidescape/translations/cs.json +++ b/homeassistant/components/kaleidescape/translations/cs.json @@ -8,6 +8,13 @@ "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" }, - "flow_title": "{model} ({name})" + "flow_title": "{model} ({name})", + "step": { + "user": { + "data": { + "host": "Hostitel" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/knx/translations/cs.json b/homeassistant/components/knx/translations/cs.json index 31c65f915dd..90c988aaeac 100644 --- a/homeassistant/components/knx/translations/cs.json +++ b/homeassistant/components/knx/translations/cs.json @@ -3,9 +3,13 @@ "abort": { "already_configured": "Slu\u017eba je ji\u017e nastavena" }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, "step": { "manual_tunnel": { "data": { + "host": "Hostitel", "port": "Port" } } @@ -15,6 +19,7 @@ "step": { "tunnel": { "data": { + "host": "Hostitel", "port": "Port" } } diff --git a/homeassistant/components/konnected/translations/cs.json b/homeassistant/components/konnected/translations/cs.json index f2cd8759a54..621ae847e34 100644 --- a/homeassistant/components/konnected/translations/cs.json +++ b/homeassistant/components/konnected/translations/cs.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "not_konn_panel": "Nejedn\u00e1 se o rozpoznan\u00e9 Konnected.io za\u0159\u00edzen\u00ed", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, diff --git a/homeassistant/components/kraken/translations/tr.json b/homeassistant/components/kraken/translations/tr.json index fa3decd322e..0d54a302a50 100644 --- a/homeassistant/components/kraken/translations/tr.json +++ b/homeassistant/components/kraken/translations/tr.json @@ -11,7 +11,7 @@ "user": { "data": { "one": "Bo\u015f", - "other": "" + "other": "Bo\u015f" }, "description": "Kuruluma ba\u015flamak ister misiniz?" } diff --git a/homeassistant/components/lacrosse_view/translations/cs.json b/homeassistant/components/lacrosse_view/translations/cs.json new file mode 100644 index 00000000000..0f5d2655f6c --- /dev/null +++ b/homeassistant/components/lacrosse_view/translations/cs.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lametric/translations/cs.json b/homeassistant/components/lametric/translations/cs.json new file mode 100644 index 00000000000..7abe7e7c1a9 --- /dev/null +++ b/homeassistant/components/lametric/translations/cs.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "manual_entry": { + "data": { + "host": "Hostitel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/landisgyr_heat_meter/translations/cs.json b/homeassistant/components/landisgyr_heat_meter/translations/cs.json new file mode 100644 index 00000000000..0887542d784 --- /dev/null +++ b/homeassistant/components/landisgyr_heat_meter/translations/cs.json @@ -0,0 +1,11 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/laundrify/translations/cs.json b/homeassistant/components/laundrify/translations/cs.json new file mode 100644 index 00000000000..9eecd724896 --- /dev/null +++ b/homeassistant/components/laundrify/translations/cs.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "reauth_confirm": { + "title": "Znovu ov\u011b\u0159it integraci" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/led_ble/translations/cs.json b/homeassistant/components/led_ble/translations/cs.json new file mode 100644 index 00000000000..99738ebc78e --- /dev/null +++ b/homeassistant/components/led_ble/translations/cs.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", + "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lg_soundbar/translations/cs.json b/homeassistant/components/lg_soundbar/translations/cs.json new file mode 100644 index 00000000000..6fabc170b6e --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/cs.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, + "step": { + "user": { + "data": { + "host": "Hostitel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/translations/cs.json b/homeassistant/components/life360/translations/cs.json index 0c267ef7163..570147cd678 100644 --- a/homeassistant/components/life360/translations/cs.json +++ b/homeassistant/components/life360/translations/cs.json @@ -2,6 +2,7 @@ "config": { "abort": { "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "create_entry": { @@ -9,11 +10,18 @@ }, "error": { "already_configured": "\u00da\u010det je ji\u017e nastaven", + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", "invalid_username": "Neplatn\u00e9 u\u017eivatelsk\u00e9 jm\u00e9no", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { + "reauth_confirm": { + "data": { + "password": "Heslo" + }, + "title": "Znovu ov\u011b\u0159it integraci" + }, "user": { "data": { "password": "Heslo", diff --git a/homeassistant/components/lifx/translations/cs.json b/homeassistant/components/lifx/translations/cs.json index 4b2e480e4bb..660884bc1e7 100644 --- a/homeassistant/components/lifx/translations/cs.json +++ b/homeassistant/components/lifx/translations/cs.json @@ -1,12 +1,25 @@ { "config": { "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed", "single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace." }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, "step": { "confirm": { "description": "Chcete nastavit LIFX?" + }, + "discovery_confirm": { + "description": "Chcete nastavit {label} ({host}) {serial}?" + }, + "user": { + "data": { + "host": "Hostitel" + } } } } diff --git a/homeassistant/components/litterrobot/translations/cs.json b/homeassistant/components/litterrobot/translations/cs.json index b6c00c05389..e5d6edc65ea 100644 --- a/homeassistant/components/litterrobot/translations/cs.json +++ b/homeassistant/components/litterrobot/translations/cs.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u00da\u010det je ji\u017e nastaven" + "already_configured": "\u00da\u010det je ji\u017e nastaven", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", @@ -9,6 +10,12 @@ "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { + "reauth_confirm": { + "data": { + "password": "Heslo" + }, + "title": "Znovu ov\u011b\u0159it integraci" + }, "user": { "data": { "password": "Heslo", diff --git a/homeassistant/components/lookin/translations/cs.json b/homeassistant/components/lookin/translations/cs.json index 50dcaf1b95f..a932275300f 100644 --- a/homeassistant/components/lookin/translations/cs.json +++ b/homeassistant/components/lookin/translations/cs.json @@ -1,9 +1,14 @@ { "config": { "abort": { - "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1" + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed" }, "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { diff --git a/homeassistant/components/meater/translations/cs.json b/homeassistant/components/meater/translations/cs.json index 72c98504526..2782d56e4b4 100644 --- a/homeassistant/components/meater/translations/cs.json +++ b/homeassistant/components/meater/translations/cs.json @@ -5,6 +5,11 @@ "unknown_auth_error": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { + "reauth_confirm": { + "data": { + "password": "Heslo" + } + }, "user": { "data": { "password": "Heslo", diff --git a/homeassistant/components/meteoclimatic/translations/cs.json b/homeassistant/components/meteoclimatic/translations/cs.json index 3b814303e69..c9f0e950664 100644 --- a/homeassistant/components/meteoclimatic/translations/cs.json +++ b/homeassistant/components/meteoclimatic/translations/cs.json @@ -1,7 +1,11 @@ { "config": { "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "error": { + "not_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed" } } } \ No newline at end of file diff --git a/homeassistant/components/mill/translations/cs.json b/homeassistant/components/mill/translations/cs.json index f6c47b4c840..7e366233d85 100644 --- a/homeassistant/components/mill/translations/cs.json +++ b/homeassistant/components/mill/translations/cs.json @@ -5,6 +5,14 @@ }, "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, + "step": { + "cloud": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/mjpeg/translations/cs.json b/homeassistant/components/mjpeg/translations/cs.json index 9c6676509bf..f5813b43ac0 100644 --- a/homeassistant/components/mjpeg/translations/cs.json +++ b/homeassistant/components/mjpeg/translations/cs.json @@ -10,19 +10,25 @@ "step": { "user": { "data": { - "name": "Jm\u00e9no" + "name": "Jm\u00e9no", + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" } } } }, "options": { "error": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" }, "step": { "init": { "data": { - "name": "Jm\u00e9no" + "name": "Jm\u00e9no", + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" } } } diff --git a/homeassistant/components/moat/translations/cs.json b/homeassistant/components/moat/translations/cs.json new file mode 100644 index 00000000000..925c1cbd337 --- /dev/null +++ b/homeassistant/components/moat/translations/cs.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", + "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Chcete nastavit {name}?" + }, + "user": { + "data": { + "address": "Za\u0159\u00edzen\u00ed" + }, + "description": "Zvolte za\u0159\u00edzen\u00ed, kter\u00e9 chcete nastavit" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/modern_forms/translations/cs.json b/homeassistant/components/modern_forms/translations/cs.json new file mode 100644 index 00000000000..4ccfa17e6d3 --- /dev/null +++ b/homeassistant/components/modern_forms/translations/cs.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, + "step": { + "user": { + "data": { + "host": "Hostitel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motioneye/translations/cs.json b/homeassistant/components/motioneye/translations/cs.json index 311a1d4d965..45f178ef29e 100644 --- a/homeassistant/components/motioneye/translations/cs.json +++ b/homeassistant/components/motioneye/translations/cs.json @@ -13,6 +13,10 @@ "step": { "user": { "data": { + "admin_password": "Heslo spr\u00e1vce", + "admin_username": "U\u017eivatelsk\u00e9 jm\u00e9no spr\u00e1vce", + "surveillance_password": "Heslo pro dohled", + "surveillance_username": "U\u017eivatelsk\u00e9 jm\u00e9no pro dohled", "url": "URL" } } diff --git a/homeassistant/components/nam/translations/cs.json b/homeassistant/components/nam/translations/cs.json index 1b979ab1412..01c1215497b 100644 --- a/homeassistant/components/nam/translations/cs.json +++ b/homeassistant/components/nam/translations/cs.json @@ -5,8 +5,28 @@ "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "credentials": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + }, + "reauth_confirm": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + }, + "user": { + "data": { + "host": "Hostitel" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/cs.json b/homeassistant/components/nextdns/translations/cs.json new file mode 100644 index 00000000000..849220d4ba6 --- /dev/null +++ b/homeassistant/components/nextdns/translations/cs.json @@ -0,0 +1,9 @@ +{ + "config": { + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_api_key": "Neplatn\u00fd kl\u00ed\u010d API", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nina/translations/cs.json b/homeassistant/components/nina/translations/cs.json index 66814b1652e..fbe3187769a 100644 --- a/homeassistant/components/nina/translations/cs.json +++ b/homeassistant/components/nina/translations/cs.json @@ -7,5 +7,11 @@ "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" } + }, + "options": { + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + } } } \ No newline at end of file diff --git a/homeassistant/components/nobo_hub/translations/cs.json b/homeassistant/components/nobo_hub/translations/cs.json new file mode 100644 index 00000000000..7c0da879b74 --- /dev/null +++ b/homeassistant/components/nobo_hub/translations/cs.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit - zkontrolujte s\u00e9riov\u00e9 \u010d\u00edslo", + "invalid_ip": "Neplatn\u00e1 IP adresa", + "invalid_serial": "Neplatn\u00e9 s\u00e9riov\u00e9 \u010d\u00edslo", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "manual": { + "data": { + "serial": "S\u00e9riov\u00e9 \u010d\u00edslo (12 \u010d\u00edslic)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/octoprint/translations/cs.json b/homeassistant/components/octoprint/translations/cs.json index fa519dfe6e1..c2e27a5efb0 100644 --- a/homeassistant/components/octoprint/translations/cs.json +++ b/homeassistant/components/octoprint/translations/cs.json @@ -1,14 +1,19 @@ { "config": { "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { "user": { "data": { + "host": "Hostitel", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no", "verify_ssl": "Ov\u011b\u0159it certifik\u00e1t SSL" } } diff --git a/homeassistant/components/onewire/translations/cs.json b/homeassistant/components/onewire/translations/cs.json index ae22bc1ce25..05c02505642 100644 --- a/homeassistant/components/onewire/translations/cs.json +++ b/homeassistant/components/onewire/translations/cs.json @@ -8,6 +8,9 @@ }, "step": { "user": { + "data": { + "host": "Hostitel" + }, "title": "Nastaven\u00ed 1-Wire" } } diff --git a/homeassistant/components/openexchangerates/translations/cs.json b/homeassistant/components/openexchangerates/translations/cs.json new file mode 100644 index 00000000000..5cd704ff76c --- /dev/null +++ b/homeassistant/components/openexchangerates/translations/cs.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plum_lightpad/translations/cs.json b/homeassistant/components/plum_lightpad/translations/cs.json index e530ca166e5..593f2cbfb08 100644 --- a/homeassistant/components/plum_lightpad/translations/cs.json +++ b/homeassistant/components/plum_lightpad/translations/cs.json @@ -4,7 +4,7 @@ "already_configured": "\u00da\u010det je ji\u017e nastaven" }, "error": { - "cannot_connect": "Nelze se p\u0159ipojit" + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" }, "step": { "user": { diff --git a/homeassistant/components/pushover/translations/cs.json b/homeassistant/components/pushover/translations/cs.json new file mode 100644 index 00000000000..ddeb87bf3fd --- /dev/null +++ b/homeassistant/components/pushover/translations/cs.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_api_key": "Neplatn\u00fd kl\u00ed\u010d API" + }, + "step": { + "reauth_confirm": { + "title": "Znovu ov\u011b\u0159it integraci" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qingping/translations/cs.json b/homeassistant/components/qingping/translations/cs.json new file mode 100644 index 00000000000..925c1cbd337 --- /dev/null +++ b/homeassistant/components/qingping/translations/cs.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", + "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Chcete nastavit {name}?" + }, + "user": { + "data": { + "address": "Za\u0159\u00edzen\u00ed" + }, + "description": "Zvolte za\u0159\u00edzen\u00ed, kter\u00e9 chcete nastavit" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qnap_qsw/translations/cs.json b/homeassistant/components/qnap_qsw/translations/cs.json index 33006d6761b..26d5197ced6 100644 --- a/homeassistant/components/qnap_qsw/translations/cs.json +++ b/homeassistant/components/qnap_qsw/translations/cs.json @@ -2,6 +2,24 @@ "config": { "abort": { "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" + }, + "step": { + "discovered_connection": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + }, + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/radiotherm/translations/cs.json b/homeassistant/components/radiotherm/translations/cs.json new file mode 100644 index 00000000000..bfa5884e24c --- /dev/null +++ b/homeassistant/components/radiotherm/translations/cs.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "confirm": { + "description": "Chcete nastavit {name} {model} ({host})?" + }, + "user": { + "data": { + "host": "Hostitel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rdw/translations/cs.json b/homeassistant/components/rdw/translations/cs.json new file mode 100644 index 00000000000..5d403348397 --- /dev/null +++ b/homeassistant/components/rdw/translations/cs.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rhasspy/translations/cs.json b/homeassistant/components/rhasspy/translations/cs.json new file mode 100644 index 00000000000..edf19d3cdb8 --- /dev/null +++ b/homeassistant/components/rhasspy/translations/cs.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "description": "Chcete povolit podporu Rhasspy?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ridwell/translations/cs.json b/homeassistant/components/ridwell/translations/cs.json index 5d43feee500..8af205757ee 100644 --- a/homeassistant/components/ridwell/translations/cs.json +++ b/homeassistant/components/ridwell/translations/cs.json @@ -7,6 +7,20 @@ "error": { "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Heslo" + }, + "title": "Znovu ov\u011b\u0159it integraci" + }, + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/risco/translations/cs.json b/homeassistant/components/risco/translations/cs.json index a4d2971e220..6c2e3d7ac00 100644 --- a/homeassistant/components/risco/translations/cs.json +++ b/homeassistant/components/risco/translations/cs.json @@ -9,6 +9,17 @@ "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { + "cloud": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + }, + "local": { + "data": { + "host": "Hostitel" + } + }, "user": { "data": { "password": "Heslo", diff --git a/homeassistant/components/roon/translations/cs.json b/homeassistant/components/roon/translations/cs.json index 150b2f5f74a..d0e8755aa8d 100644 --- a/homeassistant/components/roon/translations/cs.json +++ b/homeassistant/components/roon/translations/cs.json @@ -8,6 +8,11 @@ "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { + "fallback": { + "data": { + "host": "Hostitel" + } + }, "link": { "description": "Mus\u00edte povolit Home Assistant v Roon. Po kliknut\u00ed na Odeslat p\u0159ejd\u011bte do aplikace Roon Core, otev\u0159ete Nastaven\u00ed a na z\u00e1lo\u017ece Roz\u0161\u00ed\u0159en\u00ed povolte Home Assistant.", "title": "Autorizujte HomeAssistant v Roon" diff --git a/homeassistant/components/roon/translations/tr.json b/homeassistant/components/roon/translations/tr.json index a05dfed70f1..c205364c22b 100644 --- a/homeassistant/components/roon/translations/tr.json +++ b/homeassistant/components/roon/translations/tr.json @@ -18,6 +18,10 @@ "link": { "description": "Roon'da HomeAssistant\u0131 yetkilendirmelisiniz. G\u00f6nder'e t\u0131klad\u0131ktan sonra, Roon Core uygulamas\u0131na gidin, Ayarlar'\u0131 a\u00e7\u0131n ve Uzant\u0131lar sekmesinde HomeAssistant'\u0131 etkinle\u015ftirin.", "title": "Roon'da HomeAssistant'\u0131 Yetkilendirme" + }, + "user": { + "one": "Bo\u015f", + "other": "Bo\u015f" } } } diff --git a/homeassistant/components/sabnzbd/translations/cs.json b/homeassistant/components/sabnzbd/translations/cs.json new file mode 100644 index 00000000000..764f65efebe --- /dev/null +++ b/homeassistant/components/sabnzbd/translations/cs.json @@ -0,0 +1,8 @@ +{ + "config": { + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_api_key": "Neplatn\u00fd kl\u00ed\u010d API" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/scrape/translations/cs.json b/homeassistant/components/scrape/translations/cs.json index 8669b5b1330..43a819be694 100644 --- a/homeassistant/components/scrape/translations/cs.json +++ b/homeassistant/components/scrape/translations/cs.json @@ -3,7 +3,9 @@ "step": { "user": { "data": { - "headers": "Hlavi\u010dky" + "headers": "Hlavi\u010dky", + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" } } } diff --git a/homeassistant/components/senseme/translations/cs.json b/homeassistant/components/senseme/translations/cs.json index 80ce3019ead..d13d67837e5 100644 --- a/homeassistant/components/senseme/translations/cs.json +++ b/homeassistant/components/senseme/translations/cs.json @@ -1,12 +1,18 @@ { "config": { "abort": { - "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" }, "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" }, "step": { + "manual": { + "data": { + "host": "Hostitel" + } + }, "user": { "data": { "device": "Za\u0159\u00edzen\u00ed" diff --git a/homeassistant/components/sensibo/translations/cs.json b/homeassistant/components/sensibo/translations/cs.json index fcfba839932..e04cc828032 100644 --- a/homeassistant/components/sensibo/translations/cs.json +++ b/homeassistant/components/sensibo/translations/cs.json @@ -1,15 +1,25 @@ { "config": { "abort": { - "already_configured": "\u00da\u010det je ji\u017e nastaven" + "already_configured": "\u00da\u010det je ji\u017e nastaven", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "error": { - "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" }, "step": { + "reauth_confirm": { + "data_description": { + "api_key": "Postupujte podle dokumentace a z\u00edskejte nov\u00fd kl\u00ed\u010d api." + } + }, "user": { "data": { "api_key": "Kl\u00ed\u010d API" + }, + "data_description": { + "api_key": "Postupujte podle dokumentace a z\u00edskejte sv\u016fj kl\u00ed\u010d api." } } } diff --git a/homeassistant/components/sensorpro/translations/cs.json b/homeassistant/components/sensorpro/translations/cs.json new file mode 100644 index 00000000000..2d35a94dba3 --- /dev/null +++ b/homeassistant/components/sensorpro/translations/cs.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", + "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed", + "not_supported": "Nepodporovan\u00e9 za\u0159\u00edzen\u00ed" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Chcete nastavit {name}?" + }, + "user": { + "data": { + "address": "Za\u0159\u00edzen\u00ed" + }, + "description": "Zvolte za\u0159\u00edzen\u00ed, kter\u00e9 chcete nastavit" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensorpush/translations/cs.json b/homeassistant/components/sensorpush/translations/cs.json new file mode 100644 index 00000000000..925c1cbd337 --- /dev/null +++ b/homeassistant/components/sensorpush/translations/cs.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", + "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Chcete nastavit {name}?" + }, + "user": { + "data": { + "address": "Za\u0159\u00edzen\u00ed" + }, + "description": "Zvolte za\u0159\u00edzen\u00ed, kter\u00e9 chcete nastavit" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/senz/translations/cs.json b/homeassistant/components/senz/translations/cs.json new file mode 100644 index 00000000000..08428555c2b --- /dev/null +++ b/homeassistant/components/senz/translations/cs.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/cs.json b/homeassistant/components/simplepush/translations/cs.json new file mode 100644 index 00000000000..9026f86030f --- /dev/null +++ b/homeassistant/components/simplepush/translations/cs.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/skybell/translations/cs.json b/homeassistant/components/skybell/translations/cs.json index 08830492748..c294122142d 100644 --- a/homeassistant/components/skybell/translations/cs.json +++ b/homeassistant/components/skybell/translations/cs.json @@ -2,6 +2,24 @@ "config": { "abort": { "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Heslo" + }, + "title": "Znovu ov\u011b\u0159it integraci" + }, + "user": { + "data": { + "password": "Heslo" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/slack/translations/cs.json b/homeassistant/components/slack/translations/cs.json index a4b5fb71cbe..2cffb76f5e7 100644 --- a/homeassistant/components/slack/translations/cs.json +++ b/homeassistant/components/slack/translations/cs.json @@ -4,6 +4,8 @@ "already_configured": "Slu\u017eba je ji\u017e nastavena" }, "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { diff --git a/homeassistant/components/sleepiq/translations/cs.json b/homeassistant/components/sleepiq/translations/cs.json index efbe2a91eb1..f4d930517ad 100644 --- a/homeassistant/components/sleepiq/translations/cs.json +++ b/homeassistant/components/sleepiq/translations/cs.json @@ -1,7 +1,25 @@ { "config": { + "abort": { + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + }, "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Heslo" + }, + "title": "Znovu ov\u011b\u0159it integraci" + }, + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/solax/translations/cs.json b/homeassistant/components/solax/translations/cs.json index 2bf3fbfa3fb..0dd72e12672 100644 --- a/homeassistant/components/solax/translations/cs.json +++ b/homeassistant/components/solax/translations/cs.json @@ -1,12 +1,14 @@ { "config": { "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { "user": { "data": { "ip_address": "IP adresa", + "password": "Heslo", "port": "Port" } } diff --git a/homeassistant/components/soundtouch/translations/cs.json b/homeassistant/components/soundtouch/translations/cs.json new file mode 100644 index 00000000000..6fabc170b6e --- /dev/null +++ b/homeassistant/components/soundtouch/translations/cs.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, + "step": { + "user": { + "data": { + "host": "Hostitel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/steam_online/translations/cs.json b/homeassistant/components/steam_online/translations/cs.json index ffd864a8f1d..48635ef897d 100644 --- a/homeassistant/components/steam_online/translations/cs.json +++ b/homeassistant/components/steam_online/translations/cs.json @@ -1,9 +1,18 @@ { "config": { "abort": { - "already_configured": "Slu\u017eba je ji\u017e nastavena" + "already_configured": "Slu\u017eba je ji\u017e nastavena", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { + "reauth_confirm": { + "title": "Znovu ov\u011b\u0159it integraci" + }, "user": { "data": { "api_key": "Kl\u00ed\u010d API" diff --git a/homeassistant/components/switchbee/translations/cs.json b/homeassistant/components/switchbee/translations/cs.json new file mode 100644 index 00000000000..4b40956d82b --- /dev/null +++ b/homeassistant/components/switchbee/translations/cs.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "host": "Hostitel", + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "devices": "Zahrnout za\u0159\u00edzen\u00ed" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switchbee/translations/de.json b/homeassistant/components/switchbee/translations/de.json new file mode 100644 index 00000000000..99862c61d84 --- /dev/null +++ b/homeassistant/components/switchbee/translations/de.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Passwort", + "switch_as_light": "Schalter als Lichteinheiten initialisieren", + "username": "Benutzername" + }, + "description": "Einrichten der SwitchBee-Integration mit Home Assistant." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "devices": "Einzuschlie\u00dfende Ger\u00e4te" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switchbee/translations/el.json b/homeassistant/components/switchbee/translations/el.json new file mode 100644 index 00000000000..ad806294956 --- /dev/null +++ b/homeassistant/components/switchbee/translations/el.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "user": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "switch_as_light": "\u0391\u03c1\u03c7\u03b9\u03ba\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03c4\u03c9\u03bd \u03b4\u03b9\u03b1\u03ba\u03bf\u03c0\u03c4\u03ce\u03bd \u03c9\u03c2 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2 \u03c6\u03c9\u03c4\u03cc\u03c2", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + }, + "description": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 SwitchBee \u03bc\u03b5 \u03c4\u03bf Home Assistant." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "devices": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c0\u03bf\u03c5 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c3\u03c5\u03bc\u03c0\u03b5\u03c1\u03b9\u03bb\u03b7\u03c6\u03b8\u03bf\u03cd\u03bd" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switchbee/translations/en.json b/homeassistant/components/switchbee/translations/en.json index 8fee54f3fba..41f9ee6a043 100644 --- a/homeassistant/components/switchbee/translations/en.json +++ b/homeassistant/components/switchbee/translations/en.json @@ -1,32 +1,32 @@ { - "config": { - "abort": { - "already_configured_device": "Device is already configured" - }, - "error": { - "cannot_connect": "Failed to connect", - "invalid_auth": "Failed to Authenticate with the Central Unit", - "unknown": "Unexpected error" - }, - "step": { - "user": { - "description": "Setup SwitchBee integration with Home Assistant.", - "data": { - "host": "Central Unit IP address", - "username": "User (e-mail)", - "password": "Password", - "switch_as_light": "Initialize switches as light entities" + "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", + "password": "Password", + "switch_as_light": "Initialize switches as light entities", + "username": "Username" + }, + "description": "Setup SwitchBee integration with Home Assistant." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "devices": "Devices to include" + } } - } - } - }, - "options": { - "step": { - "init": { - "data": { - "devices": "Devices to include" - } } - } } } \ No newline at end of file diff --git a/homeassistant/components/switchbee/translations/es.json b/homeassistant/components/switchbee/translations/es.json new file mode 100644 index 00000000000..fc22055048d --- /dev/null +++ b/homeassistant/components/switchbee/translations/es.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Contrase\u00f1a", + "switch_as_light": "Inicializar interruptores como entidades luces", + "username": "Nombre de usuario" + }, + "description": "Configurar la integraci\u00f3n SwitchBee con Home Assistant." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "devices": "Dispositivos a incluir" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switchbee/translations/et.json b/homeassistant/components/switchbee/translations/et.json new file mode 100644 index 00000000000..fa6c114d434 --- /dev/null +++ b/homeassistant/components/switchbee/translations/et.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamine nurjus", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Salas\u00f5na", + "switch_as_light": "L\u00e4htesta l\u00fclitid valgusolemitena", + "username": "Kasutajanimi" + }, + "description": "Seadista SwitchBee sidumine Home Assistantiga." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "devices": "Kaasatavad seadmed" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switchbee/translations/fr.json b/homeassistant/components/switchbee/translations/fr.json new file mode 100644 index 00000000000..1f1d36f5da0 --- /dev/null +++ b/homeassistant/components/switchbee/translations/fr.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification non valide", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "host": "H\u00f4te", + "password": "Mot de passe", + "switch_as_light": "Initialiser les commutateurs en tant que lumi\u00e8res", + "username": "Nom d'utilisateur" + }, + "description": "Configurez l'int\u00e9gration de SwitchBee avec Home Assistant." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "devices": "Appareils \u00e0 inclure" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switchbee/translations/pt-BR.json b/homeassistant/components/switchbee/translations/pt-BR.json new file mode 100644 index 00000000000..2b93ff92ac3 --- /dev/null +++ b/homeassistant/components/switchbee/translations/pt-BR.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falhou ao se conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Senha", + "switch_as_light": "Inicializar switches como entidades de luz", + "username": "Nome de usu\u00e1rio" + }, + "description": "Configure a integra\u00e7\u00e3o do SwitchBee com o Home Assistant." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "devices": "Dispositivos para incluir" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switchbee/translations/zh-Hant.json b/homeassistant/components/switchbee/translations/zh-Hant.json new file mode 100644 index 00000000000..baa704a5235 --- /dev/null +++ b/homeassistant/components/switchbee/translations/zh-Hant.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "password": "\u5bc6\u78bc", + "switch_as_light": "\u5c07\u958b\u95dc\u521d\u59cb\u70ba\u71c8\u5149\u5be6\u9ad4", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "description": "\u8a2d\u5b9a SwitchBee \u63a5\u5165 Home Assistant \u6574\u5408\u3002" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "devices": "\u5305\u542b\u88dd\u7f6e" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switchbot/translations/cs.json b/homeassistant/components/switchbot/translations/cs.json index f92951a0e15..5e57c3b60a7 100644 --- a/homeassistant/components/switchbot/translations/cs.json +++ b/homeassistant/components/switchbot/translations/cs.json @@ -7,6 +7,14 @@ }, "flow_title": "{name}", "step": { + "confirm": { + "description": "Chcete nastavit {name}?" + }, + "password": { + "data": { + "password": "Heslo" + } + }, "user": { "data": { "name": "Jm\u00e9no", diff --git a/homeassistant/components/synology_dsm/translations/cs.json b/homeassistant/components/synology_dsm/translations/cs.json index dfa0a5fd347..0581726e86a 100644 --- a/homeassistant/components/synology_dsm/translations/cs.json +++ b/homeassistant/components/synology_dsm/translations/cs.json @@ -32,7 +32,8 @@ "data": { "password": "Heslo", "username": "U\u017eivatelsk\u00e9 jm\u00e9no" - } + }, + "title": "Znovu ov\u011b\u0159it integraci Synology DSM" }, "user": { "data": { diff --git a/homeassistant/components/system_bridge/translations/cs.json b/homeassistant/components/system_bridge/translations/cs.json index 372a54786bd..8d8ade98823 100644 --- a/homeassistant/components/system_bridge/translations/cs.json +++ b/homeassistant/components/system_bridge/translations/cs.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, @@ -11,6 +12,7 @@ "step": { "user": { "data": { + "host": "Hostitel", "port": "Port" } } diff --git a/homeassistant/components/tailscale/translations/cs.json b/homeassistant/components/tailscale/translations/cs.json index 3bfe94e68dd..a23047f4af8 100644 --- a/homeassistant/components/tailscale/translations/cs.json +++ b/homeassistant/components/tailscale/translations/cs.json @@ -4,6 +4,7 @@ "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" } } diff --git a/homeassistant/components/tankerkoenig/translations/cs.json b/homeassistant/components/tankerkoenig/translations/cs.json new file mode 100644 index 00000000000..3bfe94e68dd --- /dev/null +++ b/homeassistant/components/tankerkoenig/translations/cs.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tautulli/translations/cs.json b/homeassistant/components/tautulli/translations/cs.json new file mode 100644 index 00000000000..45e02001105 --- /dev/null +++ b/homeassistant/components/tautulli/translations/cs.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla_wall_connector/translations/cs.json b/homeassistant/components/tesla_wall_connector/translations/cs.json index e1bf8e7f45f..5eac883adf0 100644 --- a/homeassistant/components/tesla_wall_connector/translations/cs.json +++ b/homeassistant/components/tesla_wall_connector/translations/cs.json @@ -1,7 +1,18 @@ { "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "host": "Hostitel" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/thermobeacon/translations/cs.json b/homeassistant/components/thermobeacon/translations/cs.json new file mode 100644 index 00000000000..925c1cbd337 --- /dev/null +++ b/homeassistant/components/thermobeacon/translations/cs.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", + "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Chcete nastavit {name}?" + }, + "user": { + "data": { + "address": "Za\u0159\u00edzen\u00ed" + }, + "description": "Zvolte za\u0159\u00edzen\u00ed, kter\u00e9 chcete nastavit" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/thermopro/translations/cs.json b/homeassistant/components/thermopro/translations/cs.json new file mode 100644 index 00000000000..925c1cbd337 --- /dev/null +++ b/homeassistant/components/thermopro/translations/cs.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", + "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Chcete nastavit {name}?" + }, + "user": { + "data": { + "address": "Za\u0159\u00edzen\u00ed" + }, + "description": "Zvolte za\u0159\u00edzen\u00ed, kter\u00e9 chcete nastavit" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tilt_ble/translations/cs.json b/homeassistant/components/tilt_ble/translations/cs.json new file mode 100644 index 00000000000..925c1cbd337 --- /dev/null +++ b/homeassistant/components/tilt_ble/translations/cs.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", + "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Chcete nastavit {name}?" + }, + "user": { + "data": { + "address": "Za\u0159\u00edzen\u00ed" + }, + "description": "Zvolte za\u0159\u00edzen\u00ed, kter\u00e9 chcete nastavit" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tolo/translations/cs.json b/homeassistant/components/tolo/translations/cs.json new file mode 100644 index 00000000000..6fabc170b6e --- /dev/null +++ b/homeassistant/components/tolo/translations/cs.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, + "step": { + "user": { + "data": { + "host": "Hostitel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tomorrowio/translations/cs.json b/homeassistant/components/tomorrowio/translations/cs.json index 6b50656cd00..0a6cfc5cf12 100644 --- a/homeassistant/components/tomorrowio/translations/cs.json +++ b/homeassistant/components/tomorrowio/translations/cs.json @@ -1,5 +1,9 @@ { "config": { + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_api_key": "Neplatn\u00fd kl\u00ed\u010d API" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/trafikverket_ferry/translations/cs.json b/homeassistant/components/trafikverket_ferry/translations/cs.json new file mode 100644 index 00000000000..a23047f4af8 --- /dev/null +++ b/homeassistant/components/trafikverket_ferry/translations/cs.json @@ -0,0 +1,11 @@ +{ + "config": { + "abort": { + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_train/translations/cs.json b/homeassistant/components/trafikverket_train/translations/cs.json new file mode 100644 index 00000000000..a23047f4af8 --- /dev/null +++ b/homeassistant/components/trafikverket_train/translations/cs.json @@ -0,0 +1,11 @@ +{ + "config": { + "abort": { + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/translations/cs.json b/homeassistant/components/transmission/translations/cs.json index c2c0bf5ead0..57adb762c78 100644 --- a/homeassistant/components/transmission/translations/cs.json +++ b/homeassistant/components/transmission/translations/cs.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", @@ -9,6 +10,12 @@ "name_exists": "Jm\u00e9no ji\u017e existuje" }, "step": { + "reauth_confirm": { + "data": { + "password": "Heslo" + }, + "title": "Znovu ov\u011b\u0159it integraci" + }, "user": { "data": { "host": "Hostitel", diff --git a/homeassistant/components/ukraine_alarm/translations/cs.json b/homeassistant/components/ukraine_alarm/translations/cs.json new file mode 100644 index 00000000000..5073c9248e0 --- /dev/null +++ b/homeassistant/components/ukraine_alarm/translations/cs.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vallox/translations/cs.json b/homeassistant/components/vallox/translations/cs.json index ffef74e8f9c..d5523c658f5 100644 --- a/homeassistant/components/vallox/translations/cs.json +++ b/homeassistant/components/vallox/translations/cs.json @@ -7,7 +7,15 @@ "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "host": "Hostitel" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/venstar/translations/cs.json b/homeassistant/components/venstar/translations/cs.json index e1bf8e7f45f..5d18628f255 100644 --- a/homeassistant/components/venstar/translations/cs.json +++ b/homeassistant/components/venstar/translations/cs.json @@ -1,7 +1,20 @@ { "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "host": "Hostitel", + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/volvooncall/translations/cs.json b/homeassistant/components/volvooncall/translations/cs.json new file mode 100644 index 00000000000..c742628a5b7 --- /dev/null +++ b/homeassistant/components/volvooncall/translations/cs.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wallbox/translations/cs.json b/homeassistant/components/wallbox/translations/cs.json index 5bc59de6051..13c0827ff40 100644 --- a/homeassistant/components/wallbox/translations/cs.json +++ b/homeassistant/components/wallbox/translations/cs.json @@ -1,15 +1,24 @@ { "config": { "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { "reauth_confirm": { "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + }, + "user": { + "data": { + "password": "Heslo", "username": "U\u017eivatelsk\u00e9 jm\u00e9no" } } diff --git a/homeassistant/components/withings/translations/cs.json b/homeassistant/components/withings/translations/cs.json index c6a84f3a296..06762a5fb1b 100644 --- a/homeassistant/components/withings/translations/cs.json +++ b/homeassistant/components/withings/translations/cs.json @@ -25,6 +25,9 @@ }, "reauth": { "title": "Znovu ov\u011b\u0159it integraci" + }, + "reauth_confirm": { + "title": "Znovu ov\u011b\u0159it integraci" } } } diff --git a/homeassistant/components/wolflink/translations/sensor.cs.json b/homeassistant/components/wolflink/translations/sensor.cs.json index fff383f8b8d..18209584d39 100644 --- a/homeassistant/components/wolflink/translations/sensor.cs.json +++ b/homeassistant/components/wolflink/translations/sensor.cs.json @@ -13,8 +13,8 @@ "externe_deaktivierung": "Extern\u00ed deaktivace", "frostschutz": "Ochrana proti mrazu", "gasdruck": "Tlak plynu", - "heizbetrieb": "Re\u017eim topen\u00ed", - "heizung": "Topen\u00ed", + "heizbetrieb": "Re\u017eim vyt\u00e1p\u011bn\u00ed", + "heizung": "Vyt\u00e1p\u011bn\u00ed", "initialisierung": "Inicializace", "kalibration": "Kalibrace", "permanent": "Trval\u00fd", diff --git a/homeassistant/components/ws66i/translations/cs.json b/homeassistant/components/ws66i/translations/cs.json new file mode 100644 index 00000000000..0887542d784 --- /dev/null +++ b/homeassistant/components/ws66i/translations/cs.json @@ -0,0 +1,11 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/translations/cs.json b/homeassistant/components/xiaomi_ble/translations/cs.json new file mode 100644 index 00000000000..76d1e332913 --- /dev/null +++ b/homeassistant/components/xiaomi_ble/translations/cs.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", + "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Chcete nastavit {name}?" + }, + "user": { + "data": { + "address": "Za\u0159\u00edzen\u00ed" + }, + "description": "Zvolte za\u0159\u00edzen\u00ed, kter\u00e9 chcete nastavit" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yalexs_ble/translations/cs.json b/homeassistant/components/yalexs_ble/translations/cs.json new file mode 100644 index 00000000000..0bb0305f999 --- /dev/null +++ b/homeassistant/components/yalexs_ble/translations/cs.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", + "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yamaha_musiccast/translations/cs.json b/homeassistant/components/yamaha_musiccast/translations/cs.json new file mode 100644 index 00000000000..eef33c1db56 --- /dev/null +++ b/homeassistant/components/yamaha_musiccast/translations/cs.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "step": { + "user": { + "data": { + "host": "Hostitel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yolink/translations/cs.json b/homeassistant/components/yolink/translations/cs.json new file mode 100644 index 00000000000..0ae00fc3605 --- /dev/null +++ b/homeassistant/components/yolink/translations/cs.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + }, + "step": { + "reauth_confirm": { + "title": "Znovu ov\u011b\u0159it integraci" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/translations/cs.json b/homeassistant/components/zha/translations/cs.json index 5ce5ad2d090..9e48055c50d 100644 --- a/homeassistant/components/zha/translations/cs.json +++ b/homeassistant/components/zha/translations/cs.json @@ -9,12 +9,20 @@ }, "flow_title": "ZHA: {name}", "step": { + "confirm_hardware": { + "description": "Chcete nastavit {name}?" + }, "port_config": { "data": { "baudrate": "rychlost portu" }, "title": "Nastaven\u00ed" }, + "upload_manual_backup": { + "data": { + "uploaded_backup_file": "Nahr\u00e1t soubor" + } + }, "user": { "description": "Vyberte s\u00e9riov\u00fd port pro r\u00e1dio Zigbee", "title": "ZHA" @@ -71,6 +79,12 @@ } }, "options": { - "flow_title": "ZHA: {name}" + "flow_title": "ZHA: {name}", + "step": { + "init": { + "description": "Dopln\u011bk ZHA bude zastaven. P\u0159ejete si pokra\u010dovat?", + "title": "P\u0159ekonfigurovat ZHA" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/translations/el.json b/homeassistant/components/zha/translations/el.json index 6a0cd501197..621334e5492 100644 --- a/homeassistant/components/zha/translations/el.json +++ b/homeassistant/components/zha/translations/el.json @@ -176,6 +176,7 @@ "usb_probe_failed": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03b1\u03bd\u03af\u03c7\u03bd\u03b5\u03c5\u03c3\u03b7\u03c2 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 usb" }, "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", "invalid_backup_json": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03b1\u03bd\u03c4\u03af\u03b3\u03c1\u03b1\u03c6\u03bf \u03b1\u03c3\u03c6\u03b1\u03bb\u03b5\u03af\u03b1\u03c2 JSON" }, "flow_title": "{name}", diff --git a/homeassistant/components/zwave_js/translations/cs.json b/homeassistant/components/zwave_js/translations/cs.json index 6417d5a587f..76bd5a35e96 100644 --- a/homeassistant/components/zwave_js/translations/cs.json +++ b/homeassistant/components/zwave_js/translations/cs.json @@ -20,6 +20,9 @@ "data": { "url": "URL" } + }, + "zeroconf_confirm": { + "description": "Chcete p\u0159idat Z-Wawe JS Server s ID {home_id} nach\u00e1zej\u00edc\u00ed se na adrese {url} do Home Assistant?" } } }, @@ -39,7 +42,12 @@ } }, "options": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { From 831c87205f2a06e2c2bf7172d6dd5d6d4087575b Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Wed, 14 Sep 2022 02:00:59 +0100 Subject: [PATCH 376/955] Retry on unavailable IPMA api (#78332) Co-authored-by: J. Nick Koston --- homeassistant/components/ipma/__init__.py | 50 +++++++++++++++++++-- homeassistant/components/ipma/const.py | 3 ++ homeassistant/components/ipma/manifest.json | 2 +- homeassistant/components/ipma/weather.py | 33 ++------------ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/ipma/test_config_flow.py | 2 +- tests/components/ipma/test_weather.py | 13 +++--- 8 files changed, 65 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/ipma/__init__.py b/homeassistant/components/ipma/__init__.py index 4d675e8cc1d..315362247a2 100644 --- a/homeassistant/components/ipma/__init__.py +++ b/homeassistant/components/ipma/__init__.py @@ -1,19 +1,61 @@ """Component for the Portuguese weather service - IPMA.""" +import logging + +import async_timeout +from pyipma import IPMAException +from pyipma.api import IPMA_API +from pyipma.location import Location + from homeassistant.config_entries import ConfigEntry -from homeassistant.const import Platform +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, Platform from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession from .config_flow import IpmaFlowHandler # noqa: F401 -from .const import DOMAIN # noqa: F401 +from .const import DATA_API, DATA_LOCATION, DOMAIN DEFAULT_NAME = "ipma" PLATFORMS = [Platform.WEATHER] +_LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + +async def async_get_api(hass): + """Get the pyipma api object.""" + websession = async_get_clientsession(hass) + return IPMA_API(websession) + + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up IPMA station as config entry.""" - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + latitude = config_entry.data[CONF_LATITUDE] + longitude = config_entry.data[CONF_LONGITUDE] + + api = await async_get_api(hass) + try: + async with async_timeout.timeout(30): + location = await Location.get(api, float(latitude), float(longitude)) + + _LOGGER.debug( + "Initializing for coordinates %s, %s -> station %s (%d, %d)", + latitude, + longitude, + location.station, + location.id_station, + location.global_id_local, + ) + except IPMAException as err: + raise ConfigEntryNotReady( + f"Could not get location for ({latitude},{longitude})" + ) from err + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][config_entry.entry_id] = {DATA_API: api, DATA_LOCATION: location} + + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) return True diff --git a/homeassistant/components/ipma/const.py b/homeassistant/components/ipma/const.py index 47434d7f76b..60c8115a5c4 100644 --- a/homeassistant/components/ipma/const.py +++ b/homeassistant/components/ipma/const.py @@ -6,3 +6,6 @@ DOMAIN = "ipma" HOME_LOCATION_NAME = "Home" ENTITY_ID_SENSOR_FORMAT_HOME = f"{WEATHER_DOMAIN}.ipma_{HOME_LOCATION_NAME}" + +DATA_LOCATION = "location" +DATA_API = "api" diff --git a/homeassistant/components/ipma/manifest.json b/homeassistant/components/ipma/manifest.json index a391b24e3b4..23558600373 100644 --- a/homeassistant/components/ipma/manifest.json +++ b/homeassistant/components/ipma/manifest.json @@ -3,7 +3,7 @@ "name": "Instituto Portugu\u00eas do Mar e Atmosfera (IPMA)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ipma", - "requirements": ["pyipma==3.0.2"], + "requirements": ["pyipma==3.0.4"], "codeowners": ["@dgomes", "@abmantis"], "iot_class": "cloud_polling", "loggers": ["geopy", "pyipma"] diff --git a/homeassistant/components/ipma/weather.py b/homeassistant/components/ipma/weather.py index d20e5cb2f21..a0fe5b235b3 100644 --- a/homeassistant/components/ipma/weather.py +++ b/homeassistant/components/ipma/weather.py @@ -48,11 +48,12 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv, entity_registry -from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.sun import is_up from homeassistant.util import Throttle +from .const import DATA_API, DATA_LOCATION, DOMAIN + _LOGGER = logging.getLogger(__name__) ATTRIBUTION = "Instituto Português do Mar e Atmosfera" @@ -95,13 +96,10 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Add a weather entity from a config_entry.""" - latitude = config_entry.data[CONF_LATITUDE] - longitude = config_entry.data[CONF_LONGITUDE] + api = hass.data[DOMAIN][config_entry.entry_id][DATA_API] + location = hass.data[DOMAIN][config_entry.entry_id][DATA_LOCATION] mode = config_entry.data[CONF_MODE] - api = await async_get_api(hass) - location = await async_get_location(hass, api, latitude, longitude) - # Migrate old unique_id @callback def _async_migrator(entity_entry: entity_registry.RegistryEntry): @@ -127,29 +125,6 @@ async def async_setup_entry( async_add_entities([IPMAWeather(location, api, config_entry.data)], True) -async def async_get_api(hass): - """Get the pyipma api object.""" - websession = async_get_clientsession(hass) - return IPMA_API(websession) - - -async def async_get_location(hass, api, latitude, longitude): - """Retrieve pyipma location, location name to be used as the entity name.""" - async with async_timeout.timeout(30): - location = await Location.get(api, float(latitude), float(longitude)) - - _LOGGER.debug( - "Initializing for coordinates %s, %s -> station %s (%d, %d)", - latitude, - longitude, - location.station, - location.id_station, - location.global_id_local, - ) - - return location - - class IPMAWeather(WeatherEntity): """Representation of a weather condition.""" diff --git a/requirements_all.txt b/requirements_all.txt index eb745712dfa..9e242a7e4c2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1614,7 +1614,7 @@ pyinsteon==1.2.0 pyintesishome==1.8.0 # homeassistant.components.ipma -pyipma==3.0.2 +pyipma==3.0.4 # homeassistant.components.ipp pyipp==0.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3d9aa174186..ea6a7af0823 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1127,7 +1127,7 @@ pyicloud==1.0.0 pyinsteon==1.2.0 # homeassistant.components.ipma -pyipma==3.0.2 +pyipma==3.0.4 # homeassistant.components.ipp pyipp==0.11.0 diff --git a/tests/components/ipma/test_config_flow.py b/tests/components/ipma/test_config_flow.py index 9c1b50fee87..ea4b0b510e7 100644 --- a/tests/components/ipma/test_config_flow.py +++ b/tests/components/ipma/test_config_flow.py @@ -166,7 +166,7 @@ async def test_config_entry_migration(hass): ) with patch( - "homeassistant.components.ipma.weather.async_get_location", + "pyipma.location.Location.get", return_value=MockLocation(), ): assert await async_setup_component(hass, DOMAIN, {}) diff --git a/tests/components/ipma/test_weather.py b/tests/components/ipma/test_weather.py index 942b9654895..e129216730d 100644 --- a/tests/components/ipma/test_weather.py +++ b/tests/components/ipma/test_weather.py @@ -19,7 +19,6 @@ from homeassistant.components.weather import ( ATTR_WEATHER_TEMPERATURE, ATTR_WEATHER_WIND_BEARING, ATTR_WEATHER_WIND_SPEED, - DOMAIN as WEATHER_DOMAIN, ) from homeassistant.const import STATE_UNKNOWN @@ -181,7 +180,8 @@ async def test_setup_config_flow(hass): return_value=MockLocation(), ): entry = MockConfigEntry(domain="ipma", data=TEST_CONFIG) - await hass.config_entries.async_forward_entry_setup(entry, WEATHER_DOMAIN) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() state = hass.states.get("weather.hometown") @@ -203,7 +203,8 @@ async def test_daily_forecast(hass): return_value=MockLocation(), ): entry = MockConfigEntry(domain="ipma", data=TEST_CONFIG) - await hass.config_entries.async_forward_entry_setup(entry, WEATHER_DOMAIN) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() state = hass.states.get("weather.hometown") @@ -227,7 +228,8 @@ async def test_hourly_forecast(hass): return_value=MockLocation(), ): entry = MockConfigEntry(domain="ipma", data=TEST_CONFIG_HOURLY) - await hass.config_entries.async_forward_entry_setup(entry, WEATHER_DOMAIN) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() state = hass.states.get("weather.hometown") @@ -248,7 +250,8 @@ async def test_failed_get_observation_forecast(hass): return_value=MockBadLocation(), ): entry = MockConfigEntry(domain="ipma", data=TEST_CONFIG) - await hass.config_entries.async_forward_entry_setup(entry, WEATHER_DOMAIN) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() state = hass.states.get("weather.hometown") From 9d535b9ae9551e4fe9ee0e8b1bbfd0f209383906 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Wed, 14 Sep 2022 08:13:42 +0200 Subject: [PATCH 377/955] Bump fritzconnection to 1.10.3 (#77847) * Bump fritzconnection to 1.10.2 * bump to 1.10.3 --- homeassistant/components/fritz/manifest.json | 2 +- homeassistant/components/fritzbox_callmonitor/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/fritz/manifest.json b/homeassistant/components/fritz/manifest.json index b6aaf4d9896..d05668b9917 100644 --- a/homeassistant/components/fritz/manifest.json +++ b/homeassistant/components/fritz/manifest.json @@ -2,7 +2,7 @@ "domain": "fritz", "name": "AVM FRITZ!Box Tools", "documentation": "https://www.home-assistant.io/integrations/fritz", - "requirements": ["fritzconnection==1.10.1", "xmltodict==0.13.0"], + "requirements": ["fritzconnection==1.10.3", "xmltodict==0.13.0"], "dependencies": ["network"], "codeowners": ["@mammuth", "@AaronDavidSchneider", "@chemelli74", "@mib1185"], "config_flow": true, diff --git a/homeassistant/components/fritzbox_callmonitor/manifest.json b/homeassistant/components/fritzbox_callmonitor/manifest.json index 2d0fc96e0db..9dba2143ce1 100644 --- a/homeassistant/components/fritzbox_callmonitor/manifest.json +++ b/homeassistant/components/fritzbox_callmonitor/manifest.json @@ -3,7 +3,7 @@ "name": "AVM FRITZ!Box Call Monitor", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/fritzbox_callmonitor", - "requirements": ["fritzconnection==1.10.1"], + "requirements": ["fritzconnection==1.10.3"], "codeowners": ["@cdce8p"], "iot_class": "local_polling", "loggers": ["fritzconnection"] diff --git a/requirements_all.txt b/requirements_all.txt index 9e242a7e4c2..1e3ae64c3f3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -711,7 +711,7 @@ freesms==0.2.0 # homeassistant.components.fritz # homeassistant.components.fritzbox_callmonitor -fritzconnection==1.10.1 +fritzconnection==1.10.3 # homeassistant.components.google_translate gTTS==2.2.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ea6a7af0823..436e1d08120 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -524,7 +524,7 @@ freebox-api==0.0.10 # homeassistant.components.fritz # homeassistant.components.fritzbox_callmonitor -fritzconnection==1.10.1 +fritzconnection==1.10.3 # homeassistant.components.google_translate gTTS==2.2.4 From 9382f4be2388dc9c93eaff3876537cab0f3fcec3 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 14 Sep 2022 09:54:51 +0200 Subject: [PATCH 378/955] Update frontend to 20220907.2 (#78431) --- 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 42d7dd4b0fa..69fdc6c95a1 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==20220907.1"], + "requirements": ["home-assistant-frontend==20220907.2"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index ef5ff53c4a4..a86c2b07bf7 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -21,7 +21,7 @@ dbus-fast==1.4.0 fnvhash==0.1.0 hass-nabucasa==0.55.0 home-assistant-bluetooth==1.3.0 -home-assistant-frontend==20220907.1 +home-assistant-frontend==20220907.2 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index 1e3ae64c3f3..672f81ec72d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -857,7 +857,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220907.1 +home-assistant-frontend==20220907.2 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 436e1d08120..638d2fa5a56 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -634,7 +634,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220907.1 +home-assistant-frontend==20220907.2 # homeassistant.components.home_connect homeconnect==0.7.2 From 393f1487a5377c05b95bf6866566cd3111a32c0f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 14 Sep 2022 10:25:01 +0200 Subject: [PATCH 379/955] Remove Ambee integration (#78427) --- .strict-typing | 1 - CODEOWNERS | 2 - homeassistant/components/ambee/__init__.py | 92 ----- homeassistant/components/ambee/config_flow.py | 116 ------ homeassistant/components/ambee/const.py | 232 ----------- homeassistant/components/ambee/manifest.json | 10 - homeassistant/components/ambee/sensor.py | 77 ---- homeassistant/components/ambee/strings.json | 34 -- .../components/ambee/strings.sensor.json | 10 - .../components/ambee/translations/bg.json | 26 -- .../components/ambee/translations/ca.json | 34 -- .../components/ambee/translations/cs.json | 19 - .../components/ambee/translations/de.json | 34 -- .../components/ambee/translations/el.json | 34 -- .../components/ambee/translations/en.json | 34 -- .../components/ambee/translations/es-419.json | 14 - .../components/ambee/translations/es.json | 34 -- .../components/ambee/translations/et.json | 34 -- .../components/ambee/translations/fr.json | 33 -- .../components/ambee/translations/he.json | 26 -- .../components/ambee/translations/hu.json | 34 -- .../components/ambee/translations/id.json | 34 -- .../components/ambee/translations/it.json | 34 -- .../components/ambee/translations/ja.json | 34 -- .../components/ambee/translations/ko.json | 18 - .../components/ambee/translations/nl.json | 33 -- .../components/ambee/translations/no.json | 34 -- .../components/ambee/translations/pl.json | 34 -- .../components/ambee/translations/pt-BR.json | 34 -- .../components/ambee/translations/pt.json | 17 - .../components/ambee/translations/ru.json | 34 -- .../ambee/translations/sensor.bg.json | 10 - .../ambee/translations/sensor.ca.json | 10 - .../ambee/translations/sensor.de.json | 10 - .../ambee/translations/sensor.el.json | 10 - .../ambee/translations/sensor.en.json | 10 - .../ambee/translations/sensor.es-419.json | 10 - .../ambee/translations/sensor.es.json | 10 - .../ambee/translations/sensor.et.json | 10 - .../ambee/translations/sensor.fr.json | 10 - .../ambee/translations/sensor.he.json | 9 - .../ambee/translations/sensor.hu.json | 10 - .../ambee/translations/sensor.id.json | 10 - .../ambee/translations/sensor.it.json | 10 - .../ambee/translations/sensor.ja.json | 10 - .../ambee/translations/sensor.nl.json | 10 - .../ambee/translations/sensor.no.json | 10 - .../ambee/translations/sensor.pl.json | 10 - .../ambee/translations/sensor.pt-BR.json | 10 - .../ambee/translations/sensor.ru.json | 10 - .../ambee/translations/sensor.sv.json | 10 - .../ambee/translations/sensor.tr.json | 10 - .../ambee/translations/sensor.zh-Hant.json | 10 - .../components/ambee/translations/sk.json | 25 -- .../components/ambee/translations/sv.json | 34 -- .../components/ambee/translations/tr.json | 34 -- .../ambee/translations/zh-Hant.json | 34 -- homeassistant/generated/config_flows.py | 1 - mypy.ini | 10 - requirements_all.txt | 3 - requirements_test_all.txt | 3 - tests/components/ambee/__init__.py | 1 - tests/components/ambee/conftest.py | 53 --- .../ambee/fixtures/air_quality.json | 28 -- tests/components/ambee/fixtures/pollen.json | 43 --- tests/components/ambee/test_config_flow.py | 268 ------------- tests/components/ambee/test_init.py | 89 ----- tests/components/ambee/test_sensor.py | 360 ------------------ 68 files changed, 2441 deletions(-) delete mode 100644 homeassistant/components/ambee/__init__.py delete mode 100644 homeassistant/components/ambee/config_flow.py delete mode 100644 homeassistant/components/ambee/const.py delete mode 100644 homeassistant/components/ambee/manifest.json delete mode 100644 homeassistant/components/ambee/sensor.py delete mode 100644 homeassistant/components/ambee/strings.json delete mode 100644 homeassistant/components/ambee/strings.sensor.json delete mode 100644 homeassistant/components/ambee/translations/bg.json delete mode 100644 homeassistant/components/ambee/translations/ca.json delete mode 100644 homeassistant/components/ambee/translations/cs.json delete mode 100644 homeassistant/components/ambee/translations/de.json delete mode 100644 homeassistant/components/ambee/translations/el.json delete mode 100644 homeassistant/components/ambee/translations/en.json delete mode 100644 homeassistant/components/ambee/translations/es-419.json delete mode 100644 homeassistant/components/ambee/translations/es.json delete mode 100644 homeassistant/components/ambee/translations/et.json delete mode 100644 homeassistant/components/ambee/translations/fr.json delete mode 100644 homeassistant/components/ambee/translations/he.json delete mode 100644 homeassistant/components/ambee/translations/hu.json delete mode 100644 homeassistant/components/ambee/translations/id.json delete mode 100644 homeassistant/components/ambee/translations/it.json delete mode 100644 homeassistant/components/ambee/translations/ja.json delete mode 100644 homeassistant/components/ambee/translations/ko.json delete mode 100644 homeassistant/components/ambee/translations/nl.json delete mode 100644 homeassistant/components/ambee/translations/no.json delete mode 100644 homeassistant/components/ambee/translations/pl.json delete mode 100644 homeassistant/components/ambee/translations/pt-BR.json delete mode 100644 homeassistant/components/ambee/translations/pt.json delete mode 100644 homeassistant/components/ambee/translations/ru.json delete mode 100644 homeassistant/components/ambee/translations/sensor.bg.json delete mode 100644 homeassistant/components/ambee/translations/sensor.ca.json delete mode 100644 homeassistant/components/ambee/translations/sensor.de.json delete mode 100644 homeassistant/components/ambee/translations/sensor.el.json delete mode 100644 homeassistant/components/ambee/translations/sensor.en.json delete mode 100644 homeassistant/components/ambee/translations/sensor.es-419.json delete mode 100644 homeassistant/components/ambee/translations/sensor.es.json delete mode 100644 homeassistant/components/ambee/translations/sensor.et.json delete mode 100644 homeassistant/components/ambee/translations/sensor.fr.json delete mode 100644 homeassistant/components/ambee/translations/sensor.he.json delete mode 100644 homeassistant/components/ambee/translations/sensor.hu.json delete mode 100644 homeassistant/components/ambee/translations/sensor.id.json delete mode 100644 homeassistant/components/ambee/translations/sensor.it.json delete mode 100644 homeassistant/components/ambee/translations/sensor.ja.json delete mode 100644 homeassistant/components/ambee/translations/sensor.nl.json delete mode 100644 homeassistant/components/ambee/translations/sensor.no.json delete mode 100644 homeassistant/components/ambee/translations/sensor.pl.json delete mode 100644 homeassistant/components/ambee/translations/sensor.pt-BR.json delete mode 100644 homeassistant/components/ambee/translations/sensor.ru.json delete mode 100644 homeassistant/components/ambee/translations/sensor.sv.json delete mode 100644 homeassistant/components/ambee/translations/sensor.tr.json delete mode 100644 homeassistant/components/ambee/translations/sensor.zh-Hant.json delete mode 100644 homeassistant/components/ambee/translations/sk.json delete mode 100644 homeassistant/components/ambee/translations/sv.json delete mode 100644 homeassistant/components/ambee/translations/tr.json delete mode 100644 homeassistant/components/ambee/translations/zh-Hant.json delete mode 100644 tests/components/ambee/__init__.py delete mode 100644 tests/components/ambee/conftest.py delete mode 100644 tests/components/ambee/fixtures/air_quality.json delete mode 100644 tests/components/ambee/fixtures/pollen.json delete mode 100644 tests/components/ambee/test_config_flow.py delete mode 100644 tests/components/ambee/test_init.py delete mode 100644 tests/components/ambee/test_sensor.py diff --git a/.strict-typing b/.strict-typing index 76190758e5d..a565e04e3e5 100644 --- a/.strict-typing +++ b/.strict-typing @@ -52,7 +52,6 @@ homeassistant.components.airzone.* homeassistant.components.aladdin_connect.* homeassistant.components.alarm_control_panel.* homeassistant.components.amazon_polly.* -homeassistant.components.ambee.* homeassistant.components.ambient_station.* homeassistant.components.amcrest.* homeassistant.components.ampio.* diff --git a/CODEOWNERS b/CODEOWNERS index c8e4de87818..d5d3e597f70 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -61,8 +61,6 @@ build.json @home-assistant/supervisor /tests/components/alexa/ @home-assistant/cloud @ochlocracy /homeassistant/components/almond/ @gcampax @balloob /tests/components/almond/ @gcampax @balloob -/homeassistant/components/ambee/ @frenck -/tests/components/ambee/ @frenck /homeassistant/components/amberelectric/ @madpilot /tests/components/amberelectric/ @madpilot /homeassistant/components/ambiclimate/ @danielhiversen diff --git a/homeassistant/components/ambee/__init__.py b/homeassistant/components/ambee/__init__.py deleted file mode 100644 index 547b8720fef..00000000000 --- a/homeassistant/components/ambee/__init__.py +++ /dev/null @@ -1,92 +0,0 @@ -"""Support for Ambee.""" -from __future__ import annotations - -from ambee import AirQuality, Ambee, AmbeeAuthenticationError, Pollen - -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, Platform -from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryAuthFailed -from homeassistant.helpers.issue_registry import ( - IssueSeverity, - async_create_issue, - async_delete_issue, -) -from homeassistant.helpers.typing import ConfigType -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator - -from .const import DOMAIN, LOGGER, SCAN_INTERVAL, SERVICE_AIR_QUALITY, SERVICE_POLLEN - -PLATFORMS = [Platform.SENSOR] - - -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the Ambee integration.""" - async_create_issue( - hass, - DOMAIN, - "pending_removal", - breaks_in_ha_version="2022.10.0", - is_fixable=False, - severity=IssueSeverity.WARNING, - translation_key="pending_removal", - ) - return True - - -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Set up Ambee from a config entry.""" - hass.data.setdefault(DOMAIN, {}).setdefault(entry.entry_id, {}) - - client = Ambee( - api_key=entry.data[CONF_API_KEY], - latitude=entry.data[CONF_LATITUDE], - longitude=entry.data[CONF_LONGITUDE], - ) - - async def update_air_quality() -> AirQuality: - """Update method for updating Ambee Air Quality data.""" - try: - return await client.air_quality() - except AmbeeAuthenticationError as err: - raise ConfigEntryAuthFailed from err - - air_quality: DataUpdateCoordinator[AirQuality] = DataUpdateCoordinator( - hass, - LOGGER, - name=f"{DOMAIN}_{SERVICE_AIR_QUALITY}", - update_interval=SCAN_INTERVAL, - update_method=update_air_quality, - ) - await air_quality.async_config_entry_first_refresh() - hass.data[DOMAIN][entry.entry_id][SERVICE_AIR_QUALITY] = air_quality - - async def update_pollen() -> Pollen: - """Update method for updating Ambee Pollen data.""" - try: - return await client.pollen() - except AmbeeAuthenticationError as err: - raise ConfigEntryAuthFailed from err - - pollen: DataUpdateCoordinator[Pollen] = DataUpdateCoordinator( - hass, - LOGGER, - name=f"{DOMAIN}_{SERVICE_POLLEN}", - update_interval=SCAN_INTERVAL, - update_method=update_pollen, - ) - await pollen.async_config_entry_first_refresh() - hass.data[DOMAIN][entry.entry_id][SERVICE_POLLEN] = pollen - - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - return True - - -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Unload Ambee config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - if unload_ok: - del hass.data[DOMAIN][entry.entry_id] - if not hass.data[DOMAIN]: - async_delete_issue(hass, DOMAIN, "pending_removal") - return unload_ok diff --git a/homeassistant/components/ambee/config_flow.py b/homeassistant/components/ambee/config_flow.py deleted file mode 100644 index 7bfc1fa11af..00000000000 --- a/homeassistant/components/ambee/config_flow.py +++ /dev/null @@ -1,116 +0,0 @@ -"""Config flow to configure the Ambee integration.""" -from __future__ import annotations - -from collections.abc import Mapping -from typing import Any - -from ambee import Ambee, AmbeeAuthenticationError, AmbeeError -import voluptuous as vol - -from homeassistant.config_entries import ConfigEntry, ConfigFlow -from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME -from homeassistant.data_entry_flow import FlowResult -from homeassistant.helpers.aiohttp_client import async_get_clientsession -import homeassistant.helpers.config_validation as cv - -from .const import DOMAIN - - -class AmbeeFlowHandler(ConfigFlow, domain=DOMAIN): - """Config flow for Ambee.""" - - VERSION = 1 - - entry: ConfigEntry | None = None - - async def async_step_user( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Handle a flow initialized by the user.""" - errors = {} - - if user_input is not None: - session = async_get_clientsession(self.hass) - try: - client = Ambee( - api_key=user_input[CONF_API_KEY], - latitude=user_input[CONF_LATITUDE], - longitude=user_input[CONF_LONGITUDE], - session=session, - ) - await client.air_quality() - except AmbeeAuthenticationError: - errors["base"] = "invalid_api_key" - except AmbeeError: - errors["base"] = "cannot_connect" - else: - return self.async_create_entry( - title=user_input[CONF_NAME], - data={ - CONF_API_KEY: user_input[CONF_API_KEY], - CONF_LATITUDE: user_input[CONF_LATITUDE], - CONF_LONGITUDE: user_input[CONF_LONGITUDE], - }, - ) - - return self.async_show_form( - step_id="user", - data_schema=vol.Schema( - { - vol.Required(CONF_API_KEY): str, - vol.Optional( - CONF_NAME, default=self.hass.config.location_name - ): str, - vol.Optional( - CONF_LATITUDE, default=self.hass.config.latitude - ): cv.latitude, - vol.Optional( - CONF_LONGITUDE, default=self.hass.config.longitude - ): cv.longitude, - } - ), - errors=errors, - ) - - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: - """Handle initiation of re-authentication with Ambee.""" - self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) - return await self.async_step_reauth_confirm() - - async def async_step_reauth_confirm( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Handle re-authentication with Ambee.""" - errors = {} - if user_input is not None and self.entry: - session = async_get_clientsession(self.hass) - client = Ambee( - api_key=user_input[CONF_API_KEY], - latitude=self.entry.data[CONF_LATITUDE], - longitude=self.entry.data[CONF_LONGITUDE], - session=session, - ) - try: - await client.air_quality() - except AmbeeAuthenticationError: - errors["base"] = "invalid_api_key" - except AmbeeError: - errors["base"] = "cannot_connect" - else: - self.hass.config_entries.async_update_entry( - self.entry, - data={ - **self.entry.data, - CONF_API_KEY: user_input[CONF_API_KEY], - }, - ) - self.hass.async_create_task( - self.hass.config_entries.async_reload(self.entry.entry_id) - ) - return self.async_abort(reason="reauth_successful") - - return self.async_show_form( - step_id="reauth_confirm", - data_schema=vol.Schema({vol.Required(CONF_API_KEY): str}), - errors=errors, - ) diff --git a/homeassistant/components/ambee/const.py b/homeassistant/components/ambee/const.py deleted file mode 100644 index 83abb841629..00000000000 --- a/homeassistant/components/ambee/const.py +++ /dev/null @@ -1,232 +0,0 @@ -"""Constants for the Ambee integration.""" -from __future__ import annotations - -from datetime import timedelta -import logging -from typing import Final - -from homeassistant.components.sensor import ( - SensorDeviceClass, - SensorEntityDescription, - SensorStateClass, -) -from homeassistant.const import ( - CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - CONCENTRATION_PARTS_PER_BILLION, - CONCENTRATION_PARTS_PER_CUBIC_METER, - CONCENTRATION_PARTS_PER_MILLION, -) - -DOMAIN: Final = "ambee" -LOGGER = logging.getLogger(__package__) -SCAN_INTERVAL = timedelta(hours=1) - -DEVICE_CLASS_AMBEE_RISK: Final = "ambee__risk" - -SERVICE_AIR_QUALITY: Final = "air_quality" -SERVICE_POLLEN: Final = "pollen" - -SERVICES: dict[str, str] = { - SERVICE_AIR_QUALITY: "Air quality", - SERVICE_POLLEN: "Pollen", -} - -SENSORS: dict[str, list[SensorEntityDescription]] = { - SERVICE_AIR_QUALITY: [ - SensorEntityDescription( - key="particulate_matter_2_5", - name="Particulate matter < 2.5 μm", - native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key="particulate_matter_10", - name="Particulate matter < 10 μm", - native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key="sulphur_dioxide", - name="Sulphur dioxide (SO2)", - native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key="nitrogen_dioxide", - name="Nitrogen dioxide (NO2)", - native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key="ozone", - name="Ozone", - native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key="carbon_monoxide", - name="Carbon monoxide (CO)", - device_class=SensorDeviceClass.CO, - native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key="air_quality_index", - name="Air quality index (AQI)", - state_class=SensorStateClass.MEASUREMENT, - ), - ], - SERVICE_POLLEN: [ - SensorEntityDescription( - key="grass", - name="Grass", - icon="mdi:grass", - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, - ), - SensorEntityDescription( - key="tree", - name="Tree", - icon="mdi:tree", - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, - ), - SensorEntityDescription( - key="weed", - name="Weed", - icon="mdi:sprout", - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, - ), - SensorEntityDescription( - key="grass_risk", - name="Grass risk", - icon="mdi:grass", - device_class=DEVICE_CLASS_AMBEE_RISK, - ), - SensorEntityDescription( - key="tree_risk", - name="Tree risk", - icon="mdi:tree", - device_class=DEVICE_CLASS_AMBEE_RISK, - ), - SensorEntityDescription( - key="weed_risk", - name="Weed risk", - icon="mdi:sprout", - device_class=DEVICE_CLASS_AMBEE_RISK, - ), - SensorEntityDescription( - key="grass_poaceae", - name="Poaceae grass", - icon="mdi:grass", - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, - entity_registry_enabled_default=False, - ), - SensorEntityDescription( - key="tree_alder", - name="Alder tree", - icon="mdi:tree", - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, - entity_registry_enabled_default=False, - ), - SensorEntityDescription( - key="tree_birch", - name="Birch tree", - icon="mdi:tree", - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, - entity_registry_enabled_default=False, - ), - SensorEntityDescription( - key="tree_cypress", - name="Cypress tree", - icon="mdi:tree", - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, - entity_registry_enabled_default=False, - ), - SensorEntityDescription( - key="tree_elm", - name="Elm tree", - icon="mdi:tree", - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, - entity_registry_enabled_default=False, - ), - SensorEntityDescription( - key="tree_hazel", - name="Hazel tree", - icon="mdi:tree", - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, - entity_registry_enabled_default=False, - ), - SensorEntityDescription( - key="tree_oak", - name="Oak tree", - icon="mdi:tree", - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, - entity_registry_enabled_default=False, - ), - SensorEntityDescription( - key="tree_pine", - name="Pine tree", - icon="mdi:tree", - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, - entity_registry_enabled_default=False, - ), - SensorEntityDescription( - key="tree_plane", - name="Plane tree", - icon="mdi:tree", - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, - entity_registry_enabled_default=False, - ), - SensorEntityDescription( - key="tree_poplar", - name="Poplar tree", - icon="mdi:tree", - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, - entity_registry_enabled_default=False, - ), - SensorEntityDescription( - key="weed_chenopod", - name="Chenopod weed", - icon="mdi:sprout", - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, - entity_registry_enabled_default=False, - ), - SensorEntityDescription( - key="weed_mugwort", - name="Mugwort weed", - icon="mdi:sprout", - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, - entity_registry_enabled_default=False, - ), - SensorEntityDescription( - key="weed_nettle", - name="Nettle weed", - icon="mdi:sprout", - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, - entity_registry_enabled_default=False, - ), - SensorEntityDescription( - key="weed_ragweed", - name="Ragweed weed", - icon="mdi:sprout", - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, - entity_registry_enabled_default=False, - ), - ], -} diff --git a/homeassistant/components/ambee/manifest.json b/homeassistant/components/ambee/manifest.json deleted file mode 100644 index 3226e9de3a3..00000000000 --- a/homeassistant/components/ambee/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "domain": "ambee", - "name": "Ambee", - "config_flow": true, - "documentation": "https://www.home-assistant.io/integrations/ambee", - "requirements": ["ambee==0.4.0"], - "codeowners": ["@frenck"], - "quality_scale": "platinum", - "iot_class": "cloud_polling" -} diff --git a/homeassistant/components/ambee/sensor.py b/homeassistant/components/ambee/sensor.py deleted file mode 100644 index 8fb6c9f2a61..00000000000 --- a/homeassistant/components/ambee/sensor.py +++ /dev/null @@ -1,77 +0,0 @@ -"""Support for Ambee sensors.""" -from __future__ import annotations - -from homeassistant.components.sensor import ( - DOMAIN as SENSOR_DOMAIN, - SensorEntity, - SensorEntityDescription, -) -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import StateType -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, -) - -from .const import DOMAIN, SENSORS, SERVICES - - -async def async_setup_entry( - hass: HomeAssistant, - entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up Ambee sensors based on a config entry.""" - async_add_entities( - AmbeeSensorEntity( - coordinator=hass.data[DOMAIN][entry.entry_id][service_key], - entry_id=entry.entry_id, - description=description, - service_key=service_key, - service=SERVICES[service_key], - ) - for service_key, service_sensors in SENSORS.items() - for description in service_sensors - ) - - -class AmbeeSensorEntity(CoordinatorEntity, SensorEntity): - """Defines an Ambee sensor.""" - - _attr_has_entity_name = True - - def __init__( - self, - *, - coordinator: DataUpdateCoordinator, - entry_id: str, - description: SensorEntityDescription, - service_key: str, - service: str, - ) -> None: - """Initialize Ambee sensor.""" - super().__init__(coordinator=coordinator) - self._service_key = service_key - - self.entity_id = f"{SENSOR_DOMAIN}.{service_key}_{description.key}" - self.entity_description = description - self._attr_unique_id = f"{entry_id}_{service_key}_{description.key}" - - self._attr_device_info = DeviceInfo( - entry_type=DeviceEntryType.SERVICE, - identifiers={(DOMAIN, f"{entry_id}_{service_key}")}, - manufacturer="Ambee", - name=service, - ) - - @property - def native_value(self) -> StateType: - """Return the state of the sensor.""" - value = getattr(self.coordinator.data, self.entity_description.key) - if isinstance(value, str): - return value.lower() - return value # type: ignore[no-any-return] diff --git a/homeassistant/components/ambee/strings.json b/homeassistant/components/ambee/strings.json deleted file mode 100644 index 7d0e75877c9..00000000000 --- a/homeassistant/components/ambee/strings.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "config": { - "step": { - "user": { - "description": "Set up Ambee to integrate with Home Assistant.", - "data": { - "api_key": "[%key:common::config_flow::data::api_key%]", - "latitude": "[%key:common::config_flow::data::latitude%]", - "longitude": "[%key:common::config_flow::data::longitude%]", - "name": "[%key:common::config_flow::data::name%]" - } - }, - "reauth_confirm": { - "data": { - "description": "Re-authenticate with your Ambee account.", - "api_key": "[%key:common::config_flow::data::api_key%]" - } - } - }, - "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]" - }, - "abort": { - "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" - } - }, - "issues": { - "pending_removal": { - "title": "The Ambee integration is being removed", - "description": "The Ambee integration is pending removal from Home Assistant and will no longer be available as of Home Assistant 2022.10.\n\nThe integration is being removed, because Ambee removed their free (limited) accounts and doesn't provide a way for regular users to sign up for a paid plan anymore.\n\nRemove the Ambee integration entry from your instance to fix this issue." - } - } -} diff --git a/homeassistant/components/ambee/strings.sensor.json b/homeassistant/components/ambee/strings.sensor.json deleted file mode 100644 index 83eb3b3fd73..00000000000 --- a/homeassistant/components/ambee/strings.sensor.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "state": { - "ambee__risk": { - "low": "Low", - "moderate": "Moderate", - "high": "High", - "very high": "Very High" - } - } -} diff --git a/homeassistant/components/ambee/translations/bg.json b/homeassistant/components/ambee/translations/bg.json deleted file mode 100644 index c72dc5227ca..00000000000 --- a/homeassistant/components/ambee/translations/bg.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "config": { - "abort": { - "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\u0445 \u043f\u0440\u0438 \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": { - "reauth_confirm": { - "data": { - "api_key": "API \u043a\u043b\u044e\u0447" - } - }, - "user": { - "data": { - "api_key": "API \u043a\u043b\u044e\u0447", - "latitude": "\u0413\u0435\u043e\u0433\u0440\u0430\u0444\u0441\u043a\u0430 \u0448\u0438\u0440\u0438\u043d\u0430", - "longitude": "\u0413\u0435\u043e\u0433\u0440\u0430\u0444\u0441\u043a\u0430 \u0434\u044a\u043b\u0436\u0438\u043d\u0430", - "name": "\u0418\u043c\u0435" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/ca.json b/homeassistant/components/ambee/translations/ca.json deleted file mode 100644 index bb4d49642b5..00000000000 --- a/homeassistant/components/ambee/translations/ca.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "config": { - "abort": { - "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" - }, - "error": { - "cannot_connect": "Ha fallat la connexi\u00f3", - "invalid_api_key": "Clau API inv\u00e0lida" - }, - "step": { - "reauth_confirm": { - "data": { - "api_key": "Clau API", - "description": "Torna a autenticar-te amb el compte d'Ambee." - } - }, - "user": { - "data": { - "api_key": "Clau API", - "latitude": "Latitud", - "longitude": "Longitud", - "name": "Nom" - }, - "description": "Configura la integraci\u00f3 d'Ambee amb Home Assistant." - } - } - }, - "issues": { - "pending_removal": { - "description": "La integraci\u00f3 d'Ambee s'eliminar\u00e0 de Home Assistant i deixar\u00e0 d'estar disponible a la versi\u00f3 de Home Assistant 2022.10.\n\nLa integraci\u00f3 s'eliminar\u00e0 perqu\u00e8 Ambee ha eliminat els seus comptes gratu\u00efts (limitats) i no ha donat cap manera per als usuaris normals de registrar-se a un pla de pagament.\n\nElimina la integraci\u00f3 d'Ambee del Home Assistant per solucionar aquest problema.", - "title": "La integraci\u00f3 Ambee est\u00e0 sent eliminada" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/cs.json b/homeassistant/components/ambee/translations/cs.json deleted file mode 100644 index 88a6b354852..00000000000 --- a/homeassistant/components/ambee/translations/cs.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "abort": { - "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" - }, - "error": { - "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", - "invalid_api_key": "Neplatn\u00fd kl\u00ed\u010d API" - }, - "step": { - "user": { - "data": { - "longitude": "Zem\u011bpisn\u00e1 d\u00e9lka", - "name": "Jm\u00e9no" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/de.json b/homeassistant/components/ambee/translations/de.json deleted file mode 100644 index 8055ef5210f..00000000000 --- a/homeassistant/components/ambee/translations/de.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "config": { - "abort": { - "reauth_successful": "Die erneute Authentifizierung war erfolgreich" - }, - "error": { - "cannot_connect": "Verbindung fehlgeschlagen", - "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel" - }, - "step": { - "reauth_confirm": { - "data": { - "api_key": "API-Schl\u00fcssel", - "description": "Authentifiziere dich erneut mit deinem Ambee-Konto." - } - }, - "user": { - "data": { - "api_key": "API-Schl\u00fcssel", - "latitude": "Breitengrad", - "longitude": "L\u00e4ngengrad", - "name": "Name" - }, - "description": "Richte Ambee f\u00fcr die Integration mit Home Assistant ein." - } - } - }, - "issues": { - "pending_removal": { - "description": "Die Ambee-Integration ist dabei, aus Home Assistant entfernt zu werden und wird ab Home Assistant 2022.10 nicht mehr verf\u00fcgbar sein.\n\nDie Integration wird entfernt, weil Ambee seine kostenlosen (begrenzten) Konten entfernt hat und keine M\u00f6glichkeit mehr f\u00fcr regul\u00e4re Nutzer bietet, sich f\u00fcr einen kostenpflichtigen Plan anzumelden.\n\nEntferne den Ambee-Integrationseintrag aus deiner Instanz, um dieses Problem zu beheben.", - "title": "Die Ambee-Integration wird entfernt" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/el.json b/homeassistant/components/ambee/translations/el.json deleted file mode 100644 index 99198a39817..00000000000 --- a/homeassistant/components/ambee/translations/el.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "config": { - "abort": { - "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2" - }, - "error": { - "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", - "invalid_api_key": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API" - }, - "step": { - "reauth_confirm": { - "data": { - "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", - "description": "\u0395\u03c0\u03b1\u03bd\u03b1\u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc \u03c3\u03b1\u03c2 Ambee." - } - }, - "user": { - "data": { - "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", - "latitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03c0\u03bb\u03ac\u03c4\u03bf\u03c2", - "longitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03bc\u03ae\u03ba\u03bf\u03c2", - "name": "\u038c\u03bd\u03bf\u03bc\u03b1" - }, - "description": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03bf Ambee \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b5\u03bd\u03c3\u03c9\u03bc\u03b1\u03c4\u03c9\u03b8\u03b5\u03af \u03bc\u03b5 \u03c4\u03bf Home Assistant." - } - } - }, - "issues": { - "pending_removal": { - "description": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 Ambee \u03b5\u03ba\u03ba\u03c1\u03b5\u03bc\u03b5\u03af \u03ba\u03b1\u03c4\u03ac\u03c1\u03b3\u03b7\u03c3\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf Home Assistant \u03ba\u03b1\u03b9 \u03b4\u03b5\u03bd \u03b8\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf Home Assistant 2022.10. \n\n \u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9, \u03b5\u03c0\u03b5\u03b9\u03b4\u03ae \u03b7 Ambee \u03b1\u03c6\u03b1\u03af\u03c1\u03b5\u03c3\u03b5 \u03c4\u03bf\u03c5\u03c2 \u03b4\u03c9\u03c1\u03b5\u03ac\u03bd (\u03c0\u03b5\u03c1\u03b9\u03bf\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf\u03c5\u03c2) \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd\u03c2 \u03c4\u03bf\u03c5\u03c2 \u03ba\u03b1\u03b9 \u03b4\u03b5\u03bd \u03c0\u03b1\u03c1\u03ad\u03c7\u03b5\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd \u03c4\u03c1\u03cc\u03c0\u03bf \u03c3\u03c4\u03bf\u03c5\u03c2 \u03c4\u03b1\u03ba\u03c4\u03b9\u03ba\u03bf\u03cd\u03c2 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b5\u03c2 \u03bd\u03b1 \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03bf\u03cd\u03bd \u03c3\u03b5 \u03ad\u03bd\u03b1 \u03c0\u03c1\u03cc\u03b3\u03c1\u03b1\u03bc\u03bc\u03b1 \u03b5\u03c0\u03af \u03c0\u03bb\u03b7\u03c1\u03c9\u03bc\u03ae. \n\n \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03ba\u03b1\u03c4\u03b1\u03c7\u03ce\u03c1\u03b7\u03c3\u03b7 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2 Ambee \u03b1\u03c0\u03cc \u03c4\u03b7\u03bd \u03c0\u03b1\u03c1\u03bf\u03c5\u03c3\u03af\u03b1 \u03c3\u03b1\u03c2 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03b6\u03ae\u03c4\u03b7\u03bc\u03b1.", - "title": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 Ambee \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/en.json b/homeassistant/components/ambee/translations/en.json deleted file mode 100644 index 03f4c3241b6..00000000000 --- a/homeassistant/components/ambee/translations/en.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "config": { - "abort": { - "reauth_successful": "Re-authentication was successful" - }, - "error": { - "cannot_connect": "Failed to connect", - "invalid_api_key": "Invalid API key" - }, - "step": { - "reauth_confirm": { - "data": { - "api_key": "API Key", - "description": "Re-authenticate with your Ambee account." - } - }, - "user": { - "data": { - "api_key": "API Key", - "latitude": "Latitude", - "longitude": "Longitude", - "name": "Name" - }, - "description": "Set up Ambee to integrate with Home Assistant." - } - } - }, - "issues": { - "pending_removal": { - "description": "The Ambee integration is pending removal from Home Assistant and will no longer be available as of Home Assistant 2022.10.\n\nThe integration is being removed, because Ambee removed their free (limited) accounts and doesn't provide a way for regular users to sign up for a paid plan anymore.\n\nRemove the Ambee integration entry from your instance to fix this issue.", - "title": "The Ambee integration is being removed" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/es-419.json b/homeassistant/components/ambee/translations/es-419.json deleted file mode 100644 index de5ce971fa0..00000000000 --- a/homeassistant/components/ambee/translations/es-419.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "config": { - "step": { - "reauth_confirm": { - "data": { - "description": "Vuelva a autenticarse con su cuenta de Ambee." - } - }, - "user": { - "description": "Configure Ambee para que se integre con Home Assistant." - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/es.json b/homeassistant/components/ambee/translations/es.json deleted file mode 100644 index 205b9adaf3a..00000000000 --- a/homeassistant/components/ambee/translations/es.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "config": { - "abort": { - "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" - }, - "error": { - "cannot_connect": "No se pudo conectar", - "invalid_api_key": "Clave API no v\u00e1lida" - }, - "step": { - "reauth_confirm": { - "data": { - "api_key": "Clave API", - "description": "Vuelve a autenticarte con tu cuenta Ambee." - } - }, - "user": { - "data": { - "api_key": "Clave API", - "latitude": "Latitud", - "longitude": "Longitud", - "name": "Nombre" - }, - "description": "Configura Ambee para que se integre con Home Assistant." - } - } - }, - "issues": { - "pending_removal": { - "description": "La integraci\u00f3n Ambee est\u00e1 pendiente de eliminaci\u00f3n de Home Assistant y ya no estar\u00e1 disponible a partir de Home Assistant 2022.10. \n\nSe va a eliminar la integraci\u00f3n porque Ambee elimin\u00f3 sus cuentas gratuitas (limitadas) y ya no proporciona una forma para que los usuarios regulares se registren en un plan pago. \n\nElimina la entrada de la integraci\u00f3n Ambee de tu instancia para solucionar este problema.", - "title": "Se va a eliminar la integraci\u00f3n Ambee" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/et.json b/homeassistant/components/ambee/translations/et.json deleted file mode 100644 index abb41497581..00000000000 --- a/homeassistant/components/ambee/translations/et.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "config": { - "abort": { - "reauth_successful": "Taastuvastamine \u00f5nnestus" - }, - "error": { - "cannot_connect": "\u00dchendumine nurjus", - "invalid_api_key": "Vale API v\u00f5ti" - }, - "step": { - "reauth_confirm": { - "data": { - "api_key": "API v\u00f5ti", - "description": "Taastuvasta Ambee konto" - } - }, - "user": { - "data": { - "api_key": "API v\u00f5ti", - "latitude": "Laiuskraad", - "longitude": "Pikkuskraad", - "name": "Nimi" - }, - "description": "Seadista Ambee sidumine Home Assistantiga." - } - } - }, - "issues": { - "pending_removal": { - "description": "Ambee integratsioon on Home Assistantist eemaldamisel ja ei ole enam saadaval alates Home Assistant 2022.10.\n\nIntegratsioon eemaldatakse, sest Ambee eemaldas oma tasuta (piiratud) kontod ja ei paku tavakasutajatele enam v\u00f5imalust tasulisele plaanile registreeruda.\n\nSelle probleemi lahendamiseks eemaldage Ambee integratsiooni kirje oma instantsist.", - "title": "Ambee integratsioon eemaldatakse" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/fr.json b/homeassistant/components/ambee/translations/fr.json deleted file mode 100644 index da3932962a6..00000000000 --- a/homeassistant/components/ambee/translations/fr.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "config": { - "abort": { - "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" - }, - "error": { - "cannot_connect": "\u00c9chec de connexion", - "invalid_api_key": "Cl\u00e9 d'API non valide" - }, - "step": { - "reauth_confirm": { - "data": { - "api_key": "Cl\u00e9 d'API", - "description": "R\u00e9-authentifiez-vous avec votre compte Ambee." - } - }, - "user": { - "data": { - "api_key": "Cl\u00e9 d'API", - "latitude": "Latitude", - "longitude": "Longitude", - "name": "Nom" - }, - "description": "Configurer Ambee pour l'int\u00e9grer \u00e0 Home Assistant." - } - } - }, - "issues": { - "pending_removal": { - "title": "L'int\u00e9gration Ambee est en cours de suppression" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/he.json b/homeassistant/components/ambee/translations/he.json deleted file mode 100644 index 7b7882cd4df..00000000000 --- a/homeassistant/components/ambee/translations/he.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "config": { - "abort": { - "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" - }, - "error": { - "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", - "invalid_api_key": "\u05de\u05e4\u05ea\u05d7 API \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" - }, - "step": { - "reauth_confirm": { - "data": { - "api_key": "\u05de\u05e4\u05ea\u05d7 API" - } - }, - "user": { - "data": { - "api_key": "\u05de\u05e4\u05ea\u05d7 API", - "latitude": "\u05e7\u05d5 \u05e8\u05d5\u05d7\u05d1", - "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da", - "name": "\u05e9\u05dd" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/hu.json b/homeassistant/components/ambee/translations/hu.json deleted file mode 100644 index 98e9fbdabea..00000000000 --- a/homeassistant/components/ambee/translations/hu.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "config": { - "abort": { - "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." - }, - "error": { - "cannot_connect": "Sikertelen csatlakoz\u00e1s", - "invalid_api_key": "\u00c9rv\u00e9nytelen API kulcs" - }, - "step": { - "reauth_confirm": { - "data": { - "api_key": "API kulcs", - "description": "Hiteles\u00edtse mag\u00e1t \u00fajra az Ambee-fi\u00f3kj\u00e1val." - } - }, - "user": { - "data": { - "api_key": "API kulcs", - "latitude": "Sz\u00e9less\u00e9g", - "longitude": "Hossz\u00fas\u00e1g", - "name": "Elnevez\u00e9s" - }, - "description": "Integr\u00e1lja \u00f6ssze Ambeet Home Assistanttal." - } - } - }, - "issues": { - "pending_removal": { - "description": "Az Ambee integr\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra v\u00e1r a Home Assistantb\u00f3l, \u00e9s a 2022.10-es Home Assistant-t\u00f3l m\u00e1r nem lesz el\u00e9rhet\u0151.\n\nAz integr\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sa az\u00e9rt t\u00f6rt\u00e9nik, mert az Ambee elt\u00e1vol\u00edtotta az ingyenes (korl\u00e1tozott) fi\u00f3kjait, \u00e9s a rendszeres felhaszn\u00e1l\u00f3k sz\u00e1m\u00e1ra m\u00e1r nem biztos\u00edt lehet\u0151s\u00e9get arra, hogy fizet\u0151s csomagra regisztr\u00e1ljanak.\n\nA hiba\u00fczenet elrejt\u00e9s\u00e9hez t\u00e1vol\u00edtsa el az Ambee integr\u00e1ci\u00f3s bejegyz\u00e9st a rendszerb\u0151l.", - "title": "Az Ambee integr\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/id.json b/homeassistant/components/ambee/translations/id.json deleted file mode 100644 index 686e36fd17b..00000000000 --- a/homeassistant/components/ambee/translations/id.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "config": { - "abort": { - "reauth_successful": "Autentikasi ulang berhasil" - }, - "error": { - "cannot_connect": "Gagal terhubung", - "invalid_api_key": "Kunci API tidak valid" - }, - "step": { - "reauth_confirm": { - "data": { - "api_key": "Kunci API", - "description": "Autentikasi ulang dengan akun Ambee Anda." - } - }, - "user": { - "data": { - "api_key": "Kunci API", - "latitude": "Lintang", - "longitude": "Bujur", - "name": "Nama" - }, - "description": "Siapkan Ambee Anda untuk diintegrasikan dengan Home Assistant." - } - } - }, - "issues": { - "pending_removal": { - "description": "Integrasi Ambee sedang menunggu penghapusan dari Home Assistant dan tidak akan lagi tersedia pada Home Assistant 2022.10.\n\nIntegrasi ini dalam proses penghapusan, karena Ambee telah menghapus akun versi gratis (terbatas) mereka dan tidak menyediakan cara bagi pengguna biasa untuk mendaftar paket berbayar lagi.\n\nHapus entri integrasi Ambee dari instans Anda untuk memperbaiki masalah ini.", - "title": "Integrasi Ambee dalam proses penghapusan" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/it.json b/homeassistant/components/ambee/translations/it.json deleted file mode 100644 index f2054c8a6ff..00000000000 --- a/homeassistant/components/ambee/translations/it.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "config": { - "abort": { - "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" - }, - "error": { - "cannot_connect": "Impossibile connettersi", - "invalid_api_key": "Chiave API non valida" - }, - "step": { - "reauth_confirm": { - "data": { - "api_key": "Chiave API", - "description": "Autenticati nuovamente con il tuo account Ambee." - } - }, - "user": { - "data": { - "api_key": "Chiave API", - "latitude": "Latitudine", - "longitude": "Logitudine", - "name": "Nome" - }, - "description": "Configura Ambee per l'integrazione con Home Assistant." - } - } - }, - "issues": { - "pending_removal": { - "description": "L'integrazione Ambee \u00e8 in attesa di rimozione da Home Assistant e non sar\u00e0 pi\u00f9 disponibile a partire da Home Assistant 2022.10. \n\nL'integrazione \u00e8 stata rimossa, perch\u00e9 Ambee ha rimosso i loro account gratuiti (limitati) e non offre pi\u00f9 agli utenti regolari un modo per iscriversi a un piano a pagamento. \n\nRimuovi la voce di integrazione Ambee dalla tua istanza per risolvere questo problema.", - "title": "L'integrazione Ambee sar\u00e0 rimossa" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/ja.json b/homeassistant/components/ambee/translations/ja.json deleted file mode 100644 index 2d6bf3b2466..00000000000 --- a/homeassistant/components/ambee/translations/ja.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "config": { - "abort": { - "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" - }, - "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", - "invalid_api_key": "\u7121\u52b9\u306aAPI\u30ad\u30fc" - }, - "step": { - "reauth_confirm": { - "data": { - "api_key": "API\u30ad\u30fc", - "description": "Ambee\u30a2\u30ab\u30a6\u30f3\u30c8\u3067\u518d\u8a8d\u8a3c\u3057\u307e\u3059\u3002" - } - }, - "user": { - "data": { - "api_key": "API\u30ad\u30fc", - "latitude": "\u7def\u5ea6", - "longitude": "\u7d4c\u5ea6", - "name": "\u540d\u524d" - }, - "description": "Ambee \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u3066\u3001Home Assistant\u3068\u9023\u643a\u3059\u308b\u3088\u3046\u306b\u3057\u307e\u3059\u3002" - } - } - }, - "issues": { - "pending_removal": { - "description": "Ambee\u306e\u7d71\u5408\u306fHome Assistant\u304b\u3089\u306e\u524a\u9664\u306f\u4fdd\u7559\u3055\u308c\u3066\u3044\u307e\u3059\u304c\u3001Home Assistant 2022.10\u4ee5\u964d\u306f\u5229\u7528\u3067\u304d\u306a\u304f\u306a\u308a\u307e\u3059\u3002 \n\nAmbee\u304c\u7121\u6599(\u9650\u5b9a)\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u524a\u9664\u3057\u3001\u4e00\u822c\u30e6\u30fc\u30b6\u30fc\u304c\u6709\u6599\u30d7\u30e9\u30f3\u306b\u30b5\u30a4\u30f3\u30a2\u30c3\u30d7\u3059\u308b\u65b9\u6cd5\u3092\u63d0\u4f9b\u3057\u306a\u304f\u306a\u3063\u305f\u305f\u3081\u3001\u7d71\u5408\u304c\u524a\u9664\u3055\u308c\u308b\u3053\u3068\u306b\u306a\u308a\u307e\u3057\u305f\u3002\n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u304b\u3089Ambee\u306e\u7d71\u5408\u306e\u30a8\u30f3\u30c8\u30ea\u3092\u524a\u9664\u3057\u3066\u304f\u3060\u3055\u3044\u3002", - "title": "Ambee\u306e\u7d71\u5408\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/ko.json b/homeassistant/components/ambee/translations/ko.json deleted file mode 100644 index 574b7cd0976..00000000000 --- a/homeassistant/components/ambee/translations/ko.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "config": { - "error": { - "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", - "invalid_api_key": "API \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" - }, - "step": { - "user": { - "data": { - "api_key": "API \ud0a4", - "latitude": "\uc704\ub3c4", - "longitude": "\uacbd\ub3c4", - "name": "\uc774\ub984" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/nl.json b/homeassistant/components/ambee/translations/nl.json deleted file mode 100644 index 957c3547be2..00000000000 --- a/homeassistant/components/ambee/translations/nl.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "config": { - "abort": { - "reauth_successful": "Herauthenticatie geslaagd" - }, - "error": { - "cannot_connect": "Kan geen verbinding maken", - "invalid_api_key": "Ongeldige API-sleutel" - }, - "step": { - "reauth_confirm": { - "data": { - "api_key": "API-sleutel", - "description": "Verifieer opnieuw met uw Ambee-account." - } - }, - "user": { - "data": { - "api_key": "API-sleutel", - "latitude": "Breedtegraad", - "longitude": "Lengtegraad", - "name": "Naam" - }, - "description": "Stel Ambee in om te integreren met Home Assistant." - } - } - }, - "issues": { - "pending_removal": { - "title": "De Ambee-integratie wordt verwijderd" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/no.json b/homeassistant/components/ambee/translations/no.json deleted file mode 100644 index 2c10f596722..00000000000 --- a/homeassistant/components/ambee/translations/no.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "config": { - "abort": { - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" - }, - "error": { - "cannot_connect": "Tilkobling mislyktes", - "invalid_api_key": "Ugyldig API-n\u00f8kkel" - }, - "step": { - "reauth_confirm": { - "data": { - "api_key": "API-n\u00f8kkel", - "description": "Autentiser p\u00e5 nytt med Ambee-kontoen din." - } - }, - "user": { - "data": { - "api_key": "API-n\u00f8kkel", - "latitude": "Breddegrad", - "longitude": "Lengdegrad", - "name": "Navn" - }, - "description": "Sett opp Ambee for \u00e5 integrere med Home Assistant." - } - } - }, - "issues": { - "pending_removal": { - "description": "Ambee-integrasjonen venter p\u00e5 fjerning fra Home Assistant og vil ikke lenger v\u00e6re tilgjengelig fra Home Assistant 2022.10. \n\n Integrasjonen blir fjernet, fordi Ambee fjernet deres gratis (begrensede) kontoer og ikke gir vanlige brukere mulighet til \u00e5 registrere seg for en betalt plan lenger. \n\n Fjern Ambee-integrasjonsoppf\u00f8ringen fra forekomsten din for \u00e5 fikse dette problemet.", - "title": "Ambee-integrasjonen blir fjernet" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/pl.json b/homeassistant/components/ambee/translations/pl.json deleted file mode 100644 index 255d402175d..00000000000 --- a/homeassistant/components/ambee/translations/pl.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "config": { - "abort": { - "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" - }, - "error": { - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", - "invalid_api_key": "Nieprawid\u0142owy klucz API" - }, - "step": { - "reauth_confirm": { - "data": { - "api_key": "Klucz API", - "description": "Ponownie uwierzytelnij za pomoc\u0105 konta Ambee." - } - }, - "user": { - "data": { - "api_key": "Klucz API", - "latitude": "Szeroko\u015b\u0107 geograficzna", - "longitude": "D\u0142ugo\u015b\u0107 geograficzna", - "name": "Nazwa" - }, - "description": "Skonfiguruj Ambee, aby zintegrowa\u0107 go z Home Assistantem." - } - } - }, - "issues": { - "pending_removal": { - "description": "Integracja Ambee oczekuje na usuni\u0119cie z Home Assistanta i nie b\u0119dzie ju\u017c dost\u0119pna od Home Assistant 2022.10. \n\nIntegracja jest usuwana, poniewa\u017c Ambee usun\u0105\u0142 ich bezp\u0142atne (ograniczone) konta i nie zapewnia ju\u017c zwyk\u0142ym u\u017cytkownikom mo\u017cliwo\u015bci zarejestrowania si\u0119 w p\u0142atnym planie. \n\nUsu\u0144 integracj\u0119 Ambee z Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", - "title": "Integracja Ambee zostanie usuni\u0119ta" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/pt-BR.json b/homeassistant/components/ambee/translations/pt-BR.json deleted file mode 100644 index 3220de5104e..00000000000 --- a/homeassistant/components/ambee/translations/pt-BR.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "config": { - "abort": { - "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" - }, - "error": { - "cannot_connect": "Falha ao conectar", - "invalid_api_key": "Chave de API inv\u00e1lida" - }, - "step": { - "reauth_confirm": { - "data": { - "api_key": "Chave da API", - "description": "Re-autentique com sua conta Ambee." - } - }, - "user": { - "data": { - "api_key": "Chave da API", - "latitude": "Latitude", - "longitude": "Longitude", - "name": "Nome" - }, - "description": "Configure o Ambee para integrar com o Home Assistant." - } - } - }, - "issues": { - "pending_removal": { - "description": "A integra\u00e7\u00e3o do Ambee est\u00e1 com remo\u00e7\u00e3o pendente do Home Assistant e n\u00e3o estar\u00e1 mais dispon\u00edvel a partir do Home Assistant 2022.10. \n\n A integra\u00e7\u00e3o est\u00e1 sendo removida, porque a Ambee removeu suas contas gratuitas (limitadas) e n\u00e3o oferece mais uma maneira de usu\u00e1rios regulares se inscreverem em um plano pago. \n\n Remova a entrada de integra\u00e7\u00e3o Ambee de sua inst\u00e2ncia para corrigir esse problema.", - "title": "A integra\u00e7\u00e3o Ambee est\u00e1 sendo removida" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/pt.json b/homeassistant/components/ambee/translations/pt.json deleted file mode 100644 index 4a6d267473b..00000000000 --- a/homeassistant/components/ambee/translations/pt.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "config": { - "step": { - "reauth_confirm": { - "data": { - "api_key": "Chave da API" - } - }, - "user": { - "data": { - "latitude": "Latitude", - "name": "Nome" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/ru.json b/homeassistant/components/ambee/translations/ru.json deleted file mode 100644 index 11b3cbbf9d2..00000000000 --- a/homeassistant/components/ambee/translations/ru.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "config": { - "abort": { - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." - }, - "error": { - "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API." - }, - "step": { - "reauth_confirm": { - "data": { - "api_key": "\u041a\u043b\u044e\u0447 API", - "description": "\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Ambee" - } - }, - "user": { - "data": { - "api_key": "\u041a\u043b\u044e\u0447 API", - "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", - "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430", - "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" - }, - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Ambee." - } - } - }, - "issues": { - "pending_removal": { - "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f Ambee \u043e\u0436\u0438\u0434\u0430\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u044f \u0438\u0437 Home Assistant \u0438 \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0431\u0443\u0434\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u0441 Home Assistant \u0432\u0435\u0440\u0441\u0438\u0438 2022.10. \n\n\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430, \u043f\u043e\u0442\u043e\u043c\u0443 \u0447\u0442\u043e Ambee \u0443\u0434\u0430\u043b\u0438\u043b\u0430 \u0441\u0432\u043e\u0438 \u0431\u0435\u0441\u043f\u043b\u0430\u0442\u043d\u044b\u0435 (\u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u043d\u044b\u0435) \u0430\u043a\u043a\u0430\u0443\u043d\u0442\u044b \u0438 \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u043e\u0431\u044b\u0447\u043d\u044b\u043c \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f\u043c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u0438 \u043f\u043e\u0434\u043f\u0438\u0441\u0430\u0442\u044c\u0441\u044f \u043d\u0430 \u043f\u043b\u0430\u0442\u043d\u044b\u0439 \u0442\u0430\u0440\u0438\u0444\u043d\u044b\u0439 \u043f\u043b\u0430\u043d.\n\n\u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u043d\u0443\u044e \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044e, \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": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f Ambee \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sensor.bg.json b/homeassistant/components/ambee/translations/sensor.bg.json deleted file mode 100644 index 07977ca4abf..00000000000 --- a/homeassistant/components/ambee/translations/sensor.bg.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "state": { - "ambee__risk": { - "high": "\u0412\u0438\u0441\u043e\u043a\u043e", - "low": "\u041d\u0438\u0441\u043a\u043e", - "moderate": "\u0423\u043c\u0435\u0440\u0435\u043d\u043e", - "very high": "\u041c\u043d\u043e\u0433\u043e \u0432\u0438\u0441\u043e\u043a\u043e" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sensor.ca.json b/homeassistant/components/ambee/translations/sensor.ca.json deleted file mode 100644 index b85d6bdc8e2..00000000000 --- a/homeassistant/components/ambee/translations/sensor.ca.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "state": { - "ambee__risk": { - "high": "Alt", - "low": "Baix", - "moderate": "Moderat", - "very high": "Molt alt" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sensor.de.json b/homeassistant/components/ambee/translations/sensor.de.json deleted file mode 100644 index c96a2c50eb7..00000000000 --- a/homeassistant/components/ambee/translations/sensor.de.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "state": { - "ambee__risk": { - "high": "Hoch", - "low": "Niedrig", - "moderate": "M\u00e4\u00dfig", - "very high": "Sehr hoch" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sensor.el.json b/homeassistant/components/ambee/translations/sensor.el.json deleted file mode 100644 index 8e9af2dac05..00000000000 --- a/homeassistant/components/ambee/translations/sensor.el.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "state": { - "ambee__risk": { - "high": "\u03a5\u03c8\u03b7\u03bb\u03cc", - "low": "\u03a7\u03b1\u03bc\u03b7\u03bb\u03cc", - "moderate": "\u039c\u03ad\u03c4\u03c1\u03b9\u03bf", - "very high": "\u03a0\u03bf\u03bb\u03cd \u03c5\u03c8\u03b7\u03bb\u03cc" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sensor.en.json b/homeassistant/components/ambee/translations/sensor.en.json deleted file mode 100644 index a4b198eadf5..00000000000 --- a/homeassistant/components/ambee/translations/sensor.en.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "state": { - "ambee__risk": { - "high": "High", - "low": "Low", - "moderate": "Moderate", - "very high": "Very High" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sensor.es-419.json b/homeassistant/components/ambee/translations/sensor.es-419.json deleted file mode 100644 index a676ca7aa5e..00000000000 --- a/homeassistant/components/ambee/translations/sensor.es-419.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "state": { - "ambee__risk": { - "high": "Alto", - "low": "Bajo", - "moderate": "Moderado", - "very high": "Muy alto" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sensor.es.json b/homeassistant/components/ambee/translations/sensor.es.json deleted file mode 100644 index a676ca7aa5e..00000000000 --- a/homeassistant/components/ambee/translations/sensor.es.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "state": { - "ambee__risk": { - "high": "Alto", - "low": "Bajo", - "moderate": "Moderado", - "very high": "Muy alto" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sensor.et.json b/homeassistant/components/ambee/translations/sensor.et.json deleted file mode 100644 index 7599f2fd2c3..00000000000 --- a/homeassistant/components/ambee/translations/sensor.et.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "state": { - "ambee__risk": { - "high": "K\u00f5rge", - "low": "Madal", - "moderate": "M\u00f5\u00f5dukas", - "very high": "V\u00e4ga k\u00f5rge" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sensor.fr.json b/homeassistant/components/ambee/translations/sensor.fr.json deleted file mode 100644 index 76dc3fe6301..00000000000 --- a/homeassistant/components/ambee/translations/sensor.fr.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "state": { - "ambee__risk": { - "high": "Haute", - "low": "Faible", - "moderate": "Mod\u00e9rer", - "very high": "Tr\u00e8s \u00e9lev\u00e9" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sensor.he.json b/homeassistant/components/ambee/translations/sensor.he.json deleted file mode 100644 index 14ae06f2bc9..00000000000 --- a/homeassistant/components/ambee/translations/sensor.he.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "state": { - "ambee__risk": { - "high": "\u05d2\u05d1\u05d5\u05d4", - "low": "\u05e0\u05de\u05d5\u05da", - "very high": "\u05d2\u05d1\u05d5\u05d4 \u05de\u05d0\u05d5\u05d3" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sensor.hu.json b/homeassistant/components/ambee/translations/sensor.hu.json deleted file mode 100644 index 975d200a507..00000000000 --- a/homeassistant/components/ambee/translations/sensor.hu.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "state": { - "ambee__risk": { - "high": "Magas", - "low": "Alacsony", - "moderate": "M\u00e9rs\u00e9kelt", - "very high": "Nagyon magas" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sensor.id.json b/homeassistant/components/ambee/translations/sensor.id.json deleted file mode 100644 index 5cb74694da5..00000000000 --- a/homeassistant/components/ambee/translations/sensor.id.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "state": { - "ambee__risk": { - "high": "Tinggi", - "low": "Rendah", - "moderate": "Sedang", - "very high": "Sangat Tinggi" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sensor.it.json b/homeassistant/components/ambee/translations/sensor.it.json deleted file mode 100644 index 1c265a6ca53..00000000000 --- a/homeassistant/components/ambee/translations/sensor.it.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "state": { - "ambee__risk": { - "high": "Alto", - "low": "Basso", - "moderate": "Moderato", - "very high": "Molto alto" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sensor.ja.json b/homeassistant/components/ambee/translations/sensor.ja.json deleted file mode 100644 index a750a257864..00000000000 --- a/homeassistant/components/ambee/translations/sensor.ja.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "state": { - "ambee__risk": { - "high": "\u9ad8\u3044", - "low": "\u4f4e\u3044", - "moderate": "\u9069\u5ea6", - "very high": "\u975e\u5e38\u306b\u9ad8\u3044" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sensor.nl.json b/homeassistant/components/ambee/translations/sensor.nl.json deleted file mode 100644 index e9ba0c76a34..00000000000 --- a/homeassistant/components/ambee/translations/sensor.nl.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "state": { - "ambee__risk": { - "high": "Hoog", - "low": "Laag", - "moderate": "Matig", - "very high": "Zeer hoog" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sensor.no.json b/homeassistant/components/ambee/translations/sensor.no.json deleted file mode 100644 index cf4e4bed6ed..00000000000 --- a/homeassistant/components/ambee/translations/sensor.no.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "state": { - "ambee__risk": { - "high": "H\u00f8y", - "low": "Lav", - "moderate": "Moderat", - "very high": "Veldig h\u00f8y" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sensor.pl.json b/homeassistant/components/ambee/translations/sensor.pl.json deleted file mode 100644 index d67bdec0879..00000000000 --- a/homeassistant/components/ambee/translations/sensor.pl.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "state": { - "ambee__risk": { - "high": "wysoki", - "low": "niski", - "moderate": "umiarkowany", - "very high": "bardzo wysoki" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sensor.pt-BR.json b/homeassistant/components/ambee/translations/sensor.pt-BR.json deleted file mode 100644 index 2e0dc187368..00000000000 --- a/homeassistant/components/ambee/translations/sensor.pt-BR.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "state": { - "ambee__risk": { - "high": "Alto", - "low": "Baixo", - "moderate": "Moderado", - "very high": "Muito alto" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sensor.ru.json b/homeassistant/components/ambee/translations/sensor.ru.json deleted file mode 100644 index c0dbe8cecd6..00000000000 --- a/homeassistant/components/ambee/translations/sensor.ru.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "state": { - "ambee__risk": { - "high": "\u0412\u044b\u0441\u043e\u043a\u0438\u0439", - "low": "\u041d\u0438\u0437\u043a\u0438\u0439", - "moderate": "\u0423\u043c\u0435\u0440\u0435\u043d\u043d\u044b\u0439", - "very high": "\u041e\u0447\u0435\u043d\u044c \u0432\u044b\u0441\u043e\u043a\u0438\u0439" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sensor.sv.json b/homeassistant/components/ambee/translations/sensor.sv.json deleted file mode 100644 index d3280d4ebf4..00000000000 --- a/homeassistant/components/ambee/translations/sensor.sv.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "state": { - "ambee__risk": { - "high": "H\u00f6g", - "low": "L\u00e5g", - "moderate": "M\u00e5ttlig", - "very high": "V\u00e4ldigt h\u00f6gt" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sensor.tr.json b/homeassistant/components/ambee/translations/sensor.tr.json deleted file mode 100644 index 087bea4ed99..00000000000 --- a/homeassistant/components/ambee/translations/sensor.tr.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "state": { - "ambee__risk": { - "high": "Y\u00fcksek", - "low": "D\u00fc\u015f\u00fck", - "moderate": "Moderate", - "very high": "\u00c7ok y\u00fcksek" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sensor.zh-Hant.json b/homeassistant/components/ambee/translations/sensor.zh-Hant.json deleted file mode 100644 index 1e3c5bbe58d..00000000000 --- a/homeassistant/components/ambee/translations/sensor.zh-Hant.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "state": { - "ambee__risk": { - "high": "\u9ad8", - "low": "\u4f4e", - "moderate": "\u4e2d", - "very high": "\u6975\u9ad8" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sk.json b/homeassistant/components/ambee/translations/sk.json deleted file mode 100644 index a474631a7f8..00000000000 --- a/homeassistant/components/ambee/translations/sk.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "config": { - "abort": { - "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" - }, - "error": { - "invalid_api_key": "Neplatn\u00fd API k\u013e\u00fa\u010d" - }, - "step": { - "reauth_confirm": { - "data": { - "api_key": "API k\u013e\u00fa\u010d" - } - }, - "user": { - "data": { - "api_key": "API k\u013e\u00fa\u010d", - "latitude": "Zemepisn\u00e1 \u0161\u00edrka", - "longitude": "Zemepisn\u00e1 d\u013a\u017eka", - "name": "N\u00e1zov" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sv.json b/homeassistant/components/ambee/translations/sv.json deleted file mode 100644 index e31205118b9..00000000000 --- a/homeassistant/components/ambee/translations/sv.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "config": { - "abort": { - "reauth_successful": "\u00c5terautentisering lyckades" - }, - "error": { - "cannot_connect": "Det gick inte att ansluta.", - "invalid_api_key": "Ogiltig API-nyckel" - }, - "step": { - "reauth_confirm": { - "data": { - "api_key": "API-nyckel", - "description": "Autentisera p\u00e5 nytt med ditt Ambee-konto." - } - }, - "user": { - "data": { - "api_key": "API-nyckel", - "latitude": "Latitud", - "longitude": "Longitud", - "name": "Namn" - }, - "description": "Konfigurera Ambee f\u00f6r att integrera med Home Assistant." - } - } - }, - "issues": { - "pending_removal": { - "description": "Ambee-integrationen v\u00e4ntar p\u00e5 borttagning fr\u00e5n Home Assistant och kommer inte l\u00e4ngre att vara tillg\u00e4nglig fr\u00e5n och med Home Assistant 2022.10. \n\n Integrationen tas bort eftersom Ambee tog bort sina gratis (begr\u00e4nsade) konton och inte l\u00e4ngre ger vanliga anv\u00e4ndare m\u00f6jlighet att registrera sig f\u00f6r en betalplan. \n\n Ta bort Ambee-integreringsposten fr\u00e5n din instans f\u00f6r att \u00e5tg\u00e4rda problemet.", - "title": "Ambee-integrationen tas bort" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/tr.json b/homeassistant/components/ambee/translations/tr.json deleted file mode 100644 index 0163ea40bae..00000000000 --- a/homeassistant/components/ambee/translations/tr.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "config": { - "abort": { - "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" - }, - "error": { - "cannot_connect": "Ba\u011flanma hatas\u0131", - "invalid_api_key": "Ge\u00e7ersiz API anahtar\u0131" - }, - "step": { - "reauth_confirm": { - "data": { - "api_key": "API Anahtar\u0131", - "description": "Ambee hesab\u0131n\u0131zla yeniden kimlik do\u011frulamas\u0131 yap\u0131n." - } - }, - "user": { - "data": { - "api_key": "API Anahtar\u0131", - "latitude": "Enlem", - "longitude": "Boylam", - "name": "Ad" - }, - "description": "Ambee'yi Home Assistant ile entegre olacak \u015fekilde ayarlay\u0131n." - } - } - }, - "issues": { - "pending_removal": { - "description": "Ambee entegrasyonu Home Assistant'tan kald\u0131r\u0131lmay\u0131 beklemektedir ve Home Assistant 2022.10'dan itibaren art\u0131k kullan\u0131lamayacakt\u0131r.\n\nEntegrasyon kald\u0131r\u0131l\u0131yor \u00e7\u00fcnk\u00fc Ambee \u00fccretsiz (s\u0131n\u0131rl\u0131) hesaplar\u0131n\u0131 kald\u0131rd\u0131 ve art\u0131k normal kullan\u0131c\u0131lar\u0131n \u00fccretli bir plana kaydolmas\u0131 i\u00e7in bir yol sa\u011flam\u0131yor.\n\nBu sorunu gidermek i\u00e7in Ambee entegrasyon giri\u015fini \u00f6rne\u011finizden kald\u0131r\u0131n.", - "title": "Ambee entegrasyonu kald\u0131r\u0131l\u0131yor" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/zh-Hant.json b/homeassistant/components/ambee/translations/zh-Hant.json deleted file mode 100644 index ccebea49c6f..00000000000 --- a/homeassistant/components/ambee/translations/zh-Hant.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "config": { - "abort": { - "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" - }, - "error": { - "cannot_connect": "\u9023\u7dda\u5931\u6557", - "invalid_api_key": "API \u91d1\u9470\u7121\u6548" - }, - "step": { - "reauth_confirm": { - "data": { - "api_key": "API \u91d1\u9470", - "description": "\u91cd\u65b0\u8a8d\u8b49 Ambee \u5e33\u865f\u3002" - } - }, - "user": { - "data": { - "api_key": "API \u91d1\u9470", - "latitude": "\u7def\u5ea6", - "longitude": "\u7d93\u5ea6", - "name": "\u540d\u7a31" - }, - "description": "\u8a2d\u5b9a Ambee \u4ee5\u6574\u5408\u81f3 Home Assistant\u3002" - } - } - }, - "issues": { - "pending_removal": { - "description": "Ambee \u6574\u5408\u5373\u5c07\u7531 Home Assistant \u4e2d\u79fb\u9664\u3001\u4e26\u65bc Home Assistant 2022.10 \u7248\u5f8c\u7121\u6cd5\u518d\u4f7f\u7528\u3002\n\n\u7531\u65bc Ambee \u79fb\u9664\u4e86\u5176\u514d\u8cbb\uff08\u6709\u9650\uff09\u5e33\u865f\u3001\u4e26\u4e14\u4e0d\u518d\u63d0\u4f9b\u4e00\u822c\u4f7f\u7528\u8005\u8a3b\u518a\u4ed8\u8cbb\u670d\u52d9\u3001\u6574\u5408\u5373\u5c07\u79fb\u9664\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant to \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", - "title": "Ambee \u6574\u5408\u5373\u5c07\u79fb\u9664" - } - } -} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 22425231288..60ac2e8d511 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -24,7 +24,6 @@ FLOWS = { "aladdin_connect", "alarmdecoder", "almond", - "ambee", "amberelectric", "ambiclimate", "ambient_station", diff --git a/mypy.ini b/mypy.ini index 466f61814ba..3c886bdc1c2 100644 --- a/mypy.ini +++ b/mypy.ini @@ -279,16 +279,6 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true -[mypy-homeassistant.components.ambee.*] -check_untyped_defs = true -disallow_incomplete_defs = true -disallow_subclassing_any = true -disallow_untyped_calls = true -disallow_untyped_decorators = true -disallow_untyped_defs = true -warn_return_any = true -warn_unreachable = true - [mypy-homeassistant.components.ambient_station.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index 672f81ec72d..d2bf8734c2b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -297,9 +297,6 @@ airtouch4pyapi==1.0.5 # homeassistant.components.alpha_vantage alpha_vantage==2.3.1 -# homeassistant.components.ambee -ambee==0.4.0 - # homeassistant.components.amberelectric amberelectric==1.0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 638d2fa5a56..8376456cb53 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -269,9 +269,6 @@ airthings_cloud==0.1.0 # homeassistant.components.airtouch4 airtouch4pyapi==1.0.5 -# homeassistant.components.ambee -ambee==0.4.0 - # homeassistant.components.amberelectric amberelectric==1.0.4 diff --git a/tests/components/ambee/__init__.py b/tests/components/ambee/__init__.py deleted file mode 100644 index 94c88557803..00000000000 --- a/tests/components/ambee/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the Ambee integration.""" diff --git a/tests/components/ambee/conftest.py b/tests/components/ambee/conftest.py deleted file mode 100644 index d6dd53a9711..00000000000 --- a/tests/components/ambee/conftest.py +++ /dev/null @@ -1,53 +0,0 @@ -"""Fixtures for Ambee integration tests.""" -import json -from unittest.mock import AsyncMock, MagicMock, patch - -from ambee import AirQuality, Pollen -import pytest - -from homeassistant.components.ambee.const import DOMAIN -from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE -from homeassistant.core import HomeAssistant - -from tests.common import MockConfigEntry, load_fixture -from tests.test_util.aiohttp import AiohttpClientMocker - - -@pytest.fixture -def mock_config_entry() -> MockConfigEntry: - """Return the default mocked config entry.""" - return MockConfigEntry( - title="Home Sweet Home", - domain=DOMAIN, - data={CONF_LATITUDE: 52.42, CONF_LONGITUDE: 4.44, CONF_API_KEY: "example"}, - unique_id="unique_thingy", - ) - - -@pytest.fixture -def mock_ambee(aioclient_mock: AiohttpClientMocker): - """Return a mocked Ambee client.""" - with patch("homeassistant.components.ambee.Ambee") as ambee_mock: - client = ambee_mock.return_value - client.air_quality = AsyncMock( - return_value=AirQuality.from_dict( - json.loads(load_fixture("ambee/air_quality.json")) - ) - ) - client.pollen = AsyncMock( - return_value=Pollen.from_dict(json.loads(load_fixture("ambee/pollen.json"))) - ) - yield ambee_mock - - -@pytest.fixture -async def init_integration( - hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_ambee: MagicMock -) -> MockConfigEntry: - """Set up the Ambee integration for testing.""" - mock_config_entry.add_to_hass(hass) - - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() - - return mock_config_entry diff --git a/tests/components/ambee/fixtures/air_quality.json b/tests/components/ambee/fixtures/air_quality.json deleted file mode 100644 index be75c832949..00000000000 --- a/tests/components/ambee/fixtures/air_quality.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "message": "success", - "stations": [ - { - "CO": 0.105, - "NO2": 0.66, - "OZONE": 17.067, - "PM10": 5.24, - "PM25": 3.14, - "SO2": 0.031, - "city": "Hellendoorn", - "countryCode": "NL", - "division": "", - "lat": 52.3981, - "lng": 6.4493, - "placeName": "Hellendoorn", - "postalCode": "7447", - "state": "Overijssel", - "updatedAt": "2021-05-29T14:00:00.000Z", - "AQI": 13, - "aqiInfo": { - "pollutant": "PM2.5", - "concentration": 3.14, - "category": "Good" - } - } - ] -} diff --git a/tests/components/ambee/fixtures/pollen.json b/tests/components/ambee/fixtures/pollen.json deleted file mode 100644 index 79d581ff3e2..00000000000 --- a/tests/components/ambee/fixtures/pollen.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "message": "Success", - "lat": 52.42, - "lng": 6.42, - "data": [ - { - "Count": { - "grass_pollen": 190, - "tree_pollen": 127, - "weed_pollen": 95 - }, - "Risk": { - "grass_pollen": "High", - "tree_pollen": "Moderate", - "weed_pollen": "High" - }, - "Species": { - "Grass": { - "Grass / Poaceae": 190 - }, - "Others": 5, - "Tree": { - "Alder": 0, - "Birch": 35, - "Cypress": 0, - "Elm": 0, - "Hazel": 0, - "Oak": 55, - "Pine": 30, - "Plane": 5, - "Poplar / Cottonwood": 0 - }, - "Weed": { - "Chenopod": 0, - "Mugwort": 1, - "Nettle": 88, - "Ragweed": 3 - } - }, - "updatedAt": "2021-06-09T16:24:27.000Z" - } - ] -} diff --git a/tests/components/ambee/test_config_flow.py b/tests/components/ambee/test_config_flow.py deleted file mode 100644 index 233bbaf232b..00000000000 --- a/tests/components/ambee/test_config_flow.py +++ /dev/null @@ -1,268 +0,0 @@ -"""Tests for the Ambee config flow.""" - -from unittest.mock import patch - -from ambee import AmbeeAuthenticationError, AmbeeError - -from homeassistant.components.ambee.const import DOMAIN -from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER -from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME -from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResultType - -from tests.common import MockConfigEntry - - -async def test_full_user_flow(hass: HomeAssistant) -> None: - """Test the full user configuration flow.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) - - assert result.get("type") == FlowResultType.FORM - assert result.get("step_id") == SOURCE_USER - assert "flow_id" in result - - with patch( - "homeassistant.components.ambee.config_flow.Ambee.air_quality" - ) as mock_ambee, patch( - "homeassistant.components.ambee.async_setup_entry", return_value=True - ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_NAME: "Name", - CONF_API_KEY: "example", - CONF_LATITUDE: 52.42, - CONF_LONGITUDE: 4.44, - }, - ) - - assert result2.get("type") == FlowResultType.CREATE_ENTRY - assert result2.get("title") == "Name" - assert result2.get("data") == { - CONF_API_KEY: "example", - CONF_LATITUDE: 52.42, - CONF_LONGITUDE: 4.44, - } - - assert len(mock_setup_entry.mock_calls) == 1 - assert len(mock_ambee.mock_calls) == 1 - - -async def test_full_flow_with_authentication_error(hass: HomeAssistant) -> None: - """Test the full user configuration flow with an authentication error. - - This tests tests a full config flow, with a case the user enters an invalid - API token, but recover by entering the correct one. - """ - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) - - assert result.get("type") == FlowResultType.FORM - assert result.get("step_id") == SOURCE_USER - assert "flow_id" in result - - with patch( - "homeassistant.components.ambee.config_flow.Ambee.air_quality", - side_effect=AmbeeAuthenticationError, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_NAME: "Name", - CONF_API_KEY: "invalid", - CONF_LATITUDE: 52.42, - CONF_LONGITUDE: 4.44, - }, - ) - - assert result2.get("type") == FlowResultType.FORM - assert result2.get("step_id") == SOURCE_USER - assert result2.get("errors") == {"base": "invalid_api_key"} - assert "flow_id" in result2 - - with patch( - "homeassistant.components.ambee.config_flow.Ambee.air_quality" - ) as mock_ambee, patch( - "homeassistant.components.ambee.async_setup_entry", return_value=True - ) as mock_setup_entry: - result3 = await hass.config_entries.flow.async_configure( - result2["flow_id"], - user_input={ - CONF_NAME: "Name", - CONF_API_KEY: "example", - CONF_LATITUDE: 52.42, - CONF_LONGITUDE: 4.44, - }, - ) - - assert result3.get("type") == FlowResultType.CREATE_ENTRY - assert result3.get("title") == "Name" - assert result3.get("data") == { - CONF_API_KEY: "example", - CONF_LATITUDE: 52.42, - CONF_LONGITUDE: 4.44, - } - - assert len(mock_setup_entry.mock_calls) == 1 - assert len(mock_ambee.mock_calls) == 1 - - -async def test_api_error(hass: HomeAssistant) -> None: - """Test API error.""" - with patch( - "homeassistant.components.ambee.Ambee.air_quality", - side_effect=AmbeeError, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - data={ - CONF_NAME: "Name", - CONF_API_KEY: "example", - CONF_LATITUDE: 52.42, - CONF_LONGITUDE: 4.44, - }, - ) - - assert result.get("type") == FlowResultType.FORM - assert result.get("errors") == {"base": "cannot_connect"} - - -async def test_reauth_flow( - hass: HomeAssistant, mock_config_entry: MockConfigEntry -) -> None: - """Test the reauthentication configuration flow.""" - mock_config_entry.add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={ - "source": SOURCE_REAUTH, - "unique_id": mock_config_entry.unique_id, - "entry_id": mock_config_entry.entry_id, - }, - data=mock_config_entry.data, - ) - assert result.get("type") == FlowResultType.FORM - assert result.get("step_id") == "reauth_confirm" - assert "flow_id" in result - - with patch( - "homeassistant.components.ambee.config_flow.Ambee.air_quality" - ) as mock_ambee, patch( - "homeassistant.components.ambee.async_setup_entry", return_value=True - ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_API_KEY: "other_key"}, - ) - await hass.async_block_till_done() - - assert result2.get("type") == FlowResultType.ABORT - assert result2.get("reason") == "reauth_successful" - assert mock_config_entry.data == { - CONF_API_KEY: "other_key", - CONF_LATITUDE: 52.42, - CONF_LONGITUDE: 4.44, - } - - assert len(mock_ambee.mock_calls) == 1 - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_reauth_with_authentication_error( - hass: HomeAssistant, mock_config_entry: MockConfigEntry -) -> None: - """Test the reauthentication configuration flow with an authentication error. - - This tests tests a reauth flow, with a case the user enters an invalid - API token, but recover by entering the correct one. - """ - mock_config_entry.add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={ - "source": SOURCE_REAUTH, - "unique_id": mock_config_entry.unique_id, - "entry_id": mock_config_entry.entry_id, - }, - data=mock_config_entry.data, - ) - assert result.get("type") == FlowResultType.FORM - assert result.get("step_id") == "reauth_confirm" - assert "flow_id" in result - - with patch( - "homeassistant.components.ambee.config_flow.Ambee.air_quality", - side_effect=AmbeeAuthenticationError, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_API_KEY: "invalid", - }, - ) - - assert result2.get("type") == FlowResultType.FORM - assert result2.get("step_id") == "reauth_confirm" - assert result2.get("errors") == {"base": "invalid_api_key"} - assert "flow_id" in result2 - - with patch( - "homeassistant.components.ambee.config_flow.Ambee.air_quality" - ) as mock_ambee, patch( - "homeassistant.components.ambee.async_setup_entry", return_value=True - ) as mock_setup_entry: - result3 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_API_KEY: "other_key"}, - ) - await hass.async_block_till_done() - - assert result3.get("type") == FlowResultType.ABORT - assert result3.get("reason") == "reauth_successful" - assert mock_config_entry.data == { - CONF_API_KEY: "other_key", - CONF_LATITUDE: 52.42, - CONF_LONGITUDE: 4.44, - } - - assert len(mock_ambee.mock_calls) == 1 - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_reauth_api_error( - hass: HomeAssistant, mock_config_entry: MockConfigEntry -) -> None: - """Test API error during reauthentication.""" - mock_config_entry.add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={ - "source": SOURCE_REAUTH, - "unique_id": mock_config_entry.unique_id, - "entry_id": mock_config_entry.entry_id, - }, - data=mock_config_entry.data, - ) - assert "flow_id" in result - - with patch( - "homeassistant.components.ambee.config_flow.Ambee.air_quality", - side_effect=AmbeeError, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_API_KEY: "invalid", - }, - ) - - assert result2.get("type") == FlowResultType.FORM - assert result2.get("step_id") == "reauth_confirm" - assert result2.get("errors") == {"base": "cannot_connect"} diff --git a/tests/components/ambee/test_init.py b/tests/components/ambee/test_init.py deleted file mode 100644 index 059c58da803..00000000000 --- a/tests/components/ambee/test_init.py +++ /dev/null @@ -1,89 +0,0 @@ -"""Tests for the Ambee integration.""" -from collections.abc import Awaitable, Callable -from unittest.mock import AsyncMock, MagicMock, patch - -from aiohttp import ClientWebSocketResponse -from ambee import AmbeeConnectionError -from ambee.exceptions import AmbeeAuthenticationError -import pytest - -from homeassistant.components.ambee.const import DOMAIN -from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState -from homeassistant.core import HomeAssistant - -from tests.common import MockConfigEntry -from tests.components.repairs import get_repairs - - -async def test_load_unload_config_entry( - hass: HomeAssistant, - mock_config_entry: MockConfigEntry, - mock_ambee: AsyncMock, - hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]], -) -> None: - """Test the Ambee configuration entry loading/unloading.""" - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() - - assert mock_config_entry.state is ConfigEntryState.LOADED - - issues = await get_repairs(hass, hass_ws_client) - assert len(issues) == 1 - assert issues[0]["issue_id"] == "pending_removal" - - await hass.config_entries.async_unload(mock_config_entry.entry_id) - await hass.async_block_till_done() - - issues = await get_repairs(hass, hass_ws_client) - assert len(issues) == 0 - - assert not hass.data.get(DOMAIN) - - -@patch( - "homeassistant.components.ambee.Ambee.request", - side_effect=AmbeeConnectionError, -) -async def test_config_entry_not_ready( - mock_request: MagicMock, - hass: HomeAssistant, - mock_config_entry: MockConfigEntry, -) -> None: - """Test the Ambee configuration entry not ready.""" - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() - - assert mock_request.call_count == 1 - assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY - - -@pytest.mark.parametrize("service_name", ["air_quality", "pollen"]) -async def test_config_entry_authentication_failed( - hass: HomeAssistant, - mock_config_entry: MockConfigEntry, - mock_ambee: MagicMock, - service_name: str, -) -> None: - """Test the Ambee configuration entry not ready.""" - mock_config_entry.add_to_hass(hass) - - service = getattr(mock_ambee.return_value, service_name) - service.side_effect = AmbeeAuthenticationError - - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() - - assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR - - flows = hass.config_entries.flow.async_progress() - assert len(flows) == 1 - - flow = flows[0] - assert flow.get("step_id") == "reauth_confirm" - assert flow.get("handler") == DOMAIN - - assert "context" in flow - assert flow["context"].get("source") == SOURCE_REAUTH - assert flow["context"].get("entry_id") == mock_config_entry.entry_id diff --git a/tests/components/ambee/test_sensor.py b/tests/components/ambee/test_sensor.py deleted file mode 100644 index d143aea8f7c..00000000000 --- a/tests/components/ambee/test_sensor.py +++ /dev/null @@ -1,360 +0,0 @@ -"""Tests for the sensors provided by the Ambee integration.""" -from unittest.mock import AsyncMock - -import pytest - -from homeassistant.components.ambee.const import DEVICE_CLASS_AMBEE_RISK, DOMAIN -from homeassistant.components.sensor import ( - ATTR_STATE_CLASS, - DOMAIN as SENSOR_DOMAIN, - SensorDeviceClass, - SensorStateClass, -) -from homeassistant.const import ( - ATTR_DEVICE_CLASS, - ATTR_FRIENDLY_NAME, - ATTR_ICON, - ATTR_UNIT_OF_MEASUREMENT, - CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - CONCENTRATION_PARTS_PER_BILLION, - CONCENTRATION_PARTS_PER_CUBIC_METER, - CONCENTRATION_PARTS_PER_MILLION, -) -from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr, entity_registry as er - -from tests.common import MockConfigEntry - - -async def test_air_quality( - hass: HomeAssistant, - init_integration: MockConfigEntry, -) -> None: - """Test the Ambee Air Quality sensors.""" - entry_id = init_integration.entry_id - entity_registry = er.async_get(hass) - device_registry = dr.async_get(hass) - - state = hass.states.get("sensor.air_quality_particulate_matter_2_5") - entry = entity_registry.async_get("sensor.air_quality_particulate_matter_2_5") - assert entry - assert state - assert entry.unique_id == f"{entry_id}_air_quality_particulate_matter_2_5" - assert state.state == "3.14" - assert ( - state.attributes.get(ATTR_FRIENDLY_NAME) - == "Air quality Particulate matter < 2.5 μm" - ) - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER - ) - assert ATTR_DEVICE_CLASS not in state.attributes - assert ATTR_ICON not in state.attributes - - state = hass.states.get("sensor.air_quality_particulate_matter_10") - entry = entity_registry.async_get("sensor.air_quality_particulate_matter_10") - assert entry - assert state - assert entry.unique_id == f"{entry_id}_air_quality_particulate_matter_10" - assert state.state == "5.24" - assert ( - state.attributes.get(ATTR_FRIENDLY_NAME) - == "Air quality Particulate matter < 10 μm" - ) - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER - ) - assert ATTR_DEVICE_CLASS not in state.attributes - assert ATTR_ICON not in state.attributes - - state = hass.states.get("sensor.air_quality_sulphur_dioxide") - entry = entity_registry.async_get("sensor.air_quality_sulphur_dioxide") - assert entry - assert state - assert entry.unique_id == f"{entry_id}_air_quality_sulphur_dioxide" - assert state.state == "0.031" - assert ( - state.attributes.get(ATTR_FRIENDLY_NAME) == "Air quality Sulphur dioxide (SO2)" - ) - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == CONCENTRATION_PARTS_PER_BILLION - ) - assert ATTR_DEVICE_CLASS not in state.attributes - assert ATTR_ICON not in state.attributes - - state = hass.states.get("sensor.air_quality_nitrogen_dioxide") - entry = entity_registry.async_get("sensor.air_quality_nitrogen_dioxide") - assert entry - assert state - assert entry.unique_id == f"{entry_id}_air_quality_nitrogen_dioxide" - assert state.state == "0.66" - assert ( - state.attributes.get(ATTR_FRIENDLY_NAME) == "Air quality Nitrogen dioxide (NO2)" - ) - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == CONCENTRATION_PARTS_PER_BILLION - ) - assert ATTR_DEVICE_CLASS not in state.attributes - assert ATTR_ICON not in state.attributes - - state = hass.states.get("sensor.air_quality_ozone") - entry = entity_registry.async_get("sensor.air_quality_ozone") - assert entry - assert state - assert entry.unique_id == f"{entry_id}_air_quality_ozone" - assert state.state == "17.067" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Air quality Ozone" - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == CONCENTRATION_PARTS_PER_BILLION - ) - assert ATTR_DEVICE_CLASS not in state.attributes - assert ATTR_ICON not in state.attributes - - state = hass.states.get("sensor.air_quality_carbon_monoxide") - entry = entity_registry.async_get("sensor.air_quality_carbon_monoxide") - assert entry - assert state - assert entry.unique_id == f"{entry_id}_air_quality_carbon_monoxide" - assert state.state == "0.105" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.CO - assert ( - state.attributes.get(ATTR_FRIENDLY_NAME) == "Air quality Carbon monoxide (CO)" - ) - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == CONCENTRATION_PARTS_PER_MILLION - ) - assert ATTR_ICON not in state.attributes - - state = hass.states.get("sensor.air_quality_air_quality_index") - entry = entity_registry.async_get("sensor.air_quality_air_quality_index") - assert entry - assert state - assert entry.unique_id == f"{entry_id}_air_quality_air_quality_index" - assert state.state == "13" - assert ( - state.attributes.get(ATTR_FRIENDLY_NAME) - == "Air quality Air quality index (AQI)" - ) - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - assert ATTR_DEVICE_CLASS not in state.attributes - assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes - assert ATTR_ICON not in state.attributes - - assert entry.device_id - device_entry = device_registry.async_get(entry.device_id) - assert device_entry - assert device_entry.identifiers == {(DOMAIN, f"{entry_id}_air_quality")} - assert device_entry.manufacturer == "Ambee" - assert device_entry.name == "Air quality" - assert device_entry.entry_type is dr.DeviceEntryType.SERVICE - assert not device_entry.model - assert not device_entry.sw_version - - -async def test_pollen( - hass: HomeAssistant, - init_integration: MockConfigEntry, -) -> None: - """Test the Ambee Pollen sensors.""" - entry_id = init_integration.entry_id - entity_registry = er.async_get(hass) - device_registry = dr.async_get(hass) - - state = hass.states.get("sensor.pollen_grass") - entry = entity_registry.async_get("sensor.pollen_grass") - assert entry - assert state - assert entry.unique_id == f"{entry_id}_pollen_grass" - assert state.state == "190" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Pollen Grass" - assert state.attributes.get(ATTR_ICON) == "mdi:grass" - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == CONCENTRATION_PARTS_PER_CUBIC_METER - ) - assert ATTR_DEVICE_CLASS not in state.attributes - - state = hass.states.get("sensor.pollen_tree") - entry = entity_registry.async_get("sensor.pollen_tree") - assert entry - assert state - assert entry.unique_id == f"{entry_id}_pollen_tree" - assert state.state == "127" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Pollen Tree" - assert state.attributes.get(ATTR_ICON) == "mdi:tree" - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == CONCENTRATION_PARTS_PER_CUBIC_METER - ) - assert ATTR_DEVICE_CLASS not in state.attributes - - state = hass.states.get("sensor.pollen_weed") - entry = entity_registry.async_get("sensor.pollen_weed") - assert entry - assert state - assert entry.unique_id == f"{entry_id}_pollen_weed" - assert state.state == "95" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Pollen Weed" - assert state.attributes.get(ATTR_ICON) == "mdi:sprout" - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == CONCENTRATION_PARTS_PER_CUBIC_METER - ) - assert ATTR_DEVICE_CLASS not in state.attributes - - state = hass.states.get("sensor.pollen_grass_risk") - entry = entity_registry.async_get("sensor.pollen_grass_risk") - assert entry - assert state - assert entry.unique_id == f"{entry_id}_pollen_grass_risk" - assert state.state == "high" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_AMBEE_RISK - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Pollen Grass risk" - assert state.attributes.get(ATTR_ICON) == "mdi:grass" - assert ATTR_STATE_CLASS not in state.attributes - assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes - - state = hass.states.get("sensor.pollen_tree_risk") - entry = entity_registry.async_get("sensor.pollen_tree_risk") - assert entry - assert state - assert entry.unique_id == f"{entry_id}_pollen_tree_risk" - assert state.state == "moderate" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_AMBEE_RISK - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Pollen Tree risk" - assert state.attributes.get(ATTR_ICON) == "mdi:tree" - assert ATTR_STATE_CLASS not in state.attributes - assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes - - state = hass.states.get("sensor.pollen_weed_risk") - entry = entity_registry.async_get("sensor.pollen_weed_risk") - assert entry - assert state - assert entry.unique_id == f"{entry_id}_pollen_weed_risk" - assert state.state == "high" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_AMBEE_RISK - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Pollen Weed risk" - assert state.attributes.get(ATTR_ICON) == "mdi:sprout" - assert ATTR_STATE_CLASS not in state.attributes - assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes - - assert entry.device_id - device_entry = device_registry.async_get(entry.device_id) - assert device_entry - assert device_entry.identifiers == {(DOMAIN, f"{entry_id}_pollen")} - assert device_entry.manufacturer == "Ambee" - assert device_entry.name == "Pollen" - assert device_entry.entry_type is dr.DeviceEntryType.SERVICE - assert not device_entry.model - assert not device_entry.sw_version - - -@pytest.mark.parametrize( - "entity_id", - ( - "sensor.pollen_grass_poaceae", - "sensor.pollen_tree_alder", - "sensor.pollen_tree_birch", - "sensor.pollen_tree_cypress", - "sensor.pollen_tree_elm", - "sensor.pollen_tree_hazel", - "sensor.pollen_tree_oak", - "sensor.pollen_tree_pine", - "sensor.pollen_tree_plane", - "sensor.pollen_tree_poplar", - "sensor.pollen_weed_chenopod", - "sensor.pollen_weed_mugwort", - "sensor.pollen_weed_nettle", - "sensor.pollen_weed_ragweed", - ), -) -async def test_pollen_disabled_by_default( - hass: HomeAssistant, init_integration: MockConfigEntry, entity_id: str -) -> None: - """Test the Ambee Pollen sensors that are disabled by default.""" - entity_registry = er.async_get(hass) - - state = hass.states.get(entity_id) - assert state is None - - entry = entity_registry.async_get(entity_id) - assert entry - assert entry.disabled - assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION - - -@pytest.mark.parametrize( - "key,icon,name,value", - [ - ("grass_poaceae", "mdi:grass", "Poaceae grass", "190"), - ("tree_alder", "mdi:tree", "Alder tree", "0"), - ("tree_birch", "mdi:tree", "Birch tree", "35"), - ("tree_cypress", "mdi:tree", "Cypress tree", "0"), - ("tree_elm", "mdi:tree", "Elm tree", "0"), - ("tree_hazel", "mdi:tree", "Hazel tree", "0"), - ("tree_oak", "mdi:tree", "Oak tree", "55"), - ("tree_pine", "mdi:tree", "Pine tree", "30"), - ("tree_plane", "mdi:tree", "Plane tree", "5"), - ("tree_poplar", "mdi:tree", "Poplar tree", "0"), - ("weed_chenopod", "mdi:sprout", "Chenopod weed", "0"), - ("weed_mugwort", "mdi:sprout", "Mugwort weed", "1"), - ("weed_nettle", "mdi:sprout", "Nettle weed", "88"), - ("weed_ragweed", "mdi:sprout", "Ragweed weed", "3"), - ], -) -async def test_pollen_enable_disable_by_defaults( - hass: HomeAssistant, - mock_config_entry: MockConfigEntry, - mock_ambee: AsyncMock, - key: str, - icon: str, - name: str, - value: str, -) -> None: - """Test the Ambee Pollen sensors that are disabled by default.""" - entry_id = mock_config_entry.entry_id - entity_id = f"{SENSOR_DOMAIN}.pollen_{key}" - entity_registry = er.async_get(hass) - - # Pre-create registry entry for disabled by default sensor - entity_registry.async_get_or_create( - SENSOR_DOMAIN, - DOMAIN, - f"{entry_id}_pollen_{key}", - suggested_object_id=f"pollen_{key}", - disabled_by=None, - ) - - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() - - state = hass.states.get(entity_id) - entry = entity_registry.async_get(entity_id) - assert entry - assert state - assert entry.unique_id == f"{entry_id}_pollen_{key}" - assert state.state == value - assert state.attributes.get(ATTR_FRIENDLY_NAME) == f"Pollen {name}" - assert state.attributes.get(ATTR_ICON) == icon - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == CONCENTRATION_PARTS_PER_CUBIC_METER - ) - assert ATTR_DEVICE_CLASS not in state.attributes From b87cd926e7ef36c55ab9c8e88586d928970a6180 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 Sep 2022 11:13:48 +0200 Subject: [PATCH 380/955] Fix image-processing type hint (#78426) --- homeassistant/components/demo/image_processing.py | 5 ++--- homeassistant/components/image_processing/__init__.py | 7 +++---- pylint/plugins/hass_enforce_type_hints.py | 2 +- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/demo/image_processing.py b/homeassistant/components/demo/image_processing.py index 5d158db46ab..dc5565a1771 100644 --- a/homeassistant/components/demo/image_processing.py +++ b/homeassistant/components/demo/image_processing.py @@ -1,7 +1,6 @@ """Support for the demo image processing.""" from __future__ import annotations -from homeassistant.components.camera import Image from homeassistant.components.image_processing import ( FaceInformation, ImageProcessingFaceEntity, @@ -49,7 +48,7 @@ class DemoImageProcessingAlpr(ImageProcessingAlprEntity): """Return minimum confidence for send events.""" return 80 - def process_image(self, image: Image) -> None: + def process_image(self, image: bytes) -> None: """Process image.""" demo_data = { "AC3829": 98.3, @@ -81,7 +80,7 @@ class DemoImageProcessingFace(ImageProcessingFaceEntity): """Return minimum confidence for send events.""" return 80 - def process_image(self, image: Image) -> None: + def process_image(self, image: bytes) -> None: """Process image.""" demo_data = [ FaceInformation( diff --git a/homeassistant/components/image_processing/__init__.py b/homeassistant/components/image_processing/__init__.py index de90f7dbf81..26e6d195b92 100644 --- a/homeassistant/components/image_processing/__init__.py +++ b/homeassistant/components/image_processing/__init__.py @@ -123,11 +123,11 @@ class ImageProcessingEntity(Entity): """Return minimum confidence for do some things.""" return None - def process_image(self, image: Image) -> None: + def process_image(self, image: bytes) -> None: """Process image.""" raise NotImplementedError() - async def async_process_image(self, image: Image) -> None: + async def async_process_image(self, image: bytes) -> None: """Process image.""" return await self.hass.async_add_executor_job(self.process_image, image) @@ -137,10 +137,9 @@ class ImageProcessingEntity(Entity): This method is a coroutine. """ camera = self.hass.components.camera - image = None try: - image = await camera.async_get_image( + image: Image = await camera.async_get_image( self.camera_entity, timeout=self.timeout ) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 7cd94b3181c..6562785180d 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -1366,7 +1366,7 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ), TypeHintMatch( function_name="process_image", - arg_types={1: "Image"}, + arg_types={1: "bytes"}, return_type=None, has_async_counterpart=True, ), From dce2569389c1fe0ab7e6eb453417762374b9a151 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 Sep 2022 11:15:47 +0200 Subject: [PATCH 381/955] Improve type hints in weather (#78346) Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> --- homeassistant/components/weather/__init__.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index c55ae043622..9f8c1b9c5ce 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -45,8 +45,6 @@ from homeassistant.util import ( temperature as temperature_util, ) -# mypy: allow-untyped-defs, no-check-untyped-defs - _LOGGER = logging.getLogger(__name__) ATTR_CONDITION_CLASS = "condition_class" @@ -626,9 +624,9 @@ class WeatherEntity(Entity): @final @property - def state_attributes(self): + def state_attributes(self) -> dict[str, Any]: """Return the state attributes, converted from native units to user-configured units.""" - data = {} + data: dict[str, Any] = {} precision = self.precision @@ -705,9 +703,9 @@ class WeatherEntity(Entity): data[ATTR_WEATHER_PRECIPITATION_UNIT] = self._precipitation_unit if self.forecast is not None: - forecast = [] - for forecast_entry in self.forecast: - forecast_entry = dict(forecast_entry) + forecast: list[dict[str, Any]] = [] + for existing_forecast_entry in self.forecast: + forecast_entry: dict[str, Any] = dict(existing_forecast_entry) temperature = forecast_entry.pop( ATTR_FORECAST_NATIVE_TEMP, forecast_entry.get(ATTR_FORECAST_TEMP) From abc87b5dfabfc2381cba15bd6461d4f9a8efb585 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 Sep 2022 11:18:23 +0200 Subject: [PATCH 382/955] Improve type hints in scene (#78347) --- homeassistant/components/scene/__init__.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/scene/__init__.py b/homeassistant/components/scene/__init__.py index 5dea5965d43..813f5138ed1 100644 --- a/homeassistant/components/scene/__init__.py +++ b/homeassistant/components/scene/__init__.py @@ -4,7 +4,7 @@ from __future__ import annotations import functools as ft import importlib import logging -from typing import Any, final +from typing import Any, Final, final import voluptuous as vol @@ -17,13 +17,11 @@ from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType from homeassistant.util import dt as dt_util -# mypy: allow-untyped-defs, no-check-untyped-defs - -DOMAIN = "scene" -STATES = "states" +DOMAIN: Final = "scene" +STATES: Final = "states" -def _hass_domain_validator(config): +def _hass_domain_validator(config: dict[str, Any]) -> dict[str, Any]: """Validate platform in config for homeassistant domain.""" if CONF_PLATFORM not in config: config = {CONF_PLATFORM: HA_DOMAIN, STATES: config} @@ -31,7 +29,7 @@ def _hass_domain_validator(config): return config -def _platform_validator(config): +def _platform_validator(config: dict[str, Any]) -> dict[str, Any]: """Validate it is a valid platform.""" try: platform = importlib.import_module(f".{config[CONF_PLATFORM]}", __name__) @@ -46,7 +44,7 @@ def _platform_validator(config): if not hasattr(platform, "PLATFORM_SCHEMA"): return config - return platform.PLATFORM_SCHEMA(config) + return platform.PLATFORM_SCHEMA(config) # type: ignore[no-any-return] PLATFORM_SCHEMA = vol.Schema( From 03a24e3a055f4ab117f6699c860d9e652d7a9faa Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 Sep 2022 11:22:58 +0200 Subject: [PATCH 383/955] Improve type hints in proximity (#78348) --- .../components/proximity/__init__.py | 78 +++++++++++-------- 1 file changed, 45 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/proximity/__init__.py b/homeassistant/components/proximity/__init__.py index 8eec3e2f038..47dd663561c 100644 --- a/homeassistant/components/proximity/__init__.py +++ b/homeassistant/components/proximity/__init__.py @@ -1,4 +1,6 @@ """Support for tracking the proximity of a device.""" +from __future__ import annotations + import logging import voluptuous as vol @@ -15,7 +17,7 @@ from homeassistant.const import ( LENGTH_MILES, LENGTH_YARD, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, State import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import track_state_change @@ -23,8 +25,6 @@ from homeassistant.helpers.typing import ConfigType from homeassistant.util.distance import convert from homeassistant.util.location import distance -# mypy: allow-untyped-defs, no-check-untyped-defs - _LOGGER = logging.getLogger(__name__) ATTR_DIR_OF_TRAVEL = "dir_of_travel" @@ -79,7 +79,7 @@ def setup_proximity_component( ) zone_id = f"zone.{config[CONF_ZONE]}" - proximity = Proximity( # type:ignore[no-untyped-call] + proximity = Proximity( hass, proximity_zone, DEFAULT_DIST_TO_ZONE, @@ -113,21 +113,21 @@ class Proximity(Entity): def __init__( self, - hass, - zone_friendly_name, - dist_to, - dir_of_travel, - nearest, - ignored_zones, - proximity_devices, - tolerance, - proximity_zone, - unit_of_measurement, - ): + hass: HomeAssistant, + zone_friendly_name: str, + dist_to: str, + dir_of_travel: str, + nearest: str, + ignored_zones: list[str], + proximity_devices: list[str], + tolerance: int, + proximity_zone: str, + unit_of_measurement: str, + ) -> None: """Initialize the proximity.""" self.hass = hass self.friendly_name = zone_friendly_name - self.dist_to = dist_to + self.dist_to: str | int = dist_to self.dir_of_travel = dir_of_travel self.nearest = nearest self.ignored_zones = ignored_zones @@ -137,34 +137,40 @@ class Proximity(Entity): self._unit_of_measurement = unit_of_measurement @property - def name(self): + def name(self) -> str: """Return the name of the entity.""" return self.friendly_name @property - def state(self): + def state(self) -> str | int: """Return the state.""" return self.dist_to @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> str: """Return the unit of measurement of this entity.""" return self._unit_of_measurement @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, str]: """Return the state attributes.""" return {ATTR_DIR_OF_TRAVEL: self.dir_of_travel, ATTR_NEAREST: self.nearest} - def check_proximity_state_change(self, entity, old_state, new_state): + def check_proximity_state_change( + self, entity: str, old_state: State | None, new_state: State + ) -> None: """Perform the proximity checking.""" entity_name = new_state.name devices_to_calculate = False devices_in_zone = "" zone_state = self.hass.states.get(self.proximity_zone) - proximity_latitude = zone_state.attributes.get(ATTR_LATITUDE) - proximity_longitude = zone_state.attributes.get(ATTR_LONGITUDE) + proximity_latitude = ( + zone_state.attributes.get(ATTR_LATITUDE) if zone_state else None + ) + proximity_longitude = ( + zone_state.attributes.get(ATTR_LONGITUDE) if zone_state else None + ) # Check for devices in the monitored zone. for device in self.proximity_devices: @@ -203,11 +209,11 @@ class Proximity(Entity): return # Collect distances to the zone for all devices. - distances_to_zone = {} + distances_to_zone: dict[str, float] = {} for device in self.proximity_devices: # Ignore devices in an ignored zone. device_state = self.hass.states.get(device) - if device_state.state in self.ignored_zones: + if not device_state or device_state.state in self.ignored_zones: continue # Ignore devices if proximity cannot be calculated. @@ -215,7 +221,7 @@ class Proximity(Entity): continue # Calculate the distance to the proximity zone. - dist_to_zone = distance( + proximity = distance( proximity_latitude, proximity_longitude, device_state.attributes[ATTR_LATITUDE], @@ -223,14 +229,16 @@ class Proximity(Entity): ) # Add the device and distance to a dictionary. + if not proximity: + continue distances_to_zone[device] = round( - convert(dist_to_zone, LENGTH_METERS, self.unit_of_measurement), 1 + convert(proximity, LENGTH_METERS, self.unit_of_measurement), 1 ) # Loop through each of the distances collected and work out the # closest. - closest_device: str = None - dist_to_zone: float = None + closest_device: str | None = None + dist_to_zone: float | None = None for device, zone in distances_to_zone.items(): if not dist_to_zone or zone < dist_to_zone: @@ -238,10 +246,11 @@ class Proximity(Entity): dist_to_zone = zone # If the closest device is one of the other devices. - if closest_device != entity: + if closest_device is not None and closest_device != entity: self.dist_to = round(distances_to_zone[closest_device]) self.dir_of_travel = "unknown" device_state = self.hass.states.get(closest_device) + assert device_state self.nearest = device_state.name self.schedule_update_ha_state() return @@ -256,7 +265,7 @@ class Proximity(Entity): return # Reset the variables - distance_travelled = 0 + distance_travelled: float = 0 # Calculate the distance travelled. old_distance = distance( @@ -271,6 +280,7 @@ class Proximity(Entity): new_state.attributes[ATTR_LATITUDE], new_state.attributes[ATTR_LONGITUDE], ) + assert new_distance is not None and old_distance is not None distance_travelled = round(new_distance - old_distance, 1) # Check for tolerance @@ -282,14 +292,16 @@ class Proximity(Entity): direction_of_travel = "stationary" # Update the proximity entity - self.dist_to = round(dist_to_zone) + self.dist_to = ( + round(dist_to_zone) if dist_to_zone is not None else DEFAULT_DIST_TO_ZONE + ) self.dir_of_travel = direction_of_travel self.nearest = entity_name self.schedule_update_ha_state() _LOGGER.debug( "proximity.%s update entity: distance=%s: direction=%s: device=%s", self.friendly_name, - round(dist_to_zone), + self.dist_to, direction_of_travel, entity_name, ) From 5cccb248307d138a33c353544c57dc997b4fe917 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 Sep 2022 11:36:28 +0200 Subject: [PATCH 384/955] Improve type hints in group (#78350) --- homeassistant/components/group/__init__.py | 135 ++++++++++-------- .../components/group/media_player.py | 6 +- homeassistant/components/group/notify.py | 35 +++-- 3 files changed, 99 insertions(+), 77 deletions(-) diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index 04bc109d15b..c1759432ade 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -3,10 +3,10 @@ from __future__ import annotations from abc import abstractmethod import asyncio -from collections.abc import Iterable +from collections.abc import Collection, Iterable from contextvars import ContextVar import logging -from typing import Any, Union, cast +from typing import Any, Protocol, Union, cast import voluptuous as vol @@ -27,7 +27,15 @@ from homeassistant.const import ( STATE_ON, Platform, ) -from homeassistant.core import HomeAssistant, ServiceCall, callback, split_entity_id +from homeassistant.core import ( + CALLBACK_TYPE, + Event, + HomeAssistant, + ServiceCall, + State, + callback, + split_entity_id, +) from homeassistant.helpers import config_validation as cv, entity_registry as er, start from homeassistant.helpers.entity import Entity, async_generate_entity_id from homeassistant.helpers.entity_component import EntityComponent @@ -42,8 +50,6 @@ from homeassistant.loader import bind_hass from .const import CONF_HIDE_MEMBERS -# mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs - DOMAIN = "group" GROUP_ORDER = "group_order" @@ -79,10 +85,19 @@ _LOGGER = logging.getLogger(__name__) current_domain: ContextVar[str] = ContextVar("current_domain") -def _conf_preprocess(value): +class GroupProtocol(Protocol): + """Define the format of group platforms.""" + + def async_describe_on_off_states( + self, hass: HomeAssistant, registry: GroupIntegrationRegistry + ) -> None: + """Describe group on off states.""" + + +def _conf_preprocess(value: Any) -> dict[str, Any]: """Preprocess alternative configuration formats.""" if not isinstance(value, dict): - value = {CONF_ENTITIES: value} + return {CONF_ENTITIES: value} return value @@ -135,14 +150,15 @@ class GroupIntegrationRegistry: @bind_hass -def is_on(hass, entity_id): +def is_on(hass: HomeAssistant, entity_id: str) -> bool: """Test if the group state is in its ON-state.""" if REG_KEY not in hass.data: # Integration not setup yet, it cannot be on return False if (state := hass.states.get(entity_id)) is not None: - return state.state in hass.data[REG_KEY].on_off_mapping + registry: GroupIntegrationRegistry = hass.data[REG_KEY] + return state.state in registry.on_off_mapping return False @@ -408,10 +424,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -async def _process_group_platform(hass, domain, platform): +async def _process_group_platform( + hass: HomeAssistant, domain: str, platform: GroupProtocol +) -> None: """Process a group platform.""" current_domain.set(domain) - platform.async_describe_on_off_states(hass, hass.data[REG_KEY]) + registry: GroupIntegrationRegistry = hass.data[REG_KEY] + platform.async_describe_on_off_states(hass, registry) async def _async_process_config(hass: HomeAssistant, config: ConfigType) -> None: @@ -423,7 +442,7 @@ async def _async_process_config(hass: HomeAssistant, config: ConfigType) -> None for object_id, conf in domain_config.items(): name: str = conf.get(CONF_NAME, object_id) - entity_ids: Iterable[str] = conf.get(CONF_ENTITIES) or [] + entity_ids: Collection[str] = conf.get(CONF_ENTITIES) or [] icon: str | None = conf.get(CONF_ICON) mode = bool(conf.get(CONF_ALL)) order: int = hass.data[GROUP_ORDER] @@ -456,15 +475,12 @@ async def _async_process_config(hass: HomeAssistant, config: ConfigType) -> None class GroupEntity(Entity): """Representation of a Group of entities.""" - @property - def should_poll(self) -> bool: - """Disable polling for group.""" - return False + _attr_should_poll = False async def async_added_to_hass(self) -> None: """Register listeners.""" - async def _update_at_start(_): + async def _update_at_start(_: HomeAssistant) -> None: self.async_update_group_state() self.async_write_ha_state() @@ -487,6 +503,10 @@ class GroupEntity(Entity): class Group(Entity): """Track a group of entity ids.""" + _attr_should_poll = False + tracking: tuple[str, ...] + trackable: tuple[str, ...] + def __init__( self, hass: HomeAssistant, @@ -494,7 +514,7 @@ class Group(Entity): order: int | None = None, icon: str | None = None, user_defined: bool = True, - entity_ids: Iterable[str] | None = None, + entity_ids: Collection[str] | None = None, mode: bool | None = None, ) -> None: """Initialize a group. @@ -503,25 +523,25 @@ class Group(Entity): """ self.hass = hass self._name = name - self._state = None + self._state: str | None = None self._icon = icon self._set_tracked(entity_ids) - self._on_off = None - self._assumed = None - self._on_states = None + self._on_off: dict[str, bool] = {} + self._assumed: dict[str, bool] = {} + self._on_states: set[str] = set() self.user_defined = user_defined self.mode = any if mode: self.mode = all self._order = order self._assumed_state = False - self._async_unsub_state_changed = None + self._async_unsub_state_changed: CALLBACK_TYPE | None = None @staticmethod def create_group( hass: HomeAssistant, name: str, - entity_ids: Iterable[str] | None = None, + entity_ids: Collection[str] | None = None, user_defined: bool = True, icon: str | None = None, object_id: str | None = None, @@ -541,7 +561,7 @@ class Group(Entity): def async_create_group_entity( hass: HomeAssistant, name: str, - entity_ids: Iterable[str] | None = None, + entity_ids: Collection[str] | None = None, user_defined: bool = True, icon: str | None = None, object_id: str | None = None, @@ -577,7 +597,7 @@ class Group(Entity): async def async_create_group( hass: HomeAssistant, name: str, - entity_ids: Iterable[str] | None = None, + entity_ids: Collection[str] | None = None, user_defined: bool = True, icon: str | None = None, object_id: str | None = None, @@ -597,37 +617,32 @@ class Group(Entity): return group @property - def should_poll(self): - """No need to poll because groups will update themselves.""" - return False - - @property - def name(self): + def name(self) -> str: """Return the name of the group.""" return self._name @name.setter - def name(self, value): + def name(self, value: str) -> None: """Set Group name.""" self._name = value @property - def state(self): + def state(self) -> str | None: """Return the state of the group.""" return self._state @property - def icon(self): + def icon(self) -> str | None: """Return the icon of the group.""" return self._icon @icon.setter - def icon(self, value): + def icon(self, value: str | None) -> None: """Set Icon for group.""" self._icon = value @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes for the group.""" data = {ATTR_ENTITY_ID: self.tracking, ATTR_ORDER: self._order} if not self.user_defined: @@ -636,17 +651,19 @@ class Group(Entity): return data @property - def assumed_state(self): + def assumed_state(self) -> bool: """Test if any member has an assumed state.""" return self._assumed_state - def update_tracked_entity_ids(self, entity_ids): + def update_tracked_entity_ids(self, entity_ids: Collection[str] | None) -> None: """Update the member entity IDs.""" asyncio.run_coroutine_threadsafe( self.async_update_tracked_entity_ids(entity_ids), self.hass.loop ).result() - async def async_update_tracked_entity_ids(self, entity_ids): + async def async_update_tracked_entity_ids( + self, entity_ids: Collection[str] | None + ) -> None: """Update the member entity IDs. This method must be run in the event loop. @@ -656,7 +673,7 @@ class Group(Entity): self._reset_tracked_state() self._async_start() - def _set_tracked(self, entity_ids): + def _set_tracked(self, entity_ids: Collection[str] | None) -> None: """Tuple of entities to be tracked.""" # tracking are the entities we want to track # trackable are the entities we actually watch @@ -666,10 +683,11 @@ class Group(Entity): self.trackable = () return - excluded_domains = self.hass.data[REG_KEY].exclude_domains + registry: GroupIntegrationRegistry = self.hass.data[REG_KEY] + excluded_domains = registry.exclude_domains - tracking = [] - trackable = [] + tracking: list[str] = [] + trackable: list[str] = [] for ent_id in entity_ids: ent_id_lower = ent_id.lower() domain = split_entity_id(ent_id_lower)[0] @@ -681,14 +699,14 @@ class Group(Entity): self.tracking = tuple(tracking) @callback - def _async_start(self, *_): + def _async_start(self, _: HomeAssistant | None = None) -> None: """Start tracking members and write state.""" self._reset_tracked_state() self._async_start_tracking() self.async_write_ha_state() @callback - def _async_start_tracking(self): + def _async_start_tracking(self) -> None: """Start tracking members. This method must be run in the event loop. @@ -701,7 +719,7 @@ class Group(Entity): self._async_update_group_state() @callback - def _async_stop(self): + def _async_stop(self) -> None: """Unregister the group from Home Assistant. This method must be run in the event loop. @@ -711,20 +729,20 @@ class Group(Entity): self._async_unsub_state_changed = None @callback - def async_update_group_state(self): + def async_update_group_state(self) -> None: """Query all members and determine current group state.""" self._state = None self._async_update_group_state() - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Handle addition to Home Assistant.""" self.async_on_remove(start.async_at_start(self.hass, self._async_start)) - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Handle removal from Home Assistant.""" self._async_stop() - async def _async_state_changed_listener(self, event): + async def _async_state_changed_listener(self, event: Event) -> None: """Respond to a member state changing. This method must be run in the event loop. @@ -742,7 +760,7 @@ class Group(Entity): self._async_update_group_state(new_state) self.async_write_ha_state() - def _reset_tracked_state(self): + def _reset_tracked_state(self) -> None: """Reset tracked state.""" self._on_off = {} self._assumed = {} @@ -752,13 +770,13 @@ class Group(Entity): if (state := self.hass.states.get(entity_id)) is not None: self._see_state(state) - def _see_state(self, new_state): + def _see_state(self, new_state: State) -> None: """Keep track of the the state.""" entity_id = new_state.entity_id domain = new_state.domain state = new_state.state - registry = self.hass.data[REG_KEY] - self._assumed[entity_id] = new_state.attributes.get(ATTR_ASSUMED_STATE) + registry: GroupIntegrationRegistry = self.hass.data[REG_KEY] + self._assumed[entity_id] = bool(new_state.attributes.get(ATTR_ASSUMED_STATE)) if domain not in registry.on_states_by_domain: # Handle the group of a group case @@ -769,12 +787,12 @@ class Group(Entity): self._on_off[entity_id] = state in registry.on_off_mapping else: entity_on_state = registry.on_states_by_domain[domain] - if domain in self.hass.data[REG_KEY].on_states_by_domain: + if domain in registry.on_states_by_domain: self._on_states.update(entity_on_state) self._on_off[entity_id] = state in entity_on_state @callback - def _async_update_group_state(self, tr_state=None): + def _async_update_group_state(self, tr_state: State | None = None) -> None: """Update group state. Optionally you can provide the only state changed since last update @@ -818,4 +836,5 @@ class Group(Entity): if group_is_on: self._state = on_state else: - self._state = self.hass.data[REG_KEY].on_off_mapping[on_state] + registry: GroupIntegrationRegistry = self.hass.data[REG_KEY] + self._state = registry.on_off_mapping[on_state] diff --git a/homeassistant/components/group/media_player.py b/homeassistant/components/group/media_player.py index cbce44a359a..ddb44072080 100644 --- a/homeassistant/components/group/media_player.py +++ b/homeassistant/components/group/media_player.py @@ -103,6 +103,7 @@ class MediaPlayerGroup(MediaPlayerEntity): """Representation of a Media Group.""" _attr_available: bool = False + _attr_should_poll = False def __init__(self, unique_id: str | None, name: str, entities: list[str]) -> None: """Initialize a Media Group entity.""" @@ -216,11 +217,6 @@ class MediaPlayerGroup(MediaPlayerEntity): """Flag supported features.""" return self._supported_features - @property - def should_poll(self) -> bool: - """No polling needed for a media group.""" - return False - @property def extra_state_attributes(self) -> dict: """Return the state attributes for the media group.""" diff --git a/homeassistant/components/group/notify.py b/homeassistant/components/group/notify.py index 97c019163e8..7c4bc0c65c4 100644 --- a/homeassistant/components/group/notify.py +++ b/homeassistant/components/group/notify.py @@ -1,7 +1,10 @@ """Group platform for notify component.""" +from __future__ import annotations + import asyncio -from collections.abc import Mapping +from collections.abc import Coroutine, Mapping from copy import deepcopy +from typing import Any import voluptuous as vol @@ -13,9 +16,9 @@ from homeassistant.components.notify import ( BaseNotificationService, ) from homeassistant.const import ATTR_SERVICE +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv - -# mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType CONF_SERVICES = "services" @@ -29,46 +32,50 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def update(input_dict, update_source): +def update(input_dict: dict[str, Any], update_source: dict[str, Any]) -> dict[str, Any]: """Deep update a dictionary. Async friendly. """ for key, val in update_source.items(): if isinstance(val, Mapping): - recurse = update(input_dict.get(key, {}), val) + recurse = update(input_dict.get(key, {}), val) # type: ignore[arg-type] input_dict[key] = recurse else: input_dict[key] = update_source[key] return input_dict -async def async_get_service(hass, config, discovery_info=None): +async def async_get_service( + hass: HomeAssistant, + config: ConfigType, + discovery_info: DiscoveryInfoType | None = None, +) -> GroupNotifyPlatform: """Get the Group notification service.""" - return GroupNotifyPlatform(hass, config.get(CONF_SERVICES)) + return GroupNotifyPlatform(hass, config[CONF_SERVICES]) class GroupNotifyPlatform(BaseNotificationService): """Implement the notification service for the group notify platform.""" - def __init__(self, hass, entities): + def __init__(self, hass: HomeAssistant, entities: list[dict[str, Any]]) -> None: """Initialize the service.""" self.hass = hass self.entities = entities - async def async_send_message(self, message="", **kwargs): + async def async_send_message(self, message: str = "", **kwargs: Any) -> None: """Send message to all entities in the group.""" - payload = {ATTR_MESSAGE: message} + payload: dict[str, Any] = {ATTR_MESSAGE: message} payload.update({key: val for key, val in kwargs.items() if val}) - tasks = [] + tasks: list[Coroutine[Any, Any, bool | None]] = [] for entity in self.entities: sending_payload = deepcopy(payload.copy()) - if entity.get(ATTR_DATA) is not None: - update(sending_payload, entity.get(ATTR_DATA)) + if (data := entity.get(ATTR_DATA)) is not None: + update(sending_payload, data) tasks.append( self.hass.services.async_call( - DOMAIN, entity.get(ATTR_SERVICE), sending_payload + DOMAIN, entity[ATTR_SERVICE], sending_payload ) ) From 13c7a7bbcc7fa949755cc5014908ba049e16231e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 Sep 2022 11:58:32 +0200 Subject: [PATCH 385/955] Refactor forked_daapd to use _async_announce (#78446) --- .../components/forked_daapd/media_player.py | 121 +++++++++--------- 1 file changed, 60 insertions(+), 61 deletions(-) diff --git a/homeassistant/components/forked_daapd/media_player.py b/homeassistant/components/forked_daapd/media_player.py index 429c0d7ab60..7ef7c3687c8 100644 --- a/homeassistant/components/forked_daapd/media_player.py +++ b/homeassistant/components/forked_daapd/media_player.py @@ -668,70 +668,69 @@ class ForkedDaapdMaster(MediaPlayerEntity): if media_type == MediaType.MUSIC: media_id = async_process_play_media_url(self.hass, media_id) - saved_state = self.state # save play state - saved_mute = self.is_volume_muted - sleep_future = asyncio.create_task( - asyncio.sleep(self._tts_pause_time) - ) # start timing now, but not exact because of fd buffer + tts latency - await self._pause_and_wait_for_callback() - await self._save_and_set_tts_volumes() - # save position - saved_song_position = self._player["item_progress_ms"] - saved_queue = ( - self._queue if self._queue["count"] > 0 else None - ) # stash queue - if saved_queue: - saved_queue_position = next( - i - for i, item in enumerate(saved_queue["items"]) - if item["id"] == self._player["item_id"] - ) - self._tts_requested = True - await sleep_future - await self._api.add_to_queue(uris=media_id, playback="start", clear=True) - try: - await asyncio.wait_for( - self._tts_playing_event.wait(), timeout=TTS_TIMEOUT - ) - # we have started TTS, now wait for completion - await asyncio.sleep( - self._queue["items"][0]["length_ms"] - / 1000 # player may not have updated yet so grab length from queue - + self._tts_pause_time - ) - except asyncio.TimeoutError: - self._tts_requested = False - _LOGGER.warning("TTS request timed out") - self._tts_playing_event.clear() - # TTS done, return to normal - await self.async_turn_on() # restore outputs and volumes - if saved_mute: # mute if we were muted - await self.async_mute_volume(True) - if self._use_pipe_control(): # resume pipe - await self._api.add_to_queue( - uris=self._sources_uris[self._source], clear=True - ) - if saved_state == MediaPlayerState.PLAYING: - await self.async_media_play() - else: # restore stashed queue - if saved_queue: - uris = "" - for item in saved_queue["items"]: - uris += item["uri"] + "," - await self._api.add_to_queue( - uris=uris, - playback="start", - playback_from_position=saved_queue_position, - clear=True, - ) - await self._api.seek(position_ms=saved_song_position) - if saved_state == MediaPlayerState.PAUSED: - await self.async_media_pause() - elif saved_state != MediaPlayerState.PLAYING: - await self.async_media_stop() + await self._async_announce(media_id) else: _LOGGER.debug("Media type '%s' not supported", media_type) + async def _async_announce(self, media_id: str): + saved_state = self.state # save play state + saved_mute = self.is_volume_muted + sleep_future = asyncio.create_task( + asyncio.sleep(self._tts_pause_time) + ) # start timing now, but not exact because of fd buffer + tts latency + await self._pause_and_wait_for_callback() + await self._save_and_set_tts_volumes() + # save position + saved_song_position = self._player["item_progress_ms"] + saved_queue = self._queue if self._queue["count"] > 0 else None # stash queue + if saved_queue: + saved_queue_position = next( + i + for i, item in enumerate(saved_queue["items"]) + if item["id"] == self._player["item_id"] + ) + self._tts_requested = True + await sleep_future + await self._api.add_to_queue(uris=media_id, playback="start", clear=True) + try: + await asyncio.wait_for(self._tts_playing_event.wait(), timeout=TTS_TIMEOUT) + # we have started TTS, now wait for completion + await asyncio.sleep( + self._queue["items"][0]["length_ms"] + / 1000 # player may not have updated yet so grab length from queue + + self._tts_pause_time + ) + except asyncio.TimeoutError: + self._tts_requested = False + _LOGGER.warning("TTS request timed out") + self._tts_playing_event.clear() + # TTS done, return to normal + await self.async_turn_on() # restore outputs and volumes + if saved_mute: # mute if we were muted + await self.async_mute_volume(True) + if self._use_pipe_control(): # resume pipe + await self._api.add_to_queue( + uris=self._sources_uris[self._source], clear=True + ) + if saved_state == MediaPlayerState.PLAYING: + await self.async_media_play() + else: # restore stashed queue + if saved_queue: + uris = "" + for item in saved_queue["items"]: + uris += item["uri"] + "," + await self._api.add_to_queue( + uris=uris, + playback="start", + playback_from_position=saved_queue_position, + clear=True, + ) + await self._api.seek(position_ms=saved_song_position) + if saved_state == MediaPlayerState.PAUSED: + await self.async_media_pause() + elif saved_state != MediaPlayerState.PLAYING: + await self.async_media_stop() + async def async_select_source(self, source: str) -> None: """Change source. From fad0b00fbc6fba2b2b2d8fed2431538113f91995 Mon Sep 17 00:00:00 2001 From: Ernst Klamer Date: Wed, 14 Sep 2022 12:09:03 +0200 Subject: [PATCH 386/955] Binary sensor description for BTHome (#78408) --- .../components/bthome/binary_sensor.py | 133 ++++++++++++++++-- homeassistant/components/bthome/manifest.json | 2 +- homeassistant/components/bthome/sensor.py | 84 ++++++----- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 169 insertions(+), 54 deletions(-) diff --git a/homeassistant/components/bthome/binary_sensor.py b/homeassistant/components/bthome/binary_sensor.py index 9a5ffb94afd..a048f9202b6 100644 --- a/homeassistant/components/bthome/binary_sensor.py +++ b/homeassistant/components/bthome/binary_sensor.py @@ -3,7 +3,10 @@ from __future__ import annotations from typing import Optional -from bthome_ble import BTHOME_BINARY_SENSORS, SensorUpdate +from bthome_ble import ( + BinarySensorDeviceClass as BTHomeBinarySensorDeviceClass, + SensorUpdate, +) from homeassistant import config_entries from homeassistant.components.binary_sensor import ( @@ -23,18 +26,119 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN from .device import device_key_to_bluetooth_entity_key, sensor_device_info_to_hass -BINARY_SENSOR_DESCRIPTIONS = {} -for key in BTHOME_BINARY_SENSORS: - # Not all BTHome device classes are available in Home Assistant - DEV_CLASS: str | None = key - try: - BinarySensorDeviceClass(key) - except ValueError: - DEV_CLASS = None - BINARY_SENSOR_DESCRIPTIONS[key] = BinarySensorEntityDescription( - key=key, - device_class=DEV_CLASS, - ) +BINARY_SENSOR_DESCRIPTIONS = { + BTHomeBinarySensorDeviceClass.BATTERY: BinarySensorEntityDescription( + key=BTHomeBinarySensorDeviceClass.BATTERY, + device_class=BinarySensorDeviceClass.BATTERY, + ), + BTHomeBinarySensorDeviceClass.BATTERY_CHARGING: BinarySensorEntityDescription( + key=BTHomeBinarySensorDeviceClass.BATTERY_CHARGING, + device_class=BinarySensorDeviceClass.BATTERY_CHARGING, + ), + BTHomeBinarySensorDeviceClass.CO: BinarySensorEntityDescription( + key=BTHomeBinarySensorDeviceClass.CO, + device_class=BinarySensorDeviceClass.CO, + ), + BTHomeBinarySensorDeviceClass.COLD: BinarySensorEntityDescription( + key=BTHomeBinarySensorDeviceClass.COLD, + device_class=BinarySensorDeviceClass.COLD, + ), + BTHomeBinarySensorDeviceClass.CONNECTIVITY: BinarySensorEntityDescription( + key=BTHomeBinarySensorDeviceClass.CONNECTIVITY, + device_class=BinarySensorDeviceClass.CONNECTIVITY, + ), + BTHomeBinarySensorDeviceClass.DOOR: BinarySensorEntityDescription( + key=BTHomeBinarySensorDeviceClass.DOOR, + device_class=BinarySensorDeviceClass.DOOR, + ), + BTHomeBinarySensorDeviceClass.HEAT: BinarySensorEntityDescription( + key=BTHomeBinarySensorDeviceClass.HEAT, + device_class=BinarySensorDeviceClass.HEAT, + ), + BTHomeBinarySensorDeviceClass.GARAGE_DOOR: BinarySensorEntityDescription( + key=BTHomeBinarySensorDeviceClass.GARAGE_DOOR, + device_class=BinarySensorDeviceClass.GARAGE_DOOR, + ), + BTHomeBinarySensorDeviceClass.GAS: BinarySensorEntityDescription( + key=BTHomeBinarySensorDeviceClass.GAS, + device_class=BinarySensorDeviceClass.GAS, + ), + BTHomeBinarySensorDeviceClass.GENERIC: BinarySensorEntityDescription( + key=BTHomeBinarySensorDeviceClass.GENERIC, + ), + BTHomeBinarySensorDeviceClass.LIGHT: BinarySensorEntityDescription( + key=BTHomeBinarySensorDeviceClass.LIGHT, + device_class=BinarySensorDeviceClass.LIGHT, + ), + BTHomeBinarySensorDeviceClass.LOCK: BinarySensorEntityDescription( + key=BTHomeBinarySensorDeviceClass.LOCK, + device_class=BinarySensorDeviceClass.LOCK, + ), + BTHomeBinarySensorDeviceClass.MOISTURE: BinarySensorEntityDescription( + key=BTHomeBinarySensorDeviceClass.MOISTURE, + device_class=BinarySensorDeviceClass.MOISTURE, + ), + BTHomeBinarySensorDeviceClass.MOTION: BinarySensorEntityDescription( + key=BTHomeBinarySensorDeviceClass.MOTION, + device_class=BinarySensorDeviceClass.MOTION, + ), + BTHomeBinarySensorDeviceClass.MOVING: BinarySensorEntityDescription( + key=BTHomeBinarySensorDeviceClass.MOVING, + device_class=BinarySensorDeviceClass.MOVING, + ), + BTHomeBinarySensorDeviceClass.OCCUPANCY: BinarySensorEntityDescription( + key=BTHomeBinarySensorDeviceClass.OCCUPANCY, + device_class=BinarySensorDeviceClass.OCCUPANCY, + ), + BTHomeBinarySensorDeviceClass.OPENING: BinarySensorEntityDescription( + key=BTHomeBinarySensorDeviceClass.OPENING, + device_class=BinarySensorDeviceClass.OPENING, + ), + BTHomeBinarySensorDeviceClass.PLUG: BinarySensorEntityDescription( + key=BTHomeBinarySensorDeviceClass.PLUG, + device_class=BinarySensorDeviceClass.PLUG, + ), + BTHomeBinarySensorDeviceClass.POWER: BinarySensorEntityDescription( + key=BTHomeBinarySensorDeviceClass.POWER, + device_class=BinarySensorDeviceClass.POWER, + ), + BTHomeBinarySensorDeviceClass.PRESENCE: BinarySensorEntityDescription( + key=BTHomeBinarySensorDeviceClass.PRESENCE, + device_class=BinarySensorDeviceClass.PRESENCE, + ), + BTHomeBinarySensorDeviceClass.PROBLEM: BinarySensorEntityDescription( + key=BTHomeBinarySensorDeviceClass.PROBLEM, + device_class=BinarySensorDeviceClass.PROBLEM, + ), + BTHomeBinarySensorDeviceClass.RUNNING: BinarySensorEntityDescription( + key=BTHomeBinarySensorDeviceClass.RUNNING, + device_class=BinarySensorDeviceClass.RUNNING, + ), + BTHomeBinarySensorDeviceClass.SAFETY: BinarySensorEntityDescription( + key=BTHomeBinarySensorDeviceClass.SAFETY, + device_class=BinarySensorDeviceClass.SAFETY, + ), + BTHomeBinarySensorDeviceClass.SMOKE: BinarySensorEntityDescription( + key=BTHomeBinarySensorDeviceClass.SMOKE, + device_class=BinarySensorDeviceClass.SMOKE, + ), + BTHomeBinarySensorDeviceClass.SOUND: BinarySensorEntityDescription( + key=BTHomeBinarySensorDeviceClass.SOUND, + device_class=BinarySensorDeviceClass.SOUND, + ), + BTHomeBinarySensorDeviceClass.TAMPER: BinarySensorEntityDescription( + key=BTHomeBinarySensorDeviceClass.TAMPER, + device_class=BinarySensorDeviceClass.TAMPER, + ), + BTHomeBinarySensorDeviceClass.VIBRATION: BinarySensorEntityDescription( + key=BTHomeBinarySensorDeviceClass.VIBRATION, + device_class=BinarySensorDeviceClass.VIBRATION, + ), + BTHomeBinarySensorDeviceClass.WINDOW: BinarySensorEntityDescription( + key=BTHomeBinarySensorDeviceClass.WINDOW, + device_class=BinarySensorDeviceClass.WINDOW, + ), +} def sensor_update_to_bluetooth_data_update( @@ -48,9 +152,10 @@ def sensor_update_to_bluetooth_data_update( }, entity_descriptions={ device_key_to_bluetooth_entity_key(device_key): BINARY_SENSOR_DESCRIPTIONS[ - description.device_key.key + description.device_class ] for device_key, description in sensor_update.binary_entity_descriptions.items() + if description.device_class }, entity_data={ device_key_to_bluetooth_entity_key(device_key): sensor_values.native_value diff --git a/homeassistant/components/bthome/manifest.json b/homeassistant/components/bthome/manifest.json index 5b58581226b..3b4cbe2f4f4 100644 --- a/homeassistant/components/bthome/manifest.json +++ b/homeassistant/components/bthome/manifest.json @@ -13,7 +13,7 @@ "service_data_uuid": "0000181e-0000-1000-8000-00805f9b34fb" } ], - "requirements": ["bthome-ble==1.2.0"], + "requirements": ["bthome-ble==1.2.2"], "dependencies": ["bluetooth"], "codeowners": ["@Ernst79"], "iot_class": "local_push" diff --git a/homeassistant/components/bthome/sensor.py b/homeassistant/components/bthome/sensor.py index cfb516d07b9..d80763d4600 100644 --- a/homeassistant/components/bthome/sensor.py +++ b/homeassistant/components/bthome/sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import Optional, Union -from bthome_ble import DeviceClass, SensorUpdate, Units +from bthome_ble import SensorDeviceClass as BTHomeSensorDeviceClass, SensorUpdate, Units from homeassistant import config_entries from homeassistant.components.bluetooth.passive_update_processor import ( @@ -40,93 +40,102 @@ from .const import DOMAIN from .device import device_key_to_bluetooth_entity_key, sensor_device_info_to_hass SENSOR_DESCRIPTIONS = { - (DeviceClass.TEMPERATURE, Units.TEMP_CELSIUS): SensorEntityDescription( - key=f"{DeviceClass.TEMPERATURE}_{Units.TEMP_CELSIUS}", + (BTHomeSensorDeviceClass.TEMPERATURE, Units.TEMP_CELSIUS): SensorEntityDescription( + key=f"{BTHomeSensorDeviceClass.TEMPERATURE}_{Units.TEMP_CELSIUS}", device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=TEMP_CELSIUS, state_class=SensorStateClass.MEASUREMENT, ), - (DeviceClass.HUMIDITY, Units.PERCENTAGE): SensorEntityDescription( - key=f"{DeviceClass.HUMIDITY}_{Units.PERCENTAGE}", + (BTHomeSensorDeviceClass.HUMIDITY, Units.PERCENTAGE): SensorEntityDescription( + key=f"{BTHomeSensorDeviceClass.HUMIDITY}_{Units.PERCENTAGE}", device_class=SensorDeviceClass.HUMIDITY, native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, ), - (DeviceClass.ILLUMINANCE, Units.LIGHT_LUX): SensorEntityDescription( - key=f"{DeviceClass.ILLUMINANCE}_{Units.LIGHT_LUX}", + (BTHomeSensorDeviceClass.ILLUMINANCE, Units.LIGHT_LUX): SensorEntityDescription( + key=f"{BTHomeSensorDeviceClass.ILLUMINANCE}_{Units.LIGHT_LUX}", device_class=SensorDeviceClass.ILLUMINANCE, native_unit_of_measurement=LIGHT_LUX, state_class=SensorStateClass.MEASUREMENT, ), - (DeviceClass.PRESSURE, Units.PRESSURE_MBAR): SensorEntityDescription( - key=f"{DeviceClass.PRESSURE}_{Units.PRESSURE_MBAR}", + (BTHomeSensorDeviceClass.PRESSURE, Units.PRESSURE_MBAR): SensorEntityDescription( + key=f"{BTHomeSensorDeviceClass.PRESSURE}_{Units.PRESSURE_MBAR}", device_class=SensorDeviceClass.PRESSURE, native_unit_of_measurement=PRESSURE_MBAR, state_class=SensorStateClass.MEASUREMENT, ), - (DeviceClass.BATTERY, Units.PERCENTAGE): SensorEntityDescription( - key=f"{DeviceClass.BATTERY}_{Units.PERCENTAGE}", + (BTHomeSensorDeviceClass.BATTERY, Units.PERCENTAGE): SensorEntityDescription( + key=f"{BTHomeSensorDeviceClass.BATTERY}_{Units.PERCENTAGE}", 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), + ( + BTHomeSensorDeviceClass.VOLTAGE, + Units.ELECTRIC_POTENTIAL_VOLT, + ): SensorEntityDescription( + key=f"{BTHomeSensorDeviceClass.VOLTAGE}_{Units.ELECTRIC_POTENTIAL_VOLT}", device_class=SensorDeviceClass.VOLTAGE, native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, state_class=SensorStateClass.MEASUREMENT, ), - (DeviceClass.ENERGY, Units.ENERGY_KILO_WATT_HOUR): SensorEntityDescription( - key=str(Units.ENERGY_KILO_WATT_HOUR), + ( + BTHomeSensorDeviceClass.ENERGY, + Units.ENERGY_KILO_WATT_HOUR, + ): SensorEntityDescription( + key=f"{BTHomeSensorDeviceClass.ENERGY}_{Units.ENERGY_KILO_WATT_HOUR}", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, state_class=SensorStateClass.TOTAL_INCREASING, ), - (DeviceClass.POWER, Units.POWER_WATT): SensorEntityDescription( - key=str(Units.POWER_WATT), + (BTHomeSensorDeviceClass.POWER, Units.POWER_WATT): SensorEntityDescription( + key=f"{BTHomeSensorDeviceClass.POWER}_{Units.POWER_WATT}", device_class=SensorDeviceClass.POWER, native_unit_of_measurement=POWER_WATT, state_class=SensorStateClass.MEASUREMENT, ), ( - DeviceClass.PM10, + BTHomeSensorDeviceClass.PM10, Units.CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, ): SensorEntityDescription( - key=f"{DeviceClass.PM10}_{Units.CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}", + key=f"{BTHomeSensorDeviceClass.PM10}_{Units.CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}", device_class=SensorDeviceClass.PM10, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=SensorStateClass.MEASUREMENT, ), ( - DeviceClass.PM25, + BTHomeSensorDeviceClass.PM25, Units.CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, ): SensorEntityDescription( - key=f"{DeviceClass.PM25}_{Units.CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}", + key=f"{BTHomeSensorDeviceClass.PM25}_{Units.CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}", device_class=SensorDeviceClass.PM25, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=SensorStateClass.MEASUREMENT, ), - (DeviceClass.CO2, Units.CONCENTRATION_PARTS_PER_MILLION,): SensorEntityDescription( - key=f"{DeviceClass.CO2}_{Units.CONCENTRATION_PARTS_PER_MILLION}", + ( + BTHomeSensorDeviceClass.CO2, + Units.CONCENTRATION_PARTS_PER_MILLION, + ): SensorEntityDescription( + key=f"{BTHomeSensorDeviceClass.CO2}_{Units.CONCENTRATION_PARTS_PER_MILLION}", device_class=SensorDeviceClass.CO2, native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, state_class=SensorStateClass.MEASUREMENT, ), ( - DeviceClass.VOLATILE_ORGANIC_COMPOUNDS, + BTHomeSensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, Units.CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, ): SensorEntityDescription( - key=f"{DeviceClass.VOLATILE_ORGANIC_COMPOUNDS}_{Units.CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}", + key=f"{BTHomeSensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS}_{Units.CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}", device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=SensorStateClass.MEASUREMENT, ), ( - DeviceClass.SIGNAL_STRENGTH, + BTHomeSensorDeviceClass.SIGNAL_STRENGTH, Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ): SensorEntityDescription( - key=f"{DeviceClass.SIGNAL_STRENGTH}_{Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT}", + key=f"{BTHomeSensorDeviceClass.SIGNAL_STRENGTH}_{Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT}", device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, state_class=SensorStateClass.MEASUREMENT, @@ -134,36 +143,36 @@ SENSOR_DESCRIPTIONS = { entity_registry_enabled_default=False, ), # Used for mass sensor with kg unit - (None, Units.MASS_KILOGRAMS): SensorEntityDescription( - key=f"{DeviceClass.MASS}_{Units.MASS_KILOGRAMS}", + (BTHomeSensorDeviceClass.MASS, Units.MASS_KILOGRAMS): SensorEntityDescription( + key=f"{BTHomeSensorDeviceClass.MASS}_{Units.MASS_KILOGRAMS}", device_class=None, native_unit_of_measurement=MASS_KILOGRAMS, state_class=SensorStateClass.MEASUREMENT, ), # Used for mass sensor with lb unit - (None, Units.MASS_POUNDS): SensorEntityDescription( - key=f"{DeviceClass.MASS}_{Units.MASS_POUNDS}", + (BTHomeSensorDeviceClass.MASS, Units.MASS_POUNDS): SensorEntityDescription( + key=f"{BTHomeSensorDeviceClass.MASS}_{Units.MASS_POUNDS}", device_class=None, native_unit_of_measurement=MASS_POUNDS, state_class=SensorStateClass.MEASUREMENT, ), # Used for moisture sensor - (DeviceClass.MOISTURE, Units.PERCENTAGE): SensorEntityDescription( - key=f"{DeviceClass.MOISTURE}_{Units.PERCENTAGE}", + (BTHomeSensorDeviceClass.MOISTURE, Units.PERCENTAGE): SensorEntityDescription( + key=f"{BTHomeSensorDeviceClass.MOISTURE}_{Units.PERCENTAGE}", device_class=SensorDeviceClass.MOISTURE, native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, ), # Used for dew point sensor - (None, Units.TEMP_CELSIUS): SensorEntityDescription( - key=f"{DeviceClass.DEW_POINT}_{Units.TEMP_CELSIUS}", + (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, state_class=SensorStateClass.MEASUREMENT, ), # Used for count sensor - (None, None): SensorEntityDescription( - key=f"{DeviceClass.COUNT}", + (BTHomeSensorDeviceClass.COUNT, None): SensorEntityDescription( + key=f"{BTHomeSensorDeviceClass.COUNT}", device_class=None, native_unit_of_measurement=None, state_class=SensorStateClass.MEASUREMENT, @@ -185,6 +194,7 @@ def sensor_update_to_bluetooth_data_update( (description.device_class, description.native_unit_of_measurement) ] for device_key, description in sensor_update.entity_descriptions.items() + if description.device_class }, entity_data={ device_key_to_bluetooth_entity_key(device_key): sensor_values.native_value diff --git a/requirements_all.txt b/requirements_all.txt index d2bf8734c2b..66f55dae3de 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -464,7 +464,7 @@ bsblan==0.5.0 bt_proximity==0.2.1 # homeassistant.components.bthome -bthome-ble==1.2.0 +bthome-ble==1.2.2 # homeassistant.components.bt_home_hub_5 bthomehub5-devicelist==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8376456cb53..04f2b06be54 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -365,7 +365,7 @@ brunt==1.2.0 bsblan==0.5.0 # homeassistant.components.bthome -bthome-ble==1.2.0 +bthome-ble==1.2.2 # homeassistant.components.buienradar buienradar==1.0.5 From 26251895295d74fcd2c73e37804c23675c433247 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 Sep 2022 12:24:54 +0200 Subject: [PATCH 387/955] Use async_timeout in forked_daapd (#78451) --- homeassistant/components/forked_daapd/media_player.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/forked_daapd/media_player.py b/homeassistant/components/forked_daapd/media_player.py index 7ef7c3687c8..e11c7aaeb06 100644 --- a/homeassistant/components/forked_daapd/media_player.py +++ b/homeassistant/components/forked_daapd/media_player.py @@ -6,6 +6,7 @@ from collections import defaultdict import logging from typing import Any +import async_timeout from pyforked_daapd import ForkedDaapdAPI from pylibrespot_java import LibrespotJavaAPI @@ -647,9 +648,8 @@ class ForkedDaapdMaster(MediaPlayerEntity): self._pause_requested = True await self.async_media_pause() try: - await asyncio.wait_for( - self._paused_event.wait(), timeout=CALLBACK_TIMEOUT - ) # wait for paused + async with async_timeout.timeout(CALLBACK_TIMEOUT): + await self._paused_event.wait() # wait for paused except asyncio.TimeoutError: self._pause_requested = False self._paused_event.clear() @@ -693,7 +693,8 @@ class ForkedDaapdMaster(MediaPlayerEntity): await sleep_future await self._api.add_to_queue(uris=media_id, playback="start", clear=True) try: - await asyncio.wait_for(self._tts_playing_event.wait(), timeout=TTS_TIMEOUT) + async with async_timeout.timeout(TTS_TIMEOUT): + await self._tts_playing_event.wait() # we have started TTS, now wait for completion await asyncio.sleep( self._queue["items"][0]["length_ms"] From db44be7054124b453898dbf354cc60a066116dcf Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 Sep 2022 12:26:22 +0200 Subject: [PATCH 388/955] Sort coveragerc (#78447) --- .coveragerc | 172 ++++++++++++++++++++++++++-------------------------- 1 file changed, 86 insertions(+), 86 deletions(-) diff --git a/.coveragerc b/.coveragerc index 6f4fa46864c..0a37c94d57b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -8,9 +8,6 @@ omit = # omit pieces of code that rely on external devices being present homeassistant/components/acer_projector/* - homeassistant/components/actiontec/const.py - homeassistant/components/actiontec/device_tracker.py - homeassistant/components/actiontec/model.py homeassistant/components/acmeda/__init__.py homeassistant/components/acmeda/base.py homeassistant/components/acmeda/const.py @@ -19,6 +16,9 @@ omit = homeassistant/components/acmeda/helpers.py homeassistant/components/acmeda/hub.py homeassistant/components/acmeda/sensor.py + homeassistant/components/actiontec/const.py + homeassistant/components/actiontec/device_tracker.py + homeassistant/components/actiontec/model.py homeassistant/components/adax/__init__.py homeassistant/components/adax/climate.py homeassistant/components/adguard/__init__.py @@ -69,8 +69,8 @@ omit = homeassistant/components/apple_tv/remote.py homeassistant/components/aqualogic/* homeassistant/components/aquostv/media_player.py - homeassistant/components/arcam_fmj/media_player.py homeassistant/components/arcam_fmj/__init__.py + homeassistant/components/arcam_fmj/media_player.py homeassistant/components/arest/binary_sensor.py homeassistant/components/arest/sensor.py homeassistant/components/arest/switch.py @@ -107,9 +107,9 @@ omit = homeassistant/components/baf/switch.py homeassistant/components/baidu/tts.py homeassistant/components/balboa/__init__.py - homeassistant/components/beewi_smartclim/sensor.py homeassistant/components/bbox/device_tracker.py homeassistant/components/bbox/sensor.py + homeassistant/components/beewi_smartclim/sensor.py homeassistant/components/bitcoin/sensor.py homeassistant/components/bizkaibus/sensor.py homeassistant/components/blink/__init__.py @@ -153,8 +153,8 @@ omit = homeassistant/components/brottsplatskartan/sensor.py homeassistant/components/browser/* homeassistant/components/brunt/__init__.py - homeassistant/components/brunt/cover.py homeassistant/components/brunt/const.py + homeassistant/components/brunt/cover.py homeassistant/components/bsblan/climate.py homeassistant/components/bt_home_hub_5/device_tracker.py homeassistant/components/bt_smarthub/device_tracker.py @@ -181,20 +181,20 @@ omit = homeassistant/components/concord232/alarm_control_panel.py homeassistant/components/concord232/binary_sensor.py homeassistant/components/control4/__init__.py - homeassistant/components/control4/light.py homeassistant/components/control4/const.py homeassistant/components/control4/director_utils.py + homeassistant/components/control4/light.py homeassistant/components/coolmaster/__init__.py homeassistant/components/coolmaster/climate.py homeassistant/components/coolmaster/const.py homeassistant/components/cppm_tracker/device_tracker.py homeassistant/components/crownstone/__init__.py homeassistant/components/crownstone/const.py - homeassistant/components/crownstone/listeners.py - homeassistant/components/crownstone/helpers.py homeassistant/components/crownstone/devices.py homeassistant/components/crownstone/entry_manager.py + homeassistant/components/crownstone/helpers.py homeassistant/components/crownstone/light.py + homeassistant/components/crownstone/listeners.py homeassistant/components/cups/sensor.py homeassistant/components/currencylayer/sensor.py homeassistant/components/daikin/__init__.py @@ -261,15 +261,15 @@ omit = homeassistant/components/econet/const.py homeassistant/components/econet/sensor.py homeassistant/components/econet/water_heater.py + homeassistant/components/ecovacs/* homeassistant/components/ecowitt/__init__.py homeassistant/components/ecowitt/binary_sensor.py homeassistant/components/ecowitt/diagnostics.py homeassistant/components/ecowitt/entity.py homeassistant/components/ecowitt/sensor.py - homeassistant/components/ecovacs/* - homeassistant/components/edl21/* homeassistant/components/eddystone_temperature/sensor.py homeassistant/components/edimax/switch.py + homeassistant/components/edl21/* homeassistant/components/egardia/* homeassistant/components/eight_sleep/__init__.py homeassistant/components/eight_sleep/binary_sensor.py @@ -285,9 +285,9 @@ omit = homeassistant/components/elkm1/sensor.py homeassistant/components/elkm1/switch.py homeassistant/components/elmax/__init__.py + homeassistant/components/elmax/binary_sensor.py homeassistant/components/elmax/common.py homeassistant/components/elmax/const.py - homeassistant/components/elmax/binary_sensor.py homeassistant/components/elmax/switch.py homeassistant/components/elv/* homeassistant/components/emby/media_player.py @@ -318,9 +318,9 @@ omit = homeassistant/components/epson/media_player.py homeassistant/components/epsonworkforce/sensor.py homeassistant/components/eq3btsmart/climate.py + homeassistant/components/escea/__init__.py homeassistant/components/escea/climate.py homeassistant/components/escea/discovery.py - homeassistant/components/escea/__init__.py homeassistant/components/esphome/__init__.py homeassistant/components/esphome/binary_sensor.py homeassistant/components/esphome/bluetooth.py @@ -342,16 +342,16 @@ omit = homeassistant/components/everlights/light.py homeassistant/components/evohome/* homeassistant/components/ezviz/__init__.py - homeassistant/components/ezviz/camera.py - homeassistant/components/ezviz/coordinator.py - homeassistant/components/ezviz/const.py - homeassistant/components/ezviz/entity.py homeassistant/components/ezviz/binary_sensor.py + homeassistant/components/ezviz/camera.py + homeassistant/components/ezviz/const.py + homeassistant/components/ezviz/coordinator.py + homeassistant/components/ezviz/entity.py homeassistant/components/ezviz/sensor.py homeassistant/components/ezviz/switch.py - homeassistant/components/familyhub/camera.py homeassistant/components/faa_delays/__init__.py homeassistant/components/faa_delays/binary_sensor.py + homeassistant/components/familyhub/camera.py homeassistant/components/fastdotcom/* homeassistant/components/ffmpeg/camera.py homeassistant/components/fibaro/__init__.py @@ -424,8 +424,8 @@ omit = homeassistant/components/fritz/services.py homeassistant/components/fritz/switch.py homeassistant/components/fritzbox_callmonitor/__init__.py - homeassistant/components/fritzbox_callmonitor/const.py homeassistant/components/fritzbox_callmonitor/base.py + homeassistant/components/fritzbox_callmonitor/const.py homeassistant/components/fritzbox_callmonitor/sensor.py homeassistant/components/frontier_silicon/const.py homeassistant/components/frontier_silicon/media_player.py @@ -461,8 +461,8 @@ omit = homeassistant/components/gpsd/sensor.py homeassistant/components/greenwave/light.py homeassistant/components/group/notify.py - homeassistant/components/growatt_server/sensor.py homeassistant/components/growatt_server/__init__.py + homeassistant/components/growatt_server/sensor.py homeassistant/components/gstreamer/media_player.py homeassistant/components/gtfs/sensor.py homeassistant/components/guardian/__init__.py @@ -508,9 +508,9 @@ omit = homeassistant/components/home_connect/light.py homeassistant/components/home_connect/sensor.py homeassistant/components/home_connect/switch.py - homeassistant/components/homematic/* homeassistant/components/home_plus_control/api.py homeassistant/components/home_plus_control/switch.py + homeassistant/components/homematic/* homeassistant/components/homeworks/* homeassistant/components/honeywell/__init__.py homeassistant/components/honeywell/climate.py @@ -534,9 +534,9 @@ omit = homeassistant/components/hunterdouglas_powerview/sensor.py homeassistant/components/hunterdouglas_powerview/shade_data.py homeassistant/components/hunterdouglas_powerview/util.py + homeassistant/components/hvv_departures/__init__.py homeassistant/components/hvv_departures/binary_sensor.py homeassistant/components/hvv_departures/sensor.py - homeassistant/components/hvv_departures/__init__.py homeassistant/components/hydrawise/* homeassistant/components/ialarm/alarm_control_panel.py homeassistant/components/iammeter/sensor.py @@ -549,9 +549,6 @@ omit = homeassistant/components/icloud/account.py homeassistant/components/icloud/device_tracker.py homeassistant/components/icloud/sensor.py - homeassistant/components/izone/climate.py - homeassistant/components/izone/discovery.py - homeassistant/components/izone/__init__.py homeassistant/components/idteck_prox/* homeassistant/components/ifttt/__init__.py homeassistant/components/ifttt/alarm_control_panel.py @@ -560,6 +557,7 @@ omit = homeassistant/components/ihc/* homeassistant/components/imap/sensor.py homeassistant/components/imap_email_content/sensor.py + homeassistant/components/incomfort/* homeassistant/components/insteon/binary_sensor.py homeassistant/components/insteon/climate.py homeassistant/components/insteon/const.py @@ -572,13 +570,12 @@ omit = homeassistant/components/insteon/switch.py homeassistant/components/insteon/utils.py homeassistant/components/intellifire/__init__.py - homeassistant/components/intellifire/coordinator.py - homeassistant/components/intellifire/climate.py homeassistant/components/intellifire/binary_sensor.py + homeassistant/components/intellifire/climate.py + homeassistant/components/intellifire/coordinator.py + homeassistant/components/intellifire/entity.py homeassistant/components/intellifire/sensor.py homeassistant/components/intellifire/switch.py - homeassistant/components/intellifire/entity.py - homeassistant/components/incomfort/* homeassistant/components/intesishome/* homeassistant/components/ios/__init__.py homeassistant/components/ios/notify.py @@ -604,6 +601,9 @@ omit = homeassistant/components/isy994/util.py homeassistant/components/itach/remote.py homeassistant/components/itunes/media_player.py + homeassistant/components/izone/__init__.py + homeassistant/components/izone/climate.py + homeassistant/components/izone/discovery.py homeassistant/components/jellyfin/__init__.py homeassistant/components/jellyfin/media_source.py homeassistant/components/joaoapps_join/* @@ -687,13 +687,13 @@ omit = homeassistant/components/logi_circle/sensor.py homeassistant/components/london_underground/sensor.py homeassistant/components/lookin/__init__.py + homeassistant/components/lookin/climate.py homeassistant/components/lookin/coordinator.py homeassistant/components/lookin/entity.py + homeassistant/components/lookin/light.py + homeassistant/components/lookin/media_player.py homeassistant/components/lookin/models.py homeassistant/components/lookin/sensor.py - homeassistant/components/lookin/climate.py - homeassistant/components/lookin/media_player.py - homeassistant/components/lookin/light.py homeassistant/components/luci/device_tracker.py homeassistant/components/luftdaten/sensor.py homeassistant/components/lupusec/* @@ -778,9 +778,11 @@ omit = homeassistant/components/mullvad/binary_sensor.py homeassistant/components/mutesync/__init__.py homeassistant/components/mutesync/binary_sensor.py - homeassistant/components/nest/const.py homeassistant/components/mvglive/sensor.py homeassistant/components/mycroft/* + homeassistant/components/myq/__init__.py + homeassistant/components/myq/cover.py + homeassistant/components/myq/light.py homeassistant/components/mysensors/__init__.py homeassistant/components/mysensors/binary_sensor.py homeassistant/components/mysensors/climate.py @@ -796,9 +798,6 @@ omit = homeassistant/components/mystrom/binary_sensor.py homeassistant/components/mystrom/light.py homeassistant/components/mystrom/switch.py - homeassistant/components/myq/__init__.py - homeassistant/components/myq/cover.py - homeassistant/components/myq/light.py homeassistant/components/nad/media_player.py homeassistant/components/nanoleaf/__init__.py homeassistant/components/nanoleaf/button.py @@ -814,6 +813,7 @@ omit = homeassistant/components/neato/switch.py homeassistant/components/neato/vacuum.py homeassistant/components/nederlandse_spoorwegen/sensor.py + homeassistant/components/nest/const.py homeassistant/components/nest/legacy/* homeassistant/components/netdata/sensor.py homeassistant/components/netgear/__init__.py @@ -826,8 +826,8 @@ omit = homeassistant/components/netgear_lte/* homeassistant/components/netio/switch.py homeassistant/components/neurio_energy/sensor.py - homeassistant/components/nexia/entity.py homeassistant/components/nexia/climate.py + homeassistant/components/nexia/entity.py homeassistant/components/nexia/switch.py homeassistant/components/nextcloud/* homeassistant/components/nfandroidtv/__init__.py @@ -838,26 +838,26 @@ omit = homeassistant/components/nmap_tracker/__init__.py homeassistant/components/nmap_tracker/device_tracker.py homeassistant/components/nmbs/sensor.py + homeassistant/components/noaa_tides/sensor.py homeassistant/components/nobo_hub/__init__.py homeassistant/components/nobo_hub/climate.py + homeassistant/components/norway_air/air_quality.py + homeassistant/components/notify_events/notify.py homeassistant/components/notion/__init__.py homeassistant/components/notion/binary_sensor.py homeassistant/components/notion/sensor.py - homeassistant/components/noaa_tides/sensor.py - homeassistant/components/norway_air/air_quality.py - homeassistant/components/notify_events/notify.py homeassistant/components/nsw_fuel_station/sensor.py homeassistant/components/nuki/__init__.py - homeassistant/components/nuki/const.py homeassistant/components/nuki/binary_sensor.py + homeassistant/components/nuki/const.py homeassistant/components/nuki/lock.py homeassistant/components/nut/diagnostics.py homeassistant/components/nx584/alarm_control_panel.py homeassistant/components/nzbget/coordinator.py + homeassistant/components/oasa_telematics/sensor.py homeassistant/components/obihai/* homeassistant/components/octoprint/__init__.py homeassistant/components/oem/climate.py - homeassistant/components/oasa_telematics/sensor.py homeassistant/components/ohmconnect/sensor.py homeassistant/components/ombi/* homeassistant/components/omnilogic/__init__.py @@ -891,8 +891,8 @@ omit = homeassistant/components/opengarage/entity.py homeassistant/components/opengarage/sensor.py homeassistant/components/openhome/__init__.py - homeassistant/components/openhome/media_player.py homeassistant/components/openhome/const.py + homeassistant/components/openhome/media_player.py homeassistant/components/opensensemap/air_quality.py homeassistant/components/opensky/sensor.py homeassistant/components/opentherm_gw/__init__.py @@ -917,9 +917,9 @@ omit = homeassistant/components/overkiz/button.py homeassistant/components/overkiz/climate.py homeassistant/components/overkiz/climate_entities/* + homeassistant/components/overkiz/coordinator.py homeassistant/components/overkiz/cover.py homeassistant/components/overkiz/cover_entities/* - homeassistant/components/overkiz/coordinator.py homeassistant/components/overkiz/diagnostics.py homeassistant/components/overkiz/entity.py homeassistant/components/overkiz/executor.py @@ -948,8 +948,8 @@ omit = homeassistant/components/picotts/tts.py homeassistant/components/pilight/* homeassistant/components/ping/__init__.py - homeassistant/components/ping/const.py homeassistant/components/ping/binary_sensor.py + homeassistant/components/ping/const.py homeassistant/components/ping/device_tracker.py homeassistant/components/pioneer/media_player.py homeassistant/components/pjlink/media_player.py @@ -968,13 +968,13 @@ omit = homeassistant/components/point/binary_sensor.py homeassistant/components/point/sensor.py homeassistant/components/poolsense/__init__.py - homeassistant/components/poolsense/sensor.py homeassistant/components/poolsense/binary_sensor.py + homeassistant/components/poolsense/sensor.py homeassistant/components/powerwall/__init__.py - homeassistant/components/proliphix/climate.py homeassistant/components/progettihwsw/__init__.py homeassistant/components/progettihwsw/binary_sensor.py homeassistant/components/progettihwsw/switch.py + homeassistant/components/proliphix/climate.py homeassistant/components/prowl/notify.py homeassistant/components/proxmoxve/* homeassistant/components/proxy/camera.py @@ -999,10 +999,10 @@ omit = homeassistant/components/radio_browser/__init__.py homeassistant/components/radio_browser/media_source.py homeassistant/components/radiotherm/__init__.py - homeassistant/components/radiotherm/entity.py homeassistant/components/radiotherm/climate.py homeassistant/components/radiotherm/coordinator.py homeassistant/components/radiotherm/data.py + homeassistant/components/radiotherm/entity.py homeassistant/components/radiotherm/switch.py homeassistant/components/radiotherm/util.py homeassistant/components/rainbird/* @@ -1023,9 +1023,9 @@ omit = homeassistant/components/reddit/* homeassistant/components/rejseplanen/sensor.py homeassistant/components/remember_the_milk/__init__.py + homeassistant/components/remote_rpi_gpio/* homeassistant/components/repetier/__init__.py homeassistant/components/repetier/sensor.py - homeassistant/components/remote_rpi_gpio/* homeassistant/components/rest/notify.py homeassistant/components/rest/switch.py homeassistant/components/rfxtrx/diagnostics.py @@ -1087,8 +1087,6 @@ omit = homeassistant/components/sesame/lock.py homeassistant/components/seven_segments/image_processing.py homeassistant/components/seventeentrack/sensor.py - homeassistant/components/shiftr/* - homeassistant/components/shodan/sensor.py homeassistant/components/shelly/__init__.py homeassistant/components/shelly/binary_sensor.py homeassistant/components/shelly/climate.py @@ -1097,6 +1095,15 @@ omit = homeassistant/components/shelly/number.py homeassistant/components/shelly/sensor.py homeassistant/components/shelly/utils.py + homeassistant/components/shiftr/* + homeassistant/components/shodan/sensor.py + homeassistant/components/sia/__init__.py + homeassistant/components/sia/alarm_control_panel.py + homeassistant/components/sia/binary_sensor.py + homeassistant/components/sia/const.py + homeassistant/components/sia/hub.py + homeassistant/components/sia/sia_entity_base.py + homeassistant/components/sia/utils.py homeassistant/components/sigfox/sensor.py homeassistant/components/simplepush/__init__.py homeassistant/components/simplepush/notify.py @@ -1106,6 +1113,7 @@ omit = homeassistant/components/simplisafe/lock.py homeassistant/components/simplisafe/sensor.py homeassistant/components/simulated/sensor.py + homeassistant/components/sinch/* homeassistant/components/sisyphus/* homeassistant/components/sky_hub/* homeassistant/components/skybeacon/sensor.py @@ -1119,15 +1127,9 @@ omit = homeassistant/components/skybell/switch.py homeassistant/components/slack/__init__.py homeassistant/components/slack/notify.py - homeassistant/components/sia/__init__.py - homeassistant/components/sia/alarm_control_panel.py - homeassistant/components/sia/binary_sensor.py - homeassistant/components/sia/const.py - homeassistant/components/sia/hub.py - homeassistant/components/sia/utils.py - homeassistant/components/sia/sia_entity_base.py - homeassistant/components/sinch/* homeassistant/components/slide/* + homeassistant/components/slimproto/__init__.py + homeassistant/components/slimproto/media_player.py homeassistant/components/sma/__init__.py homeassistant/components/sma/sensor.py homeassistant/components/smappee/__init__.py @@ -1182,8 +1184,6 @@ omit = homeassistant/components/spotify/media_player.py homeassistant/components/spotify/system_health.py homeassistant/components/spotify/util.py - homeassistant/components/slimproto/__init__.py - homeassistant/components/slimproto/media_player.py homeassistant/components/squeezebox/__init__.py homeassistant/components/squeezebox/browse_media.py homeassistant/components/squeezebox/media_player.py @@ -1205,22 +1205,26 @@ omit = homeassistant/components/streamlabswater/* homeassistant/components/suez_water/* homeassistant/components/supervisord/sensor.py + homeassistant/components/supla/* homeassistant/components/surepetcare/__init__.py - homeassistant/components/surepetcare/entity.py homeassistant/components/surepetcare/binary_sensor.py + homeassistant/components/surepetcare/entity.py homeassistant/components/surepetcare/sensor.py homeassistant/components/swiss_hydrological_data/sensor.py homeassistant/components/swiss_public_transport/sensor.py homeassistant/components/swisscom/device_tracker.py - homeassistant/components/switchbot/switch.py - homeassistant/components/switchbot/binary_sensor.py + homeassistant/components/switchbee/__init__.py + homeassistant/components/switchbee/const.py + homeassistant/components/switchbee/switch.py homeassistant/components/switchbot/__init__.py + homeassistant/components/switchbot/binary_sensor.py homeassistant/components/switchbot/const.py - homeassistant/components/switchbot/entity.py + homeassistant/components/switchbot/coordinator.py homeassistant/components/switchbot/cover.py + homeassistant/components/switchbot/entity.py homeassistant/components/switchbot/light.py homeassistant/components/switchbot/sensor.py - homeassistant/components/switchbot/coordinator.py + homeassistant/components/switchbot/switch.py homeassistant/components/switchmate/switch.py homeassistant/components/syncthing/__init__.py homeassistant/components/syncthing/sensor.py @@ -1232,9 +1236,9 @@ omit = homeassistant/components/synology_dsm/binary_sensor.py homeassistant/components/synology_dsm/button.py homeassistant/components/synology_dsm/camera.py + homeassistant/components/synology_dsm/common.py homeassistant/components/synology_dsm/coordinator.py homeassistant/components/synology_dsm/diagnostics.py - homeassistant/components/synology_dsm/common.py homeassistant/components/synology_dsm/entity.py homeassistant/components/synology_dsm/sensor.py homeassistant/components/synology_dsm/service.py @@ -1320,8 +1324,8 @@ omit = homeassistant/components/totalconnect/const.py homeassistant/components/touchline/climate.py homeassistant/components/tplink_lte/* - homeassistant/components/traccar/device_tracker.py homeassistant/components/traccar/const.py + homeassistant/components/traccar/device_tracker.py homeassistant/components/tractive/__init__.py homeassistant/components/tractive/binary_sensor.py homeassistant/components/tractive/device_tracker.py @@ -1342,10 +1346,10 @@ omit = homeassistant/components/trafikverket_weatherstation/__init__.py homeassistant/components/trafikverket_weatherstation/coordinator.py homeassistant/components/trafikverket_weatherstation/sensor.py - homeassistant/components/transmission/sensor.py - homeassistant/components/transmission/switch.py homeassistant/components/transmission/const.py homeassistant/components/transmission/errors.py + homeassistant/components/transmission/sensor.py + homeassistant/components/transmission/switch.py homeassistant/components/travisci/sensor.py homeassistant/components/tuya/__init__.py homeassistant/components/tuya/alarm_control_panel.py @@ -1374,20 +1378,20 @@ omit = homeassistant/components/ubus/device_tracker.py homeassistant/components/ue_smart_radio/media_player.py homeassistant/components/ukraine_alarm/__init__.py - homeassistant/components/ukraine_alarm/const.py homeassistant/components/ukraine_alarm/binary_sensor.py + homeassistant/components/ukraine_alarm/const.py homeassistant/components/unifiled/* homeassistant/components/upb/__init__.py homeassistant/components/upb/const.py homeassistant/components/upb/light.py homeassistant/components/upb/scene.py + homeassistant/components/upc_connect/* homeassistant/components/upcloud/__init__.py homeassistant/components/upcloud/binary_sensor.py homeassistant/components/upcloud/switch.py homeassistant/components/upnp/__init__.py homeassistant/components/upnp/device.py homeassistant/components/upnp/sensor.py - homeassistant/components/upc_connect/* homeassistant/components/uscis/sensor.py homeassistant/components/vallox/__init__.py homeassistant/components/vallox/fan.py @@ -1426,17 +1430,17 @@ omit = homeassistant/components/vesync/sensor.py homeassistant/components/vesync/switch.py homeassistant/components/viaggiatreno/sensor.py + homeassistant/components/vicare/__init__.py homeassistant/components/vicare/binary_sensor.py homeassistant/components/vicare/button.py homeassistant/components/vicare/climate.py homeassistant/components/vicare/const.py homeassistant/components/vicare/diagnostics.py - homeassistant/components/vicare/__init__.py homeassistant/components/vicare/sensor.py homeassistant/components/vicare/water_heater.py homeassistant/components/vilfo/__init__.py - homeassistant/components/vilfo/sensor.py homeassistant/components/vilfo/const.py + homeassistant/components/vilfo/sensor.py homeassistant/components/vivotek/camera.py homeassistant/components/vlc/media_player.py homeassistant/components/vlc_telnet/__init__.py @@ -1469,8 +1473,8 @@ omit = homeassistant/components/wiffi/wiffi_strings.py homeassistant/components/wirelesstag/* homeassistant/components/wolflink/__init__.py - homeassistant/components/wolflink/sensor.py homeassistant/components/wolflink/const.py + homeassistant/components/wolflink/sensor.py homeassistant/components/worldtidesinfo/sensor.py homeassistant/components/worxlandroid/sensor.py homeassistant/components/x10/light.py @@ -1513,12 +1517,6 @@ omit = homeassistant/components/xiaomi_tv/media_player.py homeassistant/components/xmpp/notify.py homeassistant/components/xs1/* - homeassistant/components/yalexs_ble/__init__.py - homeassistant/components/yalexs_ble/binary_sensor.py - homeassistant/components/yalexs_ble/entity.py - homeassistant/components/yalexs_ble/lock.py - homeassistant/components/yalexs_ble/sensor.py - homeassistant/components/yalexs_ble/util.py homeassistant/components/yale_smart_alarm/__init__.py homeassistant/components/yale_smart_alarm/alarm_control_panel.py homeassistant/components/yale_smart_alarm/binary_sensor.py @@ -1528,6 +1526,12 @@ omit = homeassistant/components/yale_smart_alarm/diagnostics.py homeassistant/components/yale_smart_alarm/entity.py homeassistant/components/yale_smart_alarm/lock.py + homeassistant/components/yalexs_ble/__init__.py + homeassistant/components/yalexs_ble/binary_sensor.py + homeassistant/components/yalexs_ble/entity.py + homeassistant/components/yalexs_ble/lock.py + homeassistant/components/yalexs_ble/sensor.py + homeassistant/components/yalexs_ble/util.py homeassistant/components/yamaha_musiccast/__init__.py homeassistant/components/yamaha_musiccast/media_player.py homeassistant/components/yamaha_musiccast/number.py @@ -1571,14 +1575,13 @@ omit = homeassistant/components/zhong_hong/climate.py homeassistant/components/ziggo_mediabox_xl/media_player.py homeassistant/components/zoneminder/* - homeassistant/components/supla/* homeassistant/components/zwave_js/discovery.py homeassistant/components/zwave_js/sensor.py homeassistant/components/zwave_me/__init__.py homeassistant/components/zwave_me/binary_sensor.py homeassistant/components/zwave_me/button.py - homeassistant/components/zwave_me/cover.py homeassistant/components/zwave_me/climate.py + homeassistant/components/zwave_me/cover.py homeassistant/components/zwave_me/fan.py homeassistant/components/zwave_me/helpers.py homeassistant/components/zwave_me/light.py @@ -1587,9 +1590,6 @@ omit = homeassistant/components/zwave_me/sensor.py homeassistant/components/zwave_me/siren.py homeassistant/components/zwave_me/switch.py - homeassistant/components/switchbee/__init__.py - homeassistant/components/switchbee/const.py - homeassistant/components/switchbee/switch.py [report] # Regexes for lines to exclude from consideration From b7e9fcb9fefc2b83d892f6ca319c210b3dc21f11 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 14 Sep 2022 12:29:43 +0200 Subject: [PATCH 389/955] Replace asyncio.wait_for with async_timeout in baf (#78445) --- homeassistant/components/baf/__init__.py | 4 +++- homeassistant/components/baf/config_flow.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/baf/__init__.py b/homeassistant/components/baf/__init__.py index 7e80341deab..c9e51c79b82 100644 --- a/homeassistant/components/baf/__init__.py +++ b/homeassistant/components/baf/__init__.py @@ -5,6 +5,7 @@ import asyncio from aiobafi6 import Device, Service from aiobafi6.discovery import PORT +import async_timeout from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_IP_ADDRESS, Platform @@ -34,7 +35,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: run_future = device.async_run() try: - await asyncio.wait_for(device.async_wait_available(), timeout=RUN_TIMEOUT) + async with async_timeout.timeout(RUN_TIMEOUT): + await device.async_wait_available() except asyncio.TimeoutError as ex: run_future.cancel() raise ConfigEntryNotReady(f"Timed out connecting to {ip_address}") from ex diff --git a/homeassistant/components/baf/config_flow.py b/homeassistant/components/baf/config_flow.py index 2326d30937b..3f37df1b70a 100644 --- a/homeassistant/components/baf/config_flow.py +++ b/homeassistant/components/baf/config_flow.py @@ -7,6 +7,7 @@ from typing import Any from aiobafi6 import Device, Service from aiobafi6.discovery import PORT +import async_timeout import voluptuous as vol from homeassistant import config_entries @@ -26,7 +27,8 @@ async def async_try_connect(ip_address: str) -> Device: device = Device(Service(ip_addresses=[ip_address], port=PORT)) run_future = device.async_run() try: - await asyncio.wait_for(device.async_wait_available(), timeout=RUN_TIMEOUT) + async with async_timeout.timeout(RUN_TIMEOUT): + await device.async_wait_available() except asyncio.TimeoutError as ex: raise CannotConnect from ex finally: From 5e338d21665cb04f66fcebd9376cdda389c30c01 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 Sep 2022 13:04:09 +0200 Subject: [PATCH 390/955] Improve type hints in automation (#78368) * Improve type hints in automation * Apply suggestion * Apply suggestion * Apply suggestion * Add Protocol for IfAction * Use ConfigType for IfAction * Rename variable --- .../components/automation/__init__.py | 130 ++++++++++-------- homeassistant/components/automation/config.py | 36 +++-- homeassistant/components/automation/trace.py | 22 +-- 3 files changed, 108 insertions(+), 80 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 154c443e799..a5ea30f59d2 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -1,9 +1,9 @@ """Allow to set up simple automation rules via the config file.""" from __future__ import annotations -from collections.abc import Callable +from collections.abc import Callable, Mapping import logging -from typing import Any, cast +from typing import Any, Protocol, cast import voluptuous as vol from voluptuous.humanize import humanize_error @@ -31,9 +31,12 @@ from homeassistant.const import ( STATE_ON, ) from homeassistant.core import ( + CALLBACK_TYPE, Context, CoreState, + Event, HomeAssistant, + ServiceCall, callback, split_entity_id, valid_entity_id, @@ -99,9 +102,6 @@ from .const import ( from .helpers import async_get_blueprints from .trace import trace_automation -# mypy: allow-untyped-calls, allow-untyped-defs -# mypy: no-check-untyped-defs, no-warn-return-any - ENTITY_ID_FORMAT = DOMAIN + ".{}" @@ -120,6 +120,15 @@ SERVICE_TRIGGER = "trigger" _LOGGER = logging.getLogger(__name__) +class IfAction(Protocol): + """Define the format of if_action.""" + + config: list[ConfigType] + + def __call__(self, variables: Mapping[str, Any] | None = None) -> bool: + """AND all conditions.""" + + # AutomationActionType, AutomationTriggerData, # and AutomationTriggerInfo are deprecated as of 2022.9. AutomationActionType = TriggerActionType @@ -128,7 +137,7 @@ AutomationTriggerInfo = TriggerInfo @bind_hass -def is_on(hass, entity_id): +def is_on(hass: HomeAssistant, entity_id: str) -> bool: """ Return true if specified automation entity_id is on. @@ -143,12 +152,12 @@ def automations_with_entity(hass: HomeAssistant, entity_id: str) -> list[str]: if DOMAIN not in hass.data: return [] - component = hass.data[DOMAIN] + component: EntityComponent = hass.data[DOMAIN] return [ automation_entity.entity_id for automation_entity in component.entities - if entity_id in automation_entity.referenced_entities + if entity_id in cast(AutomationEntity, automation_entity).referenced_entities ] @@ -158,12 +167,12 @@ def entities_in_automation(hass: HomeAssistant, entity_id: str) -> list[str]: if DOMAIN not in hass.data: return [] - component = hass.data[DOMAIN] + component: EntityComponent = hass.data[DOMAIN] if (automation_entity := component.get_entity(entity_id)) is None: return [] - return list(automation_entity.referenced_entities) + return list(cast(AutomationEntity, automation_entity).referenced_entities) @callback @@ -172,12 +181,12 @@ def automations_with_device(hass: HomeAssistant, device_id: str) -> list[str]: if DOMAIN not in hass.data: return [] - component = hass.data[DOMAIN] + component: EntityComponent = hass.data[DOMAIN] return [ automation_entity.entity_id for automation_entity in component.entities - if device_id in automation_entity.referenced_devices + if device_id in cast(AutomationEntity, automation_entity).referenced_devices ] @@ -187,12 +196,12 @@ def devices_in_automation(hass: HomeAssistant, entity_id: str) -> list[str]: if DOMAIN not in hass.data: return [] - component = hass.data[DOMAIN] + component: EntityComponent = hass.data[DOMAIN] if (automation_entity := component.get_entity(entity_id)) is None: return [] - return list(automation_entity.referenced_devices) + return list(cast(AutomationEntity, automation_entity).referenced_devices) @callback @@ -201,12 +210,12 @@ def automations_with_area(hass: HomeAssistant, area_id: str) -> list[str]: if DOMAIN not in hass.data: return [] - component = hass.data[DOMAIN] + component: EntityComponent = hass.data[DOMAIN] return [ automation_entity.entity_id for automation_entity in component.entities - if area_id in automation_entity.referenced_areas + if area_id in cast(AutomationEntity, automation_entity).referenced_areas ] @@ -216,12 +225,12 @@ def areas_in_automation(hass: HomeAssistant, entity_id: str) -> list[str]: if DOMAIN not in hass.data: return [] - component = hass.data[DOMAIN] + component: EntityComponent = hass.data[DOMAIN] if (automation_entity := component.get_entity(entity_id)) is None: return [] - return list(automation_entity.referenced_areas) + return list(cast(AutomationEntity, automation_entity).referenced_areas) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @@ -238,7 +247,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if not await _async_process_config(hass, config, component): await async_get_blueprints(hass).async_populate() - async def trigger_service_handler(entity, service_call): + async def trigger_service_handler( + entity: AutomationEntity, service_call: ServiceCall + ) -> None: """Handle forced automation trigger, e.g. from frontend.""" await entity.async_trigger( {**service_call.data[ATTR_VARIABLES], "trigger": {"platform": None}}, @@ -262,7 +273,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: "async_turn_off", ) - async def reload_service_handler(service_call): + async def reload_service_handler(service_call: ServiceCall) -> None: """Remove all automations and load new ones from config.""" if (conf := await component.async_prepare_reload()) is None: return @@ -290,22 +301,22 @@ class AutomationEntity(ToggleEntity, RestoreEntity): def __init__( self, - automation_id, - name, - trigger_config, - cond_func, - action_script, - initial_state, - variables, - trigger_variables, - raw_config, - blueprint_inputs, - trace_config, - ): + automation_id: str | None, + name: str, + trigger_config: list[ConfigType], + cond_func: IfAction | None, + action_script: Script, + initial_state: bool | None, + variables: ScriptVariables | None, + trigger_variables: ScriptVariables | None, + raw_config: ConfigType | None, + blueprint_inputs: ConfigType | None, + trace_config: ConfigType, + ) -> None: """Initialize an automation entity.""" self._attr_name = name self._trigger_config = trigger_config - self._async_detach_triggers = None + self._async_detach_triggers: CALLBACK_TYPE | None = None self._cond_func = cond_func self.action_script = action_script self.action_script.change_listener = self.async_write_ha_state @@ -314,15 +325,15 @@ class AutomationEntity(ToggleEntity, RestoreEntity): self._referenced_entities: set[str] | None = None self._referenced_devices: set[str] | None = None self._logger = LOGGER - self._variables: ScriptVariables = variables - self._trigger_variables: ScriptVariables = trigger_variables + self._variables = variables + self._trigger_variables = trigger_variables self._raw_config = raw_config self._blueprint_inputs = blueprint_inputs self._trace_config = trace_config self._attr_unique_id = automation_id @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the entity state attributes.""" attrs = { ATTR_LAST_TRIGGERED: self.action_script.last_triggered, @@ -341,12 +352,12 @@ class AutomationEntity(ToggleEntity, RestoreEntity): return self._async_detach_triggers is not None or self._is_enabled @property - def referenced_areas(self): + def referenced_areas(self) -> set[str]: """Return a set of referenced areas.""" return self.action_script.referenced_areas @property - def referenced_devices(self): + def referenced_devices(self) -> set[str]: """Return a set of referenced devices.""" if self._referenced_devices is not None: return self._referenced_devices @@ -364,7 +375,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): return referenced @property - def referenced_entities(self): + def referenced_entities(self) -> set[str]: """Return a set of referenced entities.""" if self._referenced_entities is not None: return self._referenced_entities @@ -513,7 +524,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): event_data[ATTR_SOURCE] = variables["trigger"]["description"] @callback - def started_action(): + def started_action() -> None: self.hass.bus.async_fire( EVENT_AUTOMATION_TRIGGERED, event_data, context=trigger_context ) @@ -555,12 +566,12 @@ class AutomationEntity(ToggleEntity, RestoreEntity): self._logger.exception("While executing automation %s", self.entity_id) automation_trace.set_error(err) - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Remove listeners when removing automation from Home Assistant.""" await super().async_will_remove_from_hass() await self.async_disable() - async def async_enable(self): + async def async_enable(self) -> None: """Enable this automation entity. This method is a coroutine. @@ -576,7 +587,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): self.async_write_ha_state() return - async def async_enable_automation(event): + async def async_enable_automation(event: Event) -> None: """Start automation on startup.""" # Don't do anything if no longer enabled or already attached if not self._is_enabled or self._async_detach_triggers is not None: @@ -589,7 +600,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): ) self.async_write_ha_state() - async def async_disable(self, stop_actions=DEFAULT_STOP_ACTIONS): + async def async_disable(self, stop_actions: bool = DEFAULT_STOP_ACTIONS) -> None: """Disable the automation entity.""" if not self._is_enabled and not self.action_script.runs: return @@ -610,7 +621,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): ) -> Callable[[], None] | None: """Set up the triggers.""" - def log_cb(level, msg, **kwargs): + def log_cb(level: int, msg: str, **kwargs: Any) -> None: self._logger.log(level, "%s %s", msg, self.name, **kwargs) this = None @@ -650,7 +661,7 @@ async def _async_process_config( Returns if blueprints were used. """ - entities = [] + entities: list[AutomationEntity] = [] blueprints_used = False for config_key in extract_domain_configs(config, DOMAIN): @@ -681,10 +692,10 @@ async def _async_process_config( else: raw_config = cast(AutomationConfig, config_block).raw_config - automation_id = config_block.get(CONF_ID) - name = config_block.get(CONF_ALIAS) or f"{config_key} {list_no}" + automation_id: str | None = config_block.get(CONF_ID) + name: str = config_block.get(CONF_ALIAS) or f"{config_key} {list_no}" - initial_state = config_block.get(CONF_INITIAL_STATE) + initial_state: bool | None = config_block.get(CONF_INITIAL_STATE) action_script = Script( hass, @@ -743,11 +754,13 @@ async def _async_process_config( return blueprints_used -async def _async_process_if(hass, name, config, p_config): +async def _async_process_if( + hass: HomeAssistant, name: str, config: dict[str, Any], p_config: dict[str, Any] +) -> IfAction | None: """Process if checks.""" if_configs = p_config[CONF_CONDITION] - checks = [] + checks: list[condition.ConditionCheckerType] = [] for if_config in if_configs: try: checks.append(await condition.async_from_config(hass, if_config)) @@ -755,9 +768,9 @@ async def _async_process_if(hass, name, config, p_config): LOGGER.warning("Invalid condition: %s", ex) return None - def if_action(variables=None): + def if_action(variables: Mapping[str, Any] | None = None) -> bool: """AND all conditions.""" - errors = [] + errors: list[ConditionErrorIndex] = [] for index, check in enumerate(checks): try: with trace_path(["condition", str(index)]): @@ -780,9 +793,10 @@ async def _async_process_if(hass, name, config, p_config): return True - if_action.config = if_configs + result: IfAction = if_action # type: ignore[assignment] + result.config = if_configs - return if_action + return result @callback @@ -800,7 +814,7 @@ def _trigger_extract_devices(trigger_conf: dict) -> list[str]: return [trigger_conf[CONF_EVENT_DATA][CONF_DEVICE_ID]] if trigger_conf[CONF_PLATFORM] == "tag" and CONF_DEVICE_ID in trigger_conf: - return trigger_conf[CONF_DEVICE_ID] + return trigger_conf[CONF_DEVICE_ID] # type: ignore[no-any-return] return [] @@ -809,13 +823,13 @@ def _trigger_extract_devices(trigger_conf: dict) -> list[str]: def _trigger_extract_entities(trigger_conf: dict) -> list[str]: """Extract entities from a trigger config.""" if trigger_conf[CONF_PLATFORM] in ("state", "numeric_state"): - return trigger_conf[CONF_ENTITY_ID] + return trigger_conf[CONF_ENTITY_ID] # type: ignore[no-any-return] if trigger_conf[CONF_PLATFORM] == "calendar": return [trigger_conf[CONF_ENTITY_ID]] if trigger_conf[CONF_PLATFORM] == "zone": - return trigger_conf[CONF_ENTITY_ID] + [trigger_conf[CONF_ZONE]] + return trigger_conf[CONF_ENTITY_ID] + [trigger_conf[CONF_ZONE]] # type: ignore[no-any-return] if trigger_conf[CONF_PLATFORM] == "geo_location": return [trigger_conf[CONF_ZONE]] diff --git a/homeassistant/components/automation/config.py b/homeassistant/components/automation/config.py index 228e78ac446..ec35e617b07 100644 --- a/homeassistant/components/automation/config.py +++ b/homeassistant/components/automation/config.py @@ -1,6 +1,9 @@ """Config validation helper for the automation integration.""" +from __future__ import annotations + import asyncio from contextlib import suppress +from typing import Any import voluptuous as vol @@ -17,10 +20,12 @@ from homeassistant.const import ( CONF_ID, CONF_VARIABLES, ) +from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_per_platform, config_validation as cv, script from homeassistant.helpers.condition import async_validate_conditions_config from homeassistant.helpers.trigger import async_validate_trigger_config +from homeassistant.helpers.typing import ConfigType from homeassistant.loader import IntegrationNotFound from .const import ( @@ -34,9 +39,6 @@ from .const import ( ) from .helpers import async_get_blueprints -# mypy: allow-untyped-calls, allow-untyped-defs -# mypy: no-check-untyped-defs, no-warn-return-any - PACKAGE_MERGE_HINT = "list" _CONDITION_SCHEMA = vol.All(cv.ensure_list, [cv.CONDITION_SCHEMA]) @@ -63,7 +65,11 @@ PLATFORM_SCHEMA = vol.All( ) -async def async_validate_config_item(hass, config, full_config=None): +async def async_validate_config_item( + hass: HomeAssistant, + config: ConfigType, + full_config: ConfigType | None = None, +) -> blueprint.BlueprintInputs | dict[str, Any]: """Validate config item.""" if blueprint.is_blueprint_instance_config(config): blueprints = async_get_blueprints(hass) @@ -90,17 +96,21 @@ async def async_validate_config_item(hass, config, full_config=None): class AutomationConfig(dict): """Dummy class to allow adding attributes.""" - raw_config = None + raw_config: dict[str, Any] | None = None -async def _try_async_validate_config_item(hass, config, full_config=None): +async def _try_async_validate_config_item( + hass: HomeAssistant, + config: dict[str, Any], + full_config: dict[str, Any] | None = None, +) -> AutomationConfig | blueprint.BlueprintInputs | None: """Validate config item.""" raw_config = None with suppress(ValueError): raw_config = dict(config) try: - config = await async_validate_config_item(hass, config, full_config) + validated_config = await async_validate_config_item(hass, config, full_config) except ( vol.Invalid, HomeAssistantError, @@ -110,15 +120,15 @@ async def _try_async_validate_config_item(hass, config, full_config=None): async_log_exception(ex, DOMAIN, full_config or config, hass) return None - if isinstance(config, blueprint.BlueprintInputs): - return config + if isinstance(validated_config, blueprint.BlueprintInputs): + return validated_config - config = AutomationConfig(config) - config.raw_config = raw_config - return config + automation_config = AutomationConfig(validated_config) + automation_config.raw_config = raw_config + return automation_config -async def async_validate_config(hass, config): +async def async_validate_config(hass: HomeAssistant, config: ConfigType) -> ConfigType: """Validate config.""" automations = list( filter( diff --git a/homeassistant/components/automation/trace.py b/homeassistant/components/automation/trace.py index b302f99d036..ae0d0339bfa 100644 --- a/homeassistant/components/automation/trace.py +++ b/homeassistant/components/automation/trace.py @@ -1,6 +1,7 @@ """Trace support for automation.""" from __future__ import annotations +from collections.abc import Generator from contextlib import contextmanager from typing import Any @@ -9,13 +10,11 @@ from homeassistant.components.trace import ( ActionTrace, async_store_trace, ) -from homeassistant.core import Context +from homeassistant.core import Context, HomeAssistant +from homeassistant.helpers.typing import ConfigType from .const import DOMAIN -# mypy: allow-untyped-calls, allow-untyped-defs -# mypy: no-check-untyped-defs, no-warn-return-any - class AutomationTrace(ActionTrace): """Container for automation trace.""" @@ -24,9 +23,9 @@ class AutomationTrace(ActionTrace): def __init__( self, - item_id: str, - config: dict[str, Any], - blueprint_inputs: dict[str, Any], + item_id: str | None, + config: ConfigType | None, + blueprint_inputs: ConfigType | None, context: Context, ) -> None: """Container for automation trace.""" @@ -49,8 +48,13 @@ class AutomationTrace(ActionTrace): @contextmanager def trace_automation( - hass, automation_id, config, blueprint_inputs, context, trace_config -): + hass: HomeAssistant, + automation_id: str | None, + config: ConfigType | None, + blueprint_inputs: ConfigType | None, + context: Context, + trace_config: ConfigType, +) -> Generator[AutomationTrace, None, None]: """Trace action execution of automation with automation_id.""" trace = AutomationTrace(automation_id, config, blueprint_inputs, context) async_store_trace(hass, trace, trace_config[CONF_STORED_TRACES]) From efb482fb1dcf29468e50fca98f046d551d6355c7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 Sep 2022 13:05:00 +0200 Subject: [PATCH 391/955] Add demo to strict-typing (#77596) * Add demo to strict-typing * Adjust component * Adjust PR * Update homeassistant/components/demo/mailbox.py Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> --- .strict-typing | 1 + homeassistant/components/demo/alarm_control_panel.py | 2 +- homeassistant/components/demo/mailbox.py | 2 +- mypy.ini | 10 ++++++++++ 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/.strict-typing b/.strict-typing index a565e04e3e5..0cbb1aa92e9 100644 --- a/.strict-typing +++ b/.strict-typing @@ -81,6 +81,7 @@ homeassistant.components.cpuspeed.* homeassistant.components.deconz.* homeassistant.components.device_automation.* homeassistant.components.device_tracker.* +homeassistant.components.demo.* homeassistant.components.devolo_home_control.* homeassistant.components.devolo_home_network.* homeassistant.components.dhcp.* diff --git a/homeassistant/components/demo/alarm_control_panel.py b/homeassistant/components/demo/alarm_control_panel.py index b73e5444b22..3a94aaa7c29 100644 --- a/homeassistant/components/demo/alarm_control_panel.py +++ b/homeassistant/components/demo/alarm_control_panel.py @@ -31,7 +31,7 @@ async def async_setup_platform( """Set up the Demo alarm control panel platform.""" async_add_entities( [ - ManualAlarm( + ManualAlarm( # type:ignore[no-untyped-call] hass, "Security", "1234", diff --git a/homeassistant/components/demo/mailbox.py b/homeassistant/components/demo/mailbox.py index 8a7df70df80..6f0b23525e5 100644 --- a/homeassistant/components/demo/mailbox.py +++ b/homeassistant/components/demo/mailbox.py @@ -76,7 +76,7 @@ class DemoMailbox(Mailbox): """Return a list of the current messages.""" return sorted( self._messages.values(), - key=lambda item: item["info"]["origtime"], + key=lambda item: item["info"]["origtime"], # type: ignore[no-any-return] reverse=True, ) diff --git a/mypy.ini b/mypy.ini index 3c886bdc1c2..1218992ab3a 100644 --- a/mypy.ini +++ b/mypy.ini @@ -569,6 +569,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.demo.*] +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.devolo_home_control.*] check_untyped_defs = true disallow_incomplete_defs = true From ad25a966a8bd592cf140d29f18d81b726ffdcc1b Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Wed, 14 Sep 2022 19:24:51 +0800 Subject: [PATCH 392/955] Sort constants in forked_daapd (#78455) --- homeassistant/components/forked_daapd/const.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/forked_daapd/const.py b/homeassistant/components/forked_daapd/const.py index f0d915ce3e5..85b51b3b6ae 100644 --- a/homeassistant/components/forked_daapd/const.py +++ b/homeassistant/components/forked_daapd/const.py @@ -1,6 +1,7 @@ """Const for forked-daapd.""" from homeassistant.components.media_player import MediaPlayerEntityFeature +CALLBACK_TIMEOUT = 8 # max time between command and callback from forked-daapd server CAN_PLAY_TYPE = { "audio/mp4", "audio/aac", @@ -11,8 +12,6 @@ CAN_PLAY_TYPE = { "audio/aiff", "audio/wav", } - -CALLBACK_TIMEOUT = 8 # max time between command and callback from forked-daapd server CONF_LIBRESPOT_JAVA_PORT = "librespot_java_port" CONF_MAX_PLAYLISTS = "max_playlists" CONF_TTS_PAUSE_TIME = "tts_pause_time" From 374b72f05280281ca16d4810c20b6815a58d5971 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 Sep 2022 13:30:17 +0200 Subject: [PATCH 393/955] Make LimitedSizeDict a generic (#78440) * Make LimitedSizeDict a generic * Remove comments * Use super() * Apply suggestion Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> --- homeassistant/components/trace/utils.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/trace/utils.py b/homeassistant/components/trace/utils.py index 50d1590e4fd..a12618af0ec 100644 --- a/homeassistant/components/trace/utils.py +++ b/homeassistant/components/trace/utils.py @@ -1,22 +1,28 @@ """Helpers for script and automation tracing and debugging.""" +from __future__ import annotations + from collections import OrderedDict +from typing import Any, TypeVar + +_KT = TypeVar("_KT") +_VT = TypeVar("_VT") -class LimitedSizeDict(OrderedDict): +class LimitedSizeDict(OrderedDict[_KT, _VT]): """OrderedDict limited in size.""" - def __init__(self, *args, **kwds): + def __init__(self, *args: Any, **kwds: Any) -> None: """Initialize OrderedDict limited in size.""" self.size_limit = kwds.pop("size_limit", None) - OrderedDict.__init__(self, *args, **kwds) + super().__init__(*args, **kwds) self._check_size_limit() - def __setitem__(self, key, value): + def __setitem__(self, key: _KT, value: _VT) -> None: """Set item and check dict size.""" - OrderedDict.__setitem__(self, key, value) + super().__setitem__(key, value) self._check_size_limit() - def _check_size_limit(self): + def _check_size_limit(self) -> None: """Check dict size and evict items in FIFO order if needed.""" if self.size_limit is not None: while len(self) > self.size_limit: From 1fcab3365316e5818867fa075b8d0cf2deb9e034 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 Sep 2022 13:36:20 +0200 Subject: [PATCH 394/955] Improve type hints in light (#78349) --- homeassistant/components/lifx/manager.py | 2 +- homeassistant/components/lifx/util.py | 2 +- homeassistant/components/light/__init__.py | 62 +++++++++++++--------- 3 files changed, 38 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/lifx/manager.py b/homeassistant/components/lifx/manager.py index ee5428e36a8..28693ae6a60 100644 --- a/homeassistant/components/lifx/manager.py +++ b/homeassistant/components/lifx/manager.py @@ -197,7 +197,7 @@ class LIFXManager: ) await self.effects_conductor.start(effect, bulbs) elif service == SERVICE_EFFECT_COLORLOOP: - preprocess_turn_on_alternatives(self.hass, kwargs) # type: ignore[no-untyped-call] + preprocess_turn_on_alternatives(self.hass, kwargs) brightness = None if ATTR_BRIGHTNESS in kwargs: diff --git a/homeassistant/components/lifx/util.py b/homeassistant/components/lifx/util.py index 1de8bdae76a..fac53464bbc 100644 --- a/homeassistant/components/lifx/util.py +++ b/homeassistant/components/lifx/util.py @@ -70,7 +70,7 @@ def find_hsbk(hass: HomeAssistant, **kwargs: Any) -> list[float | int | None] | """ hue, saturation, brightness, kelvin = [None] * 4 - preprocess_turn_on_alternatives(hass, kwargs) # type: ignore[no-untyped-call] + preprocess_turn_on_alternatives(hass, kwargs) if ATTR_HS_COLOR in kwargs: hue, saturation = kwargs[ATTR_HS_COLOR] diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 5089f454ac8..9a40d159ad6 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -8,7 +8,7 @@ from datetime import timedelta from enum import IntEnum import logging import os -from typing import cast, final +from typing import Any, cast, final import voluptuous as vol @@ -20,7 +20,7 @@ from homeassistant.const import ( SERVICE_TURN_ON, STATE_ON, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.config_validation import ( # noqa: F401 @@ -34,8 +34,6 @@ from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass import homeassistant.util.color as color_util -# mypy: allow-untyped-defs, no-check-untyped-defs - DOMAIN = "light" SCAN_INTERVAL = timedelta(seconds=30) DATA_PROFILES = "light_profiles" @@ -288,7 +286,9 @@ def is_on(hass: HomeAssistant, entity_id: str) -> bool: return hass.states.is_state(entity_id, STATE_ON) -def preprocess_turn_on_alternatives(hass, params): +def preprocess_turn_on_alternatives( + hass: HomeAssistant, params: dict[str, Any] +) -> None: """Process extra data for turn light on request. Async friendly. @@ -316,7 +316,9 @@ def preprocess_turn_on_alternatives(hass, params): params[ATTR_BRIGHTNESS] = round(255 * brightness_pct / 100) -def filter_turn_off_params(light, params): +def filter_turn_off_params( + light: LightEntity, params: dict[str, Any] +) -> dict[str, Any]: """Filter out params not used in turn off or not supported by the light.""" supported_features = light.supported_features @@ -328,7 +330,7 @@ def filter_turn_off_params(light, params): return {k: v for k, v in params.items() if k in (ATTR_TRANSITION, ATTR_FLASH)} -def filter_turn_on_params(light, params): +def filter_turn_on_params(light: LightEntity, params: dict[str, Any]) -> dict[str, Any]: """Filter out params not supported by the light.""" supported_features = light.supported_features @@ -372,9 +374,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: profiles = hass.data[DATA_PROFILES] = Profiles(hass) await profiles.async_initialize() - def preprocess_data(data): + def preprocess_data(data: dict[str, Any]) -> dict[str | vol.Optional, Any]: """Preprocess the service data.""" - base = { + base: dict[str | vol.Optional, Any] = { entity_field: data.pop(entity_field) for entity_field in cv.ENTITY_SERVICE_FIELDS if entity_field in data @@ -384,18 +386,20 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: base["params"] = data return base - async def async_handle_light_on_service(light, call): + async def async_handle_light_on_service( + light: LightEntity, call: ServiceCall + ) -> None: """Handle turning a light on. If brightness is set to 0, this service will turn the light off. """ - params = dict(call.data["params"]) + params: dict[str, Any] = dict(call.data["params"]) # Only process params once we processed brightness step if params and ( ATTR_BRIGHTNESS_STEP in params or ATTR_BRIGHTNESS_STEP_PCT in params ): - brightness = light.brightness if light.is_on else 0 + brightness = light.brightness if light.is_on and light.brightness else 0 if ATTR_BRIGHTNESS_STEP in params: brightness += params.pop(ATTR_BRIGHTNESS_STEP) @@ -447,7 +451,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: rgb_color = color_util.color_rgbw_to_rgb(*rgbw_color) params[ATTR_HS_COLOR] = color_util.color_RGB_to_hs(*rgb_color) elif (rgbww_color := params.pop(ATTR_RGBWW_COLOR, None)) is not None: - rgb_color = color_util.color_rgbww_to_rgb( + rgb_color = color_util.color_rgbww_to_rgb( # type: ignore[call-arg] *rgbww_color, light.min_mireds, light.max_mireds ) params[ATTR_HS_COLOR] = color_util.color_RGB_to_hs(*rgb_color) @@ -460,7 +464,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: params[ATTR_RGBW_COLOR] = color_util.color_rgb_to_rgbw(*rgb_color) elif ColorMode.RGBWW in supported_color_modes: rgb_color = color_util.color_hs_to_RGB(*hs_color) - params[ATTR_RGBWW_COLOR] = color_util.color_rgb_to_rgbww( + params[ATTR_RGBWW_COLOR] = color_util.color_rgb_to_rgbww( # type: ignore[call-arg] *rgb_color, light.min_mireds, light.max_mireds ) elif ColorMode.XY in supported_color_modes: @@ -470,7 +474,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: if ColorMode.RGBW in supported_color_modes: params[ATTR_RGBW_COLOR] = color_util.color_rgb_to_rgbw(*rgb_color) elif ColorMode.RGBWW in supported_color_modes: - params[ATTR_RGBWW_COLOR] = color_util.color_rgb_to_rgbww( + params[ATTR_RGBWW_COLOR] = color_util.color_rgb_to_rgbww( # type: ignore[call-arg] *rgb_color, light.min_mireds, light.max_mireds ) elif ColorMode.HS in supported_color_modes: @@ -488,7 +492,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: params[ATTR_RGBW_COLOR] = color_util.color_rgb_to_rgbw(*rgb_color) elif ColorMode.RGBWW in supported_color_modes: rgb_color = color_util.color_xy_to_RGB(*xy_color) - params[ATTR_RGBWW_COLOR] = color_util.color_rgb_to_rgbww( + params[ATTR_RGBWW_COLOR] = color_util.color_rgb_to_rgbww( # type: ignore[call-arg] *rgb_color, light.min_mireds, light.max_mireds ) elif ATTR_RGBW_COLOR in params and ColorMode.RGBW not in supported_color_modes: @@ -497,7 +501,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: if ColorMode.RGB in supported_color_modes: params[ATTR_RGB_COLOR] = rgb_color elif ColorMode.RGBWW in supported_color_modes: - params[ATTR_RGBWW_COLOR] = color_util.color_rgb_to_rgbww( + params[ATTR_RGBWW_COLOR] = color_util.color_rgb_to_rgbww( # type: ignore[call-arg] *rgb_color, light.min_mireds, light.max_mireds ) elif ColorMode.HS in supported_color_modes: @@ -508,7 +512,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: ATTR_RGBWW_COLOR in params and ColorMode.RGBWW not in supported_color_modes ): rgbww_color = params.pop(ATTR_RGBWW_COLOR) - rgb_color = color_util.color_rgbww_to_rgb( + rgb_color = color_util.color_rgbww_to_rgb( # type: ignore[call-arg] *rgbww_color, light.min_mireds, light.max_mireds ) if ColorMode.RGB in supported_color_modes: @@ -534,7 +538,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: else: await light.async_turn_on(**filter_turn_on_params(light, params)) - async def async_handle_light_off_service(light, call): + async def async_handle_light_off_service( + light: LightEntity, call: ServiceCall + ) -> None: """Handle turning off a light.""" params = dict(call.data["params"]) @@ -543,7 +549,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: await light.async_turn_off(**filter_turn_off_params(light, params)) - async def async_handle_toggle_service(light, call): + async def async_handle_toggle_service( + light: LightEntity, call: ServiceCall + ) -> None: """Handle toggling a light.""" if light.is_on: await async_handle_light_off_service(light, call) @@ -689,7 +697,9 @@ class Profiles: self.data = await self.hass.async_add_executor_job(self._load_profile_data) @callback - def apply_default(self, entity_id: str, state_on: bool, params: dict) -> None: + def apply_default( + self, entity_id: str, state_on: bool | None, params: dict[str, Any] + ) -> None: """Return the default profile for the given light.""" for _entity_id in (entity_id, "group.all_lights"): name = f"{_entity_id}.default" @@ -700,7 +710,7 @@ class Profiles: params.setdefault(ATTR_TRANSITION, self.data[name].transition) @callback - def apply_profile(self, name: str, params: dict) -> None: + def apply_profile(self, name: str, params: dict[str, Any]) -> None: """Apply a profile.""" if (profile := self.data.get(name)) is None: return @@ -841,9 +851,9 @@ class LightEntity(ToggleEntity): return self._attr_effect @property - def capability_attributes(self): + def capability_attributes(self) -> dict[str, Any]: """Return capability attributes.""" - data = {} + data: dict[str, Any] = {} supported_features = self.supported_features supported_color_modes = self._light_internal_supported_color_modes @@ -902,12 +912,12 @@ class LightEntity(ToggleEntity): @final @property - def state_attributes(self): + def state_attributes(self) -> dict[str, Any] | None: """Return state attributes.""" if not self.is_on: return None - data = {} + data: dict[str, Any] = {} supported_features = self.supported_features color_mode = self._light_internal_color_mode From 3941290edcfff5c33775f2bd321b0f45d6fee95b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 Sep 2022 14:07:57 +0200 Subject: [PATCH 395/955] Force root import of const from other components (#78014) * Force root import of const from other components * Add missing commit * Add tests * Add tests * Apply suggestion Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> * Apply suggestion Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> --- pylint/plugins/hass_imports.py | 16 +++++++++ tests/pylint/test_imports.py | 66 ++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+) diff --git a/pylint/plugins/hass_imports.py b/pylint/plugins/hass_imports.py index de26cfef982..d3e6412d301 100644 --- a/pylint/plugins/hass_imports.py +++ b/pylint/plugins/hass_imports.py @@ -310,6 +310,12 @@ class HassImportsFormatChecker(BaseChecker): # type: ignore[misc] "hass-absolute-import", "Used when relative import should be replaced with absolute import", ), + "W7424": ( + "Import should be using the component root", + "hass-component-root-import", + "Used when an import from another component should be " + "from the component root", + ), } options = () @@ -330,6 +336,10 @@ class HassImportsFormatChecker(BaseChecker): # type: ignore[misc] for module, _alias in node.names: if module.startswith(f"{self.current_package}."): self.add_message("hass-relative-import", node=node) + if module.startswith("homeassistant.components.") and module.endswith( + "const" + ): + self.add_message("hass-component-root-import", node=node) def _visit_importfrom_relative( self, current_package: str, node: nodes.ImportFrom @@ -374,6 +384,12 @@ class HassImportsFormatChecker(BaseChecker): # type: ignore[misc] ): self.add_message("hass-relative-import", node=node) return + if node.modname.startswith("homeassistant.components.") and ( + node.modname.endswith(".const") + or "const" in {names[0] for names in node.names} + ): + self.add_message("hass-component-root-import", node=node) + return if obsolete_imports := _OBSOLETE_IMPORT.get(node.modname): for name_tuple in node.names: for obsolete_import in obsolete_imports: diff --git a/tests/pylint/test_imports.py b/tests/pylint/test_imports.py index f535e34e8de..fadaaf159a3 100644 --- a/tests/pylint/test_imports.py +++ b/tests/pylint/test_imports.py @@ -132,3 +132,69 @@ def test_bad_import( ), ): imports_checker.visit_importfrom(import_node) + + +@pytest.mark.parametrize( + "import_node", + [ + "from homeassistant.components import climate", + "from homeassistant.components.climate import ClimateEntityFeature", + ], +) +def test_good_root_import( + linter: UnittestLinter, + imports_checker: BaseChecker, + import_node: str, +) -> None: + """Ensure bad root imports are rejected.""" + + node = astroid.extract_node( + f"{import_node} #@", + "homeassistant.components.pylint_test.climate", + ) + imports_checker.visit_module(node.parent) + + with assert_no_messages(linter): + if import_node.startswith("import"): + imports_checker.visit_import(node) + if import_node.startswith("from"): + imports_checker.visit_importfrom(node) + + +@pytest.mark.parametrize( + "import_node", + [ + "import homeassistant.components.climate.const as climate", + "from homeassistant.components.climate import const", + "from homeassistant.components.climate.const import ClimateEntityFeature", + ], +) +def test_bad_root_import( + linter: UnittestLinter, + imports_checker: BaseChecker, + import_node: str, +) -> None: + """Ensure bad root imports are rejected.""" + + node = astroid.extract_node( + f"{import_node} #@", + "homeassistant.components.pylint_test.climate", + ) + imports_checker.visit_module(node.parent) + + with assert_adds_messages( + linter, + pylint.testutils.MessageTest( + msg_id="hass-component-root-import", + node=node, + args=None, + line=1, + col_offset=0, + end_line=1, + end_col_offset=len(import_node), + ), + ): + if import_node.startswith("import"): + imports_checker.visit_import(node) + if import_node.startswith("from"): + imports_checker.visit_importfrom(node) From 003d160a962cb5b6f882dfb6dbdf95b6a6656e92 Mon Sep 17 00:00:00 2001 From: Poltorak Serguei Date: Wed, 14 Sep 2022 15:19:17 +0300 Subject: [PATCH 396/955] Rework Z-Wave.Me switch multilevel devices to also use light entity (#77969) Co-authored-by: Dmitry Vlasov --- homeassistant/components/zwave_me/const.py | 1 + homeassistant/components/zwave_me/light.py | 39 ++++++++++++++++--- .../components/zwave_me/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 37 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/zwave_me/const.py b/homeassistant/components/zwave_me/const.py index 475d6394d64..84d49ff7b9d 100644 --- a/homeassistant/components/zwave_me/const.py +++ b/homeassistant/components/zwave_me/const.py @@ -10,6 +10,7 @@ class ZWaveMePlatform(StrEnum): """Included ZWaveMe platforms.""" BINARY_SENSOR = "sensorBinary" + BRIGHTNESS_LIGHT = "lightMultilevel" BUTTON = "toggleButton" CLIMATE = "thermostat" COVER = "motor" diff --git a/homeassistant/components/zwave_me/light.py b/homeassistant/components/zwave_me/light.py index 5da0f955059..7fad88a4a49 100644 --- a/homeassistant/components/zwave_me/light.py +++ b/homeassistant/components/zwave_me/light.py @@ -5,13 +5,18 @@ from typing import Any from zwave_me_ws import ZWaveMeData -from homeassistant.components.light import ATTR_RGB_COLOR, ColorMode, LightEntity +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_RGB_COLOR, + ColorMode, + LightEntity, +) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import ZWaveMeEntity +from . import ZWaveMeController, ZWaveMeEntity from .const import DOMAIN, ZWaveMePlatform @@ -40,13 +45,26 @@ async def async_setup_entry( async_dispatcher_connect( hass, f"ZWAVE_ME_NEW_{ZWaveMePlatform.RGBW_LIGHT.upper()}", add_new_device ) + async_dispatcher_connect( + hass, f"ZWAVE_ME_NEW_{ZWaveMePlatform.BRIGHTNESS_LIGHT.upper()}", add_new_device + ) class ZWaveMeRGB(ZWaveMeEntity, LightEntity): """Representation of a ZWaveMe light.""" - _attr_supported_color_modes = {ColorMode.RGB} - _attr_color_mode = ColorMode.RGB + def __init__( + self, + controller: ZWaveMeController, + device: ZWaveMeData, + ) -> None: + """Initialize the device.""" + super().__init__(controller=controller, device=device) + if device.deviceType in [ZWaveMePlatform.RGB_LIGHT, ZWaveMePlatform.RGBW_LIGHT]: + self._attr_color_mode = ColorMode.RGB + else: + self._attr_color_mode = ColorMode.BRIGHTNESS + self._attr_supported_color_modes: set[ColorMode] = {self._attr_color_mode} def turn_off(self, **kwargs: Any) -> None: """Turn the device on.""" @@ -57,8 +75,17 @@ class ZWaveMeRGB(ZWaveMeEntity, LightEntity): color = kwargs.get(ATTR_RGB_COLOR) if color is None: - color = (122, 122, 122) - cmd = "exact?red={}&green={}&blue={}".format(*color) + brightness = kwargs.get(ATTR_BRIGHTNESS) + if brightness is None: + self.controller.zwave_api.send_command(self.device.id, "on") + else: + self.controller.zwave_api.send_command( + self.device.id, f"exact?level={round(brightness / 2.55)}" + ) + return + cmd = "exact?red={}&green={}&blue={}".format( + *color if any(color) else 255, 255, 255 + ) self.controller.zwave_api.send_command(self.device.id, cmd) @property diff --git a/homeassistant/components/zwave_me/manifest.json b/homeassistant/components/zwave_me/manifest.json index 04627583d0e..4ca933f43bc 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.4", "url-normalize==1.4.3"], + "requirements": ["zwave_me_ws==0.2.6", "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 66f55dae3de..41c5426ddaf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2611,4 +2611,4 @@ zm-py==0.5.2 zwave-js-server-python==0.41.1 # homeassistant.components.zwave_me -zwave_me_ws==0.2.4 +zwave_me_ws==0.2.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 04f2b06be54..56dd954db46 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1797,4 +1797,4 @@ zigpy==0.50.3 zwave-js-server-python==0.41.1 # homeassistant.components.zwave_me -zwave_me_ws==0.2.4 +zwave_me_ws==0.2.6 From ae865a07b202537127a2e13930b65b59706e23f0 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Wed, 14 Sep 2022 20:40:20 +0800 Subject: [PATCH 397/955] Cleanup async_announce in forked_daapd (#78457) --- .../components/forked_daapd/media_player.py | 45 ++++++++++--------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/forked_daapd/media_player.py b/homeassistant/components/forked_daapd/media_player.py index e11c7aaeb06..9aea4f59c5c 100644 --- a/homeassistant/components/forked_daapd/media_player.py +++ b/homeassistant/components/forked_daapd/media_player.py @@ -672,7 +672,8 @@ class ForkedDaapdMaster(MediaPlayerEntity): else: _LOGGER.debug("Media type '%s' not supported", media_type) - async def _async_announce(self, media_id: str): + async def _async_announce(self, media_id: str) -> None: + """Play a URI.""" saved_state = self.state # save play state saved_mute = self.is_volume_muted sleep_future = asyncio.create_task( @@ -696,14 +697,14 @@ class ForkedDaapdMaster(MediaPlayerEntity): async with async_timeout.timeout(TTS_TIMEOUT): await self._tts_playing_event.wait() # we have started TTS, now wait for completion - await asyncio.sleep( - self._queue["items"][0]["length_ms"] - / 1000 # player may not have updated yet so grab length from queue - + self._tts_pause_time - ) except asyncio.TimeoutError: self._tts_requested = False _LOGGER.warning("TTS request timed out") + await asyncio.sleep( + self._queue["items"][0]["length_ms"] + / 1000 # player may not have updated yet so grab length from queue + + self._tts_pause_time + ) self._tts_playing_event.clear() # TTS done, return to normal await self.async_turn_on() # restore outputs and volumes @@ -715,22 +716,22 @@ class ForkedDaapdMaster(MediaPlayerEntity): ) if saved_state == MediaPlayerState.PLAYING: await self.async_media_play() - else: # restore stashed queue - if saved_queue: - uris = "" - for item in saved_queue["items"]: - uris += item["uri"] + "," - await self._api.add_to_queue( - uris=uris, - playback="start", - playback_from_position=saved_queue_position, - clear=True, - ) - await self._api.seek(position_ms=saved_song_position) - if saved_state == MediaPlayerState.PAUSED: - await self.async_media_pause() - elif saved_state != MediaPlayerState.PLAYING: - await self.async_media_stop() + return + if not saved_queue: + return + # Restore stashed queue + await self._api.add_to_queue( + uris=",".join(item["uri"] for item in saved_queue["items"]), + playback="start", + playback_from_position=saved_queue_position, + clear=True, + ) + await self._api.seek(position_ms=saved_song_position) + if saved_state == MediaPlayerState.PAUSED: + await self.async_media_pause() + return + if saved_state != MediaPlayerState.PLAYING: + await self.async_media_stop() async def async_select_source(self, source: str) -> None: """Change source. From 99ebac13ed0dced03c064f07d2aeaa6d5f8fadb6 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 14 Sep 2022 15:19:43 +0200 Subject: [PATCH 398/955] Bump openevsewifi to 1.1.2 (#78460) Update openevsewifi to 1.1.2 --- homeassistant/components/openevse/manifest.json | 2 +- requirements_all.txt | 2 +- script/pip_check | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/openevse/manifest.json b/homeassistant/components/openevse/manifest.json index 3a8984af253..37ab8c4c031 100644 --- a/homeassistant/components/openevse/manifest.json +++ b/homeassistant/components/openevse/manifest.json @@ -2,7 +2,7 @@ "domain": "openevse", "name": "OpenEVSE", "documentation": "https://www.home-assistant.io/integrations/openevse", - "requirements": ["openevsewifi==1.1.0"], + "requirements": ["openevsewifi==1.1.2"], "codeowners": [], "iot_class": "local_polling", "loggers": ["openevsewifi"] diff --git a/requirements_all.txt b/requirements_all.txt index 41c5426ddaf..3812c4fed8c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1200,7 +1200,7 @@ open-meteo==0.2.1 openerz-api==0.1.0 # homeassistant.components.openevse -openevsewifi==1.1.0 +openevsewifi==1.1.2 # homeassistant.components.openhome openhomedevice==2.0.2 diff --git a/script/pip_check b/script/pip_check index 5b69e1569c6..ae780b07d60 100755 --- a/script/pip_check +++ b/script/pip_check @@ -3,7 +3,7 @@ PIP_CACHE=$1 # Number of existing dependency conflicts # Update if a PR resolve one! -DEPENDENCY_CONFLICTS=5 +DEPENDENCY_CONFLICTS=4 PIP_CHECK=$(pip check --cache-dir=$PIP_CACHE) LINE_COUNT=$(echo "$PIP_CHECK" | wc -l) From 219cee2ca9f6cd9eb7e0abcbda6d9540240e20d3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 Sep 2022 15:23:29 +0200 Subject: [PATCH 399/955] Move Trace classes to separate module (#78433) --- homeassistant/components/trace/__init__.py | 167 +----------------- homeassistant/components/trace/models.py | 170 +++++++++++++++++++ tests/components/trace/test_websocket_api.py | 6 +- 3 files changed, 175 insertions(+), 168 deletions(-) create mode 100644 homeassistant/components/trace/models.py diff --git a/homeassistant/components/trace/__init__.py b/homeassistant/components/trace/__init__.py index e6a10746a38..bb4f046a7e2 100644 --- a/homeassistant/components/trace/__init__.py +++ b/homeassistant/components/trace/__init__.py @@ -1,30 +1,17 @@ """Support for script and automation tracing and debugging.""" from __future__ import annotations -import abc -from collections import deque -import datetime as dt import logging -from typing import Any import voluptuous as vol from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.core import Context, HomeAssistant +from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.json import ExtendedJSONEncoder from homeassistant.helpers.storage import Store -from homeassistant.helpers.trace import ( - TraceElement, - script_execution_get, - trace_id_get, - trace_id_set, - trace_set_child_id, -) from homeassistant.helpers.typing import ConfigType -import homeassistant.util.dt as dt_util -import homeassistant.util.uuid as uuid_util from . import websocket_api from .const import ( @@ -34,6 +21,7 @@ from .const import ( DATA_TRACES_RESTORED, DEFAULT_STORED_TRACES, ) +from .models import ActionTrace, BaseTrace, RestoredTrace # noqa: F401 from .utils import LimitedSizeDict _LOGGER = logging.getLogger(__name__) @@ -187,154 +175,3 @@ async def async_restore_traces(hass: HomeAssistant) -> None: _LOGGER.exception("Failed to restore trace") continue _async_store_restored_trace(hass, trace) - - -class BaseTrace(abc.ABC): - """Base container for a script or automation trace.""" - - context: Context - key: str - - def as_dict(self) -> dict[str, Any]: - """Return an dictionary version of this ActionTrace for saving.""" - return { - "extended_dict": self.as_extended_dict(), - "short_dict": self.as_short_dict(), - } - - @abc.abstractmethod - def as_extended_dict(self) -> dict[str, Any]: - """Return an extended dictionary version of this ActionTrace.""" - - @abc.abstractmethod - def as_short_dict(self) -> dict[str, Any]: - """Return a brief dictionary version of this ActionTrace.""" - - -class ActionTrace(BaseTrace): - """Base container for a script or automation trace.""" - - _domain: str | None = None - - def __init__( - self, - item_id: str | None, - config: dict[str, Any] | None, - blueprint_inputs: dict[str, Any] | None, - context: Context, - ) -> None: - """Container for script trace.""" - self._trace: dict[str, deque[TraceElement]] | None = None - self._config = config - self._blueprint_inputs = blueprint_inputs - self.context: Context = context - self._error: Exception | None = None - self._state: str = "running" - self._script_execution: str | None = None - self.run_id: str = uuid_util.random_uuid_hex() - self._timestamp_finish: dt.datetime | None = None - self._timestamp_start: dt.datetime = dt_util.utcnow() - self.key = f"{self._domain}.{item_id}" - self._dict: dict[str, Any] | None = None - self._short_dict: dict[str, Any] | None = None - if trace_id_get(): - trace_set_child_id(self.key, self.run_id) - trace_id_set((self.key, self.run_id)) - - def set_trace(self, trace: dict[str, deque[TraceElement]] | None) -> None: - """Set action trace.""" - self._trace = trace - - def set_error(self, ex: Exception) -> None: - """Set error.""" - self._error = ex - - def finished(self) -> None: - """Set finish time.""" - self._timestamp_finish = dt_util.utcnow() - self._state = "stopped" - self._script_execution = script_execution_get() - - def as_extended_dict(self) -> dict[str, Any]: - """Return an extended dictionary version of this ActionTrace.""" - if self._dict: - return self._dict - - result = dict(self.as_short_dict()) - - traces = {} - if self._trace: - for key, trace_list in self._trace.items(): - traces[key] = [item.as_dict() for item in trace_list] - - result.update( - { - "trace": traces, - "config": self._config, - "blueprint_inputs": self._blueprint_inputs, - "context": self.context, - } - ) - - if self._state == "stopped": - # Execution has stopped, save the result - self._dict = result - return result - - def as_short_dict(self) -> dict[str, Any]: - """Return a brief dictionary version of this ActionTrace.""" - if self._short_dict: - return self._short_dict - - last_step = None - - if self._trace: - last_step = list(self._trace)[-1] - domain, item_id = self.key.split(".", 1) - - result = { - "last_step": last_step, - "run_id": self.run_id, - "state": self._state, - "script_execution": self._script_execution, - "timestamp": { - "start": self._timestamp_start, - "finish": self._timestamp_finish, - }, - "domain": domain, - "item_id": item_id, - } - if self._error is not None: - result["error"] = str(self._error) - - if self._state == "stopped": - # Execution has stopped, save the result - self._short_dict = result - return result - - -class RestoredTrace(BaseTrace): - """Container for a restored script or automation trace.""" - - def __init__(self, data: dict[str, Any]) -> None: - """Restore from dict.""" - extended_dict = data["extended_dict"] - short_dict = data["short_dict"] - context = Context( - user_id=extended_dict["context"]["user_id"], - parent_id=extended_dict["context"]["parent_id"], - id=extended_dict["context"]["id"], - ) - self.context = context - self.key = f"{extended_dict['domain']}.{extended_dict['item_id']}" - self.run_id = extended_dict["run_id"] - self._dict = extended_dict - self._short_dict = short_dict - - def as_extended_dict(self) -> dict[str, Any]: - """Return an extended dictionary version of this RestoredTrace.""" - return self._dict - - def as_short_dict(self) -> dict[str, Any]: - """Return a brief dictionary version of this RestoredTrace.""" - return self._short_dict diff --git a/homeassistant/components/trace/models.py b/homeassistant/components/trace/models.py new file mode 100644 index 00000000000..9530554449e --- /dev/null +++ b/homeassistant/components/trace/models.py @@ -0,0 +1,170 @@ +"""Containers for a script or automation trace.""" +from __future__ import annotations + +import abc +from collections import deque +import datetime as dt +from typing import Any + +from homeassistant.core import Context +from homeassistant.helpers.trace import ( + TraceElement, + script_execution_get, + trace_id_get, + trace_id_set, + trace_set_child_id, +) +import homeassistant.util.dt as dt_util +import homeassistant.util.uuid as uuid_util + + +class BaseTrace(abc.ABC): + """Base container for a script or automation trace.""" + + context: Context + key: str + run_id: str + + def as_dict(self) -> dict[str, Any]: + """Return an dictionary version of this ActionTrace for saving.""" + return { + "extended_dict": self.as_extended_dict(), + "short_dict": self.as_short_dict(), + } + + @abc.abstractmethod + def as_extended_dict(self) -> dict[str, Any]: + """Return an extended dictionary version of this ActionTrace.""" + + @abc.abstractmethod + def as_short_dict(self) -> dict[str, Any]: + """Return a brief dictionary version of this ActionTrace.""" + + +class ActionTrace(BaseTrace): + """Base container for a script or automation trace.""" + + _domain: str | None = None + + def __init__( + self, + item_id: str | None, + config: dict[str, Any] | None, + blueprint_inputs: dict[str, Any] | None, + context: Context, + ) -> None: + """Container for script trace.""" + self._trace: dict[str, deque[TraceElement]] | None = None + self._config = config + self._blueprint_inputs = blueprint_inputs + self.context: Context = context + self._error: Exception | None = None + self._state: str = "running" + self._script_execution: str | None = None + self.run_id: str = uuid_util.random_uuid_hex() + self._timestamp_finish: dt.datetime | None = None + self._timestamp_start: dt.datetime = dt_util.utcnow() + self.key = f"{self._domain}.{item_id}" + self._dict: dict[str, Any] | None = None + self._short_dict: dict[str, Any] | None = None + if trace_id_get(): + trace_set_child_id(self.key, self.run_id) + trace_id_set((self.key, self.run_id)) + + def set_trace(self, trace: dict[str, deque[TraceElement]] | None) -> None: + """Set action trace.""" + self._trace = trace + + def set_error(self, ex: Exception) -> None: + """Set error.""" + self._error = ex + + def finished(self) -> None: + """Set finish time.""" + self._timestamp_finish = dt_util.utcnow() + self._state = "stopped" + self._script_execution = script_execution_get() + + def as_extended_dict(self) -> dict[str, Any]: + """Return an extended dictionary version of this ActionTrace.""" + if self._dict: + return self._dict + + result = dict(self.as_short_dict()) + + traces = {} + if self._trace: + for key, trace_list in self._trace.items(): + traces[key] = [item.as_dict() for item in trace_list] + + result.update( + { + "trace": traces, + "config": self._config, + "blueprint_inputs": self._blueprint_inputs, + "context": self.context, + } + ) + + if self._state == "stopped": + # Execution has stopped, save the result + self._dict = result + return result + + def as_short_dict(self) -> dict[str, Any]: + """Return a brief dictionary version of this ActionTrace.""" + if self._short_dict: + return self._short_dict + + last_step = None + + if self._trace: + last_step = list(self._trace)[-1] + domain, item_id = self.key.split(".", 1) + + result = { + "last_step": last_step, + "run_id": self.run_id, + "state": self._state, + "script_execution": self._script_execution, + "timestamp": { + "start": self._timestamp_start, + "finish": self._timestamp_finish, + }, + "domain": domain, + "item_id": item_id, + } + if self._error is not None: + result["error"] = str(self._error) + + if self._state == "stopped": + # Execution has stopped, save the result + self._short_dict = result + return result + + +class RestoredTrace(BaseTrace): + """Container for a restored script or automation trace.""" + + def __init__(self, data: dict[str, Any]) -> None: + """Restore from dict.""" + extended_dict = data["extended_dict"] + short_dict = data["short_dict"] + context = Context( + user_id=extended_dict["context"]["user_id"], + parent_id=extended_dict["context"]["parent_id"], + id=extended_dict["context"]["id"], + ) + self.context = context + self.key = f"{extended_dict['domain']}.{extended_dict['item_id']}" + self.run_id = extended_dict["run_id"] + self._dict = extended_dict + self._short_dict = short_dict + + def as_extended_dict(self) -> dict[str, Any]: + """Return an extended dictionary version of this RestoredTrace.""" + return self._dict + + def as_short_dict(self) -> dict[str, Any]: + """Return a brief dictionary version of this RestoredTrace.""" + return self._short_dict diff --git a/tests/components/trace/test_websocket_api.py b/tests/components/trace/test_websocket_api.py index 21eefb14c1b..69d41968f9b 100644 --- a/tests/components/trace/test_websocket_api.py +++ b/tests/components/trace/test_websocket_api.py @@ -554,7 +554,7 @@ async def test_trace_overflow(hass, hass_ws_client, domain, stored_traces): # Trigger "moon" enough times to overflow the max number of stored traces with patch( - "homeassistant.components.trace.uuid_util.random_uuid_hex", + "homeassistant.components.trace.models.uuid_util.random_uuid_hex", wraps=mock_random_uuid_hex, ): for _ in range(stored_traces or DEFAULT_STORED_TRACES): @@ -628,7 +628,7 @@ async def test_restore_traces_overflow( # Trigger "moon" enough times to overflow the max number of stored traces with patch( - "homeassistant.components.trace.uuid_util.random_uuid_hex", + "homeassistant.components.trace.models.uuid_util.random_uuid_hex", wraps=mock_random_uuid_hex, ): for _ in range(DEFAULT_STORED_TRACES - num_restored_moon_traces + 1): @@ -698,7 +698,7 @@ async def test_restore_traces_late_overflow( # Trigger "moon" enough times to overflow the max number of stored traces with patch( - "homeassistant.components.trace.uuid_util.random_uuid_hex", + "homeassistant.components.trace.models.uuid_util.random_uuid_hex", wraps=mock_random_uuid_hex, ): for _ in range(DEFAULT_STORED_TRACES - num_restored_moon_traces + 1): From 855b0dfdba9f732a7dc471cb55ff9221adedf95e Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Wed, 14 Sep 2022 21:49:00 +0800 Subject: [PATCH 400/955] Pass tasks instead of coros to asyncio.wait in forked_daapd (#78462) * Remove coroutines from asyncio.wait in forked_daapd * Update homeassistant/components/forked_daapd/media_player.py Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- .../components/forked_daapd/media_player.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/forked_daapd/media_player.py b/homeassistant/components/forked_daapd/media_player.py index 9aea4f59c5c..cb33c78fc52 100644 --- a/homeassistant/components/forked_daapd/media_player.py +++ b/homeassistant/components/forked_daapd/media_player.py @@ -418,13 +418,15 @@ class ForkedDaapdMaster(MediaPlayerEntity): # restore state await self._api.set_volume(volume=self._last_volume * 100) if self._last_outputs: - futures = [] + futures: list[asyncio.Task[int]] = [] for output in self._last_outputs: futures.append( - self._api.change_output( - output["id"], - selected=output["selected"], - volume=output["volume"], + asyncio.create_task( + self._api.change_output( + output["id"], + selected=output["selected"], + volume=output["volume"], + ) ) ) await asyncio.wait(futures) From 2ba0f42accde760adf7d99ef9dd6aebd38cb517f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 14 Sep 2022 16:47:08 +0200 Subject: [PATCH 401/955] Prevent deleting blueprints which are in use (#78444) --- .../components/automation/__init__.py | 24 ++++ .../components/automation/helpers.py | 9 +- .../components/blueprint/__init__.py | 2 +- homeassistant/components/blueprint/errors.py | 8 ++ homeassistant/components/blueprint/models.py | 6 + homeassistant/components/script/__init__.py | 25 ++++- homeassistant/components/script/helpers.py | 9 +- tests/components/blueprint/test_models.py | 4 +- .../blueprint/test_websocket_api.py | 105 +++++++++++++++++- .../blueprints/script/test_service.yaml | 8 ++ 10 files changed, 192 insertions(+), 8 deletions(-) create mode 100644 tests/testing_config/blueprints/script/test_service.yaml diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index a5ea30f59d2..454edce5cac 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -9,6 +9,7 @@ import voluptuous as vol from voluptuous.humanize import humanize_error from homeassistant.components import blueprint +from homeassistant.components.blueprint import CONF_USE_BLUEPRINT from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_MODE, @@ -20,6 +21,7 @@ from homeassistant.const import ( CONF_EVENT_DATA, CONF_ID, CONF_MODE, + CONF_PATH, CONF_PLATFORM, CONF_VARIABLES, CONF_ZONE, @@ -233,6 +235,21 @@ def areas_in_automation(hass: HomeAssistant, entity_id: str) -> list[str]: return list(cast(AutomationEntity, automation_entity).referenced_areas) +@callback +def automations_with_blueprint(hass: HomeAssistant, blueprint_path: str) -> list[str]: + """Return all automations that reference the blueprint.""" + if DOMAIN not in hass.data: + return [] + + component = hass.data[DOMAIN] + + return [ + automation_entity.entity_id + for automation_entity in component.entities + if automation_entity.referenced_blueprint == blueprint_path + ] + + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up all automations.""" hass.data[DOMAIN] = component = EntityComponent(LOGGER, DOMAIN, hass) @@ -356,6 +373,13 @@ class AutomationEntity(ToggleEntity, RestoreEntity): """Return a set of referenced areas.""" return self.action_script.referenced_areas + @property + def referenced_blueprint(self) -> str | None: + """Return referenced blueprint or None.""" + if self._blueprint_inputs is None: + return None + return cast(str, self._blueprint_inputs[CONF_USE_BLUEPRINT][CONF_PATH]) + @property def referenced_devices(self) -> set[str]: """Return a set of referenced devices.""" diff --git a/homeassistant/components/automation/helpers.py b/homeassistant/components/automation/helpers.py index 3be11afe18b..7c2efc17bf4 100644 --- a/homeassistant/components/automation/helpers.py +++ b/homeassistant/components/automation/helpers.py @@ -8,8 +8,15 @@ from .const import DOMAIN, LOGGER DATA_BLUEPRINTS = "automation_blueprints" +def _blueprint_in_use(hass: HomeAssistant, blueprint_path: str) -> bool: + """Return True if any automation references the blueprint.""" + from . import automations_with_blueprint # pylint: disable=import-outside-toplevel + + return len(automations_with_blueprint(hass, blueprint_path)) > 0 + + @singleton(DATA_BLUEPRINTS) @callback def async_get_blueprints(hass: HomeAssistant) -> blueprint.DomainBlueprints: """Get automation blueprints.""" - return blueprint.DomainBlueprints(hass, DOMAIN, LOGGER) + return blueprint.DomainBlueprints(hass, DOMAIN, LOGGER, _blueprint_in_use) diff --git a/homeassistant/components/blueprint/__init__.py b/homeassistant/components/blueprint/__init__.py index 23ab6398333..3087309f36a 100644 --- a/homeassistant/components/blueprint/__init__.py +++ b/homeassistant/components/blueprint/__init__.py @@ -3,7 +3,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.typing import ConfigType from . import websocket_api -from .const import DOMAIN # noqa: F401 +from .const import CONF_USE_BLUEPRINT, DOMAIN # noqa: F401 from .errors import ( # noqa: F401 BlueprintException, BlueprintWithNameException, diff --git a/homeassistant/components/blueprint/errors.py b/homeassistant/components/blueprint/errors.py index aceca533d23..fe714542e0f 100644 --- a/homeassistant/components/blueprint/errors.py +++ b/homeassistant/components/blueprint/errors.py @@ -91,3 +91,11 @@ class FileAlreadyExists(BlueprintWithNameException): def __init__(self, domain: str, blueprint_name: str) -> None: """Initialize blueprint exception.""" super().__init__(domain, blueprint_name, "Blueprint already exists") + + +class BlueprintInUse(BlueprintWithNameException): + """Error when a blueprint is in use.""" + + def __init__(self, domain: str, blueprint_name: str) -> None: + """Initialize blueprint exception.""" + super().__init__(domain, blueprint_name, "Blueprint in use") diff --git a/homeassistant/components/blueprint/models.py b/homeassistant/components/blueprint/models.py index 0d90c663b4f..f77a2bed9a4 100644 --- a/homeassistant/components/blueprint/models.py +++ b/homeassistant/components/blueprint/models.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from collections.abc import Callable import logging import pathlib import shutil @@ -35,6 +36,7 @@ from .const import ( ) from .errors import ( BlueprintException, + BlueprintInUse, FailedToLoad, FileAlreadyExists, InvalidBlueprint, @@ -183,11 +185,13 @@ class DomainBlueprints: hass: HomeAssistant, domain: str, logger: logging.Logger, + blueprint_in_use: Callable[[HomeAssistant, str], bool], ) -> None: """Initialize a domain blueprints instance.""" self.hass = hass self.domain = domain self.logger = logger + self._blueprint_in_use = blueprint_in_use self._blueprints: dict[str, Blueprint | None] = {} self._load_lock = asyncio.Lock() @@ -302,6 +306,8 @@ class DomainBlueprints: async def async_remove_blueprint(self, blueprint_path: str) -> None: """Remove a blueprint file.""" + if self._blueprint_in_use(self.hass, blueprint_path): + raise BlueprintInUse(self.domain, blueprint_path) path = self.blueprint_folder / blueprint_path await self.hass.async_add_executor_job(path.unlink) self._blueprints[blueprint_path] = None diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py index efad242fbd0..53bd256c624 100644 --- a/homeassistant/components/script/__init__.py +++ b/homeassistant/components/script/__init__.py @@ -8,7 +8,7 @@ from typing import Any, cast import voluptuous as vol from voluptuous.humanize import humanize_error -from homeassistant.components.blueprint import BlueprintInputs +from homeassistant.components.blueprint import CONF_USE_BLUEPRINT, BlueprintInputs from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_MODE, @@ -18,6 +18,7 @@ from homeassistant.const import ( CONF_ICON, CONF_MODE, CONF_NAME, + CONF_PATH, CONF_SEQUENCE, CONF_VARIABLES, SERVICE_RELOAD, @@ -165,6 +166,21 @@ def areas_in_script(hass: HomeAssistant, entity_id: str) -> list[str]: return list(script_entity.script.referenced_areas) +@callback +def scripts_with_blueprint(hass: HomeAssistant, blueprint_path: str) -> list[str]: + """Return all scripts that reference the blueprint.""" + if DOMAIN not in hass.data: + return [] + + component = hass.data[DOMAIN] + + return [ + script_entity.entity_id + for script_entity in component.entities + if script_entity.referenced_blueprint == blueprint_path + ] + + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Load the scripts from the configuration.""" hass.data[DOMAIN] = component = EntityComponent(LOGGER, DOMAIN, hass) @@ -372,6 +388,13 @@ class ScriptEntity(ToggleEntity, RestoreEntity): """Return true if script is on.""" return self.script.is_running + @property + def referenced_blueprint(self): + """Return referenced blueprint or None.""" + if self._blueprint_inputs is None: + return None + return self._blueprint_inputs[CONF_USE_BLUEPRINT][CONF_PATH] + @callback def async_change_listener(self): """Update state.""" diff --git a/homeassistant/components/script/helpers.py b/homeassistant/components/script/helpers.py index 3c78138a4ec..9f0d4399d3d 100644 --- a/homeassistant/components/script/helpers.py +++ b/homeassistant/components/script/helpers.py @@ -8,8 +8,15 @@ from .const import DOMAIN, LOGGER DATA_BLUEPRINTS = "script_blueprints" +def _blueprint_in_use(hass: HomeAssistant, blueprint_path: str) -> bool: + """Return True if any script references the blueprint.""" + from . import scripts_with_blueprint # pylint: disable=import-outside-toplevel + + return len(scripts_with_blueprint(hass, blueprint_path)) > 0 + + @singleton(DATA_BLUEPRINTS) @callback def async_get_blueprints(hass: HomeAssistant) -> DomainBlueprints: """Get script blueprints.""" - return DomainBlueprints(hass, DOMAIN, LOGGER) + return DomainBlueprints(hass, DOMAIN, LOGGER, _blueprint_in_use) diff --git a/tests/components/blueprint/test_models.py b/tests/components/blueprint/test_models.py index 497e8b36e99..02ed94709db 100644 --- a/tests/components/blueprint/test_models.py +++ b/tests/components/blueprint/test_models.py @@ -47,7 +47,9 @@ def blueprint_2(): @pytest.fixture def domain_bps(hass): """Domain blueprints fixture.""" - return models.DomainBlueprints(hass, "automation", logging.getLogger(__name__)) + return models.DomainBlueprints( + hass, "automation", logging.getLogger(__name__), None + ) def test_blueprint_model_init(): diff --git a/tests/components/blueprint/test_websocket_api.py b/tests/components/blueprint/test_websocket_api.py index eb2d12f5081..05c0e4adc4c 100644 --- a/tests/components/blueprint/test_websocket_api.py +++ b/tests/components/blueprint/test_websocket_api.py @@ -8,13 +8,26 @@ from homeassistant.setup import async_setup_component from homeassistant.util.yaml import parse_yaml +@pytest.fixture +def automation_config(): + """Automation config.""" + return {} + + +@pytest.fixture +def script_config(): + """Script config.""" + return {} + + @pytest.fixture(autouse=True) -async def setup_bp(hass): +async def setup_bp(hass, automation_config, script_config): """Fixture to set up the blueprint component.""" assert await async_setup_component(hass, "blueprint", {}) - # Trigger registration of automation blueprints - await async_setup_component(hass, "automation", {}) + # Trigger registration of automation and script blueprints + await async_setup_component(hass, "automation", automation_config) + await async_setup_component(hass, "script", script_config) async def test_list_blueprints(hass, hass_ws_client): @@ -251,3 +264,89 @@ async def test_delete_non_exist_file_blueprint(hass, aioclient_mock, hass_ws_cli assert msg["id"] == 9 assert not msg["success"] + + +@pytest.mark.parametrize( + "automation_config", + ( + { + "automation": { + "use_blueprint": { + "path": "test_event_service.yaml", + "input": { + "trigger_event": "blueprint_event", + "service_to_call": "test.automation", + "a_number": 5, + }, + } + } + }, + ), +) +async def test_delete_blueprint_in_use_by_automation( + hass, aioclient_mock, hass_ws_client +): + """Test deleting a blueprint which is in use.""" + + with patch("pathlib.Path.unlink", return_value=Mock()) as unlink_mock: + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 9, + "type": "blueprint/delete", + "path": "test_event_service.yaml", + "domain": "automation", + } + ) + + msg = await client.receive_json() + + assert not unlink_mock.mock_calls + assert msg["id"] == 9 + assert not msg["success"] + assert msg["error"] == { + "code": "unknown_error", + "message": "Blueprint in use", + } + + +@pytest.mark.parametrize( + "script_config", + ( + { + "script": { + "test_script": { + "use_blueprint": { + "path": "test_service.yaml", + "input": { + "service_to_call": "test.automation", + }, + } + } + } + }, + ), +) +async def test_delete_blueprint_in_use_by_script(hass, aioclient_mock, hass_ws_client): + """Test deleting a blueprint which is in use.""" + + with patch("pathlib.Path.unlink", return_value=Mock()) as unlink_mock: + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 9, + "type": "blueprint/delete", + "path": "test_service.yaml", + "domain": "script", + } + ) + + msg = await client.receive_json() + + assert not unlink_mock.mock_calls + assert msg["id"] == 9 + assert not msg["success"] + assert msg["error"] == { + "code": "unknown_error", + "message": "Blueprint in use", + } diff --git a/tests/testing_config/blueprints/script/test_service.yaml b/tests/testing_config/blueprints/script/test_service.yaml new file mode 100644 index 00000000000..4de991e90dc --- /dev/null +++ b/tests/testing_config/blueprints/script/test_service.yaml @@ -0,0 +1,8 @@ +blueprint: + name: "Call service" + domain: script + input: + service_to_call: +sequence: + service: !input service_to_call + entity_id: light.kitchen From 650aae49fe4d0cf5e2bf82c2a4ab5dcb74c53ead Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Wed, 14 Sep 2022 19:06:29 +0200 Subject: [PATCH 402/955] Support AMD SoC CPU temperature (#78472) This adds support for CPU temperature readings on AMD SoC based systems like the AMD G-Series GX-222G found in FUJITSU FUTRO S920. --- homeassistant/components/systemmonitor/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py index 7a57c06ef58..eb889264151 100644 --- a/homeassistant/components/systemmonitor/sensor.py +++ b/homeassistant/components/systemmonitor/sensor.py @@ -306,6 +306,7 @@ CPU_SENSOR_PREFIXES = [ "Tctl", "cpu0-thermal", "cpu0_thermal", + "k10temp 1", ] From fd05d949cc76d340dad9bf7dc3ff5d937d8d1b1e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 Sep 2022 19:09:31 +0200 Subject: [PATCH 403/955] Fix device_class in demo (#78463) Use _attr_device_class in demo --- homeassistant/components/demo/binary_sensor.py | 7 +------ homeassistant/components/demo/cover.py | 7 +------ homeassistant/components/demo/media_player.py | 10 ++-------- tests/components/google_assistant/__init__.py | 2 +- 4 files changed, 5 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/demo/binary_sensor.py b/homeassistant/components/demo/binary_sensor.py index 584c0cf88f1..ee718e85cc0 100644 --- a/homeassistant/components/demo/binary_sensor.py +++ b/homeassistant/components/demo/binary_sensor.py @@ -61,7 +61,7 @@ class DemoBinarySensor(BinarySensorEntity): self._unique_id = unique_id self._attr_name = name self._state = state - self._sensor_type = device_class + self._attr_device_class = device_class @property def device_info(self) -> DeviceInfo: @@ -79,11 +79,6 @@ class DemoBinarySensor(BinarySensorEntity): """Return the unique id.""" return self._unique_id - @property - def device_class(self) -> BinarySensorDeviceClass: - """Return the class of this sensor.""" - return self._sensor_type - @property def is_on(self) -> bool: """Return true if the binary sensor is on.""" diff --git a/homeassistant/components/demo/cover.py b/homeassistant/components/demo/cover.py index fbb8a171516..845bd9976a3 100644 --- a/homeassistant/components/demo/cover.py +++ b/homeassistant/components/demo/cover.py @@ -85,7 +85,7 @@ class DemoCover(CoverEntity): self._unique_id = unique_id self._attr_name = name self._position = position - self._device_class = device_class + self._attr_device_class = device_class self._supported_features = supported_features self._set_position: int | None = None self._set_tilt_position: int | None = None @@ -142,11 +142,6 @@ class DemoCover(CoverEntity): """Return if the cover is opening.""" return self._is_opening - @property - def device_class(self) -> CoverDeviceClass | None: - """Return the class of this device, from component DEVICE_CLASSES.""" - return self._device_class - @property def supported_features(self) -> int: """Flag supported features.""" diff --git a/homeassistant/components/demo/media_player.py b/homeassistant/components/demo/media_player.py index 8dbf059c2ca..e0c05e853c1 100644 --- a/homeassistant/components/demo/media_player.py +++ b/homeassistant/components/demo/media_player.py @@ -120,7 +120,7 @@ class AbstractDemoPlayer(MediaPlayerEntity): self._shuffle = False self._sound_mode_list = SOUND_MODE_LIST self._sound_mode = DEFAULT_SOUND_MODE - self._device_class = device_class + self._attr_device_class = device_class @property def state(self) -> str: @@ -152,11 +152,6 @@ class AbstractDemoPlayer(MediaPlayerEntity): """Return a list of available sound modes.""" return self._sound_mode_list - @property - def device_class(self) -> MediaPlayerDeviceClass | None: - """Return the device class of the media player.""" - return self._device_class - def turn_on(self) -> None: """Turn the media player on.""" self._player_state = MediaPlayerState.PLAYING @@ -427,12 +422,11 @@ class DemoTVShowPlayer(AbstractDemoPlayer): # We only implement the methods that we support - _attr_device_class = MediaPlayerDeviceClass.TV _attr_media_content_type = MediaType.TVSHOW def __init__(self) -> None: """Initialize the demo device.""" - super().__init__("Lounge room") + super().__init__("Lounge room", MediaPlayerDeviceClass.TV) self._cur_episode = 1 self._episode_count = 13 self._source = "dvd" diff --git a/tests/components/google_assistant/__init__.py b/tests/components/google_assistant/__init__.py index 423bb1b55d7..2122818bbb4 100644 --- a/tests/components/google_assistant/__init__.py +++ b/tests/components/google_assistant/__init__.py @@ -221,7 +221,7 @@ DEMO_DEVICES = [ "action.devices.traits.TransportControl", "action.devices.traits.MediaState", ], - "type": "action.devices.types.SETTOP", + "type": "action.devices.types.TV", "willReportState": False, }, { From 996bcbdac6f23b0e72374411489e4daf27fab189 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 Sep 2022 20:16:23 +0200 Subject: [PATCH 404/955] Make EntityComponent generic (#78473) --- .../components/automation/__init__.py | 28 ++++++++++--------- homeassistant/components/counter/__init__.py | 2 +- homeassistant/components/dominos/__init__.py | 4 +-- .../components/image_processing/__init__.py | 4 ++- .../components/input_boolean/__init__.py | 2 +- .../components/input_button/__init__.py | 2 +- .../components/input_datetime/__init__.py | 2 +- .../components/input_number/__init__.py | 2 +- .../components/input_select/__init__.py | 2 +- .../components/input_text/__init__.py | 2 +- homeassistant/components/mailbox/__init__.py | 2 +- homeassistant/components/person/__init__.py | 2 +- homeassistant/components/plant/__init__.py | 2 +- .../components/remember_the_milk/__init__.py | 2 +- homeassistant/components/rest/__init__.py | 3 +- homeassistant/components/schedule/__init__.py | 2 +- homeassistant/components/script/__init__.py | 14 +++------- homeassistant/components/timer/__init__.py | 2 +- homeassistant/components/zone/__init__.py | 2 +- homeassistant/helpers/entity_component.py | 19 +++++++------ homeassistant/helpers/reload.py | 3 +- 21 files changed, 53 insertions(+), 50 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 454edce5cac..b1ec0c68e4b 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -154,12 +154,12 @@ def automations_with_entity(hass: HomeAssistant, entity_id: str) -> list[str]: if DOMAIN not in hass.data: return [] - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[AutomationEntity] = hass.data[DOMAIN] return [ automation_entity.entity_id for automation_entity in component.entities - if entity_id in cast(AutomationEntity, automation_entity).referenced_entities + if entity_id in automation_entity.referenced_entities ] @@ -169,12 +169,12 @@ def entities_in_automation(hass: HomeAssistant, entity_id: str) -> list[str]: if DOMAIN not in hass.data: return [] - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[AutomationEntity] = hass.data[DOMAIN] if (automation_entity := component.get_entity(entity_id)) is None: return [] - return list(cast(AutomationEntity, automation_entity).referenced_entities) + return list(automation_entity.referenced_entities) @callback @@ -183,12 +183,12 @@ def automations_with_device(hass: HomeAssistant, device_id: str) -> list[str]: if DOMAIN not in hass.data: return [] - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[AutomationEntity] = hass.data[DOMAIN] return [ automation_entity.entity_id for automation_entity in component.entities - if device_id in cast(AutomationEntity, automation_entity).referenced_devices + if device_id in automation_entity.referenced_devices ] @@ -198,12 +198,12 @@ def devices_in_automation(hass: HomeAssistant, entity_id: str) -> list[str]: if DOMAIN not in hass.data: return [] - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[AutomationEntity] = hass.data[DOMAIN] if (automation_entity := component.get_entity(entity_id)) is None: return [] - return list(cast(AutomationEntity, automation_entity).referenced_devices) + return list(automation_entity.referenced_devices) @callback @@ -212,12 +212,12 @@ def automations_with_area(hass: HomeAssistant, area_id: str) -> list[str]: if DOMAIN not in hass.data: return [] - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[AutomationEntity] = hass.data[DOMAIN] return [ automation_entity.entity_id for automation_entity in component.entities - if area_id in cast(AutomationEntity, automation_entity).referenced_areas + if area_id in automation_entity.referenced_areas ] @@ -227,12 +227,12 @@ def areas_in_automation(hass: HomeAssistant, entity_id: str) -> list[str]: if DOMAIN not in hass.data: return [] - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[AutomationEntity] = hass.data[DOMAIN] if (automation_entity := component.get_entity(entity_id)) is None: return [] - return list(cast(AutomationEntity, automation_entity).referenced_areas) + return list(automation_entity.referenced_areas) @callback @@ -252,7 +252,9 @@ def automations_with_blueprint(hass: HomeAssistant, blueprint_path: str) -> list async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up all automations.""" - hass.data[DOMAIN] = component = EntityComponent(LOGGER, DOMAIN, hass) + hass.data[DOMAIN] = component = EntityComponent[AutomationEntity]( + LOGGER, DOMAIN, hass + ) # Process integration platforms right away since # we will create entities before firing EVENT_COMPONENT_LOADED diff --git a/homeassistant/components/counter/__init__.py b/homeassistant/components/counter/__init__.py index 113826c2291..dedeb428c0c 100644 --- a/homeassistant/components/counter/__init__.py +++ b/homeassistant/components/counter/__init__.py @@ -93,7 +93,7 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the counters.""" - component = EntityComponent(_LOGGER, DOMAIN, hass) + component = EntityComponent[Counter](_LOGGER, DOMAIN, hass) id_manager = collection.IDManager() yaml_collection = collection.YamlCollection( diff --git a/homeassistant/components/dominos/__init__.py b/homeassistant/components/dominos/__init__.py index 31feaa7687e..ecfe7b65a7d 100644 --- a/homeassistant/components/dominos/__init__.py +++ b/homeassistant/components/dominos/__init__.py @@ -67,9 +67,9 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up is called when Home Assistant is loading our component.""" dominos = Dominos(hass, config) - component = EntityComponent(_LOGGER, DOMAIN, hass) + component = EntityComponent[DominosOrder](_LOGGER, DOMAIN, hass) hass.data[DOMAIN] = {} - entities = [] + entities: list[DominosOrder] = [] conf = config[DOMAIN] hass.services.register( diff --git a/homeassistant/components/image_processing/__init__.py b/homeassistant/components/image_processing/__init__.py index 26e6d195b92..8987a366aee 100644 --- a/homeassistant/components/image_processing/__init__.py +++ b/homeassistant/components/image_processing/__init__.py @@ -85,7 +85,9 @@ class FaceInformation(TypedDict, total=False): async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the image processing.""" - component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL) + component = EntityComponent[ImageProcessingEntity]( + _LOGGER, DOMAIN, hass, SCAN_INTERVAL + ) await component.async_setup(config) diff --git a/homeassistant/components/input_boolean/__init__.py b/homeassistant/components/input_boolean/__init__.py index f1cdb145a7c..d1d19247121 100644 --- a/homeassistant/components/input_boolean/__init__.py +++ b/homeassistant/components/input_boolean/__init__.py @@ -92,7 +92,7 @@ def is_on(hass: HomeAssistant, entity_id: str) -> bool: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up an input boolean.""" - component = EntityComponent(_LOGGER, DOMAIN, hass) + component = EntityComponent[InputBoolean](_LOGGER, DOMAIN, hass) # Process integration platforms right away since # we will create entities before firing EVENT_COMPONENT_LOADED diff --git a/homeassistant/components/input_button/__init__.py b/homeassistant/components/input_button/__init__.py index 14ff940ff64..f425c8e3da2 100644 --- a/homeassistant/components/input_button/__init__.py +++ b/homeassistant/components/input_button/__init__.py @@ -77,7 +77,7 @@ class InputButtonStorageCollection(collection.StorageCollection): async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up an input button.""" - component = EntityComponent(_LOGGER, DOMAIN, hass) + component = EntityComponent[InputButton](_LOGGER, DOMAIN, hass) # Process integration platforms right away since # we will create entities before firing EVENT_COMPONENT_LOADED diff --git a/homeassistant/components/input_datetime/__init__.py b/homeassistant/components/input_datetime/__init__.py index afd94ea60f4..5913789d53f 100644 --- a/homeassistant/components/input_datetime/__init__.py +++ b/homeassistant/components/input_datetime/__init__.py @@ -130,7 +130,7 @@ RELOAD_SERVICE_SCHEMA = vol.Schema({}) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up an input datetime.""" - component = EntityComponent(_LOGGER, DOMAIN, hass) + component = EntityComponent[InputDatetime](_LOGGER, DOMAIN, hass) # Process integration platforms right away since # we will create entities before firing EVENT_COMPONENT_LOADED diff --git a/homeassistant/components/input_number/__init__.py b/homeassistant/components/input_number/__init__.py index 3a7f7b29f13..99e54dc9baa 100644 --- a/homeassistant/components/input_number/__init__.py +++ b/homeassistant/components/input_number/__init__.py @@ -107,7 +107,7 @@ STORAGE_VERSION = 1 async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up an input slider.""" - component = EntityComponent(_LOGGER, DOMAIN, hass) + component = EntityComponent[InputNumber](_LOGGER, DOMAIN, hass) # Process integration platforms right away since # we will create entities before firing EVENT_COMPONENT_LOADED diff --git a/homeassistant/components/input_select/__init__.py b/homeassistant/components/input_select/__init__.py index 41b079f0888..f30b2ca1e36 100644 --- a/homeassistant/components/input_select/__init__.py +++ b/homeassistant/components/input_select/__init__.py @@ -132,7 +132,7 @@ class InputSelectStore(Store): async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up an input select.""" - component = EntityComponent(_LOGGER, DOMAIN, hass) + component = EntityComponent[InputSelect](_LOGGER, DOMAIN, hass) # Process integration platforms right away since # we will create entities before firing EVENT_COMPONENT_LOADED diff --git a/homeassistant/components/input_text/__init__.py b/homeassistant/components/input_text/__init__.py index 072f17c72a3..6069ae8143a 100644 --- a/homeassistant/components/input_text/__init__.py +++ b/homeassistant/components/input_text/__init__.py @@ -107,7 +107,7 @@ RELOAD_SERVICE_SCHEMA = vol.Schema({}) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up an input text.""" - component = EntityComponent(_LOGGER, DOMAIN, hass) + component = EntityComponent[InputText](_LOGGER, DOMAIN, hass) # Process integration platforms right away since # we will create entities before firing EVENT_COMPONENT_LOADED diff --git a/homeassistant/components/mailbox/__init__.py b/homeassistant/components/mailbox/__init__.py index 4e65d989b98..f97b2c5337b 100644 --- a/homeassistant/components/mailbox/__init__.py +++ b/homeassistant/components/mailbox/__init__.py @@ -83,7 +83,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: mailboxes.append(mailbox) mailbox_entity = MailboxEntity(mailbox) - component = EntityComponent( + component = EntityComponent[MailboxEntity]( logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL ) await component.async_add_entities([mailbox_entity]) diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py index c41be68d6ea..0823e9e4b55 100644 --- a/homeassistant/components/person/__init__.py +++ b/homeassistant/components/person/__init__.py @@ -326,7 +326,7 @@ The following persons point at invalid users: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the person component.""" - entity_component = EntityComponent(_LOGGER, DOMAIN, hass) + entity_component = EntityComponent[Person](_LOGGER, DOMAIN, hass) id_manager = collection.IDManager() yaml_collection = collection.YamlCollection( logging.getLogger(f"{__name__}.yaml_collection"), id_manager diff --git a/homeassistant/components/plant/__init__.py b/homeassistant/components/plant/__init__.py index 0d95ccbc300..69f440b6859 100644 --- a/homeassistant/components/plant/__init__.py +++ b/homeassistant/components/plant/__init__.py @@ -111,7 +111,7 @@ CONFIG_SCHEMA = vol.Schema({DOMAIN: {cv.string: PLANT_SCHEMA}}, extra=vol.ALLOW_ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Plant component.""" - component = EntityComponent(_LOGGER, DOMAIN, hass) + component = EntityComponent[Plant](_LOGGER, DOMAIN, hass) entities = [] for plant_name, plant_config in config[DOMAIN].items(): diff --git a/homeassistant/components/remember_the_milk/__init__.py b/homeassistant/components/remember_the_milk/__init__.py index fbc2518ce1b..3331f9c61d4 100644 --- a/homeassistant/components/remember_the_milk/__init__.py +++ b/homeassistant/components/remember_the_milk/__init__.py @@ -52,7 +52,7 @@ SERVICE_SCHEMA_COMPLETE_TASK = vol.Schema({vol.Required(CONF_ID): cv.string}) def setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Remember the milk component.""" - component = EntityComponent(_LOGGER, DOMAIN, hass) + component = EntityComponent[RememberTheMilk](_LOGGER, DOMAIN, hass) stored_rtm_config = RememberTheMilkConfiguration(hass) for rtm_config in config[DOMAIN]: diff --git a/homeassistant/components/rest/__init__.py b/homeassistant/components/rest/__init__.py index f8e6941572a..282f05aada8 100644 --- a/homeassistant/components/rest/__init__.py +++ b/homeassistant/components/rest/__init__.py @@ -27,6 +27,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.helpers import discovery, template +from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import ( DEFAULT_SCAN_INTERVAL, EntityComponent, @@ -53,7 +54,7 @@ COORDINATOR_AWARE_PLATFORMS = [SENSOR_DOMAIN, BINARY_SENSOR_DOMAIN] async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the rest platforms.""" - component = EntityComponent(_LOGGER, DOMAIN, hass) + component = EntityComponent[Entity](_LOGGER, DOMAIN, hass) _async_setup_shared_data(hass) async def reload_service_handler(service: ServiceCall) -> None: diff --git a/homeassistant/components/schedule/__init__.py b/homeassistant/components/schedule/__init__.py index 394e2ae3c36..fefb5189e3c 100644 --- a/homeassistant/components/schedule/__init__.py +++ b/homeassistant/components/schedule/__init__.py @@ -154,7 +154,7 @@ ENTITY_SCHEMA = vol.Schema( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up an input select.""" - component = EntityComponent(LOGGER, DOMAIN, hass) + component = EntityComponent[Schedule](LOGGER, DOMAIN, hass) # Process integration platforms right away since # we will create entities before firing EVENT_COMPONENT_LOADED diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py index 53bd256c624..a5ea2a17e0b 100644 --- a/homeassistant/components/script/__init__.py +++ b/homeassistant/components/script/__init__.py @@ -183,7 +183,7 @@ def scripts_with_blueprint(hass: HomeAssistant, blueprint_path: str) -> list[str async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Load the scripts from the configuration.""" - hass.data[DOMAIN] = component = EntityComponent(LOGGER, DOMAIN, hass) + hass.data[DOMAIN] = component = EntityComponent[ScriptEntity](LOGGER, DOMAIN, hass) # Process integration platforms right away since # we will create entities before firing EVENT_COMPONENT_LOADED @@ -205,9 +205,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def turn_on_service(service: ServiceCall) -> None: """Call a service to turn script on.""" variables = service.data.get(ATTR_VARIABLES) - script_entities: list[ScriptEntity] = cast( - list[ScriptEntity], await component.async_extract_from_service(service) - ) + script_entities = await component.async_extract_from_service(service) for script_entity in script_entities: await script_entity.async_turn_on( variables=variables, context=service.context, wait=False @@ -216,9 +214,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def turn_off_service(service: ServiceCall) -> None: """Cancel a script.""" # Stopping a script is ok to be done in parallel - script_entities: list[ScriptEntity] = cast( - list[ScriptEntity], await component.async_extract_from_service(service) - ) + script_entities = await component.async_extract_from_service(service) if not script_entities: return @@ -232,9 +228,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def toggle_service(service: ServiceCall) -> None: """Toggle a script.""" - script_entities: list[ScriptEntity] = cast( - list[ScriptEntity], await component.async_extract_from_service(service) - ) + script_entities = await component.async_extract_from_service(service) for script_entity in script_entities: await script_entity.async_toggle(context=service.context, wait=False) diff --git a/homeassistant/components/timer/__init__.py b/homeassistant/components/timer/__init__.py index ff50e96a18c..6b141ccea4c 100644 --- a/homeassistant/components/timer/__init__.py +++ b/homeassistant/components/timer/__init__.py @@ -106,7 +106,7 @@ RELOAD_SERVICE_SCHEMA = vol.Schema({}) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up an input select.""" - component = EntityComponent(_LOGGER, DOMAIN, hass) + component = EntityComponent[Timer](_LOGGER, DOMAIN, hass) id_manager = collection.IDManager() yaml_collection = collection.YamlCollection( diff --git a/homeassistant/components/zone/__init__.py b/homeassistant/components/zone/__init__.py index aa910a7789e..816d8a62f18 100644 --- a/homeassistant/components/zone/__init__.py +++ b/homeassistant/components/zone/__init__.py @@ -185,7 +185,7 @@ class ZoneStorageCollection(collection.StorageCollection): async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up configured zones as well as Home Assistant zone if necessary.""" - component = entity_component.EntityComponent(_LOGGER, DOMAIN, hass) + component = entity_component.EntityComponent[Zone](_LOGGER, DOMAIN, hass) id_manager = collection.IDManager() yaml_collection = collection.IDLessCollection( diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index 1cef123b292..fb627820060 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -7,7 +7,7 @@ from datetime import timedelta from itertools import chain import logging from types import ModuleType -from typing import Any +from typing import Any, Generic, TypeVar import voluptuous as vol @@ -30,6 +30,8 @@ from .typing import ConfigType, DiscoveryInfoType DEFAULT_SCAN_INTERVAL = timedelta(seconds=15) DATA_INSTANCES = "entity_components" +_EntityT = TypeVar("_EntityT", bound=entity.Entity) + @bind_hass async def async_update_entity(hass: HomeAssistant, entity_id: str) -> None: @@ -52,7 +54,7 @@ async def async_update_entity(hass: HomeAssistant, entity_id: str) -> None: await entity_obj.async_update_ha_state(True) -class EntityComponent: +class EntityComponent(Generic[_EntityT]): """The EntityComponent manages platforms that manages entities. This class has the following responsibilities: @@ -86,18 +88,19 @@ class EntityComponent: hass.data.setdefault(DATA_INSTANCES, {})[domain] = self @property - def entities(self) -> Iterable[entity.Entity]: + def entities(self) -> Iterable[_EntityT]: """Return an iterable that returns all entities.""" return chain.from_iterable( - platform.entities.values() for platform in self._platforms.values() + platform.entities.values() # type: ignore[misc] + for platform in self._platforms.values() ) - def get_entity(self, entity_id: str) -> entity.Entity | None: + def get_entity(self, entity_id: str) -> _EntityT | None: """Get an entity.""" for platform in self._platforms.values(): entity_obj = platform.entities.get(entity_id) if entity_obj is not None: - return entity_obj + return entity_obj # type: ignore[return-value] return None def setup(self, config: ConfigType) -> None: @@ -176,14 +179,14 @@ class EntityComponent: async def async_extract_from_service( self, service_call: ServiceCall, expand_group: bool = True - ) -> list[entity.Entity]: + ) -> list[_EntityT]: """Extract all known and available entities from a service call. Will return an empty list if entities specified but unknown. This method must be run in the event loop. """ - return await service.async_extract_entities( + return await service.async_extract_entities( # type: ignore[return-value] self.hass, self.entities, service_call, expand_group ) diff --git a/homeassistant/helpers/reload.py b/homeassistant/helpers/reload.py index 83698557eb6..75529476dd2 100644 --- a/homeassistant/helpers/reload.py +++ b/homeassistant/helpers/reload.py @@ -14,6 +14,7 @@ from homeassistant.loader import async_get_integration from homeassistant.setup import async_setup_component from . import config_per_platform +from .entity import Entity from .entity_component import EntityComponent from .entity_platform import EntityPlatform, async_get_platforms from .service import async_register_admin_service @@ -120,7 +121,7 @@ async def _async_setup_platform( ) return - entity_component: EntityComponent = hass.data[integration_platform] + entity_component: EntityComponent[Entity] = hass.data[integration_platform] tasks = [ entity_component.async_setup_platform(integration_name, p_config) for p_config in platform_configs From a46982befb07e2987ece7e704e201010234293d9 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Wed, 14 Sep 2022 14:31:54 -0400 Subject: [PATCH 405/955] Add Google Sheets integration (#77853) Co-authored-by: Allen Porter --- CODEOWNERS | 1 + .../components/google_sheets/__init__.py | 113 +++++++ .../google_sheets/application_credentials.py | 27 ++ .../components/google_sheets/config_flow.py | 102 ++++++ .../components/google_sheets/const.py | 10 + .../components/google_sheets/manifest.json | 10 + .../components/google_sheets/services.yaml | 24 ++ .../components/google_sheets/strings.json | 31 ++ .../google_sheets/translations/en.json | 31 ++ .../generated/application_credentials.py | 1 + homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + .../google_sheets/test_config_flow.py | 314 ++++++++++++++++++ tests/components/google_sheets/test_init.py | 214 ++++++++++++ 14 files changed, 882 insertions(+) create mode 100644 homeassistant/components/google_sheets/__init__.py create mode 100644 homeassistant/components/google_sheets/application_credentials.py create mode 100644 homeassistant/components/google_sheets/config_flow.py create mode 100644 homeassistant/components/google_sheets/const.py create mode 100644 homeassistant/components/google_sheets/manifest.json create mode 100644 homeassistant/components/google_sheets/services.yaml create mode 100644 homeassistant/components/google_sheets/strings.json create mode 100644 homeassistant/components/google_sheets/translations/en.json create mode 100644 tests/components/google_sheets/test_config_flow.py create mode 100644 tests/components/google_sheets/test_init.py diff --git a/CODEOWNERS b/CODEOWNERS index d5d3e597f70..4c3ebac5bf1 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -421,6 +421,7 @@ build.json @home-assistant/supervisor /homeassistant/components/google_assistant/ @home-assistant/cloud /tests/components/google_assistant/ @home-assistant/cloud /homeassistant/components/google_cloud/ @lufton +/homeassistant/components/google_sheets/ @tkdrob /homeassistant/components/google_travel_time/ @eifinger /tests/components/google_travel_time/ @eifinger /homeassistant/components/govee_ble/ @bdraco diff --git a/homeassistant/components/google_sheets/__init__.py b/homeassistant/components/google_sheets/__init__.py new file mode 100644 index 00000000000..a4c10da7f23 --- /dev/null +++ b/homeassistant/components/google_sheets/__init__.py @@ -0,0 +1,113 @@ +"""Support for Google Sheets.""" +from __future__ import annotations + +from datetime import datetime +from typing import cast + +import aiohttp +from google.auth.exceptions import RefreshError +from google.oauth2.credentials import Credentials +from gspread import Client +import voluptuous as vol + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN +from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers.config_entry_oauth2_flow import ( + OAuth2Session, + async_get_config_entry_implementation, +) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.selector import ConfigEntrySelector + +from .const import DATA_CONFIG_ENTRY, DEFAULT_ACCESS, DOMAIN + +DATA = "data" +WORKSHEET = "worksheet" + +SERVICE_APPEND_SHEET = "append_sheet" + +SHEET_SERVICE_SCHEMA = vol.All( + { + vol.Required(DATA_CONFIG_ENTRY): ConfigEntrySelector(), + vol.Optional(WORKSHEET): cv.string, + vol.Required(DATA): dict, + }, +) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Google Sheets from a config entry.""" + implementation = await async_get_config_entry_implementation(hass, entry) + session = OAuth2Session(hass, entry, implementation) + try: + await session.async_ensure_token_valid() + except aiohttp.ClientResponseError as err: + if 400 <= err.status < 500: + raise ConfigEntryAuthFailed( + "OAuth session is not valid, reauth required" + ) from err + raise ConfigEntryNotReady from err + except aiohttp.ClientError as err: + raise ConfigEntryNotReady from err + + if not async_entry_has_scopes(hass, entry): + raise ConfigEntryAuthFailed("Required scopes are not present, reauth required") + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = session + + await async_setup_service(hass) + + return True + + +def async_entry_has_scopes(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Verify that the config entry desired scope is present in the oauth token.""" + return DEFAULT_ACCESS in entry.data.get(CONF_TOKEN, {}).get("scope", "").split(" ") + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + hass.data[DOMAIN].pop(entry.entry_id) + return True + + +async def async_setup_service(hass: HomeAssistant) -> None: + """Add the services for Google Sheets.""" + + def _append_to_sheet(call: ServiceCall, entry: ConfigEntry) -> None: + """Run append in the executor.""" + service = Client(Credentials(entry.data[CONF_TOKEN][CONF_ACCESS_TOKEN])) + try: + sheet = service.open_by_key(entry.unique_id) + except RefreshError as ex: + entry.async_start_reauth(hass) + raise ex + worksheet = sheet.worksheet(call.data.get(WORKSHEET, sheet.sheet1.title)) + row_data = {"created": str(datetime.now())} | call.data[DATA] + columns: list[str] = next(iter(worksheet.get_values("A1:ZZ1")), []) + row = [row_data.get(column, "") for column in columns] + for key, value in row_data.items(): + if key not in columns: + columns.append(key) + worksheet.update_cell(1, len(columns), key) + row.append(value) + worksheet.append_row(row) + + async def append_to_sheet(call: ServiceCall) -> None: + """Append new line of data to a Google Sheets document.""" + + entry = cast( + ConfigEntry, + hass.config_entries.async_get_entry(call.data[DATA_CONFIG_ENTRY]), + ) + session: OAuth2Session = hass.data[DOMAIN][entry.entry_id] + await session.async_ensure_token_valid() + await hass.async_add_executor_job(_append_to_sheet, call, entry) + + hass.services.async_register( + DOMAIN, + SERVICE_APPEND_SHEET, + append_to_sheet, + schema=SHEET_SERVICE_SCHEMA, + ) diff --git a/homeassistant/components/google_sheets/application_credentials.py b/homeassistant/components/google_sheets/application_credentials.py new file mode 100644 index 00000000000..c54356b659e --- /dev/null +++ b/homeassistant/components/google_sheets/application_credentials.py @@ -0,0 +1,27 @@ +"""application_credentials platform for Google Sheets.""" + +import oauth2client + +from homeassistant.components.application_credentials import AuthorizationServer +from homeassistant.core import HomeAssistant + +AUTHORIZATION_SERVER = AuthorizationServer( + oauth2client.GOOGLE_AUTH_URI, oauth2client.GOOGLE_TOKEN_URI +) + + +async def async_get_authorization_server(hass: HomeAssistant) -> AuthorizationServer: + """Return authorization server.""" + return AuthorizationServer( + oauth2client.GOOGLE_AUTH_URI, + oauth2client.GOOGLE_TOKEN_URI, + ) + + +async def async_get_description_placeholders(hass: HomeAssistant) -> dict[str, str]: + """Return description placeholders for the credentials dialog.""" + return { + "oauth_consent_url": "https://console.cloud.google.com/apis/credentials/consent", + "more_info_url": "https://www.home-assistant.io/integrations/google_sheets/", + "oauth_creds_url": "https://console.cloud.google.com/apis/credentials", + } diff --git a/homeassistant/components/google_sheets/config_flow.py b/homeassistant/components/google_sheets/config_flow.py new file mode 100644 index 00000000000..d19a5b5c3fa --- /dev/null +++ b/homeassistant/components/google_sheets/config_flow.py @@ -0,0 +1,102 @@ +"""Config flow for Google Sheets integration.""" +from __future__ import annotations + +from collections.abc import Mapping +import logging +from typing import Any + +from google.oauth2.credentials import Credentials +from gspread import Client, GSpreadException + +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import config_entry_oauth2_flow + +from .const import DEFAULT_ACCESS, DEFAULT_NAME, DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class OAuth2FlowHandler( + config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN +): + """Config flow to handle Google Sheets OAuth2 authentication.""" + + DOMAIN = DOMAIN + + @property + def logger(self) -> logging.Logger: + """Return logger.""" + return logging.getLogger(__name__) + + @property + def extra_authorize_data(self) -> dict[str, Any]: + """Extra data that needs to be appended to the authorize url.""" + return { + "scope": DEFAULT_ACCESS, + # Add params to ensure we get back a refresh token + "access_type": "offline", + "prompt": "consent", + } + + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + """Perform reauth upon an API authentication error.""" + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm reauth dialog.""" + if user_input is None: + return self.async_show_form(step_id="reauth_confirm") + return await self.async_step_user() + + def _async_reauth_entry(self) -> ConfigEntry | None: + """Return existing entry for reauth.""" + if self.source != SOURCE_REAUTH or not ( + entry_id := self.context.get("entry_id") + ): + return None + return next( + ( + entry + for entry in self._async_current_entries() + if entry.entry_id == entry_id + ), + None, + ) + + async def async_oauth_create_entry(self, data: dict[str, Any]) -> FlowResult: + """Create an entry for the flow, or update existing entry.""" + service = Client(Credentials(data[CONF_TOKEN][CONF_ACCESS_TOKEN])) + + if entry := self._async_reauth_entry(): + _LOGGER.debug("service.open_by_key") + try: + await self.hass.async_add_executor_job( + service.open_by_key, + entry.unique_id, + ) + except GSpreadException as err: + _LOGGER.error( + "Could not find spreadsheet '%s': %s", entry.unique_id, str(err) + ) + return self.async_abort(reason="open_spreadsheet_failure") + + self.hass.config_entries.async_update_entry(entry, data=data) + await self.hass.config_entries.async_reload(entry.entry_id) + return self.async_abort(reason="reauth_successful") + + try: + doc = await self.hass.async_add_executor_job( + service.create, "Home Assistant" + ) + except GSpreadException as err: + _LOGGER.error("Error creating spreadsheet: %s", str(err)) + return self.async_abort(reason="create_spreadsheet_failure") + + await self.async_set_unique_id(doc.id) + return self.async_create_entry( + title=DEFAULT_NAME, data=data, description_placeholders={"url": doc.url} + ) diff --git a/homeassistant/components/google_sheets/const.py b/homeassistant/components/google_sheets/const.py new file mode 100644 index 00000000000..f8f065972f9 --- /dev/null +++ b/homeassistant/components/google_sheets/const.py @@ -0,0 +1,10 @@ +"""Constants for Google Sheets integration.""" +from __future__ import annotations + +from typing import Final + +DOMAIN = "google_sheets" + +DATA_CONFIG_ENTRY: Final = "config_entry" +DEFAULT_NAME = "Google Sheets" +DEFAULT_ACCESS = "https://www.googleapis.com/auth/drive.file" diff --git a/homeassistant/components/google_sheets/manifest.json b/homeassistant/components/google_sheets/manifest.json new file mode 100644 index 00000000000..c8d86210b42 --- /dev/null +++ b/homeassistant/components/google_sheets/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "google_sheets", + "name": "Google Sheets", + "config_flow": true, + "dependencies": ["application_credentials"], + "documentation": "https://www.home-assistant.io/integrations/google_sheets/", + "requirements": ["gspread==5.5.0"], + "codeowners": ["@tkdrob"], + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/google_sheets/services.yaml b/homeassistant/components/google_sheets/services.yaml new file mode 100644 index 00000000000..7524ba50fb5 --- /dev/null +++ b/homeassistant/components/google_sheets/services.yaml @@ -0,0 +1,24 @@ +append_sheet: + name: Append to Sheet + description: Append data to a worksheet in Google Sheets. + fields: + config_entry: + name: Sheet + description: The sheet to add data to + required: true + selector: + config_entry: + integration: google_sheets + worksheet: + name: Worksheet + description: Name of the worksheet. Defaults to the first one in the document. + example: "Sheet1" + selector: + text: + data: + name: Data + description: Data to be appended to the worksheet. This puts the values on a new row underneath the matching column (key). Any new key is placed on the top of a new column. + required: true + example: '{"hello": world, "cool": True, "count": 5}' + selector: + object: diff --git a/homeassistant/components/google_sheets/strings.json b/homeassistant/components/google_sheets/strings.json new file mode 100644 index 00000000000..858f6856954 --- /dev/null +++ b/homeassistant/components/google_sheets/strings.json @@ -0,0 +1,31 @@ +{ + "config": { + "step": { + "pick_implementation": { + "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]" + }, + "auth": { + "title": "Link Google Account" + } + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "timeout_connect": "[%key:common::config_flow::error::timeout_connect%]", + "oauth_error": "[%key:common::config_flow::abort::oauth2_error%]", + "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "invalid_access_token": "[%key:common::config_flow::error::invalid_access_token%]", + "unknown": "[%key:common::config_flow::error::unknown%]", + "create_spreadsheet_failure": "Error while creating spreadsheet, see error log for details", + "open_spreadsheet_failure": "Error while opening spreadsheet, see error log for details" + }, + "create_entry": { + "default": "Successfully authenticated and spreadsheet created at: {url}" + } + }, + "application_credentials": { + "description": "Follow the [instructions]({more_info_url}) for [OAuth consent screen]({oauth_consent_url}) to give Home Assistant access to your Google Sheets. You also need to create Application Credentials linked to your account:\n1. Go to [Credentials]({oauth_creds_url}) and click **Create Credentials**.\n1. From the drop-down list select **OAuth client ID**.\n1. Select **Web application** for the Application Type.\n\n" + } +} diff --git a/homeassistant/components/google_sheets/translations/en.json b/homeassistant/components/google_sheets/translations/en.json new file mode 100644 index 00000000000..c7348a0fa40 --- /dev/null +++ b/homeassistant/components/google_sheets/translations/en.json @@ -0,0 +1,31 @@ +{ + "application_credentials": { + "description": "Follow the [instructions]({more_info_url}) for [OAuth consent screen]({oauth_consent_url}) to give Home Assistant access to your Google Sheets. You also need to create Application Credentials linked to your account:\n1. Go to [Credentials]({oauth_creds_url}) and click **Create Credentials**.\n1. From the drop-down list select **OAuth client ID**.\n1. Select **Web application** for the Application Type.\n\n" + }, + "config": { + "abort": { + "already_configured": "Account is already configured", + "already_in_progress": "Configuration flow is already in progress", + "cannot_connect": "Failed to connect", + "create_spreadsheet_failure": "Error while creating spreadsheet, see error log for details", + "invalid_access_token": "Invalid access token", + "missing_configuration": "The component is not configured. Please follow the documentation.", + "oauth_error": "Received invalid token data.", + "open_spreadsheet_failure": "Error while opening spreadsheet, see error log for details", + "reauth_successful": "Re-authentication was successful", + "timeout_connect": "Timeout establishing connection", + "unknown": "Unexpected error" + }, + "create_entry": { + "default": "Successfully authenticated and spreadsheet created at: {url}" + }, + "step": { + "auth": { + "title": "Link Google Account" + }, + "pick_implementation": { + "title": "Pick Authentication Method" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/application_credentials.py b/homeassistant/generated/application_credentials.py index 4673cf2378d..fb2b04989f7 100644 --- a/homeassistant/generated/application_credentials.py +++ b/homeassistant/generated/application_credentials.py @@ -8,6 +8,7 @@ To update, run python3 -m script.hassfest APPLICATION_CREDENTIALS = [ "geocaching", "google", + "google_sheets", "home_connect", "lametric", "lyric", diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 60ac2e8d511..1aa49e279db 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -142,6 +142,7 @@ FLOWS = { "gogogate2", "goodwe", "google", + "google_sheets", "google_travel_time", "govee_ble", "gpslogger", diff --git a/requirements_all.txt b/requirements_all.txt index 3812c4fed8c..72c3c86d9e7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -798,6 +798,9 @@ gridnet==4.0.0 # homeassistant.components.growatt_server growattServer==1.2.2 +# homeassistant.components.google_sheets +gspread==5.5.0 + # homeassistant.components.gstreamer gstreamer-player==1.1.2 diff --git a/tests/components/google_sheets/test_config_flow.py b/tests/components/google_sheets/test_config_flow.py new file mode 100644 index 00000000000..3fcd2f99ed0 --- /dev/null +++ b/tests/components/google_sheets/test_config_flow.py @@ -0,0 +1,314 @@ +"""Test the Google Sheets config flow.""" + +from collections.abc import Generator +from unittest.mock import Mock, patch + +from gspread import GSpreadException +import oauth2client +import pytest + +from homeassistant import config_entries +from homeassistant.components.application_credentials import ( + ClientCredential, + async_import_client_credential, +) +from homeassistant.components.google_sheets.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_entry_oauth2_flow +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + +CLIENT_ID = "1234" +CLIENT_SECRET = "5678" +SHEET_ID = "google-sheet-id" +TITLE = "Google Sheets" + + +@pytest.fixture +async def setup_credentials(hass: HomeAssistant) -> None: + """Fixture to setup credentials.""" + assert await async_setup_component(hass, "application_credentials", {}) + await async_import_client_credential( + hass, + DOMAIN, + ClientCredential(CLIENT_ID, CLIENT_SECRET), + ) + + +@pytest.fixture(autouse=True) +async def mock_client() -> Generator[Mock, None, None]: + """Fixture to setup a fake spreadsheet client library.""" + with patch( + "homeassistant.components.google_sheets.config_flow.Client" + ) as mock_client: + yield mock_client + + +async def test_full_flow( + hass: HomeAssistant, + hass_client_no_auth, + aioclient_mock, + current_request_with_host, + setup_credentials, + mock_client, +) -> None: + """Check full flow.""" + result = await hass.config_entries.flow.async_init( + "google_sheets", context={"source": config_entries.SOURCE_USER} + ) + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) + + assert result["url"] == ( + f"{oauth2client.GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}&scope=https://www.googleapis.com/auth/drive.file" + "&access_type=offline&prompt=consent" + ) + + client = await hass_client_no_auth() + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + # Prepare fake client library response when creating the sheet + mock_create = Mock() + mock_create.return_value.id = SHEET_ID + mock_client.return_value.create = mock_create + + aioclient_mock.post( + oauth2client.GOOGLE_TOKEN_URI, + json={ + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + }, + ) + + with patch( + "homeassistant.components.google_sheets.async_setup_entry", return_value=True + ) as mock_setup: + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert len(mock_setup.mock_calls) == 1 + assert len(mock_client.mock_calls) == 2 + + assert result.get("type") == "create_entry" + assert result.get("title") == TITLE + assert "result" in result + assert result.get("result").unique_id == SHEET_ID + assert "token" in result.get("result").data + assert result.get("result").data["token"].get("access_token") == "mock-access-token" + assert ( + result.get("result").data["token"].get("refresh_token") == "mock-refresh-token" + ) + + +async def test_create_sheet_error( + hass: HomeAssistant, + hass_client_no_auth, + aioclient_mock, + current_request_with_host, + setup_credentials, + mock_client, +) -> None: + """Test case where creating the spreadsheet fails.""" + result = await hass.config_entries.flow.async_init( + "google_sheets", context={"source": config_entries.SOURCE_USER} + ) + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) + + assert result["url"] == ( + f"{oauth2client.GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}&scope=https://www.googleapis.com/auth/drive.file" + "&access_type=offline&prompt=consent" + ) + + client = await hass_client_no_auth() + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + # Prepare fake exception creating the spreadsheet + mock_create = Mock() + mock_create.side_effect = GSpreadException() + mock_client.return_value.create = mock_create + + aioclient_mock.post( + oauth2client.GOOGLE_TOKEN_URI, + json={ + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + }, + ) + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result.get("type") == "abort" + assert result.get("reason") == "create_spreadsheet_failure" + + +async def test_reauth( + hass: HomeAssistant, + hass_client_no_auth, + aioclient_mock, + current_request_with_host, + setup_credentials, + mock_client, +) -> None: + """Test the reauthentication case updates the existing config entry.""" + + config_entry = MockConfigEntry( + domain=DOMAIN, + unique_id=SHEET_ID, + data={ + "token": { + "access_token": "mock-access-token", + }, + }, + ) + config_entry.add_to_hass(hass) + + config_entry.async_start_reauth(hass) + await hass.async_block_till_done() + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + result = flows[0] + assert result["step_id"] == "reauth_confirm" + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) + assert result["url"] == ( + f"{oauth2client.GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}&scope=https://www.googleapis.com/auth/drive.file" + "&access_type=offline&prompt=consent" + ) + client = await hass_client_no_auth() + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + # Config flow will lookup existing key to make sure it still exists + mock_open = Mock() + mock_open.return_value.id = SHEET_ID + mock_client.return_value.open_by_key = mock_open + + aioclient_mock.post( + oauth2client.GOOGLE_TOKEN_URI, + json={ + "refresh_token": "mock-refresh-token", + "access_token": "updated-access-token", + "type": "Bearer", + "expires_in": 60, + }, + ) + + with patch( + "homeassistant.components.google_sheets.async_setup_entry", return_value=True + ) as mock_setup: + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert len(mock_setup.mock_calls) == 1 + + assert result.get("type") == "abort" + assert result.get("reason") == "reauth_successful" + + assert config_entry.unique_id == SHEET_ID + assert "token" in config_entry.data + # Verify access token is refreshed + assert config_entry.data["token"].get("access_token") == "updated-access-token" + assert config_entry.data["token"].get("refresh_token") == "mock-refresh-token" + + +async def test_reauth_abort( + hass: HomeAssistant, + hass_client_no_auth, + aioclient_mock, + current_request_with_host, + setup_credentials, + mock_client, +) -> None: + """Test failure case during reauth.""" + + config_entry = MockConfigEntry( + domain=DOMAIN, + unique_id=SHEET_ID, + data={ + "token": { + "access_token": "mock-access-token", + }, + }, + ) + config_entry.add_to_hass(hass) + + config_entry.async_start_reauth(hass) + await hass.async_block_till_done() + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + result = flows[0] + assert result["step_id"] == "reauth_confirm" + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) + assert result["url"] == ( + f"{oauth2client.GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}&scope=https://www.googleapis.com/auth/drive.file" + "&access_type=offline&prompt=consent" + ) + client = await hass_client_no_auth() + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + # Simulate failure looking up existing spreadsheet + mock_open = Mock() + mock_open.return_value.id = SHEET_ID + mock_open.side_effect = GSpreadException() + mock_client.return_value.open_by_key = mock_open + + aioclient_mock.post( + oauth2client.GOOGLE_TOKEN_URI, + json={ + "refresh_token": "mock-refresh-token", + "access_token": "updated-access-token", + "type": "Bearer", + "expires_in": 60, + }, + ) + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result.get("type") == "abort" + assert result.get("reason") == "open_spreadsheet_failure" diff --git a/tests/components/google_sheets/test_init.py b/tests/components/google_sheets/test_init.py new file mode 100644 index 00000000000..d060e01bac2 --- /dev/null +++ b/tests/components/google_sheets/test_init.py @@ -0,0 +1,214 @@ +"""Tests for Google Sheets.""" + +from collections.abc import Awaitable, Callable, Generator +import http +import time +from unittest.mock import patch + +import pytest + +from homeassistant.components.application_credentials import ( + ClientCredential, + async_import_client_credential, +) +from homeassistant.components.google_sheets import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry +from tests.test_util.aiohttp import AiohttpClientMocker + +TEST_SHEET_ID = "google-sheet-it" + +ComponentSetup = Callable[[], Awaitable[None]] + + +@pytest.fixture(name="scopes") +def mock_scopes() -> list[str]: + """Fixture to set the scopes present in the OAuth token.""" + return ["https://www.googleapis.com/auth/drive.file"] + + +@pytest.fixture(name="expires_at") +def mock_expires_at() -> int: + """Fixture to set the oauth token expiration time.""" + return time.time() + 3600 + + +@pytest.fixture(name="config_entry") +def mock_config_entry(expires_at: int, scopes: list[str]) -> MockConfigEntry: + """Fixture for MockConfigEntry.""" + return MockConfigEntry( + domain=DOMAIN, + unique_id=TEST_SHEET_ID, + data={ + "auth_implementation": DOMAIN, + "token": { + "access_token": "mock-access-token", + "refresh_token": "mock-refresh-token", + "expires_at": expires_at, + "scope": " ".join(scopes), + }, + }, + ) + + +@pytest.fixture(name="setup_integration") +async def mock_setup_integration( + hass: HomeAssistant, config_entry: MockConfigEntry +) -> Generator[ComponentSetup, None, None]: + """Fixture for setting up the component.""" + config_entry.add_to_hass(hass) + + assert await async_setup_component(hass, "application_credentials", {}) + await async_import_client_credential( + hass, + DOMAIN, + ClientCredential("client-id", "client-secret"), + DOMAIN, + ) + + async def func() -> None: + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + + yield func + + # Verify clean unload + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + await hass.config_entries.async_unload(entries[0].entry_id) + await hass.async_block_till_done() + + assert not hass.data.get(DOMAIN) + assert entries[0].state is ConfigEntryState.NOT_LOADED + + +async def test_setup_success( + hass: HomeAssistant, setup_integration: ComponentSetup +) -> None: + """Test successful setup and unload.""" + await setup_integration() + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert entries[0].state is ConfigEntryState.LOADED + + +@pytest.mark.parametrize( + "scopes", + [ + [], + [ + "https://www.googleapis.com/auth/drive.file+plus+extra" + ], # Required scope is a prefix + ["https://www.googleapis.com/auth/drive.readonly"], + ], + ids=["no_scope", "required_scope_prefix", "other_scope"], +) +async def test_missing_required_scopes_requires_reauth( + hass: HomeAssistant, setup_integration: ComponentSetup +) -> None: + """Test that reauth is invoked when required scopes are not present.""" + await setup_integration() + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert entries[0].state is ConfigEntryState.SETUP_ERROR + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + assert flows[0]["step_id"] == "reauth_confirm" + + +@pytest.mark.parametrize("expires_at", [time.time() - 3600], ids=["expired"]) +async def test_expired_token_refresh_success( + hass: HomeAssistant, + setup_integration: ComponentSetup, + scopes: list[str], + aioclient_mock: AiohttpClientMocker, +) -> None: + """Test expired token is refreshed.""" + + aioclient_mock.post( + "https://oauth2.googleapis.com/token", + json={ + "access_token": "updated-access-token", + "refresh_token": "updated-refresh-token", + "expires_at": time.time() + 3600, + "expires_in": 3600, + }, + ) + + await setup_integration() + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert entries[0].state is ConfigEntryState.LOADED + assert entries[0].data["token"]["access_token"] == "updated-access-token" + assert entries[0].data["token"]["expires_in"] == 3600 + + +@pytest.mark.parametrize( + "expires_at,status,expected_state", + [ + ( + time.time() - 3600, + http.HTTPStatus.UNAUTHORIZED, + ConfigEntryState.SETUP_ERROR, + ), + ( + time.time() - 3600, + http.HTTPStatus.INTERNAL_SERVER_ERROR, + ConfigEntryState.SETUP_RETRY, + ), + ], + ids=["failure_requires_reauth", "transient_failure"], +) +async def test_expired_token_refresh_failure( + hass: HomeAssistant, + setup_integration: ComponentSetup, + scopes: list[str], + aioclient_mock: AiohttpClientMocker, + status: http.HTTPStatus, + expected_state: ConfigEntryState, +) -> None: + """Test failure while refreshing token with a transient error.""" + + aioclient_mock.post( + "https://oauth2.googleapis.com/token", + status=status, + ) + + await setup_integration() + + # Verify a transient failure has occurred + entries = hass.config_entries.async_entries(DOMAIN) + assert entries[0].state is expected_state + + +async def test_append_sheet( + hass: HomeAssistant, + setup_integration: ComponentSetup, + config_entry: MockConfigEntry, +) -> None: + """Test successful setup and unload.""" + await setup_integration() + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert entries[0].state is ConfigEntryState.LOADED + + with patch("homeassistant.components.google_sheets.Client") as mock_client: + await hass.services.async_call( + DOMAIN, + "append_sheet", + { + "config_entry": config_entry.entry_id, + "worksheet": "Sheet1", + "data": {"foo": "bar"}, + }, + blocking=True, + ) + assert len(mock_client.mock_calls) == 8 From 8a55f8570339e4b689f8057be5db8a60db38f4ab Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 14 Sep 2022 20:45:46 +0200 Subject: [PATCH 406/955] Improve MQTT debug log of retained messages (#78453) Improve MQTT debug log for retained messages --- homeassistant/components/mqtt/client.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index 57f51593ed4..884e589ba05 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -392,7 +392,8 @@ class MQTT: self._mqttc.publish, topic, payload, qos, retain ) _LOGGER.debug( - "Transmitting message on %s: '%s', mid: %s", + "Transmitting%s message on %s: '%s', mid: %s", + " retained" if retain else "", topic, payload, msg_info.mid, @@ -610,9 +611,9 @@ class MQTT: @callback def _mqtt_handle_message(self, msg: MQTTMessage) -> None: _LOGGER.debug( - "Received message on %s%s: %s", + "Received%s message on %s: %s", + " retained" if msg.retain else "", msg.topic, - " (retained)" if msg.retain else "", msg.payload[0:8192], ) timestamp = dt_util.utcnow() From 6678f660a8a1bb11ab11d857b6afc23289569a82 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 14 Sep 2022 20:51:24 +0200 Subject: [PATCH 407/955] Remove U.S. Citizenship and Immigration Services (USCIS) integration (#78432) * Remove U.S. Citizenship and Immigration Services (USCIS) integration * Update .coveragerc Fix due to sorting in https://github.com/home-assistant/core/pull/78447 Co-authored-by: Shay Levy --- .coveragerc | 1 - homeassistant/components/uscis/__init__.py | 1 - homeassistant/components/uscis/manifest.json | 9 -- homeassistant/components/uscis/sensor.py | 100 ------------------ homeassistant/components/uscis/strings.json | 8 -- .../components/uscis/translations/ca.json | 8 -- .../components/uscis/translations/de.json | 8 -- .../components/uscis/translations/el.json | 8 -- .../components/uscis/translations/en.json | 8 -- .../components/uscis/translations/es.json | 8 -- .../components/uscis/translations/et.json | 8 -- .../components/uscis/translations/fr.json | 7 -- .../components/uscis/translations/hu.json | 8 -- .../components/uscis/translations/id.json | 8 -- .../components/uscis/translations/it.json | 8 -- .../components/uscis/translations/ja.json | 8 -- .../components/uscis/translations/nl.json | 7 -- .../components/uscis/translations/no.json | 8 -- .../components/uscis/translations/pl.json | 8 -- .../components/uscis/translations/pt-BR.json | 8 -- .../components/uscis/translations/ru.json | 8 -- .../components/uscis/translations/sv.json | 8 -- .../components/uscis/translations/tr.json | 8 -- .../uscis/translations/zh-Hant.json | 8 -- requirements_all.txt | 3 - 25 files changed, 272 deletions(-) delete mode 100644 homeassistant/components/uscis/__init__.py delete mode 100644 homeassistant/components/uscis/manifest.json delete mode 100644 homeassistant/components/uscis/sensor.py delete mode 100644 homeassistant/components/uscis/strings.json delete mode 100644 homeassistant/components/uscis/translations/ca.json delete mode 100644 homeassistant/components/uscis/translations/de.json delete mode 100644 homeassistant/components/uscis/translations/el.json delete mode 100644 homeassistant/components/uscis/translations/en.json delete mode 100644 homeassistant/components/uscis/translations/es.json delete mode 100644 homeassistant/components/uscis/translations/et.json delete mode 100644 homeassistant/components/uscis/translations/fr.json delete mode 100644 homeassistant/components/uscis/translations/hu.json delete mode 100644 homeassistant/components/uscis/translations/id.json delete mode 100644 homeassistant/components/uscis/translations/it.json delete mode 100644 homeassistant/components/uscis/translations/ja.json delete mode 100644 homeassistant/components/uscis/translations/nl.json delete mode 100644 homeassistant/components/uscis/translations/no.json delete mode 100644 homeassistant/components/uscis/translations/pl.json delete mode 100644 homeassistant/components/uscis/translations/pt-BR.json delete mode 100644 homeassistant/components/uscis/translations/ru.json delete mode 100644 homeassistant/components/uscis/translations/sv.json delete mode 100644 homeassistant/components/uscis/translations/tr.json delete mode 100644 homeassistant/components/uscis/translations/zh-Hant.json diff --git a/.coveragerc b/.coveragerc index 0a37c94d57b..ec883a005d5 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1392,7 +1392,6 @@ omit = homeassistant/components/upnp/__init__.py homeassistant/components/upnp/device.py homeassistant/components/upnp/sensor.py - homeassistant/components/uscis/sensor.py homeassistant/components/vallox/__init__.py homeassistant/components/vallox/fan.py homeassistant/components/vallox/sensor.py diff --git a/homeassistant/components/uscis/__init__.py b/homeassistant/components/uscis/__init__.py deleted file mode 100644 index f45e0ab9353..00000000000 --- a/homeassistant/components/uscis/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The uscis component.""" diff --git a/homeassistant/components/uscis/manifest.json b/homeassistant/components/uscis/manifest.json deleted file mode 100644 index 0680848f70a..00000000000 --- a/homeassistant/components/uscis/manifest.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "domain": "uscis", - "name": "U.S. Citizenship and Immigration Services (USCIS)", - "documentation": "https://www.home-assistant.io/integrations/uscis", - "requirements": ["uscisstatus==0.1.1"], - "codeowners": [], - "iot_class": "cloud_polling", - "loggers": ["uscisstatus"] -} diff --git a/homeassistant/components/uscis/sensor.py b/homeassistant/components/uscis/sensor.py deleted file mode 100644 index a26bb655c58..00000000000 --- a/homeassistant/components/uscis/sensor.py +++ /dev/null @@ -1,100 +0,0 @@ -"""Support for USCIS Case Status.""" -from __future__ import annotations - -from datetime import timedelta -import logging - -import uscisstatus -import voluptuous as vol - -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.const import CONF_NAME -from homeassistant.core import HomeAssistant -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.issue_registry import IssueSeverity, create_issue -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.util import Throttle - -_LOGGER = logging.getLogger(__name__) - -DEFAULT_NAME = "USCIS" - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Required("case_id"): cv.string, - } -) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the platform in Home Assistant and Case Information.""" - create_issue( - hass, - "uscis", - "pending_removal", - breaks_in_ha_version="2022.10.0", - is_fixable=False, - severity=IssueSeverity.WARNING, - translation_key="pending_removal", - ) - _LOGGER.warning( - "The USCIS sensor component is deprecated and will be removed in Home Assistant 2022.10" - ) - uscis = UscisSensor(config["case_id"], config[CONF_NAME]) - uscis.update() - if uscis.valid_case_id: - add_entities([uscis]) - else: - _LOGGER.error("Setup USCIS Sensor Fail check if your Case ID is Valid") - - -class UscisSensor(SensorEntity): - """USCIS Sensor will check case status on daily basis.""" - - MIN_TIME_BETWEEN_UPDATES = timedelta(hours=24) - - CURRENT_STATUS = "current_status" - LAST_CASE_UPDATE = "last_update_date" - - def __init__(self, case, name): - """Initialize the sensor.""" - self._state = None - self._case_id = case - self._attributes = None - self.valid_case_id = None - self._name = name - - @property - def name(self): - """Return the name.""" - return self._name - - @property - def native_value(self): - """Return the state.""" - return self._state - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - return self._attributes - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Fetch data from the USCIS website and update state attributes.""" - try: - status = uscisstatus.get_case_status(self._case_id) - self._attributes = {self.CURRENT_STATUS: status["status"]} - self._state = status["date"] - self.valid_case_id = True - - except ValueError: - _LOGGER("Please Check that you have valid USCIS case id") - self.valid_case_id = False diff --git a/homeassistant/components/uscis/strings.json b/homeassistant/components/uscis/strings.json deleted file mode 100644 index b8dec86db18..00000000000 --- a/homeassistant/components/uscis/strings.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "issues": { - "pending_removal": { - "title": "The USCIS integration is being removed", - "description": "The U.S. Citizenship and Immigration Services (USCIS) integration is pending removal from Home Assistant and will no longer be available as of Home Assistant 2022.10.\n\nThe integration is being removed, because it relies on webscraping, which is not allowed.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." - } - } -} diff --git a/homeassistant/components/uscis/translations/ca.json b/homeassistant/components/uscis/translations/ca.json deleted file mode 100644 index 858214eee34..00000000000 --- a/homeassistant/components/uscis/translations/ca.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "issues": { - "pending_removal": { - "description": "La integraci\u00f3 de Serveis de Ciutadania i Immigraci\u00f3 dels Estats Units (USCIS) est\u00e0 pendent d'eliminar-se de Home Assistant i ja no estar\u00e0 disponible a partir de Home Assistant 2022.10. \n\nLa integraci\u00f3 s'est\u00e0 eliminant, perqu\u00e8 es basa en el 'webscraping', que no est\u00e0 adm\u00e8s. \n\nElimina la configuraci\u00f3 YAML corresponent del fitxer configuration.yaml i reinicia Home Assistant per arreglar aquest error.", - "title": "La integraci\u00f3 USCIS est\u00e0 sent eliminada" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/uscis/translations/de.json b/homeassistant/components/uscis/translations/de.json deleted file mode 100644 index 273a340d892..00000000000 --- a/homeassistant/components/uscis/translations/de.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "issues": { - "pending_removal": { - "description": "Die Integration der U.S. Citizenship and Immigration Services (USCIS) wird aus Home Assistant entfernt und steht ab Home Assistant 2022.10 nicht mehr zur Verf\u00fcgung.\n\nDie Integration wird entfernt, weil sie auf Webscraping beruht, was nicht erlaubt ist.\n\nEntferne die YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", - "title": "Die USCIS-Integration wird entfernt" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/uscis/translations/el.json b/homeassistant/components/uscis/translations/el.json deleted file mode 100644 index d89ae50773a..00000000000 --- a/homeassistant/components/uscis/translations/el.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "issues": { - "pending_removal": { - "description": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03c9\u03bd \u03a5\u03c0\u03b7\u03c1\u03b5\u03c3\u03b9\u03ce\u03bd \u0399\u03b8\u03b1\u03b3\u03ad\u03bd\u03b5\u03b9\u03b1\u03c2 \u03ba\u03b1\u03b9 \u039c\u03b5\u03c4\u03b1\u03bd\u03ac\u03c3\u03c4\u03b5\u03c5\u03c3\u03b7\u03c2 \u03c4\u03c9\u03bd \u0397\u03a0\u0391 (USCIS) \u03b5\u03ba\u03ba\u03c1\u03b5\u03bc\u03b5\u03af \u03c0\u03c1\u03bf\u03c2 \u03b1\u03c6\u03b1\u03af\u03c1\u03b5\u03c3\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf Home Assistant \u03ba\u03b1\u03b9 \u03b4\u03b5\u03bd \u03b8\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf Home Assistant 2022.10.\n\n\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03b1\u03c6\u03b1\u03b9\u03c1\u03b5\u03af\u03c4\u03b1\u03b9, \u03b5\u03c0\u03b5\u03b9\u03b4\u03ae \u03b2\u03b1\u03c3\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03c3\u03b5 webscraping, \u03c4\u03bf \u03bf\u03c0\u03bf\u03af\u03bf \u03b4\u03b5\u03bd \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03b5\u03c4\u03b1\u03b9.\n\n\u0391\u03c6\u03b1\u03b9\u03c1\u03ad\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", - "title": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 USCIS \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/uscis/translations/en.json b/homeassistant/components/uscis/translations/en.json deleted file mode 100644 index 24e7e9ceea0..00000000000 --- a/homeassistant/components/uscis/translations/en.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "issues": { - "pending_removal": { - "description": "The U.S. Citizenship and Immigration Services (USCIS) integration is pending removal from Home Assistant and will no longer be available as of Home Assistant 2022.10.\n\nThe integration is being removed, because it relies on webscraping, which is not allowed.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", - "title": "The USCIS integration is being removed" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/uscis/translations/es.json b/homeassistant/components/uscis/translations/es.json deleted file mode 100644 index cb8689e1b8b..00000000000 --- a/homeassistant/components/uscis/translations/es.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "issues": { - "pending_removal": { - "description": "La integraci\u00f3n de los Servicios de Inmigraci\u00f3n y Ciudadan\u00eda de los EE.UU. (USCIS) est\u00e1 pendiente de eliminaci\u00f3n de Home Assistant y ya no estar\u00e1 disponible a partir de Home Assistant 2022.10. \n\nSe va a eliminar la integraci\u00f3n porque se basa en webscraping, algo que no est\u00e1 permitido. \n\nElimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", - "title": "Se va a eliminar la integraci\u00f3n USCIS" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/uscis/translations/et.json b/homeassistant/components/uscis/translations/et.json deleted file mode 100644 index 0dc9325b715..00000000000 --- a/homeassistant/components/uscis/translations/et.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "issues": { - "pending_removal": { - "description": "USA kodakondsus- ja immigratsiooniteenistuse (USCIS) integratsioon ootab eemaldamist Home Assistantist ja ei ole enam k\u00e4ttesaadav alates Home Assistant 2022.10.\n\nIntegratsioon eemaldatakse, sest see p\u00f5hineb veebiotsingul, mis ei ole lubatud.\n\nProbleemi lahendamiseks eemaldage YAML-konfiguratsioon failist configuration.yaml ja k\u00e4ivitage Home Assistant uuesti.", - "title": "USCIS-i sidumine eemaldatakse" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/uscis/translations/fr.json b/homeassistant/components/uscis/translations/fr.json deleted file mode 100644 index 8d9e1c3f8ba..00000000000 --- a/homeassistant/components/uscis/translations/fr.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "issues": { - "pending_removal": { - "title": "L'int\u00e9gration USCIS est en cours de suppression" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/uscis/translations/hu.json b/homeassistant/components/uscis/translations/hu.json deleted file mode 100644 index 181cbaf076f..00000000000 --- a/homeassistant/components/uscis/translations/hu.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "issues": { - "pending_removal": { - "description": "A U.S. Citizenship and Immigration Services (USCIS) integr\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra v\u00e1r a Home Assistantb\u00f3l, \u00e9s a Home Assistant 2022.10-t\u0151l m\u00e1r nem lesz el\u00e9rhet\u0151.\n\nAz integr\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sa az\u00e9rt t\u00f6rt\u00e9nik, mert webscrapingre t\u00e1maszkodik, ami nem megengedett.\n\nA probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", - "title": "Az USCIS integr\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/uscis/translations/id.json b/homeassistant/components/uscis/translations/id.json deleted file mode 100644 index 37e7278a916..00000000000 --- a/homeassistant/components/uscis/translations/id.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "issues": { - "pending_removal": { - "description": "Integrasi Layanan Kewarganegaraan dan Imigrasi AS (USCIS) sedang menunggu penghapusan dari Home Assistant dan tidak akan lagi tersedia pada Home Assistant 2022.10.\n\nIntegrasi ini dalam proses penghapusan, karena bergantung pada proses webscraping, yang tidak diizinkan.\n\nHapus konfigurasi YAML dari file configuration.yaml Anda dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", - "title": "Integrasi USCIS dalam proses penghapusan" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/uscis/translations/it.json b/homeassistant/components/uscis/translations/it.json deleted file mode 100644 index 1cb9e54a6b7..00000000000 --- a/homeassistant/components/uscis/translations/it.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "issues": { - "pending_removal": { - "description": "L'integrazione U.S. Citizenship and Immigration Services (USCIS) \u00e8 in attesa di rimozione da Home Assistant e non sar\u00e0 pi\u00f9 disponibile a partire da Home Assistant 2022.10. \n\nL'integrazione sar\u00e0 rimossa, perch\u00e9 si basa sul webscraping, che non \u00e8 consentito. \n\nRimuovi la configurazione YAML dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", - "title": "L'integrazione USCIS verr\u00e0 rimossa" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/uscis/translations/ja.json b/homeassistant/components/uscis/translations/ja.json deleted file mode 100644 index 46f021952d8..00000000000 --- a/homeassistant/components/uscis/translations/ja.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "issues": { - "pending_removal": { - "description": "\u7c73\u56fd\u5e02\u6c11\u6a29\u79fb\u6c11\u5c40(US Citizenship and Immigration Services (USCIS))\u306e\u7d71\u5408\u306f\u3001Home Assistant\u304b\u3089\u306e\u524a\u9664\u306f\u4fdd\u7559\u3055\u308c\u3066\u3044\u307e\u3059\u304c\u3001Home Assistant 2022.10\u4ee5\u964d\u306f\u5229\u7528\u3067\u304d\u306a\u304f\u306a\u308a\u307e\u3059\u3002 \n\n\u3053\u306e\u7d71\u5408\u306f\u3001\u8a31\u53ef\u3055\u308c\u3066\u3044\u306a\u3044Web\u30b9\u30af\u30ec\u30a4\u30d4\u30f3\u30b0\u306b\u4f9d\u5b58\u3057\u3066\u3044\u308b\u305f\u3081\u3001\u524a\u9664\u3055\u308c\u308b\u4e88\u5b9a\u3067\u3059\u3002\n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089YAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", - "title": "USCIS\u306e\u7d71\u5408\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/uscis/translations/nl.json b/homeassistant/components/uscis/translations/nl.json deleted file mode 100644 index b4f2be41ed0..00000000000 --- a/homeassistant/components/uscis/translations/nl.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "issues": { - "pending_removal": { - "title": "De USCIS-integratie wordt verwijderd" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/uscis/translations/no.json b/homeassistant/components/uscis/translations/no.json deleted file mode 100644 index a4db40941ed..00000000000 --- a/homeassistant/components/uscis/translations/no.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "issues": { - "pending_removal": { - "description": "Integrasjonen med US Citizenship and Immigration Services (USCIS) venter p\u00e5 fjerning fra Home Assistant og vil ikke lenger v\u00e6re tilgjengelig fra og med Home Assistant 2022.10. \n\n Integrasjonen blir fjernet, fordi den er avhengig av webscraping, noe som ikke er tillatt. \n\n Fjern YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", - "title": "USCIS-integrasjonen fjernes" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/uscis/translations/pl.json b/homeassistant/components/uscis/translations/pl.json deleted file mode 100644 index 82e02996f7c..00000000000 --- a/homeassistant/components/uscis/translations/pl.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "issues": { - "pending_removal": { - "description": "Integracja US Citizenship and Immigration Services (USCIS) oczekuje na usuni\u0119cie z Home Assistanta i nie b\u0119dzie ju\u017c dost\u0119pna od Home Assistant 2022.10. \n\nIntegracja jest usuwana, poniewa\u017c opiera si\u0119 na webscrapingu, co jest niedozwolone. \n\nUsu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", - "title": "Trwa usuwanie integracji USCIS" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/uscis/translations/pt-BR.json b/homeassistant/components/uscis/translations/pt-BR.json deleted file mode 100644 index 76182bfa2d2..00000000000 --- a/homeassistant/components/uscis/translations/pt-BR.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "issues": { - "pending_removal": { - "description": "A integra\u00e7\u00e3o dos Servi\u00e7os de Cidadania e Imigra\u00e7\u00e3o dos EUA (USCIS) est\u00e1 pendente de remo\u00e7\u00e3o do Home Assistant e n\u00e3o estar\u00e1 mais dispon\u00edvel a partir do Home Assistant 2022.10. \n\n A integra\u00e7\u00e3o est\u00e1 sendo removida, pois depende de webscraping, o que n\u00e3o \u00e9 permitido. \n\n Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", - "title": "A integra\u00e7\u00e3o do USCIS est\u00e1 sendo removida" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/uscis/translations/ru.json b/homeassistant/components/uscis/translations/ru.json deleted file mode 100644 index d6cd9da954e..00000000000 --- a/homeassistant/components/uscis/translations/ru.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "issues": { - "pending_removal": { - "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0421\u043b\u0443\u0436\u0431\u044b \u0433\u0440\u0430\u0436\u0434\u0430\u043d\u0441\u0442\u0432\u0430 \u0438 \u0438\u043c\u043c\u0438\u0433\u0440\u0430\u0446\u0438\u0438 \u0421\u0428\u0410 (USCIS) \u043e\u0436\u0438\u0434\u0430\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u044f \u0438\u0437 Home Assistant \u0438 \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0431\u0443\u0434\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u0441 Home Assistant \u0432\u0435\u0440\u0441\u0438\u0438 2022.10. \n\n\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u043e\u043d\u0430 \u043e\u0441\u043d\u043e\u0432\u0430\u043d\u0430 \u043d\u0430 \u0432\u0435\u0431-\u0441\u043a\u0440\u0430\u043f\u0438\u043d\u0433\u0435, \u0447\u0442\u043e \u0437\u0430\u043f\u0440\u0435\u0449\u0435\u043d\u043e. \n\n\u0423\u0434\u0430\u043b\u0438\u0442\u0435 YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \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": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f USCIS \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/uscis/translations/sv.json b/homeassistant/components/uscis/translations/sv.json deleted file mode 100644 index a139ce02491..00000000000 --- a/homeassistant/components/uscis/translations/sv.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "issues": { - "pending_removal": { - "description": "Integrationen av US Citizenship and Immigration Services (USCIS) v\u00e4ntar p\u00e5 borttagning fr\u00e5n Home Assistant och kommer inte l\u00e4ngre att vara tillg\u00e4nglig fr\u00e5n och med Home Assistant 2022.10. \n\n Integrationen tas bort eftersom den f\u00f6rlitar sig p\u00e5 webbskrapning, vilket inte \u00e4r till\u00e5tet. \n\n Ta bort YAML-konfigurationen fr\u00e5n filen configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", - "title": "USCIS-integrationen tas bort" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/uscis/translations/tr.json b/homeassistant/components/uscis/translations/tr.json deleted file mode 100644 index 0e27f5bdac5..00000000000 --- a/homeassistant/components/uscis/translations/tr.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "issues": { - "pending_removal": { - "description": "ABD Vatanda\u015fl\u0131k ve G\u00f6\u00e7menlik Hizmetleri (USCIS) entegrasyonu, Home Assistant'tan kald\u0131r\u0131lmay\u0131 bekliyor ve Home Assistant 2022.10'dan itibaren art\u0131k kullan\u0131lamayacak. \n\n Entegrasyon kald\u0131r\u0131l\u0131yor, \u00e7\u00fcnk\u00fc izin verilmeyen web taramaya dayan\u0131yor. \n\n YAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu gidermek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", - "title": "USCIS entegrasyonu kald\u0131r\u0131l\u0131yor" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/uscis/translations/zh-Hant.json b/homeassistant/components/uscis/translations/zh-Hant.json deleted file mode 100644 index ccc72d3d1dd..00000000000 --- a/homeassistant/components/uscis/translations/zh-Hant.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "issues": { - "pending_removal": { - "description": "\u7f8e\u570b\u516c\u6c11\u8207\u79fb\u6c11\u670d\u52d9\uff08USCIS: U.S. Citizenship and Immigration Services\uff09\u6574\u5408\u5373\u5c07\u7531 Home Assistant \u4e2d\u79fb\u9664\u3001\u4e26\u65bc Home Assistant 2022.10 \u7248\u5f8c\u7121\u6cd5\u518d\u4f7f\u7528\u3002\n\n\u7531\u65bc\u4f7f\u7528\u4e86\u4e0d\u88ab\u5141\u8a31\u7684\u7db2\u8def\u8cc7\u6599\u64f7\u53d6\uff08webscraping\uff09\u65b9\u5f0f\u3001\u6574\u5408\u5373\u5c07\u79fb\u9664\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant to \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", - "title": "USCIS \u6574\u5408\u5373\u5c07\u79fb\u9664" - } - } -} \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index 72c3c86d9e7..e2e1bc34a8b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2451,9 +2451,6 @@ upcloud-api==2.0.0 # homeassistant.components.zwave_me url-normalize==1.4.3 -# homeassistant.components.uscis -uscisstatus==0.1.1 - # homeassistant.components.uvc uvcclient==0.11.0 From 43053d05b4c192cb114865127d3b1ada12d777aa Mon Sep 17 00:00:00 2001 From: Teemu R Date: Wed, 14 Sep 2022 21:54:01 +0200 Subject: [PATCH 408/955] Bump python-songpal to 0.15.1 (#78481) --- homeassistant/components/songpal/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/songpal/manifest.json b/homeassistant/components/songpal/manifest.json index 2aa58b16a7e..1fb61547445 100644 --- a/homeassistant/components/songpal/manifest.json +++ b/homeassistant/components/songpal/manifest.json @@ -3,7 +3,7 @@ "name": "Sony Songpal", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/songpal", - "requirements": ["python-songpal==0.15"], + "requirements": ["python-songpal==0.15.1"], "codeowners": ["@rytilahti", "@shenxn"], "ssdp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index e2e1bc34a8b..d00b1b6e48c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2011,7 +2011,7 @@ python-ripple-api==0.0.3 python-smarttub==0.0.33 # homeassistant.components.songpal -python-songpal==0.15 +python-songpal==0.15.1 # homeassistant.components.tado python-tado==0.12.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 56dd954db46..41cec9e9b3c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1380,7 +1380,7 @@ python-picnic-api==1.1.0 python-smarttub==0.0.33 # homeassistant.components.songpal -python-songpal==0.15 +python-songpal==0.15.1 # homeassistant.components.tado python-tado==0.12.0 From 1b3088e41aad9e3903b719ee2f614e2de5d48835 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Thu, 15 Sep 2022 00:29:59 +0300 Subject: [PATCH 409/955] Bump aioswitcher to 3.0.0 (#78471) --- homeassistant/components/switcher_kis/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switcher_kis/manifest.json b/homeassistant/components/switcher_kis/manifest.json index f5b7bdae148..30a2ac3bb48 100644 --- a/homeassistant/components/switcher_kis/manifest.json +++ b/homeassistant/components/switcher_kis/manifest.json @@ -3,7 +3,7 @@ "name": "Switcher", "documentation": "https://www.home-assistant.io/integrations/switcher_kis/", "codeowners": ["@tomerfi", "@thecode"], - "requirements": ["aioswitcher==2.0.6"], + "requirements": ["aioswitcher==3.0.0"], "quality_scale": "platinum", "iot_class": "local_push", "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index d00b1b6e48c..cc72695b4ad 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -262,7 +262,7 @@ aioslimproto==2.1.1 aiosteamist==0.3.2 # homeassistant.components.switcher_kis -aioswitcher==2.0.6 +aioswitcher==3.0.0 # homeassistant.components.syncthing aiosyncthing==0.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 41cec9e9b3c..d8be23fa019 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -237,7 +237,7 @@ aioslimproto==2.1.1 aiosteamist==0.3.2 # homeassistant.components.switcher_kis -aioswitcher==2.0.6 +aioswitcher==3.0.0 # homeassistant.components.syncthing aiosyncthing==0.5.1 From bcf01e88733b5c268ca97755063a0f2dedb08bdd Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 15 Sep 2022 00:29:28 +0000 Subject: [PATCH 410/955] [ci skip] Translation update --- .../android_ip_webcam/translations/cs.json | 1 + .../components/awair/translations/cs.json | 18 +++++ .../components/brunt/translations/cs.json | 1 + .../devolo_home_network/translations/cs.json | 7 ++ .../components/escea/translations/cs.json | 3 +- .../fully_kiosk/translations/cs.json | 3 + .../geocaching/translations/cs.json | 1 + .../google_sheets/translations/fr.json | 28 +++++++ .../google_sheets/translations/hu.json | 31 ++++++++ .../google_sheets/translations/pt-BR.json | 31 ++++++++ .../google_sheets/translations/ru.json | 31 ++++++++ .../homeassistant_alerts/translations/cs.json | 8 ++ .../components/lametric/translations/cs.json | 8 +- .../landisgyr_heat_meter/translations/cs.json | 7 ++ .../components/life360/translations/cs.json | 1 + .../components/lookin/translations/cs.json | 5 ++ .../components/mill/translations/cs.json | 5 ++ .../components/nest/translations/cs.json | 1 + .../components/nobo_hub/translations/cs.json | 1 + .../openexchangerates/translations/cs.json | 11 ++- .../opentherm_gw/translations/cs.json | 3 +- .../components/plugwise/translations/cs.json | 3 +- .../components/pushover/translations/cs.json | 10 +++ .../components/risco/translations/cs.json | 5 +- .../components/scrape/translations/cs.json | 3 + .../components/senz/translations/cs.json | 1 + .../components/skybell/translations/cs.json | 1 + .../components/sleepiq/translations/cs.json | 1 + .../components/smhi/translations/cs.json | 3 + .../components/sql/translations/cs.json | 7 ++ .../components/switchbee/translations/ca.json | 32 ++++++++ .../components/switchbee/translations/hu.json | 32 ++++++++ .../components/switchbee/translations/ru.json | 32 ++++++++ .../trafikverket_ferry/translations/cs.json | 1 + .../trafikverket_train/translations/cs.json | 1 + .../translations/cs.json | 3 + .../volvooncall/translations/cs.json | 1 + .../components/ws66i/translations/cs.json | 7 ++ .../yalexs_ble/translations/cs.json | 3 +- .../components/yolink/translations/cs.json | 1 + .../components/zha/translations/cs.json | 79 ++++++++++++++++++- 41 files changed, 423 insertions(+), 8 deletions(-) create mode 100644 homeassistant/components/google_sheets/translations/fr.json create mode 100644 homeassistant/components/google_sheets/translations/hu.json create mode 100644 homeassistant/components/google_sheets/translations/pt-BR.json create mode 100644 homeassistant/components/google_sheets/translations/ru.json create mode 100644 homeassistant/components/homeassistant_alerts/translations/cs.json create mode 100644 homeassistant/components/sql/translations/cs.json create mode 100644 homeassistant/components/switchbee/translations/ca.json create mode 100644 homeassistant/components/switchbee/translations/hu.json create mode 100644 homeassistant/components/switchbee/translations/ru.json diff --git a/homeassistant/components/android_ip_webcam/translations/cs.json b/homeassistant/components/android_ip_webcam/translations/cs.json index 20f0a9bf4fb..543988bca9b 100644 --- a/homeassistant/components/android_ip_webcam/translations/cs.json +++ b/homeassistant/components/android_ip_webcam/translations/cs.json @@ -12,6 +12,7 @@ "data": { "host": "Hostitel", "password": "Heslo", + "port": "Port", "username": "U\u017eivatelsk\u00e9 jm\u00e9no" } } diff --git a/homeassistant/components/awair/translations/cs.json b/homeassistant/components/awair/translations/cs.json index 3b0110001b3..6821388f2d4 100644 --- a/homeassistant/components/awair/translations/cs.json +++ b/homeassistant/components/awair/translations/cs.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "\u00da\u010det je ji\u017e nastaven", + "already_configured_account": "\u00da\u010det je ji\u017e nastaven", "already_configured_device": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed", "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9", @@ -12,10 +13,27 @@ "unknown": "Neo\u010dek\u00e1van\u00e1 chyba", "unreachable": "Nepoda\u0159ilo se p\u0159ipojit" }, + "flow_title": "{model} ({device_id})", "step": { + "cloud": { + "data": { + "access_token": "P\u0159\u00edstupov\u00fd token", + "email": "E-mail" + } + }, "discovery_confirm": { "description": "Chcete nastavit {model} ({device_id})?" }, + "local": { + "data": { + "host": "IP adresa" + } + }, + "local_pick": { + "data": { + "host": "IP adresa" + } + }, "reauth": { "data": { "access_token": "P\u0159\u00edstupov\u00fd token", diff --git a/homeassistant/components/brunt/translations/cs.json b/homeassistant/components/brunt/translations/cs.json index 5ac20c2bde8..e5d6edc65ea 100644 --- a/homeassistant/components/brunt/translations/cs.json +++ b/homeassistant/components/brunt/translations/cs.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u00da\u010det je ji\u017e nastaven", "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "error": { diff --git a/homeassistant/components/devolo_home_network/translations/cs.json b/homeassistant/components/devolo_home_network/translations/cs.json index 0887542d784..04f18366eaf 100644 --- a/homeassistant/components/devolo_home_network/translations/cs.json +++ b/homeassistant/components/devolo_home_network/translations/cs.json @@ -6,6 +6,13 @@ "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "ip_address": "IP adresa" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/escea/translations/cs.json b/homeassistant/components/escea/translations/cs.json index 3f0012e00d2..d2e2907679e 100644 --- a/homeassistant/components/escea/translations/cs.json +++ b/homeassistant/components/escea/translations/cs.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed" + "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed", + "single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace." } } } \ No newline at end of file diff --git a/homeassistant/components/fully_kiosk/translations/cs.json b/homeassistant/components/fully_kiosk/translations/cs.json index dd68d899002..737979c68e8 100644 --- a/homeassistant/components/fully_kiosk/translations/cs.json +++ b/homeassistant/components/fully_kiosk/translations/cs.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u00da\u010det je ji\u017e nastaven" + }, "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", diff --git a/homeassistant/components/geocaching/translations/cs.json b/homeassistant/components/geocaching/translations/cs.json index 0ae00fc3605..5b7d9c2db8e 100644 --- a/homeassistant/components/geocaching/translations/cs.json +++ b/homeassistant/components/geocaching/translations/cs.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u00da\u010det je ji\u017e nastaven", "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, diff --git a/homeassistant/components/google_sheets/translations/fr.json b/homeassistant/components/google_sheets/translations/fr.json new file mode 100644 index 00000000000..74f69cf8ebe --- /dev/null +++ b/homeassistant/components/google_sheets/translations/fr.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", + "cannot_connect": "\u00c9chec de connexion", + "create_spreadsheet_failure": "Erreur lors de la cr\u00e9ation de la feuille de calcul, consultez le journal des erreurs pour plus de d\u00e9tails", + "invalid_access_token": "Jeton d'acc\u00e8s non valide", + "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation.", + "oauth_error": "Des donn\u00e9es de jeton non valides ont \u00e9t\u00e9 re\u00e7ues.", + "open_spreadsheet_failure": "Erreur lors de l'ouverture de la feuille de calcul, consultez le journal des erreurs pour plus de d\u00e9tails", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi", + "timeout_connect": "D\u00e9lai d'attente pour \u00e9tablir la connexion expir\u00e9", + "unknown": "Erreur inattendue" + }, + "create_entry": { + "default": "Authentification r\u00e9ussie\u00a0; feuille de calcul cr\u00e9\u00e9e \u00e0 l'adresse\u00a0: {url}" + }, + "step": { + "auth": { + "title": "Associer un compte Google" + }, + "pick_implementation": { + "title": "S\u00e9lectionner une m\u00e9thode d'authentification" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_sheets/translations/hu.json b/homeassistant/components/google_sheets/translations/hu.json new file mode 100644 index 00000000000..38afcd5db15 --- /dev/null +++ b/homeassistant/components/google_sheets/translations/hu.json @@ -0,0 +1,31 @@ +{ + "application_credentials": { + "description": "K\u00f6vesse az [utas\u00edt\u00e1sokat]({more_info_url}) az [OAuth hozz\u00e1j\u00e1rul\u00e1si k\u00e9perny\u0151]({oauth_consent_url}) eset\u00e9ben, hogy a Home Assistant hozz\u00e1f\u00e9rhessen a Google Sheets adatlapjaihoz. A fi\u00f3kj\u00e1hoz kapcsol\u00f3d\u00f3 Alkalmaz\u00e1si hiteles\u00edt\u0151 adatokat is l\u00e9tre kell hoznia:\n1. Menjen a [Hiteles\u00edt\u00e9si adatok]({oauth_creds_url}) men\u00fcpontra, \u00e9s kattintson a **Hiteles\u00edt\u00e9si adatok l\u00e9trehoz\u00e1sa** gombra.\n1. A leg\u00f6rd\u00fcl\u0151 list\u00e1b\u00f3l v\u00e1lassza ki az **OAuth \u00fcgyf\u00e9l azonos\u00edt\u00f3t**.\n1. Az Alkalmaz\u00e1s t\u00edpus\u00e1hoz v\u00e1lassza a **Webalkalmaz\u00e1s** lehet\u0151s\u00e9get.\n\n" + }, + "config": { + "abort": { + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "create_spreadsheet_failure": "Hiba a t\u00e1bl\u00e1zat l\u00e9trehoz\u00e1sa k\u00f6zben, a r\u00e9szletek\u00e9rt tekintse meg a hibanapl\u00f3t", + "invalid_access_token": "\u00c9rv\u00e9nytelen hozz\u00e1f\u00e9r\u00e9si token", + "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rem, k\u00f6vesse a dokument\u00e1ci\u00f3t.", + "oauth_error": "\u00c9rv\u00e9nytelen token adatok \u00e9rkeztek.", + "open_spreadsheet_failure": "Hiba a t\u00e1bl\u00e1zat megnyit\u00e1sakor, a r\u00e9szletek\u00e9rt tekintse meg a hibanapl\u00f3t", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt.", + "timeout_connect": "Id\u0151t\u00fall\u00e9p\u00e9s a kapcsolat l\u00e9trehoz\u00e1sa sor\u00e1n", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "create_entry": { + "default": "Sikeresen hiteles\u00edtett \u00e9s l\u00e9trehozott t\u00e1bl\u00e1zat a k\u00f6vetkez\u0151 helyen: {url}" + }, + "step": { + "auth": { + "title": "Google-fi\u00f3k \u00f6sszekapcsol\u00e1sa" + }, + "pick_implementation": { + "title": "V\u00e1lasszon egy hiteles\u00edt\u00e9si m\u00f3dszert" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_sheets/translations/pt-BR.json b/homeassistant/components/google_sheets/translations/pt-BR.json new file mode 100644 index 00000000000..e8b4f23a4ed --- /dev/null +++ b/homeassistant/components/google_sheets/translations/pt-BR.json @@ -0,0 +1,31 @@ +{ + "application_credentials": { + "description": "Siga as [instru\u00e7\u00f5es]( {more_info_url} ) para a [tela de consentimento do OAuth]( {oauth_consent_url} ) para conceder ao Home Assistant acesso as suas Planilhas Google. Voc\u00ea tamb\u00e9m precisa criar credenciais de aplicativo vinculadas \u00e0 sua conta:\n 1. Acesse [Credentials]( {oauth_creds_url} ) e clique em **Create Credentials**.\n 1. Na lista suspensa, selecione **ID do cliente OAuth**.\n 1. Selecione **Aplicativo da Web** para o Tipo de aplicativo. \n\n" + }, + "config": { + "abort": { + "already_configured": "A conta j\u00e1 est\u00e1 configurada", + "already_in_progress": "A configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "cannot_connect": "Falhou ao conectar", + "create_spreadsheet_failure": "Erro ao criar planilha, veja o log de erros para detalhes", + "invalid_access_token": "Token de acesso inv\u00e1lido", + "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.", + "oauth_error": "Dados de token inv\u00e1lidos recebidos.", + "open_spreadsheet_failure": "Erro ao abrir a planilha, veja o log de erros para detalhes", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida", + "timeout_connect": "Tempo limite estabelecendo conex\u00e3o", + "unknown": "Erro inesperado" + }, + "create_entry": { + "default": "Autentica\u00e7\u00e3o com sucesso e planilha criada em: {url}" + }, + "step": { + "auth": { + "title": "Vincular Conta do Google" + }, + "pick_implementation": { + "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_sheets/translations/ru.json b/homeassistant/components/google_sheets/translations/ru.json new file mode 100644 index 00000000000..34b6905c1ae --- /dev/null +++ b/homeassistant/components/google_sheets/translations/ru.json @@ -0,0 +1,31 @@ +{ + "application_credentials": { + "description": "\u0421\u043b\u0435\u0434\u0443\u0439\u0442\u0435 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c]({more_info_url}) \u043d\u0430 [\u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0435 OAuth]({oauth_consent_url}), \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u0438\u0442\u044c Home Assistant \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u0412\u0430\u0448\u0438\u043c Google \u0422\u0430\u0431\u043b\u0438\u0446\u0430\u043c. \u0422\u0430\u043a\u0436\u0435 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f, \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0435 \u0441 \u0412\u0430\u0448\u0438\u043c \u0430\u043a\u043a\u0430\u0443\u043d\u0442\u043e\u043c:\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u0432 [\u0423\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0439]({oauth_creds_url}) \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 **\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f**.\n2. \u0412 \u0432\u044b\u043f\u0430\u0434\u0430\u044e\u0449\u0435\u043c \u0441\u043f\u0438\u0441\u043a\u0435 \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 **\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043a\u043b\u0438\u0435\u043d\u0442\u0430 OAuth**.\n3. \u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 **\u0412\u0435\u0431 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435** \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0422\u0438\u043f\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f." + }, + "config": { + "abort": { + "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "create_spreadsheet_failure": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0438 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u0442\u0430\u0431\u043b\u0438\u0446\u044b, \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0441\u0442\u0438 \u0441\u043c. \u0432 \u0436\u0443\u0440\u043d\u0430\u043b\u0435 \u043e\u0448\u0438\u0431\u043e\u043a.", + "invalid_access_token": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430.", + "missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439.", + "oauth_error": "\u041f\u043e\u043b\u0443\u0447\u0435\u043d\u044b \u043d\u0435\u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0442\u043e\u043a\u0435\u043d\u0430.", + "open_spreadsheet_failure": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u043e\u0442\u043a\u0440\u044b\u0442\u0438\u0438 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u0442\u0430\u0431\u043b\u0438\u0446\u044b, \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0441\u0442\u0438 \u0441\u043c. \u0432 \u0436\u0443\u0440\u043d\u0430\u043b\u0435 \u043e\u0448\u0438\u0431\u043e\u043a.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e.", + "timeout_connect": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "create_entry": { + "default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0448\u043b\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e, \u0442\u0430\u0431\u043b\u0438\u0446\u0430 \u0441\u043e\u0437\u0434\u0430\u043d\u0430 \u043f\u043e \u0430\u0434\u0440\u0435\u0441\u0443: {url}" + }, + "step": { + "auth": { + "title": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c Google" + }, + "pick_implementation": { + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_alerts/translations/cs.json b/homeassistant/components/homeassistant_alerts/translations/cs.json new file mode 100644 index 00000000000..33cafb8bba3 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/translations/cs.json @@ -0,0 +1,8 @@ +{ + "issues": { + "alert": { + "description": "{description}", + "title": "{title}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lametric/translations/cs.json b/homeassistant/components/lametric/translations/cs.json index 7abe7e7c1a9..59280c6c2bc 100644 --- a/homeassistant/components/lametric/translations/cs.json +++ b/homeassistant/components/lametric/translations/cs.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "authorize_url_timeout": "\u010casov\u00fd limit autoriza\u010dn\u00edho URL vypr\u0161el", + "no_url_available": "Nen\u00ed k dispozici \u017e\u00e1dn\u00e1 adresa URL. Informace o t\u00e9to chyb\u011b naleznete [v sekci n\u00e1pov\u011bdy]({docs_url})" }, "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", @@ -10,8 +12,12 @@ "step": { "manual_entry": { "data": { + "api_key": "Kl\u00ed\u010d API", "host": "Hostitel" } + }, + "pick_implementation": { + "title": "Vyberte metodu ov\u011b\u0159en\u00ed" } } } diff --git a/homeassistant/components/landisgyr_heat_meter/translations/cs.json b/homeassistant/components/landisgyr_heat_meter/translations/cs.json index 0887542d784..500211d103c 100644 --- a/homeassistant/components/landisgyr_heat_meter/translations/cs.json +++ b/homeassistant/components/landisgyr_heat_meter/translations/cs.json @@ -6,6 +6,13 @@ "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "setup_serial_manual_path": { + "data": { + "device": "Cesta k USB za\u0159\u00edzen\u00ed" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/life360/translations/cs.json b/homeassistant/components/life360/translations/cs.json index 570147cd678..89e4299178d 100644 --- a/homeassistant/components/life360/translations/cs.json +++ b/homeassistant/components/life360/translations/cs.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u00da\u010det je ji\u017e nastaven", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" diff --git a/homeassistant/components/lookin/translations/cs.json b/homeassistant/components/lookin/translations/cs.json index a932275300f..9193ce36658 100644 --- a/homeassistant/components/lookin/translations/cs.json +++ b/homeassistant/components/lookin/translations/cs.json @@ -16,6 +16,11 @@ "data": { "name": "Jm\u00e9no" } + }, + "user": { + "data": { + "ip_address": "IP adresa" + } } } } diff --git a/homeassistant/components/mill/translations/cs.json b/homeassistant/components/mill/translations/cs.json index 7e366233d85..10f846dc59a 100644 --- a/homeassistant/components/mill/translations/cs.json +++ b/homeassistant/components/mill/translations/cs.json @@ -12,6 +12,11 @@ "password": "Heslo", "username": "U\u017eivatelsk\u00e9 jm\u00e9no" } + }, + "local": { + "data": { + "ip_address": "IP adresa" + } } } } diff --git a/homeassistant/components/nest/translations/cs.json b/homeassistant/components/nest/translations/cs.json index a0fe869cd36..b41ce556cfa 100644 --- a/homeassistant/components/nest/translations/cs.json +++ b/homeassistant/components/nest/translations/cs.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u00da\u010det je ji\u017e nastaven", "authorize_url_timeout": "\u010casov\u00fd limit autoriza\u010dn\u00edho URL vypr\u0161el", "missing_configuration": "Komponenta nen\u00ed nastavena. Postupujte podle dokumentace.", "no_url_available": "Nen\u00ed k dispozici \u017e\u00e1dn\u00e1 adresa URL. Informace o t\u00e9to chyb\u011b naleznete [v sekci n\u00e1pov\u011bdy]({docs_url})", diff --git a/homeassistant/components/nobo_hub/translations/cs.json b/homeassistant/components/nobo_hub/translations/cs.json index 7c0da879b74..d5e80179cf6 100644 --- a/homeassistant/components/nobo_hub/translations/cs.json +++ b/homeassistant/components/nobo_hub/translations/cs.json @@ -12,6 +12,7 @@ "step": { "manual": { "data": { + "ip_address": "IP adresa", "serial": "S\u00e9riov\u00e9 \u010d\u00edslo (12 \u010d\u00edslic)" } } diff --git a/homeassistant/components/openexchangerates/translations/cs.json b/homeassistant/components/openexchangerates/translations/cs.json index 5cd704ff76c..af472a5e3d1 100644 --- a/homeassistant/components/openexchangerates/translations/cs.json +++ b/homeassistant/components/openexchangerates/translations/cs.json @@ -2,12 +2,21 @@ "config": { "abort": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", - "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9", + "timeout_connect": "Vypr\u0161el \u010dasov\u00fd limit pro nav\u00e1z\u00e1n\u00ed spojen\u00ed" }, "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "timeout_connect": "Vypr\u0161el \u010dasov\u00fd limit pro nav\u00e1z\u00e1n\u00ed spojen\u00ed", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "api_key": "Kl\u00ed\u010d API" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/translations/cs.json b/homeassistant/components/opentherm_gw/translations/cs.json index 5bf8d4fc385..6177aba85eb 100644 --- a/homeassistant/components/opentherm_gw/translations/cs.json +++ b/homeassistant/components/opentherm_gw/translations/cs.json @@ -3,7 +3,8 @@ "error": { "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", - "id_exists": "ID br\u00e1ny ji\u017e existuje" + "id_exists": "ID br\u00e1ny ji\u017e existuje", + "timeout_connect": "Vypr\u0161el \u010dasov\u00fd limit pro nav\u00e1z\u00e1n\u00ed spojen\u00ed" }, "step": { "init": { diff --git a/homeassistant/components/plugwise/translations/cs.json b/homeassistant/components/plugwise/translations/cs.json index c1c193e04a2..a7f5dae7c97 100644 --- a/homeassistant/components/plugwise/translations/cs.json +++ b/homeassistant/components/plugwise/translations/cs.json @@ -12,7 +12,8 @@ "step": { "user": { "data": { - "flow_type": "Typ p\u0159ipojen\u00ed" + "flow_type": "Typ p\u0159ipojen\u00ed", + "host": "IP adresa" }, "description": "Produkt:", "title": "Typ Plugwise" diff --git a/homeassistant/components/pushover/translations/cs.json b/homeassistant/components/pushover/translations/cs.json index ddeb87bf3fd..55a2608b01f 100644 --- a/homeassistant/components/pushover/translations/cs.json +++ b/homeassistant/components/pushover/translations/cs.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Slu\u017eba je ji\u017e nastavena", "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "error": { @@ -9,7 +10,16 @@ }, "step": { "reauth_confirm": { + "data": { + "api_key": "Kl\u00ed\u010d API" + }, "title": "Znovu ov\u011b\u0159it integraci" + }, + "user": { + "data": { + "api_key": "Kl\u00ed\u010d API", + "name": "Jm\u00e9no" + } } } } diff --git a/homeassistant/components/risco/translations/cs.json b/homeassistant/components/risco/translations/cs.json index 6c2e3d7ac00..8698052c0fa 100644 --- a/homeassistant/components/risco/translations/cs.json +++ b/homeassistant/components/risco/translations/cs.json @@ -12,12 +12,15 @@ "cloud": { "data": { "password": "Heslo", + "pin": "PIN k\u00f3d", "username": "U\u017eivatelsk\u00e9 jm\u00e9no" } }, "local": { "data": { - "host": "Hostitel" + "host": "Hostitel", + "pin": "PIN k\u00f3d", + "port": "Port" } }, "user": { diff --git a/homeassistant/components/scrape/translations/cs.json b/homeassistant/components/scrape/translations/cs.json index 43a819be694..6c5458481b8 100644 --- a/homeassistant/components/scrape/translations/cs.json +++ b/homeassistant/components/scrape/translations/cs.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u00da\u010det je ji\u017e nastaven" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/senz/translations/cs.json b/homeassistant/components/senz/translations/cs.json index 08428555c2b..efc86e3e58e 100644 --- a/homeassistant/components/senz/translations/cs.json +++ b/homeassistant/components/senz/translations/cs.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u00da\u010det je ji\u017e nastaven", "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1" } } diff --git a/homeassistant/components/skybell/translations/cs.json b/homeassistant/components/skybell/translations/cs.json index c294122142d..7bfe03b16a4 100644 --- a/homeassistant/components/skybell/translations/cs.json +++ b/homeassistant/components/skybell/translations/cs.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u00da\u010det je ji\u017e nastaven", "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "error": { diff --git a/homeassistant/components/sleepiq/translations/cs.json b/homeassistant/components/sleepiq/translations/cs.json index f4d930517ad..257c4399a1c 100644 --- a/homeassistant/components/sleepiq/translations/cs.json +++ b/homeassistant/components/sleepiq/translations/cs.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u00da\u010det je ji\u017e nastaven", "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "error": { diff --git a/homeassistant/components/smhi/translations/cs.json b/homeassistant/components/smhi/translations/cs.json index 6d65b6411c9..a9ce15dda40 100644 --- a/homeassistant/components/smhi/translations/cs.json +++ b/homeassistant/components/smhi/translations/cs.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u00da\u010det je ji\u017e nastaven" + }, "error": { "wrong_location": "Lokalita pouze pro \u0160v\u00e9dsko" }, diff --git a/homeassistant/components/sql/translations/cs.json b/homeassistant/components/sql/translations/cs.json new file mode 100644 index 00000000000..0907c1eb1b6 --- /dev/null +++ b/homeassistant/components/sql/translations/cs.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "\u00da\u010det je ji\u017e nastaven" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switchbee/translations/ca.json b/homeassistant/components/switchbee/translations/ca.json new file mode 100644 index 00000000000..745b3ed79cf --- /dev/null +++ b/homeassistant/components/switchbee/translations/ca.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3", + "password": "Contrasenya", + "switch_as_light": "Inicialitza els interruptors com a entitats de llum", + "username": "Nom d'usuari" + }, + "description": "Configura la integraci\u00f3 SwitchBee amb Home Assistant." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "devices": "Dispositius a incloure" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switchbee/translations/hu.json b/homeassistant/components/switchbee/translations/hu.json new file mode 100644 index 00000000000..01b3bc0010b --- /dev/null +++ b/homeassistant/components/switchbee/translations/hu.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "user": { + "data": { + "host": "C\u00edm", + "password": "Jelsz\u00f3", + "switch_as_light": "A kapcsol\u00f3k l\u00e1mpak\u00e9nt t\u00f6rt\u00e9n\u0151 inicializ\u00e1l\u00e1sa", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + }, + "description": "SwitchBee integr\u00e1ci\u00f3 be\u00e1ll\u00edt\u00e1sa a Home Assistant rendszerrel." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "devices": "A benne foglalt eszk\u00f6z\u00f6k" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switchbee/translations/ru.json b/homeassistant/components/switchbee/translations/ru.json new file mode 100644 index 00000000000..13ea7317296 --- /dev/null +++ b/homeassistant/components/switchbee/translations/ru.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "switch_as_light": "\u0418\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043e\u0431\u044a\u0435\u043a\u0442\u044b \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0442\u0435\u043b\u0435\u0439 \u043a\u0430\u043a \u043e\u0441\u0432\u0435\u0449\u0435\u043d\u0438\u0435", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 SwitchBee." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "devices": "\u0412\u044b\u0431\u0440\u0430\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_ferry/translations/cs.json b/homeassistant/components/trafikverket_ferry/translations/cs.json index a23047f4af8..89ded7d388c 100644 --- a/homeassistant/components/trafikverket_ferry/translations/cs.json +++ b/homeassistant/components/trafikverket_ferry/translations/cs.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u00da\u010det je ji\u017e nastaven", "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "error": { diff --git a/homeassistant/components/trafikverket_train/translations/cs.json b/homeassistant/components/trafikverket_train/translations/cs.json index a23047f4af8..89ded7d388c 100644 --- a/homeassistant/components/trafikverket_train/translations/cs.json +++ b/homeassistant/components/trafikverket_train/translations/cs.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u00da\u010det je ji\u017e nastaven", "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "error": { diff --git a/homeassistant/components/trafikverket_weatherstation/translations/cs.json b/homeassistant/components/trafikverket_weatherstation/translations/cs.json index 36573e02f3a..8648caa3d3c 100644 --- a/homeassistant/components/trafikverket_weatherstation/translations/cs.json +++ b/homeassistant/components/trafikverket_weatherstation/translations/cs.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u00da\u010det je ji\u017e nastaven" + }, "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" diff --git a/homeassistant/components/volvooncall/translations/cs.json b/homeassistant/components/volvooncall/translations/cs.json index c742628a5b7..7fd8c7e53ae 100644 --- a/homeassistant/components/volvooncall/translations/cs.json +++ b/homeassistant/components/volvooncall/translations/cs.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u00da\u010det je ji\u017e nastaven", "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "error": { diff --git a/homeassistant/components/ws66i/translations/cs.json b/homeassistant/components/ws66i/translations/cs.json index 0887542d784..04f18366eaf 100644 --- a/homeassistant/components/ws66i/translations/cs.json +++ b/homeassistant/components/ws66i/translations/cs.json @@ -6,6 +6,13 @@ "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "ip_address": "IP adresa" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/yalexs_ble/translations/cs.json b/homeassistant/components/yalexs_ble/translations/cs.json index 0bb0305f999..829ab3d3453 100644 --- a/homeassistant/components/yalexs_ble/translations/cs.json +++ b/homeassistant/components/yalexs_ble/translations/cs.json @@ -9,6 +9,7 @@ "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" - } + }, + "flow_title": "{name}" } } \ No newline at end of file diff --git a/homeassistant/components/yolink/translations/cs.json b/homeassistant/components/yolink/translations/cs.json index 0ae00fc3605..5b7d9c2db8e 100644 --- a/homeassistant/components/yolink/translations/cs.json +++ b/homeassistant/components/yolink/translations/cs.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u00da\u010det je ji\u017e nastaven", "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, diff --git a/homeassistant/components/zha/translations/cs.json b/homeassistant/components/zha/translations/cs.json index 9e48055c50d..13e8eabddad 100644 --- a/homeassistant/components/zha/translations/cs.json +++ b/homeassistant/components/zha/translations/cs.json @@ -9,9 +9,41 @@ }, "flow_title": "ZHA: {name}", "step": { + "choose_automatic_backup": { + "description": "Obnovit nastaven\u00ed s\u00edt\u011b z automatick\u00e9 z\u00e1lohy" + }, + "choose_formation_strategy": { + "description": "Vyberte nastaven\u00ed s\u00edt\u011b sv\u00e9ho r\u00e1dia", + "menu_options": { + "form_new_network": "Vymazat nastaven\u00ed s\u00edt\u011b a vytvo\u0159it novou s\u00ed\u0165", + "reuse_settings": "Zachovat nastaven\u00ed s\u00edt\u011b r\u00e1dia" + } + }, + "choose_serial_port": { + "description": "Zvolte s\u00e9riov\u00fd port pro sv\u00e9 r\u00e1dio Zigbee", + "title": "Zvolte s\u00e9riov\u00fd port" + }, "confirm_hardware": { "description": "Chcete nastavit {name}?" }, + "manual_pick_radio_type": { + "data": { + "radio_type": "Typ r\u00e1dia" + }, + "title": "Typ r\u00e1dia" + }, + "manual_port_config": { + "title": "Nastaven\u00ed s\u00e9riov\u00e9ho portu" + }, + "maybe_confirm_ezsp_restore": { + "data": { + "overwrite_coordinator_ieee": "Trvale nahradit IEEE adresu r\u00e1dia" + }, + "title": "P\u0159epsat adresu IEEE r\u00e1dia" + }, + "pick_radio": { + "description": "Vyberte typ sv\u00e9ho r\u00e1dia Zigbee" + }, "port_config": { "data": { "baudrate": "rychlost portu" @@ -21,7 +53,9 @@ "upload_manual_backup": { "data": { "uploaded_backup_file": "Nahr\u00e1t soubor" - } + }, + "description": "Obnovit nastaven\u00ed s\u00edt\u011b z nahran\u00e9ho z\u00e1lo\u017en\u00edho souboru JSON. Soubor m\u016f\u017eete st\u00e1hnout z jin\u00e9 instalace ZHA v **Nastaven\u00ed s\u00edt\u011b** nebo pou\u017eijte soubor `coordinator_backup.json` z integrace Zigbee2MQTT.", + "title": "Nahr\u00e1t ru\u010dn\u00ed z\u00e1lohu" }, "user": { "description": "Vyberte s\u00e9riov\u00fd port pro r\u00e1dio Zigbee", @@ -79,11 +113,54 @@ } }, "options": { + "abort": { + "not_zha_device": "Toto za\u0159\u00edzen\u00ed nen\u00ed za\u0159\u00edzen\u00ed zha", + "single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace." + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, "flow_title": "ZHA: {name}", "step": { + "choose_automatic_backup": { + "description": "Obnovit nastaven\u00ed s\u00edt\u011b z automatick\u00e9 z\u00e1lohy" + }, + "choose_formation_strategy": { + "description": "Vyberte nastaven\u00ed s\u00edt\u011b sv\u00e9ho r\u00e1dia", + "menu_options": { + "form_new_network": "Vymazat nastaven\u00ed s\u00edt\u011b a vytvo\u0159it novou s\u00ed\u0165", + "reuse_settings": "Zachovat nastaven\u00ed s\u00edt\u011b r\u00e1dia" + } + }, + "choose_serial_port": { + "description": "Zvolte s\u00e9riov\u00fd port pro sv\u00e9 r\u00e1dio Zigbee", + "title": "Zvolte s\u00e9riov\u00fd port" + }, "init": { "description": "Dopln\u011bk ZHA bude zastaven. P\u0159ejete si pokra\u010dovat?", "title": "P\u0159ekonfigurovat ZHA" + }, + "manual_pick_radio_type": { + "data": { + "radio_type": "Typ r\u00e1dia" + }, + "title": "Typ r\u00e1dia" + }, + "manual_port_config": { + "title": "Nastaven\u00ed s\u00e9riov\u00e9ho portu" + }, + "maybe_confirm_ezsp_restore": { + "data": { + "overwrite_coordinator_ieee": "Trvale nahradit IEEE adresu r\u00e1dia" + }, + "title": "P\u0159epsat adresu IEEE r\u00e1dia" + }, + "upload_manual_backup": { + "data": { + "uploaded_backup_file": "Nahr\u00e1t soubor" + }, + "description": "Obnovit nastaven\u00ed s\u00edt\u011b z nahran\u00e9ho z\u00e1lo\u017en\u00edho souboru JSON. Soubor m\u016f\u017eete st\u00e1hnout z jin\u00e9 instalace ZHA v **Nastaven\u00ed s\u00edt\u011b** nebo pou\u017eijte soubor `coordinator_backup.json` z integrace Zigbee2MQTT.", + "title": "Nahr\u00e1t ru\u010dn\u00ed z\u00e1lohu" } } } From 0a1fd36e0369bab18078282b1fe0202acb4e1594 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 15 Sep 2022 07:40:56 +0200 Subject: [PATCH 411/955] Catch up with statistics after DB migration is done (#78469) * Catch up with statistics after DB migration is done * Don't access the database from the event loop * Fix deadlocking test * Fix test --- homeassistant/components/recorder/core.py | 5 ++++- tests/components/recorder/test_init.py | 1 + tests/components/recorder/test_migrate.py | 2 ++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 30ece9e98a5..8706bc1c7a8 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -615,6 +615,10 @@ class Recorder(threading.Thread): self.hass.add_job(self.async_set_db_ready) + # Catch up with missed statistics + with session_scope(session=self.get_session()) as session: + self._schedule_compile_missing_statistics(session) + _LOGGER.debug("Recorder processing the queue") self.hass.add_job(self._async_set_recorder_ready_migration_done) self._run_event_loop() @@ -1118,7 +1122,6 @@ class Recorder(threading.Thread): with session_scope(session=self.get_session()) as session: end_incomplete_runs(session, self.run_history.recording_start) self.run_history.start(session) - self._schedule_compile_missing_statistics(session) self._open_event_session() diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index 0c3a41ab8ef..05ae1f1a372 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -1272,6 +1272,7 @@ async def test_database_corruption_while_running(hass, tmpdir, caplog): sqlite3_exception = DatabaseError("statement", {}, []) sqlite3_exception.__cause__ = sqlite3.DatabaseError() + await async_wait_recording_done(hass) with patch.object( get_instance(hass).event_session, "close", diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index bbac01bb5d3..c5b4774ab34 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -336,6 +336,8 @@ async def test_schema_migrate(hass, start_version, live): ), patch( "homeassistant.components.recorder.migration._apply_update", wraps=_instrument_apply_update, + ), patch( + "homeassistant.components.recorder.Recorder._schedule_compile_missing_statistics", ): recorder_helper.async_initialize_recorder(hass) hass.async_create_task( From 30702bdcd218cc34ba455e345c301978ae4755aa Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 15 Sep 2022 07:41:11 +0200 Subject: [PATCH 412/955] Deduplicate some code in scripts and automations (#78443) --- .../components/automation/__init__.py | 74 +++++++---------- homeassistant/components/script/__init__.py | 80 +++++++------------ 2 files changed, 56 insertions(+), 98 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index b1ec0c68e4b..b4fc82f52b9 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -148,9 +148,10 @@ def is_on(hass: HomeAssistant, entity_id: str) -> bool: return hass.states.is_state(entity_id, STATE_ON) -@callback -def automations_with_entity(hass: HomeAssistant, entity_id: str) -> list[str]: - """Return all automations that reference the entity.""" +def _automations_with_x( + hass: HomeAssistant, referenced_id: str, property_name: str +) -> list[str]: + """Return all automations that reference the x.""" if DOMAIN not in hass.data: return [] @@ -159,13 +160,14 @@ def automations_with_entity(hass: HomeAssistant, entity_id: str) -> list[str]: return [ automation_entity.entity_id for automation_entity in component.entities - if entity_id in automation_entity.referenced_entities + if referenced_id in getattr(automation_entity, property_name) ] -@callback -def entities_in_automation(hass: HomeAssistant, entity_id: str) -> list[str]: - """Return all entities in a scene.""" +def _x_in_automation( + hass: HomeAssistant, entity_id: str, property_name: str +) -> list[str]: + """Return all x in an automation.""" if DOMAIN not in hass.data: return [] @@ -174,65 +176,43 @@ def entities_in_automation(hass: HomeAssistant, entity_id: str) -> list[str]: if (automation_entity := component.get_entity(entity_id)) is None: return [] - return list(automation_entity.referenced_entities) + return list(getattr(automation_entity, property_name)) + + +@callback +def automations_with_entity(hass: HomeAssistant, entity_id: str) -> list[str]: + """Return all automations that reference the entity.""" + return _automations_with_x(hass, entity_id, "referenced_entities") + + +@callback +def entities_in_automation(hass: HomeAssistant, entity_id: str) -> list[str]: + """Return all entities in an automation.""" + return _x_in_automation(hass, entity_id, "referenced_entities") @callback def automations_with_device(hass: HomeAssistant, device_id: str) -> list[str]: """Return all automations that reference the device.""" - if DOMAIN not in hass.data: - return [] - - component: EntityComponent[AutomationEntity] = hass.data[DOMAIN] - - return [ - automation_entity.entity_id - for automation_entity in component.entities - if device_id in automation_entity.referenced_devices - ] + return _automations_with_x(hass, device_id, "referenced_devices") @callback def devices_in_automation(hass: HomeAssistant, entity_id: str) -> list[str]: - """Return all devices in a scene.""" - if DOMAIN not in hass.data: - return [] - - component: EntityComponent[AutomationEntity] = hass.data[DOMAIN] - - if (automation_entity := component.get_entity(entity_id)) is None: - return [] - - return list(automation_entity.referenced_devices) + """Return all devices in an automation.""" + return _x_in_automation(hass, entity_id, "referenced_devices") @callback def automations_with_area(hass: HomeAssistant, area_id: str) -> list[str]: """Return all automations that reference the area.""" - if DOMAIN not in hass.data: - return [] - - component: EntityComponent[AutomationEntity] = hass.data[DOMAIN] - - return [ - automation_entity.entity_id - for automation_entity in component.entities - if area_id in automation_entity.referenced_areas - ] + return _automations_with_x(hass, area_id, "referenced_areas") @callback def areas_in_automation(hass: HomeAssistant, entity_id: str) -> list[str]: """Return all areas in an automation.""" - if DOMAIN not in hass.data: - return [] - - component: EntityComponent[AutomationEntity] = hass.data[DOMAIN] - - if (automation_entity := component.get_entity(entity_id)) is None: - return [] - - return list(automation_entity.referenced_areas) + return _x_in_automation(hass, entity_id, "referenced_areas") @callback diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py index a5ea2a17e0b..9791f0e588e 100644 --- a/homeassistant/components/script/__init__.py +++ b/homeassistant/components/script/__init__.py @@ -79,9 +79,10 @@ def is_on(hass, entity_id): return hass.states.is_state(entity_id, STATE_ON) -@callback -def scripts_with_entity(hass: HomeAssistant, entity_id: str) -> list[str]: - """Return all scripts that reference the entity.""" +def _scripts_with_x( + hass: HomeAssistant, referenced_id: str, property_name: str +) -> list[str]: + """Return all scripts that reference the x.""" if DOMAIN not in hass.data: return [] @@ -90,80 +91,57 @@ def scripts_with_entity(hass: HomeAssistant, entity_id: str) -> list[str]: return [ script_entity.entity_id for script_entity in component.entities - if entity_id in script_entity.script.referenced_entities + if referenced_id in getattr(script_entity.script, property_name) ] +def _x_in_script(hass: HomeAssistant, entity_id: str, property_name: str) -> list[str]: + """Return all x in a script.""" + if DOMAIN not in hass.data: + return [] + + component = hass.data[DOMAIN] + + if (script_entity := component.get_entity(entity_id)) is None: + return [] + + return list(getattr(script_entity.script, property_name)) + + +@callback +def scripts_with_entity(hass: HomeAssistant, entity_id: str) -> list[str]: + """Return all scripts that reference the entity.""" + return _scripts_with_x(hass, entity_id, "referenced_entities") + + @callback def entities_in_script(hass: HomeAssistant, entity_id: str) -> list[str]: """Return all entities in script.""" - if DOMAIN not in hass.data: - return [] - - component = hass.data[DOMAIN] - - if (script_entity := component.get_entity(entity_id)) is None: - return [] - - return list(script_entity.script.referenced_entities) + return _x_in_script(hass, entity_id, "referenced_entities") @callback def scripts_with_device(hass: HomeAssistant, device_id: str) -> list[str]: """Return all scripts that reference the device.""" - if DOMAIN not in hass.data: - return [] - - component = hass.data[DOMAIN] - - return [ - script_entity.entity_id - for script_entity in component.entities - if device_id in script_entity.script.referenced_devices - ] + return _scripts_with_x(hass, device_id, "referenced_devices") @callback def devices_in_script(hass: HomeAssistant, entity_id: str) -> list[str]: """Return all devices in script.""" - if DOMAIN not in hass.data: - return [] - - component = hass.data[DOMAIN] - - if (script_entity := component.get_entity(entity_id)) is None: - return [] - - return list(script_entity.script.referenced_devices) + return _x_in_script(hass, entity_id, "referenced_devices") @callback def scripts_with_area(hass: HomeAssistant, area_id: str) -> list[str]: """Return all scripts that reference the area.""" - if DOMAIN not in hass.data: - return [] - - component = hass.data[DOMAIN] - - return [ - script_entity.entity_id - for script_entity in component.entities - if area_id in script_entity.script.referenced_areas - ] + return _scripts_with_x(hass, area_id, "referenced_areas") @callback def areas_in_script(hass: HomeAssistant, entity_id: str) -> list[str]: """Return all areas in a script.""" - if DOMAIN not in hass.data: - return [] - - component = hass.data[DOMAIN] - - if (script_entity := component.get_entity(entity_id)) is None: - return [] - - return list(script_entity.script.referenced_areas) + return _x_in_script(hass, entity_id, "referenced_areas") @callback From 84a812ad05e8f4763ff9c2429882174c742f09de Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 15 Sep 2022 08:29:46 +0200 Subject: [PATCH 413/955] Allow setting number selector step size to 'any' (#78265) * Allow setting number selector step size to 'any' * Improve test coverage --- homeassistant/components/threshold/config_flow.py | 6 +++--- homeassistant/helpers/selector.py | 15 +++++++++------ tests/helpers/test_selector.py | 7 +++++++ 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/threshold/config_flow.py b/homeassistant/components/threshold/config_flow.py index 45ccdcb4a5c..e3af2e9c567 100644 --- a/homeassistant/components/threshold/config_flow.py +++ b/homeassistant/components/threshold/config_flow.py @@ -31,17 +31,17 @@ OPTIONS_SCHEMA = vol.Schema( CONF_HYSTERESIS, default=DEFAULT_HYSTERESIS ): selector.NumberSelector( selector.NumberSelectorConfig( - mode=selector.NumberSelectorMode.BOX, step=1e-3 + mode=selector.NumberSelectorMode.BOX, step="any" ), ), vol.Optional(CONF_LOWER): selector.NumberSelector( selector.NumberSelectorConfig( - mode=selector.NumberSelectorMode.BOX, step=1e-3 + mode=selector.NumberSelectorMode.BOX, step="any" ), ), vol.Optional(CONF_UPPER): selector.NumberSelector( selector.NumberSelectorConfig( - mode=selector.NumberSelectorMode.BOX, step=1e-3 + mode=selector.NumberSelectorMode.BOX, step="any" ), ), } diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index e5fa493330e..bd6dff03858 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -2,7 +2,7 @@ from __future__ import annotations from collections.abc import Callable, Sequence -from typing import Any, TypedDict, cast +from typing import Any, Literal, TypedDict, cast from uuid import UUID import voluptuous as vol @@ -609,7 +609,7 @@ class NumberSelectorConfig(TypedDict, total=False): min: float max: float - step: float + step: float | Literal["any"] unit_of_measurement: str mode: NumberSelectorMode @@ -621,7 +621,7 @@ class NumberSelectorMode(StrEnum): SLIDER = "slider" -def has_min_max_if_slider(data: Any) -> Any: +def validate_slider(data: Any) -> Any: """Validate configuration.""" if data["mode"] == "box": return data @@ -629,6 +629,9 @@ def has_min_max_if_slider(data: Any) -> Any: if "min" not in data or "max" not in data: raise vol.Invalid("min and max are required in slider mode") + if "step" in data and data["step"] == "any": + raise vol.Invalid("step 'any' is not allowed in slider mode") + return data @@ -645,8 +648,8 @@ class NumberSelector(Selector): vol.Optional("max"): vol.Coerce(float), # Controls slider steps, and up/down keyboard binding for the box # user input is not rounded - vol.Optional("step", default=1): vol.All( - vol.Coerce(float), vol.Range(min=1e-3) + vol.Optional("step", default=1): vol.Any( + "any", vol.All(vol.Coerce(float), vol.Range(min=1e-3)) ), vol.Optional(CONF_UNIT_OF_MEASUREMENT): str, vol.Optional(CONF_MODE, default=NumberSelectorMode.SLIDER): vol.All( @@ -654,7 +657,7 @@ class NumberSelector(Selector): ), } ), - has_min_max_if_slider, + validate_slider, ) def __init__(self, config: NumberSelectorConfig | None = None) -> None: diff --git a/tests/helpers/test_selector.py b/tests/helpers/test_selector.py index 4c20a3b7906..4b4072bd06c 100644 --- a/tests/helpers/test_selector.py +++ b/tests/helpers/test_selector.py @@ -241,6 +241,7 @@ def test_area_selector_schema(schema, valid_selections, invalid_selections): ), ({"min": 10, "max": 1000, "mode": "slider", "step": 0.5}, (), ()), ({"mode": "box"}, (10,), ()), + ({"mode": "box", "step": "any"}, (), ()), ), ) def test_number_selector_schema(schema, valid_selections, invalid_selections): @@ -253,6 +254,12 @@ def test_number_selector_schema(schema, valid_selections, invalid_selections): ( {}, # Must have mandatory fields {"mode": "slider"}, # Must have min+max in slider mode + { + "mode": "slider", + "min": 0, + "max": 1, + "step": "any", # Can't combine slider with step any + }, ), ) def test_number_selector_schema_error(schema): From ade4fcaebd9fb441b78bddf67ea39598b7d64fac Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Thu, 15 Sep 2022 08:36:56 +0200 Subject: [PATCH 414/955] Use asyncio in XiaomiAqara instead of threading (#74979) Co-authored-by: Martin Hjelmare --- .../components/xiaomi_aqara/__init__.py | 68 +++++++++++-------- .../components/xiaomi_aqara/binary_sensor.py | 12 ++-- .../components/xiaomi_aqara/config_flow.py | 2 +- .../components/xiaomi_aqara/const.py | 2 + .../components/xiaomi_aqara/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 7 files changed, 53 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/xiaomi_aqara/__init__.py b/homeassistant/components/xiaomi_aqara/__init__.py index e99851dae9f..aba9da05611 100644 --- a/homeassistant/components/xiaomi_aqara/__init__.py +++ b/homeassistant/components/xiaomi_aqara/__init__.py @@ -1,12 +1,13 @@ """Support for Xiaomi Gateways.""" +import asyncio from datetime import timedelta import logging import voluptuous as vol -from xiaomi_gateway import XiaomiGateway, XiaomiGatewayDiscovery +from xiaomi_gateway import AsyncXiaomiGatewayMulticast, XiaomiGateway from homeassistant.components import persistent_notification -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_DEVICE_ID, @@ -34,6 +35,8 @@ from .const import ( DEFAULT_DISCOVERY_RETRY, DOMAIN, GATEWAYS_KEY, + KEY_SETUP_LOCK, + KEY_UNSUB_STOP, LISTENER_KEY, ) @@ -143,6 +146,7 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up the xiaomi aqara components from a config entry.""" hass.data.setdefault(DOMAIN, {}) + setup_lock = hass.data[DOMAIN].setdefault(KEY_SETUP_LOCK, asyncio.Lock()) hass.data[DOMAIN].setdefault(GATEWAYS_KEY, {}) # Connect to Xiaomi Aqara Gateway @@ -158,24 +162,28 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) hass.data[DOMAIN][GATEWAYS_KEY][entry.entry_id] = xiaomi_gateway - gateway_discovery = hass.data[DOMAIN].setdefault( - LISTENER_KEY, - XiaomiGatewayDiscovery(hass.add_job, [], entry.data[CONF_INTERFACE]), - ) + async with setup_lock: + if LISTENER_KEY not in hass.data[DOMAIN]: + multicast = AsyncXiaomiGatewayMulticast( + interface=entry.data[CONF_INTERFACE] + ) + hass.data[DOMAIN][LISTENER_KEY] = multicast - if len(hass.data[DOMAIN][GATEWAYS_KEY]) == 1: - # start listining for local pushes (only once) - await hass.async_add_executor_job(gateway_discovery.listen) + # start listining for local pushes (only once) + await multicast.start_listen() - # register stop callback to shutdown listining for local pushes - def stop_xiaomi(event): - """Stop Xiaomi Socket.""" - _LOGGER.debug("Shutting down Xiaomi Gateway Listener") - gateway_discovery.stop_listen() + # register stop callback to shutdown listining for local pushes + @callback + def stop_xiaomi(event): + """Stop Xiaomi Socket.""" + _LOGGER.debug("Shutting down Xiaomi Gateway Listener") + multicast.stop_listen() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_xiaomi) + unsub = hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_xiaomi) + hass.data[DOMAIN][KEY_UNSUB_STOP] = unsub - gateway_discovery.gateways[entry.data[CONF_HOST]] = xiaomi_gateway + multicast = hass.data[DOMAIN][LISTENER_KEY] + multicast.register_gateway(entry.data[CONF_HOST], xiaomi_gateway.multicast_callback) _LOGGER.debug( "Gateway with host '%s' connected, listening for broadcasts", entry.data[CONF_HOST], @@ -201,23 +209,32 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload a config entry.""" - if entry.data[CONF_KEY] is not None: + if config_entry.data[CONF_KEY] is not None: platforms = GATEWAY_PLATFORMS else: platforms = GATEWAY_PLATFORMS_NO_KEY - unload_ok = await hass.config_entries.async_unload_platforms(entry, platforms) + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, platforms + ) if unload_ok: - hass.data[DOMAIN][GATEWAYS_KEY].pop(entry.entry_id) + hass.data[DOMAIN][GATEWAYS_KEY].pop(config_entry.entry_id) - if len(hass.data[DOMAIN][GATEWAYS_KEY]) == 0: + loaded_entries = [ + entry + for entry in hass.config_entries.async_entries(DOMAIN) + if entry.state == ConfigEntryState.LOADED + ] + if len(loaded_entries) == 1: # No gateways left, stop Xiaomi socket + unsub_stop = hass.data[DOMAIN].pop(KEY_UNSUB_STOP) + unsub_stop() hass.data[DOMAIN].pop(GATEWAYS_KEY) _LOGGER.debug("Shutting down Xiaomi Gateway Listener") - gateway_discovery = hass.data[DOMAIN].pop(LISTENER_KEY) - await hass.async_add_executor_job(gateway_discovery.stop_listen) + multicast = hass.data[DOMAIN].pop(LISTENER_KEY) + multicast.stop_listen() return unload_ok @@ -262,12 +279,9 @@ class XiaomiDevice(Entity): self._is_gateway = False self._device_id = self._sid - def _add_push_data_job(self, *args): - self.hass.add_job(self.push_data, *args) - async def async_added_to_hass(self): """Start unavailability tracking.""" - self._xiaomi_hub.callbacks[self._sid].append(self._add_push_data_job) + self._xiaomi_hub.callbacks[self._sid].append(self.push_data) self._async_track_unavailable() @property diff --git a/homeassistant/components/xiaomi_aqara/binary_sensor.py b/homeassistant/components/xiaomi_aqara/binary_sensor.py index 9ed79bc8250..591e97304db 100644 --- a/homeassistant/components/xiaomi_aqara/binary_sensor.py +++ b/homeassistant/components/xiaomi_aqara/binary_sensor.py @@ -287,7 +287,7 @@ class XiaomiMotionSensor(XiaomiBinarySensor): ) if self.entity_id is not None: - self._hass.bus.fire( + self._hass.bus.async_fire( "xiaomi_aqara.motion", {"entity_id": self.entity_id} ) @@ -473,7 +473,7 @@ class XiaomiVibration(XiaomiBinarySensor): _LOGGER.warning("Unsupported movement_type detected: %s", value) return False - self.hass.bus.fire( + self.hass.bus.async_fire( "xiaomi_aqara.movement", {"entity_id": self.entity_id, "movement_type": value}, ) @@ -533,7 +533,7 @@ class XiaomiButton(XiaomiBinarySensor): _LOGGER.warning("Unsupported click_type detected: %s", value) return False - self._hass.bus.fire( + self._hass.bus.async_fire( "xiaomi_aqara.click", {"entity_id": self.entity_id, "click_type": click_type}, ) @@ -570,7 +570,7 @@ class XiaomiCube(XiaomiBinarySensor): def parse_data(self, data, raw_data): """Parse data sent by gateway.""" if self._data_key in data: - self._hass.bus.fire( + self._hass.bus.async_fire( "xiaomi_aqara.cube_action", {"entity_id": self.entity_id, "action_type": data[self._data_key]}, ) @@ -582,7 +582,7 @@ class XiaomiCube(XiaomiBinarySensor): if isinstance(data["rotate"], int) else data["rotate"].replace(",", ".") ) - self._hass.bus.fire( + self._hass.bus.async_fire( "xiaomi_aqara.cube_action", { "entity_id": self.entity_id, @@ -598,7 +598,7 @@ class XiaomiCube(XiaomiBinarySensor): if isinstance(data["rotate_degree"], int) else data["rotate_degree"].replace(",", ".") ) - self._hass.bus.fire( + self._hass.bus.async_fire( "xiaomi_aqara.cube_action", { "entity_id": self.entity_id, diff --git a/homeassistant/components/xiaomi_aqara/config_flow.py b/homeassistant/components/xiaomi_aqara/config_flow.py index f9f8a761321..cc86fda85f5 100644 --- a/homeassistant/components/xiaomi_aqara/config_flow.py +++ b/homeassistant/components/xiaomi_aqara/config_flow.py @@ -106,7 +106,7 @@ class XiaomiAqaraFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_settings() # Discover Xiaomi Aqara Gateways in the netwerk to get required SIDs. - xiaomi = XiaomiGatewayDiscovery(self.hass.add_job, [], self.interface) + xiaomi = XiaomiGatewayDiscovery(self.interface) try: await self.hass.async_add_executor_job(xiaomi.discover_gateways) except gaierror: diff --git a/homeassistant/components/xiaomi_aqara/const.py b/homeassistant/components/xiaomi_aqara/const.py index 11706cdb6fb..d137941d614 100644 --- a/homeassistant/components/xiaomi_aqara/const.py +++ b/homeassistant/components/xiaomi_aqara/const.py @@ -4,6 +4,8 @@ DOMAIN = "xiaomi_aqara" GATEWAYS_KEY = "gateways" LISTENER_KEY = "listener" +KEY_UNSUB_STOP = "unsub_stop" +KEY_SETUP_LOCK = "setup_lock" ZEROCONF_GATEWAY = "lumi-gateway" ZEROCONF_ACPARTNER = "lumi-acpartner" diff --git a/homeassistant/components/xiaomi_aqara/manifest.json b/homeassistant/components/xiaomi_aqara/manifest.json index bcc3eef933e..a70fb90f961 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.13.4"], + "requirements": ["PyXiaomiGateway==0.14.1"], "after_dependencies": ["discovery"], "codeowners": ["@danielhiversen", "@syssi"], "zeroconf": ["_miio._udp.local."], diff --git a/requirements_all.txt b/requirements_all.txt index cc72695b4ad..3331d2e9b0a 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.13.4 +PyXiaomiGateway==0.14.1 # homeassistant.components.remember_the_milk RtmAPI==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d8be23fa019..82423ebd025 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.13.4 +PyXiaomiGateway==0.14.1 # homeassistant.components.remember_the_milk RtmAPI==0.7.2 From c0cf9d87295c071ccff883b53c2fc790f5ce7a46 Mon Sep 17 00:00:00 2001 From: Avi Miller Date: Thu, 15 Sep 2022 16:53:58 +1000 Subject: [PATCH 415/955] Add infrared brightness select entity for LIFX Night Vision bulbs (#77943) * Add infrared brightness select entity for LIFX Night Vision bulbs Signed-off-by: Avi Miller * Code refactored from review comments Signed-off-by: Avi Miller * Update and refactor from code review feedback Signed-off-by: Avi Miller Signed-off-by: Avi Miller --- homeassistant/components/lifx/__init__.py | 2 +- homeassistant/components/lifx/const.py | 8 +- homeassistant/components/lifx/coordinator.py | 33 ++- homeassistant/components/lifx/light.py | 11 +- homeassistant/components/lifx/manifest.json | 2 +- homeassistant/components/lifx/select.py | 69 ++++++ homeassistant/components/lifx/util.py | 13 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/lifx/__init__.py | 12 + tests/components/lifx/test_select.py | 239 +++++++++++++++++++ 11 files changed, 385 insertions(+), 8 deletions(-) create mode 100644 homeassistant/components/lifx/select.py create mode 100644 tests/components/lifx/test_select.py diff --git a/homeassistant/components/lifx/__init__.py b/homeassistant/components/lifx/__init__.py index 5c91efa1d02..2f20cb0e366 100644 --- a/homeassistant/components/lifx/__init__.py +++ b/homeassistant/components/lifx/__init__.py @@ -57,7 +57,7 @@ CONFIG_SCHEMA = vol.All( ) -PLATFORMS = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.LIGHT] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.LIGHT, Platform.SELECT] DISCOVERY_INTERVAL = timedelta(minutes=15) MIGRATION_INTERVAL = timedelta(minutes=5) diff --git a/homeassistant/components/lifx/const.py b/homeassistant/components/lifx/const.py index 74960d59bd1..8acfa35802e 100644 --- a/homeassistant/components/lifx/const.py +++ b/homeassistant/components/lifx/const.py @@ -37,7 +37,13 @@ ATTR_REMAINING = "remaining" ATTR_ZONES = "zones" HEV_CYCLE_STATE = "hev_cycle_state" - +INFRARED_BRIGHTNESS = "infrared_brightness" +INFRARED_BRIGHTNESS_VALUES_MAP = { + 0: "Disabled", + 16383: "25%", + 32767: "50%", + 65535: "100%", +} DATA_LIFX_MANAGER = "lifx_manager" _LOGGER = logging.getLogger(__package__) diff --git a/homeassistant/components/lifx/coordinator.py b/homeassistant/components/lifx/coordinator.py index d30af851e7d..7d3a51562d1 100644 --- a/homeassistant/components/lifx/coordinator.py +++ b/homeassistant/components/lifx/coordinator.py @@ -9,20 +9,29 @@ from typing import Any, cast from aiolifx.aiolifx import Light from aiolifx.connection import LIFXConnection +from homeassistant.const import Platform from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import ( _LOGGER, ATTR_REMAINING, + DOMAIN, IDENTIFY_WAVEFORM, MESSAGE_RETRIES, MESSAGE_TIMEOUT, TARGET_ANY, UNAVAILABLE_GRACE, ) -from .util import async_execute_lifx, get_real_mac_addr, lifx_features +from .util import ( + async_execute_lifx, + get_real_mac_addr, + infrared_brightness_option_to_value, + infrared_brightness_value_to_option, + lifx_features, +) REQUEST_REFRESH_DELAY = 0.35 LIFX_IDENTIFY_DELAY = 3.0 @@ -83,6 +92,18 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator): """Return the label of the bulb.""" return cast(str, self.device.label) + @property + def current_infrared_brightness(self) -> str | None: + """Return the current infrared brightness as a string.""" + return infrared_brightness_value_to_option(self.device.infrared_brightness) + + def async_get_entity_id(self, platform: Platform, key: str) -> str | None: + """Return the entity_id from the platform and key provided.""" + ent_reg = er.async_get(self.hass) + return ent_reg.async_get_entity_id( + platform, DOMAIN, f"{self.serial_number}_{key}" + ) + async def async_identify_bulb(self) -> None: """Identify the device by flashing it three times.""" bulb: Light = self.device @@ -103,6 +124,7 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator): self.device.get_hostfirmware() if self.device.product is None: self.device.get_version() + response = await async_execute_lifx(self.device.get_color) if self.device.product is None: @@ -114,12 +136,16 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator): if self.device.mac_addr == TARGET_ANY: self.device.mac_addr = response.target_addr + # Update model-specific configuration if lifx_features(self.device)["multizone"]: await self.async_update_color_zones() if lifx_features(self.device)["hev"]: await self.async_get_hev_cycle() + if lifx_features(self.device)["infrared"]: + response = await async_execute_lifx(self.device.get_infrared) + async def async_update_color_zones(self) -> None: """Get updated color information for each zone.""" zone = 0 @@ -199,3 +225,8 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator): await async_execute_lifx( partial(self.device.set_hev_cycle, enable=enable, duration=duration) ) + + async def async_set_infrared_brightness(self, option: str) -> None: + """Set infrared brightness.""" + infrared_brightness = infrared_brightness_option_to_value(option) + await async_execute_lifx(partial(self.device.set_infrared, infrared_brightness)) diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index ec3223c03a2..314f7bd915e 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -19,7 +19,7 @@ from homeassistant.components.light import ( LightEntityFeature, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_platform @@ -29,12 +29,14 @@ from homeassistant.helpers.event import async_track_point_in_utc_time import homeassistant.util.color as color_util from .const import ( + _LOGGER, ATTR_DURATION, ATTR_INFRARED, ATTR_POWER, ATTR_ZONES, DATA_LIFX_MANAGER, DOMAIN, + INFRARED_BRIGHTNESS, ) from .coordinator import LIFXUpdateCoordinator from .entity import LIFXEntity @@ -212,6 +214,13 @@ class LIFXLight(LIFXEntity, LightEntity): return if ATTR_INFRARED in kwargs: + infrared_entity_id = self.coordinator.async_get_entity_id( + Platform.SELECT, INFRARED_BRIGHTNESS + ) + _LOGGER.warning( + "The 'infrared' attribute of 'lifx.set_state' is deprecated: call 'select.select_option' targeting '%s' instead", + infrared_entity_id, + ) bulb.set_infrared(convert_8_to_16(kwargs[ATTR_INFRARED])) if ATTR_TRANSITION in kwargs: diff --git a/homeassistant/components/lifx/manifest.json b/homeassistant/components/lifx/manifest.json index 83408f87bb5..bbc2e1bea15 100644 --- a/homeassistant/components/lifx/manifest.json +++ b/homeassistant/components/lifx/manifest.json @@ -3,7 +3,7 @@ "name": "LIFX", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/lifx", - "requirements": ["aiolifx==0.8.2", "aiolifx_effects==0.2.2"], + "requirements": ["aiolifx==0.8.4", "aiolifx_effects==0.2.2"], "quality_scale": "platinum", "dependencies": ["network"], "homekit": { diff --git a/homeassistant/components/lifx/select.py b/homeassistant/components/lifx/select.py new file mode 100644 index 00000000000..a1cfb4624d5 --- /dev/null +++ b/homeassistant/components/lifx/select.py @@ -0,0 +1,69 @@ +"""Select sensor entities for LIFX integration.""" +from __future__ import annotations + +from homeassistant.components.select import SelectEntity, SelectEntityDescription +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, INFRARED_BRIGHTNESS, INFRARED_BRIGHTNESS_VALUES_MAP +from .coordinator import LIFXUpdateCoordinator +from .entity import LIFXEntity +from .util import lifx_features + +INFRARED_BRIGHTNESS_ENTITY = SelectEntityDescription( + key=INFRARED_BRIGHTNESS, + name="Infrared brightness", + entity_category=EntityCategory.CONFIG, +) + +INFRARED_BRIGHTNESS_OPTIONS = list(INFRARED_BRIGHTNESS_VALUES_MAP.values()) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up LIFX from a config entry.""" + coordinator: LIFXUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + if lifx_features(coordinator.device)["infrared"]: + async_add_entities( + [ + LIFXInfraredBrightnessSelectEntity( + coordinator, description=INFRARED_BRIGHTNESS_ENTITY + ) + ] + ) + + +class LIFXInfraredBrightnessSelectEntity(LIFXEntity, SelectEntity): + """LIFX Nightvision infrared brightness configuration entity.""" + + _attr_has_entity_name = True + _attr_options = INFRARED_BRIGHTNESS_OPTIONS + + def __init__( + self, coordinator: LIFXUpdateCoordinator, description: SelectEntityDescription + ) -> None: + """Initialise the IR brightness config entity.""" + super().__init__(coordinator) + self.entity_description = description + self._attr_name = description.name + self._attr_unique_id = f"{coordinator.serial_number}_{description.key}" + self._attr_current_option = coordinator.current_infrared_brightness + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._async_update_attrs() + super()._handle_coordinator_update() + + @callback + def _async_update_attrs(self) -> None: + """Handle coordinator updates.""" + self._attr_current_option = self.coordinator.current_infrared_brightness + + async def async_select_option(self, option: str) -> None: + """Update the infrared brightness value.""" + await self.coordinator.async_set_infrared_brightness(option) diff --git a/homeassistant/components/lifx/util.py b/homeassistant/components/lifx/util.py index fac53464bbc..2136ab5f63b 100644 --- a/homeassistant/components/lifx/util.py +++ b/homeassistant/components/lifx/util.py @@ -25,7 +25,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr import homeassistant.util.color as color_util -from .const import _LOGGER, DOMAIN, OVERALL_TIMEOUT +from .const import _LOGGER, DOMAIN, INFRARED_BRIGHTNESS_VALUES_MAP, OVERALL_TIMEOUT FIX_MAC_FW = AwesomeVersion("3.70") @@ -45,6 +45,17 @@ def async_get_legacy_entry(hass: HomeAssistant) -> ConfigEntry | None: return None +def infrared_brightness_value_to_option(value: int) -> str | None: + """Convert infrared brightness from value to option.""" + return INFRARED_BRIGHTNESS_VALUES_MAP.get(value, None) + + +def infrared_brightness_option_to_value(option: str) -> int | None: + """Convert infrared brightness option to value.""" + option_values = {v: k for k, v in INFRARED_BRIGHTNESS_VALUES_MAP.items()} + return option_values.get(option, None) + + def convert_8_to_16(value: int) -> int: """Scale an 8 bit level into 16 bits.""" return (value << 8) | value diff --git a/requirements_all.txt b/requirements_all.txt index 3331d2e9b0a..af2f3682680 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -190,7 +190,7 @@ aiokafka==0.7.2 aiokef==0.2.16 # homeassistant.components.lifx -aiolifx==0.8.2 +aiolifx==0.8.4 # homeassistant.components.lifx aiolifx_effects==0.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 82423ebd025..8e00894075f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -168,7 +168,7 @@ aiohue==4.5.0 aiokafka==0.7.2 # homeassistant.components.lifx -aiolifx==0.8.2 +aiolifx==0.8.4 # homeassistant.components.lifx aiolifx_effects==0.2.2 diff --git a/tests/components/lifx/__init__.py b/tests/components/lifx/__init__.py index 05d7e9a1ddf..8f6e19188b6 100644 --- a/tests/components/lifx/__init__.py +++ b/tests/components/lifx/__init__.py @@ -62,6 +62,9 @@ class MockLifxCommand: self.bulb = bulb self.calls = [] self.msg_kwargs = kwargs + for k, v in kwargs.items(): + if k != "callb": + setattr(self.bulb, k, v) def __call__(self, *args, **kwargs): """Call command.""" @@ -130,6 +133,15 @@ def _mocked_clean_bulb() -> Light: return bulb +def _mocked_infrared_bulb() -> Light: + bulb = _mocked_bulb() + bulb.product = 29 # LIFX A19 Night Vision + bulb.infrared_brightness = 65535 + bulb.set_infrared = MockLifxCommand(bulb) + bulb.get_infrared = MockLifxCommand(bulb, infrared_brightness=65535) + return bulb + + def _mocked_light_strip() -> Light: bulb = _mocked_bulb() bulb.product = 31 # LIFX Z diff --git a/tests/components/lifx/test_select.py b/tests/components/lifx/test_select.py new file mode 100644 index 00000000000..bc2d6f0fc1e --- /dev/null +++ b/tests/components/lifx/test_select.py @@ -0,0 +1,239 @@ +"""Tests for the lifx integration select entity.""" +from datetime import timedelta + +from homeassistant.components import lifx +from homeassistant.components.lifx.const import DOMAIN +from homeassistant.components.select import DOMAIN as SELECT_DOMAIN +from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, STATE_UNKNOWN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util + +from . import ( + DEFAULT_ENTRY_TITLE, + IP_ADDRESS, + MAC_ADDRESS, + SERIAL, + MockLifxCommand, + _mocked_infrared_bulb, + _patch_config_flow_try_connect, + _patch_device, + _patch_discovery, +) + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_infrared_brightness(hass: HomeAssistant) -> None: + """Test getting and setting infrared brightness.""" + + config_entry = MockConfigEntry( + domain=DOMAIN, + title=DEFAULT_ENTRY_TITLE, + data={CONF_HOST: IP_ADDRESS}, + unique_id=MAC_ADDRESS, + ) + config_entry.add_to_hass(hass) + bulb = _mocked_infrared_bulb() + 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() + + unique_id = f"{SERIAL}_infrared_brightness" + entity_id = "select.my_bulb_infrared_brightness" + + entity_registry = er.async_get(hass) + entity = entity_registry.async_get(entity_id) + assert entity + assert not entity.disabled + assert entity.unique_id == unique_id + + state = hass.states.get(entity_id) + assert state.state == "100%" + + +async def test_set_infrared_brightness_25_percent(hass: HomeAssistant) -> None: + """Test getting and setting infrared brightness.""" + + config_entry = MockConfigEntry( + domain=DOMAIN, + title=DEFAULT_ENTRY_TITLE, + data={CONF_HOST: IP_ADDRESS}, + unique_id=MAC_ADDRESS, + ) + config_entry.add_to_hass(hass) + bulb = _mocked_infrared_bulb() + 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 = "select.my_bulb_infrared_brightness" + + await hass.services.async_call( + SELECT_DOMAIN, + "select_option", + {ATTR_ENTITY_ID: entity_id, "option": "25%"}, + blocking=True, + ) + + bulb.get_infrared = MockLifxCommand(bulb, infrared_brightness=16383) + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=30)) + await hass.async_block_till_done() + + assert bulb.set_infrared.calls[0][0][0] == 16383 + + state = hass.states.get(entity_id) + assert state.state == "25%" + + bulb.set_infrared.reset_mock() + + +async def test_set_infrared_brightness_50_percent(hass: HomeAssistant) -> None: + """Test getting and setting infrared brightness.""" + + config_entry = MockConfigEntry( + domain=DOMAIN, + title=DEFAULT_ENTRY_TITLE, + data={CONF_HOST: IP_ADDRESS}, + unique_id=MAC_ADDRESS, + ) + config_entry.add_to_hass(hass) + bulb = _mocked_infrared_bulb() + 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 = "select.my_bulb_infrared_brightness" + + await hass.services.async_call( + SELECT_DOMAIN, + "select_option", + {ATTR_ENTITY_ID: entity_id, "option": "50%"}, + blocking=True, + ) + + bulb.get_infrared = MockLifxCommand(bulb, infrared_brightness=32767) + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=30)) + await hass.async_block_till_done() + + assert bulb.set_infrared.calls[0][0][0] == 32767 + + state = hass.states.get(entity_id) + assert state.state == "50%" + + bulb.set_infrared.reset_mock() + + +async def test_set_infrared_brightness_100_percent(hass: HomeAssistant) -> None: + """Test getting and setting infrared brightness.""" + + config_entry = MockConfigEntry( + domain=DOMAIN, + title=DEFAULT_ENTRY_TITLE, + data={CONF_HOST: IP_ADDRESS}, + unique_id=MAC_ADDRESS, + ) + config_entry.add_to_hass(hass) + bulb = _mocked_infrared_bulb() + 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 = "select.my_bulb_infrared_brightness" + + await hass.services.async_call( + SELECT_DOMAIN, + "select_option", + {ATTR_ENTITY_ID: entity_id, "option": "100%"}, + blocking=True, + ) + + bulb.get_infrared = MockLifxCommand(bulb, infrared_brightness=65535) + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=30)) + await hass.async_block_till_done() + + assert bulb.set_infrared.calls[0][0][0] == 65535 + + state = hass.states.get(entity_id) + assert state.state == "100%" + + bulb.set_infrared.reset_mock() + + +async def test_disable_infrared(hass: HomeAssistant) -> None: + """Test getting and setting infrared brightness.""" + + config_entry = MockConfigEntry( + domain=DOMAIN, + title=DEFAULT_ENTRY_TITLE, + data={CONF_HOST: IP_ADDRESS}, + unique_id=MAC_ADDRESS, + ) + config_entry.add_to_hass(hass) + bulb = _mocked_infrared_bulb() + 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 = "select.my_bulb_infrared_brightness" + + await hass.services.async_call( + SELECT_DOMAIN, + "select_option", + {ATTR_ENTITY_ID: entity_id, "option": "Disabled"}, + blocking=True, + ) + + bulb.get_infrared = MockLifxCommand(bulb, infrared_brightness=0) + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=30)) + await hass.async_block_till_done() + + assert bulb.set_infrared.calls[0][0][0] == 0 + + state = hass.states.get(entity_id) + assert state.state == "Disabled" + + bulb.set_infrared.reset_mock() + + +async def test_invalid_infrared_brightness(hass: HomeAssistant) -> None: + """Test getting and setting infrared brightness.""" + + config_entry = MockConfigEntry( + domain=DOMAIN, + title=DEFAULT_ENTRY_TITLE, + data={CONF_HOST: IP_ADDRESS}, + unique_id=MAC_ADDRESS, + ) + config_entry.add_to_hass(hass) + bulb = _mocked_infrared_bulb() + 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 = "select.my_bulb_infrared_brightness" + + bulb.get_infrared = MockLifxCommand(bulb, infrared_brightness=12345) + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=30)) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state.state == STATE_UNKNOWN From aa46ba4ad58b5660304c1d238bf56da5e1186376 Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Thu, 15 Sep 2022 08:56:30 +0200 Subject: [PATCH 416/955] Add device class TV to AndroidTV (#78487) --- homeassistant/components/androidtv/media_player.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index f9004de1c52..241ac12e780 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -21,6 +21,7 @@ import voluptuous as vol from homeassistant.components import persistent_notification from homeassistant.components.media_player import ( + MediaPlayerDeviceClass, MediaPlayerEntity, MediaPlayerEntityFeature, MediaPlayerState, @@ -205,6 +206,8 @@ def adb_decorator( class ADBDevice(MediaPlayerEntity): """Representation of an Android TV or Fire TV device.""" + _attr_device_class = MediaPlayerDeviceClass.TV + def __init__( self, aftv, From 4e7a99dc7700d4af1a261ebd2d9290137a734033 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 15 Sep 2022 09:57:35 +0200 Subject: [PATCH 417/955] Update sentry-sdk to 1.9.8 (#78496) --- homeassistant/components/sentry/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sentry/manifest.json b/homeassistant/components/sentry/manifest.json index 849b40170f8..9855c281ac4 100644 --- a/homeassistant/components/sentry/manifest.json +++ b/homeassistant/components/sentry/manifest.json @@ -3,7 +3,7 @@ "name": "Sentry", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sentry", - "requirements": ["sentry-sdk==1.9.5"], + "requirements": ["sentry-sdk==1.9.8"], "codeowners": ["@dcramer", "@frenck"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index af2f3682680..133b8c14e62 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2221,7 +2221,7 @@ sensorpro-ble==0.5.0 sensorpush-ble==1.5.2 # homeassistant.components.sentry -sentry-sdk==1.9.5 +sentry-sdk==1.9.8 # homeassistant.components.sharkiq sharkiq==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8e00894075f..3b0229166df 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1518,7 +1518,7 @@ sensorpro-ble==0.5.0 sensorpush-ble==1.5.2 # homeassistant.components.sentry -sentry-sdk==1.9.5 +sentry-sdk==1.9.8 # homeassistant.components.sharkiq sharkiq==0.0.1 From ec2afd2bce42e9dcd14788e9b20d1c55edb1ab00 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 15 Sep 2022 10:24:18 +0200 Subject: [PATCH 418/955] Update pipdeptree to 2.3.1 (#78497) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 99e2f8d8402..e89af314beb 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -15,7 +15,7 @@ mock-open==1.4.0 mypy==0.971 pre-commit==2.20.0 pylint==2.15.0 -pipdeptree==2.2.1 +pipdeptree==2.3.1 pytest-aiohttp==0.3.0 pytest-cov==3.0.0 pytest-freezegun==0.4.2 From 11789dd0791af10b3163538b22538f122647ecaf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 15 Sep 2022 10:40:52 +0200 Subject: [PATCH 419/955] Bump bleak-retry-connector to 0.17.1 (#78474) --- 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 acc8a6977e3..1b1ec016e82 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -6,7 +6,7 @@ "quality_scale": "internal", "requirements": [ "bleak==0.17.0", - "bleak-retry-connector==1.15.1", + "bleak-retry-connector==1.17.1", "bluetooth-adapters==0.4.1", "bluetooth-auto-recovery==0.3.3", "dbus-fast==1.4.0" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a86c2b07bf7..3ee61afca23 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.8.0 bcrypt==3.1.7 -bleak-retry-connector==1.15.1 +bleak-retry-connector==1.17.1 bleak==0.17.0 bluetooth-adapters==0.4.1 bluetooth-auto-recovery==0.3.3 diff --git a/requirements_all.txt b/requirements_all.txt index 133b8c14e62..ceab6c312d1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -405,7 +405,7 @@ bimmer_connected==0.10.2 bizkaibus==0.1.1 # homeassistant.components.bluetooth -bleak-retry-connector==1.15.1 +bleak-retry-connector==1.17.1 # homeassistant.components.bluetooth bleak==0.17.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3b0229166df..2abb171ba09 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -326,7 +326,7 @@ bellows==0.33.1 bimmer_connected==0.10.2 # homeassistant.components.bluetooth -bleak-retry-connector==1.15.1 +bleak-retry-connector==1.17.1 # homeassistant.components.bluetooth bleak==0.17.0 From 035f206e95b86fdf35a738db0021f60523973f27 Mon Sep 17 00:00:00 2001 From: Vincent Knoop Pathuis <48653141+vpathuis@users.noreply.github.com> Date: Thu, 15 Sep 2022 10:51:19 +0200 Subject: [PATCH 420/955] Bump ultraheat-api to 0.4.3 (#78295) --- homeassistant/components/landisgyr_heat_meter/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/landisgyr_heat_meter/manifest.json b/homeassistant/components/landisgyr_heat_meter/manifest.json index 359ca1acea6..9d1faa570b7 100644 --- a/homeassistant/components/landisgyr_heat_meter/manifest.json +++ b/homeassistant/components/landisgyr_heat_meter/manifest.json @@ -3,7 +3,7 @@ "name": "Landis+Gyr Heat Meter", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/landisgyr_heat_meter", - "requirements": ["ultraheat-api==0.4.1"], + "requirements": ["ultraheat-api==0.4.3"], "ssdp": [], "zeroconf": [], "homekit": {}, diff --git a/requirements_all.txt b/requirements_all.txt index ceab6c312d1..b19c46ea543 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2432,7 +2432,7 @@ twitchAPI==2.5.2 uasiren==0.0.1 # homeassistant.components.landisgyr_heat_meter -ultraheat-api==0.4.1 +ultraheat-api==0.4.3 # homeassistant.components.unifiprotect unifi-discovery==1.1.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2abb171ba09..091926106f0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1663,7 +1663,7 @@ twitchAPI==2.5.2 uasiren==0.0.1 # homeassistant.components.landisgyr_heat_meter -ultraheat-api==0.4.1 +ultraheat-api==0.4.3 # homeassistant.components.unifiprotect unifi-discovery==1.1.6 From 08449dc1bcdb5e0991985b3d0c96e43522a26651 Mon Sep 17 00:00:00 2001 From: Federico Marani Date: Thu, 15 Sep 2022 10:52:33 +0200 Subject: [PATCH 421/955] Bump aioftp to 0.21.3 (#78257) --- homeassistant/components/yi/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/yi/manifest.json b/homeassistant/components/yi/manifest.json index 23299542736..d0560ff13f5 100644 --- a/homeassistant/components/yi/manifest.json +++ b/homeassistant/components/yi/manifest.json @@ -2,7 +2,7 @@ "domain": "yi", "name": "Yi Home Cameras", "documentation": "https://www.home-assistant.io/integrations/yi", - "requirements": ["aioftp==0.12.0"], + "requirements": ["aioftp==0.21.3"], "dependencies": ["ffmpeg"], "codeowners": ["@bachya"], "iot_class": "local_polling", diff --git a/requirements_all.txt b/requirements_all.txt index b19c46ea543..3f66b3517a6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -159,7 +159,7 @@ aioesphomeapi==10.13.0 aioflo==2021.11.0 # homeassistant.components.yi -aioftp==0.12.0 +aioftp==0.21.3 # homeassistant.components.github aiogithubapi==22.2.4 From 0a13fe99d250db2daefc29c6592c421f3a9acd41 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 15 Sep 2022 11:00:25 +0200 Subject: [PATCH 422/955] Move mypy override for device_registry (#78493) --- .strict-typing | 1 + homeassistant/helpers/device_registry.py | 2 -- mypy.ini | 3 +++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.strict-typing b/.strict-typing index 0cbb1aa92e9..e421aa6a354 100644 --- a/.strict-typing +++ b/.strict-typing @@ -17,6 +17,7 @@ homeassistant.helpers.area_registry homeassistant.helpers.condition homeassistant.helpers.debounce homeassistant.helpers.deprecation +homeassistant.helpers.device_registry homeassistant.helpers.discovery homeassistant.helpers.dispatcher homeassistant.helpers.entity diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 57497df2d7e..908db74d40d 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -21,8 +21,6 @@ from .debounce import Debouncer from .frame import report from .typing import UNDEFINED, UndefinedType -# mypy: disallow_any_generics - if TYPE_CHECKING: from homeassistant.config_entries import ConfigEntry diff --git a/mypy.ini b/mypy.ini index 1218992ab3a..758dd96ef42 100644 --- a/mypy.ini +++ b/mypy.ini @@ -63,6 +63,9 @@ disallow_any_generics = true [mypy-homeassistant.helpers.deprecation] disallow_any_generics = true +[mypy-homeassistant.helpers.device_registry] +disallow_any_generics = true + [mypy-homeassistant.helpers.discovery] disallow_any_generics = true From 2cc45cd30273282ab8e4ac2918a34deab050b596 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 15 Sep 2022 11:00:55 +0200 Subject: [PATCH 423/955] Use new media player enums in bluesound (#78096) --- .../components/bluesound/media_player.py | 26 +++++++------------ 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index 36af7d46489..23611e5bef5 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -19,12 +19,11 @@ import xmltodict from homeassistant.components import media_source from homeassistant.components.media_player import ( PLATFORM_SCHEMA, + BrowseMedia, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, MediaType, -) -from homeassistant.components.media_player.browse_media import ( - BrowseMedia, async_process_play_media_url, ) from homeassistant.const import ( @@ -35,10 +34,6 @@ from homeassistant.const import ( CONF_PORT, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, - STATE_IDLE, - STATE_OFF, - STATE_PAUSED, - STATE_PLAYING, ) from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -68,7 +63,6 @@ DEFAULT_PORT = 11000 NODE_OFFLINE_CHECK_TIMEOUT = 180 NODE_RETRY_INITIATION = timedelta(minutes=3) -STATE_GROUPED = "grouped" SYNC_STATUS_INTERVAL = timedelta(minutes=5) UPDATE_CAPTURE_INTERVAL = timedelta(minutes=30) @@ -554,20 +548,20 @@ class BluesoundPlayer(MediaPlayerEntity): return self._services_items @property - def state(self): + def state(self) -> MediaPlayerState: """Return the state of the device.""" if self._status is None: - return STATE_OFF + return MediaPlayerState.OFF if self.is_grouped and not self.is_master: - return STATE_GROUPED + return MediaPlayerState.IDLE status = self._status.get("state") if status in ("pause", "stop"): - return STATE_PAUSED + return MediaPlayerState.PAUSED if status in ("stream", "play"): - return STATE_PLAYING - return STATE_IDLE + return MediaPlayerState.PLAYING + return MediaPlayerState.IDLE @property def media_title(self): @@ -620,14 +614,14 @@ class BluesoundPlayer(MediaPlayerEntity): return None mediastate = self.state - if self._last_status_update is None or mediastate == STATE_IDLE: + if self._last_status_update is None or mediastate == MediaPlayerState.IDLE: return None if (position := self._status.get("secs")) is None: return None position = float(position) - if mediastate == STATE_PLAYING: + if mediastate == MediaPlayerState.PLAYING: position += (dt_util.utcnow() - self._last_status_update).total_seconds() return position From c37d294d12d715f5091eedcd0dfe446f39807dbd Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 15 Sep 2022 11:02:37 +0200 Subject: [PATCH 424/955] Use reload helper to reload rest component (#78491) --- homeassistant/components/rest/__init__.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/rest/__init__.py b/homeassistant/components/rest/__init__.py index 282f05aada8..96ed1ada7ae 100644 --- a/homeassistant/components/rest/__init__.py +++ b/homeassistant/components/rest/__init__.py @@ -1,6 +1,7 @@ """The rest component.""" import asyncio +import contextlib import logging import httpx @@ -26,13 +27,13 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import discovery, template -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.entity_component import ( - DEFAULT_SCAN_INTERVAL, - EntityComponent, +from homeassistant.helpers.entity_component import DEFAULT_SCAN_INTERVAL +from homeassistant.helpers.reload import ( + async_integration_yaml_config, + async_reload_integration_platforms, ) -from homeassistant.helpers.reload import async_reload_integration_platforms from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -54,12 +55,14 @@ COORDINATOR_AWARE_PLATFORMS = [SENSOR_DOMAIN, BINARY_SENSOR_DOMAIN] async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the rest platforms.""" - component = EntityComponent[Entity](_LOGGER, DOMAIN, hass) _async_setup_shared_data(hass) async def reload_service_handler(service: ServiceCall) -> None: """Remove all user-defined groups and load new ones from config.""" - if (conf := await component.async_prepare_reload()) is None: + conf = None + with contextlib.suppress(HomeAssistantError): + conf = await async_integration_yaml_config(hass, DOMAIN) + if conf is None: return await async_reload_integration_platforms(hass, DOMAIN, PLATFORMS) _async_setup_shared_data(hass) From 19ea95a6e4f210bb18c3606a7763c309ee35c113 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 15 Sep 2022 11:29:11 +0200 Subject: [PATCH 425/955] Enable disallow-any-generics in update (#78501) --- homeassistant/components/update/__init__.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/update/__init__.py b/homeassistant/components/update/__init__.py index 6cc03700ed8..9ec748c1ed5 100644 --- a/homeassistant/components/update/__init__.py +++ b/homeassistant/components/update/__init__.py @@ -73,10 +73,12 @@ __all__ = [ "UpdateEntityFeature", ] +# mypy: disallow-any-generics + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up Select entities.""" - component = hass.data[DOMAIN] = EntityComponent( + component = hass.data[DOMAIN] = EntityComponent[UpdateEntity]( _LOGGER, DOMAIN, hass, SCAN_INTERVAL ) await component.async_setup(config) @@ -109,13 +111,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[UpdateEntity] = hass.data[DOMAIN] return await component.async_setup_entry(entry) async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[UpdateEntity] = hass.data[DOMAIN] return await component.async_unload_entry(entry) @@ -437,11 +439,11 @@ class UpdateEntity(RestoreEntity): async def websocket_release_notes( hass: HomeAssistant, connection: websocket_api.connection.ActiveConnection, - msg: dict, + msg: dict[str, Any], ) -> None: """Get the full release notes for a entity.""" - component = hass.data[DOMAIN] - entity: UpdateEntity | None = component.get_entity(msg["entity_id"]) + component: EntityComponent[UpdateEntity] = hass.data[DOMAIN] + entity = component.get_entity(msg["entity_id"]) if entity is None: connection.send_error( From b4afb1cb6b496867d39dd679acaee07f571f5b9b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 15 Sep 2022 11:53:00 +0200 Subject: [PATCH 426/955] Make use of generic EntityComponent (#78492) --- .../components/automation/__init__.py | 4 ++-- homeassistant/components/camera/__init__.py | 19 ++++++------------- .../components/camera/media_source.py | 9 +++------ homeassistant/components/group/__init__.py | 18 +++++++----------- homeassistant/components/person/__init__.py | 11 +++++------ homeassistant/components/remote/__init__.py | 10 ++++++---- homeassistant/components/script/__init__.py | 6 +++--- 7 files changed, 32 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index b4fc82f52b9..841f015fa0f 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -221,7 +221,7 @@ def automations_with_blueprint(hass: HomeAssistant, blueprint_path: str) -> list if DOMAIN not in hass.data: return [] - component = hass.data[DOMAIN] + component: EntityComponent[AutomationEntity] = hass.data[DOMAIN] return [ automation_entity.entity_id @@ -661,7 +661,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): async def _async_process_config( hass: HomeAssistant, config: dict[str, Any], - component: EntityComponent, + component: EntityComponent[AutomationEntity], ) -> bool: """Process config and add automations. diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 6e2b36070ae..fa807dd1440 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -322,12 +322,9 @@ def async_register_rtsp_to_web_rtc_provider( async def _async_refresh_providers(hass: HomeAssistant) -> None: """Check all cameras for any state changes for registered providers.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[Camera] = hass.data[DOMAIN] await asyncio.gather( - *( - cast(Camera, camera).async_refresh_providers() - for camera in component.entities - ) + *(camera.async_refresh_providers() for camera in component.entities) ) @@ -343,7 +340,7 @@ def _async_get_rtsp_to_web_rtc_providers( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the camera component.""" - component = hass.data[DOMAIN] = EntityComponent( + component = hass.data[DOMAIN] = EntityComponent[Camera]( _LOGGER, DOMAIN, hass, SCAN_INTERVAL ) @@ -363,7 +360,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def preload_stream(_event: Event) -> None: for camera in component.entities: - camera = cast(Camera, camera) camera_prefs = prefs.get(camera.entity_id) if not camera_prefs.preload_stream: continue @@ -380,7 +376,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: def update_tokens(time: datetime) -> None: """Update tokens of the entities.""" for entity in component.entities: - entity = cast(Camera, entity) entity.async_update_token() entity.async_write_ha_state() @@ -411,13 +406,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[Camera] = hass.data[DOMAIN] return await component.async_setup_entry(entry) async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[Camera] = hass.data[DOMAIN] return await component.async_unload_entry(entry) @@ -698,7 +693,7 @@ class CameraView(HomeAssistantView): requires_auth = False - def __init__(self, component: EntityComponent) -> None: + def __init__(self, component: EntityComponent[Camera]) -> None: """Initialize a basic camera view.""" self.component = component @@ -707,8 +702,6 @@ class CameraView(HomeAssistantView): if (camera := self.component.get_entity(entity_id)) is None: raise web.HTTPNotFound() - camera = cast(Camera, camera) - authenticated = ( request[KEY_AUTHENTICATED] or request.query.get("token") in camera.access_tokens diff --git a/homeassistant/components/camera/media_source.py b/homeassistant/components/camera/media_source.py index 117f65edb07..e386e864ded 100644 --- a/homeassistant/components/camera/media_source.py +++ b/homeassistant/components/camera/media_source.py @@ -1,8 +1,6 @@ """Expose cameras as media sources.""" from __future__ import annotations -from typing import Optional, cast - from homeassistant.components.media_player import BrowseError, MediaClass from homeassistant.components.media_source.error import Unresolvable from homeassistant.components.media_source.models import ( @@ -37,8 +35,8 @@ class CameraMediaSource(MediaSource): async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia: """Resolve media to a url.""" - component: EntityComponent = self.hass.data[DOMAIN] - camera = cast(Optional[Camera], component.get_entity(item.identifier)) + component: EntityComponent[Camera] = self.hass.data[DOMAIN] + camera = component.get_entity(item.identifier) if not camera: raise Unresolvable(f"Could not resolve media item: {item.identifier}") @@ -72,11 +70,10 @@ class CameraMediaSource(MediaSource): can_stream_hls = "stream" in self.hass.config.components # Root. List cameras. - component: EntityComponent = self.hass.data[DOMAIN] + component: EntityComponent[Camera] = self.hass.data[DOMAIN] children = [] not_shown = 0 for camera in component.entities: - camera = cast(Camera, camera) stream_type = camera.frontend_stream_type if stream_type is None: diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index c1759432ade..bdf295e35ae 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -6,7 +6,7 @@ import asyncio from collections.abc import Collection, Iterable from contextvars import ContextVar import logging -from typing import Any, Protocol, Union, cast +from typing import Any, Protocol, cast import voluptuous as vol @@ -119,9 +119,9 @@ CONFIG_SCHEMA = vol.Schema( ) -def _async_get_component(hass: HomeAssistant) -> EntityComponent: +def _async_get_component(hass: HomeAssistant) -> EntityComponent[Group]: if (component := hass.data.get(DOMAIN)) is None: - component = hass.data[DOMAIN] = EntityComponent(_LOGGER, DOMAIN, hass) + component = hass.data[DOMAIN] = EntityComponent[Group](_LOGGER, DOMAIN, hass) return component @@ -288,11 +288,11 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up all groups found defined in the configuration.""" if DOMAIN not in hass.data: - hass.data[DOMAIN] = EntityComponent(_LOGGER, DOMAIN, hass) + hass.data[DOMAIN] = EntityComponent[Group](_LOGGER, DOMAIN, hass) await async_process_integration_platform_for_component(hass, DOMAIN) - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[Group] = hass.data[DOMAIN] hass.data[REG_KEY] = GroupIntegrationRegistry() @@ -302,11 +302,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def reload_service_handler(service: ServiceCall) -> None: """Remove all user-defined groups and load new ones from config.""" - auto = [ - cast(Group, e) - for e in component.entities - if not cast(Group, e).user_defined - ] + auto = [e for e in component.entities if not e.user_defined] if (conf := await component.async_prepare_reload()) is None: return @@ -331,7 +327,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Handle dynamic group service functions.""" object_id = service.data[ATTR_OBJECT_ID] entity_id = f"{DOMAIN}.{object_id}" - group: Group | None = cast(Union[Group, None], component.get_entity(entity_id)) + group = component.get_entity(entity_id) # new group if service.service == SERVICE_SET and group is None: diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py index 0823e9e4b55..86a132027d8 100644 --- a/homeassistant/components/person/__init__.py +++ b/homeassistant/components/person/__init__.py @@ -2,7 +2,6 @@ from __future__ import annotations import logging -from typing import cast import voluptuous as vol @@ -107,7 +106,7 @@ async def async_add_user_device_tracker( hass: HomeAssistant, user_id: str, device_tracker_entity_id: str ): """Add a device tracker to a person linked to a user.""" - coll = cast(PersonStorageCollection, hass.data[DOMAIN][1]) + coll: PersonStorageCollection = hass.data[DOMAIN][1] for person in coll.async_items(): if person.get(ATTR_USER_ID) != user_id: @@ -134,12 +133,12 @@ def persons_with_entity(hass: HomeAssistant, entity_id: str) -> list[str]: ): return [] - component: EntityComponent = hass.data[DOMAIN][2] + component: EntityComponent[Person] = hass.data[DOMAIN][2] return [ person_entity.entity_id for person_entity in component.entities - if entity_id in cast(Person, person_entity).device_trackers + if entity_id in person_entity.device_trackers ] @@ -149,12 +148,12 @@ def entities_in_person(hass: HomeAssistant, entity_id: str) -> list[str]: if DOMAIN not in hass.data: return [] - component: EntityComponent = hass.data[DOMAIN][2] + component: EntityComponent[Person] = hass.data[DOMAIN][2] if (person_entity := component.get_entity(entity_id)) is None: return [] - return cast(Person, person_entity).device_trackers + return person_entity.device_trackers CREATE_FIELDS = { diff --git a/homeassistant/components/remote/__init__.py b/homeassistant/components/remote/__init__.py index b1b856cfa29..6ba5ca89d2d 100644 --- a/homeassistant/components/remote/__init__.py +++ b/homeassistant/components/remote/__init__.py @@ -7,7 +7,7 @@ from datetime import timedelta from enum import IntEnum import functools as ft import logging -from typing import Any, cast, final +from typing import Any, final import voluptuous as vol @@ -88,7 +88,7 @@ def is_on(hass: HomeAssistant, entity_id: str) -> bool: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Track states and offer events for remotes.""" - component = hass.data[DOMAIN] = EntityComponent( + component = hass.data[DOMAIN] = EntityComponent[RemoteEntity]( _LOGGER, DOMAIN, hass, SCAN_INTERVAL ) await component.async_setup(config) @@ -145,12 +145,14 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - return await cast(EntityComponent, hass.data[DOMAIN]).async_setup_entry(entry) + component: EntityComponent[RemoteEntity] = hass.data[DOMAIN] + return await component.async_setup_entry(entry) async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - return await cast(EntityComponent, hass.data[DOMAIN]).async_unload_entry(entry) + component: EntityComponent[RemoteEntity] = hass.data[DOMAIN] + return await component.async_unload_entry(entry) @dataclass diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py index 9791f0e588e..dce99620e44 100644 --- a/homeassistant/components/script/__init__.py +++ b/homeassistant/components/script/__init__.py @@ -86,7 +86,7 @@ def _scripts_with_x( if DOMAIN not in hass.data: return [] - component = hass.data[DOMAIN] + component: EntityComponent[ScriptEntity] = hass.data[DOMAIN] return [ script_entity.entity_id @@ -100,7 +100,7 @@ def _x_in_script(hass: HomeAssistant, entity_id: str, property_name: str) -> lis if DOMAIN not in hass.data: return [] - component = hass.data[DOMAIN] + component: EntityComponent[ScriptEntity] = hass.data[DOMAIN] if (script_entity := component.get_entity(entity_id)) is None: return [] @@ -150,7 +150,7 @@ def scripts_with_blueprint(hass: HomeAssistant, blueprint_path: str) -> list[str if DOMAIN not in hass.data: return [] - component = hass.data[DOMAIN] + component: EntityComponent[ScriptEntity] = hass.data[DOMAIN] return [ script_entity.entity_id From cf138c72662bccff602a84641658bd4fadf2564a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 15 Sep 2022 11:53:16 +0200 Subject: [PATCH 427/955] Update pyotp to 2.7.0 (#78500) --- homeassistant/auth/mfa_modules/notify.py | 2 +- homeassistant/auth/mfa_modules/totp.py | 2 +- homeassistant/components/otp/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/auth/mfa_modules/notify.py b/homeassistant/auth/mfa_modules/notify.py index 1de6c38aecf..0a65c42b520 100644 --- a/homeassistant/auth/mfa_modules/notify.py +++ b/homeassistant/auth/mfa_modules/notify.py @@ -26,7 +26,7 @@ from . import ( SetupFlow, ) -REQUIREMENTS = ["pyotp==2.6.0"] +REQUIREMENTS = ["pyotp==2.7.0"] CONF_MESSAGE = "message" diff --git a/homeassistant/auth/mfa_modules/totp.py b/homeassistant/auth/mfa_modules/totp.py index 397a7fcd386..7db4919ba6f 100644 --- a/homeassistant/auth/mfa_modules/totp.py +++ b/homeassistant/auth/mfa_modules/totp.py @@ -19,7 +19,7 @@ from . import ( SetupFlow, ) -REQUIREMENTS = ["pyotp==2.6.0", "PyQRCode==1.2.1"] +REQUIREMENTS = ["pyotp==2.7.0", "PyQRCode==1.2.1"] CONFIG_SCHEMA = MULTI_FACTOR_AUTH_MODULE_SCHEMA.extend({}, extra=vol.PREVENT_EXTRA) diff --git a/homeassistant/components/otp/manifest.json b/homeassistant/components/otp/manifest.json index 0c16e660aa9..7916e3cb4b7 100644 --- a/homeassistant/components/otp/manifest.json +++ b/homeassistant/components/otp/manifest.json @@ -2,7 +2,7 @@ "domain": "otp", "name": "One-Time Password (OTP)", "documentation": "https://www.home-assistant.io/integrations/otp", - "requirements": ["pyotp==2.6.0"], + "requirements": ["pyotp==2.7.0"], "codeowners": [], "quality_scale": "internal", "iot_class": "local_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 3f66b3517a6..a3ad7b5931f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1772,7 +1772,7 @@ pyotgw==2.0.3 # homeassistant.auth.mfa_modules.notify # homeassistant.auth.mfa_modules.totp # homeassistant.components.otp -pyotp==2.6.0 +pyotp==2.7.0 # homeassistant.components.overkiz pyoverkiz==1.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 091926106f0..5da458075ef 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1243,7 +1243,7 @@ pyotgw==2.0.3 # homeassistant.auth.mfa_modules.notify # homeassistant.auth.mfa_modules.totp # homeassistant.components.otp -pyotp==2.6.0 +pyotp==2.7.0 # homeassistant.components.overkiz pyoverkiz==1.5.0 From 19c1065387c916d89810a39c0fe75c06720c985f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 15 Sep 2022 11:53:40 +0200 Subject: [PATCH 428/955] Update pytest to 7.1.3 (#78503) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index e89af314beb..63833940bdf 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.2 +pytest==7.1.3 requests_mock==1.9.2 respx==0.19.2 stdlib-list==0.7.0 From 0448afabb6c194d598b853b992349dd5ee665eb3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 15 Sep 2022 11:53:56 +0200 Subject: [PATCH 429/955] Sort strict-typing alphabetically (#78506) --- .strict-typing | 28 ++++----- mypy.ini | 152 ++++++++++++++++++++++++------------------------- 2 files changed, 90 insertions(+), 90 deletions(-) diff --git a/.strict-typing b/.strict-typing index e421aa6a354..0905c17232b 100644 --- a/.strict-typing +++ b/.strict-typing @@ -39,10 +39,9 @@ homeassistant.util.unit_system # --- Add components below this line --- homeassistant.components -homeassistant.components.alert.* homeassistant.components.abode.* -homeassistant.components.acer_projector.* homeassistant.components.accuweather.* +homeassistant.components.acer_projector.* homeassistant.components.actiontec.* homeassistant.components.adguard.* homeassistant.components.aftership.* @@ -52,6 +51,7 @@ homeassistant.components.airvisual.* homeassistant.components.airzone.* homeassistant.components.aladdin_connect.* homeassistant.components.alarm_control_panel.* +homeassistant.components.alert.* homeassistant.components.amazon_polly.* homeassistant.components.ambient_station.* homeassistant.components.amcrest.* @@ -77,12 +77,12 @@ homeassistant.components.calendar.* homeassistant.components.camera.* homeassistant.components.canary.* homeassistant.components.cover.* -homeassistant.components.crownstone.* homeassistant.components.cpuspeed.* +homeassistant.components.crownstone.* homeassistant.components.deconz.* +homeassistant.components.demo.* homeassistant.components.device_automation.* homeassistant.components.device_tracker.* -homeassistant.components.demo.* homeassistant.components.devolo_home_control.* homeassistant.components.devolo_home_network.* homeassistant.components.dhcp.* @@ -94,8 +94,8 @@ homeassistant.components.efergy.* homeassistant.components.elgato.* homeassistant.components.elkm1.* homeassistant.components.emulated_hue.* -homeassistant.components.esphome.* homeassistant.components.energy.* +homeassistant.components.esphome.* homeassistant.components.evil_genius_labs.* homeassistant.components.fan.* homeassistant.components.fastdotcom.* @@ -106,11 +106,11 @@ homeassistant.components.fitbit.* homeassistant.components.flunearyou.* homeassistant.components.flux_led.* homeassistant.components.forecast_solar.* +homeassistant.components.fritz.* homeassistant.components.fritzbox.* homeassistant.components.fritzbox_callmonitor.* homeassistant.components.fronius.* homeassistant.components.frontend.* -homeassistant.components.fritz.* homeassistant.components.fully_kiosk.* homeassistant.components.geo_location.* homeassistant.components.geocaching.* @@ -149,8 +149,8 @@ homeassistant.components.image_processing.* homeassistant.components.input_button.* homeassistant.components.input_select.* homeassistant.components.integration.* -homeassistant.components.isy994.* homeassistant.components.iqvia.* +homeassistant.components.isy994.* homeassistant.components.jellyfin.* homeassistant.components.jewish_calendar.* homeassistant.components.kaleidescape.* @@ -160,8 +160,8 @@ homeassistant.components.lacrosse_view.* homeassistant.components.lametric.* homeassistant.components.laundrify.* homeassistant.components.lcn.* -homeassistant.components.light.* homeassistant.components.lifx.* +homeassistant.components.light.* homeassistant.components.litterrobot.* homeassistant.components.local_ip.* homeassistant.components.lock.* @@ -196,15 +196,15 @@ homeassistant.components.onewire.* homeassistant.components.open_meteo.* homeassistant.components.openexchangerates.* homeassistant.components.openuv.* -homeassistant.components.peco.* homeassistant.components.overkiz.* +homeassistant.components.peco.* homeassistant.components.persistent_notification.* homeassistant.components.pi_hole.* homeassistant.components.powerwall.* homeassistant.components.proximity.* homeassistant.components.prusalink.* -homeassistant.components.pvoutput.* homeassistant.components.pure_energie.* +homeassistant.components.pvoutput.* homeassistant.components.qnap_qsw.* homeassistant.components.rainmachine.* homeassistant.components.rdw.* @@ -223,9 +223,9 @@ homeassistant.components.samsungtv.* homeassistant.components.scene.* homeassistant.components.schedule.* homeassistant.components.select.* +homeassistant.components.senseme.* homeassistant.components.sensibo.* homeassistant.components.sensor.* -homeassistant.components.senseme.* homeassistant.components.senz.* homeassistant.components.shelly.* homeassistant.components.simplisafe.* @@ -233,9 +233,9 @@ homeassistant.components.slack.* homeassistant.components.sleepiq.* homeassistant.components.smhi.* homeassistant.components.ssdp.* -homeassistant.components.stookalert.* homeassistant.components.statistics.* homeassistant.components.steamist.* +homeassistant.components.stookalert.* homeassistant.components.stream.* homeassistant.components.sun.* homeassistant.components.surepetcare.* @@ -249,8 +249,8 @@ homeassistant.components.tautulli.* homeassistant.components.tcp.* homeassistant.components.tile.* homeassistant.components.tilt_ble.* -homeassistant.components.tplink.* homeassistant.components.tolo.* +homeassistant.components.tplink.* homeassistant.components.tractive.* homeassistant.components.tradfri.* homeassistant.components.trafikverket_ferry.* @@ -279,7 +279,7 @@ homeassistant.components.whois.* homeassistant.components.wiz.* homeassistant.components.worldclock.* homeassistant.components.yale_smart_alarm.* -homeassistant.components.zodiac.* homeassistant.components.zeroconf.* +homeassistant.components.zodiac.* homeassistant.components.zone.* homeassistant.components.zwave_js.* diff --git a/mypy.ini b/mypy.ini index 758dd96ef42..9308ce22c52 100644 --- a/mypy.ini +++ b/mypy.ini @@ -142,16 +142,6 @@ warn_return_any = true warn_unreachable = true no_implicit_reexport = true -[mypy-homeassistant.components.alert.*] -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.abode.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -162,7 +152,7 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true -[mypy-homeassistant.components.acer_projector.*] +[mypy-homeassistant.components.accuweather.*] check_untyped_defs = true disallow_incomplete_defs = true disallow_subclassing_any = true @@ -172,7 +162,7 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true -[mypy-homeassistant.components.accuweather.*] +[mypy-homeassistant.components.acer_projector.*] check_untyped_defs = true disallow_incomplete_defs = true disallow_subclassing_any = true @@ -272,6 +262,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.alert.*] +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.amazon_polly.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -522,16 +522,6 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true -[mypy-homeassistant.components.crownstone.*] -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.cpuspeed.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -542,6 +532,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.crownstone.*] +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.deconz.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -552,6 +552,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.demo.*] +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.device_automation.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -572,16 +582,6 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true -[mypy-homeassistant.components.demo.*] -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.devolo_home_control.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -692,7 +692,7 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true -[mypy-homeassistant.components.esphome.*] +[mypy-homeassistant.components.energy.*] check_untyped_defs = true disallow_incomplete_defs = true disallow_subclassing_any = true @@ -702,7 +702,7 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true -[mypy-homeassistant.components.energy.*] +[mypy-homeassistant.components.esphome.*] check_untyped_defs = true disallow_incomplete_defs = true disallow_subclassing_any = true @@ -812,6 +812,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.fritz.*] +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.fritzbox.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -852,16 +862,6 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true -[mypy-homeassistant.components.fritz.*] -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.fully_kiosk.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -1242,7 +1242,7 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true -[mypy-homeassistant.components.isy994.*] +[mypy-homeassistant.components.iqvia.*] check_untyped_defs = true disallow_incomplete_defs = true disallow_subclassing_any = true @@ -1252,7 +1252,7 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true -[mypy-homeassistant.components.iqvia.*] +[mypy-homeassistant.components.isy994.*] check_untyped_defs = true disallow_incomplete_defs = true disallow_subclassing_any = true @@ -1352,7 +1352,7 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true -[mypy-homeassistant.components.light.*] +[mypy-homeassistant.components.lifx.*] check_untyped_defs = true disallow_incomplete_defs = true disallow_subclassing_any = true @@ -1362,7 +1362,7 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true -[mypy-homeassistant.components.lifx.*] +[mypy-homeassistant.components.light.*] check_untyped_defs = true disallow_incomplete_defs = true disallow_subclassing_any = true @@ -1712,7 +1712,7 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true -[mypy-homeassistant.components.peco.*] +[mypy-homeassistant.components.overkiz.*] check_untyped_defs = true disallow_incomplete_defs = true disallow_subclassing_any = true @@ -1722,7 +1722,7 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true -[mypy-homeassistant.components.overkiz.*] +[mypy-homeassistant.components.peco.*] check_untyped_defs = true disallow_incomplete_defs = true disallow_subclassing_any = true @@ -1782,7 +1782,7 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true -[mypy-homeassistant.components.pvoutput.*] +[mypy-homeassistant.components.pure_energie.*] check_untyped_defs = true disallow_incomplete_defs = true disallow_subclassing_any = true @@ -1792,7 +1792,7 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true -[mypy-homeassistant.components.pure_energie.*] +[mypy-homeassistant.components.pvoutput.*] check_untyped_defs = true disallow_incomplete_defs = true disallow_subclassing_any = true @@ -1982,6 +1982,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.senseme.*] +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.sensibo.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -2002,16 +2012,6 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true -[mypy-homeassistant.components.senseme.*] -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.senz.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -2082,16 +2082,6 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true -[mypy-homeassistant.components.stookalert.*] -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.statistics.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -2112,6 +2102,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.stookalert.*] +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.stream.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -2243,7 +2243,7 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true -[mypy-homeassistant.components.tplink.*] +[mypy-homeassistant.components.tolo.*] check_untyped_defs = true disallow_incomplete_defs = true disallow_subclassing_any = true @@ -2253,7 +2253,7 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true -[mypy-homeassistant.components.tolo.*] +[mypy-homeassistant.components.tplink.*] check_untyped_defs = true disallow_incomplete_defs = true disallow_subclassing_any = true @@ -2544,7 +2544,7 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true -[mypy-homeassistant.components.zodiac.*] +[mypy-homeassistant.components.zeroconf.*] check_untyped_defs = true disallow_incomplete_defs = true disallow_subclassing_any = true @@ -2554,7 +2554,7 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true -[mypy-homeassistant.components.zeroconf.*] +[mypy-homeassistant.components.zodiac.*] check_untyped_defs = true disallow_incomplete_defs = true disallow_subclassing_any = true From eae384bbf70f2031d2671b373518eaddb291ba46 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 15 Sep 2022 11:54:18 +0200 Subject: [PATCH 430/955] Update sqlalchemy to 1.4.41 (#78507) --- homeassistant/components/recorder/manifest.json | 2 +- homeassistant/components/sql/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/recorder/manifest.json b/homeassistant/components/recorder/manifest.json index 9486d2eaf1e..51fd4a6dbe3 100644 --- a/homeassistant/components/recorder/manifest.json +++ b/homeassistant/components/recorder/manifest.json @@ -2,7 +2,7 @@ "domain": "recorder", "name": "Recorder", "documentation": "https://www.home-assistant.io/integrations/recorder", - "requirements": ["sqlalchemy==1.4.40", "fnvhash==0.1.0"], + "requirements": ["sqlalchemy==1.4.41", "fnvhash==0.1.0"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal", "iot_class": "local_push" diff --git a/homeassistant/components/sql/manifest.json b/homeassistant/components/sql/manifest.json index 3e3faaa7372..cb1556b3154 100644 --- a/homeassistant/components/sql/manifest.json +++ b/homeassistant/components/sql/manifest.json @@ -2,7 +2,7 @@ "domain": "sql", "name": "SQL", "documentation": "https://www.home-assistant.io/integrations/sql", - "requirements": ["sqlalchemy==1.4.40"], + "requirements": ["sqlalchemy==1.4.41"], "codeowners": ["@dgomes", "@gjohansson-ST"], "config_flow": true, "iot_class": "local_polling" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3ee61afca23..a84284f152d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -37,7 +37,7 @@ pyudev==0.23.2 pyyaml==6.0 requests==2.28.1 scapy==2.4.5 -sqlalchemy==1.4.40 +sqlalchemy==1.4.41 typing-extensions>=3.10.0.2,<5.0 voluptuous-serialize==2.5.0 voluptuous==0.13.1 diff --git a/requirements_all.txt b/requirements_all.txt index a3ad7b5931f..3dd0e954178 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2291,7 +2291,7 @@ spotipy==2.20.0 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.4.40 +sqlalchemy==1.4.41 # homeassistant.components.srp_energy srpenergy==1.3.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5da458075ef..e605fbe13f5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1570,7 +1570,7 @@ spotipy==2.20.0 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.4.40 +sqlalchemy==1.4.41 # homeassistant.components.srp_energy srpenergy==1.3.6 From b56eabc35bd59316276ea31daabddb21635ff140 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 15 Sep 2022 11:58:24 +0200 Subject: [PATCH 431/955] Enable disallow-any-generics in number (#78502) --- homeassistant/components/number/__init__.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/number/__init__.py b/homeassistant/components/number/__init__.py index 4990fbfa7f8..5f92bdfed8b 100644 --- a/homeassistant/components/number/__init__.py +++ b/homeassistant/components/number/__init__.py @@ -78,10 +78,12 @@ VALID_UNITS: dict[str, tuple[str, ...]] = { NumberDeviceClass.TEMPERATURE: temperature_util.VALID_UNITS, } +# mypy: disallow-any-generics + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up Number entities.""" - component = hass.data[DOMAIN] = EntityComponent( + component = hass.data[DOMAIN] = EntityComponent[NumberEntity]( _LOGGER, DOMAIN, hass, SCAN_INTERVAL ) await component.async_setup(config) @@ -115,13 +117,13 @@ async def async_set_value(entity: NumberEntity, service_call: ServiceCall) -> No async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[NumberEntity] = hass.data[DOMAIN] return await component.async_setup_entry(entry) async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[NumberEntity] = hass.data[DOMAIN] return await component.async_unload_entry(entry) @@ -423,7 +425,9 @@ class NumberEntity(Entity): """Set new value.""" await self.hass.async_add_executor_job(self.set_value, value) - def _convert_to_state_value(self, value: float, method: Callable) -> float: + def _convert_to_state_value( + self, value: float, method: Callable[[float, int], float] + ) -> float: """Convert a value in the number's native unit to the configured unit.""" native_unit_of_measurement = self.native_unit_of_measurement From ada1cff4b181b79d2bd1260092ccd664cb532357 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 15 Sep 2022 12:00:52 +0200 Subject: [PATCH 432/955] Use new media player enums in homekit_controller (#78105) * Use new media player enums in homekit_controller * Replace OK/PROBLEM with ON/OFF * Fix tests --- .../homekit_controller/media_player.py | 28 ++++++++----------- .../specific_devices/test_lg_tv.py | 2 +- 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/homekit_controller/media_player.py b/homeassistant/components/homekit_controller/media_player.py index 092652ed17d..5c791f165e2 100644 --- a/homeassistant/components/homekit_controller/media_player.py +++ b/homeassistant/components/homekit_controller/media_player.py @@ -16,15 +16,9 @@ from homeassistant.components.media_player import ( MediaPlayerDeviceClass, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - STATE_IDLE, - STATE_OK, - STATE_PAUSED, - STATE_PLAYING, - STATE_PROBLEM, -) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -35,9 +29,9 @@ _LOGGER = logging.getLogger(__name__) HK_TO_HA_STATE = { - CurrentMediaStateValues.PLAYING: STATE_PLAYING, - CurrentMediaStateValues.PAUSED: STATE_PAUSED, - CurrentMediaStateValues.STOPPED: STATE_IDLE, + CurrentMediaStateValues.PLAYING: MediaPlayerState.PLAYING, + CurrentMediaStateValues.PAUSED: MediaPlayerState.PAUSED, + CurrentMediaStateValues.STOPPED: MediaPlayerState.IDLE, } @@ -163,21 +157,21 @@ class HomeKitTelevision(HomeKitEntity, MediaPlayerEntity): return char.value @property - def state(self) -> str: + def state(self) -> MediaPlayerState: """State of the tv.""" active = self.service.value(CharacteristicsTypes.ACTIVE) if not active: - return STATE_PROBLEM + return MediaPlayerState.OFF homekit_state = self.service.value(CharacteristicsTypes.CURRENT_MEDIA_STATE) if homekit_state is not None: - return HK_TO_HA_STATE.get(homekit_state, STATE_OK) + return HK_TO_HA_STATE.get(homekit_state, MediaPlayerState.ON) - return STATE_OK + return MediaPlayerState.ON async def async_media_play(self) -> None: """Send play command.""" - if self.state == STATE_PLAYING: + if self.state == MediaPlayerState.PLAYING: _LOGGER.debug("Cannot play while already playing") return @@ -192,7 +186,7 @@ class HomeKitTelevision(HomeKitEntity, MediaPlayerEntity): async def async_media_pause(self) -> None: """Send pause command.""" - if self.state == STATE_PAUSED: + if self.state == MediaPlayerState.PAUSED: _LOGGER.debug("Cannot pause while already paused") return @@ -207,7 +201,7 @@ class HomeKitTelevision(HomeKitEntity, MediaPlayerEntity): async def async_media_stop(self) -> None: """Send stop command.""" - if self.state == STATE_IDLE: + if self.state == MediaPlayerState.IDLE: _LOGGER.debug("Cannot stop when already idle") return diff --git a/tests/components/homekit_controller/specific_devices/test_lg_tv.py b/tests/components/homekit_controller/specific_devices/test_lg_tv.py index 1140ee2dabe..ec26d3a7247 100644 --- a/tests/components/homekit_controller/specific_devices/test_lg_tv.py +++ b/tests/components/homekit_controller/specific_devices/test_lg_tv.py @@ -54,7 +54,7 @@ async def test_lg_tv(hass): # The LG TV doesn't (at least at this patch level) report # its media state via CURRENT_MEDIA_STATE. Therefore "ok" # is the best we can say. - state="ok", + state="on", ), ], ), From aa0fd8c935b6549581acf82d5bd51ac90fc98168 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 15 Sep 2022 12:05:29 +0200 Subject: [PATCH 433/955] Avoid mutating globals in nina tests (#78513) --- tests/components/nina/test_config_flow.py | 29 ++++++++++++----------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/tests/components/nina/test_config_flow.py b/tests/components/nina/test_config_flow.py index a93cecdd102..9d4a0e9376d 100644 --- a/tests/components/nina/test_config_flow.py +++ b/tests/components/nina/test_config_flow.py @@ -1,6 +1,7 @@ """Test the Nina config flow.""" from __future__ import annotations +from copy import deepcopy import json from typing import Any from unittest.mock import patch @@ -70,7 +71,7 @@ async def test_step_user_connection_error(hass: HomeAssistant) -> None: ): result: dict[str, Any] = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=DUMMY_DATA + DOMAIN, context={"source": SOURCE_USER}, data=deepcopy(DUMMY_DATA) ) assert result["type"] == data_entry_flow.FlowResultType.FORM @@ -85,7 +86,7 @@ async def test_step_user_unexpected_exception(hass: HomeAssistant) -> None: ): result: dict[str, Any] = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=DUMMY_DATA + DOMAIN, context={"source": SOURCE_USER}, data=deepcopy(DUMMY_DATA) ) assert result["type"] == data_entry_flow.FlowResultType.ABORT @@ -102,7 +103,7 @@ async def test_step_user(hass: HomeAssistant) -> None: ): result: dict[str, Any] = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=DUMMY_DATA + DOMAIN, context={"source": SOURCE_USER}, data=deepcopy(DUMMY_DATA) ) assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY @@ -132,11 +133,11 @@ async def test_step_user_already_configured(hass: HomeAssistant) -> None: wraps=mocked_request_function, ): result: dict[str, Any] = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=DUMMY_DATA + DOMAIN, context={"source": SOURCE_USER}, data=deepcopy(DUMMY_DATA) ) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=DUMMY_DATA + DOMAIN, context={"source": SOURCE_USER}, data=deepcopy(DUMMY_DATA) ) assert result["type"] == data_entry_flow.FlowResultType.ABORT @@ -149,9 +150,9 @@ async def test_options_flow_init(hass: HomeAssistant) -> None: domain=DOMAIN, title="NINA", data={ - CONF_FILTER_CORONA: DUMMY_DATA[CONF_FILTER_CORONA], - CONF_MESSAGE_SLOTS: DUMMY_DATA[CONF_MESSAGE_SLOTS], - CONST_REGION_A_TO_D: DUMMY_DATA[CONST_REGION_A_TO_D], + CONF_FILTER_CORONA: deepcopy(DUMMY_DATA[CONF_FILTER_CORONA]), + CONF_MESSAGE_SLOTS: deepcopy(DUMMY_DATA[CONF_MESSAGE_SLOTS]), + CONST_REGION_A_TO_D: deepcopy(DUMMY_DATA[CONST_REGION_A_TO_D]), CONF_REGIONS: {"095760000000": "Aach"}, }, ) @@ -187,8 +188,8 @@ async def test_options_flow_init(hass: HomeAssistant) -> None: assert result["data"] is None assert dict(config_entry.data) == { - CONF_FILTER_CORONA: DUMMY_DATA[CONF_FILTER_CORONA], - CONF_MESSAGE_SLOTS: DUMMY_DATA[CONF_MESSAGE_SLOTS], + CONF_FILTER_CORONA: deepcopy(DUMMY_DATA[CONF_FILTER_CORONA]), + CONF_MESSAGE_SLOTS: deepcopy(DUMMY_DATA[CONF_MESSAGE_SLOTS]), CONST_REGION_A_TO_D: ["072350000000_1"], CONST_REGION_E_TO_H: [], CONST_REGION_I_TO_L: [], @@ -206,7 +207,7 @@ async def test_options_flow_with_no_selection(hass: HomeAssistant) -> None: config_entry = MockConfigEntry( domain=DOMAIN, title="NINA", - data=DUMMY_DATA, + data=deepcopy(DUMMY_DATA), ) config_entry.add_to_hass(hass) @@ -246,7 +247,7 @@ async def test_options_flow_connection_error(hass: HomeAssistant) -> None: config_entry = MockConfigEntry( domain=DOMAIN, title="NINA", - data=DUMMY_DATA, + data=deepcopy(DUMMY_DATA), ) config_entry.add_to_hass(hass) @@ -271,7 +272,7 @@ async def test_options_flow_unexpected_exception(hass: HomeAssistant) -> None: config_entry = MockConfigEntry( domain=DOMAIN, title="NINA", - data=DUMMY_DATA, + data=deepcopy(DUMMY_DATA), ) config_entry.add_to_hass(hass) @@ -295,7 +296,7 @@ async def test_options_flow_entity_removal(hass: HomeAssistant) -> None: config_entry = MockConfigEntry( domain=DOMAIN, title="NINA", - data=DUMMY_DATA, + data=deepcopy(DUMMY_DATA) | {CONF_REGIONS: {"095760000000": "Aach"}}, ) config_entry.add_to_hass(hass) From c0b04e9f91a34dfee0ceb12770148317fe3e2cbf Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 15 Sep 2022 12:13:36 +0200 Subject: [PATCH 434/955] Sort some code in the search integration (#78519) --- homeassistant/components/search/__init__.py | 121 +++++++++++--------- 1 file changed, 65 insertions(+), 56 deletions(-) diff --git a/homeassistant/components/search/__init__.py b/homeassistant/components/search/__init__.py index bfde6f38a73..e7df1de860e 100644 --- a/homeassistant/components/search/__init__.py +++ b/homeassistant/components/search/__init__.py @@ -30,13 +30,14 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ( "area", "automation", + "blueprint", "config_entry", "device", "entity", "group", + "person", "scene", "script", - "person", ) ), vol.Required("item_id"): str, @@ -66,9 +67,16 @@ class Searcher: """ # These types won't be further explored. Config entries + Output types. - DONT_RESOLVE = {"scene", "automation", "script", "group", "config_entry", "area"} + DONT_RESOLVE = { + "area", + "automation", + "config_entry", + "group", + "scene", + "script", + } # These types exist as an entity and so need cleanup in results - EXIST_AS_ENTITY = {"script", "scene", "automation", "group", "person"} + EXIST_AS_ENTITY = {"automation", "group", "person", "scene", "script"} def __init__( self, @@ -130,6 +138,7 @@ class Searcher: """Resolve an area.""" for device in device_registry.async_entries_for_area(self._device_reg, area_id): self._add_or_resolve("device", device.id) + for entity_entry in entity_registry.async_entries_for_area( self._entity_reg, area_id ): @@ -141,6 +150,39 @@ class Searcher: for entity_id in automation.automations_with_area(self.hass, area_id): self._add_or_resolve("entity", entity_id) + @callback + def _resolve_automation(self, automation_entity_id) -> None: + """Resolve an automation. + + Will only be called if automation is an entry point. + """ + for entity in automation.entities_in_automation( + self.hass, automation_entity_id + ): + self._add_or_resolve("entity", entity) + + for device in automation.devices_in_automation(self.hass, automation_entity_id): + self._add_or_resolve("device", device) + + for area in automation.areas_in_automation(self.hass, automation_entity_id): + self._add_or_resolve("area", area) + + @callback + def _resolve_config_entry(self, config_entry_id) -> None: + """Resolve a config entry. + + Will only be called if config entry is an entry point. + """ + for device_entry in device_registry.async_entries_for_config_entry( + self._device_reg, config_entry_id + ): + self._add_or_resolve("device", device_entry.id) + + for entity_entry in entity_registry.async_entries_for_config_entry( + self._entity_reg, config_entry_id + ): + self._add_or_resolve("entity", entity_entry.entity_id) + @callback def _resolve_device(self, device_id) -> None: """Resolve a device.""" @@ -206,21 +248,31 @@ class Searcher: self._add_or_resolve(domain, entity_id) @callback - def _resolve_automation(self, automation_entity_id) -> None: - """Resolve an automation. + def _resolve_group(self, group_entity_id) -> None: + """Resolve a group. - Will only be called if automation is an entry point. + Will only be called if group is an entry point. """ - for entity in automation.entities_in_automation( - self.hass, automation_entity_id - ): + for entity_id in group.get_entity_ids(self.hass, group_entity_id): + self._add_or_resolve("entity", entity_id) + + @callback + def _resolve_person(self, person_entity_id) -> None: + """Resolve a person. + + Will only be called if person is an entry point. + """ + for entity in person.entities_in_person(self.hass, person_entity_id): self._add_or_resolve("entity", entity) - for device in automation.devices_in_automation(self.hass, automation_entity_id): - self._add_or_resolve("device", device) + @callback + def _resolve_scene(self, scene_entity_id) -> None: + """Resolve a scene. - for area in automation.areas_in_automation(self.hass, automation_entity_id): - self._add_or_resolve("area", area) + Will only be called if scene is an entry point. + """ + for entity in scene.entities_in_scene(self.hass, scene_entity_id): + self._add_or_resolve("entity", entity) @callback def _resolve_script(self, script_entity_id) -> None: @@ -236,46 +288,3 @@ class Searcher: for area in script.areas_in_script(self.hass, script_entity_id): self._add_or_resolve("area", area) - - @callback - def _resolve_group(self, group_entity_id) -> None: - """Resolve a group. - - Will only be called if group is an entry point. - """ - for entity_id in group.get_entity_ids(self.hass, group_entity_id): - self._add_or_resolve("entity", entity_id) - - @callback - def _resolve_scene(self, scene_entity_id) -> None: - """Resolve a scene. - - Will only be called if scene is an entry point. - """ - for entity in scene.entities_in_scene(self.hass, scene_entity_id): - self._add_or_resolve("entity", entity) - - @callback - def _resolve_person(self, person_entity_id) -> None: - """Resolve a person. - - Will only be called if person is an entry point. - """ - for entity in person.entities_in_person(self.hass, person_entity_id): - self._add_or_resolve("entity", entity) - - @callback - def _resolve_config_entry(self, config_entry_id) -> None: - """Resolve a config entry. - - Will only be called if config entry is an entry point. - """ - for device_entry in device_registry.async_entries_for_config_entry( - self._device_reg, config_entry_id - ): - self._add_or_resolve("device", device_entry.id) - - for entity_entry in entity_registry.async_entries_for_config_entry( - self._entity_reg, config_entry_id - ): - self._add_or_resolve("entity", entity_entry.entity_id) From 6dc3c0b572ebfeaeeace5f2f46de6eadfad077aa Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 15 Sep 2022 12:45:18 +0200 Subject: [PATCH 435/955] Update black to 22.8.0 (#78509) --- .pre-commit-config.yaml | 2 +- requirements_test_pre_commit.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 56faefc1a85..7900d7bda27 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,7 +5,7 @@ repos: - id: pyupgrade args: [--py39-plus] - repo: https://github.com/psf/black - rev: 22.6.0 + rev: 22.8.0 hooks: - id: black args: diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 115e826537a..0b1d9e97d39 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -1,7 +1,7 @@ # Automatically generated from .pre-commit-config.yaml by gen_requirements_all.py, do not edit bandit==1.7.4 -black==22.6.0 +black==22.8.0 codespell==2.1.0 flake8-comprehensions==3.10.0 flake8-docstrings==1.6.0 From 2c03578f6fedcc8ec73721efcb3b46034ab13424 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 15 Sep 2022 13:25:12 +0200 Subject: [PATCH 436/955] Bump yalexs_ble to 1.9.2 (#78508) --- homeassistant/components/yalexs_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/yalexs_ble/manifest.json b/homeassistant/components/yalexs_ble/manifest.json index 673521f9e06..c70669f7cc6 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.0"], + "requirements": ["yalexs-ble==1.9.2"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "bluetooth": [ diff --git a/requirements_all.txt b/requirements_all.txt index 3dd0e954178..4a62fe39c1b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2554,7 +2554,7 @@ xs1-api-client==3.0.0 yalesmartalarmclient==0.3.9 # homeassistant.components.yalexs_ble -yalexs-ble==1.9.0 +yalexs-ble==1.9.2 # homeassistant.components.august yalexs==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e605fbe13f5..cc1ed7a49db 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1758,7 +1758,7 @@ xmltodict==0.13.0 yalesmartalarmclient==0.3.9 # homeassistant.components.yalexs_ble -yalexs-ble==1.9.0 +yalexs-ble==1.9.2 # homeassistant.components.august yalexs==1.2.1 From 8ede711f68fab6ba437a6977f3c8754dc7eb6c8e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 15 Sep 2022 13:25:25 +0200 Subject: [PATCH 437/955] Bump PySwitchbot to 0.19.9 (#78504) --- 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 41b3f5aa61b..a321c964edc 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.19.8"], + "requirements": ["PySwitchbot==0.19.9"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 4a62fe39c1b..b62f526bc98 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.19.8 +PySwitchbot==0.19.9 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cc1ed7a49db..4e36a0e576f 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.19.8 +PySwitchbot==0.19.9 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From 2331e2a55c194a0f59101fa58afd9fa209966681 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 15 Sep 2022 13:25:40 +0200 Subject: [PATCH 438/955] Bump led-ble to 0.10.1 (#78511) --- homeassistant/components/led_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/led_ble/manifest.json b/homeassistant/components/led_ble/manifest.json index 89b4fdb26af..65725ed482a 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/ble_ble", - "requirements": ["led-ble==0.10.0"], + "requirements": ["led-ble==0.10.1"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "bluetooth": [ diff --git a/requirements_all.txt b/requirements_all.txt index b62f526bc98..650857cbdf6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -974,7 +974,7 @@ lakeside==0.12 laundrify_aio==1.1.2 # homeassistant.components.led_ble -led-ble==0.10.0 +led-ble==0.10.1 # homeassistant.components.foscam libpyfoscam==1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4e36a0e576f..90ef39f27c1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -709,7 +709,7 @@ lacrosse-view==0.0.9 laundrify_aio==1.1.2 # homeassistant.components.led_ble -led-ble==0.10.0 +led-ble==0.10.1 # homeassistant.components.foscam libpyfoscam==1.0 From 4168892d0a3ce00f62c392ebbe600aab0bf437ac Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 15 Sep 2022 13:25:50 +0200 Subject: [PATCH 439/955] Bump aiohomekit to 1.5.8 (#78515) --- 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 0b67f80bac5..338015d8c40 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==1.5.7"], + "requirements": ["aiohomekit==1.5.8"], "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 650857cbdf6..9374651ffee 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==1.5.7 +aiohomekit==1.5.8 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 90ef39f27c1..bda4ed283ac 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==1.5.7 +aiohomekit==1.5.8 # homeassistant.components.emulated_hue # homeassistant.components.http From 96de76fc6fe04d7cd31c2d85b9c8c2dfc9df8679 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 15 Sep 2022 14:04:07 +0200 Subject: [PATCH 440/955] Adjust MEDIA_CLASS_MAP in dlna-dms (#78458) * Adjust MEDIA_CLASS_MAP in dlna-dms * Revert --- homeassistant/components/dlna_dms/const.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/dlna_dms/const.py b/homeassistant/components/dlna_dms/const.py index c38ab64e112..8d4cb6352ee 100644 --- a/homeassistant/components/dlna_dms/const.py +++ b/homeassistant/components/dlna_dms/const.py @@ -5,7 +5,7 @@ from collections.abc import Mapping import logging from typing import Final -from homeassistant.components.media_player import MediaClass, MediaType +from homeassistant.components.media_player import MediaClass LOGGER = logging.getLogger(__package__) @@ -41,7 +41,7 @@ PROTOCOL_ANY: Final = "*" STREAMABLE_PROTOCOLS: Final = [PROTOCOL_HTTP, PROTOCOL_RTSP, PROTOCOL_ANY] # Map UPnP object class to media_player media class -MEDIA_CLASS_MAP: Mapping[str, str] = { +MEDIA_CLASS_MAP: Mapping[str, MediaClass] = { "object": MediaClass.URL, "object.item": MediaClass.URL, "object.item.imageItem": MediaClass.IMAGE, @@ -71,8 +71,8 @@ MEDIA_CLASS_MAP: Mapping[str, str] = { "object.container.genre.musicGenre": MediaClass.GENRE, "object.container.genre.movieGenre": MediaClass.GENRE, "object.container.channelGroup": MediaClass.CHANNEL, - "object.container.channelGroup.audioChannelGroup": MediaType.CHANNELS, - "object.container.channelGroup.videoChannelGroup": MediaType.CHANNELS, + "object.container.channelGroup.audioChannelGroup": MediaClass.CHANNEL, + "object.container.channelGroup.videoChannelGroup": MediaClass.CHANNEL, "object.container.epgContainer": MediaClass.DIRECTORY, "object.container.storageSystem": MediaClass.DIRECTORY, "object.container.storageVolume": MediaClass.DIRECTORY, From a38d998000209a6dcaef5dba4049eb1cb7dfebff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Thu, 15 Sep 2022 14:05:34 +0200 Subject: [PATCH 441/955] Bump awesomeversion from 22.8.0 to 22.9.0 (#78525) --- 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 a84284f152d..8f6ffbf2cbd 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -8,7 +8,7 @@ async-upnp-client==0.31.2 async_timeout==4.0.2 atomicwrites-homeassistant==1.4.1 attrs==21.2.0 -awesomeversion==22.8.0 +awesomeversion==22.9.0 bcrypt==3.1.7 bleak-retry-connector==1.17.1 bleak==0.17.0 diff --git a/pyproject.toml b/pyproject.toml index 8611dab5d4f..fda755febd3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ dependencies = [ "async_timeout==4.0.2", "attrs==21.2.0", "atomicwrites-homeassistant==1.4.1", - "awesomeversion==22.8.0", + "awesomeversion==22.9.0", "bcrypt==3.1.7", "certifi>=2021.5.30", "ciso8601==2.2.0", diff --git a/requirements.txt b/requirements.txt index f2ad6f875b1..05693477c37 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ astral==2.2 async_timeout==4.0.2 attrs==21.2.0 atomicwrites-homeassistant==1.4.1 -awesomeversion==22.8.0 +awesomeversion==22.9.0 bcrypt==3.1.7 certifi>=2021.5.30 ciso8601==2.2.0 From cd2abf368ff3a12e588a078fd02ca3d6870fe44c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 15 Sep 2022 14:26:09 +0200 Subject: [PATCH 442/955] Use self._attr_state in vlc_telnet media player (#78517) --- .../components/vlc_telnet/media_player.py | 24 +++++++------------ 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/vlc_telnet/media_player.py b/homeassistant/components/vlc_telnet/media_player.py index 130d092cbd7..ef3406bda28 100644 --- a/homeassistant/components/vlc_telnet/media_player.py +++ b/homeassistant/components/vlc_telnet/media_player.py @@ -96,7 +96,6 @@ class VlcDevice(MediaPlayerEntity): self._name = name self._volume: float | None = None self._muted: bool | None = None - self._state: str | None = None self._media_position_updated_at: datetime | None = None self._media_position: int | None = None self._media_duration: int | None = None @@ -134,7 +133,7 @@ class VlcDevice(MediaPlayerEntity): ) return - self._state = MediaPlayerState.IDLE + self._attr_state = MediaPlayerState.IDLE self._available = True LOGGER.info("Connected to vlc host: %s", self._vlc.host) @@ -144,13 +143,13 @@ class VlcDevice(MediaPlayerEntity): self._volume = status.audio_volume / MAX_VOLUME state = status.state if state == "playing": - self._state = MediaPlayerState.PLAYING + self._attr_state = MediaPlayerState.PLAYING elif state == "paused": - self._state = MediaPlayerState.PAUSED + self._attr_state = MediaPlayerState.PAUSED else: - self._state = MediaPlayerState.IDLE + self._attr_state = MediaPlayerState.IDLE - if self._state != MediaPlayerState.IDLE: + if self._attr_state != MediaPlayerState.IDLE: self._media_duration = (await self._vlc.get_length()).length time_output = await self._vlc.get_time() vlc_position = time_output.time @@ -191,11 +190,6 @@ class VlcDevice(MediaPlayerEntity): """Return the name of the device.""" return self._name - @property - def state(self) -> str | None: - """Return the state of the device.""" - return self._state - @property def available(self) -> bool: """Return True if entity is available.""" @@ -267,7 +261,7 @@ class VlcDevice(MediaPlayerEntity): async def async_media_play(self) -> None: """Send play command.""" await self._vlc.play() - self._state = MediaPlayerState.PLAYING + self._attr_state = MediaPlayerState.PLAYING @catch_vlc_errors async def async_media_pause(self) -> None: @@ -278,13 +272,13 @@ class VlcDevice(MediaPlayerEntity): # pause. await self._vlc.pause() - self._state = MediaPlayerState.PAUSED + self._attr_state = MediaPlayerState.PAUSED @catch_vlc_errors async def async_media_stop(self) -> None: """Send stop command.""" await self._vlc.stop() - self._state = MediaPlayerState.IDLE + self._attr_state = MediaPlayerState.IDLE @catch_vlc_errors async def async_play_media( @@ -310,7 +304,7 @@ class VlcDevice(MediaPlayerEntity): ) await self._vlc.add(media_id) - self._state = MediaPlayerState.PLAYING + self._attr_state = MediaPlayerState.PLAYING @catch_vlc_errors async def async_media_previous_track(self) -> None: From 69ca055fd89391f3e1e7931cd0476f6c00a762d9 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 15 Sep 2022 14:33:12 +0200 Subject: [PATCH 443/955] Update requests_mock to 1.10.0 (#78510) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 63833940bdf..a94e29ef4fb 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -25,7 +25,7 @@ pytest-sugar==0.9.5 pytest-timeout==2.1.0 pytest-xdist==2.5.0 pytest==7.1.3 -requests_mock==1.9.2 +requests_mock==1.10.0 respx==0.19.2 stdlib-list==0.7.0 tomli==2.0.1;python_version<"3.11" From a443cefa7b56ac4663cf38686d4a3930c0376fde Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 15 Sep 2022 14:44:20 +0200 Subject: [PATCH 444/955] Use self._attr_state in demo media player (#78520) --- homeassistant/components/demo/media_player.py | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/demo/media_player.py b/homeassistant/components/demo/media_player.py index e0c05e853c1..8c4df015d18 100644 --- a/homeassistant/components/demo/media_player.py +++ b/homeassistant/components/demo/media_player.py @@ -114,7 +114,7 @@ class AbstractDemoPlayer(MediaPlayerEntity): ) -> None: """Initialize the demo device.""" self._attr_name = name - self._player_state = MediaPlayerState.PLAYING + self._attr_state = MediaPlayerState.PLAYING self._volume_level = 1.0 self._volume_muted = False self._shuffle = False @@ -122,11 +122,6 @@ class AbstractDemoPlayer(MediaPlayerEntity): self._sound_mode = DEFAULT_SOUND_MODE self._attr_device_class = device_class - @property - def state(self) -> str: - """Return the state of the player.""" - return self._player_state - @property def volume_level(self) -> float: """Return the volume level of the media player (0..1).""" @@ -154,12 +149,12 @@ class AbstractDemoPlayer(MediaPlayerEntity): def turn_on(self) -> None: """Turn the media player on.""" - self._player_state = MediaPlayerState.PLAYING + self._attr_state = MediaPlayerState.PLAYING self.schedule_update_ha_state() def turn_off(self) -> None: """Turn the media player off.""" - self._player_state = MediaPlayerState.OFF + self._attr_state = MediaPlayerState.OFF self.schedule_update_ha_state() def mute_volume(self, mute: bool) -> None: @@ -184,17 +179,17 @@ class AbstractDemoPlayer(MediaPlayerEntity): def media_play(self) -> None: """Send play command.""" - self._player_state = MediaPlayerState.PLAYING + self._attr_state = MediaPlayerState.PLAYING self.schedule_update_ha_state() def media_pause(self) -> None: """Send pause command.""" - self._player_state = MediaPlayerState.PAUSED + self._attr_state = MediaPlayerState.PAUSED self.schedule_update_ha_state() def media_stop(self) -> None: """Send stop command.""" - self._player_state = MediaPlayerState.OFF + self._attr_state = MediaPlayerState.OFF self.schedule_update_ha_state() def set_shuffle(self, shuffle: bool) -> None: @@ -264,7 +259,7 @@ class DemoYoutubePlayer(AbstractDemoPlayer): position = self._progress - if self._player_state == MediaPlayerState.PLAYING: + if self.state == MediaPlayerState.PLAYING: position += int( (dt_util.utcnow() - self._progress_updated_at).total_seconds() ) @@ -277,7 +272,7 @@ class DemoYoutubePlayer(AbstractDemoPlayer): Returns value from homeassistant.util.dt.utcnow(). """ - if self._player_state == MediaPlayerState.PLAYING: + if self.state == MediaPlayerState.PLAYING: return self._progress_updated_at return None @@ -396,7 +391,7 @@ class DemoMusicPlayer(AbstractDemoPlayer): """Clear players playlist.""" self.tracks = [] self._cur_track = 0 - self._player_state = MediaPlayerState.OFF + self._attr_state = MediaPlayerState.OFF self.schedule_update_ha_state() def set_repeat(self, repeat: RepeatMode) -> None: From de7e12eeaf566a0ee15569cfd75dda68bb8cc5ec Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 15 Sep 2022 14:47:05 +0200 Subject: [PATCH 445/955] Enable disallow-any-generics in light (#78499) --- homeassistant/components/light/__init__.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 9a40d159ad6..e179672f45a 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -112,6 +112,8 @@ COLOR_MODES_COLOR = { ColorMode.XY, } +# mypy: disallow-any-generics + def filter_supported_color_modes(color_modes: Iterable[ColorMode]) -> set[ColorMode]: """Filter the given color modes.""" @@ -167,7 +169,7 @@ def color_temp_supported(color_modes: Iterable[ColorMode | str] | None) -> bool: return ColorMode.COLOR_TEMP in color_modes -def get_supported_color_modes(hass: HomeAssistant, entity_id: str) -> set | None: +def get_supported_color_modes(hass: HomeAssistant, entity_id: str) -> set[str] | None: """Get supported color modes for a light entity. First try the statemachine, then entity registry. @@ -366,7 +368,7 @@ def filter_turn_on_params(light: LightEntity, params: dict[str, Any]) -> dict[st async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: C901 """Expose light control via state machine and services.""" - component = hass.data[DOMAIN] = EntityComponent( + component = hass.data[DOMAIN] = EntityComponent[LightEntity]( _LOGGER, DOMAIN, hass, SCAN_INTERVAL ) await component.async_setup(config) @@ -583,13 +585,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - component = cast(EntityComponent, hass.data[DOMAIN]) + component: EntityComponent[LightEntity] = hass.data[DOMAIN] return await component.async_setup_entry(entry) async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - component = cast(EntityComponent, hass.data[DOMAIN]) + component: EntityComponent[LightEntity] = hass.data[DOMAIN] return await component.async_unload_entry(entry) @@ -868,8 +870,10 @@ class LightEntity(ToggleEntity): return data - def _light_internal_convert_color(self, color_mode: ColorMode | str) -> dict: - data: dict[str, tuple] = {} + def _light_internal_convert_color( + self, color_mode: ColorMode | str + ) -> dict[str, tuple[float, ...]]: + data: dict[str, tuple[float, ...]] = {} if color_mode == ColorMode.HS and self.hs_color: hs_color = self.hs_color data[ATTR_HS_COLOR] = (round(hs_color[0], 3), round(hs_color[1], 3)) From 8dbe293ae28550cd10fb24c201772e9e2448e4bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Thu, 15 Sep 2022 15:01:40 +0200 Subject: [PATCH 446/955] Add version to templates (#78484) --- homeassistant/helpers/template.py | 8 +++++++ tests/helpers/test_template.py | 39 +++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index ea6b764a75a..3999231eb68 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -23,6 +23,7 @@ from typing import Any, NoReturn, TypeVar, cast, overload from urllib.parse import urlencode as urllib_urlencode import weakref +from awesomeversion import AwesomeVersion import jinja2 from jinja2 import pass_context, pass_environment from jinja2.sandbox import ImmutableSandboxedEnvironment @@ -1529,6 +1530,11 @@ def arc_tangent2(*args, default=_SENTINEL): return default +def version(value): + """Filter and function to get version object of the value.""" + return AwesomeVersion(value) + + def square_root(value, default=_SENTINEL): """Filter and function to get square root of the value.""" try: @@ -2001,6 +2007,7 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): self.filters["slugify"] = slugify self.filters["iif"] = iif self.filters["bool"] = forgiving_boolean + self.filters["version"] = version self.globals["log"] = logarithm self.globals["sin"] = sine self.globals["cos"] = cosine @@ -2033,6 +2040,7 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): self.globals["slugify"] = slugify self.globals["iif"] = iif self.globals["bool"] = forgiving_boolean + self.globals["version"] = version self.tests["is_number"] = is_number self.tests["match"] = regex_match self.tests["search"] = regex_search diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 3186c10b20e..fa9ec4e76d6 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -1566,6 +1566,45 @@ def test_timedelta(mock_is_safe, hass): assert result == "15 days" +def test_version(hass): + """Test version filter and function.""" + filter_result = template.Template( + "{{ '2099.9.9' | version}}", + hass, + ).async_render() + function_result = template.Template( + "{{ version('2099.9.9')}}", + hass, + ).async_render() + assert filter_result == function_result == "2099.9.9" + + filter_result = template.Template( + "{{ '2099.9.9' | version < '2099.9.10' }}", + hass, + ).async_render() + function_result = template.Template( + "{{ version('2099.9.9') < '2099.9.10' }}", + hass, + ).async_render() + assert filter_result == function_result is True + + filter_result = template.Template( + "{{ '2099.9.9' | version == '2099.9.9' }}", + hass, + ).async_render() + function_result = template.Template( + "{{ version('2099.9.9') == '2099.9.9' }}", + hass, + ).async_render() + assert filter_result == function_result is True + + with pytest.raises(TemplateError): + template.Template( + "{{ version(None) < '2099.9.10' }}", + hass, + ).async_render() + + def test_regex_match(hass): """Test regex_match method.""" tpl = template.Template( From 6f02f7c6cee268f3adebf78a37ae698770a981d6 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Thu, 15 Sep 2022 16:01:55 +0200 Subject: [PATCH 447/955] Bump pyfritzhome to 0.6.7 (#78324) --- homeassistant/components/fritzbox/manifest.json | 2 +- homeassistant/components/fritzbox/sensor.py | 10 ++++------ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/fritzbox/__init__.py | 2 +- tests/components/fritzbox/test_switch.py | 4 ++-- 6 files changed, 10 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/fritzbox/manifest.json b/homeassistant/components/fritzbox/manifest.json index 710f7e8f0c4..422db12b68a 100644 --- a/homeassistant/components/fritzbox/manifest.json +++ b/homeassistant/components/fritzbox/manifest.json @@ -2,7 +2,7 @@ "domain": "fritzbox", "name": "AVM FRITZ!SmartHome", "documentation": "https://www.home-assistant.io/integrations/fritzbox", - "requirements": ["pyfritzhome==0.6.5"], + "requirements": ["pyfritzhome==0.6.7"], "ssdp": [ { "st": "urn:schemas-upnp-org:device:fritzbox:1" diff --git a/homeassistant/components/fritzbox/sensor.py b/homeassistant/components/fritzbox/sensor.py index 4908cfa84a3..4467c9fe1ea 100644 --- a/homeassistant/components/fritzbox/sensor.py +++ b/homeassistant/components/fritzbox/sensor.py @@ -87,7 +87,7 @@ SENSOR_TYPES: Final[tuple[FritzSensorEntityDescription, ...]] = ( device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, suitable=lambda device: device.has_powermeter, # type: ignore[no-any-return] - native_value=lambda device: device.power / 1000 if device.power else 0.0, + native_value=lambda device: round((device.power or 0.0) / 1000, 3), ), FritzSensorEntityDescription( key="voltage", @@ -96,9 +96,7 @@ SENSOR_TYPES: Final[tuple[FritzSensorEntityDescription, ...]] = ( device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, suitable=lambda device: device.has_powermeter, # type: ignore[no-any-return] - native_value=lambda device: device.voltage - if getattr(device, "voltage", None) - else 0.0, + native_value=lambda device: round((device.voltage or 0.0) / 1000, 2), ), FritzSensorEntityDescription( key="electric_current", @@ -107,7 +105,7 @@ SENSOR_TYPES: Final[tuple[FritzSensorEntityDescription, ...]] = ( device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, suitable=lambda device: device.has_powermeter, # type: ignore[no-any-return] - native_value=lambda device: device.power / device.voltage / 1000 + native_value=lambda device: round(device.power / device.voltage, 3) if device.power and getattr(device, "voltage", None) else 0.0, ), @@ -118,7 +116,7 @@ SENSOR_TYPES: Final[tuple[FritzSensorEntityDescription, ...]] = ( device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, suitable=lambda device: device.has_powermeter, # type: ignore[no-any-return] - native_value=lambda device: device.energy / 1000 if device.energy else 0.0, + native_value=lambda device: (device.energy or 0.0) / 1000, ), # Thermostat Sensors FritzSensorEntityDescription( diff --git a/requirements_all.txt b/requirements_all.txt index 9374651ffee..54e41610684 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1566,7 +1566,7 @@ pyforked-daapd==0.1.11 pyfreedompro==1.1.0 # homeassistant.components.fritzbox -pyfritzhome==0.6.5 +pyfritzhome==0.6.7 # homeassistant.components.fronius pyfronius==0.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bda4ed283ac..fe38fd99219 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1091,7 +1091,7 @@ pyforked-daapd==0.1.11 pyfreedompro==1.1.0 # homeassistant.components.fritzbox -pyfritzhome==0.6.5 +pyfritzhome==0.6.7 # homeassistant.components.fronius pyfronius==0.7.1 diff --git a/tests/components/fritzbox/__init__.py b/tests/components/fritzbox/__init__.py index 05cd60059fa..3ed4327d8e3 100644 --- a/tests/components/fritzbox/__init__.py +++ b/tests/components/fritzbox/__init__.py @@ -121,7 +121,7 @@ class FritzDeviceSwitchMock(FritzDeviceBaseMock): battery_level = None device_lock = "fake_locked_device" energy = 1234 - voltage = 230 + voltage = 230000 fw_version = "1.2.3" has_alarm = False has_powermeter = True diff --git a/tests/components/fritzbox/test_switch.py b/tests/components/fritzbox/test_switch.py index 75799a08d48..362fdfac951 100644 --- a/tests/components/fritzbox/test_switch.py +++ b/tests/components/fritzbox/test_switch.py @@ -77,14 +77,14 @@ async def test_setup(hass: HomeAssistant, fritz: Mock): ], [ f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_voltage", - "230", + "230.0", f"{CONF_FAKE_NAME} Voltage", ELECTRIC_POTENTIAL_VOLT, SensorStateClass.MEASUREMENT, ], [ f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_electric_current", - "0.0246869565217391", + "0.025", f"{CONF_FAKE_NAME} Electric Current", ELECTRIC_CURRENT_AMPERE, SensorStateClass.MEASUREMENT, From bb5c1ad6590b60a7650b49cf09d5e6fa73ba962f Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Thu, 15 Sep 2022 16:05:58 +0200 Subject: [PATCH 448/955] Remove some low level calls from Fritz (#77848) Co-authored-by: Michael <35783820+mib1185@users.noreply.github.com> --- homeassistant/components/fritz/common.py | 31 +++++++++--------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index d424b38ec24..6364ada9fb2 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -207,38 +207,35 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator): self.fritz_hosts = FritzHosts(fc=self.connection) self.fritz_guest_wifi = FritzGuestWLAN(fc=self.connection) self.fritz_status = FritzStatus(fc=self.connection) - info = self.connection.call_action("DeviceInfo:1", "GetInfo") + info = self.fritz_status.get_device_info() _LOGGER.debug( "gathered device info of %s %s", self.host, { - **info, + **vars(info), "NewDeviceLog": "***omitted***", "NewSerialNumber": "***omitted***", }, ) if not self._unique_id: - self._unique_id = info["NewSerialNumber"] + self._unique_id = info.serial_number - self._model = info.get("NewModelName") - self._current_firmware = info.get("NewSoftwareVersion") + self._model = info.model_name + self._current_firmware = info.software_version ( self._update_available, self._latest_firmware, self._release_url, ) = self._update_device_info() - if "Layer3Forwarding1" in self.connection.services: - if connection_type := self.connection.call_action( - "Layer3Forwarding1", "GetDefaultConnectionService" - ).get("NewDefaultConnectionService"): - # Return NewDefaultConnectionService sample: "1.WANPPPConnection.1" - self.device_conn_type = connection_type[2:][:-2] - self.device_is_router = self.connection.call_action( - self.device_conn_type, "GetInfo" - ).get("NewEnable") + + if self.fritz_status.has_wan_support: + self.device_conn_type = ( + self.fritz_status.get_default_connection_service().connection_service + ) + self.device_is_router = self.fritz_status.has_wan_enabled async def _async_update_data(self) -> None: """Update FritzboxTools data.""" @@ -401,11 +398,7 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator): wan_access=None, ) - if ( - "Hosts1" not in self.connection.services - or "X_AVM-DE_GetMeshListPath" - not in self.connection.services["Hosts1"].actions - ) or ( + if not self.fritz_status.device_has_mesh_support or ( self._options and self._options.get(CONF_OLD_DISCOVERY, DEFAULT_CONF_OLD_DISCOVERY) ): From 7d1150901108738c2767cdcc6ea093c32615aefd Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 15 Sep 2022 17:47:07 +0200 Subject: [PATCH 449/955] Cleanup self._attr_state in samsungtv media player (#78518) --- homeassistant/components/samsungtv/media_player.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index 2d457eb29bd..3e544b181f1 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -116,7 +116,6 @@ class SamsungTVDevice(MediaPlayerEntity): self._playing: bool = True self._attr_name: str | None = config_entry.data.get(CONF_NAME) - self._attr_state: str | None = None self._attr_unique_id = config_entry.unique_id self._attr_is_volume_muted: bool = False self._attr_device_class = MediaPlayerDeviceClass.TV @@ -198,7 +197,7 @@ class SamsungTVDevice(MediaPlayerEntity): else MediaPlayerState.OFF ) if self._attr_state != old_state: - LOGGER.debug("TV %s state updated to %s", self._host, self._attr_state) + LOGGER.debug("TV %s state updated to %s", self._host, self.state) if self._attr_state != MediaPlayerState.ON: if self._dmr_device and self._dmr_device.is_subscribed: @@ -357,7 +356,7 @@ class SamsungTVDevice(MediaPlayerEntity): if self._auth_failed: return False return ( - self._attr_state == MediaPlayerState.ON + self.state == MediaPlayerState.ON or self._on_script is not None or self._mac is not None or self._power_off_in_progress() From b29605060a74c441550708ccf4ace4b697f66ae6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 15 Sep 2022 17:48:05 +0200 Subject: [PATCH 450/955] Enforce MediaPlayerState in hdmi_cec media player (#78522) --- homeassistant/components/hdmi_cec/__init__.py | 33 ++----------------- .../components/hdmi_cec/media_player.py | 25 ++++++-------- homeassistant/components/hdmi_cec/switch.py | 20 +++++++---- tests/components/hdmi_cec/test_switch.py | 17 +++------- 4 files changed, 29 insertions(+), 66 deletions(-) diff --git a/homeassistant/components/hdmi_cec/__init__.py b/homeassistant/components/hdmi_cec/__init__.py index fa64b3dac41..05d5ab57841 100644 --- a/homeassistant/components/hdmi_cec/__init__.py +++ b/homeassistant/components/hdmi_cec/__init__.py @@ -17,11 +17,6 @@ from pycec.const import ( KEY_MUTE_TOGGLE, KEY_VOLUME_DOWN, KEY_VOLUME_UP, - POWER_OFF, - POWER_ON, - STATUS_PLAY, - STATUS_STILL, - STATUS_STOP, ) from pycec.network import HDMINetwork, PhysicalAddress from pycec.tcp import TcpAdapter @@ -35,12 +30,6 @@ from homeassistant.const import ( CONF_PLATFORM, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, - STATE_IDLE, - STATE_OFF, - STATE_ON, - STATE_PAUSED, - STATE_PLAYING, - STATE_UNAVAILABLE, ) from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.helpers import discovery, event @@ -382,7 +371,6 @@ class CecEntity(Entity): def __init__(self, device, logical) -> None: """Initialize the device.""" self._device = device - self._state: str | None = None self._logical_address = logical self.entity_id = "%s.%d" % (DOMAIN, self._logical_address) self._set_attr_name() @@ -405,27 +393,9 @@ class CecEntity(Entity): self._attr_name = f"{self._device.type_name} {self._logical_address} ({self._device.osd_name})" def _hdmi_cec_unavailable(self, callback_event): - # Change state to unavailable. Without this, entity would remain in - # its last state, since the state changes are pushed. - self._state = STATE_UNAVAILABLE + self._attr_available = False self.schedule_update_ha_state(False) - def update(self): - """Update device status.""" - device = self._device - if device.power_status in [POWER_OFF, 3]: - self._state = STATE_OFF - elif device.status == STATUS_PLAY: - self._state = STATE_PLAYING - elif device.status == STATUS_STOP: - self._state = STATE_IDLE - elif device.status == STATUS_STILL: - self._state = STATE_PAUSED - elif device.power_status in [POWER_ON, 4]: - self._state = STATE_ON - else: - _LOGGER.warning("Unknown state: %d", device.power_status) - async def async_added_to_hass(self): """Register HDMI callbacks after initialization.""" self._device.set_update_callback(self._update) @@ -435,6 +405,7 @@ class CecEntity(Entity): def _update(self, device=None): """Device status changed, schedule an update.""" + self._attr_available = True self.schedule_update_ha_state(True) @property diff --git a/homeassistant/components/hdmi_cec/media_player.py b/homeassistant/components/hdmi_cec/media_player.py index 203ce85a6b2..cfe73ff7c40 100644 --- a/homeassistant/components/hdmi_cec/media_player.py +++ b/homeassistant/components/hdmi_cec/media_player.py @@ -89,7 +89,7 @@ class CecPlayerEntity(CecEntity, MediaPlayerEntity): def turn_on(self) -> None: """Turn device on.""" self._device.turn_on() - self._state = MediaPlayerState.ON + self._attr_state = MediaPlayerState.ON def clear_playlist(self) -> None: """Clear players playlist.""" @@ -98,12 +98,12 @@ class CecPlayerEntity(CecEntity, MediaPlayerEntity): def turn_off(self) -> None: """Turn device off.""" self._device.turn_off() - self._state = MediaPlayerState.OFF + self._attr_state = MediaPlayerState.OFF def media_stop(self) -> None: """Stop playback.""" self.send_keypress(KEY_STOP) - self._state = MediaPlayerState.IDLE + self._attr_state = MediaPlayerState.IDLE def play_media(self, media_type: str, media_id: str, **kwargs: Any) -> None: """Not supported.""" @@ -124,7 +124,7 @@ class CecPlayerEntity(CecEntity, MediaPlayerEntity): def media_pause(self) -> None: """Pause playback.""" self.send_keypress(KEY_PAUSE) - self._state = MediaPlayerState.PAUSED + self._attr_state = MediaPlayerState.PAUSED def select_source(self, source: str) -> None: """Not supported.""" @@ -133,7 +133,7 @@ class CecPlayerEntity(CecEntity, MediaPlayerEntity): def media_play(self) -> None: """Start playback.""" self.send_keypress(KEY_PLAY) - self._state = MediaPlayerState.PLAYING + self._attr_state = MediaPlayerState.PLAYING def volume_up(self) -> None: """Increase volume.""" @@ -145,25 +145,20 @@ class CecPlayerEntity(CecEntity, MediaPlayerEntity): _LOGGER.debug("%s: volume down", self._logical_address) self.send_keypress(KEY_VOLUME_DOWN) - @property - def state(self) -> str | None: - """Cache state of device.""" - return self._state - def update(self) -> None: """Update device status.""" device = self._device if device.power_status in [POWER_OFF, 3]: - self._state = MediaPlayerState.OFF + self._attr_state = MediaPlayerState.OFF elif not self.support_pause: if device.power_status in [POWER_ON, 4]: - self._state = MediaPlayerState.ON + self._attr_state = MediaPlayerState.ON elif device.status == STATUS_PLAY: - self._state = MediaPlayerState.PLAYING + self._attr_state = MediaPlayerState.PLAYING elif device.status == STATUS_STOP: - self._state = MediaPlayerState.IDLE + self._attr_state = MediaPlayerState.IDLE elif device.status == STATUS_STILL: - self._state = MediaPlayerState.PAUSED + self._attr_state = MediaPlayerState.PAUSED else: _LOGGER.warning("Unknown state: %s", device.status) diff --git a/homeassistant/components/hdmi_cec/switch.py b/homeassistant/components/hdmi_cec/switch.py index b44e5ce5c64..a554594a219 100644 --- a/homeassistant/components/hdmi_cec/switch.py +++ b/homeassistant/components/hdmi_cec/switch.py @@ -4,8 +4,9 @@ from __future__ import annotations import logging from typing import Any +from pycec.const import POWER_OFF, POWER_ON + from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN, SwitchEntity -from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -44,16 +45,21 @@ class CecSwitchEntity(CecEntity, SwitchEntity): def turn_on(self, **kwargs: Any) -> None: """Turn device on.""" self._device.turn_on() - self._state = STATE_ON + self._attr_is_on = True self.schedule_update_ha_state(force_refresh=False) def turn_off(self, **kwargs: Any) -> None: """Turn device off.""" self._device.turn_off() - self._state = STATE_OFF + self._attr_is_on = False self.schedule_update_ha_state(force_refresh=False) - @property - def is_on(self) -> bool: - """Return True if entity is on.""" - return self._state == STATE_ON + def update(self) -> None: + """Update device status.""" + device = self._device + if device.power_status in {POWER_OFF, 3}: + self._attr_is_on = False + elif device.power_status in {POWER_ON, 4}: + self._attr_is_on = True + else: + _LOGGER.warning("Unknown state: %d", device.power_status) diff --git a/tests/components/hdmi_cec/test_switch.py b/tests/components/hdmi_cec/test_switch.py index 61e1e03e4a6..999037967dd 100644 --- a/tests/components/hdmi_cec/test_switch.py +++ b/tests/components/hdmi_cec/test_switch.py @@ -1,15 +1,9 @@ """Tests for the HDMI-CEC switch platform.""" +from pycec.const import POWER_OFF, POWER_ON, STATUS_PLAY, STATUS_STILL, STATUS_STOP +from pycec.network import PhysicalAddress import pytest -from homeassistant.components.hdmi_cec import ( - EVENT_HDMI_CEC_UNAVAILABLE, - POWER_OFF, - POWER_ON, - STATUS_PLAY, - STATUS_STILL, - STATUS_STOP, - PhysicalAddress, -) +from homeassistant.components.hdmi_cec import EVENT_HDMI_CEC_UNAVAILABLE from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.const import ( ATTR_ENTITY_ID, @@ -20,7 +14,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, ) -from tests.components.hdmi_cec import MockHDMIDevice +from . import MockHDMIDevice @pytest.mark.parametrize("config", [{}, {"platform": "switch"}]) @@ -236,9 +230,6 @@ async def test_icon( assert state.attributes["icon"] == expected_icon -@pytest.mark.xfail( - reason="The code only sets the state to unavailable, doesn't set the `_attr_available` to false." -) async def test_unavailable_status(hass, create_hdmi_network, create_cec_entity): """Test entity goes into unavailable status when expected.""" hdmi_network = await create_hdmi_network() From 4dba2a85db568a3c8a9a5e1783689a60465415c2 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 15 Sep 2022 17:57:48 +0200 Subject: [PATCH 451/955] Improve type hints in trace (#78441) --- homeassistant/components/trace/__init__.py | 63 +++++++++++++--------- 1 file changed, 39 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/trace/__init__.py b/homeassistant/components/trace/__init__.py index bb4f046a7e2..3d7510b57b2 100644 --- a/homeassistant/components/trace/__init__.py +++ b/homeassistant/components/trace/__init__.py @@ -1,12 +1,14 @@ """Support for script and automation tracing and debugging.""" from __future__ import annotations +from collections.abc import Mapping import logging +from typing import Any import voluptuous as vol from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.core import HomeAssistant +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.json import ExtendedJSONEncoder @@ -21,7 +23,7 @@ from .const import ( DATA_TRACES_RESTORED, DEFAULT_STORED_TRACES, ) -from .models import ActionTrace, BaseTrace, RestoredTrace # noqa: F401 +from .models import ActionTrace, BaseTrace, RestoredTrace from .utils import LimitedSizeDict _LOGGER = logging.getLogger(__name__) @@ -35,6 +37,13 @@ TRACE_CONFIG_SCHEMA = { vol.Optional(CONF_STORED_TRACES, default=DEFAULT_STORED_TRACES): cv.positive_int } +TraceData = dict[str, LimitedSizeDict[str, BaseTrace]] + + +@callback +def _get_data(hass: HomeAssistant) -> TraceData: + return hass.data[DATA_TRACE] + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Initialize the trace integration.""" @@ -45,15 +54,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ) hass.data[DATA_TRACE_STORE] = store - async def _async_store_traces_at_stop(*_) -> None: + async def _async_store_traces_at_stop(_: Event) -> None: """Save traces to storage.""" _LOGGER.debug("Storing traces") try: await store.async_save( - { - key: list(traces.values()) - for key, traces in hass.data[DATA_TRACE].items() - } + {key: list(traces.values()) for key, traces in _get_data(hass).items()} ) except HomeAssistantError as exc: _LOGGER.error("Error storing traces", exc_info=exc) @@ -64,25 +70,30 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -async def async_get_trace(hass, key, run_id): +async def async_get_trace( + hass: HomeAssistant, key: str, run_id: str +) -> dict[str, BaseTrace]: """Return the requested trace.""" # Restore saved traces if not done await async_restore_traces(hass) - return hass.data[DATA_TRACE][key][run_id].as_extended_dict() + return _get_data(hass)[key][run_id].as_extended_dict() -async def async_list_contexts(hass, key): +async def async_list_contexts( + hass: HomeAssistant, key: str | None +) -> dict[str, dict[str, str]]: """List contexts for which we have traces.""" # Restore saved traces if not done await async_restore_traces(hass) + values: Mapping[str, LimitedSizeDict[str, BaseTrace] | None] if key is not None: - values = {key: hass.data[DATA_TRACE].get(key, {})} + values = {key: _get_data(hass).get(key)} else: - values = hass.data[DATA_TRACE] + values = _get_data(hass) - def _trace_id(run_id, key) -> dict: + def _trace_id(run_id: str, key: str) -> dict[str, str]: """Make trace_id for the response.""" domain, item_id = key.split(".", 1) return {"run_id": run_id, "domain": domain, "item_id": item_id} @@ -90,28 +101,32 @@ async def async_list_contexts(hass, key): return { trace.context.id: _trace_id(trace.run_id, key) for key, traces in values.items() + if traces is not None for trace in traces.values() } -def _get_debug_traces(hass, key): +def _get_debug_traces(hass: HomeAssistant, key: str) -> list[dict[str, Any]]: """Return a serializable list of debug traces for a script or automation.""" - traces = [] + traces: list[dict[str, Any]] = [] - for trace in hass.data[DATA_TRACE].get(key, {}).values(): - traces.append(trace.as_short_dict()) + if traces_for_key := _get_data(hass).get(key): + for trace in traces_for_key.values(): + traces.append(trace.as_short_dict()) return traces -async def async_list_traces(hass, wanted_domain, wanted_key): +async def async_list_traces( + hass: HomeAssistant, wanted_domain: str, wanted_key: str | None +) -> list[dict[str, Any]]: """List traces for a domain.""" # Restore saved traces if not done already await async_restore_traces(hass) if not wanted_key: - traces = [] - for key in hass.data[DATA_TRACE]: + traces: list[dict[str, Any]] = [] + for key in _get_data(hass): domain = key.split(".", 1)[0] if domain == wanted_domain: traces.extend(_get_debug_traces(hass, key)) @@ -126,7 +141,7 @@ def async_store_trace( ) -> None: """Store a trace if its key is valid.""" if key := trace.key: - traces = hass.data[DATA_TRACE] + traces = _get_data(hass) if key not in traces: traces[key] = LimitedSizeDict(size_limit=stored_traces) else: @@ -137,7 +152,7 @@ def async_store_trace( def _async_store_restored_trace(hass: HomeAssistant, trace: RestoredTrace) -> None: """Store a restored trace and move it to the end of the LimitedSizeDict.""" key = trace.key - traces = hass.data[DATA_TRACE] + traces = _get_data(hass) if key not in traces: traces[key] = LimitedSizeDict() traces[key][trace.run_id] = trace @@ -151,7 +166,7 @@ async def async_restore_traces(hass: HomeAssistant) -> None: hass.data[DATA_TRACES_RESTORED] = True - store = hass.data[DATA_TRACE_STORE] + store: Store[dict[str, list]] = hass.data[DATA_TRACE_STORE] try: restored_traces = await store.async_load() or {} except HomeAssistantError: @@ -162,7 +177,7 @@ async def async_restore_traces(hass: HomeAssistant) -> None: # Add stored traces in reversed order to priorize the newest traces for json_trace in reversed(traces): if ( - (stored_traces := hass.data[DATA_TRACE].get(key)) + (stored_traces := _get_data(hass).get(key)) and stored_traces.size_limit is not None and len(stored_traces) >= stored_traces.size_limit ): From dd20a7ea62fc003748c5f0cf99be25c69c9b5a05 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 15 Sep 2022 18:01:24 +0200 Subject: [PATCH 452/955] Display statistics in the source's unit (#78031) --- homeassistant/components/demo/__init__.py | 5 + .../components/recorder/db_schema.py | 3 +- .../components/recorder/migration.py | 19 + homeassistant/components/recorder/models.py | 1 + .../components/recorder/statistics.py | 149 +++-- .../components/recorder/websocket_api.py | 6 +- homeassistant/components/sensor/recorder.py | 46 +- homeassistant/components/tibber/sensor.py | 1 + tests/components/recorder/db_schema_29.py | 616 ++++++++++++++++++ tests/components/recorder/test_migrate.py | 117 +++- tests/components/recorder/test_statistics.py | 15 +- .../components/recorder/test_websocket_api.py | 62 +- tests/components/sensor/test_recorder.py | 60 +- 13 files changed, 964 insertions(+), 136 deletions(-) create mode 100644 tests/components/recorder/db_schema_29.py diff --git a/homeassistant/components/demo/__init__.py b/homeassistant/components/demo/__init__.py index 7ed989903e5..4d0ef03c564 100644 --- a/homeassistant/components/demo/__init__.py +++ b/homeassistant/components/demo/__init__.py @@ -295,6 +295,7 @@ async def _insert_statistics(hass: HomeAssistant) -> None: metadata: StatisticMetaData = { "source": DOMAIN, "name": "Outdoor temperature", + "state_unit_of_measurement": TEMP_CELSIUS, "statistic_id": f"{DOMAIN}:temperature_outdoor", "unit_of_measurement": TEMP_CELSIUS, "has_mean": True, @@ -308,6 +309,7 @@ async def _insert_statistics(hass: HomeAssistant) -> None: metadata = { "source": DOMAIN, "name": "Energy consumption 1", + "state_unit_of_measurement": ENERGY_KILO_WATT_HOUR, "statistic_id": f"{DOMAIN}:energy_consumption_kwh", "unit_of_measurement": ENERGY_KILO_WATT_HOUR, "has_mean": False, @@ -320,6 +322,7 @@ async def _insert_statistics(hass: HomeAssistant) -> None: metadata = { "source": DOMAIN, "name": "Energy consumption 2", + "state_unit_of_measurement": ENERGY_MEGA_WATT_HOUR, "statistic_id": f"{DOMAIN}:energy_consumption_mwh", "unit_of_measurement": ENERGY_MEGA_WATT_HOUR, "has_mean": False, @@ -334,6 +337,7 @@ async def _insert_statistics(hass: HomeAssistant) -> None: metadata = { "source": DOMAIN, "name": "Gas consumption 1", + "state_unit_of_measurement": VOLUME_CUBIC_METERS, "statistic_id": f"{DOMAIN}:gas_consumption_m3", "unit_of_measurement": VOLUME_CUBIC_METERS, "has_mean": False, @@ -348,6 +352,7 @@ async def _insert_statistics(hass: HomeAssistant) -> None: metadata = { "source": DOMAIN, "name": "Gas consumption 2", + "state_unit_of_measurement": VOLUME_CUBIC_FEET, "statistic_id": f"{DOMAIN}:gas_consumption_ft3", "unit_of_measurement": VOLUME_CUBIC_FEET, "has_mean": False, diff --git a/homeassistant/components/recorder/db_schema.py b/homeassistant/components/recorder/db_schema.py index 40c0453ea0b..363604d525b 100644 --- a/homeassistant/components/recorder/db_schema.py +++ b/homeassistant/components/recorder/db_schema.py @@ -53,7 +53,7 @@ from .models import StatisticData, StatisticMetaData, process_timestamp # pylint: disable=invalid-name Base = declarative_base() -SCHEMA_VERSION = 29 +SCHEMA_VERSION = 30 _StatisticsBaseSelfT = TypeVar("_StatisticsBaseSelfT", bound="StatisticsBase") @@ -494,6 +494,7 @@ class StatisticsMeta(Base): # type: ignore[misc,valid-type] id = Column(Integer, Identity(), primary_key=True) statistic_id = Column(String(255), index=True, unique=True) source = Column(String(32)) + state_unit_of_measurement = Column(String(255)) unit_of_measurement = Column(String(255)) has_mean = Column(Boolean) has_sum = Column(Boolean) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index ab9b93de5e5..e2169727382 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -747,6 +747,25 @@ def _apply_update( # noqa: C901 _create_index( session_maker, "statistics_meta", "ix_statistics_meta_statistic_id" ) + elif new_version == 30: + _add_columns( + session_maker, + "statistics_meta", + ["state_unit_of_measurement VARCHAR(255)"], + ) + # When querying the database, be careful to only explicitly query for columns + # which were present in schema version 30. If querying the table, SQLAlchemy + # will refer to future columns. + with session_scope(session=session_maker()) as session: + for statistics_meta in session.query( + StatisticsMeta.id, StatisticsMeta.unit_of_measurement + ): + session.query(StatisticsMeta).filter_by(id=statistics_meta.id).update( + { + StatisticsMeta.state_unit_of_measurement: statistics_meta.unit_of_measurement, + }, + synchronize_session=False, + ) else: raise ValueError(f"No schema migration defined for version {new_version}") diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index 2004c3ec30d..78ebaabc0fd 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -64,6 +64,7 @@ class StatisticMetaData(TypedDict): has_sum: bool name: str | None source: str + state_unit_of_measurement: str | None statistic_id: str unit_of_measurement: str | None diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index a1ab58ee011..8585ca37fac 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -12,7 +12,7 @@ import logging import os import re from statistics import mean -from typing import TYPE_CHECKING, Any, Literal, overload +from typing import TYPE_CHECKING, Any, Literal from sqlalchemy import bindparam, func, lambda_stmt, select from sqlalchemy.engine.row import Row @@ -24,6 +24,9 @@ from sqlalchemy.sql.selectable import Subquery import voluptuous as vol from homeassistant.const import ( + ENERGY_KILO_WATT_HOUR, + POWER_KILO_WATT, + POWER_WATT, PRESSURE_PA, TEMP_CELSIUS, VOLUME_CUBIC_FEET, @@ -115,6 +118,7 @@ QUERY_STATISTIC_META = [ StatisticsMeta.id, StatisticsMeta.statistic_id, StatisticsMeta.source, + StatisticsMeta.state_unit_of_measurement, StatisticsMeta.unit_of_measurement, StatisticsMeta.has_mean, StatisticsMeta.has_sum, @@ -127,24 +131,49 @@ QUERY_STATISTIC_META_ID = [ ] -# Convert pressure, temperature and volume statistics from the normalized unit used for -# statistics to the unit configured by the user -STATISTIC_UNIT_TO_DISPLAY_UNIT_CONVERSIONS = { - PRESSURE_PA: lambda x, units: pressure_util.convert( - x, PRESSURE_PA, units.pressure_unit - ) - if x is not None - else None, - TEMP_CELSIUS: lambda x, units: temperature_util.convert( - x, TEMP_CELSIUS, units.temperature_unit - ) - if x is not None - else None, - VOLUME_CUBIC_METERS: lambda x, units: volume_util.convert( - x, VOLUME_CUBIC_METERS, _configured_unit(VOLUME_CUBIC_METERS, units) - ) - if x is not None - else None, +def _convert_power(value: float | None, state_unit: str, _: UnitSystem) -> float | None: + """Convert power in W to to_unit.""" + if value is None: + return None + if state_unit == POWER_KILO_WATT: + return value / 1000 + return value + + +def _convert_pressure( + value: float | None, state_unit: str, _: UnitSystem +) -> float | None: + """Convert pressure in Pa to to_unit.""" + if value is None: + return None + return pressure_util.convert(value, PRESSURE_PA, state_unit) + + +def _convert_temperature( + value: float | None, state_unit: str, _: UnitSystem +) -> float | None: + """Convert temperature in °C to to_unit.""" + if value is None: + return None + return temperature_util.convert(value, TEMP_CELSIUS, state_unit) + + +def _convert_volume(value: float | None, _: str, units: UnitSystem) -> float | None: + """Convert volume in m³ to ft³ or m³.""" + if value is None: + return None + return volume_util.convert(value, VOLUME_CUBIC_METERS, _volume_unit(units)) + + +# Convert power, pressure, temperature and volume statistics from the normalized unit +# used for statistics to the unit configured by the user +STATISTIC_UNIT_TO_DISPLAY_UNIT_CONVERSIONS: dict[ + str, Callable[[float | None, str, UnitSystem], float | None] +] = { + POWER_WATT: _convert_power, + PRESSURE_PA: _convert_pressure, + TEMP_CELSIUS: _convert_temperature, + VOLUME_CUBIC_METERS: _convert_volume, } # Convert volume statistics from the display unit configured by the user @@ -154,7 +183,7 @@ DISPLAY_UNIT_TO_STATISTIC_UNIT_CONVERSIONS: dict[ str, Callable[[float, UnitSystem], float] ] = { VOLUME_CUBIC_FEET: lambda x, units: volume_util.convert( - x, _configured_unit(VOLUME_CUBIC_METERS, units), VOLUME_CUBIC_METERS + x, _volume_unit(units), VOLUME_CUBIC_METERS ), } @@ -268,6 +297,8 @@ def _update_or_add_metadata( old_metadata["has_mean"] != new_metadata["has_mean"] or old_metadata["has_sum"] != new_metadata["has_sum"] or old_metadata["name"] != new_metadata["name"] + or old_metadata["state_unit_of_measurement"] + != new_metadata["state_unit_of_measurement"] or old_metadata["unit_of_measurement"] != new_metadata["unit_of_measurement"] ): session.query(StatisticsMeta).filter_by(statistic_id=statistic_id).update( @@ -275,6 +306,9 @@ def _update_or_add_metadata( StatisticsMeta.has_mean: new_metadata["has_mean"], StatisticsMeta.has_sum: new_metadata["has_sum"], StatisticsMeta.name: new_metadata["name"], + StatisticsMeta.state_unit_of_measurement: new_metadata[ + "state_unit_of_measurement" + ], StatisticsMeta.unit_of_measurement: new_metadata["unit_of_measurement"], }, synchronize_session=False, @@ -737,12 +771,13 @@ def get_metadata_with_session( meta["statistic_id"]: ( meta["id"], { - "source": meta["source"], - "statistic_id": meta["statistic_id"], - "unit_of_measurement": meta["unit_of_measurement"], "has_mean": meta["has_mean"], "has_sum": meta["has_sum"], "name": meta["name"], + "source": meta["source"], + "state_unit_of_measurement": meta["state_unit_of_measurement"], + "statistic_id": meta["statistic_id"], + "unit_of_measurement": meta["unit_of_measurement"], }, ) for meta in result @@ -767,27 +802,26 @@ def get_metadata( ) -@overload -def _configured_unit(unit: None, units: UnitSystem) -> None: - ... +def _volume_unit(units: UnitSystem) -> str: + """Return the preferred volume unit according to unit system.""" + if units.is_metric: + return VOLUME_CUBIC_METERS + return VOLUME_CUBIC_FEET -@overload -def _configured_unit(unit: str, units: UnitSystem) -> str: - ... +def _configured_unit( + unit: str | None, state_unit: str | None, units: UnitSystem +) -> str | None: + """Return the pressure and temperature units configured by the user. - -def _configured_unit(unit: str | None, units: UnitSystem) -> str | None: - """Return the pressure and temperature units configured by the user.""" - if unit == PRESSURE_PA: - return units.pressure_unit - if unit == TEMP_CELSIUS: - return units.temperature_unit + Energy and volume is normalized for the energy dashboard. + For other units, display in the unit of the source. + """ + if unit == ENERGY_KILO_WATT_HOUR: + return ENERGY_KILO_WATT_HOUR if unit == VOLUME_CUBIC_METERS: - if units.is_metric: - return VOLUME_CUBIC_METERS - return VOLUME_CUBIC_FEET - return unit + return _volume_unit(units) + return state_unit def clear_statistics(instance: Recorder, statistic_ids: list[str]) -> None: @@ -834,10 +868,10 @@ def list_statistic_ids( """ result = {} - def _display_unit(hass: HomeAssistant, unit: str | None) -> str | None: - if unit is None: - return None - return _configured_unit(unit, hass.config.units) + def _display_unit( + hass: HomeAssistant, statistic_unit: str | None, state_unit: str | None + ) -> str | None: + return _configured_unit(statistic_unit, state_unit, hass.config.units) # Query the database with session_scope(hass=hass) as session: @@ -852,7 +886,7 @@ def list_statistic_ids( "name": meta["name"], "source": meta["source"], "display_unit_of_measurement": _display_unit( - hass, meta["unit_of_measurement"] + hass, meta["unit_of_measurement"], meta["state_unit_of_measurement"] ), "unit_of_measurement": meta["unit_of_measurement"], } @@ -876,7 +910,7 @@ def list_statistic_ids( "name": meta["name"], "source": meta["source"], "display_unit_of_measurement": _display_unit( - hass, meta["unit_of_measurement"] + hass, meta["unit_of_measurement"], meta["state_unit_of_measurement"] ), "unit_of_measurement": meta["unit_of_measurement"], } @@ -1295,7 +1329,7 @@ def _sorted_statistics_to_dict( need_stat_at_start_time: set[int] = set() stats_at_start_time = {} - def no_conversion(val: Any, _: Any) -> float | None: + def no_conversion(val: Any, _unit: str | None, _units: Any) -> float | None: """Return x.""" return val # type: ignore[no-any-return] @@ -1321,10 +1355,13 @@ def _sorted_statistics_to_dict( # Append all statistic entries, and optionally do unit conversion for meta_id, group in groupby(stats, lambda stat: stat.metadata_id): # type: ignore[no-any-return] unit = metadata[meta_id]["unit_of_measurement"] + state_unit = metadata[meta_id]["state_unit_of_measurement"] statistic_id = metadata[meta_id]["statistic_id"] - convert: Callable[[Any, Any], float | None] - if convert_units: - convert = STATISTIC_UNIT_TO_DISPLAY_UNIT_CONVERSIONS.get(unit, lambda x, units: x) # type: ignore[arg-type,no-any-return] + convert: Callable[[Any, Any, Any], float | None] + if unit is not None and convert_units: + convert = STATISTIC_UNIT_TO_DISPLAY_UNIT_CONVERSIONS.get( + unit, no_conversion + ) else: convert = no_conversion ent_results = result[meta_id] @@ -1336,14 +1373,14 @@ def _sorted_statistics_to_dict( "statistic_id": statistic_id, "start": start if start_time_as_datetime else start.isoformat(), "end": end.isoformat(), - "mean": convert(db_state.mean, units), - "min": convert(db_state.min, units), - "max": convert(db_state.max, units), + "mean": convert(db_state.mean, state_unit, units), + "min": convert(db_state.min, state_unit, units), + "max": convert(db_state.max, state_unit, units), "last_reset": process_timestamp_to_utc_isoformat( db_state.last_reset ), - "state": convert(db_state.state, units), - "sum": convert(db_state.sum, units), + "state": convert(db_state.state, state_unit, units), + "sum": convert(db_state.sum, state_unit, units), } ) @@ -1531,7 +1568,7 @@ def adjust_statistics( units = instance.hass.config.units statistic_unit = metadata[statistic_id][1]["unit_of_measurement"] - display_unit = _configured_unit(statistic_unit, units) + display_unit = _configured_unit(statistic_unit, None, units) convert = DISPLAY_UNIT_TO_STATISTIC_UNIT_CONVERSIONS.get(display_unit, lambda x, units: x) # type: ignore[arg-type] sum_adjustment = convert(sum_adjustment, units) diff --git a/homeassistant/components/recorder/websocket_api.py b/homeassistant/components/recorder/websocket_api.py index 70552bca67e..c625620f4c0 100644 --- a/homeassistant/components/recorder/websocket_api.py +++ b/homeassistant/components/recorder/websocket_api.py @@ -219,7 +219,10 @@ async def ws_get_statistics_metadata( def ws_update_statistics_metadata( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict ) -> None: - """Update statistics metadata for a statistic_id.""" + """Update statistics metadata for a statistic_id. + + Only the normalized unit of measurement can be updated. + """ get_instance(hass).async_update_statistics_metadata( msg["statistic_id"], new_unit_of_measurement=msg["unit_of_measurement"] ) @@ -286,6 +289,7 @@ def ws_import_statistics( """Adjust sum statistics.""" metadata = msg["metadata"] stats = msg["stats"] + metadata["state_unit_of_measurement"] = metadata["unit_of_measurement"] if valid_entity_id(metadata["statistic_id"]): async_import_statistics(hass, metadata, stats) diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index b2542d98738..dfbcc67f80d 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -87,7 +87,7 @@ UNIT_CONVERSIONS: dict[str, dict[str, Callable]] = { ENERGY_MEGA_WATT_HOUR: lambda x: x * 1000, ENERGY_WATT_HOUR: lambda x: x / 1000, }, - # Convert power W + # Convert power to W SensorDeviceClass.POWER: { POWER_WATT: lambda x: x, POWER_KILO_WATT: lambda x: x * 1000, @@ -202,9 +202,9 @@ def _normalize_states( entity_history: Iterable[State], device_class: str | None, entity_id: str, -) -> tuple[str | None, list[tuple[float, State]]]: +) -> tuple[str | None, str | None, list[tuple[float, State]]]: """Normalize units.""" - unit = None + state_unit = None if device_class not in UNIT_CONVERSIONS: # We're not normalizing this device class, return the state as they are @@ -238,9 +238,9 @@ def _normalize_states( extra, LINK_DEV_STATISTICS, ) - return None, [] - unit = fstates[0][1].attributes.get(ATTR_UNIT_OF_MEASUREMENT) - return unit, fstates + return None, None, [] + state_unit = fstates[0][1].attributes.get(ATTR_UNIT_OF_MEASUREMENT) + return state_unit, state_unit, fstates fstates = [] @@ -249,9 +249,9 @@ def _normalize_states( fstate = _parse_float(state.state) except ValueError: continue - unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + state_unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) # Exclude unsupported units from statistics - if unit not in UNIT_CONVERSIONS[device_class]: + if state_unit not in UNIT_CONVERSIONS[device_class]: if WARN_UNSUPPORTED_UNIT not in hass.data: hass.data[WARN_UNSUPPORTED_UNIT] = set() if entity_id not in hass.data[WARN_UNSUPPORTED_UNIT]: @@ -259,14 +259,14 @@ def _normalize_states( _LOGGER.warning( "%s has unit %s which is unsupported for device_class %s", entity_id, - unit, + state_unit, device_class, ) continue - fstates.append((UNIT_CONVERSIONS[device_class][unit](fstate), state)) + fstates.append((UNIT_CONVERSIONS[device_class][state_unit](fstate), state)) - return DEVICE_CLASS_UNITS[device_class], fstates + return DEVICE_CLASS_UNITS[device_class], state_unit, fstates def _suggest_report_issue(hass: HomeAssistant, entity_id: str) -> str: @@ -455,7 +455,7 @@ def _compile_statistics( # noqa: C901 device_class = _state.attributes.get(ATTR_DEVICE_CLASS) entity_history = history_list[entity_id] - unit, fstates = _normalize_states( + normalized_unit, state_unit, fstates = _normalize_states( hass, session, old_metadatas, @@ -469,7 +469,9 @@ def _compile_statistics( # noqa: C901 state_class = _state.attributes[ATTR_STATE_CLASS] - to_process.append((entity_id, unit, state_class, fstates)) + to_process.append( + (entity_id, normalized_unit, state_unit, state_class, fstates) + ) if "sum" in wanted_statistics[entity_id]: to_query.append(entity_id) @@ -478,13 +480,14 @@ def _compile_statistics( # noqa: C901 ) for ( # pylint: disable=too-many-nested-blocks entity_id, - unit, + normalized_unit, + state_unit, state_class, fstates, ) in to_process: # Check metadata if old_metadata := old_metadatas.get(entity_id): - if old_metadata[1]["unit_of_measurement"] != unit: + if old_metadata[1]["unit_of_measurement"] != normalized_unit: if WARN_UNSTABLE_UNIT not in hass.data: hass.data[WARN_UNSTABLE_UNIT] = set() if entity_id not in hass.data[WARN_UNSTABLE_UNIT]: @@ -496,7 +499,7 @@ def _compile_statistics( # noqa: C901 "Go to %s to fix this", "normalized " if device_class in DEVICE_CLASS_UNITS else "", entity_id, - unit, + normalized_unit, old_metadata[1]["unit_of_measurement"], old_metadata[1]["unit_of_measurement"], LINK_DEV_STATISTICS, @@ -509,8 +512,9 @@ def _compile_statistics( # noqa: C901 "has_sum": "sum" in wanted_statistics[entity_id], "name": None, "source": RECORDER_DOMAIN, + "state_unit_of_measurement": state_unit, "statistic_id": entity_id, - "unit_of_measurement": unit, + "unit_of_measurement": normalized_unit, } # Make calculations @@ -627,7 +631,7 @@ def list_statistic_ids( for state in entities: state_class = state.attributes[ATTR_STATE_CLASS] device_class = state.attributes.get(ATTR_DEVICE_CLASS) - native_unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + state_unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) provided_statistics = DEFAULT_STATISTICS[state_class] if statistic_type is not None and statistic_type not in provided_statistics: @@ -649,12 +653,13 @@ def list_statistic_ids( "has_sum": "sum" in provided_statistics, "name": None, "source": RECORDER_DOMAIN, + "state_unit_of_measurement": state_unit, "statistic_id": state.entity_id, - "unit_of_measurement": native_unit, + "unit_of_measurement": state_unit, } continue - if native_unit not in UNIT_CONVERSIONS[device_class]: + if state_unit not in UNIT_CONVERSIONS[device_class]: continue statistics_unit = DEVICE_CLASS_UNITS[device_class] @@ -663,6 +668,7 @@ def list_statistic_ids( "has_sum": "sum" in provided_statistics, "name": None, "source": RECORDER_DOMAIN, + "state_unit_of_measurement": state_unit, "statistic_id": state.entity_id, "unit_of_measurement": statistics_unit, } diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index ca0c253590f..93fdba107ed 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -642,6 +642,7 @@ class TibberDataCoordinator(DataUpdateCoordinator): has_sum=True, name=f"{home.name} {sensor_type}", source=TIBBER_DOMAIN, + state_unit_of_measurement=unit, statistic_id=statistic_id, unit_of_measurement=unit, ) diff --git a/tests/components/recorder/db_schema_29.py b/tests/components/recorder/db_schema_29.py new file mode 100644 index 00000000000..54aa4b2b13c --- /dev/null +++ b/tests/components/recorder/db_schema_29.py @@ -0,0 +1,616 @@ +"""Models for SQLAlchemy. + +This file contains the model definitions for schema version 28. +It is used to test the schema migration logic. +""" +from __future__ import annotations + +from collections.abc import Callable +from datetime import datetime, timedelta +import logging +from typing import Any, TypeVar, cast + +import ciso8601 +from fnvhash import fnv1a_32 +from sqlalchemy import ( + JSON, + BigInteger, + Boolean, + Column, + DateTime, + Float, + ForeignKey, + Identity, + Index, + Integer, + SmallInteger, + String, + Text, + distinct, + type_coerce, +) +from sqlalchemy.dialects import mysql, oracle, postgresql, sqlite +from sqlalchemy.ext.declarative import declared_attr +from sqlalchemy.orm import aliased, declarative_base, relationship +from sqlalchemy.orm.session import Session + +from homeassistant.components.recorder.const import ALL_DOMAIN_EXCLUDE_ATTRS +from homeassistant.components.recorder.models import ( + StatisticData, + StatisticMetaData, + process_timestamp, +) +from homeassistant.const import ( + MAX_LENGTH_EVENT_CONTEXT_ID, + MAX_LENGTH_EVENT_EVENT_TYPE, + MAX_LENGTH_EVENT_ORIGIN, + MAX_LENGTH_STATE_ENTITY_ID, + MAX_LENGTH_STATE_STATE, +) +from homeassistant.core import Context, Event, EventOrigin, State, split_entity_id +from homeassistant.helpers.json import ( + JSON_DECODE_EXCEPTIONS, + JSON_DUMP, + json_bytes, + json_loads, +) +import homeassistant.util.dt as dt_util + +# SQLAlchemy Schema +# pylint: disable=invalid-name +Base = declarative_base() + +SCHEMA_VERSION = 29 + +_StatisticsBaseSelfT = TypeVar("_StatisticsBaseSelfT", bound="StatisticsBase") + +_LOGGER = logging.getLogger(__name__) + +TABLE_EVENTS = "events" +TABLE_EVENT_DATA = "event_data" +TABLE_STATES = "states" +TABLE_STATE_ATTRIBUTES = "state_attributes" +TABLE_RECORDER_RUNS = "recorder_runs" +TABLE_SCHEMA_CHANGES = "schema_changes" +TABLE_STATISTICS = "statistics" +TABLE_STATISTICS_META = "statistics_meta" +TABLE_STATISTICS_RUNS = "statistics_runs" +TABLE_STATISTICS_SHORT_TERM = "statistics_short_term" + +ALL_TABLES = [ + TABLE_STATES, + TABLE_STATE_ATTRIBUTES, + TABLE_EVENTS, + TABLE_EVENT_DATA, + TABLE_RECORDER_RUNS, + TABLE_SCHEMA_CHANGES, + TABLE_STATISTICS, + TABLE_STATISTICS_META, + TABLE_STATISTICS_RUNS, + TABLE_STATISTICS_SHORT_TERM, +] + +TABLES_TO_CHECK = [ + TABLE_STATES, + TABLE_EVENTS, + TABLE_RECORDER_RUNS, + TABLE_SCHEMA_CHANGES, +] + +LAST_UPDATED_INDEX = "ix_states_last_updated" +ENTITY_ID_LAST_UPDATED_INDEX = "ix_states_entity_id_last_updated" +EVENTS_CONTEXT_ID_INDEX = "ix_events_context_id" +STATES_CONTEXT_ID_INDEX = "ix_states_context_id" + + +class FAST_PYSQLITE_DATETIME(sqlite.DATETIME): # type: ignore[misc] + """Use ciso8601 to parse datetimes instead of sqlalchemy built-in regex.""" + + def result_processor(self, dialect, coltype): # type: ignore[no-untyped-def] + """Offload the datetime parsing to ciso8601.""" + return lambda value: None if value is None else ciso8601.parse_datetime(value) + + +JSON_VARIENT_CAST = Text().with_variant( + postgresql.JSON(none_as_null=True), "postgresql" +) +JSONB_VARIENT_CAST = Text().with_variant( + postgresql.JSONB(none_as_null=True), "postgresql" +) +DATETIME_TYPE = ( + DateTime(timezone=True) + .with_variant(mysql.DATETIME(timezone=True, fsp=6), "mysql") + .with_variant(FAST_PYSQLITE_DATETIME(), "sqlite") +) +DOUBLE_TYPE = ( + Float() + .with_variant(mysql.DOUBLE(asdecimal=False), "mysql") + .with_variant(oracle.DOUBLE_PRECISION(), "oracle") + .with_variant(postgresql.DOUBLE_PRECISION(), "postgresql") +) + + +class JSONLiteral(JSON): # type: ignore[misc] + """Teach SA how to literalize json.""" + + def literal_processor(self, dialect: str) -> Callable[[Any], str]: + """Processor to convert a value to JSON.""" + + def process(value: Any) -> str: + """Dump json.""" + return JSON_DUMP(value) + + return process + + +EVENT_ORIGIN_ORDER = [EventOrigin.local, EventOrigin.remote] +EVENT_ORIGIN_TO_IDX = {origin: idx for idx, origin in enumerate(EVENT_ORIGIN_ORDER)} + + +class Events(Base): # type: ignore[misc,valid-type] + """Event history data.""" + + __table_args__ = ( + # Used for fetching events at a specific time + # see logbook + Index("ix_events_event_type_time_fired", "event_type", "time_fired"), + {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, + ) + __tablename__ = TABLE_EVENTS + event_id = Column(Integer, Identity(), primary_key=True) + event_type = Column(String(MAX_LENGTH_EVENT_EVENT_TYPE)) + event_data = Column(Text().with_variant(mysql.LONGTEXT, "mysql")) + origin = Column(String(MAX_LENGTH_EVENT_ORIGIN)) # no longer used for new rows + origin_idx = Column(SmallInteger) + time_fired = Column(DATETIME_TYPE, index=True) + context_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID), index=True) + context_user_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID)) + context_parent_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID)) + data_id = Column(Integer, ForeignKey("event_data.data_id"), index=True) + event_data_rel = relationship("EventData") + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + @staticmethod + def from_event(event: Event) -> Events: + """Create an event database object from a native event.""" + return Events( + event_type=event.event_type, + event_data=None, + origin_idx=EVENT_ORIGIN_TO_IDX.get(event.origin), + time_fired=event.time_fired, + context_id=event.context.id, + context_user_id=event.context.user_id, + context_parent_id=event.context.parent_id, + ) + + def to_native(self, validate_entity_id: bool = True) -> Event | None: + """Convert to a native HA Event.""" + context = Context( + id=self.context_id, + user_id=self.context_user_id, + parent_id=self.context_parent_id, + ) + try: + return Event( + self.event_type, + json_loads(self.event_data) if self.event_data else {}, + EventOrigin(self.origin) + if self.origin + else EVENT_ORIGIN_ORDER[self.origin_idx], + process_timestamp(self.time_fired), + context=context, + ) + except JSON_DECODE_EXCEPTIONS: + # When json_loads fails + _LOGGER.exception("Error converting to event: %s", self) + return None + + +class EventData(Base): # type: ignore[misc,valid-type] + """Event data history.""" + + __table_args__ = ( + {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, + ) + __tablename__ = TABLE_EVENT_DATA + data_id = Column(Integer, Identity(), primary_key=True) + hash = Column(BigInteger, index=True) + # Note that this is not named attributes to avoid confusion with the states table + shared_data = Column(Text().with_variant(mysql.LONGTEXT, "mysql")) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + @staticmethod + def from_event(event: Event) -> EventData: + """Create object from an event.""" + shared_data = json_bytes(event.data) + return EventData( + shared_data=shared_data.decode("utf-8"), + hash=EventData.hash_shared_data_bytes(shared_data), + ) + + @staticmethod + def shared_data_bytes_from_event(event: Event) -> bytes: + """Create shared_data from an event.""" + return json_bytes(event.data) + + @staticmethod + def hash_shared_data_bytes(shared_data_bytes: bytes) -> int: + """Return the hash of json encoded shared data.""" + return cast(int, fnv1a_32(shared_data_bytes)) + + def to_native(self) -> dict[str, Any]: + """Convert to an HA state object.""" + try: + return cast(dict[str, Any], json_loads(self.shared_data)) + except JSON_DECODE_EXCEPTIONS: + _LOGGER.exception("Error converting row to event data: %s", self) + return {} + + +class States(Base): # type: ignore[misc,valid-type] + """State change history.""" + + __table_args__ = ( + # Used for fetching the state of entities at a specific time + # (get_states in history.py) + Index(ENTITY_ID_LAST_UPDATED_INDEX, "entity_id", "last_updated"), + {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, + ) + __tablename__ = TABLE_STATES + state_id = Column(Integer, Identity(), primary_key=True) + entity_id = Column(String(MAX_LENGTH_STATE_ENTITY_ID)) + state = Column(String(MAX_LENGTH_STATE_STATE)) + attributes = Column( + Text().with_variant(mysql.LONGTEXT, "mysql") + ) # no longer used for new rows + event_id = Column( # no longer used for new rows + Integer, ForeignKey("events.event_id", ondelete="CASCADE"), index=True + ) + last_changed = Column(DATETIME_TYPE) + last_updated = Column(DATETIME_TYPE, default=dt_util.utcnow, index=True) + old_state_id = Column(Integer, ForeignKey("states.state_id"), index=True) + attributes_id = Column( + Integer, ForeignKey("state_attributes.attributes_id"), index=True + ) + context_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID), index=True) + context_user_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID)) + context_parent_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID)) + origin_idx = Column(SmallInteger) # 0 is local, 1 is remote + old_state = relationship("States", remote_side=[state_id]) + state_attributes = relationship("StateAttributes") + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + @staticmethod + def from_event(event: Event) -> States: + """Create object from a state_changed event.""" + entity_id = event.data["entity_id"] + state: State | None = event.data.get("new_state") + dbstate = States( + entity_id=entity_id, + attributes=None, + context_id=event.context.id, + context_user_id=event.context.user_id, + context_parent_id=event.context.parent_id, + origin_idx=EVENT_ORIGIN_TO_IDX.get(event.origin), + ) + + # None state means the state was removed from the state machine + if state is None: + dbstate.state = "" + dbstate.last_updated = event.time_fired + dbstate.last_changed = None + return dbstate + + dbstate.state = state.state + dbstate.last_updated = state.last_updated + if state.last_updated == state.last_changed: + dbstate.last_changed = None + else: + dbstate.last_changed = state.last_changed + + return dbstate + + def to_native(self, validate_entity_id: bool = True) -> State | None: + """Convert to an HA state object.""" + context = Context( + id=self.context_id, + user_id=self.context_user_id, + parent_id=self.context_parent_id, + ) + try: + attrs = json_loads(self.attributes) if self.attributes else {} + except JSON_DECODE_EXCEPTIONS: + # When json_loads fails + _LOGGER.exception("Error converting row to state: %s", self) + return None + if self.last_changed is None or self.last_changed == self.last_updated: + last_changed = last_updated = process_timestamp(self.last_updated) + else: + last_updated = process_timestamp(self.last_updated) + last_changed = process_timestamp(self.last_changed) + return State( + self.entity_id, + self.state, + # Join the state_attributes table on attributes_id to get the attributes + # for newer states + attrs, + last_changed, + last_updated, + context=context, + validate_entity_id=validate_entity_id, + ) + + +class StateAttributes(Base): # type: ignore[misc,valid-type] + """State attribute change history.""" + + __table_args__ = ( + {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, + ) + __tablename__ = TABLE_STATE_ATTRIBUTES + attributes_id = Column(Integer, Identity(), primary_key=True) + hash = Column(BigInteger, index=True) + # Note that this is not named attributes to avoid confusion with the states table + shared_attrs = Column(Text().with_variant(mysql.LONGTEXT, "mysql")) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + @staticmethod + def from_event(event: Event) -> StateAttributes: + """Create object from a state_changed event.""" + state: State | None = event.data.get("new_state") + # None state means the state was removed from the state machine + attr_bytes = b"{}" if state is None else json_bytes(state.attributes) + dbstate = StateAttributes(shared_attrs=attr_bytes.decode("utf-8")) + dbstate.hash = StateAttributes.hash_shared_attrs_bytes(attr_bytes) + return dbstate + + @staticmethod + def shared_attrs_bytes_from_event( + event: Event, exclude_attrs_by_domain: dict[str, set[str]] + ) -> bytes: + """Create shared_attrs from a state_changed event.""" + state: State | None = event.data.get("new_state") + # None state means the state was removed from the state machine + if state is None: + return b"{}" + domain = split_entity_id(state.entity_id)[0] + exclude_attrs = ( + exclude_attrs_by_domain.get(domain, set()) | ALL_DOMAIN_EXCLUDE_ATTRS + ) + return json_bytes( + {k: v for k, v in state.attributes.items() if k not in exclude_attrs} + ) + + @staticmethod + def hash_shared_attrs_bytes(shared_attrs_bytes: bytes) -> int: + """Return the hash of json encoded shared attributes.""" + return cast(int, fnv1a_32(shared_attrs_bytes)) + + def to_native(self) -> dict[str, Any]: + """Convert to an HA state object.""" + try: + return cast(dict[str, Any], json_loads(self.shared_attrs)) + except JSON_DECODE_EXCEPTIONS: + # When json_loads fails + _LOGGER.exception("Error converting row to state attributes: %s", self) + return {} + + +class StatisticsBase: + """Statistics base class.""" + + id = Column(Integer, Identity(), primary_key=True) + created = Column(DATETIME_TYPE, default=dt_util.utcnow) + + @declared_attr # type: ignore[misc] + def metadata_id(self) -> Column: + """Define the metadata_id column for sub classes.""" + return Column( + Integer, + ForeignKey(f"{TABLE_STATISTICS_META}.id", ondelete="CASCADE"), + index=True, + ) + + start = Column(DATETIME_TYPE, index=True) + mean = Column(DOUBLE_TYPE) + min = Column(DOUBLE_TYPE) + max = Column(DOUBLE_TYPE) + last_reset = Column(DATETIME_TYPE) + state = Column(DOUBLE_TYPE) + sum = Column(DOUBLE_TYPE) + + @classmethod + def from_stats( + cls: type[_StatisticsBaseSelfT], metadata_id: int, stats: StatisticData + ) -> _StatisticsBaseSelfT: + """Create object from a statistics.""" + return cls( # type: ignore[call-arg,misc] + metadata_id=metadata_id, + **stats, + ) + + +class Statistics(Base, StatisticsBase): # type: ignore[misc,valid-type] + """Long term statistics.""" + + duration = timedelta(hours=1) + + __table_args__ = ( + # Used for fetching statistics for a certain entity at a specific time + Index("ix_statistics_statistic_id_start", "metadata_id", "start", unique=True), + ) + __tablename__ = TABLE_STATISTICS + + +class StatisticsShortTerm(Base, StatisticsBase): # type: ignore[misc,valid-type] + """Short term statistics.""" + + duration = timedelta(minutes=5) + + __table_args__ = ( + # Used for fetching statistics for a certain entity at a specific time + Index( + "ix_statistics_short_term_statistic_id_start", + "metadata_id", + "start", + unique=True, + ), + ) + __tablename__ = TABLE_STATISTICS_SHORT_TERM + + +class StatisticsMeta(Base): # type: ignore[misc,valid-type] + """Statistics meta data.""" + + __table_args__ = ( + {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, + ) + __tablename__ = TABLE_STATISTICS_META + id = Column(Integer, Identity(), primary_key=True) + statistic_id = Column(String(255), index=True, unique=True) + source = Column(String(32)) + unit_of_measurement = Column(String(255)) + has_mean = Column(Boolean) + has_sum = Column(Boolean) + name = Column(String(255)) + + @staticmethod + def from_meta(meta: StatisticMetaData) -> StatisticsMeta: + """Create object from meta data.""" + return StatisticsMeta(**meta) + + +class RecorderRuns(Base): # type: ignore[misc,valid-type] + """Representation of recorder run.""" + + __table_args__ = (Index("ix_recorder_runs_start_end", "start", "end"),) + __tablename__ = TABLE_RECORDER_RUNS + run_id = Column(Integer, Identity(), primary_key=True) + start = Column(DateTime(timezone=True), default=dt_util.utcnow) + end = Column(DateTime(timezone=True)) + closed_incorrect = Column(Boolean, default=False) + created = Column(DateTime(timezone=True), default=dt_util.utcnow) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + end = ( + f"'{self.end.isoformat(sep=' ', timespec='seconds')}'" if self.end else None + ) + return ( + f"" + ) + + def entity_ids(self, point_in_time: datetime | None = None) -> list[str]: + """Return the entity ids that existed in this run. + + Specify point_in_time if you want to know which existed at that point + in time inside the run. + """ + session = Session.object_session(self) + + assert session is not None, "RecorderRuns need to be persisted" + + query = session.query(distinct(States.entity_id)).filter( + States.last_updated >= self.start + ) + + if point_in_time is not None: + query = query.filter(States.last_updated < point_in_time) + elif self.end is not None: + query = query.filter(States.last_updated < self.end) + + return [row[0] for row in query] + + def to_native(self, validate_entity_id: bool = True) -> RecorderRuns: + """Return self, native format is this model.""" + return self + + +class SchemaChanges(Base): # type: ignore[misc,valid-type] + """Representation of schema version changes.""" + + __tablename__ = TABLE_SCHEMA_CHANGES + change_id = Column(Integer, Identity(), primary_key=True) + schema_version = Column(Integer) + changed = Column(DateTime(timezone=True), default=dt_util.utcnow) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + +class StatisticsRuns(Base): # type: ignore[misc,valid-type] + """Representation of statistics run.""" + + __tablename__ = TABLE_STATISTICS_RUNS + run_id = Column(Integer, Identity(), primary_key=True) + start = Column(DateTime(timezone=True), index=True) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + +EVENT_DATA_JSON = type_coerce( + EventData.shared_data.cast(JSONB_VARIENT_CAST), JSONLiteral(none_as_null=True) +) +OLD_FORMAT_EVENT_DATA_JSON = type_coerce( + Events.event_data.cast(JSONB_VARIENT_CAST), JSONLiteral(none_as_null=True) +) + +SHARED_ATTRS_JSON = type_coerce( + StateAttributes.shared_attrs.cast(JSON_VARIENT_CAST), JSON(none_as_null=True) +) +OLD_FORMAT_ATTRS_JSON = type_coerce( + States.attributes.cast(JSON_VARIENT_CAST), JSON(none_as_null=True) +) + +ENTITY_ID_IN_EVENT: Column = EVENT_DATA_JSON["entity_id"] +OLD_ENTITY_ID_IN_EVENT: Column = OLD_FORMAT_EVENT_DATA_JSON["entity_id"] +DEVICE_ID_IN_EVENT: Column = EVENT_DATA_JSON["device_id"] +OLD_STATE = aliased(States, name="old_state") diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index c5b4774ab34..cbba4dab26b 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -21,18 +21,23 @@ from sqlalchemy.pool import StaticPool from homeassistant.bootstrap import async_setup_component from homeassistant.components import persistent_notification as pn, recorder from homeassistant.components.recorder import db_schema, migration +from homeassistant.components.recorder.const import SQLITE_URL_PREFIX from homeassistant.components.recorder.db_schema import ( SCHEMA_VERSION, RecorderRuns, States, ) +from homeassistant.components.recorder.statistics import get_start_time from homeassistant.components.recorder.util import session_scope from homeassistant.helpers import recorder as recorder_helper +from homeassistant.setup import setup_component import homeassistant.util.dt as dt_util -from .common import async_wait_recording_done, create_engine_test +from .common import async_wait_recording_done, create_engine_test, wait_recording_done -from tests.common import async_fire_time_changed +from tests.common import async_fire_time_changed, get_test_home_assistant + +ORIG_TZ = dt_util.DEFAULT_TIME_ZONE def _get_native_states(hass, entity_id): @@ -358,6 +363,114 @@ async def test_schema_migrate(hass, start_version, live): assert recorder.util.async_migration_in_progress(hass) is not True +def test_set_state_unit(caplog, tmpdir): + """Test state unit column is initialized.""" + + def _create_engine_29(*args, **kwargs): + """Test version of create_engine that initializes with old schema. + + This simulates an existing db with the old schema. + """ + module = "tests.components.recorder.db_schema_29" + importlib.import_module(module) + old_db_schema = sys.modules[module] + engine = create_engine(*args, **kwargs) + old_db_schema.Base.metadata.create_all(engine) + with Session(engine) as session: + session.add(recorder.db_schema.StatisticsRuns(start=get_start_time())) + session.add( + recorder.db_schema.SchemaChanges( + schema_version=old_db_schema.SCHEMA_VERSION + ) + ) + session.commit() + return engine + + test_db_file = tmpdir.mkdir("sqlite").join("test_run_info.db") + dburl = f"{SQLITE_URL_PREFIX}//{test_db_file}" + + module = "tests.components.recorder.db_schema_29" + importlib.import_module(module) + old_db_schema = sys.modules[module] + + external_energy_metadata_1 = { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": "test", + "statistic_id": "test:total_energy_import_tariff_1", + "unit_of_measurement": "kWh", + } + external_co2_metadata = { + "has_mean": True, + "has_sum": False, + "name": "Fossil percentage", + "source": "test", + "statistic_id": "test:fossil_percentage", + "unit_of_measurement": "%", + } + + # Create some statistics_meta with schema version 29 + with patch.object(recorder, "db_schema", old_db_schema), patch.object( + recorder.migration, "SCHEMA_VERSION", old_db_schema.SCHEMA_VERSION + ), patch( + "homeassistant.components.recorder.core.create_engine", new=_create_engine_29 + ): + hass = get_test_home_assistant() + recorder_helper.async_initialize_recorder(hass) + setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) + wait_recording_done(hass) + wait_recording_done(hass) + + with session_scope(hass=hass) as session: + session.add( + recorder.db_schema.StatisticsMeta.from_meta(external_energy_metadata_1) + ) + session.add( + recorder.db_schema.StatisticsMeta.from_meta(external_co2_metadata) + ) + + with session_scope(hass=hass) as session: + tmp = session.query(recorder.db_schema.StatisticsMeta).all() + assert len(tmp) == 2 + assert tmp[0].id == 1 + assert tmp[0].statistic_id == "test:total_energy_import_tariff_1" + assert tmp[0].unit_of_measurement == "kWh" + assert not hasattr(tmp[0], "state_unit_of_measurement") + assert tmp[1].id == 2 + assert tmp[1].statistic_id == "test:fossil_percentage" + assert tmp[1].unit_of_measurement == "%" + assert not hasattr(tmp[1], "state_unit_of_measurement") + + hass.stop() + dt_util.DEFAULT_TIME_ZONE = ORIG_TZ + + # Test that the state_unit column is initialized during migration from schema 28 + hass = get_test_home_assistant() + recorder_helper.async_initialize_recorder(hass) + setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) + hass.start() + wait_recording_done(hass) + wait_recording_done(hass) + + with session_scope(hass=hass) as session: + tmp = session.query(recorder.db_schema.StatisticsMeta).all() + assert len(tmp) == 2 + assert tmp[0].id == 1 + assert tmp[0].statistic_id == "test:total_energy_import_tariff_1" + assert tmp[0].unit_of_measurement == "kWh" + assert hasattr(tmp[0], "state_unit_of_measurement") + assert tmp[0].state_unit_of_measurement == "kWh" + assert tmp[1].id == 2 + assert tmp[1].statistic_id == "test:fossil_percentage" + assert hasattr(tmp[1], "state_unit_of_measurement") + assert tmp[1].state_unit_of_measurement == "%" + assert tmp[1].state_unit_of_measurement == "%" + + hass.stop() + dt_util.DEFAULT_TIME_ZONE = ORIG_TZ + + def test_invalid_update(hass): """Test that an invalid new version raises an exception.""" with pytest.raises(ValueError): diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index 970a7feac61..beb7cef2fb9 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -35,10 +35,9 @@ from homeassistant.helpers import recorder as recorder_helper from homeassistant.setup import setup_component import homeassistant.util.dt as dt_util -from .common import async_wait_recording_done, do_adhoc_statistics +from .common import async_wait_recording_done, do_adhoc_statistics, wait_recording_done from tests.common import get_test_home_assistant, mock_registry -from tests.components.recorder.common import wait_recording_done ORIG_TZ = dt_util.DEFAULT_TIME_ZONE @@ -157,11 +156,12 @@ def mock_sensor_statistics(): """Generate fake statistics.""" return { "meta": { - "statistic_id": entity_id, - "unit_of_measurement": "dogs", "has_mean": True, "has_sum": False, "name": None, + "state_unit_of_measurement": "dogs", + "statistic_id": entity_id, + "unit_of_measurement": "dogs", }, "stat": {"start": start}, } @@ -488,6 +488,7 @@ async def test_import_statistics( "has_sum": True, "name": "Total imported energy", "source": source, + "state_unit_of_measurement": "kWh", "statistic_id": statistic_id, "unit_of_measurement": "kWh", } @@ -542,6 +543,7 @@ async def test_import_statistics( "has_sum": True, "name": "Total imported energy", "source": source, + "state_unit_of_measurement": "kWh", "statistic_id": statistic_id, "unit_of_measurement": "kWh", }, @@ -601,7 +603,7 @@ async def test_import_statistics( ] } - # Update the previously inserted statistics + rename + # Update the previously inserted statistics + rename and change unit external_statistics = { "start": period1, "max": 1, @@ -612,6 +614,7 @@ async def test_import_statistics( "sum": 5, } external_metadata["name"] = "Total imported energy renamed" + external_metadata["state_unit_of_measurement"] = "MWh" import_fn(hass, external_metadata, (external_statistics,)) await async_wait_recording_done(hass) statistic_ids = list_statistic_ids(hass) @@ -635,6 +638,7 @@ async def test_import_statistics( "has_sum": True, "name": "Total imported energy renamed", "source": source, + "state_unit_of_measurement": "MWh", "statistic_id": statistic_id, "unit_of_measurement": "kWh", }, @@ -1051,6 +1055,7 @@ def test_duplicate_statistics_handle_integrity_error(hass_recorder, caplog): "has_sum": True, "name": "Total imported energy", "source": "test", + "state_unit_of_measurement": "kWh", "statistic_id": "test:total_energy_import_tariff_1", "unit_of_measurement": "kWh", } diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index cdec26be26d..1e0633248bd 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -30,21 +30,26 @@ from .common import ( from tests.common import async_fire_time_changed -POWER_SENSOR_ATTRIBUTES = { +POWER_SENSOR_KW_ATTRIBUTES = { "device_class": "power", "state_class": "measurement", "unit_of_measurement": "kW", } -PRESSURE_SENSOR_ATTRIBUTES = { +PRESSURE_SENSOR_HPA_ATTRIBUTES = { "device_class": "pressure", "state_class": "measurement", "unit_of_measurement": "hPa", } -TEMPERATURE_SENSOR_ATTRIBUTES = { +TEMPERATURE_SENSOR_C_ATTRIBUTES = { "device_class": "temperature", "state_class": "measurement", "unit_of_measurement": "°C", } +TEMPERATURE_SENSOR_F_ATTRIBUTES = { + "device_class": "temperature", + "state_class": "measurement", + "unit_of_measurement": "°F", +} ENERGY_SENSOR_ATTRIBUTES = { "device_class": "energy", "state_class": "total", @@ -60,12 +65,14 @@ GAS_SENSOR_ATTRIBUTES = { @pytest.mark.parametrize( "units, attributes, state, value", [ - (IMPERIAL_SYSTEM, POWER_SENSOR_ATTRIBUTES, 10, 10000), - (METRIC_SYSTEM, POWER_SENSOR_ATTRIBUTES, 10, 10000), - (IMPERIAL_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, 10, 50), - (METRIC_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, 10, 10), - (IMPERIAL_SYSTEM, PRESSURE_SENSOR_ATTRIBUTES, 1000, 14.503774389728312), - (METRIC_SYSTEM, PRESSURE_SENSOR_ATTRIBUTES, 1000, 100000), + (IMPERIAL_SYSTEM, POWER_SENSOR_KW_ATTRIBUTES, 10, 10), + (METRIC_SYSTEM, POWER_SENSOR_KW_ATTRIBUTES, 10, 10), + (IMPERIAL_SYSTEM, TEMPERATURE_SENSOR_C_ATTRIBUTES, 10, 10), + (METRIC_SYSTEM, TEMPERATURE_SENSOR_C_ATTRIBUTES, 10, 10), + (IMPERIAL_SYSTEM, TEMPERATURE_SENSOR_F_ATTRIBUTES, 10, 10), + (METRIC_SYSTEM, TEMPERATURE_SENSOR_F_ATTRIBUTES, 10, 10), + (IMPERIAL_SYSTEM, PRESSURE_SENSOR_HPA_ATTRIBUTES, 1000, 1000), + (METRIC_SYSTEM, PRESSURE_SENSOR_HPA_ATTRIBUTES, 1000, 1000), ], ) async def test_statistics_during_period( @@ -129,12 +136,12 @@ async def test_statistics_during_period( @pytest.mark.parametrize( "units, attributes, state, value", [ - (IMPERIAL_SYSTEM, POWER_SENSOR_ATTRIBUTES, 10, 10000), - (METRIC_SYSTEM, POWER_SENSOR_ATTRIBUTES, 10, 10000), - (IMPERIAL_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, 10, 50), - (METRIC_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, 10, 10), - (IMPERIAL_SYSTEM, PRESSURE_SENSOR_ATTRIBUTES, 1000, 14.503774389728312), - (METRIC_SYSTEM, PRESSURE_SENSOR_ATTRIBUTES, 1000, 100000), + (IMPERIAL_SYSTEM, POWER_SENSOR_KW_ATTRIBUTES, 10, 10), + (METRIC_SYSTEM, POWER_SENSOR_KW_ATTRIBUTES, 10, 10), + (IMPERIAL_SYSTEM, TEMPERATURE_SENSOR_C_ATTRIBUTES, 10, 10), + (METRIC_SYSTEM, TEMPERATURE_SENSOR_C_ATTRIBUTES, 10, 10), + (IMPERIAL_SYSTEM, PRESSURE_SENSOR_HPA_ATTRIBUTES, 1000, 1000), + (METRIC_SYSTEM, PRESSURE_SENSOR_HPA_ATTRIBUTES, 1000, 1000), ], ) async def test_statistics_during_period_in_the_past( @@ -302,12 +309,14 @@ async def test_statistics_during_period_bad_end_time( @pytest.mark.parametrize( "units, attributes, display_unit, statistics_unit", [ - (IMPERIAL_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W", "W"), - (METRIC_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W", "W"), - (IMPERIAL_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, "°F", "°C"), - (METRIC_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, "°C", "°C"), - (IMPERIAL_SYSTEM, PRESSURE_SENSOR_ATTRIBUTES, "psi", "Pa"), - (METRIC_SYSTEM, PRESSURE_SENSOR_ATTRIBUTES, "Pa", "Pa"), + (IMPERIAL_SYSTEM, POWER_SENSOR_KW_ATTRIBUTES, "kW", "W"), + (METRIC_SYSTEM, POWER_SENSOR_KW_ATTRIBUTES, "kW", "W"), + (IMPERIAL_SYSTEM, TEMPERATURE_SENSOR_C_ATTRIBUTES, "°C", "°C"), + (METRIC_SYSTEM, TEMPERATURE_SENSOR_C_ATTRIBUTES, "°C", "°C"), + (IMPERIAL_SYSTEM, TEMPERATURE_SENSOR_F_ATTRIBUTES, "°F", "°C"), + (METRIC_SYSTEM, TEMPERATURE_SENSOR_F_ATTRIBUTES, "°F", "°C"), + (IMPERIAL_SYSTEM, PRESSURE_SENSOR_HPA_ATTRIBUTES, "hPa", "Pa"), + (METRIC_SYSTEM, PRESSURE_SENSOR_HPA_ATTRIBUTES, "hPa", "Pa"), ], ) async def test_list_statistic_ids( @@ -429,9 +438,9 @@ async def test_clear_statistics(hass, hass_ws_client, recorder_mock): now = dt_util.utcnow() units = METRIC_SYSTEM - attributes = POWER_SENSOR_ATTRIBUTES + attributes = POWER_SENSOR_KW_ATTRIBUTES state = 10 - value = 10000 + value = 10 hass.config.units = units await async_setup_component(hass, "sensor", {}) @@ -555,7 +564,7 @@ async def test_update_statistics_metadata( now = dt_util.utcnow() units = METRIC_SYSTEM - attributes = POWER_SENSOR_ATTRIBUTES + attributes = POWER_SENSOR_KW_ATTRIBUTES state = 10 hass.config.units = units @@ -575,7 +584,7 @@ async def test_update_statistics_metadata( assert response["result"] == [ { "statistic_id": "sensor.test", - "display_unit_of_measurement": "W", + "display_unit_of_measurement": "kW", "has_mean": True, "has_sum": False, "name": None, @@ -602,7 +611,7 @@ async def test_update_statistics_metadata( assert response["result"] == [ { "statistic_id": "sensor.test", - "display_unit_of_measurement": new_unit, + "display_unit_of_measurement": "kW", "has_mean": True, "has_sum": False, "name": None, @@ -1016,6 +1025,7 @@ async def test_import_statistics( "has_sum": True, "name": "Total imported energy", "source": source, + "state_unit_of_measurement": "kWh", "statistic_id": statistic_id, "unit_of_measurement": "kWh", }, diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index e7421e6a616..b20b270ee69 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -84,12 +84,12 @@ def set_time_zone(): ("humidity", "%", "%", "%", 13.050847, -10, 30), ("humidity", None, None, None, 13.050847, -10, 30), ("pressure", "Pa", "Pa", "Pa", 13.050847, -10, 30), - ("pressure", "hPa", "Pa", "Pa", 1305.0847, -1000, 3000), - ("pressure", "mbar", "Pa", "Pa", 1305.0847, -1000, 3000), - ("pressure", "inHg", "Pa", "Pa", 44195.25, -33863.89, 101591.67), - ("pressure", "psi", "Pa", "Pa", 89982.42, -68947.57, 206842.71), + ("pressure", "hPa", "hPa", "Pa", 13.050847, -10, 30), + ("pressure", "mbar", "mbar", "Pa", 13.050847, -10, 30), + ("pressure", "inHg", "inHg", "Pa", 13.050847, -10, 30), + ("pressure", "psi", "psi", "Pa", 13.050847, -10, 30), ("temperature", "°C", "°C", "°C", 13.050847, -10, 30), - ("temperature", "°F", "°C", "°C", -10.52731, -23.33333, -1.111111), + ("temperature", "°F", "°F", "°C", 13.050847, -10, 30), ], ) def test_compile_hourly_statistics( @@ -1513,12 +1513,12 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): ("humidity", "%", 30), ("humidity", None, 30), ("pressure", "Pa", 30), - ("pressure", "hPa", 3000), - ("pressure", "mbar", 3000), - ("pressure", "inHg", 101591.67), - ("pressure", "psi", 206842.71), + ("pressure", "hPa", 30), + ("pressure", "mbar", 30), + ("pressure", "inHg", 30), + ("pressure", "psi", 30), ("temperature", "°C", 30), - ("temperature", "°F", -1.111111), + ("temperature", "°F", 30), ], ) def test_compile_hourly_statistics_unchanged( @@ -1600,12 +1600,12 @@ def test_compile_hourly_statistics_partially_unavailable(hass_recorder, caplog): ("humidity", "%", 30), ("humidity", None, 30), ("pressure", "Pa", 30), - ("pressure", "hPa", 3000), - ("pressure", "mbar", 3000), - ("pressure", "inHg", 101591.67), - ("pressure", "psi", 206842.71), + ("pressure", "hPa", 30), + ("pressure", "mbar", 30), + ("pressure", "inHg", 30), + ("pressure", "psi", 30), ("temperature", "°C", 30), - ("temperature", "°F", -1.111111), + ("temperature", "°F", 30), ], ) def test_compile_hourly_statistics_unavailable( @@ -1685,12 +1685,12 @@ def test_compile_hourly_statistics_fails(hass_recorder, caplog): ("measurement", "gas", "m³", "m³", "m³", "mean"), ("measurement", "gas", "ft³", "m³", "m³", "mean"), ("measurement", "pressure", "Pa", "Pa", "Pa", "mean"), - ("measurement", "pressure", "hPa", "Pa", "Pa", "mean"), - ("measurement", "pressure", "mbar", "Pa", "Pa", "mean"), - ("measurement", "pressure", "inHg", "Pa", "Pa", "mean"), - ("measurement", "pressure", "psi", "Pa", "Pa", "mean"), + ("measurement", "pressure", "hPa", "hPa", "Pa", "mean"), + ("measurement", "pressure", "mbar", "mbar", "Pa", "mean"), + ("measurement", "pressure", "inHg", "inHg", "Pa", "mean"), + ("measurement", "pressure", "psi", "psi", "Pa", "mean"), ("measurement", "temperature", "°C", "°C", "°C", "mean"), - ("measurement", "temperature", "°F", "°C", "°C", "mean"), + ("measurement", "temperature", "°F", "°F", "°C", "mean"), ], ) def test_list_statistic_ids( @@ -2162,13 +2162,21 @@ def test_compile_hourly_statistics_changing_device_class_1( @pytest.mark.parametrize( - "device_class,state_unit,statistic_unit,mean,min,max", + "device_class,state_unit,display_unit,statistic_unit,mean,min,max", [ - ("power", "kW", "W", 13050.847, -10000, 30000), + ("power", "kW", "kW", "W", 13.050847, -10, 30), ], ) def test_compile_hourly_statistics_changing_device_class_2( - hass_recorder, caplog, device_class, state_unit, statistic_unit, mean, min, max + hass_recorder, + caplog, + device_class, + state_unit, + display_unit, + statistic_unit, + mean, + min, + max, ): """Test compiling hourly statistics where device class changes from one hour to the next.""" zero = dt_util.utcnow() @@ -2191,7 +2199,7 @@ def test_compile_hourly_statistics_changing_device_class_2( assert statistic_ids == [ { "statistic_id": "sensor.test1", - "display_unit_of_measurement": statistic_unit, + "display_unit_of_measurement": display_unit, "has_mean": True, "has_sum": False, "name": None, @@ -2240,7 +2248,7 @@ def test_compile_hourly_statistics_changing_device_class_2( assert statistic_ids == [ { "statistic_id": "sensor.test1", - "display_unit_of_measurement": statistic_unit, + "display_unit_of_measurement": display_unit, "has_mean": True, "has_sum": False, "name": None, @@ -2325,6 +2333,7 @@ def test_compile_hourly_statistics_changing_statistics( "has_sum": False, "name": None, "source": "recorder", + "state_unit_of_measurement": None, "statistic_id": "sensor.test1", "unit_of_measurement": None, }, @@ -2360,6 +2369,7 @@ def test_compile_hourly_statistics_changing_statistics( "has_sum": True, "name": None, "source": "recorder", + "state_unit_of_measurement": None, "statistic_id": "sensor.test1", "unit_of_measurement": None, }, From 635ed562eebe7919e6edfee3c22a70b4ce33e7d0 Mon Sep 17 00:00:00 2001 From: Ricardo Steijn <61013287+RicArch97@users.noreply.github.com> Date: Thu, 15 Sep 2022 18:05:55 +0200 Subject: [PATCH 453/955] crownstone-sse: bump to 2.0.4 (#78538) --- homeassistant/components/crownstone/const.py | 1 + homeassistant/components/crownstone/entry_manager.py | 2 ++ homeassistant/components/crownstone/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/crownstone/const.py b/homeassistant/components/crownstone/const.py index a362435b9ce..9b3624a4575 100644 --- a/homeassistant/components/crownstone/const.py +++ b/homeassistant/components/crownstone/const.py @@ -7,6 +7,7 @@ from homeassistant.const import Platform # Platforms DOMAIN: Final = "crownstone" +PROJECT_NAME: Final = "home-assistant-core" PLATFORMS: Final[list[Platform]] = [Platform.LIGHT] # Listeners diff --git a/homeassistant/components/crownstone/entry_manager.py b/homeassistant/components/crownstone/entry_manager.py index dcae7ef4705..2f74daa8629 100644 --- a/homeassistant/components/crownstone/entry_manager.py +++ b/homeassistant/components/crownstone/entry_manager.py @@ -27,6 +27,7 @@ from .const import ( CONF_USB_SPHERE, DOMAIN, PLATFORMS, + PROJECT_NAME, SSE_LISTENERS, UART_LISTENERS, ) @@ -84,6 +85,7 @@ class CrownstoneEntryManager: password=password, access_token=self.cloud.access_token, websession=aiohttp_client.async_create_clientsession(self.hass), + project_name=PROJECT_NAME, ) # Listen for events in the background, without task tracking asyncio.create_task(self.async_process_events(self.sse)) diff --git a/homeassistant/components/crownstone/manifest.json b/homeassistant/components/crownstone/manifest.json index cdc79e7f0b5..39abd998be7 100644 --- a/homeassistant/components/crownstone/manifest.json +++ b/homeassistant/components/crownstone/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/crownstone", "requirements": [ "crownstone-cloud==1.4.9", - "crownstone-sse==2.0.3", + "crownstone-sse==2.0.4", "crownstone-uart==2.1.0", "pyserial==3.5" ], diff --git a/requirements_all.txt b/requirements_all.txt index 54e41610684..da7254a1dee 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -523,7 +523,7 @@ croniter==1.0.6 crownstone-cloud==1.4.9 # homeassistant.components.crownstone -crownstone-sse==2.0.3 +crownstone-sse==2.0.4 # homeassistant.components.crownstone crownstone-uart==2.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fe38fd99219..7914be321ae 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -400,7 +400,7 @@ croniter==1.0.6 crownstone-cloud==1.4.9 # homeassistant.components.crownstone -crownstone-sse==2.0.3 +crownstone-sse==2.0.4 # homeassistant.components.crownstone crownstone-uart==2.1.0 From 8300f8234ce81db05c8c1b5fa953b185a50024f5 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 16 Sep 2022 00:20:47 +0200 Subject: [PATCH 454/955] Make async_extract_entities generic (#78490) --- homeassistant/helpers/entity_component.py | 2 +- homeassistant/helpers/service.py | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index fb627820060..86d5436e287 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -186,7 +186,7 @@ class EntityComponent(Generic[_EntityT]): This method must be run in the event loop. """ - return await service.async_extract_entities( # type: ignore[return-value] + return await service.async_extract_entities( self.hass, self.entities, service_call, expand_group ) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index cf7fb3b2304..7675686844c 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -6,7 +6,7 @@ from collections.abc import Awaitable, Callable, Iterable import dataclasses from functools import partial, wraps import logging -from typing import TYPE_CHECKING, Any, TypedDict +from typing import TYPE_CHECKING, Any, TypedDict, TypeVar from typing_extensions import TypeGuard import voluptuous as vol @@ -48,6 +48,8 @@ if TYPE_CHECKING: from .entity import Entity from .entity_platform import EntityPlatform + _EntityT = TypeVar("_EntityT", bound=Entity) + CONF_SERVICE_ENTITY_ID = "entity_id" CONF_SERVICE_DATA_TEMPLATE = "data_template" @@ -276,10 +278,10 @@ def extract_entity_ids( @bind_hass async def async_extract_entities( hass: HomeAssistant, - entities: Iterable[Entity], + entities: Iterable[_EntityT], service_call: ServiceCall, expand_group: bool = True, -) -> list[Entity]: +) -> list[_EntityT]: """Extract a list of entity objects from a service call. Will convert group entity ids to the entity ids it represents. From ec258410c5b06de23e62acce5e86044ae549cd7e Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 16 Sep 2022 00:29:29 +0000 Subject: [PATCH 455/955] [ci skip] Translation update --- .../amberelectric/translations/no.json | 1 + .../amberelectric/translations/pl.json | 5 + .../android_ip_webcam/translations/bg.json | 14 ++ .../automation/translations/pl.json | 13 ++ .../bluemaestro/translations/pl.json | 22 +++ .../components/bluetooth/translations/bg.json | 16 +++ .../components/bluetooth/translations/pl.json | 12 +- .../components/bthome/translations/bg.json | 4 +- .../components/bthome/translations/pl.json | 10 ++ .../components/demo/translations/bg.json | 12 ++ .../components/ecowitt/translations/pl.json | 20 +++ .../components/escea/translations/bg.json | 8 ++ .../components/fibaro/translations/pl.json | 10 +- .../google_sheets/translations/bg.json | 23 +++ .../google_sheets/translations/ca.json | 31 +++++ .../google_sheets/translations/de.json | 31 +++++ .../google_sheets/translations/es.json | 31 +++++ .../google_sheets/translations/id.json | 31 +++++ .../google_sheets/translations/it.json | 31 +++++ .../google_sheets/translations/no.json | 31 +++++ .../google_sheets/translations/pl.json | 31 +++++ .../google_sheets/translations/pt.json | 15 ++ .../google_sheets/translations/zh-Hant.json | 31 +++++ .../components/govee_ble/translations/bg.json | 20 +++ .../components/hassio/translations/bg.json | 1 + .../homeassistant/translations/bg.json | 1 + .../homeassistant_alerts/translations/bg.json | 8 ++ .../components/hue/translations/pl.json | 5 +- .../components/icloud/translations/pl.json | 7 + .../components/inkbird/translations/bg.json | 20 +++ .../justnimbus/translations/bg.json | 12 ++ .../lacrosse_view/translations/bg.json | 13 ++ .../components/lametric/translations/bg.json | 14 ++ .../components/lametric/translations/pl.json | 26 ++++ .../landisgyr_heat_meter/translations/pl.json | 5 + .../components/led_ble/translations/bg.json | 2 + .../components/led_ble/translations/pl.json | 10 +- .../components/lifx/translations/bg.json | 18 +++ .../litterrobot/translations/pl.json | 1 + .../components/melnor/translations/pl.json | 13 ++ .../components/moat/translations/bg.json | 20 +++ .../components/mqtt/translations/pl.json | 6 + .../components/mysensors/translations/bg.json | 9 ++ .../components/nobo_hub/translations/bg.json | 5 + .../components/nobo_hub/translations/pl.json | 44 ++++++ .../openexchangerates/translations/bg.json | 22 +++ .../components/overkiz/translations/pl.json | 3 +- .../overkiz/translations/sensor.pl.json | 2 +- .../p1_monitor/translations/pl.json | 3 + .../prusalink/translations/sensor.pl.json | 10 +- .../pure_energie/translations/pl.json | 3 + .../components/pushover/translations/pl.json | 12 +- .../components/qingping/translations/bg.json | 1 + .../components/risco/translations/pl.json | 4 + .../components/sensibo/translations/pl.json | 6 + .../components/sensor/translations/bg.json | 1 + .../components/sensor/translations/pl.json | 2 + .../components/sensorpro/translations/pl.json | 22 +++ .../sensorpush/translations/bg.json | 20 +++ .../simplisafe/translations/bg.json | 2 + .../components/skybell/translations/pl.json | 7 + .../speedtestdotnet/translations/pl.json | 13 ++ .../components/switchbee/translations/bg.json | 21 +++ .../components/switchbee/translations/id.json | 32 +++++ .../components/switchbee/translations/it.json | 32 +++++ .../components/switchbee/translations/no.json | 32 +++++ .../components/switchbee/translations/pl.json | 32 +++++ .../components/switchbot/translations/bg.json | 10 ++ .../thermobeacon/translations/bg.json | 1 + .../thermobeacon/translations/pl.json | 3 +- .../components/thermopro/translations/bg.json | 3 +- .../components/tilt_ble/translations/no.json | 11 +- .../components/tilt_ble/translations/pl.json | 21 +++ .../components/timer/translations/bg.json | 2 +- .../tuya/translations/sensor.pl.json | 8 +- .../unifiprotect/translations/pl.json | 4 + .../volvooncall/translations/pl.json | 12 ++ .../wolflink/translations/sensor.bg.json | 1 + .../wolflink/translations/sensor.pl.json | 2 +- .../xiaomi_ble/translations/bg.json | 21 +++ .../xiaomi_miio/translations/select.pl.json | 10 ++ .../yalexs_ble/translations/bg.json | 10 ++ .../components/zha/translations/bg.json | 39 +++++- .../components/zha/translations/pl.json | 131 +++++++++++++++++- .../zodiac/translations/sensor.pl.json | 24 ++-- 85 files changed, 1251 insertions(+), 37 deletions(-) create mode 100644 homeassistant/components/bluemaestro/translations/pl.json create mode 100644 homeassistant/components/ecowitt/translations/pl.json create mode 100644 homeassistant/components/escea/translations/bg.json create mode 100644 homeassistant/components/google_sheets/translations/bg.json create mode 100644 homeassistant/components/google_sheets/translations/ca.json create mode 100644 homeassistant/components/google_sheets/translations/de.json create mode 100644 homeassistant/components/google_sheets/translations/es.json create mode 100644 homeassistant/components/google_sheets/translations/id.json create mode 100644 homeassistant/components/google_sheets/translations/it.json create mode 100644 homeassistant/components/google_sheets/translations/no.json create mode 100644 homeassistant/components/google_sheets/translations/pl.json create mode 100644 homeassistant/components/google_sheets/translations/pt.json create mode 100644 homeassistant/components/google_sheets/translations/zh-Hant.json create mode 100644 homeassistant/components/govee_ble/translations/bg.json create mode 100644 homeassistant/components/homeassistant_alerts/translations/bg.json create mode 100644 homeassistant/components/inkbird/translations/bg.json create mode 100644 homeassistant/components/justnimbus/translations/bg.json create mode 100644 homeassistant/components/melnor/translations/pl.json create mode 100644 homeassistant/components/moat/translations/bg.json create mode 100644 homeassistant/components/nobo_hub/translations/pl.json create mode 100644 homeassistant/components/openexchangerates/translations/bg.json create mode 100644 homeassistant/components/sensorpro/translations/pl.json create mode 100644 homeassistant/components/sensorpush/translations/bg.json create mode 100644 homeassistant/components/switchbee/translations/bg.json create mode 100644 homeassistant/components/switchbee/translations/id.json create mode 100644 homeassistant/components/switchbee/translations/it.json create mode 100644 homeassistant/components/switchbee/translations/no.json create mode 100644 homeassistant/components/switchbee/translations/pl.json create mode 100644 homeassistant/components/tilt_ble/translations/pl.json create mode 100644 homeassistant/components/xiaomi_ble/translations/bg.json diff --git a/homeassistant/components/amberelectric/translations/no.json b/homeassistant/components/amberelectric/translations/no.json index 27c0111934b..380a8cdeda9 100644 --- a/homeassistant/components/amberelectric/translations/no.json +++ b/homeassistant/components/amberelectric/translations/no.json @@ -2,6 +2,7 @@ "config": { "error": { "invalid_api_token": "Ugyldig API-n\u00f8kkel", + "no_site": "Ingen side oppgitt", "unknown_error": "Uventet feil" }, "step": { diff --git a/homeassistant/components/amberelectric/translations/pl.json b/homeassistant/components/amberelectric/translations/pl.json index 2810149273f..9c7197a9ac0 100644 --- a/homeassistant/components/amberelectric/translations/pl.json +++ b/homeassistant/components/amberelectric/translations/pl.json @@ -1,5 +1,10 @@ { "config": { + "error": { + "invalid_api_token": "Nieprawid\u0142owy klucz API", + "no_site": "Nie podano strony", + "unknown_error": "Nieoczekiwany b\u0142\u0105d" + }, "step": { "site": { "data": { diff --git a/homeassistant/components/android_ip_webcam/translations/bg.json b/homeassistant/components/android_ip_webcam/translations/bg.json index 946b62a8690..7d31d58dcc0 100644 --- a/homeassistant/components/android_ip_webcam/translations/bg.json +++ b/homeassistant/components/android_ip_webcam/translations/bg.json @@ -1,7 +1,21 @@ { "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" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "port": "\u041f\u043e\u0440\u0442", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/automation/translations/pl.json b/homeassistant/components/automation/translations/pl.json index 959fb48b355..81bbbcbc7cd 100644 --- a/homeassistant/components/automation/translations/pl.json +++ b/homeassistant/components/automation/translations/pl.json @@ -1,4 +1,17 @@ { + "issues": { + "service_not_found": { + "fix_flow": { + "step": { + "confirm": { + "description": "Automatyzacja \"{name}\" (`{entity_id}`) posiada akcj\u0119, kt\u00f3ra wywo\u0142uje nieznan\u0105 us\u0142ug\u0119: `{service}`. \n\nTen b\u0142\u0105d uniemo\u017cliwia prawid\u0142owe dzia\u0142anie automatyzacji. Mo\u017ce ta us\u0142uga nie jest ju\u017c dost\u0119pna, a mo\u017ce spowodowa\u0142a to liter\u00f3wka. \n\nAby naprawi\u0107 ten b\u0142\u0105d, [edytuj automatyzacj\u0119]({edit}) i usu\u0144 dzia\u0142anie wywo\u0142uj\u0105ce t\u0119 us\u0142ug\u0119. \n\nKliknij ZATWIERD\u0179 poni\u017cej, aby potwierdzi\u0107, \u017ce naprawi\u0142e\u015b t\u0119 automatyzacj\u0119.", + "title": "{name} korzysta z nieznanej us\u0142ugi" + } + } + }, + "title": "{name} korzysta z nieznanej us\u0142ugi" + } + }, "state": { "_": { "off": "wy\u0142.", diff --git a/homeassistant/components/bluemaestro/translations/pl.json b/homeassistant/components/bluemaestro/translations/pl.json new file mode 100644 index 00000000000..4715905a2e9 --- /dev/null +++ b/homeassistant/components/bluemaestro/translations/pl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "already_in_progress": "Konfiguracja jest ju\u017c w toku", + "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci", + "not_supported": "Urz\u0105dzenie nie jest obs\u0142ugiwane" + }, + "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/bluetooth/translations/bg.json b/homeassistant/components/bluetooth/translations/bg.json index 9b010d6629d..a34ca0f83f1 100644 --- a/homeassistant/components/bluetooth/translations/bg.json +++ b/homeassistant/components/bluetooth/translations/bg.json @@ -1,6 +1,16 @@ { "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" + }, + "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}?" + }, + "enable_bluetooth": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 Bluetooth?" + }, "multiple_adapters": { "data": { "adapter": "\u0410\u0434\u0430\u043f\u0442\u0435\u0440" @@ -9,6 +19,12 @@ }, "single_adapter": { "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 Bluetooth \u0430\u0434\u0430\u043f\u0442\u0435\u0440\u0430 {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" } } }, diff --git a/homeassistant/components/bluetooth/translations/pl.json b/homeassistant/components/bluetooth/translations/pl.json index 9c52b5a136f..4c1a692e1ba 100644 --- a/homeassistant/components/bluetooth/translations/pl.json +++ b/homeassistant/components/bluetooth/translations/pl.json @@ -12,6 +12,15 @@ "enable_bluetooth": { "description": "Czy chcesz skonfigurowa\u0107 Bluetooth?" }, + "multiple_adapters": { + "data": { + "adapter": "Adapter" + }, + "description": "Wybierz adapter Bluetooth do konfiguracji" + }, + "single_adapter": { + "description": "Czy chcesz skonfigurowa\u0107 adapter Bluetooth {name}?" + }, "user": { "data": { "address": "Urz\u0105dzenie" @@ -26,7 +35,8 @@ "data": { "adapter": "Adapter Bluetooth u\u017cywany do skanowania", "passive": "Skanowanie pasywne" - } + }, + "description": "Nas\u0142uchiwanie pasywne wymaga BlueZ 5.63 lub nowszego z w\u0142\u0105czonymi funkcjami eksperymentalnymi." } } } diff --git a/homeassistant/components/bthome/translations/bg.json b/homeassistant/components/bthome/translations/bg.json index e8427c23986..895fcac7c4f 100644 --- a/homeassistant/components/bthome/translations/bg.json +++ b/homeassistant/components/bthome/translations/bg.json @@ -2,6 +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", + "no_devices_found": "\u041d\u044f\u043c\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\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" }, "flow_title": "{name}", @@ -12,7 +13,8 @@ "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" } } } diff --git a/homeassistant/components/bthome/translations/pl.json b/homeassistant/components/bthome/translations/pl.json index 814c4cd9757..db19044b347 100644 --- a/homeassistant/components/bthome/translations/pl.json +++ b/homeassistant/components/bthome/translations/pl.json @@ -6,11 +6,21 @@ "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci", "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" }, + "error": { + "decryption_failed": "Podany klucz (bindkey) nie zadzia\u0142a\u0142, dane czujnika nie mog\u0142y zosta\u0107 odszyfrowane. Sprawd\u017a go i spr\u00f3buj ponownie.", + "expected_32_characters": "Oczekiwano 32-znakowego szesnastkowego klucza bindkey." + }, "flow_title": "{name}", "step": { "bluetooth_confirm": { "description": "Czy chcesz skonfigurowa\u0107 {name}?" }, + "get_encryption_key": { + "data": { + "bindkey": "Bindkey" + }, + "description": "Dane przesy\u0142ane przez sensor s\u0105 szyfrowane. Aby je odszyfrowa\u0107, potrzebujemy 32-znakowego szesnastkowego klucza bindkey." + }, "user": { "data": { "address": "Urz\u0105dzenie" diff --git a/homeassistant/components/demo/translations/bg.json b/homeassistant/components/demo/translations/bg.json index 9609b0e64d9..3256ee582f9 100644 --- a/homeassistant/components/demo/translations/bg.json +++ b/homeassistant/components/demo/translations/bg.json @@ -1,3 +1,15 @@ { + "issues": { + "bad_psu": { + "fix_flow": { + "step": { + "confirm": { + "title": "\u0417\u0430\u0445\u0440\u0430\u043d\u0432\u0430\u043d\u0435\u0442\u043e \u0442\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u0431\u044a\u0434\u0435 \u0441\u043c\u0435\u043d\u0435\u043d\u043e" + } + } + }, + "title": "\u0417\u0430\u0445\u0440\u0430\u043d\u0432\u0430\u043d\u0435\u0442\u043e \u043d\u0435 \u0435 \u0441\u0442\u0430\u0431\u0438\u043b\u043d\u043e" + } + }, "title": "\u0414\u0435\u043c\u043e\u043d\u0441\u0442\u0440\u0430\u0446\u0438\u044f" } \ No newline at end of file diff --git a/homeassistant/components/ecowitt/translations/pl.json b/homeassistant/components/ecowitt/translations/pl.json new file mode 100644 index 00000000000..64fb3e6e4ef --- /dev/null +++ b/homeassistant/components/ecowitt/translations/pl.json @@ -0,0 +1,20 @@ +{ + "config": { + "create_entry": { + "default": "Aby zako\u0144czy\u0107 konfiguracj\u0119 integracji, u\u017cyj aplikacji Ecowitt (na telefonie) lub uzyskaj dost\u0119p do Ecowitt WebUI w przegl\u0105darce pod adresem IP stacji. \n\nWybierz swoj\u0105 stacj\u0119 - > Menu \"Others\" - > DIY Upload Servers. Kliknij dalej i wybierz \"Customized\" \n\n- IP serwera: `{server}`\n- \u015acie\u017cka: `{path}`\n- Port: `{port}` \n\nKliknij \u201eZapisz\u201d." + }, + "error": { + "invalid_port": "Port jest ju\u017c u\u017cywany.", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "path": "\u015acie\u017cka do tokena bezpiecze\u0144stwa", + "port": "Port nas\u0142uchiwania" + }, + "description": "Czy chcesz rozpocz\u0105\u0107 konfiguracj\u0119?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/escea/translations/bg.json b/homeassistant/components/escea/translations/bg.json new file mode 100644 index 00000000000..ed4c38dba5a --- /dev/null +++ b/homeassistant/components/escea/translations/bg.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u041d\u044f\u043c\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\u0430", + "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." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fibaro/translations/pl.json b/homeassistant/components/fibaro/translations/pl.json index ce4b2652d80..da7240c3e85 100644 --- a/homeassistant/components/fibaro/translations/pl.json +++ b/homeassistant/components/fibaro/translations/pl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", @@ -9,6 +10,13 @@ "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { + "reauth_confirm": { + "data": { + "password": "Has\u0142o" + }, + "description": "Zaktualizuj has\u0142o dla u\u017cytkownika {username}", + "title": "Ponownie uwierzytelnij integracj\u0119" + }, "user": { "data": { "import_plugins": "Zaimportowa\u0107 encje z wtyczek fibaro?", diff --git a/homeassistant/components/google_sheets/translations/bg.json b/homeassistant/components/google_sheets/translations/bg.json new file mode 100644 index 00000000000..edd42ae2269 --- /dev/null +++ b/homeassistant/components/google_sheets/translations/bg.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "create_spreadsheet_failure": "\u0413\u0440\u0435\u0448\u043a\u0430 \u043f\u0440\u0438 \u0441\u044a\u0437\u0434\u0430\u0432\u0430\u043d\u0435 \u043d\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u0430 \u0442\u0430\u0431\u043b\u0438\u0446\u0430, \u0432\u0438\u0436\u0442\u0435 \u0436\u0443\u0440\u043d\u0430\u043b\u0430 \u0437\u0430 \u0433\u0440\u0435\u0448\u043a\u0438 \u0437\u0430 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0441\u0442\u0438", + "missing_configuration": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u044a\u0442 \u043d\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d. \u041c\u043e\u043b\u044f, \u0441\u043b\u0435\u0434\u0432\u0430\u0439\u0442\u0435 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0442\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", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "create_entry": { + "default": "\u0423\u0441\u043f\u0435\u0448\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0438 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u0430\u0442\u0430 \u0442\u0430\u0431\u043b\u0438\u0446\u0430 \u0435 \u0441\u044a\u0437\u0434\u0430\u0434\u0435\u043d\u0430 \u043d\u0430 \u0430\u0434\u0440\u0435\u0441: {url}" + }, + "step": { + "auth": { + "title": "\u0421\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 \u0430\u043a\u0430\u0443\u043d\u0442 \u0432 Google" + }, + "pick_implementation": { + "title": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u043c\u0435\u0442\u043e\u0434 \u0437\u0430 \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_sheets/translations/ca.json b/homeassistant/components/google_sheets/translations/ca.json new file mode 100644 index 00000000000..bc67a556573 --- /dev/null +++ b/homeassistant/components/google_sheets/translations/ca.json @@ -0,0 +1,31 @@ +{ + "application_credentials": { + "description": "Segueix les [instruccions]({more_info_url}) de [la pantalla de consentiment OAuth]({oauth_consent_url}) perqu\u00e8 Home Assistant tingui acc\u00e9s als teus fulls de c\u00e0lcul de Google. Tamb\u00e9 has de crear les credencials d'aplicaci\u00f3 enlla\u00e7ades al teu compte:\n 1. V\u00e9s a [Credencials]({oauth_creds_url}) i fes clic a **Crear credencials**.\n 2. A la llista desplegable, selecciona **ID de client OAuth**.\n 3. Selecciona **Aplicaci\u00f3 Web** al tipus d'aplicaci\u00f3.\n \n " + }, + "config": { + "abort": { + "already_configured": "El compte ja est\u00e0 configurat", + "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", + "cannot_connect": "Ha fallat la connexi\u00f3", + "create_spreadsheet_failure": "Error en crear el full de c\u00e0lcul, consulta el registre d'errors per m\u00e9s informaci\u00f3", + "invalid_access_token": "Token d'acc\u00e9s inv\u00e0lid", + "missing_configuration": "El component no est\u00e0 configurat. Mira'n la documentaci\u00f3.", + "oauth_error": "S'han rebut dades token inv\u00e0lides.", + "open_spreadsheet_failure": "Error en obrir el full de c\u00e0lcul, consulta el registre d'errors per m\u00e9s informaci\u00f3", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament", + "timeout_connect": "S'ha esgotat el temps m\u00e0xim d'espera per establir connexi\u00f3", + "unknown": "Error inesperat" + }, + "create_entry": { + "default": "S'ha autenticat correctament i s'ha creat un full de c\u00e0lcul a: {url}" + }, + "step": { + "auth": { + "title": "Vinculaci\u00f3 amb compte de Google" + }, + "pick_implementation": { + "title": "Selecciona el m\u00e8tode d'autenticaci\u00f3" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_sheets/translations/de.json b/homeassistant/components/google_sheets/translations/de.json new file mode 100644 index 00000000000..a8c198f5b29 --- /dev/null +++ b/homeassistant/components/google_sheets/translations/de.json @@ -0,0 +1,31 @@ +{ + "application_credentials": { + "description": "Folge den [Anweisungen]({more_info_url}) f\u00fcr den [OAuth-Zustimmungsbildschirm]({oauth_consent_url}), um dem Home Assistant Zugriff auf deine Google Sheets zu geben. Du musst auch Anwendungsnachweise erstellen, die mit deinem Konto verkn\u00fcpft sind:\n1. Gehe zu [Anmeldeinformationen]({oauth_creds_url}) und klicke auf **Anmeldeinformationen erstellen**.\n1. W\u00e4hle aus der Dropdown-Liste **OAuth-Client-ID**.\n1. W\u00e4hle **Webanwendung** f\u00fcr den Anwendungstyp.\n\n" + }, + "config": { + "abort": { + "already_configured": "Konto wurde bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "cannot_connect": "Verbindung fehlgeschlagen", + "create_spreadsheet_failure": "Fehler beim Erstellen der Tabelle, siehe Fehlerprotokoll f\u00fcr Details", + "invalid_access_token": "Ung\u00fcltiger Zugriffs-Token", + "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen.", + "oauth_error": "Ung\u00fcltige Token-Daten empfangen.", + "open_spreadsheet_failure": "Fehler beim \u00d6ffnen der Tabelle, siehe Fehlerprotokoll f\u00fcr Details", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich", + "timeout_connect": "Zeit\u00fcberschreitung beim Verbindungsaufbau", + "unknown": "Unerwarteter Fehler" + }, + "create_entry": { + "default": "Erfolgreich authentifiziert und Tabelle erstellt unter: {url}" + }, + "step": { + "auth": { + "title": "Google-Konto verkn\u00fcpfen" + }, + "pick_implementation": { + "title": "W\u00e4hle die Authentifizierungsmethode" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_sheets/translations/es.json b/homeassistant/components/google_sheets/translations/es.json new file mode 100644 index 00000000000..b538396dfb2 --- /dev/null +++ b/homeassistant/components/google_sheets/translations/es.json @@ -0,0 +1,31 @@ +{ + "application_credentials": { + "description": "Sigue las [instrucciones]({more_info_url}) para la [pantalla de consentimiento de OAuth]({oauth_consent_url}) para otorgarle a Home Assistant acceso a tus hojas de c\u00e1lculo de Google. Tambi\u00e9n tienes que crear credenciales de aplicaci\u00f3n vinculadas a tu cuenta:\n1. Ve a [Credenciales]({oauth_creds_url}) y haz clic en **Crear credenciales**.\n1. En la lista desplegable, selecciona **ID de cliente de OAuth**.\n1. Selecciona **Aplicaci\u00f3n web** para el Tipo de aplicaci\u00f3n.\n\n" + }, + "config": { + "abort": { + "already_configured": "La cuenta ya est\u00e1 configurada", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", + "cannot_connect": "No se pudo conectar", + "create_spreadsheet_failure": "Error al crear la hoja de c\u00e1lculo, consulta el registro de errores para obtener m\u00e1s detalles.", + "invalid_access_token": "Token de acceso no v\u00e1lido", + "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", + "oauth_error": "Se han recibido datos de token no v\u00e1lidos.", + "open_spreadsheet_failure": "Error al abrir la hoja de c\u00e1lculo, consulta el registro de errores para obtener m\u00e1s detalles", + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente", + "timeout_connect": "Tiempo de espera agotado para establecer la conexi\u00f3n", + "unknown": "Error inesperado" + }, + "create_entry": { + "default": "Autenticado con \u00e9xito y hoja de c\u00e1lculo creada en: {url}" + }, + "step": { + "auth": { + "title": "Vincular cuenta de Google" + }, + "pick_implementation": { + "title": "Selecciona el m\u00e9todo de autenticaci\u00f3n" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_sheets/translations/id.json b/homeassistant/components/google_sheets/translations/id.json new file mode 100644 index 00000000000..474fa65b005 --- /dev/null +++ b/homeassistant/components/google_sheets/translations/id.json @@ -0,0 +1,31 @@ +{ + "application_credentials": { + "description": "Ikuti [instruksi]({more_info_url}) untuk [layar persetujuan OAuth]({oauth_consent_url}) untuk memberikan akses Home Assistant ke Google Spreadsheet Anda. Anda juga perlu membuat Kredensial Aplikasi yang ditautkan ke akun Anda:\n1. Buka [Kredensial]({oauth_creds_url}) dan klik **Buat Kredensial**.\n1. Dari daftar drop-down pilih **OAuth client ID**.\n1. Pilih **Aplikasi web** untuk Jenis Aplikasi.\n\n" + }, + "config": { + "abort": { + "already_configured": "Akun sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "cannot_connect": "Gagal terhubung", + "create_spreadsheet_failure": "Kesalahan saat membuat spreadsheet, lihat log kesalahan untuk detailnya", + "invalid_access_token": "Token akses tidak valid", + "missing_configuration": "Komponen tidak dikonfigurasi. Ikuti petunjuk dalam dokumentasi.", + "oauth_error": "Menerima respons token yang tidak valid.", + "open_spreadsheet_failure": "Kesalahan saat membuka spreadsheet, lihat log kesalahan untuk detailnya", + "reauth_successful": "Autentikasi ulang berhasil", + "timeout_connect": "Tenggang waktu membuat koneksi habis", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "create_entry": { + "default": "Berhasil mengautentikasi dan spreadsheet dibuat di: {url}" + }, + "step": { + "auth": { + "title": "Tautkan Akun Google" + }, + "pick_implementation": { + "title": "Pilih Metode Autentikasi" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_sheets/translations/it.json b/homeassistant/components/google_sheets/translations/it.json new file mode 100644 index 00000000000..6d8f315a84a --- /dev/null +++ b/homeassistant/components/google_sheets/translations/it.json @@ -0,0 +1,31 @@ +{ + "application_credentials": { + "description": "Segui le [istruzioni]({more_info_url}) per la [schermata di consenso OAuth]({oauth_consent_url}) per consentire a Home Assistant di accedere ai tuoi Fogli Google. Devi anche creare le Credenziali dell'Applicazione collegate al tuo account:\n1. Vai a [Credenziali]({oauth_creds_url}) e fai clic su **Crea credenziali**.\n1. Dall'elenco a discesa seleziona **ID client OAuth**.\n1. Seleziona **Applicazione Web** per il Tipo di applicazione.\n\n" + }, + "config": { + "abort": { + "already_configured": "L'account \u00e8 gi\u00e0 configurato", + "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", + "cannot_connect": "Impossibile connettersi", + "create_spreadsheet_failure": "Errore durante la creazione del foglio di calcolo, vedere il registro degli errori per i dettagli", + "invalid_access_token": "Token di accesso non valido", + "missing_configuration": "Il componente non \u00e8 configurato. Segui la documentazione.", + "oauth_error": "Ricevuti dati token non validi.", + "open_spreadsheet_failure": "Errore durante l'apertura del foglio di calcolo, vedere il registro degli errori per i dettagli", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente", + "timeout_connect": "Tempo scaduto per stabile la connessione.", + "unknown": "Errore imprevisto" + }, + "create_entry": { + "default": "Autenticato con successo e foglio di calcolo creato a: {url}" + }, + "step": { + "auth": { + "title": "Collega l'account Google" + }, + "pick_implementation": { + "title": "Scegli il metodo di autenticazione" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_sheets/translations/no.json b/homeassistant/components/google_sheets/translations/no.json new file mode 100644 index 00000000000..ad70662b3e2 --- /dev/null +++ b/homeassistant/components/google_sheets/translations/no.json @@ -0,0 +1,31 @@ +{ + "application_credentials": { + "description": "F\u00f8lg [instruksjonene]({more_info_url}) for [OAuth-samtykkeskjermen]({oauth_consent_url}) for \u00e5 gi Home Assistant tilgang til Google Regneark. Du m\u00e5 ogs\u00e5 opprette programlegitimasjon som er koblet til kontoen din:\n1. G\u00e5 til [Legitimasjon]({oauth_creds_url}) og klikk **Opprett legitimasjon**.\n1. Velg **OAuth-klient-ID** fra rullegardinlisten.\n1. Velg **Webprogram** for applikasjonstypen.\n\n" + }, + "config": { + "abort": { + "already_configured": "Kontoen er allerede konfigurert", + "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", + "cannot_connect": "Tilkobling mislyktes", + "create_spreadsheet_failure": "Feil under oppretting av regneark, se feillogg for detaljer", + "invalid_access_token": "Ugyldig tilgangstoken", + "missing_configuration": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen", + "oauth_error": "Mottatt ugyldige token data.", + "open_spreadsheet_failure": "Feil under \u00e5pning av regnearket, se feillogg for detaljer", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", + "timeout_connect": "Tidsavbrudd oppretter forbindelse", + "unknown": "Uventet feil" + }, + "create_entry": { + "default": "Vellykket autentisert og regneark opprettet p\u00e5: {url}" + }, + "step": { + "auth": { + "title": "Koble til Google-kontoen" + }, + "pick_implementation": { + "title": "Velg godkjenningsmetode" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_sheets/translations/pl.json b/homeassistant/components/google_sheets/translations/pl.json new file mode 100644 index 00000000000..c976b9f1910 --- /dev/null +++ b/homeassistant/components/google_sheets/translations/pl.json @@ -0,0 +1,31 @@ +{ + "application_credentials": { + "description": "Post\u0119puj zgodnie z [instrukcjami]({more_info_url}) na [ekran akceptacji OAuth]({oauth_consent_url}), aby przyzna\u0107 Home Assistantowi dost\u0119p do Arkuszy Google. Musisz r\u00f3wnie\u017c utworzy\u0107 po\u015bwiadczenia aplikacji powi\u0105zane z Twoim kontem:\n1. Przejd\u017a do [Po\u015bwiadczenia]({oauth_creds_url}) i kliknij **Utw\u00f3rz po\u015bwiadczenia**.\n2. Z listy rozwijanej wybierz **Identyfikator klienta OAuth**.\n3. Wybierz **Aplikacja internetowa** jako Typ aplikacji. \n\n" + }, + "config": { + "abort": { + "already_configured": "Konto jest ju\u017c skonfigurowane", + "already_in_progress": "Konfiguracja jest ju\u017c w toku", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "create_spreadsheet_failure": "B\u0142\u0105d podczas tworzenia arkusza kalkulacyjnego, szczeg\u00f3\u0142y b\u0142\u0119du znajdziesz w logach", + "invalid_access_token": "Niepoprawny token dost\u0119pu", + "missing_configuration": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105.", + "oauth_error": "Otrzymano nieprawid\u0142owe dane tokena.", + "open_spreadsheet_failure": "B\u0142\u0105d podczas otwierania arkusza kalkulacyjnego, szczeg\u00f3\u0142y b\u0142\u0119du znajdziesz w logach", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119", + "timeout_connect": "Limit czasu na nawi\u0105zanie po\u0142\u0105czenia", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "create_entry": { + "default": "Pomy\u015blnie uwierzytelniono i utworzono arkusz kalkulacyjny pod adresem: {url}" + }, + "step": { + "auth": { + "title": "Po\u0142\u0105czenie z kontem Google" + }, + "pick_implementation": { + "title": "Wybierz metod\u0119 uwierzytelniania" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_sheets/translations/pt.json b/homeassistant/components/google_sheets/translations/pt.json new file mode 100644 index 00000000000..60c19d18dc2 --- /dev/null +++ b/homeassistant/components/google_sheets/translations/pt.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "open_spreadsheet_failure": "Erro ao abrir a planilha, veja o log de erros para detalhes" + }, + "create_entry": { + "default": "Autentica\u00e7\u00e3o com sucesso e planilha criada em: {url}" + }, + "step": { + "auth": { + "title": "Vincular Conta do Google" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_sheets/translations/zh-Hant.json b/homeassistant/components/google_sheets/translations/zh-Hant.json new file mode 100644 index 00000000000..29ca87347e8 --- /dev/null +++ b/homeassistant/components/google_sheets/translations/zh-Hant.json @@ -0,0 +1,31 @@ +{ + "application_credentials": { + "description": "\u8ddf\u96a8[\u8aaa\u660e]({more_info_url})\u4ee5\u8a2d\u5b9a\u81f3 [OAuth \u540c\u610f\u756b\u9762]({oauth_consent_url})\u3001\u4f9b Home Assistant \u5b58\u53d6\u60a8\u7684 Google \u8868\u683c\u3002\u540c\u6642\u9700\u8981\u65b0\u589e\u9023\u7d50\u81f3\u5e33\u865f\u7684\u61c9\u7528\u7a0b\u5f0f\u6191\u8b49\uff1a\n1. \u700f\u89bd\u81f3 [\u6191\u8b49]({oauth_creds_url}) \u9801\u9762\u4e26\u9ede\u9078 **\u5efa\u7acb\u6191\u8b49**\u3002\n1. \u7531\u4e0b\u62c9\u9078\u55ae\u4e2d\u9078\u64c7 **OAuth \u7528\u6236\u7aef ID**\u3002\n1. \u61c9\u7528\u7a0b\u5f0f\u985e\u578b\u5247\u9078\u64c7 **Web \u61c9\u7528\u7a0b\u5f0f**\u3002\n\n" + }, + "config": { + "abort": { + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "create_spreadsheet_failure": "\u5efa\u7acb\u8868\u683c\u6642\u767c\u751f\u932f\u8aa4\u3001\u8acb\u53c3\u8003\u932f\u8aa4\u65e5\u8a8c\u4ee5\u7372\u5f97\u8a73\u7d30\u8cc7\u6599", + "invalid_access_token": "\u5b58\u53d6\u6b0a\u6756\u7121\u6548", + "missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002", + "oauth_error": "\u6536\u5230\u7121\u6548\u7684\u6b0a\u6756\u8cc7\u6599\u3002", + "open_spreadsheet_failure": "\u958b\u555f\u8868\u683c\u6642\u767c\u751f\u932f\u8aa4\u3001\u8acb\u53c3\u8003\u932f\u8aa4\u65e5\u8a8c\u4ee5\u7372\u5f97\u8a73\u7d30\u8cc7\u6599", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f", + "timeout_connect": "\u5efa\u7acb\u9023\u7dda\u903e\u6642", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "create_entry": { + "default": "\u5df2\u6210\u529f\u8a8d\u8b49\u4e26\u65bc\u9023\u7d50\u4f4d\u7f6e\u5efa\u7acb\u8868\u683c\uff1a{url}" + }, + "step": { + "auth": { + "title": "\u9023\u7d50 Google \u5e33\u865f" + }, + "pick_implementation": { + "title": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/govee_ble/translations/bg.json b/homeassistant/components/govee_ble/translations/bg.json new file mode 100644 index 00000000000..a61dac839ad --- /dev/null +++ b/homeassistant/components/govee_ble/translations/bg.json @@ -0,0 +1,20 @@ +{ + "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 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\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/hassio/translations/bg.json b/homeassistant/components/hassio/translations/bg.json index a5581901d78..68fcf5f343e 100644 --- a/homeassistant/components/hassio/translations/bg.json +++ b/homeassistant/components/hassio/translations/bg.json @@ -5,6 +5,7 @@ "disk_total": "\u0414\u0438\u0441\u043a \u043e\u0431\u0449\u043e", "disk_used": "\u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u043d \u0434\u0438\u0441\u043a", "docker_version": "\u0412\u0435\u0440\u0441\u0438\u044f \u043d\u0430 Docker", + "host_os": "\u041e\u043f\u0435\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u0430 \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u043d\u0430 \u0445\u043e\u0441\u0442\u0430", "installed_addons": "\u0418\u043d\u0441\u0442\u0430\u043b\u0438\u0440\u0430\u043d\u0438 \u0434\u043e\u0431\u0430\u0432\u043a\u0438", "supervisor_version": "\u0412\u0435\u0440\u0441\u0438\u044f \u043d\u0430 Supervisor", "update_channel": "\u041a\u0430\u043d\u0430\u043b \u0437\u0430 \u0430\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u043d\u0435" diff --git a/homeassistant/components/homeassistant/translations/bg.json b/homeassistant/components/homeassistant/translations/bg.json index dab7fd6426a..822b2534c40 100644 --- a/homeassistant/components/homeassistant/translations/bg.json +++ b/homeassistant/components/homeassistant/translations/bg.json @@ -5,6 +5,7 @@ "docker": "Docker", "hassio": "Supervisor", "installation_type": "\u0422\u0438\u043f \u0438\u043d\u0441\u0442\u0430\u043b\u0430\u0446\u0438\u044f", + "os_name": "\u0424\u0430\u043c\u0438\u043b\u0438\u044f \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u0438 \u0441\u0438\u0441\u0442\u0435\u043c\u0438", "os_version": "\u0412\u0435\u0440\u0441\u0438\u044f \u043d\u0430 \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u0430\u0442\u0430 \u0441\u0438\u0441\u0442\u0435\u043c\u0430", "python_version": "\u0412\u0435\u0440\u0441\u0438\u044f \u043d\u0430 Python", "timezone": "\u0427\u0430\u0441\u043e\u0432\u0430 \u0437\u043e\u043d\u0430", diff --git a/homeassistant/components/homeassistant_alerts/translations/bg.json b/homeassistant/components/homeassistant_alerts/translations/bg.json new file mode 100644 index 00000000000..33cafb8bba3 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/translations/bg.json @@ -0,0 +1,8 @@ +{ + "issues": { + "alert": { + "description": "{description}", + "title": "{title}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/translations/pl.json b/homeassistant/components/hue/translations/pl.json index abfb6fa2a05..ff8ab182dd4 100644 --- a/homeassistant/components/hue/translations/pl.json +++ b/homeassistant/components/hue/translations/pl.json @@ -44,6 +44,8 @@ "button_2": "drugi", "button_3": "trzeci", "button_4": "czwarty", + "clock_wise": "obr\u00f3t w prawo", + "counter_clock_wise": "obr\u00f3t w lewo", "dim_down": "zmniejszenie jasno\u015bci", "dim_up": "zwi\u0119kszenie jasno\u015bci", "double_buttons_1_3": "pierwszy i trzeci", @@ -61,7 +63,8 @@ "remote_double_button_long_press": "oba przyciski \"{subtype}\" zostan\u0105 zwolnione po d\u0142ugim naci\u015bni\u0119ciu", "remote_double_button_short_press": "oba przyciski \"{subtype}\" zostan\u0105 zwolnione", "repeat": "przycisk \"{subtype}\" zostanie przytrzymany", - "short_release": "przycisk \"{subtype}\" zostanie zwolniony po kr\u00f3tkim naci\u015bni\u0119ciu" + "short_release": "przycisk \"{subtype}\" zostanie zwolniony po kr\u00f3tkim naci\u015bni\u0119ciu", + "start": "\"{subtype}\" zostanie lekko naci\u015bni\u0119ty" } }, "options": { diff --git a/homeassistant/components/icloud/translations/pl.json b/homeassistant/components/icloud/translations/pl.json index e111518710b..a726cd5a78d 100644 --- a/homeassistant/components/icloud/translations/pl.json +++ b/homeassistant/components/icloud/translations/pl.json @@ -18,6 +18,13 @@ "description": "Twoje poprzednio wprowadzone has\u0142o dla {username} ju\u017c nie dzia\u0142a. Zaktualizuj swoje has\u0142o, aby nadal korzysta\u0107 z tej integracji.", "title": "Ponownie uwierzytelnij integracj\u0119" }, + "reauth_confirm": { + "data": { + "password": "Has\u0142o" + }, + "description": "Twoje poprzednio wprowadzone has\u0142o dla {username} ju\u017c nie dzia\u0142a. Zaktualizuj swoje has\u0142o, aby nadal korzysta\u0107 z tej integracji.", + "title": "Ponownie uwierzytelnij integracj\u0119" + }, "trusted_device": { "data": { "trusted_device": "Zaufane urz\u0105dzenie" diff --git a/homeassistant/components/inkbird/translations/bg.json b/homeassistant/components/inkbird/translations/bg.json new file mode 100644 index 00000000000..a61dac839ad --- /dev/null +++ b/homeassistant/components/inkbird/translations/bg.json @@ -0,0 +1,20 @@ +{ + "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 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\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/justnimbus/translations/bg.json b/homeassistant/components/justnimbus/translations/bg.json new file mode 100644 index 00000000000..9c8ae1484d5 --- /dev/null +++ b/homeassistant/components/justnimbus/translations/bg.json @@ -0,0 +1,12 @@ +{ + "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", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lacrosse_view/translations/bg.json b/homeassistant/components/lacrosse_view/translations/bg.json index c0ccf23f5b5..652dea38fcc 100644 --- a/homeassistant/components/lacrosse_view/translations/bg.json +++ b/homeassistant/components/lacrosse_view/translations/bg.json @@ -1,7 +1,20 @@ { "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\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\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" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/lametric/translations/bg.json b/homeassistant/components/lametric/translations/bg.json index 05b1a3f63b7..86a81cc31e9 100644 --- a/homeassistant/components/lametric/translations/bg.json +++ b/homeassistant/components/lametric/translations/bg.json @@ -8,12 +8,26 @@ "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "step": { + "choice_enter_manual_or_fetch_cloud": { + "menu_options": { + "manual_entry": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u0440\u044a\u0447\u043d\u043e", + "pick_implementation": "\u0418\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u0430\u043d\u0435 \u043e\u0442 LaMetric.com (\u043f\u0440\u0435\u043f\u043e\u0440\u044a\u0447\u0438\u0442\u0435\u043b\u043d\u043e)" + } + }, "manual_entry": { "data": { "api_key": "API \u043a\u043b\u044e\u0447", "host": "\u0425\u043e\u0441\u0442" } + }, + "pick_implementation": { + "title": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u043c\u0435\u0442\u043e\u0434 \u0437\u0430 \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" } } + }, + "issues": { + "manual_migration": { + "title": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u0430 \u0435 \u0440\u044a\u0447\u043d\u0430 \u043c\u0438\u0433\u0440\u0430\u0446\u0438\u044f \u0437\u0430 LaMetric" + } } } \ No newline at end of file diff --git a/homeassistant/components/lametric/translations/pl.json b/homeassistant/components/lametric/translations/pl.json index 77e50661cae..5cc897a8e66 100644 --- a/homeassistant/components/lametric/translations/pl.json +++ b/homeassistant/components/lametric/translations/pl.json @@ -3,6 +3,10 @@ "abort": { "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji", + "invalid_discovery_info": "Otrzymano nieprawid\u0142owe informacje wykrywania.", + "link_local_address": "Po\u0142\u0105czenie lokalnego adresu nie jest obs\u0142ugiwane", + "missing_configuration": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105.", + "no_devices": "Autoryzowany u\u017cytkownik nie posiada urz\u0105dze\u0144 LaMetric", "no_url_available": "Brak dost\u0119pnego adresu URL. Aby uzyska\u0107 informacje na temat tego b\u0142\u0119du, [sprawd\u017a sekcj\u0119 pomocy] ({docs_url})" }, "error": { @@ -10,15 +14,37 @@ "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { + "choice_enter_manual_or_fetch_cloud": { + "description": "Urz\u0105dzenie LaMetric mo\u017cna skonfigurowa\u0107 w Home Assistant na dwa r\u00f3\u017cne sposoby. \n\nMo\u017cesz samodzielnie wprowadzi\u0107 wszystkie informacje o urz\u0105dzeniu i tokeny API lub Home Assistant mo\u017ce je zaimportowa\u0107 z Twojego konta LaMetric.com.", + "menu_options": { + "manual_entry": "Wprowad\u017a r\u0119cznie", + "pick_implementation": "Importuj z LaMetric.com (zalecane)" + } + }, "manual_entry": { "data": { "api_key": "Klucz API", "host": "Nazwa hosta lub adres IP" + }, + "data_description": { + "api_key": "Ten klucz API znajdziesz na [stronie urz\u0105dze\u0144 na swoim koncie programisty LaMetric](https://developer.lametric.com/user/devices).", + "host": "Adres IP lub nazwa hosta LaMetric TIME w Twojej sieci." } }, "pick_implementation": { "title": "Wybierz metod\u0119 uwierzytelniania" + }, + "user_cloud_select_device": { + "data": { + "device": "Wybierz urz\u0105dzenie LaMetric do dodania" + } } } + }, + "issues": { + "manual_migration": { + "description": "Integracja LaMetric zosta\u0142a zmodernizowana: jest teraz konfigurowana za pomoc\u0105 interfejsu u\u017cytkownika, a komunikacja odbywa si\u0119 lokalnie. \n\nNiestety nie ma mo\u017cliwo\u015bci automatycznej migracji i dlatego wymaga si\u0119 ponownej konfiguracji LaMetric za pomoc\u0105 Home Assistanta. Zapoznaj si\u0119 z dokumentacj\u0105 integracji Home Assistant LaMetric, aby dowiedzie\u0107 si\u0119, jak to skonfigurowa\u0107. \n\nUsu\u0144 konfiguracj\u0119 LaMetric YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Wymagana jest r\u0119czna migracja dla LaMetric" + } } } \ No newline at end of file diff --git a/homeassistant/components/landisgyr_heat_meter/translations/pl.json b/homeassistant/components/landisgyr_heat_meter/translations/pl.json index 8dcc31a5a11..3478a5c0c2b 100644 --- a/homeassistant/components/landisgyr_heat_meter/translations/pl.json +++ b/homeassistant/components/landisgyr_heat_meter/translations/pl.json @@ -12,6 +12,11 @@ "data": { "device": "\u015acie\u017cka urz\u0105dzenia USB" } + }, + "user": { + "data": { + "device": "Wybierz urz\u0105dzenie" + } } } } diff --git a/homeassistant/components/led_ble/translations/bg.json b/homeassistant/components/led_ble/translations/bg.json index d264cc05431..58e74997565 100644 --- a/homeassistant/components/led_ble/translations/bg.json +++ b/homeassistant/components/led_ble/translations/bg.json @@ -2,6 +2,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", + "no_devices_found": "\u041d\u044f\u043c\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\u0430", + "no_unconfigured_devices": "\u041d\u0435 \u0441\u0430 \u043e\u0442\u043a\u0440\u0438\u0442\u0438 \u043d\u0435\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430.", "not_supported": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u043d\u0435 \u0441\u0435 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430" }, "error": { diff --git a/homeassistant/components/led_ble/translations/pl.json b/homeassistant/components/led_ble/translations/pl.json index b59a7e3bc02..44100e63777 100644 --- a/homeassistant/components/led_ble/translations/pl.json +++ b/homeassistant/components/led_ble/translations/pl.json @@ -4,12 +4,20 @@ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "already_in_progress": "Konfiguracja jest ju\u017c w toku", "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci", + "no_unconfigured_devices": "Nie znaleziono nieskonfigurowanych urz\u0105dze\u0144.", "not_supported": "Urz\u0105dzenie nie jest obs\u0142ugiwane" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "unknown": "Nieoczekiwany b\u0142\u0105d" }, - "flow_title": "{name}" + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "Adres Bluetooth" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/lifx/translations/bg.json b/homeassistant/components/lifx/translations/bg.json index e7ce46d836e..056e965f723 100644 --- a/homeassistant/components/lifx/translations/bg.json +++ b/homeassistant/components/lifx/translations/bg.json @@ -1,12 +1,30 @@ { "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": "\u0412 \u043c\u0440\u0435\u0436\u0430\u0442\u0430 \u043d\u044f\u043c\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 LIFX \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430.", "single_instance_allowed": "\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 \u043d\u0430 LIFX." }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "flow_title": "{label} ({host}) {serial}", "step": { "confirm": { "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 LIFX?" + }, + "discovery_confirm": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 {label} ({host}) {serial}?" + }, + "pick_device": { + "data": { + "device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + } + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } } } } diff --git a/homeassistant/components/litterrobot/translations/pl.json b/homeassistant/components/litterrobot/translations/pl.json index c0a83866936..306acce8743 100644 --- a/homeassistant/components/litterrobot/translations/pl.json +++ b/homeassistant/components/litterrobot/translations/pl.json @@ -14,6 +14,7 @@ "data": { "password": "Has\u0142o" }, + "description": "Zaktualizuj has\u0142o dla u\u017cytkownika {username}", "title": "Ponownie uwierzytelnij integracj\u0119" }, "user": { diff --git a/homeassistant/components/melnor/translations/pl.json b/homeassistant/components/melnor/translations/pl.json new file mode 100644 index 00000000000..707f4d67873 --- /dev/null +++ b/homeassistant/components/melnor/translations/pl.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "W pobli\u017cu nie ma \u017cadnych urz\u0105dze\u0144 Melnor Bluetooth." + }, + "step": { + "bluetooth_confirm": { + "description": "Czy chcesz doda\u0107 zaw\u00f3r Melnor Bluetooth o nazwie `{name}` do Home Assistanta?", + "title": "Wykryty zaw\u00f3r Melnor Bluetooth" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moat/translations/bg.json b/homeassistant/components/moat/translations/bg.json new file mode 100644 index 00000000000..a61dac839ad --- /dev/null +++ b/homeassistant/components/moat/translations/bg.json @@ -0,0 +1,20 @@ +{ + "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 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\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/mqtt/translations/pl.json b/homeassistant/components/mqtt/translations/pl.json index 8b57c465af1..dad798296a1 100644 --- a/homeassistant/components/mqtt/translations/pl.json +++ b/homeassistant/components/mqtt/translations/pl.json @@ -49,6 +49,12 @@ "button_triple_press": "przycisk \"{subtype}\" zostanie trzykrotnie naci\u015bni\u0119ty" } }, + "issues": { + "deprecated_yaml": { + "description": "Znaleziono r\u0119cznie skonfigurowane platformy MQTT z kluczem `{platform}`.\n\nAby rozwi\u0105za\u0107 ten problem, przenie\u015b konfiguracj\u0119 do klucza integracji `mqtt` i uruchom ponownie Home Assistant. Zobacz [dokumentacj\u0119]( {more_info_url} ), aby uzyska\u0107 wi\u0119cej informacji.", + "title": "Twoja r\u0119cznie skonfigurowana platforma MQTT {platform} wymaga uwagi" + } + }, "options": { "error": { "bad_birth": "Nieprawid\u0142owy temat \"birth\"", diff --git a/homeassistant/components/mysensors/translations/bg.json b/homeassistant/components/mysensors/translations/bg.json index 8abf7bbd1fa..f5422095553 100644 --- a/homeassistant/components/mysensors/translations/bg.json +++ b/homeassistant/components/mysensors/translations/bg.json @@ -4,6 +4,7 @@ "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "invalid_port": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d \u043d\u043e\u043c\u0435\u0440 \u043d\u0430 \u043f\u043e\u0440\u0442", "invalid_serial": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d \u0441\u0435\u0440\u0438\u0435\u043d \u043f\u043e\u0440\u0442", + "mqtt_required": "MQTT \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0435 \u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430", "port_out_of_range": "\u041d\u043e\u043c\u0435\u0440\u044a\u0442 \u043d\u0430 \u043f\u043e\u0440\u0442\u0430 \u0442\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u0431\u044a\u0434\u0435 \u043d\u0430\u0439-\u043c\u0430\u043b\u043a\u043e 1 \u0438 \u043d\u0430\u0439-\u043c\u043d\u043e\u0433\u043e 65535", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, @@ -27,6 +28,14 @@ "device": "IP \u0430\u0434\u0440\u0435\u0441 \u043d\u0430 \u0448\u043b\u044e\u0437\u0430", "tcp_port": "\u043f\u043e\u0440\u0442" } + }, + "select_gateway_type": { + "description": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u043a\u043e\u0439 \u0448\u043b\u044e\u0437 \u0434\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0442\u0435.", + "menu_options": { + "gw_mqtt": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 MQTT \u0448\u043b\u044e\u0437", + "gw_serial": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0441\u0435\u0440\u0438\u0435\u043d \u0448\u043b\u044e\u0437", + "gw_tcp": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 TCP \u0448\u043b\u044e\u0437" + } } } } diff --git a/homeassistant/components/nobo_hub/translations/bg.json b/homeassistant/components/nobo_hub/translations/bg.json index ed6e5d4bba2..f7372a7a5a7 100644 --- a/homeassistant/components/nobo_hub/translations/bg.json +++ b/homeassistant/components/nobo_hub/translations/bg.json @@ -14,6 +14,11 @@ "ip_address": "IP \u0430\u0434\u0440\u0435\u0441", "serial": "\u0421\u0435\u0440\u0438\u0435\u043d \u043d\u043e\u043c\u0435\u0440 (12 \u0446\u0438\u0444\u0440\u0438)" } + }, + "selected": { + "data": { + "serial_suffix": "\u0421\u0443\u0444\u0438\u043a\u0441 \u043d\u0430 \u0441\u0435\u0440\u0438\u0439\u043d\u0438\u044f \u043d\u043e\u043c\u0435\u0440 (3 \u0446\u0438\u0444\u0440\u0438)" + } } } } diff --git a/homeassistant/components/nobo_hub/translations/pl.json b/homeassistant/components/nobo_hub/translations/pl.json new file mode 100644 index 00000000000..58964ecdede --- /dev/null +++ b/homeassistant/components/nobo_hub/translations/pl.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia \u2014 sprawd\u017a numer seryjny", + "invalid_ip": "Nieprawid\u0142owy adres IP", + "invalid_serial": "Nieprawid\u0142owy numer seryjny", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "manual": { + "data": { + "ip_address": "Adres IP", + "serial": "Numer seryjny (12 cyfr)" + }, + "description": "Skonfiguruj Nob\u00f8 Ecohub, kt\u00f3ry nie zosta\u0142 wykryty w Twojej sieci lokalnej. Je\u015bli koncentrator znajduje si\u0119 w innej sieci, nadal mo\u017cesz si\u0119 z nim po\u0142\u0105czy\u0107, wprowadzaj\u0105c pe\u0142ny numer seryjny (12 cyfr) i jego adres IP." + }, + "selected": { + "data": { + "serial_suffix": "Sufiks numeru seryjnego (3 cyfry)" + }, + "description": "Konfiguracja {hub}.\n\nAby po\u0142\u0105czy\u0107 si\u0119 z koncentratorem, musisz wprowadzi\u0107 3 ostatnie cyfry numeru seryjnego koncentratora." + }, + "user": { + "data": { + "device": "Wykryte huby" + }, + "description": "Wybierz Nob\u00f8 Ecohub do skonfigurowania." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "override_type": "Typ nadpisania" + }, + "description": "Wybierz typ nadpisania \u201eTeraz\u201d, aby zako\u0144czy\u0107 nadpisywania przy zmianie profilu w nast\u0119pnym tygodniu." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openexchangerates/translations/bg.json b/homeassistant/components/openexchangerates/translations/bg.json new file mode 100644 index 00000000000..6c5c49465c3 --- /dev/null +++ b/homeassistant/components/openexchangerates/translations/bg.json @@ -0,0 +1,22 @@ +{ + "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", + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "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 \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", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447", + "base": "\u041e\u0441\u043d\u043e\u0432\u043d\u0430 \u0432\u0430\u043b\u0443\u0442\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/pl.json b/homeassistant/components/overkiz/translations/pl.json index 7044dc717fd..23065776db4 100644 --- a/homeassistant/components/overkiz/translations/pl.json +++ b/homeassistant/components/overkiz/translations/pl.json @@ -11,7 +11,8 @@ "server_in_maintenance": "Serwer wy\u0142\u0105czony z powodu przerwy technicznej", "too_many_attempts": "Zbyt wiele pr\u00f3b z nieprawid\u0142owym tokenem, konto tymczasowo zablokowane", "too_many_requests": "Zbyt wiele \u017c\u0105da\u0144, spr\u00f3buj ponownie p\u00f3\u017aniej.", - "unknown": "Nieoczekiwany b\u0142\u0105d" + "unknown": "Nieoczekiwany b\u0142\u0105d", + "unknown_user": "Nieznany u\u017cytkownik. Konta Somfy Protect nie s\u0105 obs\u0142ugiwane przez t\u0119 integracj\u0119." }, "flow_title": "Bramka: {gateway_id}", "step": { diff --git a/homeassistant/components/overkiz/translations/sensor.pl.json b/homeassistant/components/overkiz/translations/sensor.pl.json index 440cf4998f8..dd826600f49 100644 --- a/homeassistant/components/overkiz/translations/sensor.pl.json +++ b/homeassistant/components/overkiz/translations/sensor.pl.json @@ -16,7 +16,7 @@ "external_gateway": "bramka zewn\u0119trzna", "local_user": "u\u017cytkownik lokalny", "lsc": "LSC", - "myself": "Ja", + "myself": "ja", "rain": "deszcz", "saac": "SAAC", "security": "bezpiecze\u0144stwo", diff --git a/homeassistant/components/p1_monitor/translations/pl.json b/homeassistant/components/p1_monitor/translations/pl.json index 9ec0f1bcef0..93e2b7a83bd 100644 --- a/homeassistant/components/p1_monitor/translations/pl.json +++ b/homeassistant/components/p1_monitor/translations/pl.json @@ -9,6 +9,9 @@ "host": "Nazwa hosta lub adres IP", "name": "Nazwa" }, + "data_description": { + "host": "Adres IP lub nazwa hosta instalacji monitora P1." + }, "description": "Skonfiguruj P1 Monitor, aby zintegrowa\u0107 go z Home Assistantem." } } diff --git a/homeassistant/components/prusalink/translations/sensor.pl.json b/homeassistant/components/prusalink/translations/sensor.pl.json index a28232a63a1..10f21587c74 100644 --- a/homeassistant/components/prusalink/translations/sensor.pl.json +++ b/homeassistant/components/prusalink/translations/sensor.pl.json @@ -1,11 +1,11 @@ { "state": { "prusalink__printer_state": { - "cancelling": "Anulowanie", - "idle": "Bezczynny", - "paused": "Wstrzymany", - "pausing": "Wstrzymywanie", - "printing": "Drukowanie" + "cancelling": "anulowanie", + "idle": "bezczynny", + "paused": "wstrzymany", + "pausing": "wstrzymywanie", + "printing": "drukowanie" } } } \ No newline at end of file diff --git a/homeassistant/components/pure_energie/translations/pl.json b/homeassistant/components/pure_energie/translations/pl.json index 526326fccc1..891393e1d2f 100644 --- a/homeassistant/components/pure_energie/translations/pl.json +++ b/homeassistant/components/pure_energie/translations/pl.json @@ -12,6 +12,9 @@ "user": { "data": { "host": "Nazwa hosta lub adres IP" + }, + "data_description": { + "host": "Adres IP lub nazwa hosta Pure Energie Meter." } }, "zeroconf_confirm": { diff --git a/homeassistant/components/pushover/translations/pl.json b/homeassistant/components/pushover/translations/pl.json index 01f297f75cc..02195af9229 100644 --- a/homeassistant/components/pushover/translations/pl.json +++ b/homeassistant/components/pushover/translations/pl.json @@ -6,7 +6,8 @@ }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", - "invalid_api_key": "Nieprawid\u0142owy klucz API" + "invalid_api_key": "Nieprawid\u0142owy klucz API", + "invalid_user_key": "Nieprawid\u0142owy klucz u\u017cytkownika" }, "step": { "reauth_confirm": { @@ -18,9 +19,16 @@ "user": { "data": { "api_key": "Klucz API", - "name": "Nazwa" + "name": "Nazwa", + "user_key": "Klucz u\u017cytkownika" } } } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfiguracja Pushover 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 Pushover zostanie usuni\u0119ta" + } } } \ No newline at end of file diff --git a/homeassistant/components/qingping/translations/bg.json b/homeassistant/components/qingping/translations/bg.json index d7b7634c9f7..af9a13197df 100644 --- a/homeassistant/components/qingping/translations/bg.json +++ b/homeassistant/components/qingping/translations/bg.json @@ -2,6 +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", + "no_devices_found": "\u041d\u044f\u043c\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\u0430", "not_supported": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u043d\u0435 \u0441\u0435 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430" }, "flow_title": "{name}", diff --git a/homeassistant/components/risco/translations/pl.json b/homeassistant/components/risco/translations/pl.json index e5523667231..0e4ea5302d7 100644 --- a/homeassistant/components/risco/translations/pl.json +++ b/homeassistant/components/risco/translations/pl.json @@ -28,6 +28,10 @@ "password": "Has\u0142o", "pin": "Kod PIN", "username": "Nazwa u\u017cytkownika" + }, + "menu_options": { + "cloud": "Chmura Risco (zalecane)", + "local": "Lokalny Panel Risco (zaawansowane)" } } } diff --git a/homeassistant/components/sensibo/translations/pl.json b/homeassistant/components/sensibo/translations/pl.json index 830ab3399b9..dc2fdda0065 100644 --- a/homeassistant/components/sensibo/translations/pl.json +++ b/homeassistant/components/sensibo/translations/pl.json @@ -15,11 +15,17 @@ "reauth_confirm": { "data": { "api_key": "Klucz API" + }, + "data_description": { + "api_key": "Post\u0119puj zgodnie z dokumentacj\u0105, aby uzyska\u0107 nowy klucz API." } }, "user": { "data": { "api_key": "Klucz API" + }, + "data_description": { + "api_key": "Post\u0119puj zgodnie z dokumentacj\u0105, aby uzyska\u0107 klucz API." } } } diff --git a/homeassistant/components/sensor/translations/bg.json b/homeassistant/components/sensor/translations/bg.json index b657bdb03b6..f4ea74ca57e 100644 --- a/homeassistant/components/sensor/translations/bg.json +++ b/homeassistant/components/sensor/translations/bg.json @@ -6,6 +6,7 @@ "is_illuminance": "\u0422\u0435\u043a\u0443\u0449\u0430 \u043e\u0441\u0432\u0435\u0442\u0435\u043d\u043e\u0441\u0442 \u043d\u0430 {entity_name}", "is_power": "\u0422\u0435\u043a\u0443\u0449\u0430 \u043c\u043e\u0449\u043d\u043e\u0441\u0442 \u043d\u0430 {entity_name}", "is_pressure": "\u0422\u0435\u043a\u0443\u0449\u043e \u043d\u0430\u043b\u044f\u0433\u0430\u043d\u0435 \u043d\u0430 {entity_name}", + "is_reactive_power": "\u0422\u0435\u043a\u0443\u0449\u0430 \u0440\u0435\u0430\u043a\u0442\u0438\u0432\u043d\u0430 \u043c\u043e\u0449\u043d\u043e\u0441\u0442 \u043d\u0430 {entity_name}", "is_signal_strength": "\u0422\u0435\u043a\u0443\u0449\u0430 \u0441\u0438\u043b\u0430 \u043d\u0430 \u0441\u0438\u0433\u043d\u0430\u043b\u0430 \u043d\u0430 {entity_name}", "is_temperature": "\u0422\u0435\u043a\u0443\u0449\u0430 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 \u043d\u0430 {entity_name}", "is_value": "\u0422\u0435\u043a\u0443\u0449\u0430 \u0441\u0442\u043e\u0439\u043d\u043e\u0441\u0442 \u043d\u0430 {entity_name}" diff --git a/homeassistant/components/sensor/translations/pl.json b/homeassistant/components/sensor/translations/pl.json index b6e93865425..360d7a2509b 100644 --- a/homeassistant/components/sensor/translations/pl.json +++ b/homeassistant/components/sensor/translations/pl.json @@ -11,6 +11,7 @@ "is_gas": "obecny poziom gazu {entity_name}", "is_humidity": "obecna wilgotno\u015b\u0107 {entity_name}", "is_illuminance": "obecne nat\u0119\u017cenie o\u015bwietlenia {entity_name}", + "is_moisture": "obecna wilgotno\u015b\u0107 {entity_name}", "is_nitrogen_dioxide": "obecny poziom st\u0119\u017cenia dwutlenku azotu {entity_name}", "is_nitrogen_monoxide": "obecny poziom st\u0119\u017cenia tlenku azotu {entity_name}", "is_nitrous_oxide": "obecny poziom st\u0119\u017cenia podtlenku azotu {entity_name}", @@ -40,6 +41,7 @@ "gas": "{entity_name} wykryje zmian\u0119 poziomu gazu", "humidity": "zmieni si\u0119 wilgotno\u015b\u0107 {entity_name}", "illuminance": "zmieni si\u0119 nat\u0119\u017cenie o\u015bwietlenia {entity_name}", + "moisture": "zmieni si\u0119 wilgotno\u015b\u0107 {entity_name}", "nitrogen_dioxide": "zmieni si\u0119 st\u0119\u017cenie dwutlenku azotu w {entity_name}", "nitrogen_monoxide": "{entity_name} wykryje zmian\u0119 st\u0119\u017cenia tlenku azotu", "nitrous_oxide": "{entity_name} wykryje zmian\u0119 st\u0119\u017cenia podtlenku azotu", diff --git a/homeassistant/components/sensorpro/translations/pl.json b/homeassistant/components/sensorpro/translations/pl.json new file mode 100644 index 00000000000..4715905a2e9 --- /dev/null +++ b/homeassistant/components/sensorpro/translations/pl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "already_in_progress": "Konfiguracja jest ju\u017c w toku", + "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci", + "not_supported": "Urz\u0105dzenie nie jest obs\u0142ugiwane" + }, + "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/sensorpush/translations/bg.json b/homeassistant/components/sensorpush/translations/bg.json new file mode 100644 index 00000000000..a61dac839ad --- /dev/null +++ b/homeassistant/components/sensorpush/translations/bg.json @@ -0,0 +1,20 @@ +{ + "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 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\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/simplisafe/translations/bg.json b/homeassistant/components/simplisafe/translations/bg.json index da70697442c..d1bb1c2f67f 100644 --- a/homeassistant/components/simplisafe/translations/bg.json +++ b/homeassistant/components/simplisafe/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": { + "identifier_exists": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0430\u043d", "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" }, @@ -24,6 +25,7 @@ }, "user": { "data": { + "auth_code": "\u041a\u043e\u0434 \u0437\u0430 \u043e\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f", "password": "\u041f\u0430\u0440\u043e\u043b\u0430", "username": "E-mail \u0430\u0434\u0440\u0435\u0441" } diff --git a/homeassistant/components/skybell/translations/pl.json b/homeassistant/components/skybell/translations/pl.json index 51aac2f85ac..8b5c8dda53c 100644 --- a/homeassistant/components/skybell/translations/pl.json +++ b/homeassistant/components/skybell/translations/pl.json @@ -14,6 +14,7 @@ "data": { "password": "Has\u0142o" }, + "description": "Zaktualizuj has\u0142o dla u\u017cytkownika {email}", "title": "Ponownie uwierzytelnij integracj\u0119" }, "user": { @@ -23,5 +24,11 @@ } } } + }, + "issues": { + "removed_yaml": { + "description": "Konfiguracja Skybell za pomoc\u0105 YAML zosta\u0142a usuni\u0119ta. \n\nTwoja istniej\u0105ca konfiguracja YAML nie jest u\u017cywana przez Home Assistant. \n\nUsu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistant, aby rozwi\u0105za\u0107 ten problem.", + "title": "Konfiguracja YAML dla Skybell zosta\u0142a usuni\u0119ta" + } } } \ No newline at end of file diff --git a/homeassistant/components/speedtestdotnet/translations/pl.json b/homeassistant/components/speedtestdotnet/translations/pl.json index b06d7cdd285..237d05ffd9c 100644 --- a/homeassistant/components/speedtestdotnet/translations/pl.json +++ b/homeassistant/components/speedtestdotnet/translations/pl.json @@ -9,6 +9,19 @@ } } }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Zaktualizuj wszystkie automatyzacje lub skrypty korzystaj\u0105ce z tej us\u0142ugi, aby zamiast tego u\u017cywa\u0142y us\u0142ugi \u201ehomeassistant.update_entity\u201d z encj\u0105 docelow\u0105 Speedtest. Nast\u0119pnie kliknij ZATWIERD\u0179 poni\u017cej, aby oznaczy\u0107 ten problem jako rozwi\u0105zany.", + "title": "Us\u0142uga speedtest zostanie usuni\u0119ta" + } + } + }, + "title": "Us\u0142uga speedtest zostanie usuni\u0119ta" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/switchbee/translations/bg.json b/homeassistant/components/switchbee/translations/bg.json new file mode 100644 index 00000000000..d3c0c1a8e77 --- /dev/null +++ b/homeassistant/components/switchbee/translations/bg.json @@ -0,0 +1,21 @@ +{ + "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", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switchbee/translations/id.json b/homeassistant/components/switchbee/translations/id.json new file mode 100644 index 00000000000..71fc2092c6c --- /dev/null +++ b/homeassistant/components/switchbee/translations/id.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Kata Sandi", + "switch_as_light": "Inisialisasi sakelar sebagai entitas lampu", + "username": "Nama Pengguna" + }, + "description": "Siapkan integrasi SwitchBee dengan Home Assistant." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "devices": "Perangkat yang disertakan" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switchbee/translations/it.json b/homeassistant/components/switchbee/translations/it.json new file mode 100644 index 00000000000..ebd7915f049 --- /dev/null +++ b/homeassistant/components/switchbee/translations/it.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Password", + "switch_as_light": "Inizializza gli interruttori come entit\u00e0 luce", + "username": "Nome utente" + }, + "description": "Imposta l'integrazione di SwitchBee con Home Assistant." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "devices": "Dispositivi da includere" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switchbee/translations/no.json b/homeassistant/components/switchbee/translations/no.json new file mode 100644 index 00000000000..22f16d5a328 --- /dev/null +++ b/homeassistant/components/switchbee/translations/no.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "host": "Vert", + "password": "Passord", + "switch_as_light": "Initialiser brytere som lysenheter", + "username": "Brukernavn" + }, + "description": "Sett opp SwitchBee-integrasjon med Home Assistant." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "devices": "Enheter som skal inkluderes" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switchbee/translations/pl.json b/homeassistant/components/switchbee/translations/pl.json new file mode 100644 index 00000000000..f2233b1550c --- /dev/null +++ b/homeassistant/components/switchbee/translations/pl.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP", + "password": "Has\u0142o", + "switch_as_light": "Zainicjuj prze\u0142\u0105czniki jako encje \u015bwiat\u0142a", + "username": "Nazwa u\u017cytkownika" + }, + "description": "Konfiguracja integracji SwitchBee z Home Assistantem." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "devices": "Urz\u0105dzenia do uwzgl\u0119dnienia" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switchbot/translations/bg.json b/homeassistant/components/switchbot/translations/bg.json index 05b3a4459bb..196b5511f77 100644 --- a/homeassistant/components/switchbot/translations/bg.json +++ b/homeassistant/components/switchbot/translations/bg.json @@ -8,8 +8,18 @@ }, "flow_title": "{name}", "step": { + "confirm": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 {name}?" + }, + "password": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + }, + "description": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e {name} \u0438\u0437\u0438\u0441\u043a\u0432\u0430 \u043f\u0430\u0440\u043e\u043b\u0430" + }, "user": { "data": { + "address": "\u0410\u0434\u0440\u0435\u0441 \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e", "mac": "MAC \u0430\u0434\u0440\u0435\u0441 \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e", "name": "\u0418\u043c\u0435", "password": "\u041f\u0430\u0440\u043e\u043b\u0430" diff --git a/homeassistant/components/thermobeacon/translations/bg.json b/homeassistant/components/thermobeacon/translations/bg.json index d7b7634c9f7..af9a13197df 100644 --- a/homeassistant/components/thermobeacon/translations/bg.json +++ b/homeassistant/components/thermobeacon/translations/bg.json @@ -2,6 +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", + "no_devices_found": "\u041d\u044f\u043c\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\u0430", "not_supported": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u043d\u0435 \u0441\u0435 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430" }, "flow_title": "{name}", diff --git a/homeassistant/components/thermobeacon/translations/pl.json b/homeassistant/components/thermobeacon/translations/pl.json index 51168716783..4715905a2e9 100644 --- a/homeassistant/components/thermobeacon/translations/pl.json +++ b/homeassistant/components/thermobeacon/translations/pl.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "already_in_progress": "Konfiguracja jest ju\u017c w toku", - "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci" + "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci", + "not_supported": "Urz\u0105dzenie nie jest obs\u0142ugiwane" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/thermopro/translations/bg.json b/homeassistant/components/thermopro/translations/bg.json index 81695457d85..a61dac839ad 100644 --- a/homeassistant/components/thermopro/translations/bg.json +++ b/homeassistant/components/thermopro/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", + "no_devices_found": "\u041d\u044f\u043c\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\u0430" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/tilt_ble/translations/no.json b/homeassistant/components/tilt_ble/translations/no.json index 9e85034b506..28ec4582177 100644 --- a/homeassistant/components/tilt_ble/translations/no.json +++ b/homeassistant/components/tilt_ble/translations/no.json @@ -1,11 +1,20 @@ { "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", + "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket" + }, "flow_title": "{name}", "step": { + "bluetooth_confirm": { + "description": "Vil du konfigurere {name}?" + }, "user": { "data": { "address": "Enhet" - } + }, + "description": "Velg en enhet du vil konfigurere" } } } diff --git a/homeassistant/components/tilt_ble/translations/pl.json b/homeassistant/components/tilt_ble/translations/pl.json new file mode 100644 index 00000000000..51168716783 --- /dev/null +++ b/homeassistant/components/tilt_ble/translations/pl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "already_in_progress": "Konfiguracja jest ju\u017c w toku", + "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci" + }, + "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/timer/translations/bg.json b/homeassistant/components/timer/translations/bg.json index c471d3eff18..11462e3bb99 100644 --- a/homeassistant/components/timer/translations/bg.json +++ b/homeassistant/components/timer/translations/bg.json @@ -1,7 +1,7 @@ { "state": { "_": { - "active": "\u0430\u043a\u0442\u0438\u0432\u0435\u043d", + "active": "\u0410\u043a\u0442\u0438\u0432\u0435\u043d", "idle": "\u043d\u0435\u0440\u0430\u0431\u043e\u0442\u0435\u0449", "paused": "\u0432 \u043f\u0430\u0443\u0437\u0430" } diff --git a/homeassistant/components/tuya/translations/sensor.pl.json b/homeassistant/components/tuya/translations/sensor.pl.json index 0529ebebba0..74e59c260cd 100644 --- a/homeassistant/components/tuya/translations/sensor.pl.json +++ b/homeassistant/components/tuya/translations/sensor.pl.json @@ -1,10 +1,10 @@ { "state": { "tuya__air_quality": { - "good": "Dobra", - "great": "\u015awietna", - "mild": "Umiarkowana", - "severe": "Z\u0142a" + "good": "dobra", + "great": "\u015bwietna", + "mild": "umiarkowana", + "severe": "z\u0142a" }, "tuya__status": { "boiling_temp": "temperatura wrzenia", diff --git a/homeassistant/components/unifiprotect/translations/pl.json b/homeassistant/components/unifiprotect/translations/pl.json index 1752e40ac3c..a0c4e7eb72c 100644 --- a/homeassistant/components/unifiprotect/translations/pl.json +++ b/homeassistant/components/unifiprotect/translations/pl.json @@ -42,11 +42,15 @@ } }, "options": { + "error": { + "invalid_mac_list": "Musi to by\u0107 lista adres\u00f3w MAC oddzielonych przecinkami" + }, "step": { "init": { "data": { "all_updates": "Metryki w czasie rzeczywistym (UWAGA: Znacznie zwi\u0119ksza u\u017cycie CPU)", "disable_rtsp": "Wy\u0142\u0105cz strumie\u0144 RTSP", + "ignored_devices": "Oddzielona przecinkami lista adres\u00f3w MAC urz\u0105dze\u0144 do zignorowania", "max_media": "Maksymalna liczba zdarze\u0144 do za\u0142adowania dla przegl\u0105darki medi\u00f3w (zwi\u0119ksza u\u017cycie pami\u0119ci RAM)", "override_connection_host": "Zast\u0105p host po\u0142\u0105czenia" }, diff --git a/homeassistant/components/volvooncall/translations/pl.json b/homeassistant/components/volvooncall/translations/pl.json index fc0e5482c42..abf266a4f60 100644 --- a/homeassistant/components/volvooncall/translations/pl.json +++ b/homeassistant/components/volvooncall/translations/pl.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "already_configured": "Konto jest ju\u017c skonfigurowane", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" + }, "error": { "invalid_auth": "Niepoprawne uwierzytelnienie", "unknown": "Nieoczekiwany b\u0142\u0105d" @@ -7,11 +11,19 @@ "step": { "user": { "data": { + "mutable": "Zezwalaj na zdalne uruchamianie / zamykanie / itp.", "password": "Has\u0142o", "region": "Region", + "scandinavian_miles": "U\u017cywaj skandynawskich mil", "username": "Nazwa u\u017cytkownika" } } } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfiguracja platformy Volvo On Call za pomoc\u0105 YAML zostanie usuni\u0119ta w przysz\u0142ej wersji Home Assistant. \n\nTwoja istniej\u0105ca konfiguracja zosta\u0142a automatycznie zaimportowana do interfejsu u\u017cytkownika. Usu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Konfiguracja YAML dla Volvo On Call zostanie usuni\u0119ta" + } } } \ No newline at end of file diff --git a/homeassistant/components/wolflink/translations/sensor.bg.json b/homeassistant/components/wolflink/translations/sensor.bg.json index 6d41058320d..52d381f1b26 100644 --- a/homeassistant/components/wolflink/translations/sensor.bg.json +++ b/homeassistant/components/wolflink/translations/sensor.bg.json @@ -3,6 +3,7 @@ "wolflink__state": { "1_x_warmwasser": "1 x DHW", "auto": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u043d", + "deaktiviert": "\u041d\u0435\u0430\u043a\u0442\u0438\u0432\u0435\u043d", "dhw_prior": "DHWPrior", "gasdruck": "\u041d\u0430\u043b\u044f\u0433\u0430\u043d\u0435 \u043d\u0430 \u0433\u0430\u0437\u0430", "heizung": "\u041e\u0442\u043e\u043f\u043b\u0435\u043d\u0438\u0435", diff --git a/homeassistant/components/wolflink/translations/sensor.pl.json b/homeassistant/components/wolflink/translations/sensor.pl.json index 176f0c49b57..6ac6f2b0f0f 100644 --- a/homeassistant/components/wolflink/translations/sensor.pl.json +++ b/homeassistant/components/wolflink/translations/sensor.pl.json @@ -74,7 +74,7 @@ "test": "test", "tpw": "TPW", "urlaubsmodus": "tryb wakacyjny", - "ventilprufung": "Kontrola zawor\u00f3w", + "ventilprufung": "kontrola zawor\u00f3w", "vorspulen": "p\u0142ukanie wst\u0119pne", "warmwasser": "CWU", "warmwasser_schnellstart": "szybki start CWU", diff --git a/homeassistant/components/xiaomi_ble/translations/bg.json b/homeassistant/components/xiaomi_ble/translations/bg.json new file mode 100644 index 00000000000..70cd9d4fa7f --- /dev/null +++ b/homeassistant/components/xiaomi_ble/translations/bg.json @@ -0,0 +1,21 @@ +{ + "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 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\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 \u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + }, + "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/xiaomi_miio/translations/select.pl.json b/homeassistant/components/xiaomi_miio/translations/select.pl.json index ba5a0ab727f..92a1539b9ce 100644 --- a/homeassistant/components/xiaomi_miio/translations/select.pl.json +++ b/homeassistant/components/xiaomi_miio/translations/select.pl.json @@ -1,9 +1,19 @@ { "state": { + "xiaomi_miio__display_orientation": { + "forward": "Do przodu", + "left": "W lewo", + "right": "W prawo" + }, "xiaomi_miio__led_brightness": { "bright": "jasne", "dim": "ciemne", "off": "wy\u0142\u0105czone" + }, + "xiaomi_miio__ptc_level": { + "high": "Wysoki", + "low": "Niski", + "medium": "\u015aredni" } } } \ No newline at end of file diff --git a/homeassistant/components/yalexs_ble/translations/bg.json b/homeassistant/components/yalexs_ble/translations/bg.json index fce4c934f7e..d849af39077 100644 --- a/homeassistant/components/yalexs_ble/translations/bg.json +++ b/homeassistant/components/yalexs_ble/translations/bg.json @@ -1,5 +1,15 @@ { "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 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\u0430", + "no_unconfigured_devices": "\u041d\u0435 \u0441\u0430 \u043e\u0442\u043a\u0440\u0438\u0442\u0438 \u043d\u0435\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\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", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, "flow_title": "{name}", "step": { "user": { diff --git a/homeassistant/components/zha/translations/bg.json b/homeassistant/components/zha/translations/bg.json index cc58b42b11a..18814ad0a33 100644 --- a/homeassistant/components/zha/translations/bg.json +++ b/homeassistant/components/zha/translations/bg.json @@ -8,6 +8,11 @@ "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" }, "step": { + "choose_formation_strategy": { + "menu_options": { + "reuse_settings": "\u0417\u0430\u043f\u0430\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438\u0442\u0435 \u043d\u0430 \u0440\u0430\u0434\u0438\u043e \u043c\u0440\u0435\u0436\u0430\u0442\u0430" + } + }, "choose_serial_port": { "title": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0441\u0435\u0440\u0438\u0435\u043d \u043f\u043e\u0440\u0442" }, @@ -17,10 +22,18 @@ "confirm_hardware": { "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 {name}?" }, + "manual_pick_radio_type": { + "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", + "title": "\u0422\u0438\u043f \u0440\u0430\u0434\u0438\u043e" + }, "manual_port_config": { "data": { "baudrate": "\u0441\u043a\u043e\u0440\u043e\u0441\u0442 \u043d\u0430 \u043f\u043e\u0440\u0442\u0430" }, + "description": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438\u0442\u0435 \u043d\u0430 \u0441\u0435\u0440\u0438\u0439\u043d\u0438\u044f \u043f\u043e\u0440\u0442", "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043d\u0430 \u0441\u0435\u0440\u0438\u0439\u043d\u0438\u044f \u043f\u043e\u0440\u0442" }, "pick_radio": { @@ -97,15 +110,39 @@ "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" }, + "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": { + "description": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u043c\u0440\u0435\u0436\u043e\u0432\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0437\u0430 \u0432\u0430\u0448\u0435\u0442\u043e \u0440\u0430\u0434\u0438\u043e.", + "menu_options": { + "reuse_settings": "\u0417\u0430\u043f\u0430\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438\u0442\u0435 \u043d\u0430 \u0440\u0430\u0434\u0438\u043e \u043c\u0440\u0435\u0436\u0430\u0442\u0430", + "upload_manual_backup": "\u041a\u0430\u0447\u0432\u0430\u043d\u0435 \u043d\u0430 \u0440\u044a\u0447\u043d\u043e \u0440\u0435\u0437\u0435\u0440\u0432\u043d\u043e \u043a\u043e\u043f\u0438\u0435" + } + }, "choose_serial_port": { + "description": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0441\u0435\u0440\u0438\u0435\u043d \u043f\u043e\u0440\u0442 \u0437\u0430 \u0432\u0430\u0448\u0435\u0442\u043e Zigbee \u0440\u0430\u0434\u0438\u043e", "title": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0441\u0435\u0440\u0438\u0435\u043d \u043f\u043e\u0440\u0442" }, + "init": { + "description": "ZHA \u0449\u0435 \u0431\u044a\u0434\u0435 \u0441\u043f\u0440\u044f\u043d. \u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043f\u0440\u043e\u0434\u044a\u043b\u0436\u0438\u0442\u0435?", + "title": "\u041f\u0440\u0435\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 ZHA" + }, + "manual_pick_radio_type": { + "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", + "title": "\u0422\u0438\u043f \u0440\u0430\u0434\u0438\u043e" + }, "manual_port_config": { "data": { "baudrate": "\u0441\u043a\u043e\u0440\u043e\u0441\u0442 \u043d\u0430 \u043f\u043e\u0440\u0442\u0430" - } + }, + "description": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438\u0442\u0435 \u043d\u0430 \u0441\u0435\u0440\u0438\u0439\u043d\u0438\u044f \u043f\u043e\u0440\u0442", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043d\u0430 \u0441\u0435\u0440\u0438\u0439\u043d\u0438\u044f \u043f\u043e\u0440\u0442" }, "upload_manual_backup": { "data": { diff --git a/homeassistant/components/zha/translations/pl.json b/homeassistant/components/zha/translations/pl.json index 99c519782db..17563a1b8f3 100644 --- a/homeassistant/components/zha/translations/pl.json +++ b/homeassistant/components/zha/translations/pl.json @@ -6,13 +6,64 @@ "usb_probe_failed": "Nie uda\u0142o si\u0119 sondowa\u0107 urz\u0105dzenia USB" }, "error": { - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_backup_json": "Nieprawid\u0142owa kopia zapasowa JSON" }, "flow_title": "{name}", "step": { + "choose_automatic_backup": { + "data": { + "choose_automatic_backup": "Wybierz automatyczn\u0105 kopi\u0119 zapasow\u0105" + }, + "description": "Przywr\u00f3\u0107 ustawienia sieciowe z automatycznej kopii zapasowej", + "title": "Przywracanie z automatycznej kopii zapasowej" + }, + "choose_formation_strategy": { + "description": "Wybierz ustawienia sieciowe radia.", + "menu_options": { + "choose_automatic_backup": "Przywr\u00f3\u0107 z automatycznej kopii zapasowej", + "form_new_network": "Usu\u0144 ustawienia sieciowe i utw\u00f3rz now\u0105 sie\u0107", + "reuse_settings": "Zachowaj ustawienia sieciowe radia", + "upload_manual_backup": "Prze\u015blij r\u0119czn\u0105 kopi\u0119 zapasow\u0105" + }, + "title": "Tworzenie sieci" + }, + "choose_serial_port": { + "data": { + "path": "\u015acie\u017cka urz\u0105dzenia szeregowego" + }, + "description": "Wybierz port szeregowy dla swojego radia Zigbee", + "title": "Wyb\u00f3r portu szeregowego" + }, "confirm": { "description": "Czy chcesz skonfigurowa\u0107 {name}?" }, + "confirm_hardware": { + "description": "Czy chcesz skonfigurowa\u0107 {name}?" + }, + "manual_pick_radio_type": { + "data": { + "radio_type": "Typ radia" + }, + "description": "Wybierz typ radia Zigbee", + "title": "Typ radia" + }, + "manual_port_config": { + "data": { + "baudrate": "pr\u0119dko\u015b\u0107 portu", + "flow_control": "kontrola przep\u0142ywu danych", + "path": "\u015acie\u017cka urz\u0105dzenia szeregowego" + }, + "description": "Wprowad\u017a ustawienia portu szeregowego", + "title": "Ustawienia portu szeregowego" + }, + "maybe_confirm_ezsp_restore": { + "data": { + "overwrite_coordinator_ieee": "Trwa\u0142e zast\u0105pienie radiowego adresu IEEE" + }, + "description": "Twoja kopia zapasowa ma inny adres IEEE ni\u017c twoje radio. Aby sie\u0107 dzia\u0142a\u0142a prawid\u0142owo, nale\u017cy r\u00f3wnie\u017c zmieni\u0107 adres IEEE radia. \n\nTo jest trwa\u0142a operacja.", + "title": "Nadpisanie adresu IEEE radia" + }, "pick_radio": { "data": { "radio_type": "Typ radia" @@ -29,6 +80,13 @@ "description": "Wprowadzanie ustawie\u0144 dla portu", "title": "Ustawienia" }, + "upload_manual_backup": { + "data": { + "uploaded_backup_file": "Prze\u015blij plik" + }, + "description": "Przywr\u00f3\u0107 ustawienia sieciowe z pliku kopii zapasowej JSON. Mo\u017cesz go pobra\u0107 z innej instalacji ZHA z **Ustawienia sieci** lub u\u017cy\u0107 pliku `coordinator_backup.json` z Zigbee2MQTT.", + "title": "Przesy\u0142anie r\u0119cznej kopii zapasowej" + }, "user": { "data": { "path": "\u015acie\u017cka urz\u0105dzenia szeregowego" @@ -111,5 +169,76 @@ "remote_button_short_release": "przycisk \"{subtype}\" zostanie zwolniony", "remote_button_triple_press": "przycisk \"{subtype}\" zostanie trzykrotnie naci\u015bni\u0119ty" } + }, + "options": { + "abort": { + "not_zha_device": "To urz\u0105dzenie nie jest urz\u0105dzeniem zha", + "usb_probe_failed": "Nie uda\u0142o si\u0119 sondowa\u0107 urz\u0105dzenia USB" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_backup_json": "Nieprawid\u0142owa kopia zapasowa JSON" + }, + "flow_title": "{name}", + "step": { + "choose_automatic_backup": { + "data": { + "choose_automatic_backup": "Wybierz automatyczn\u0105 kopi\u0119 zapasow\u0105" + }, + "description": "Przywr\u00f3\u0107 ustawienia sieciowe z automatycznej kopii zapasowej", + "title": "Przywracanie z automatycznej kopii zapasowej" + }, + "choose_formation_strategy": { + "description": "Wybierz ustawienia sieciowe radia.", + "menu_options": { + "choose_automatic_backup": "Przywr\u00f3\u0107 z automatycznej kopii zapasowej", + "form_new_network": "Usu\u0144 ustawienia sieciowe i utw\u00f3rz now\u0105 sie\u0107", + "reuse_settings": "Zachowaj ustawienia sieciowe radia", + "upload_manual_backup": "Prze\u015blij r\u0119czn\u0105 kopi\u0119 zapasow\u0105" + }, + "title": "Tworzenie sieci" + }, + "choose_serial_port": { + "data": { + "path": "\u015acie\u017cka urz\u0105dzenia szeregowego" + }, + "description": "Wybierz port szeregowy dla swojego radia Zigbee", + "title": "Wyb\u00f3r portu szeregowego" + }, + "init": { + "description": "ZHA zostanie zatrzymany. Czy chcesz kontynuowa\u0107?", + "title": "Zmiana konfiguracji ZHA" + }, + "manual_pick_radio_type": { + "data": { + "radio_type": "Typ radia" + }, + "description": "Wybierz typ radia Zigbee", + "title": "Typ radia" + }, + "manual_port_config": { + "data": { + "baudrate": "pr\u0119dko\u015b\u0107 portu", + "flow_control": "kontrola przep\u0142ywu danych", + "path": "\u015acie\u017cka urz\u0105dzenia szeregowego" + }, + "description": "Wprowad\u017a ustawienia portu szeregowego", + "title": "Ustawienia portu szeregowego" + }, + "maybe_confirm_ezsp_restore": { + "data": { + "overwrite_coordinator_ieee": "Trwa\u0142e zast\u0105pienie radiowego adresu IEEE" + }, + "description": "Twoja kopia zapasowa ma inny adres IEEE ni\u017c twoje radio. Aby sie\u0107 dzia\u0142a\u0142a prawid\u0142owo, nale\u017cy r\u00f3wnie\u017c zmieni\u0107 adres IEEE radia. \n\nTo jest trwa\u0142a operacja.", + "title": "Nadpisanie adresu IEEE radia" + }, + "upload_manual_backup": { + "data": { + "uploaded_backup_file": "Prze\u015blij plik" + }, + "description": "Przywr\u00f3\u0107 ustawienia sieciowe z pliku kopii zapasowej JSON. Mo\u017cesz go pobra\u0107 z innej instalacji ZHA z **Ustawienia sieci** lub u\u017cy\u0107 pliku `coordinator_backup.json` z Zigbee2MQTT.", + "title": "Przesy\u0142anie r\u0119cznej kopii zapasowej" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/zodiac/translations/sensor.pl.json b/homeassistant/components/zodiac/translations/sensor.pl.json index 7aecf4724a1..7b2f8c77a4d 100644 --- a/homeassistant/components/zodiac/translations/sensor.pl.json +++ b/homeassistant/components/zodiac/translations/sensor.pl.json @@ -1,18 +1,18 @@ { "state": { "zodiac__sign": { - "aquarius": "Wodnik", - "aries": "Baran", - "cancer": "Rak", - "capricorn": "Kozioro\u017cec", - "gemini": "Bli\u017ani\u0119ta", - "leo": "Lew", - "libra": "Waga", - "pisces": "Ryby", - "sagittarius": "Strzelec", - "scorpio": "Skorpion", - "taurus": "Byk", - "virgo": "Panna" + "aquarius": "wodnik", + "aries": "baran", + "cancer": "rak", + "capricorn": "kozioro\u017cec", + "gemini": "bli\u017ani\u0119ta", + "leo": "lew", + "libra": "waga", + "pisces": "ryby", + "sagittarius": "strzelec", + "scorpio": "skorpion", + "taurus": "byk", + "virgo": "panna" } } } \ No newline at end of file From e7ca40156fcdf3e6f43dda0d88a69d1c73c4ff8e Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Fri, 16 Sep 2022 01:07:02 -0500 Subject: [PATCH 456/955] Bump life360 package to 5.1.1 (#78550) --- homeassistant/components/life360/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/life360/manifest.json b/homeassistant/components/life360/manifest.json index c1a69b245ff..8f0c44f342b 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.0"], + "requirements": ["life360==5.1.1"], "iot_class": "cloud_polling", "loggers": ["life360"] } diff --git a/requirements_all.txt b/requirements_all.txt index da7254a1dee..f3c8cf72aef 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -989,7 +989,7 @@ librouteros==3.2.0 libsoundtouch==0.8 # homeassistant.components.life360 -life360==5.1.0 +life360==5.1.1 # homeassistant.components.osramlightify lightify==1.0.7.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7914be321ae..14f2725619d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -721,7 +721,7 @@ librouteros==3.2.0 libsoundtouch==0.8 # homeassistant.components.life360 -life360==5.1.0 +life360==5.1.1 # homeassistant.components.logi_circle logi_circle==0.2.3 From 564150169bfc69efdfeda25a99d803441f3a4b10 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 16 Sep 2022 08:35:12 +0200 Subject: [PATCH 457/955] Update LaMetric config entry using DHCP discovery data (#78527) * Update LaMetric config entry using DHCP discovery data * Update translations --- .../components/lametric/config_flow.py | 18 +++++++ .../components/lametric/manifest.json | 3 +- .../components/lametric/strings.json | 3 +- .../components/lametric/translations/en.json | 3 +- homeassistant/generated/dhcp.py | 1 + tests/components/lametric/test_config_flow.py | 50 ++++++++++++++++++- 6 files changed, 74 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/lametric/config_flow.py b/homeassistant/components/lametric/config_flow.py index 4bb293b0a4d..b91fff6cf37 100644 --- a/homeassistant/components/lametric/config_flow.py +++ b/homeassistant/components/lametric/config_flow.py @@ -20,6 +20,7 @@ from demetriek import ( import voluptuous as vol from yarl import URL +from homeassistant.components.dhcp import DhcpServiceInfo from homeassistant.components.ssdp import ( ATTR_UPNP_FRIENDLY_NAME, ATTR_UPNP_SERIAL, @@ -29,6 +30,7 @@ from homeassistant.const import CONF_API_KEY, CONF_DEVICE, CONF_HOST, CONF_MAC from homeassistant.data_entry_flow import AbortFlow, FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.config_entry_oauth2_flow import AbstractOAuth2FlowHandler +from homeassistant.helpers.device_registry import format_mac from homeassistant.helpers.selector import ( SelectOptionDict, SelectSelector, @@ -245,6 +247,22 @@ class LaMetricFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN): }, ) + async def async_step_dhcp(self, discovery_info: DhcpServiceInfo) -> FlowResult: + """Handle dhcp discovery to update existing entries.""" + mac = format_mac(discovery_info.macaddress) + for entry in self._async_current_entries(): + if format_mac(entry.data[CONF_MAC]) == mac: + self.hass.config_entries.async_update_entry( + entry, + data=entry.data | {CONF_HOST: discovery_info.ip}, + ) + self.hass.async_create_task( + self.hass.config_entries.async_reload(entry.entry_id) + ) + return self.async_abort(reason="already_configured") + + return self.async_abort(reason="unknown") + # Replace OAuth create entry with a fetch devices step # LaMetric only use OAuth to get device information, but doesn't # use it later on. diff --git a/homeassistant/components/lametric/manifest.json b/homeassistant/components/lametric/manifest.json index 735c05e659c..251378860be 100644 --- a/homeassistant/components/lametric/manifest.json +++ b/homeassistant/components/lametric/manifest.json @@ -12,5 +12,6 @@ { "deviceType": "urn:schemas-upnp-org:device:LaMetric:1" } - ] + ], + "dhcp": [{ "registered_devices": true }] } diff --git a/homeassistant/components/lametric/strings.json b/homeassistant/components/lametric/strings.json index 53271a8d0d8..433f70df18d 100644 --- a/homeassistant/components/lametric/strings.json +++ b/homeassistant/components/lametric/strings.json @@ -38,7 +38,8 @@ "link_local_address": "Link local addresses are not supported", "missing_configuration": "The LaMetric integration is not configured. Please follow the documentation.", "no_devices": "The authorized user has no LaMetric devices", - "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]" + "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]", + "unknown": "[%key:common::config_flow::error::unknown%]" } }, "issues": { diff --git a/homeassistant/components/lametric/translations/en.json b/homeassistant/components/lametric/translations/en.json index c02b7d6d05f..52e483ec1f0 100644 --- a/homeassistant/components/lametric/translations/en.json +++ b/homeassistant/components/lametric/translations/en.json @@ -7,7 +7,8 @@ "link_local_address": "Link local addresses are not supported", "missing_configuration": "The LaMetric integration is not configured. Please follow the documentation.", "no_devices": "The authorized user has no LaMetric devices", - "no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})" + "no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})", + "unknown": "Unexpected error" }, "error": { "cannot_connect": "Failed to connect", diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 8ced5265136..11e0bbb0405 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -60,6 +60,7 @@ DHCP: list[dict[str, str | bool]] = [ {'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'}, diff --git a/tests/components/lametric/test_config_flow.py b/tests/components/lametric/test_config_flow.py index 2134ee135f6..338fe5052d1 100644 --- a/tests/components/lametric/test_config_flow.py +++ b/tests/components/lametric/test_config_flow.py @@ -11,13 +11,14 @@ from demetriek import ( ) import pytest +from homeassistant.components.dhcp import DhcpServiceInfo from homeassistant.components.lametric.const import DOMAIN from homeassistant.components.ssdp import ( ATTR_UPNP_FRIENDLY_NAME, ATTR_UPNP_SERIAL, SsdpServiceInfo, ) -from homeassistant.config_entries import SOURCE_SSDP, SOURCE_USER +from homeassistant.config_entries import SOURCE_DHCP, SOURCE_SSDP, SOURCE_USER from homeassistant.const import CONF_API_KEY, CONF_DEVICE, CONF_HOST, CONF_MAC from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -695,3 +696,50 @@ async def test_cloud_errors( assert len(mock_lametric_config_flow.device.mock_calls) == 2 assert len(mock_lametric_config_flow.notify.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_dhcp_discovery_updates_entry( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> None: + """Test DHCP discovery updates config entries.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_DHCP}, + data=DhcpServiceInfo( + hostname="lametric", + ip="127.0.0.42", + macaddress="aa:bb:cc:dd:ee:ff", + ), + ) + + assert result.get("type") == FlowResultType.ABORT + assert result.get("reason") == "already_configured" + assert mock_config_entry.data == { + CONF_API_KEY: "mock-from-fixture", + CONF_HOST: "127.0.0.42", + CONF_MAC: "AA:BB:CC:DD:EE:FF", + } + + +async def test_dhcp_unknown_device( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> None: + """Test unknown DHCP discovery aborts flow.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_DHCP}, + data=DhcpServiceInfo( + hostname="lametric", + ip="127.0.0.42", + macaddress="aa:bb:cc:dd:ee:00", + ), + ) + + assert result.get("type") == FlowResultType.ABORT + assert result.get("reason") == "unknown" From a19a7e64d5c179af6444f184d7449a34746aaf19 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 16 Sep 2022 11:18:00 +0200 Subject: [PATCH 458/955] Fix WebSocket condition testing (#78570) --- .../components/websocket_api/commands.py | 3 +- .../components/websocket_api/test_commands.py | 36 +++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index d4596619241..c42d48a604e 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -616,8 +616,7 @@ async def handle_test_condition( from homeassistant.helpers import condition # Do static + dynamic validation of the condition - config = cv.CONDITION_SCHEMA(msg["condition"]) - config = await condition.async_validate_condition_config(hass, config) + config = await condition.async_validate_condition_config(hass, msg["condition"]) # Test the condition check_condition = await condition.async_from_config(hass, config) connection.send_result( diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 354a4edeb0d..a5905a809a9 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -1603,6 +1603,42 @@ async def test_test_condition(hass, websocket_client): assert msg["success"] assert msg["result"]["result"] is True + await websocket_client.send_json( + { + "id": 6, + "type": "test_condition", + "condition": { + "condition": "template", + "value_template": "{{ is_state('hello.world', 'paulus') }}", + }, + "variables": {"hello": "world"}, + } + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == 6 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] + assert msg["result"]["result"] is True + + await websocket_client.send_json( + { + "id": 7, + "type": "test_condition", + "condition": { + "condition": "template", + "value_template": "{{ is_state('hello.world', 'frenck') }}", + }, + "variables": {"hello": "world"}, + } + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == 7 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] + assert msg["result"]["result"] is False + async def test_execute_script(hass, websocket_client): """Test testing a condition.""" From b093c2840b97c1585ec26bf2431d3d708ad6dc4f Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Fri, 16 Sep 2022 12:30:29 +0300 Subject: [PATCH 459/955] Remove name key from config flow in Mikrotik (#78571) --- .../components/mikrotik/config_flow.py | 11 +----- tests/components/mikrotik/test_config_flow.py | 38 +------------------ 2 files changed, 3 insertions(+), 46 deletions(-) diff --git a/homeassistant/components/mikrotik/config_flow.py b/homeassistant/components/mikrotik/config_flow.py index d506c2c75e4..ed62734578f 100644 --- a/homeassistant/components/mikrotik/config_flow.py +++ b/homeassistant/components/mikrotik/config_flow.py @@ -8,7 +8,6 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import ( CONF_HOST, - CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_USERNAME, @@ -49,12 +48,7 @@ class MikrotikFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a flow initialized by the user.""" errors = {} if user_input is not None: - for entry in self._async_current_entries(): - if entry.data[CONF_HOST] == user_input[CONF_HOST]: - return self.async_abort(reason="already_configured") - if entry.data[CONF_NAME] == user_input[CONF_NAME]: - errors[CONF_NAME] = "name_exists" - break + self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]}) try: await self.hass.async_add_executor_job(get_api, user_input) @@ -66,13 +60,12 @@ class MikrotikFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if not errors: return self.async_create_entry( - title=user_input[CONF_NAME], data=user_input + title=f"{DEFAULT_NAME} ({user_input[CONF_HOST]})", data=user_input ) return self.async_show_form( step_id="user", data_schema=vol.Schema( { - vol.Required(CONF_NAME, default=DEFAULT_NAME): str, vol.Required(CONF_HOST): str, vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str, diff --git a/tests/components/mikrotik/test_config_flow.py b/tests/components/mikrotik/test_config_flow.py index 704bf92066e..6a71806cea9 100644 --- a/tests/components/mikrotik/test_config_flow.py +++ b/tests/components/mikrotik/test_config_flow.py @@ -1,5 +1,4 @@ """Test Mikrotik setup process.""" -from datetime import timedelta from unittest.mock import patch import librouteros @@ -14,7 +13,6 @@ from homeassistant.components.mikrotik.const import ( ) from homeassistant.const import ( CONF_HOST, - CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_USERNAME, @@ -24,7 +22,6 @@ from homeassistant.const import ( from tests.common import MockConfigEntry DEMO_USER_INPUT = { - CONF_NAME: "Home router", CONF_HOST: "0.0.0.0", CONF_USERNAME: "username", CONF_PASSWORD: "password", @@ -32,20 +29,7 @@ DEMO_USER_INPUT = { CONF_VERIFY_SSL: False, } -DEMO_CONFIG = { - CONF_NAME: "Home router", - CONF_HOST: "0.0.0.0", - CONF_USERNAME: "username", - CONF_PASSWORD: "password", - CONF_PORT: 8278, - CONF_VERIFY_SSL: False, - CONF_FORCE_DHCP: False, - CONF_ARP_PING: False, - CONF_DETECTION_TIME: timedelta(seconds=30), -} - DEMO_CONFIG_ENTRY = { - CONF_NAME: "Home router", CONF_HOST: "0.0.0.0", CONF_USERNAME: "username", CONF_PASSWORD: "password", @@ -97,8 +81,7 @@ async def test_flow_works(hass, api): ) assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert result["title"] == "Home router" - assert result["data"][CONF_NAME] == "Home router" + assert result["title"] == "Mikrotik (0.0.0.0)" assert result["data"][CONF_HOST] == "0.0.0.0" assert result["data"][CONF_USERNAME] == "username" assert result["data"][CONF_PASSWORD] == "password" @@ -151,25 +134,6 @@ async def test_host_already_configured(hass, auth_error): assert result["reason"] == "already_configured" -async def test_name_exists(hass, api): - """Test name already configured.""" - - entry = MockConfigEntry(domain=DOMAIN, data=DEMO_CONFIG_ENTRY) - entry.add_to_hass(hass) - user_input = DEMO_USER_INPUT.copy() - user_input[CONF_HOST] = "0.0.0.1" - - 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=user_input - ) - - assert result["type"] == "form" - assert result["errors"] == {CONF_NAME: "name_exists"} - - async def test_connection_error(hass, conn_error): """Test error when connection is unsuccessful.""" From 085abc74ee7dab777149d8937cffc8aaa6883880 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 16 Sep 2022 12:24:20 +0200 Subject: [PATCH 460/955] Reduce overhead to update passive bluetooth devices (#78545) --- .../components/bluetooth/__init__.py | 12 +- homeassistant/components/bluetooth/manager.py | 26 ++- .../bluetooth/passive_update_coordinator.py | 7 +- .../bluetooth/passive_update_processor.py | 6 +- .../bluetooth/update_coordinator.py | 60 +++-- homeassistant/components/yalexs_ble/entity.py | 4 +- tests/components/bluemaestro/test_sensor.py | 20 +- tests/components/bluetooth/__init__.py | 67 +++++- .../test_active_update_coordinator.py | 115 ++-------- .../test_passive_update_coordinator.py | 72 ++---- .../test_passive_update_processor.py | 205 +++++++----------- tests/components/bthome/test_binary_sensor.py | 22 +- tests/components/bthome/test_sensor.py | 22 +- tests/components/govee_ble/test_sensor.py | 20 +- tests/components/inkbird/test_sensor.py | 20 +- tests/components/moat/test_sensor.py | 22 +- .../components/qingping/test_binary_sensor.py | 20 +- tests/components/qingping/test_sensor.py | 20 +- tests/components/sensorpro/test_sensor.py | 20 +- tests/components/sensorpush/test_sensor.py | 25 +-- tests/components/thermobeacon/test_sensor.py | 20 +- tests/components/thermopro/test_sensor.py | 23 +- tests/components/tilt_ble/test_sensor.py | 24 +- .../xiaomi_ble/test_binary_sensor.py | 42 +--- tests/components/xiaomi_ble/test_sensor.py | 179 +++++---------- 25 files changed, 364 insertions(+), 709 deletions(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 8ba3a503a30..2132e1c8b66 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -103,6 +103,16 @@ def async_discovered_service_info( return _get_manager(hass).async_discovered_service_info(connectable) +@hass_callback +def async_last_service_info( + hass: HomeAssistant, address: str, connectable: bool = True +) -> BluetoothServiceInfoBleak | None: + """Return the last service info for an address.""" + if DATA_MANAGER not in hass.data: + return None + return _get_manager(hass).async_last_service_info(address, connectable) + + @hass_callback def async_ble_device_from_address( hass: HomeAssistant, address: str, connectable: bool = True @@ -173,7 +183,7 @@ async def async_process_advertisements( @hass_callback def async_track_unavailable( hass: HomeAssistant, - callback: Callable[[str], None], + callback: Callable[[BluetoothServiceInfoBleak], None], address: str, connectable: bool = True, ) -> Callable[[], None]: diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index 672ef91542e..47941c8d5c1 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -143,9 +143,11 @@ class BluetoothManager: self.hass = hass self._integration_matcher = integration_matcher self._cancel_unavailable_tracking: list[CALLBACK_TYPE] = [] - self._unavailable_callbacks: dict[str, list[Callable[[str], None]]] = {} + self._unavailable_callbacks: dict[ + str, list[Callable[[BluetoothServiceInfoBleak], None]] + ] = {} self._connectable_unavailable_callbacks: dict[ - str, list[Callable[[str], None]] + str, list[Callable[[BluetoothServiceInfoBleak], None]] ] = {} self._callback_index = BluetoothCallbackMatcherIndex() self._bleak_callbacks: list[ @@ -269,12 +271,12 @@ class BluetoothManager: } disappeared = history_set.difference(active_addresses) for address in disappeared: - del history[address] + service_info = history.pop(address) if not (callbacks := unavailable_callbacks.get(address)): continue for callback in callbacks: try: - callback(address) + callback(service_info) except Exception: # pylint: disable=broad-except _LOGGER.exception("Error in unavailable callback") @@ -358,7 +360,10 @@ class BluetoothManager: @hass_callback def async_track_unavailable( - self, callback: Callable[[str], None], address: str, connectable: bool + self, + callback: Callable[[BluetoothServiceInfoBleak], None], + address: str, + connectable: bool, ) -> Callable[[], None]: """Register a callback.""" unavailable_callbacks = self._get_unavailable_callbacks_by_type(connectable) @@ -430,9 +435,16 @@ class BluetoothManager: def async_discovered_service_info( self, connectable: bool ) -> Iterable[BluetoothServiceInfoBleak]: - """Return if the address is present.""" + """Return all the discovered services info.""" return self._get_history_by_type(connectable).values() + @hass_callback + def async_last_service_info( + self, address: str, connectable: bool + ) -> BluetoothServiceInfoBleak | None: + """Return the last service info for an address.""" + return self._get_history_by_type(connectable).get(address) + @hass_callback def async_rediscover_address(self, address: str) -> None: """Trigger discovery of devices which have already been seen.""" @@ -448,7 +460,7 @@ class BluetoothManager: def _get_unavailable_callbacks_by_type( self, connectable: bool - ) -> dict[str, list[Callable[[str], None]]]: + ) -> dict[str, list[Callable[[BluetoothServiceInfoBleak], None]]]: """Return the unavailable callbacks by type.""" return ( self._connectable_unavailable_callbacks diff --git a/homeassistant/components/bluetooth/passive_update_coordinator.py b/homeassistant/components/bluetooth/passive_update_coordinator.py index 296e49e2fa0..1eae49a6cab 100644 --- a/homeassistant/components/bluetooth/passive_update_coordinator.py +++ b/homeassistant/components/bluetooth/passive_update_coordinator.py @@ -41,9 +41,11 @@ class PassiveBluetoothDataUpdateCoordinator(BasePassiveBluetoothCoordinator): update_callback() @callback - def _async_handle_unavailable(self, address: str) -> None: + def _async_handle_unavailable( + self, service_info: BluetoothServiceInfoBleak + ) -> None: """Handle the device going unavailable.""" - super()._async_handle_unavailable(address) + super()._async_handle_unavailable(service_info) self.async_update_listeners() @callback @@ -73,7 +75,6 @@ class PassiveBluetoothDataUpdateCoordinator(BasePassiveBluetoothCoordinator): change: BluetoothChange, ) -> None: """Handle a Bluetooth event.""" - super()._async_handle_bluetooth_event(service_info, change) self.async_update_listeners() diff --git a/homeassistant/components/bluetooth/passive_update_processor.py b/homeassistant/components/bluetooth/passive_update_processor.py index 5ea2f3f0742..b04447cc4ee 100644 --- a/homeassistant/components/bluetooth/passive_update_processor.py +++ b/homeassistant/components/bluetooth/passive_update_processor.py @@ -101,9 +101,11 @@ class PassiveBluetoothProcessorCoordinator( return remove_processor @callback - def _async_handle_unavailable(self, address: str) -> None: + def _async_handle_unavailable( + self, service_info: BluetoothServiceInfoBleak + ) -> None: """Handle the device going unavailable.""" - super()._async_handle_unavailable(address) + super()._async_handle_unavailable(service_info) for processor in self._processors: processor.async_handle_unavailable() diff --git a/homeassistant/components/bluetooth/update_coordinator.py b/homeassistant/components/bluetooth/update_coordinator.py index 9348095f2b1..2c99f189852 100644 --- a/homeassistant/components/bluetooth/update_coordinator.py +++ b/homeassistant/components/bluetooth/update_coordinator.py @@ -1,8 +1,8 @@ """Update coordinator for the Bluetooth integration.""" from __future__ import annotations +from abc import abstractmethod import logging -import time from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback @@ -11,6 +11,8 @@ from . import ( BluetoothChange, BluetoothScanningMode, BluetoothServiceInfoBleak, + async_address_present, + async_last_service_info, async_register_callback, async_track_unavailable, ) @@ -33,14 +35,13 @@ class BasePassiveBluetoothCoordinator: """Initialize the coordinator.""" self.hass = hass self.logger = logger - self.name: str | None = None self.address = address self.connectable = connectable self._cancel_track_unavailable: CALLBACK_TYPE | None = None self._cancel_bluetooth_advertisements: CALLBACK_TYPE | None = None - self._present = False self.mode = mode - self.last_seen = 0.0 + self._last_unavailable_time = 0.0 + self._last_name = address @callback def async_start(self) -> CALLBACK_TYPE: @@ -53,10 +54,41 @@ class BasePassiveBluetoothCoordinator: return _async_cancel + @callback + @abstractmethod + def _async_handle_bluetooth_event( + self, + service_info: BluetoothServiceInfoBleak, + change: BluetoothChange, + ) -> None: + """Handle a bluetooth event.""" + + @property + def name(self) -> str: + """Return last known name of the device.""" + if service_info := async_last_service_info( + self.hass, self.address, self.connectable + ): + return service_info.name + return self._last_name + + @property + def last_seen(self) -> float: + """Return the last time the device was seen.""" + # If the device is unavailable it will not have a service + # info and fall through below. + if service_info := async_last_service_info( + self.hass, self.address, self.connectable + ): + return service_info.time + # This is the time from the last advertisement that + # was set when the unavailable callback was called. + return self._last_unavailable_time + @property def available(self) -> bool: """Return if the device is available.""" - return self._present + return async_address_present(self.hass, self.address, self.connectable) @callback def _async_start(self) -> None: @@ -84,17 +116,9 @@ class BasePassiveBluetoothCoordinator: self._cancel_track_unavailable = None @callback - def _async_handle_unavailable(self, address: str) -> None: - """Handle the device going unavailable.""" - self._present = False - - @callback - def _async_handle_bluetooth_event( - self, - service_info: BluetoothServiceInfoBleak, - change: BluetoothChange, + def _async_handle_unavailable( + self, service_info: BluetoothServiceInfoBleak ) -> None: - """Handle a Bluetooth event.""" - self.last_seen = time.monotonic() - self.name = service_info.name - self._present = True + """Handle the device going unavailable.""" + self._last_unavailable_time = service_info.time + self._last_name = service_info.name diff --git a/homeassistant/components/yalexs_ble/entity.py b/homeassistant/components/yalexs_ble/entity.py index fa80698831b..d2395f3e39e 100644 --- a/homeassistant/components/yalexs_ble/entity.py +++ b/homeassistant/components/yalexs_ble/entity.py @@ -56,7 +56,9 @@ class YALEXSBLEEntity(Entity): self.async_write_ha_state() @callback - def _async_device_unavailable(self, _address: str) -> None: + def _async_device_unavailable( + self, _service_info: bluetooth.BluetoothServiceInfoBleak + ) -> None: """Handle device not longer being seen by the bluetooth stack.""" self._attr_available = False self.async_write_ha_state() diff --git a/tests/components/bluemaestro/test_sensor.py b/tests/components/bluemaestro/test_sensor.py index 2f964e65481..e1c7b27673c 100644 --- a/tests/components/bluemaestro/test_sensor.py +++ b/tests/components/bluemaestro/test_sensor.py @@ -1,15 +1,14 @@ """Test the BlueMaestro sensors.""" -from unittest.mock import patch from homeassistant.components.bluemaestro.const import DOMAIN -from homeassistant.components.bluetooth import BluetoothChange from homeassistant.components.sensor import ATTR_STATE_CLASS from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT from . import BLUEMAESTRO_SERVICE_INFO from tests.common import MockConfigEntry +from tests.components.bluetooth import inject_bluetooth_service_info async def test_sensors(hass): @@ -20,22 +19,11 @@ async def test_sensors(hass): ) entry.add_to_hass(hass) - saved_callback = None - - def _async_register_callback(_hass, _callback, _matcher, _mode): - nonlocal saved_callback - saved_callback = _callback - return lambda: None - - with patch( - "homeassistant.components.bluetooth.update_coordinator.async_register_callback", - _async_register_callback, - ): - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() assert len(hass.states.async_all("sensor")) == 0 - saved_callback(BLUEMAESTRO_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + inject_bluetooth_service_info(hass, BLUEMAESTRO_SERVICE_INFO) await hass.async_block_till_done() assert len(hass.states.async_all("sensor")) == 4 diff --git a/tests/components/bluetooth/__init__.py b/tests/components/bluetooth/__init__.py index 7c559e00adc..a836740bb9b 100644 --- a/tests/components/bluetooth/__init__.py +++ b/tests/components/bluetooth/__init__.py @@ -19,6 +19,16 @@ from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry +__all__ = ( + "inject_advertisement", + "inject_advertisement_with_source", + "inject_advertisement_with_time_and_source", + "inject_advertisement_with_time_and_source_connectable", + "inject_bluetooth_service_info", + "patch_all_discovered_devices", + "patch_discovered_devices", +) + def _get_manager() -> BluetoothManager: """Return the bluetooth manager.""" @@ -80,6 +90,51 @@ def inject_advertisement_with_time_and_source_connectable( ) +def inject_bluetooth_service_info_bleak( + hass: HomeAssistant, info: models.BluetoothServiceInfoBleak +) -> None: + """Inject an advertisement into the manager with connectable status.""" + advertisement_data = AdvertisementData( # type: ignore[no-untyped-call] + local_name=None if info.name == "" else info.name, + manufacturer_data=info.manufacturer_data, + service_data=info.service_data, + service_uuids=info.service_uuids, + ) + device = BLEDevice( # type: ignore[no-untyped-call] + address=info.address, + name=info.name, + details={}, + rssi=info.rssi, + ) + inject_advertisement_with_time_and_source_connectable( + hass, + device, + advertisement_data, + info.time, + SOURCE_LOCAL, + connectable=info.connectable, + ) + + +def inject_bluetooth_service_info( + hass: HomeAssistant, info: models.BluetoothServiceInfo +) -> None: + """Inject a BluetoothServiceInfo into the manager.""" + advertisement_data = AdvertisementData( # type: ignore[no-untyped-call] + local_name=None if info.name == "" else info.name, + manufacturer_data=info.manufacturer_data, + service_data=info.service_data, + service_uuids=info.service_uuids, + ) + device = BLEDevice( # type: ignore[no-untyped-call] + address=info.address, + name=info.name, + details={}, + rssi=info.rssi, + ) + inject_advertisement(hass, device, advertisement_data) + + def patch_all_discovered_devices(mock_discovered: list[BLEDevice]) -> None: """Mock all the discovered devices from all the scanners.""" return patch.object( @@ -87,18 +142,6 @@ def patch_all_discovered_devices(mock_discovered: list[BLEDevice]) -> None: ) -def patch_history(mock_history: dict[str, models.BluetoothServiceInfoBleak]) -> None: - """Patch the history.""" - return patch.dict(_get_manager()._history, mock_history) - - -def patch_connectable_history( - mock_history: dict[str, models.BluetoothServiceInfoBleak] -) -> None: - """Patch the connectable history.""" - return patch.dict(_get_manager()._connectable_history, mock_history) - - def patch_discovered_devices(mock_discovered: list[BLEDevice]) -> None: """Mock the combined best path to discovered devices from all the scanners.""" return patch.object( diff --git a/tests/components/bluetooth/test_active_update_coordinator.py b/tests/components/bluetooth/test_active_update_coordinator.py index 7677584e890..ecec5f12171 100644 --- a/tests/components/bluetooth/test_active_update_coordinator.py +++ b/tests/components/bluetooth/test_active_update_coordinator.py @@ -3,13 +3,12 @@ from __future__ import annotations import asyncio import logging -from unittest.mock import MagicMock, call, patch +from unittest.mock import MagicMock, call from bleak import BleakError from homeassistant.components.bluetooth import ( DOMAIN, - BluetoothChange, BluetoothScanningMode, BluetoothServiceInfoBleak, ) @@ -21,6 +20,8 @@ from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo from homeassistant.setup import async_setup_component +from tests.components.bluetooth import inject_bluetooth_service_info + _LOGGER = logging.getLogger(__name__) @@ -60,26 +61,14 @@ async def test_basic_usage(hass: HomeAssistant, mock_bleak_scanner_start): poll_method=_poll, ) assert coordinator.available is False # no data yet - saved_callback = None processor = MagicMock() coordinator.async_register_processor(processor) async_handle_update = processor.async_handle_update - def _async_register_callback(_hass, _callback, _matcher, _mode): - nonlocal saved_callback - saved_callback = _callback - return lambda: None + cancel = coordinator.async_start() - with patch( - "homeassistant.components.bluetooth.update_coordinator.async_register_callback", - _async_register_callback, - ): - cancel = coordinator.async_start() - - assert saved_callback is not None - - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) await hass.async_block_till_done() assert coordinator.available is True @@ -126,38 +115,26 @@ async def test_poll_can_be_skipped(hass: HomeAssistant, mock_bleak_scanner_start ), ) assert coordinator.available is False # no data yet - saved_callback = None processor = MagicMock() coordinator.async_register_processor(processor) async_handle_update = processor.async_handle_update - def _async_register_callback(_hass, _callback, _matcher, _mode): - nonlocal saved_callback - saved_callback = _callback - return lambda: None + cancel = coordinator.async_start() - with patch( - "homeassistant.components.bluetooth.update_coordinator.async_register_callback", - _async_register_callback, - ): - cancel = coordinator.async_start() - - assert saved_callback is not None - - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) await hass.async_block_till_done() assert async_handle_update.mock_calls[-1] == call({"testdata": True}) flag = False - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) await hass.async_block_till_done() assert async_handle_update.mock_calls[-1] == call({"testdata": None}) flag = True - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) await hass.async_block_till_done() assert async_handle_update.mock_calls[-1] == call({"testdata": True}) @@ -200,27 +177,15 @@ async def test_bleak_error_and_recover( ), ) assert coordinator.available is False # no data yet - saved_callback = None processor = MagicMock() coordinator.async_register_processor(processor) async_handle_update = processor.async_handle_update - def _async_register_callback(_hass, _callback, _matcher, _mode): - nonlocal saved_callback - saved_callback = _callback - return lambda: None - - with patch( - "homeassistant.components.bluetooth.update_coordinator.async_register_callback", - _async_register_callback, - ): - cancel = coordinator.async_start() - - assert saved_callback is not None + cancel = coordinator.async_start() # First poll fails - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) await hass.async_block_till_done() assert async_handle_update.mock_calls[-1] == call({"testdata": None}) @@ -231,7 +196,7 @@ async def test_bleak_error_and_recover( # Second poll works flag = False - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) await hass.async_block_till_done() assert async_handle_update.mock_calls[-1] == call({"testdata": False}) @@ -272,33 +237,21 @@ async def test_poll_failure_and_recover(hass: HomeAssistant, mock_bleak_scanner_ ), ) assert coordinator.available is False # no data yet - saved_callback = None processor = MagicMock() coordinator.async_register_processor(processor) async_handle_update = processor.async_handle_update - def _async_register_callback(_hass, _callback, _matcher, _mode): - nonlocal saved_callback - saved_callback = _callback - return lambda: None - - with patch( - "homeassistant.components.bluetooth.update_coordinator.async_register_callback", - _async_register_callback, - ): - cancel = coordinator.async_start() - - assert saved_callback is not None + cancel = coordinator.async_start() # First poll fails - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) await hass.async_block_till_done() assert async_handle_update.mock_calls[-1] == call({"testdata": None}) # Second poll works flag = False - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) await hass.async_block_till_done() assert async_handle_update.mock_calls[-1] == call({"testdata": False}) @@ -334,29 +287,17 @@ async def test_second_poll_needed(hass: HomeAssistant, mock_bleak_scanner_start) poll_method=_poll, ) assert coordinator.available is False # no data yet - saved_callback = None processor = MagicMock() coordinator.async_register_processor(processor) async_handle_update = processor.async_handle_update - def _async_register_callback(_hass, _callback, _matcher, _mode): - nonlocal saved_callback - saved_callback = _callback - return lambda: None - - with patch( - "homeassistant.components.bluetooth.update_coordinator.async_register_callback", - _async_register_callback, - ): - cancel = coordinator.async_start() - - assert saved_callback is not None + cancel = coordinator.async_start() # First poll gets queued - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) # Second poll gets stuck behind first poll - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) await hass.async_block_till_done() assert async_handle_update.mock_calls[-1] == call({"testdata": 1}) @@ -392,31 +333,19 @@ async def test_rate_limit(hass: HomeAssistant, mock_bleak_scanner_start): poll_method=_poll, ) assert coordinator.available is False # no data yet - saved_callback = None processor = MagicMock() coordinator.async_register_processor(processor) async_handle_update = processor.async_handle_update - def _async_register_callback(_hass, _callback, _matcher, _mode): - nonlocal saved_callback - saved_callback = _callback - return lambda: None - - with patch( - "homeassistant.components.bluetooth.update_coordinator.async_register_callback", - _async_register_callback, - ): - cancel = coordinator.async_start() - - assert saved_callback is not None + cancel = coordinator.async_start() # First poll gets queued - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) # Second poll gets stuck behind first poll - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) # Third poll gets stuck behind first poll doesn't get queued - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) await hass.async_block_till_done() assert async_handle_update.mock_calls[-1] == call({"testdata": 1}) diff --git a/tests/components/bluetooth/test_passive_update_coordinator.py b/tests/components/bluetooth/test_passive_update_coordinator.py index 2335bf51485..bf7c0a48467 100644 --- a/tests/components/bluetooth/test_passive_update_coordinator.py +++ b/tests/components/bluetooth/test_passive_update_coordinator.py @@ -20,9 +20,10 @@ from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util -from . import patch_all_discovered_devices, patch_history +from . import patch_all_discovered_devices from tests.common import async_fire_time_changed +from tests.components.bluetooth import inject_bluetooth_service_info _LOGGER = logging.getLogger(__name__) @@ -65,32 +66,20 @@ async def test_basic_usage(hass, mock_bleak_scanner_start): hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE ) assert coordinator.available is False # no data yet - saved_callback = None - - def _async_register_callback(_hass, _callback, _matcher, _mode): - nonlocal saved_callback - saved_callback = _callback - return lambda: None mock_listener = MagicMock() unregister_listener = coordinator.async_add_listener(mock_listener) - with patch( - "homeassistant.components.bluetooth.update_coordinator.async_register_callback", - _async_register_callback, - ): - cancel = coordinator.async_start() + cancel = coordinator.async_start() - assert saved_callback is not None - - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) assert len(mock_listener.mock_calls) == 1 assert coordinator.data == {"rssi": GENERIC_BLUETOOTH_SERVICE_INFO.rssi} assert coordinator.available is True unregister_listener() - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) assert len(mock_listener.mock_calls) == 1 assert coordinator.data == {"rssi": GENERIC_BLUETOOTH_SERVICE_INFO.rssi} @@ -107,21 +96,11 @@ async def test_context_compatiblity_with_data_update_coordinator( hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE ) assert coordinator.available is False # no data yet - saved_callback = None - - def _async_register_callback(_hass, _callback, _matcher, _mode): - nonlocal saved_callback - saved_callback = _callback - return lambda: None mock_listener = MagicMock() coordinator.async_add_listener(mock_listener) - with patch( - "homeassistant.components.bluetooth.update_coordinator.async_register_callback", - _async_register_callback, - ): - coordinator.async_start() + coordinator.async_start() assert not set(coordinator.async_contexts()) @@ -158,41 +137,27 @@ async def test_unavailable_callbacks_mark_the_coordinator_unavailable( hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.PASSIVE ) assert coordinator.available is False # no data yet - saved_callback = None - - def _async_register_callback(_hass, _callback, _matcher, _mode): - nonlocal saved_callback - saved_callback = _callback - return lambda: None mock_listener = MagicMock() coordinator.async_add_listener(mock_listener) - with patch( - "homeassistant.components.bluetooth.update_coordinator.async_register_callback", - _async_register_callback, - ): - coordinator.async_start() + coordinator.async_start() assert coordinator.available is False - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) assert coordinator.available is True - with patch_all_discovered_devices( - [MagicMock(address="44:44:33:11:23:45")] - ), patch_history({"aa:bb:cc:dd:ee:ff": MagicMock()}): + with patch_all_discovered_devices([MagicMock(address="44:44:33:11:23:45")]): async_fire_time_changed( hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS) ) await hass.async_block_till_done() assert coordinator.available is False - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) assert coordinator.available is True - with patch_all_discovered_devices( - [MagicMock(address="44:44:33:11:23:45")] - ), patch_history({"aa:bb:cc:dd:ee:ff": MagicMock()}): + with patch_all_discovered_devices([MagicMock(address="44:44:33:11:23:45")]): async_fire_time_changed( hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS) ) @@ -209,21 +174,10 @@ async def test_passive_bluetooth_coordinator_entity(hass, mock_bleak_scanner_sta entity = PassiveBluetoothCoordinatorEntity(coordinator) assert entity.available is False - saved_callback = None - - def _async_register_callback(_hass, _callback, _matcher, _mode): - nonlocal saved_callback - saved_callback = _callback - return lambda: None - - with patch( - "homeassistant.components.bluetooth.update_coordinator.async_register_callback", - _async_register_callback, - ): - coordinator.async_start() + coordinator.async_start() assert coordinator.available is False - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) assert coordinator.available is True entity.hass = hass await entity.async_update() diff --git a/tests/components/bluetooth/test_passive_update_processor.py b/tests/components/bluetooth/test_passive_update_processor.py index 2ae6f77b28d..99b16131ddc 100644 --- a/tests/components/bluetooth/test_passive_update_processor.py +++ b/tests/components/bluetooth/test_passive_update_processor.py @@ -3,6 +3,7 @@ from __future__ import annotations from datetime import timedelta import logging +import time from unittest.mock import MagicMock, patch from home_assistant_bluetooth import BluetoothServiceInfo @@ -16,6 +17,7 @@ from homeassistant.components.bluetooth import ( DOMAIN, BluetoothChange, BluetoothScanningMode, + BluetoothServiceInfoBleak, ) from homeassistant.components.bluetooth.const import UNAVAILABLE_TRACK_SECONDS from homeassistant.components.bluetooth.passive_update_processor import ( @@ -32,9 +34,13 @@ from homeassistant.helpers.entity import DeviceInfo from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util -from . import patch_all_discovered_devices, patch_connectable_history, patch_history +from . import patch_all_discovered_devices from tests.common import MockEntityPlatform, async_fire_time_changed +from tests.components.bluetooth import ( + inject_bluetooth_service_info, + inject_bluetooth_service_info_bleak, +) _LOGGER = logging.getLogger(__name__) @@ -50,6 +56,7 @@ GENERIC_BLUETOOTH_SERVICE_INFO = BluetoothServiceInfo( service_uuids=[], source="local", ) + GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( devices={ None: DeviceInfo( @@ -105,21 +112,11 @@ async def test_basic_usage(hass, mock_bleak_scanner_start): _mock_update_method, ) assert coordinator.available is False # no data yet - saved_callback = None - - def _async_register_callback(_hass, _callback, _matcher, _mode): - nonlocal saved_callback - saved_callback = _callback - return lambda: None processor = PassiveBluetoothDataProcessor(_async_generate_mock_data) - with patch( - "homeassistant.components.bluetooth.update_coordinator.async_register_callback", - _async_register_callback, - ): - unregister_processor = coordinator.async_register_processor(processor) - cancel_coordinator = coordinator.async_start() + unregister_processor = coordinator.async_register_processor(processor) + cancel_coordinator = coordinator.async_start() entity_key = PassiveBluetoothEntityKey("temperature", None) entity_key_events = [] @@ -149,7 +146,7 @@ async def test_basic_usage(hass, mock_bleak_scanner_start): mock_add_entities, ) - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) # Each listener should receive the same data # since both match @@ -159,7 +156,7 @@ async def test_basic_usage(hass, mock_bleak_scanner_start): # There should be 4 calls to create entities assert len(mock_entity.mock_calls) == 2 - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) # Each listener should receive the same data # since both match @@ -174,7 +171,7 @@ async def test_basic_usage(hass, mock_bleak_scanner_start): cancel_listener() cancel_async_add_entities_listener() - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) # Each listener should not trigger any more now # that they were cancelled @@ -217,20 +214,11 @@ async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start): _mock_update_method, ) assert coordinator.available is False # no data yet - saved_callback = None - - def _async_register_callback(_hass, _callback, _matcher, _mode): - nonlocal saved_callback - saved_callback = _callback - return lambda: None processor = PassiveBluetoothDataProcessor(_async_generate_mock_data) - with patch( - "homeassistant.components.bluetooth.update_coordinator.async_register_callback", - _async_register_callback, - ): - unregister_processor = coordinator.async_register_processor(processor) - cancel_coordinator = coordinator.async_start() + + unregister_processor = coordinator.async_register_processor(processor) + cancel_coordinator = coordinator.async_start() mock_entity = MagicMock() mock_add_entities = MagicMock() @@ -242,38 +230,49 @@ async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start): assert coordinator.available is False assert processor.available is False - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + now = time.monotonic() + service_info_at_time = BluetoothServiceInfoBleak( + name="Generic", + address="aa:bb:cc:dd:ee:ff", + rssi=-95, + manufacturer_data={ + 1: b"\x01\x01\x01\x01\x01\x01\x01\x01", + }, + service_data={}, + service_uuids=[], + source="local", + time=now, + device=MagicMock(), + advertisement=MagicMock(), + connectable=True, + ) + + inject_bluetooth_service_info_bleak(hass, service_info_at_time) assert len(mock_add_entities.mock_calls) == 1 assert coordinator.available is True assert processor.available is True - with patch_all_discovered_devices( - [MagicMock(address="44:44:33:11:23:45")] - ), patch_history({"aa:bb:cc:dd:ee:ff": MagicMock()}), patch_connectable_history( - {"aa:bb:cc:dd:ee:ff": MagicMock()}, - ): + with patch_all_discovered_devices([MagicMock(address="44:44:33:11:23:45")]): async_fire_time_changed( hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS) ) await hass.async_block_till_done() assert coordinator.available is False assert processor.available is False + assert coordinator.last_seen == service_info_at_time.time - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + inject_bluetooth_service_info_bleak(hass, service_info_at_time) assert len(mock_add_entities.mock_calls) == 1 assert coordinator.available is True assert processor.available is True - with patch_all_discovered_devices( - [MagicMock(address="44:44:33:11:23:45")] - ), patch_history({"aa:bb:cc:dd:ee:ff": MagicMock()}), patch_connectable_history( - {"aa:bb:cc:dd:ee:ff": MagicMock()}, - ): + with patch_all_discovered_devices([MagicMock(address="44:44:33:11:23:45")]): async_fire_time_changed( hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS) ) await hass.async_block_till_done() assert coordinator.available is False assert processor.available is False + assert coordinator.last_seen == service_info_at_time.time unregister_processor() cancel_coordinator() @@ -304,21 +303,11 @@ async def test_no_updates_once_stopping(hass, mock_bleak_scanner_start): _mock_update_method, ) assert coordinator.available is False # no data yet - saved_callback = None - - def _async_register_callback(_hass, _callback, _matcher, _mode): - nonlocal saved_callback - saved_callback = _callback - return lambda: None processor = PassiveBluetoothDataProcessor(_async_generate_mock_data) - with patch( - "homeassistant.components.bluetooth.update_coordinator.async_register_callback", - _async_register_callback, - ): - unregister_processor = coordinator.async_register_processor(processor) - cancel_coordinator = coordinator.async_start() + unregister_processor = coordinator.async_register_processor(processor) + cancel_coordinator = coordinator.async_start() all_events = [] @@ -330,13 +319,13 @@ async def test_no_updates_once_stopping(hass, mock_bleak_scanner_start): _all_listener, ) - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) assert len(all_events) == 1 hass.state = CoreState.stopping # We should stop processing events once hass is stopping - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) assert len(all_events) == 1 unregister_processor() cancel_coordinator() @@ -361,7 +350,7 @@ async def test_exception_from_update_method(hass, caplog, mock_bleak_scanner_sta """Generate mock data.""" nonlocal run_count run_count += 1 - if run_count == 2: + if run_count == 1: raise Exception("Test exception") return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE @@ -390,7 +379,7 @@ async def test_exception_from_update_method(hass, caplog, mock_bleak_scanner_sta processor.async_add_listener(MagicMock()) - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) assert processor.available is True # We should go unavailable once we get an exception @@ -424,7 +413,7 @@ async def test_bad_data_from_update_method(hass, mock_bleak_scanner_start): """Generate mock data.""" nonlocal run_count run_count += 1 - if run_count == 2: + if run_count == 1: return "bad_data" return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE @@ -453,7 +442,7 @@ async def test_bad_data_from_update_method(hass, mock_bleak_scanner_start): processor.async_add_listener(MagicMock()) - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) assert processor.available is True # We should go unavailable once we get bad data @@ -788,20 +777,11 @@ async def test_integration_with_entity(hass, mock_bleak_scanner_start): _mock_update_method, ) assert coordinator.available is False # no data yet - saved_callback = None - - def _async_register_callback(_hass, _callback, _matcher, _mode): - nonlocal saved_callback - saved_callback = _callback - return lambda: None processor = PassiveBluetoothDataProcessor(_async_generate_mock_data) - with patch( - "homeassistant.components.bluetooth.update_coordinator.async_register_callback", - _async_register_callback, - ): - coordinator.async_register_processor(processor) - cancel_coordinator = coordinator.async_start() + + coordinator.async_register_processor(processor) + cancel_coordinator = coordinator.async_start() processor.async_add_listener(MagicMock()) @@ -812,19 +792,19 @@ async def test_integration_with_entity(hass, mock_bleak_scanner_start): mock_add_entities, ) - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) # First call with just the remote sensor entities results in them being added assert len(mock_add_entities.mock_calls) == 1 - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) # Second call with just the remote sensor entities does not add them again assert len(mock_add_entities.mock_calls) == 1 - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) # Third call with primary and remote sensor entities adds the primary sensor entities assert len(mock_add_entities.mock_calls) == 2 - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) # Forth call with both primary and remote sensor entities does not add them again assert len(mock_add_entities.mock_calls) == 2 @@ -908,20 +888,11 @@ async def test_integration_with_entity_without_a_device(hass, mock_bleak_scanner _mock_update_method, ) assert coordinator.available is False # no data yet - saved_callback = None - - def _async_register_callback(_hass, _callback, _matcher, _mode): - nonlocal saved_callback - saved_callback = _callback - return lambda: None processor = PassiveBluetoothDataProcessor(_async_generate_mock_data) - with patch( - "homeassistant.components.bluetooth.update_coordinator.async_register_callback", - _async_register_callback, - ): - coordinator.async_register_processor(processor) - cancel_coordinator = coordinator.async_start() + + coordinator.async_register_processor(processor) + cancel_coordinator = coordinator.async_start() mock_add_entities = MagicMock() @@ -930,11 +901,11 @@ async def test_integration_with_entity_without_a_device(hass, mock_bleak_scanner mock_add_entities, ) - saved_callback(NO_DEVICES_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + inject_bluetooth_service_info(hass, NO_DEVICES_BLUETOOTH_SERVICE_INFO) # First call with just the remote sensor entities results in them being added assert len(mock_add_entities.mock_calls) == 1 - saved_callback(NO_DEVICES_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + inject_bluetooth_service_info(hass, NO_DEVICES_BLUETOOTH_SERVICE_INFO) # Second call with just the remote sensor entities does not add them again assert len(mock_add_entities.mock_calls) == 1 @@ -982,20 +953,11 @@ async def test_passive_bluetooth_entity_with_entity_platform( _mock_update_method, ) assert coordinator.available is False # no data yet - saved_callback = None - - def _async_register_callback(_hass, _callback, _matcher, _mode): - nonlocal saved_callback - saved_callback = _callback - return lambda: None processor = PassiveBluetoothDataProcessor(_async_generate_mock_data) - with patch( - "homeassistant.components.bluetooth.update_coordinator.async_register_callback", - _async_register_callback, - ): - coordinator.async_register_processor(processor) - cancel_coordinator = coordinator.async_start() + + coordinator.async_register_processor(processor) + cancel_coordinator = coordinator.async_start() processor.async_add_entities_listener( PassiveBluetoothProcessorEntity, @@ -1003,9 +965,9 @@ async def test_passive_bluetooth_entity_with_entity_platform( entity_platform.async_add_entities(entities) ), ) - saved_callback(NO_DEVICES_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + inject_bluetooth_service_info(hass, NO_DEVICES_BLUETOOTH_SERVICE_INFO) await hass.async_block_till_done() - saved_callback(NO_DEVICES_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + inject_bluetooth_service_info(hass, NO_DEVICES_BLUETOOTH_SERVICE_INFO) await hass.async_block_till_done() assert ( hass.states.get("test_domain.test_platform_aa_bb_cc_dd_ee_ff_temperature") @@ -1079,12 +1041,6 @@ async def test_integration_multiple_entity_platforms(hass, mock_bleak_scanner_st _mock_update_method, ) assert coordinator.available is False # no data yet - saved_callback = None - - def _async_register_callback(_hass, _callback, _matcher, _mode): - nonlocal saved_callback - saved_callback = _callback - return lambda: None binary_sensor_processor = PassiveBluetoothDataProcessor( lambda service_info: BINARY_SENSOR_PASSIVE_BLUETOOTH_DATA_UPDATE @@ -1093,13 +1049,9 @@ async def test_integration_multiple_entity_platforms(hass, mock_bleak_scanner_st lambda service_info: SENSOR_PASSIVE_BLUETOOTH_DATA_UPDATE ) - with patch( - "homeassistant.components.bluetooth.update_coordinator.async_register_callback", - _async_register_callback, - ): - coordinator.async_register_processor(binary_sensor_processor) - coordinator.async_register_processor(sesnor_processor) - cancel_coordinator = coordinator.async_start() + coordinator.async_register_processor(binary_sensor_processor) + coordinator.async_register_processor(sesnor_processor) + cancel_coordinator = coordinator.async_start() binary_sensor_processor.async_add_listener(MagicMock()) sesnor_processor.async_add_listener(MagicMock()) @@ -1116,7 +1068,7 @@ async def test_integration_multiple_entity_platforms(hass, mock_bleak_scanner_st mock_add_binary_sensor_entities, ) - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) # First call with just the remote sensor entities results in them being added assert len(mock_add_binary_sensor_entities.mock_calls) == 1 assert len(mock_add_sensor_entities.mock_calls) == 1 @@ -1193,33 +1145,24 @@ async def test_exception_from_coordinator_update_method( _mock_update_method, ) assert coordinator.available is False # no data yet - saved_callback = None - - def _async_register_callback(_hass, _callback, _matcher, _mode): - nonlocal saved_callback - saved_callback = _callback - return lambda: None processor = PassiveBluetoothDataProcessor(_async_generate_mock_data) - with patch( - "homeassistant.components.bluetooth.update_coordinator.async_register_callback", - _async_register_callback, - ): - unregister_processor = coordinator.async_register_processor(processor) - cancel_coordinator = coordinator.async_start() + + unregister_processor = coordinator.async_register_processor(processor) + cancel_coordinator = coordinator.async_start() processor.async_add_listener(MagicMock()) - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) assert processor.available is True # We should go unavailable once we get an exception - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) assert "Test exception" in caplog.text assert processor.available is False # We should go available again once we get data again - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) assert processor.available is True unregister_processor() cancel_coordinator() diff --git a/tests/components/bthome/test_binary_sensor.py b/tests/components/bthome/test_binary_sensor.py index fc7b128d124..64b19b17a81 100644 --- a/tests/components/bthome/test_binary_sensor.py +++ b/tests/components/bthome/test_binary_sensor.py @@ -1,17 +1,16 @@ """Test BTHome binary sensors.""" import logging -from unittest.mock import patch import pytest -from homeassistant.components.bluetooth import BluetoothChange from homeassistant.components.bthome.const import DOMAIN from homeassistant.const import ATTR_FRIENDLY_NAME, STATE_OFF, STATE_ON from . import make_advertisement from tests.common import MockConfigEntry +from tests.components.bluetooth import inject_bluetooth_service_info _LOGGER = logging.getLogger(__name__) @@ -81,25 +80,14 @@ async def test_binary_sensors( ) entry.add_to_hass(hass) - saved_callback = None - - def _async_register_callback(_hass, _callback, _matcher, _mode): - nonlocal saved_callback - saved_callback = _callback - return lambda: None - - with patch( - "homeassistant.components.bluetooth.update_coordinator.async_register_callback", - _async_register_callback, - ): - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 - saved_callback( + inject_bluetooth_service_info( + hass, advertisement, - BluetoothChange.ADVERTISEMENT, ) await hass.async_block_till_done() diff --git a/tests/components/bthome/test_sensor.py b/tests/components/bthome/test_sensor.py index bb0c5b3f459..78b247aa393 100644 --- a/tests/components/bthome/test_sensor.py +++ b/tests/components/bthome/test_sensor.py @@ -1,10 +1,8 @@ """Test the BTHome sensors.""" -from unittest.mock import patch import pytest -from homeassistant.components.bluetooth import BluetoothChange 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 @@ -12,6 +10,7 @@ from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT from . import make_advertisement, make_encrypted_advertisement from tests.common import MockConfigEntry +from tests.components.bluetooth import inject_bluetooth_service_info @pytest.mark.parametrize( @@ -340,25 +339,14 @@ async def test_sensors( ) entry.add_to_hass(hass) - saved_callback = None - - def _async_register_callback(_hass, _callback, _matcher, _mode): - nonlocal saved_callback - saved_callback = _callback - return lambda: None - - with patch( - "homeassistant.components.bluetooth.update_coordinator.async_register_callback", - _async_register_callback, - ): - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 - saved_callback( + inject_bluetooth_service_info( + hass, advertisement, - BluetoothChange.ADVERTISEMENT, ) await hass.async_block_till_done() assert len(hass.states.async_all()) == len(result) diff --git a/tests/components/govee_ble/test_sensor.py b/tests/components/govee_ble/test_sensor.py index e7828fdc496..0e52d2278c3 100644 --- a/tests/components/govee_ble/test_sensor.py +++ b/tests/components/govee_ble/test_sensor.py @@ -1,8 +1,6 @@ """Test the Govee BLE sensors.""" -from unittest.mock import patch -from homeassistant.components.bluetooth import BluetoothChange from homeassistant.components.govee_ble.const import DOMAIN from homeassistant.components.sensor import ATTR_STATE_CLASS from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT @@ -10,6 +8,7 @@ from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT from . import GVH5075_SERVICE_INFO from tests.common import MockConfigEntry +from tests.components.bluetooth import inject_bluetooth_service_info async def test_sensors(hass): @@ -20,22 +19,11 @@ async def test_sensors(hass): ) entry.add_to_hass(hass) - saved_callback = None - - def _async_register_callback(_hass, _callback, _matcher, _mode): - nonlocal saved_callback - saved_callback = _callback - return lambda: None - - with patch( - "homeassistant.components.bluetooth.update_coordinator.async_register_callback", - _async_register_callback, - ): - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 - saved_callback(GVH5075_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + inject_bluetooth_service_info(hass, GVH5075_SERVICE_INFO) await hass.async_block_till_done() assert len(hass.states.async_all()) == 3 diff --git a/tests/components/inkbird/test_sensor.py b/tests/components/inkbird/test_sensor.py index c54c6e3c242..38ff15b6bb1 100644 --- a/tests/components/inkbird/test_sensor.py +++ b/tests/components/inkbird/test_sensor.py @@ -1,8 +1,6 @@ """Test the INKBIRD config flow.""" -from unittest.mock import patch -from homeassistant.components.bluetooth import BluetoothChange from homeassistant.components.inkbird.const import DOMAIN from homeassistant.components.sensor import ATTR_STATE_CLASS from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT @@ -10,6 +8,7 @@ from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT from . import SPS_SERVICE_INFO from tests.common import MockConfigEntry +from tests.components.bluetooth import inject_bluetooth_service_info async def test_sensors(hass): @@ -20,22 +19,11 @@ async def test_sensors(hass): ) entry.add_to_hass(hass) - saved_callback = None - - def _async_register_callback(_hass, _callback, _matcher, _mode): - nonlocal saved_callback - saved_callback = _callback - return lambda: None - - with patch( - "homeassistant.components.bluetooth.update_coordinator.async_register_callback", - _async_register_callback, - ): - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 - saved_callback(SPS_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + inject_bluetooth_service_info(hass, SPS_SERVICE_INFO) await hass.async_block_till_done() assert len(hass.states.async_all()) == 3 diff --git a/tests/components/moat/test_sensor.py b/tests/components/moat/test_sensor.py index 6424144106b..45b9dbc0e8a 100644 --- a/tests/components/moat/test_sensor.py +++ b/tests/components/moat/test_sensor.py @@ -1,8 +1,6 @@ """Test the Moat sensors.""" -from unittest.mock import patch -from homeassistant.components.bluetooth import BluetoothChange from homeassistant.components.moat.const import DOMAIN from homeassistant.components.sensor import ATTR_STATE_CLASS from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT @@ -10,32 +8,22 @@ from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT from . import MOAT_S2_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="61DE521B-F0BF-9F44-64D4-75BBE1738105", + unique_id="aa:bb:cc:dd:ee:ff", ) entry.add_to_hass(hass) - saved_callback = None - - def _async_register_callback(_hass, _callback, _matcher, _mode): - nonlocal saved_callback - saved_callback = _callback - return lambda: None - - with patch( - "homeassistant.components.bluetooth.update_coordinator.async_register_callback", - _async_register_callback, - ): - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 - saved_callback(MOAT_S2_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + inject_bluetooth_service_info(hass, MOAT_S2_SERVICE_INFO) await hass.async_block_till_done() assert len(hass.states.async_all()) == 4 diff --git a/tests/components/qingping/test_binary_sensor.py b/tests/components/qingping/test_binary_sensor.py index 54e863cd158..fc7f79ae8d9 100644 --- a/tests/components/qingping/test_binary_sensor.py +++ b/tests/components/qingping/test_binary_sensor.py @@ -1,14 +1,13 @@ """Test the Qingping binary sensors.""" -from unittest.mock import patch -from homeassistant.components.bluetooth import BluetoothChange from homeassistant.components.qingping.const import DOMAIN from homeassistant.const import ATTR_FRIENDLY_NAME from . import LIGHT_AND_SIGNAL_SERVICE_INFO from tests.common import MockConfigEntry +from tests.components.bluetooth import inject_bluetooth_service_info async def test_binary_sensors(hass): @@ -19,22 +18,11 @@ async def test_binary_sensors(hass): ) entry.add_to_hass(hass) - saved_callback = None - - def _async_register_callback(_hass, _callback, _matcher, _mode): - nonlocal saved_callback - saved_callback = _callback - return lambda: None - - with patch( - "homeassistant.components.bluetooth.update_coordinator.async_register_callback", - _async_register_callback, - ): - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() assert len(hass.states.async_all("binary_sensor")) == 0 - saved_callback(LIGHT_AND_SIGNAL_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + inject_bluetooth_service_info(hass, LIGHT_AND_SIGNAL_SERVICE_INFO) await hass.async_block_till_done() assert len(hass.states.async_all("binary_sensor")) == 1 diff --git a/tests/components/qingping/test_sensor.py b/tests/components/qingping/test_sensor.py index 0f7e7c6a58e..135844c0728 100644 --- a/tests/components/qingping/test_sensor.py +++ b/tests/components/qingping/test_sensor.py @@ -1,8 +1,6 @@ """Test the Qingping sensors.""" -from unittest.mock import patch -from homeassistant.components.bluetooth import BluetoothChange from homeassistant.components.qingping.const import DOMAIN from homeassistant.components.sensor import ATTR_STATE_CLASS from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT @@ -10,6 +8,7 @@ from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT from . import LIGHT_AND_SIGNAL_SERVICE_INFO from tests.common import MockConfigEntry +from tests.components.bluetooth import inject_bluetooth_service_info async def test_sensors(hass): @@ -20,22 +19,11 @@ async def test_sensors(hass): ) entry.add_to_hass(hass) - saved_callback = None - - def _async_register_callback(_hass, _callback, _matcher, _mode): - nonlocal saved_callback - saved_callback = _callback - return lambda: None - - with patch( - "homeassistant.components.bluetooth.update_coordinator.async_register_callback", - _async_register_callback, - ): - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() assert len(hass.states.async_all("sensor")) == 0 - saved_callback(LIGHT_AND_SIGNAL_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + inject_bluetooth_service_info(hass, LIGHT_AND_SIGNAL_SERVICE_INFO) await hass.async_block_till_done() assert len(hass.states.async_all("sensor")) == 1 diff --git a/tests/components/sensorpro/test_sensor.py b/tests/components/sensorpro/test_sensor.py index 0d27d07995f..242c845a9ce 100644 --- a/tests/components/sensorpro/test_sensor.py +++ b/tests/components/sensorpro/test_sensor.py @@ -1,8 +1,6 @@ """Test the SensorPro sensors.""" -from unittest.mock import patch -from homeassistant.components.bluetooth import BluetoothChange from homeassistant.components.sensor import ATTR_STATE_CLASS from homeassistant.components.sensorpro.const import DOMAIN from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT @@ -10,6 +8,7 @@ from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT from . import SENSORPRO_SERVICE_INFO from tests.common import MockConfigEntry +from tests.components.bluetooth import inject_bluetooth_service_info async def test_sensors(hass): @@ -20,22 +19,11 @@ async def test_sensors(hass): ) entry.add_to_hass(hass) - saved_callback = None - - def _async_register_callback(_hass, _callback, _matcher, _mode): - nonlocal saved_callback - saved_callback = _callback - return lambda: None - - with patch( - "homeassistant.components.bluetooth.update_coordinator.async_register_callback", - _async_register_callback, - ): - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() assert len(hass.states.async_all("sensor")) == 0 - saved_callback(SENSORPRO_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + inject_bluetooth_service_info(hass, SENSORPRO_SERVICE_INFO) await hass.async_block_till_done() assert len(hass.states.async_all("sensor")) == 4 diff --git a/tests/components/sensorpush/test_sensor.py b/tests/components/sensorpush/test_sensor.py index 34179985d78..ae3cbe047de 100644 --- a/tests/components/sensorpush/test_sensor.py +++ b/tests/components/sensorpush/test_sensor.py @@ -1,8 +1,5 @@ -"""Test the SensorPush config flow.""" +"""Test the SensorPush sensors.""" -from unittest.mock import patch - -from homeassistant.components.bluetooth import BluetoothChange from homeassistant.components.sensor import ATTR_STATE_CLASS from homeassistant.components.sensorpush.const import DOMAIN from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT @@ -10,32 +7,22 @@ from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT from . import HTPWX_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="61DE521B-F0BF-9F44-64D4-75BBE1738105", + unique_id="4125DDBA-2774-4851-9889-6AADDD4CAC3D", ) entry.add_to_hass(hass) - saved_callback = None - - def _async_register_callback(_hass, _callback, _matcher, _mode): - nonlocal saved_callback - saved_callback = _callback - return lambda: None - - with patch( - "homeassistant.components.bluetooth.update_coordinator.async_register_callback", - _async_register_callback, - ): - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 - saved_callback(HTPWX_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + inject_bluetooth_service_info(hass, HTPWX_SERVICE_INFO) await hass.async_block_till_done() assert len(hass.states.async_all()) == 3 diff --git a/tests/components/thermobeacon/test_sensor.py b/tests/components/thermobeacon/test_sensor.py index 147f37787b8..69619c94bbe 100644 --- a/tests/components/thermobeacon/test_sensor.py +++ b/tests/components/thermobeacon/test_sensor.py @@ -1,8 +1,6 @@ """Test the ThermoBeacon sensors.""" -from unittest.mock import patch -from homeassistant.components.bluetooth import BluetoothChange from homeassistant.components.sensor import ATTR_STATE_CLASS from homeassistant.components.thermobeacon.const import DOMAIN from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT @@ -10,6 +8,7 @@ from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT from . import THERMOBEACON_SERVICE_INFO from tests.common import MockConfigEntry +from tests.components.bluetooth import inject_bluetooth_service_info async def test_sensors(hass): @@ -20,22 +19,11 @@ async def test_sensors(hass): ) entry.add_to_hass(hass) - saved_callback = None - - def _async_register_callback(_hass, _callback, _matcher, _mode): - nonlocal saved_callback - saved_callback = _callback - return lambda: None - - with patch( - "homeassistant.components.bluetooth.update_coordinator.async_register_callback", - _async_register_callback, - ): - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() assert len(hass.states.async_all("sensor")) == 0 - saved_callback(THERMOBEACON_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + inject_bluetooth_service_info(hass, THERMOBEACON_SERVICE_INFO) await hass.async_block_till_done() assert len(hass.states.async_all("sensor")) == 4 diff --git a/tests/components/thermopro/test_sensor.py b/tests/components/thermopro/test_sensor.py index 908101faf56..34b2b1a5aeb 100644 --- a/tests/components/thermopro/test_sensor.py +++ b/tests/components/thermopro/test_sensor.py @@ -1,8 +1,5 @@ """Test the ThermoPro config flow.""" -from unittest.mock import patch - -from homeassistant.components.bluetooth import BluetoothChange from homeassistant.components.sensor import ATTR_STATE_CLASS from homeassistant.components.thermopro.const import DOMAIN from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT @@ -10,32 +7,22 @@ from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT from . import TP357_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="61DE521B-F0BF-9F44-64D4-75BBE1738105", + unique_id="4125DDBA-2774-4851-9889-6AADDD4CAC3D", ) entry.add_to_hass(hass) - saved_callback = None - - def _async_register_callback(_hass, _callback, _matcher, _mode): - nonlocal saved_callback - saved_callback = _callback - return lambda: None - - with patch( - "homeassistant.components.bluetooth.update_coordinator.async_register_callback", - _async_register_callback, - ): - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 - saved_callback(TP357_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + inject_bluetooth_service_info(hass, TP357_SERVICE_INFO) await hass.async_block_till_done() assert len(hass.states.async_all()) == 2 diff --git a/tests/components/tilt_ble/test_sensor.py b/tests/components/tilt_ble/test_sensor.py index cfd83cb6573..28454034864 100644 --- a/tests/components/tilt_ble/test_sensor.py +++ b/tests/components/tilt_ble/test_sensor.py @@ -2,9 +2,6 @@ from __future__ import annotations -from unittest.mock import patch - -from homeassistant.components.bluetooth import BluetoothCallback, BluetoothChange from homeassistant.components.sensor import ATTR_STATE_CLASS from homeassistant.components.tilt_ble.const import DOMAIN from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT @@ -13,33 +10,22 @@ from homeassistant.core import HomeAssistant from . import TILT_GREEN_SERVICE_INFO from tests.common import MockConfigEntry +from tests.components.bluetooth import inject_bluetooth_service_info async def test_sensors(hass: HomeAssistant): """Test setting up creates the sensors.""" entry = MockConfigEntry( domain=DOMAIN, - unique_id="61DE521B-F0BF-9F44-64D4-75BBE1738105", + unique_id="F6:0F:28:F2:1F:CB", ) entry.add_to_hass(hass) - saved_callback: BluetoothCallback | None = None - - def _async_register_callback(_hass, _callback, _matcher, _mode): - nonlocal saved_callback - saved_callback = _callback - return lambda: None - - with patch( - "homeassistant.components.bluetooth.update_coordinator.async_register_callback", - _async_register_callback, - ): - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 - assert saved_callback is not None - saved_callback(TILT_GREEN_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + inject_bluetooth_service_info(hass, TILT_GREEN_SERVICE_INFO) await hass.async_block_till_done() assert len(hass.states.async_all()) == 2 diff --git a/tests/components/xiaomi_ble/test_binary_sensor.py b/tests/components/xiaomi_ble/test_binary_sensor.py index dd49b4d181d..eb369f20268 100644 --- a/tests/components/xiaomi_ble/test_binary_sensor.py +++ b/tests/components/xiaomi_ble/test_binary_sensor.py @@ -1,14 +1,12 @@ """Test Xiaomi binary sensors.""" -from unittest.mock import patch - -from homeassistant.components.bluetooth import BluetoothChange from homeassistant.components.xiaomi_ble.const import DOMAIN from homeassistant.const import ATTR_FRIENDLY_NAME from . import make_advertisement from tests.common import MockConfigEntry +from tests.components.bluetooth import inject_bluetooth_service_info_bleak async def test_smoke_sensor(hass): @@ -20,27 +18,16 @@ async def test_smoke_sensor(hass): ) entry.add_to_hass(hass) - saved_callback = None - - def _async_register_callback(_hass, _callback, _matcher, _mode): - nonlocal saved_callback - saved_callback = _callback - return lambda: None - - with patch( - "homeassistant.components.bluetooth.update_coordinator.async_register_callback", - _async_register_callback, - ): - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 - saved_callback( + inject_bluetooth_service_info_bleak( + hass, make_advertisement( "54:EF:44:E3:9C:BC", b"XY\x97\tf\xbc\x9c\xe3D\xefT\x01" b"\x08\x12\x05\x00\x00\x00q^\xbe\x90", ), - BluetoothChange.ADVERTISEMENT, ) await hass.async_block_till_done() assert len(hass.states.async_all()) == 1 @@ -62,29 +49,18 @@ async def test_moisture(hass): ) entry.add_to_hass(hass) - saved_callback = None - - def _async_register_callback(_hass, _callback, _matcher, _mode): - nonlocal saved_callback - saved_callback = _callback - return lambda: None - - with patch( - "homeassistant.components.bluetooth.update_coordinator.async_register_callback", - _async_register_callback, - ): - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 # WARNING: This test data is synthetic, rather than captured from a real device # obj type is 0x1014, payload len is 0x2 and payload is 0xf400 - saved_callback( + inject_bluetooth_service_info_bleak( + hass, make_advertisement( "C4:7C:8D:6A:3E:7A", b"q \x5d\x01iz>j\x8d|\xc4\r\x14\x10\x02\xf4\x00" ), - BluetoothChange.ADVERTISEMENT, ) await hass.async_block_till_done() diff --git a/tests/components/xiaomi_ble/test_sensor.py b/tests/components/xiaomi_ble/test_sensor.py index 25c118bed49..17f9254b5ff 100644 --- a/tests/components/xiaomi_ble/test_sensor.py +++ b/tests/components/xiaomi_ble/test_sensor.py @@ -1,11 +1,6 @@ """Test the Xiaomi config flow.""" -from unittest.mock import patch -from homeassistant.components.bluetooth import ( - BluetoothChange, - async_get_advertisement_callback, -) from homeassistant.components.sensor import ATTR_STATE_CLASS from homeassistant.components.xiaomi_ble.const import DOMAIN from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT @@ -13,6 +8,7 @@ from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT from . import MMC_T201_1_SERVICE_INFO, make_advertisement from tests.common import MockConfigEntry +from tests.components.bluetooth import inject_bluetooth_service_info_bleak async def test_sensors(hass): @@ -23,22 +19,11 @@ async def test_sensors(hass): ) entry.add_to_hass(hass) - saved_callback = None - - def _async_register_callback(_hass, _callback, _matcher, _mode): - nonlocal saved_callback - saved_callback = _callback - return lambda: None - - with patch( - "homeassistant.components.bluetooth.update_coordinator.async_register_callback", - _async_register_callback, - ): - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 - saved_callback(MMC_T201_1_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + inject_bluetooth_service_info_bleak(hass, MMC_T201_1_SERVICE_INFO) await hass.async_block_till_done() assert len(hass.states.async_all()) == 2 @@ -63,29 +48,18 @@ async def test_xiaomi_formaldeyhde(hass): ) entry.add_to_hass(hass) - saved_callback = None - - def _async_register_callback(_hass, _callback, _matcher, _mode): - nonlocal saved_callback - saved_callback = _callback - return lambda: None - - with patch( - "homeassistant.components.bluetooth.update_coordinator.async_register_callback", - _async_register_callback, - ): - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 # WARNING: This test data is synthetic, rather than captured from a real device # obj type is 0x1010, payload len is 0x2 and payload is 0xf400 - saved_callback( + inject_bluetooth_service_info_bleak( + hass, make_advertisement( "C4:7C:8D:6A:3E:7A", b"q \x5d\x01iz>j\x8d|\xc4\r\x10\x10\x02\xf4\x00" ), - BluetoothChange.ADVERTISEMENT, ) await hass.async_block_till_done() @@ -110,29 +84,18 @@ async def test_xiaomi_consumable(hass): ) entry.add_to_hass(hass) - saved_callback = None - - def _async_register_callback(_hass, _callback, _matcher, _mode): - nonlocal saved_callback - saved_callback = _callback - return lambda: None - - with patch( - "homeassistant.components.bluetooth.update_coordinator.async_register_callback", - _async_register_callback, - ): - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 # WARNING: This test data is synthetic, rather than captured from a real device # obj type is 0x1310, payload len is 0x2 and payload is 0x6000 - saved_callback( + inject_bluetooth_service_info_bleak( + hass, make_advertisement( "C4:7C:8D:6A:3E:7A", b"q \x5d\x01iz>j\x8d|\xc4\r\x13\x10\x02\x60\x00" ), - BluetoothChange.ADVERTISEMENT, ) await hass.async_block_till_done() @@ -157,29 +120,16 @@ async def test_xiaomi_battery_voltage(hass): ) entry.add_to_hass(hass) - saved_callback = None - - def _async_register_callback(_hass, _callback, _matcher, _mode): - nonlocal saved_callback - saved_callback = _callback - return lambda: None - - with patch( - "homeassistant.components.bluetooth.update_coordinator.async_register_callback", - _async_register_callback, - ): - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - assert len(hass.states.async_all()) == 0 + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() # WARNING: This test data is synthetic, rather than captured from a real device # obj type is 0x0a10, payload len is 0x2 and payload is 0x6400 - saved_callback( + inject_bluetooth_service_info_bleak( + hass, make_advertisement( "C4:7C:8D:6A:3E:7A", b"q \x5d\x01iz>j\x8d|\xc4\r\x0a\x10\x02\x64\x00" ), - BluetoothChange.ADVERTISEMENT, ) await hass.async_block_till_done() @@ -211,44 +161,33 @@ async def test_xiaomi_HHCCJCY01(hass): ) entry.add_to_hass(hass) - saved_callback = None - - def _async_register_callback(_hass, _callback, _matcher, _mode): - nonlocal saved_callback - saved_callback = _callback - return lambda: None - - with patch( - "homeassistant.components.bluetooth.update_coordinator.async_register_callback", - _async_register_callback, - ): - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 - saved_callback( + inject_bluetooth_service_info_bleak( + hass, make_advertisement( "C4:7C:8D:6A:3E:7A", b"q \x98\x00fz>j\x8d|\xc4\r\x07\x10\x03\x00\x00\x00" ), - BluetoothChange.ADVERTISEMENT, ) - saved_callback( + inject_bluetooth_service_info_bleak( + hass, make_advertisement( "C4:7C:8D:6A:3E:7A", b"q \x98\x00hz>j\x8d|\xc4\r\t\x10\x02W\x02" ), - BluetoothChange.ADVERTISEMENT, ) - saved_callback( + inject_bluetooth_service_info_bleak( + hass, make_advertisement( "C4:7C:8D:6A:3E:7A", b"q \x98\x00Gz>j\x8d|\xc4\r\x08\x10\x01@" ), - BluetoothChange.ADVERTISEMENT, ) - saved_callback( + inject_bluetooth_service_info_bleak( + hass, make_advertisement( "C4:7C:8D:6A:3E:7A", b"q \x98\x00iz>j\x8d|\xc4\r\x04\x10\x02\xf4\x00" ), - BluetoothChange.ADVERTISEMENT, ) await hass.async_block_till_done() assert len(hass.states.async_all()) == 5 @@ -296,56 +235,45 @@ async def test_xiaomi_HHCCJCY01_not_connectable(hass): """This device has multiple advertisements before all sensors are visible but not connectable.""" entry = MockConfigEntry( domain=DOMAIN, - unique_id="C4:7C:8D:6A:3E:7B", + unique_id="C4:7C:8D:6A:3E:7A", ) entry.add_to_hass(hass) - saved_callback = None - - def _async_register_callback(_hass, _callback, _matcher, _mode): - nonlocal saved_callback - saved_callback = _callback - return lambda: None - - with patch( - "homeassistant.components.bluetooth.update_coordinator.async_register_callback", - _async_register_callback, - ): - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 - saved_callback( + inject_bluetooth_service_info_bleak( + hass, make_advertisement( "C4:7C:8D:6A:3E:7A", b"q \x98\x00fz>j\x8d|\xc4\r\x07\x10\x03\x00\x00\x00", connectable=False, ), - BluetoothChange.ADVERTISEMENT, ) - saved_callback( + inject_bluetooth_service_info_bleak( + hass, make_advertisement( "C4:7C:8D:6A:3E:7A", b"q \x98\x00hz>j\x8d|\xc4\r\t\x10\x02W\x02", connectable=False, ), - BluetoothChange.ADVERTISEMENT, ) - saved_callback( + inject_bluetooth_service_info_bleak( + hass, make_advertisement( "C4:7C:8D:6A:3E:7A", b"q \x98\x00Gz>j\x8d|\xc4\r\x08\x10\x01@", connectable=False, ), - BluetoothChange.ADVERTISEMENT, ) - saved_callback( + inject_bluetooth_service_info_bleak( + hass, make_advertisement( "C4:7C:8D:6A:3E:7A", b"q \x98\x00iz>j\x8d|\xc4\r\x04\x10\x02\xf4\x00", connectable=False, ), - BluetoothChange.ADVERTISEMENT, ) await hass.async_block_till_done() assert len(hass.states.async_all()) == 4 @@ -392,34 +320,36 @@ async def test_xiaomi_HHCCJCY01_only_some_sources_connectable(hass): ) entry.add_to_hass(hass) - saved_callback = async_get_advertisement_callback(hass) - assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 - saved_callback( + inject_bluetooth_service_info_bleak( + hass, make_advertisement( "C4:7C:8D:6A:3E:7A", b"q \x98\x00fz>j\x8d|\xc4\r\x07\x10\x03\x00\x00\x00", connectable=True, ), ) - saved_callback( + inject_bluetooth_service_info_bleak( + hass, make_advertisement( "C4:7C:8D:6A:3E:7A", b"q \x98\x00hz>j\x8d|\xc4\r\t\x10\x02W\x02", connectable=False, ), ) - saved_callback( + inject_bluetooth_service_info_bleak( + hass, make_advertisement( "C4:7C:8D:6A:3E:7A", b"q \x98\x00Gz>j\x8d|\xc4\r\x08\x10\x01@", connectable=False, ), ) - saved_callback( + inject_bluetooth_service_info_bleak( + hass, make_advertisement( "C4:7C:8D:6A:3E:7A", b"q \x98\x00iz>j\x8d|\xc4\r\x04\x10\x02\xf4\x00", @@ -477,27 +407,16 @@ async def test_xiaomi_CGDK2(hass): ) entry.add_to_hass(hass) - saved_callback = None - - def _async_register_callback(_hass, _callback, _matcher, _mode): - nonlocal saved_callback - saved_callback = _callback - return lambda: None - - with patch( - "homeassistant.components.bluetooth.update_coordinator.async_register_callback", - _async_register_callback, - ): - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 - saved_callback( + inject_bluetooth_service_info_bleak( + hass, make_advertisement( "58:2D:34:12:20:89", b"XXo\x06\x07\x89 \x124-X_\x17m\xd5O\x02\x00\x00/\xa4S\xfa", ), - BluetoothChange.ADVERTISEMENT, ) await hass.async_block_till_done() assert len(hass.states.async_all()) == 1 From fbd265aa2d407fe37528f12d503a7ae91e7a3284 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 16 Sep 2022 12:51:58 +0200 Subject: [PATCH 461/955] Update pyupgrade to v2.38.0 (#78573) --- .pre-commit-config.yaml | 2 +- requirements_test_pre_commit.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7900d7bda27..8442d7abecc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/asottile/pyupgrade - rev: v2.37.3 + rev: v2.38.0 hooks: - id: pyupgrade args: [--py39-plus] diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 0b1d9e97d39..1bb2e0a70e4 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -12,5 +12,5 @@ mccabe==0.6.1 pycodestyle==2.8.0 pydocstyle==6.1.1 pyflakes==2.4.0 -pyupgrade==2.37.3 +pyupgrade==2.38.0 yamllint==1.27.1 From 383c83d15f255e2c4c123915259c513af7fda0c1 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 16 Sep 2022 12:52:24 +0200 Subject: [PATCH 462/955] Improve notify typing (#78575) --- homeassistant/components/notify/legacy.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/notify/legacy.py b/homeassistant/components/notify/legacy.py index 3d6d1582848..c3bb02896e0 100644 --- a/homeassistant/components/notify/legacy.py +++ b/homeassistant/components/notify/legacy.py @@ -2,7 +2,7 @@ from __future__ import annotations import asyncio -from collections.abc import Coroutine +from collections.abc import Callable, Coroutine from functools import partial from typing import Any, Optional, Protocol, cast @@ -162,9 +162,11 @@ async def async_reload(hass: HomeAssistant, integration_name: str) -> None: if not _async_integration_has_notify_services(hass, integration_name): return + notify_services: list[BaseNotificationService] = hass.data[NOTIFY_SERVICES][ + integration_name + ] tasks = [ - notify_service.async_register_services() - for notify_service in hass.data[NOTIFY_SERVICES][integration_name] + notify_service.async_register_services() for notify_service in notify_services ] await asyncio.gather(*tasks) @@ -173,15 +175,20 @@ async def async_reload(hass: HomeAssistant, integration_name: str) -> None: @bind_hass async def async_reset_platform(hass: HomeAssistant, integration_name: str) -> None: """Unregister notify services for an integration.""" - if NOTIFY_DISCOVERY_DISPATCHER in hass.data: - hass.data[NOTIFY_DISCOVERY_DISPATCHER]() + notify_discovery_dispatcher: Callable[[], None] | None = hass.data.get( + NOTIFY_DISCOVERY_DISPATCHER + ) + if notify_discovery_dispatcher: + notify_discovery_dispatcher() hass.data[NOTIFY_DISCOVERY_DISPATCHER] = None if not _async_integration_has_notify_services(hass, integration_name): return + notify_services: list[BaseNotificationService] = hass.data[NOTIFY_SERVICES][ + integration_name + ] tasks = [ - notify_service.async_unregister_services() - for notify_service in hass.data[NOTIFY_SERVICES][integration_name] + notify_service.async_unregister_services() for notify_service in notify_services ] await asyncio.gather(*tasks) From 491177e5d367d595b6b8d042bfc5d491a17484c7 Mon Sep 17 00:00:00 2001 From: Jafar Atili Date: Fri, 16 Sep 2022 15:19:50 +0300 Subject: [PATCH 463/955] Address late review of SwitchBee (#78412) --- .coveragerc | 1 + .../components/switchbee/__init__.py | 115 ++------------- .../components/switchbee/config_flow.py | 51 +------ homeassistant/components/switchbee/const.py | 10 -- .../components/switchbee/coordinator.py | 74 ++++++++++ .../components/switchbee/manifest.json | 2 +- .../components/switchbee/strings.json | 12 +- homeassistant/components/switchbee/switch.py | 90 +++++++----- .../components/switchbee/translations/en.json | 48 +++---- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/switchbee/__init__.py | 135 ------------------ .../switchbee/fixtures/switchbee.json | 135 ++++++++++++++++++ .../components/switchbee/test_config_flow.py | 64 +++------ 14 files changed, 313 insertions(+), 428 deletions(-) create mode 100644 homeassistant/components/switchbee/coordinator.py create mode 100644 tests/components/switchbee/fixtures/switchbee.json diff --git a/.coveragerc b/.coveragerc index ec883a005d5..e2eda11cb1e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1215,6 +1215,7 @@ omit = homeassistant/components/swisscom/device_tracker.py homeassistant/components/switchbee/__init__.py homeassistant/components/switchbee/const.py + homeassistant/components/switchbee/coordinator.py homeassistant/components/switchbee/switch.py homeassistant/components/switchbot/__init__.py homeassistant/components/switchbot/binary_sensor.py diff --git a/homeassistant/components/switchbee/__init__.py b/homeassistant/components/switchbee/__init__.py index ca8d792111b..a5523c51a7d 100644 --- a/homeassistant/components/switchbee/__init__.py +++ b/homeassistant/components/switchbee/__init__.py @@ -2,29 +2,16 @@ from __future__ import annotations -from datetime import timedelta -import logging - from switchbee.api import CentralUnitAPI, SwitchBeeError -from switchbee.device import DeviceType from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.device_registry import format_mac -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed - -from .const import ( - CONF_DEFUALT_ALLOWED, - CONF_DEVICES, - CONF_SWITCHES_AS_LIGHTS, - DOMAIN, - SCAN_INTERVAL_SEC, -) - -_LOGGER = logging.getLogger(__name__) +from .const import DOMAIN +from .coordinator import SwitchBeeCoordinator PLATFORMS: list[Platform] = [Platform.SWITCH] @@ -35,30 +22,24 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: central_unit = entry.data[CONF_HOST] user = entry.data[CONF_USERNAME] password = entry.data[CONF_PASSWORD] - devices_map: dict[str, DeviceType] = {s.display: s for s in DeviceType} - allowed_devices = [ - devices_map[device] - for device in entry.options.get(CONF_DEVICES, CONF_DEFUALT_ALLOWED) - ] + websession = async_get_clientsession(hass, verify_ssl=False) api = CentralUnitAPI(central_unit, user, password, websession) try: await api.connect() - except SwitchBeeError: - return False + except SwitchBeeError as exp: + raise ConfigEntryNotReady("Failed to connect to the Central Unit") from exp coordinator = SwitchBeeCoordinator( hass, api, - SCAN_INTERVAL_SEC, - allowed_devices, - entry.data[CONF_SWITCHES_AS_LIGHTS], ) + await coordinator.async_config_entry_first_refresh() entry.async_on_unload(entry.add_update_listener(update_listener)) hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True @@ -74,83 +55,3 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def update_listener(hass: HomeAssistant, config_entry: ConfigEntry) -> None: """Update listener.""" await hass.config_entries.async_reload(config_entry.entry_id) - - -class SwitchBeeCoordinator(DataUpdateCoordinator): - """Class to manage fetching Freedompro data API.""" - - def __init__( - self, - hass, - swb_api, - scan_interval, - devices: list[DeviceType], - switch_as_light: bool, - ): - """Initialize.""" - self._api: CentralUnitAPI = swb_api - self._reconnect_counts: int = 0 - self._devices_to_include: list[DeviceType] = devices - self._prev_devices_to_include_to_include: list[DeviceType] = [] - self._mac_addr_fmt: str = format_mac(swb_api.mac) - self._switch_as_light = switch_as_light - super().__init__( - hass, - _LOGGER, - name=DOMAIN, - update_interval=timedelta(seconds=scan_interval), - ) - - @property - def api(self) -> CentralUnitAPI: - """Return SwitchBee API object.""" - return self._api - - @property - def mac_formated(self) -> str: - """Return formatted MAC address.""" - return self._mac_addr_fmt - - @property - def switch_as_light(self) -> bool: - """Return switch_as_ligh config.""" - return self._switch_as_light - - async def _async_update_data(self): - - if self._reconnect_counts != self._api.reconnect_count: - self._reconnect_counts = self._api.reconnect_count - _LOGGER.debug( - "Central Unit re-connected again due to invalid token, total %i", - self._reconnect_counts, - ) - - config_changed = False - - if set(self._prev_devices_to_include_to_include) != set( - self._devices_to_include - ): - self._prev_devices_to_include_to_include = self._devices_to_include - config_changed = True - - # The devices are loaded once during the config_entry - if not self._api.devices or config_changed: - # Try to load the devices from the CU for the first time - try: - await self._api.fetch_configuration(self._devices_to_include) - except SwitchBeeError as exp: - raise UpdateFailed( - f"Error communicating with API: {exp}" - ) from SwitchBeeError - else: - _LOGGER.debug("Loaded devices") - - # Get the state of the devices - try: - await self._api.fetch_states() - except SwitchBeeError as exp: - raise UpdateFailed( - f"Error communicating with API: {exp}" - ) from SwitchBeeError - else: - return self._api.devices diff --git a/homeassistant/components/switchbee/config_flow.py b/homeassistant/components/switchbee/config_flow.py index 38d33bd4981..c20878cc2c2 100644 --- a/homeassistant/components/switchbee/config_flow.py +++ b/homeassistant/components/switchbee/config_flow.py @@ -5,19 +5,18 @@ import logging from typing import Any from switchbee.api import CentralUnitAPI, SwitchBeeError -from switchbee.device import DeviceType import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import format_mac -from .const import CONF_DEFUALT_ALLOWED, CONF_DEVICES, CONF_SWITCHES_AS_LIGHTS, DOMAIN +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -26,7 +25,6 @@ STEP_USER_DATA_SCHEMA = vol.Schema( vol.Required(CONF_HOST): cv.string, vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_SWITCHES_AS_LIGHTS, default=False): cv.boolean, } ) @@ -43,9 +41,9 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]): except SwitchBeeError as exp: _LOGGER.error(exp) if "LOGIN_FAILED" in str(exp): - raise InvalidAuth from SwitchBeeError + raise InvalidAuth from exp - raise CannotConnect from SwitchBeeError + raise CannotConnect from exp return format_mac(api.mac) @@ -83,47 +81,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors ) - @staticmethod - @callback - def async_get_options_flow( - config_entry: config_entries.ConfigEntry, - ) -> OptionsFlowHandler: - """Get the options flow for this handler.""" - return OptionsFlowHandler(config_entry) - - -class OptionsFlowHandler(config_entries.OptionsFlow): - """Handle a option flow for AEMET.""" - - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: - """Initialize options flow.""" - self.config_entry = config_entry - - async def async_step_init(self, user_input=None) -> FlowResult: - """Handle options flow.""" - - if user_input is not None: - return self.async_create_entry(title="", data=user_input) - - all_devices = [ - DeviceType.Switch, - DeviceType.TimedSwitch, - DeviceType.GroupSwitch, - DeviceType.TimedPowerSwitch, - ] - - data_schema = { - vol.Required( - CONF_DEVICES, - default=self.config_entry.options.get( - CONF_DEVICES, - CONF_DEFUALT_ALLOWED, - ), - ): cv.multi_select([device.display for device in all_devices]), - } - - return self.async_show_form(step_id="init", data_schema=vol.Schema(data_schema)) - class CannotConnect(HomeAssistantError): """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/switchbee/const.py b/homeassistant/components/switchbee/const.py index be818346589..12cc5e77a63 100644 --- a/homeassistant/components/switchbee/const.py +++ b/homeassistant/components/switchbee/const.py @@ -1,14 +1,4 @@ """Constants for the SwitchBee Smart Home integration.""" -from switchbee.device import DeviceType - DOMAIN = "switchbee" SCAN_INTERVAL_SEC = 5 -CONF_SCAN_INTERVAL = "scan_interval" -CONF_SWITCHES_AS_LIGHTS = "switch_as_light" -CONF_DEVICES = "devices" -CONF_DEFUALT_ALLOWED = [ - DeviceType.Switch.display, - DeviceType.TimedPowerSwitch.display, - DeviceType.TimedSwitch.display, -] diff --git a/homeassistant/components/switchbee/coordinator.py b/homeassistant/components/switchbee/coordinator.py new file mode 100644 index 00000000000..d43f3602e29 --- /dev/null +++ b/homeassistant/components/switchbee/coordinator.py @@ -0,0 +1,74 @@ +"""SwitchBee integration Coordinator.""" + +from datetime import timedelta +import logging + +from switchbee.api import CentralUnitAPI, SwitchBeeError +from switchbee.device import DeviceType, SwitchBeeBaseDevice + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import format_mac +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN, SCAN_INTERVAL_SEC + +_LOGGER = logging.getLogger(__name__) + + +class SwitchBeeCoordinator(DataUpdateCoordinator[dict[int, SwitchBeeBaseDevice]]): + """Class to manage fetching Freedompro data API.""" + + def __init__( + self, + hass: HomeAssistant, + swb_api: CentralUnitAPI, + ) -> None: + """Initialize.""" + self.api: CentralUnitAPI = swb_api + self._reconnect_counts: int = 0 + self.mac_formated: str = format_mac(swb_api.mac) + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=timedelta(seconds=SCAN_INTERVAL_SEC), + ) + + async def _async_update_data(self) -> dict[int, SwitchBeeBaseDevice]: + """Update data via library.""" + + if self._reconnect_counts != self.api.reconnect_count: + self._reconnect_counts = self.api.reconnect_count + _LOGGER.debug( + "Central Unit re-connected again due to invalid token, total %i", + self._reconnect_counts, + ) + + # The devices are loaded once during the config_entry + if not self.api.devices: + # Try to load the devices from the CU for the first time + try: + await self.api.fetch_configuration( + [ + DeviceType.Switch, + DeviceType.TimedSwitch, + DeviceType.GroupSwitch, + DeviceType.TimedPowerSwitch, + ] + ) + except SwitchBeeError as exp: + raise UpdateFailed( + f"Error communicating with API: {exp}" + ) from SwitchBeeError + else: + _LOGGER.debug("Loaded devices") + + # Get the state of the devices + try: + await self.api.fetch_states() + except SwitchBeeError as exp: + raise UpdateFailed( + f"Error communicating with API: {exp}" + ) from SwitchBeeError + + return self.api.devices diff --git a/homeassistant/components/switchbee/manifest.json b/homeassistant/components/switchbee/manifest.json index ba0e4a454ce..f368fa1e3fa 100644 --- a/homeassistant/components/switchbee/manifest.json +++ b/homeassistant/components/switchbee/manifest.json @@ -3,7 +3,7 @@ "name": "SwitchBee", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/switchbee", - "requirements": ["pyswitchbee==1.4.7"], + "requirements": ["pyswitchbee==1.4.8"], "codeowners": ["@jafar-atili"], "iot_class": "local_polling" } diff --git a/homeassistant/components/switchbee/strings.json b/homeassistant/components/switchbee/strings.json index 531e19fda00..36fbb36d065 100644 --- a/homeassistant/components/switchbee/strings.json +++ b/homeassistant/components/switchbee/strings.json @@ -6,8 +6,7 @@ "data": { "host": "[%key:common::config_flow::data::host%]", "username": "[%key:common::config_flow::data::username%]", - "password": "[%key:common::config_flow::data::password%]", - "switch_as_light": "Initialize switches as light entities" + "password": "[%key:common::config_flow::data::password%]" } } }, @@ -19,14 +18,5 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } - }, - "options": { - "step": { - "init": { - "data": { - "devices": "Devices to include" - } - } - } } } diff --git a/homeassistant/components/switchbee/switch.py b/homeassistant/components/switchbee/switch.py index de648d4232c..04320dcf4cc 100644 --- a/homeassistant/components/switchbee/switch.py +++ b/homeassistant/components/switchbee/switch.py @@ -9,13 +9,13 @@ from switchbee.device import ApiStateCommand, DeviceType, SwitchBeeBaseDevice from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import aiohttp_client +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import SwitchBeeCoordinator from .const import DOMAIN +from .coordinator import SwitchBeeCoordinator _LOGGER = logging.getLogger(__name__) @@ -25,37 +25,35 @@ async def async_setup_entry( ) -> None: """Set up Switchbee switch.""" coordinator: SwitchBeeCoordinator = hass.data[DOMAIN][entry.entry_id] - device_types = ( - [DeviceType.TimedPowerSwitch] - if coordinator.switch_as_light - else [ + + async_add_entities( + SwitchBeeSwitchEntity(device, coordinator) + for device in coordinator.data.values() + if device.type + in [ DeviceType.TimedPowerSwitch, DeviceType.GroupSwitch, DeviceType.Switch, DeviceType.TimedSwitch, - DeviceType.TwoWay, ] ) - async_add_entities( - Device(hass, device, coordinator) - for device in coordinator.data.values() - if device.type in device_types - ) - -class Device(CoordinatorEntity, SwitchEntity): +class SwitchBeeSwitchEntity(CoordinatorEntity[SwitchBeeCoordinator], SwitchEntity): """Representation of an Switchbee switch.""" - def __init__(self, hass, device: SwitchBeeBaseDevice, coordinator): + def __init__( + self, + device: SwitchBeeBaseDevice, + coordinator: SwitchBeeCoordinator, + ) -> None: """Initialize the Switchbee switch.""" super().__init__(coordinator) - self._session = aiohttp_client.async_get_clientsession(hass) self._attr_name = f"{device.name}" self._device_id = device.id self._attr_unique_id = f"{coordinator.mac_formated}-{device.id}" self._attr_is_on = False - self._attr_available = True + self._is_online = True self._attr_has_entity_name = True self._device = device self._attr_device_info = DeviceInfo( @@ -75,11 +73,32 @@ class Device(CoordinatorEntity, SwitchEntity): ), ) + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._is_online and self.coordinator.last_update_success + @callback def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" + self._update_from_coordinator() + super()._handle_coordinator_update() + + def _update_from_coordinator(self) -> None: + """Update the entity attributes from the coordinator data.""" async def async_refresh_state(): + """Refresh the device state in the Central Unit. + + This function addresses issue of a device that came online back but still report + unavailable state (-1). + Such device (offline device) will keep reporting unavailable state (-1) + until it has been actuated by the user (state changed to on/off). + + With this code we keep trying setting dummy state for the device + in order for it to start reporting its real state back (assuming it came back online) + + """ try: await self.coordinator.api.set_state(self._device_id, "dummy") @@ -92,35 +111,30 @@ class Device(CoordinatorEntity, SwitchEntity): # This specific call will refresh the state of the device in the CU self.hass.async_create_task(async_refresh_state()) - if self.available: + # if the device was online (now offline), log message and mark it as Unavailable + if self._is_online: _LOGGER.error( "%s switch is not responding, check the status in the SwitchBee mobile app", self.name, ) - self._attr_available = False - self.async_write_ha_state() - return None + self._is_online = False - if not self.available: + return + + # check if the device was offline (now online) and bring it back + if not self._is_online: _LOGGER.info( "%s switch is now responding", self.name, ) - self._attr_available = True + self._is_online = True - # timed power switch state will represent a number of minutes until it goes off - # regulare switches state is ON/OFF + # timed power switch state is an integer representing the number of minutes left until it goes off + # regulare switches state is ON/OFF (1/0 respectively) self._attr_is_on = ( self.coordinator.data[self._device_id].state != ApiStateCommand.OFF ) - super()._handle_coordinator_update() - - async def async_added_to_hass(self) -> None: - """When entity is added to hass.""" - await super().async_added_to_hass() - self._handle_coordinator_update() - async def async_turn_on(self, **kwargs: Any) -> None: """Async function to set on to switch.""" return await self._async_set_state(ApiStateCommand.ON) @@ -129,13 +143,13 @@ class Device(CoordinatorEntity, SwitchEntity): """Async function to set off to switch.""" return await self._async_set_state(ApiStateCommand.OFF) - async def _async_set_state(self, state): + async def _async_set_state(self, state: ApiStateCommand) -> None: try: await self.coordinator.api.set_state(self._device_id, state) except (SwitchBeeError, SwitchBeeDeviceOfflineError) as exp: - _LOGGER.error( - "Failed to set %s state %s, error: %s", self._attr_name, state, exp - ) - self._async_write_ha_state() - else: await self.coordinator.async_refresh() + raise HomeAssistantError( + f"Failed to set {self._attr_name} state {state}, {str(exp)}" + ) from exp + + await self.coordinator.async_refresh() diff --git a/homeassistant/components/switchbee/translations/en.json b/homeassistant/components/switchbee/translations/en.json index 41f9ee6a043..8cbe7b4c56a 100644 --- a/homeassistant/components/switchbee/translations/en.json +++ b/homeassistant/components/switchbee/translations/en.json @@ -1,32 +1,22 @@ { - "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", - "password": "Password", - "switch_as_light": "Initialize switches as light entities", - "username": "Username" - }, - "description": "Setup SwitchBee integration with Home Assistant." + "config": { + "abort": { + "already_configured_device": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Failed to Authenticate with the Central Unit", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "description": "Setup SwitchBee integration with Home Assistant.", + "data": { + "host": "Central Unit IP address", + "username": "User (e-mail)", + "password": "Password" } - } - }, - "options": { - "step": { - "init": { - "data": { - "devices": "Devices to include" - } - } - } - } + } + } + } } \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index f3c8cf72aef..2ec953b75b8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1912,7 +1912,7 @@ pystiebeleltron==0.0.1.dev2 pysuez==0.1.19 # homeassistant.components.switchbee -pyswitchbee==1.4.7 +pyswitchbee==1.4.8 # homeassistant.components.syncthru pysyncthru==0.7.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 14f2725619d..8536fec55e4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1335,7 +1335,7 @@ pyspcwebgw==0.4.0 pysqueezebox==0.6.0 # homeassistant.components.switchbee -pyswitchbee==1.4.7 +pyswitchbee==1.4.8 # homeassistant.components.syncthru pysyncthru==0.7.10 diff --git a/tests/components/switchbee/__init__.py b/tests/components/switchbee/__init__.py index 5043a70c35c..b8b1bf485a3 100644 --- a/tests/components/switchbee/__init__.py +++ b/tests/components/switchbee/__init__.py @@ -1,140 +1,5 @@ """Tests for the SwitchBee Smart Home integration.""" -MOCK_GET_CONFIGURATION = { - "status": "OK", - "data": { - "mac": "A8-21-08-E7-67-B6", - "name": "Residence", - "version": "1.4.4(4)", - "lastConfChange": 1661856874511, - "zones": [ - { - "name": "Sensor Setting", - "items": [ - { - "id": 200000, - "name": "home", - "hw": "VIRTUAL", - "type": "ALARM_SYSTEM", - }, - { - "id": 200010, - "name": "away", - "hw": "VIRTUAL", - "type": "ALARM_SYSTEM", - }, - ], - }, - { - "name": "General", - "items": [ - { - "operations": [113], - "id": 100080, - "name": "All Lights", - "hw": "VIRTUAL", - "type": "GROUP_SWITCH", - }, - { - "operations": [ - {"itemId": 21, "value": 100}, - {"itemId": 333, "value": 100}, - ], - "id": 100160, - "name": "Sunrise", - "hw": "VIRTUAL", - "type": "SCENARIO", - }, - ], - }, - { - "name": "Entrance", - "items": [ - { - "id": 113, - "name": "Staircase Lights", - "hw": "DIMMABLE_SWITCH", - "type": "TIMED_SWITCH", - }, - { - "id": 222, - "name": "Front Door", - "hw": "REGULAR_SWITCH", - "type": "TIMED_SWITCH", - }, - ], - }, - { - "name": "Kitchen", - "items": [ - {"id": 21, "name": "Shutter ", "hw": "SHUTTER", "type": "SHUTTER"}, - { - "operations": [593, 581, 171], - "id": 481, - "name": "Leds", - "hw": "DIMMABLE_SWITCH", - "type": "GROUP_SWITCH", - }, - { - "id": 12, - "name": "Walls", - "hw": "DIMMABLE_SWITCH", - "type": "DIMMER", - }, - ], - }, - { - "name": "Two Way Zone", - "items": [ - { - "operations": [113], - "id": 72, - "name": "Staircase Lights", - "hw": "DIMMABLE_SWITCH", - "type": "TWO_WAY", - } - ], - }, - { - "name": "Facilities ", - "items": [ - { - "id": 321, - "name": "Boiler", - "hw": "TIMED_POWER_SWITCH", - "type": "TIMED_POWER", - }, - { - "modes": ["COOL", "HEAT", "FAN"], - "temperatureUnits": "CELSIUS", - "id": 271, - "name": "HVAC", - "hw": "THERMOSTAT", - "type": "THERMOSTAT", - }, - { - "id": 571, - "name": "Repeater", - "hw": "REPEATER", - "type": "REPEATER", - }, - ], - }, - { - "name": "Alarm", - "items": [ - { - "operations": [{"itemId": 113, "value": 100}], - "id": 81, - "name": "Open Home", - "hw": "STIKER_SWITCH", - "type": "SCENARIO", - } - ], - }, - ], - }, -} MOCK_FAILED_TO_LOGIN_MSG = ( "Central Unit replied with failure: {'status': 'LOGIN_FAILED'}" ) diff --git a/tests/components/switchbee/fixtures/switchbee.json b/tests/components/switchbee/fixtures/switchbee.json new file mode 100644 index 00000000000..6cbbe8be7d2 --- /dev/null +++ b/tests/components/switchbee/fixtures/switchbee.json @@ -0,0 +1,135 @@ +{ + "status": "OK", + "data": { + "mac": "A8-21-08-E7-67-B6", + "name": "Residence", + "version": "1.4.4(4)", + "lastConfChange": 1661856874511, + "zones": [ + { + "name": "Sensor Setting", + "items": [ + { + "id": 200000, + "name": "home", + "hw": "VIRTUAL", + "type": "ALARM_SYSTEM" + }, + { + "id": 200010, + "name": "away", + "hw": "VIRTUAL", + "type": "ALARM_SYSTEM" + } + ] + }, + { + "name": "General", + "items": [ + { + "operations": [113], + "id": 100080, + "name": "All Lights", + "hw": "VIRTUAL", + "type": "GROUP_SWITCH" + }, + { + "operations": [ + { "itemId": 21, "value": 100 }, + { "itemId": 333, "value": 100 } + ], + "id": 100160, + "name": "Sunrise", + "hw": "VIRTUAL", + "type": "SCENARIO" + } + ] + }, + { + "name": "Entrance", + "items": [ + { + "id": 113, + "name": "Staircase Lights", + "hw": "DIMMABLE_SWITCH", + "type": "TIMED_SWITCH" + }, + { + "id": 222, + "name": "Front Door", + "hw": "REGULAR_SWITCH", + "type": "TIMED_SWITCH" + } + ] + }, + { + "name": "Kitchen", + "items": [ + { "id": 21, "name": "Shutter ", "hw": "SHUTTER", "type": "SHUTTER" }, + { + "operations": [593, 581, 171], + "id": 481, + "name": "Leds", + "hw": "DIMMABLE_SWITCH", + "type": "GROUP_SWITCH" + }, + { + "id": 12, + "name": "Walls", + "hw": "DIMMABLE_SWITCH", + "type": "DIMMER" + } + ] + }, + { + "name": "Two Way Zone", + "items": [ + { + "operations": [113], + "id": 72, + "name": "Staircase Lights", + "hw": "DIMMABLE_SWITCH", + "type": "TWO_WAY" + } + ] + }, + { + "name": "Facilities ", + "items": [ + { + "id": 321, + "name": "Boiler", + "hw": "TIMED_POWER_SWITCH", + "type": "TIMED_POWER" + }, + { + "modes": ["COOL", "HEAT", "FAN"], + "temperatureUnits": "CELSIUS", + "id": 271, + "name": "HVAC", + "hw": "THERMOSTAT", + "type": "THERMOSTAT" + }, + { + "id": 571, + "name": "Repeater", + "hw": "REPEATER", + "type": "REPEATER" + } + ] + }, + { + "name": "Alarm", + "items": [ + { + "operations": [{ "itemId": 113, "value": 100 }], + "id": 81, + "name": "Open Home", + "hw": "STIKER_SWITCH", + "type": "SCENARIO" + } + ] + } + ] + } +} diff --git a/tests/components/switchbee/test_config_flow.py b/tests/components/switchbee/test_config_flow.py index 541259853a3..8b9637a7695 100644 --- a/tests/components/switchbee/test_config_flow.py +++ b/tests/components/switchbee/test_config_flow.py @@ -1,21 +1,23 @@ """Test the SwitchBee Smart Home config flow.""" +import json from unittest.mock import patch from homeassistant import config_entries -from homeassistant.components.switchbee.config_flow import DeviceType, SwitchBeeError -from homeassistant.components.switchbee.const import CONF_SWITCHES_AS_LIGHTS, DOMAIN -from homeassistant.const import CONF_DEVICES, CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.components.switchbee.config_flow import SwitchBeeError +from homeassistant.components.switchbee.const import DOMAIN +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_FORM, FlowResultType +from homeassistant.data_entry_flow import FlowResultType -from . import MOCK_FAILED_TO_LOGIN_MSG, MOCK_GET_CONFIGURATION, MOCK_INVALID_TOKEN_MGS +from . import MOCK_FAILED_TO_LOGIN_MSG, MOCK_INVALID_TOKEN_MGS -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, load_fixture async def test_form(hass): """Test we get the form.""" + coordinator_data = json.loads(load_fixture("switchbee.json", "switchbee")) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -24,7 +26,7 @@ async def test_form(hass): with patch( "switchbee.api.CentralUnitAPI.get_configuration", - return_value=MOCK_GET_CONFIGURATION, + return_value=coordinator_data, ), patch( "homeassistant.components.switchbee.async_setup_entry", return_value=True, @@ -40,7 +42,6 @@ async def test_form(hass): CONF_HOST: "1.1.1.1", CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password", - CONF_SWITCHES_AS_LIGHTS: False, }, ) await hass.async_block_till_done() @@ -51,7 +52,6 @@ async def test_form(hass): CONF_HOST: "1.1.1.1", CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password", - CONF_SWITCHES_AS_LIGHTS: False, } @@ -71,16 +71,16 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: CONF_HOST: "1.1.1.1", CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password", - CONF_SWITCHES_AS_LIGHTS: False, }, ) - assert result2["type"] == RESULT_TYPE_FORM + 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} ) @@ -95,11 +95,10 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: CONF_HOST: "1.1.1.1", CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password", - CONF_SWITCHES_AS_LIGHTS: False, }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -119,16 +118,17 @@ async def test_form_unknown_error(hass): CONF_HOST: "1.1.1.1", CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password", - CONF_SWITCHES_AS_LIGHTS: False, }, ) - assert form_result["type"] == RESULT_TYPE_FORM + assert form_result["type"] == FlowResultType.FORM assert form_result["errors"] == {"base": "unknown"} async def test_form_entry_exists(hass): """Test we handle an already existing entry.""" + + coordinator_data = json.loads(load_fixture("switchbee.json", "switchbee")) MockConfigEntry( unique_id="a8:21:08:e7:67:b6", domain=DOMAIN, @@ -136,7 +136,6 @@ async def test_form_entry_exists(hass): CONF_HOST: "1.1.1.1", CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password", - CONF_SWITCHES_AS_LIGHTS: False, }, title="1.1.1.1", ).add_to_hass(hass) @@ -150,7 +149,7 @@ async def test_form_entry_exists(hass): return_value=True, ), patch( "switchbee.api.CentralUnitAPI.get_configuration", - return_value=MOCK_GET_CONFIGURATION, + return_value=coordinator_data, ), patch( "switchbee.api.CentralUnitAPI.fetch_states", return_value=None ): @@ -160,39 +159,8 @@ async def test_form_entry_exists(hass): CONF_HOST: "1.2.2.2", CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password", - CONF_SWITCHES_AS_LIGHTS: False, }, ) assert form_result["type"] == FlowResultType.ABORT assert form_result["reason"] == "already_configured" - - -async def test_option_flow(hass): - """Test config flow options.""" - entry = MockConfigEntry( - unique_id="a8:21:08:e7:67:b6", - domain=DOMAIN, - data={ - CONF_HOST: "1.1.1.1", - CONF_USERNAME: "test-username", - CONF_PASSWORD: "test-password", - CONF_SWITCHES_AS_LIGHTS: False, - }, - title="1.1.1.1", - ) - entry.add_to_hass(hass) - - result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == "form" - assert result["step_id"] == "init" - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={ - CONF_DEVICES: [DeviceType.Switch.display, DeviceType.GroupSwitch.display], - }, - ) - assert result["type"] == "create_entry" - assert result["data"] == { - CONF_DEVICES: [DeviceType.Switch.display, DeviceType.GroupSwitch.display], - } From f2a661026fdccd09c69e76d90d40f927d4dff651 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 16 Sep 2022 15:11:12 +0200 Subject: [PATCH 464/955] Fix kira remote implementation (#77878) --- homeassistant/components/kira/remote.py | 24 ++++------------ homeassistant/components/kira/sensor.py | 38 ++++--------------------- 2 files changed, 11 insertions(+), 51 deletions(-) diff --git a/homeassistant/components/kira/remote.py b/homeassistant/components/kira/remote.py index f728ffa3d62..4c06216a210 100644 --- a/homeassistant/components/kira/remote.py +++ b/homeassistant/components/kira/remote.py @@ -1,13 +1,13 @@ """Support for Keene Electronics IR-IP devices.""" from __future__ import annotations -import functools as ft +from collections.abc import Iterable import logging +from typing import Any from homeassistant.components import remote from homeassistant.const import CONF_DEVICE, CONF_NAME from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -31,32 +31,18 @@ def setup_platform( add_entities([KiraRemote(device, kira)]) -class KiraRemote(Entity): +class KiraRemote(remote.RemoteEntity): """Remote representation used to send commands to a Kira device.""" def __init__(self, name, kira): """Initialize KiraRemote class.""" _LOGGER.debug("KiraRemote device init started for: %s", name) - self._name = name + self._attr_name = name self._kira = kira - @property - def name(self): - """Return the Kira device's name.""" - return self._name - - def update(self) -> None: - """No-op.""" - - def send_command(self, command, **kwargs): + def send_command(self, command: Iterable[str], **kwargs: Any) -> None: """Send a command to one device.""" for single_command in command: code_tuple = (single_command, kwargs.get(remote.ATTR_DEVICE)) _LOGGER.info("Sending Command: %s to %s", *code_tuple) self._kira.sendCode(code_tuple) - - async def async_send_command(self, command, **kwargs): - """Send a command to a device.""" - return await self.hass.async_add_executor_job( - ft.partial(self.send_command, command, **kwargs) - ) diff --git a/homeassistant/components/kira/sensor.py b/homeassistant/components/kira/sensor.py index d4488781849..e1a4f08dd14 100644 --- a/homeassistant/components/kira/sensor.py +++ b/homeassistant/components/kira/sensor.py @@ -13,8 +13,6 @@ from . import CONF_SENSOR, DOMAIN _LOGGER = logging.getLogger(__name__) -ICON = "mdi:remote" - def setup_platform( hass: HomeAssistant, @@ -34,44 +32,20 @@ def setup_platform( class KiraReceiver(SensorEntity): """Implementation of a Kira Receiver.""" + _attr_force_update = True # repeated states have meaning in Kira + _attr_icon = "mdi:remote" _attr_should_poll = False def __init__(self, name, kira): """Initialize the sensor.""" - self._name = name - self._state = None - self._device = STATE_UNKNOWN + self._attr_name = name + self._attr_extra_state_attributes = {CONF_DEVICE: STATE_UNKNOWN} kira.registerCallback(self._update_callback) def _update_callback(self, code): code_name, device = code _LOGGER.debug("Kira Code: %s", code_name) - self._state = code_name - self._device = device + self._attr_native_value = code_name + self._attr_extra_state_attributes[CONF_DEVICE] = device self.schedule_update_ha_state() - - @property - def name(self): - """Return the name of the receiver.""" - return self._name - - @property - def icon(self): - """Return icon.""" - return ICON - - @property - def native_value(self): - """Return the state of the receiver.""" - return self._state - - @property - def extra_state_attributes(self): - """Return the state attributes of the device.""" - return {CONF_DEVICE: self._device} - - @property - def force_update(self) -> bool: - """Kira should force updates. Repeated states have meaning.""" - return True From 42c28cd074edbe50b4dee0a9059bbf799627c1ef Mon Sep 17 00:00:00 2001 From: Jafar Atili Date: Fri, 16 Sep 2022 17:10:40 +0300 Subject: [PATCH 465/955] Address late review of SwitchBee (#78585) --- homeassistant/components/switchbee/switch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/switchbee/switch.py b/homeassistant/components/switchbee/switch.py index 04320dcf4cc..298c30fa7b8 100644 --- a/homeassistant/components/switchbee/switch.py +++ b/homeassistant/components/switchbee/switch.py @@ -76,7 +76,7 @@ class SwitchBeeSwitchEntity(CoordinatorEntity[SwitchBeeCoordinator], SwitchEntit @property def available(self) -> bool: """Return True if entity is available.""" - return self._is_online and self.coordinator.last_update_success + return self._is_online and super().available @callback def _handle_coordinator_update(self) -> None: From 501b8b341f794f93a882c9b6e8e05538c44a63df Mon Sep 17 00:00:00 2001 From: Hurzelchen Date: Fri, 16 Sep 2022 16:17:46 +0200 Subject: [PATCH 466/955] Use commands enum in LG Netcast (#78584) lg_netcast: Use LG_COMMAND constants instead of magic numbers --- .../components/lg_netcast/media_player.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/lg_netcast/media_player.py b/homeassistant/components/lg_netcast/media_player.py index 922cb1de4a0..6f3508e22eb 100644 --- a/homeassistant/components/lg_netcast/media_player.py +++ b/homeassistant/components/lg_netcast/media_player.py @@ -4,7 +4,7 @@ from __future__ import annotations from datetime import datetime from typing import Any -from pylgnetcast import LgNetCastClient, LgNetCastError +from pylgnetcast import LG_COMMAND, LgNetCastClient, LgNetCastError from requests import RequestException import voluptuous as vol @@ -212,7 +212,7 @@ class LgTVDevice(MediaPlayerEntity): def turn_off(self) -> None: """Turn off media player.""" - self.send_command(1) + self.send_command(LG_COMMAND.POWER) def turn_on(self) -> None: """Turn on the media player.""" @@ -221,11 +221,11 @@ class LgTVDevice(MediaPlayerEntity): def volume_up(self) -> None: """Volume up the media player.""" - self.send_command(24) + self.send_command(LG_COMMAND.VOLUME_UP) def volume_down(self) -> None: """Volume down media player.""" - self.send_command(25) + self.send_command(LG_COMMAND.VOLUME_DOWN) def set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" @@ -233,7 +233,7 @@ class LgTVDevice(MediaPlayerEntity): def mute_volume(self, mute: bool) -> None: """Send mute command.""" - self.send_command(26) + self.send_command(LG_COMMAND.MUTE_TOGGLE) def select_source(self, source: str) -> None: """Select input source.""" @@ -250,21 +250,21 @@ class LgTVDevice(MediaPlayerEntity): """Send play command.""" self._playing = True self._state = MediaPlayerState.PLAYING - self.send_command(33) + self.send_command(LG_COMMAND.PLAY) def media_pause(self) -> None: """Send media pause command to media player.""" self._playing = False self._state = MediaPlayerState.PAUSED - self.send_command(34) + self.send_command(LG_COMMAND.PAUSE) def media_next_track(self) -> None: """Send next track command.""" - self.send_command(36) + self.send_command(LG_COMMAND.FAST_FORWARD) def media_previous_track(self) -> None: """Send the previous track command.""" - self.send_command(37) + self.send_command(LG_COMMAND.REWIND) def play_media(self, media_type: str, media_id: str, **kwargs: Any) -> None: """Tune to channel.""" From 0b97dcf0bd64b538f7dabc5de8203048b8645d1b Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 16 Sep 2022 16:46:26 +0200 Subject: [PATCH 467/955] Use vol.Coerce for notify SourceType enum (#77930) --- homeassistant/components/device_tracker/legacy.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/device_tracker/legacy.py b/homeassistant/components/device_tracker/legacy.py index 8216c5fba27..09fd5dce432 100644 --- a/homeassistant/components/device_tracker/legacy.py +++ b/homeassistant/components/device_tracker/legacy.py @@ -71,12 +71,7 @@ from .const import ( SERVICE_SEE: Final = "see" -SOURCE_TYPES: Final[tuple[str, ...]] = ( - SourceType.GPS, - SourceType.ROUTER, - SourceType.BLUETOOTH, - SourceType.BLUETOOTH_LE, -) +SOURCE_TYPES = [cls.value for cls in SourceType] NEW_DEVICE_DEFAULTS_SCHEMA = vol.Any( None, @@ -108,7 +103,7 @@ SERVICE_SEE_PAYLOAD_SCHEMA: Final[vol.Schema] = vol.Schema( ATTR_GPS_ACCURACY: cv.positive_int, ATTR_BATTERY: cv.positive_int, ATTR_ATTRIBUTES: dict, - ATTR_SOURCE_TYPE: vol.In(SOURCE_TYPES), + ATTR_SOURCE_TYPE: vol.Coerce(SourceType), ATTR_CONSIDER_HOME: cv.time_period, # Temp workaround for iOS app introduced in 0.65 vol.Optional("battery_status"): str, From 8774f34271e6e6be4064532791b0e6c947d5412b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 16 Sep 2022 17:42:44 +0200 Subject: [PATCH 468/955] Update Awair config entry on discovery (#78521) --- homeassistant/components/awair/config_flow.py | 5 ++- tests/components/awair/test_config_flow.py | 35 +++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/awair/config_flow.py b/homeassistant/components/awair/config_flow.py index becf6ce46ff..99b1545e792 100644 --- a/homeassistant/components/awair/config_flow.py +++ b/homeassistant/components/awair/config_flow.py @@ -39,7 +39,10 @@ class AwairFlowHandler(ConfigFlow, domain=DOMAIN): if self._device is not None: await self.async_set_unique_id(self._device.mac_address) - self._abort_if_unique_id_configured(error="already_configured_device") + self._abort_if_unique_id_configured( + updates={CONF_HOST: self._device.device_addr}, + error="already_configured_device", + ) self.context.update( { "host": host, diff --git a/tests/components/awair/test_config_flow.py b/tests/components/awair/test_config_flow.py index 16fca099d8c..b939b62641a 100644 --- a/tests/components/awair/test_config_flow.py +++ b/tests/components/awair/test_config_flow.py @@ -1,4 +1,5 @@ """Define tests for the Awair config flow.""" +from typing import Any from unittest.mock import Mock, patch from aiohttp.client_exceptions import ClientConnectorError @@ -364,3 +365,37 @@ async def test_unsuccessful_create_zeroconf_entry(hass: HomeAssistant): ) assert result["type"] == data_entry_flow.FlowResultType.ABORT + + +async def test_zeroconf_discovery_update_configuration( + hass: HomeAssistant, local_devices: Any +) -> None: + """Test updating an existing Awair config entry with discovery info.""" + + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: "127.0.0.1"}, + unique_id=LOCAL_UNIQUE_ID, + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.awair.async_setup_entry", + return_value=True, + ) as mock_setup_entry, patch( + "python_awair.AwairClient.query", side_effect=[local_devices] + ), patch( + "homeassistant.components.awair.async_setup_entry", + return_value=True, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data=ZEROCONF_DISCOVERY, + ) + + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "already_configured_device" + + assert config_entry.data[CONF_HOST] == ZEROCONF_DISCOVERY.host + assert mock_setup_entry.call_count == 0 From e507e0031789605a49d7e32e428a7d57948107c8 Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Fri, 16 Sep 2022 12:56:50 -0600 Subject: [PATCH 469/955] Bump pylitterbot to 2022.9.3 (#78590) --- 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 a4c9f3cd54e..cb9b67210fb 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.9.1"], + "requirements": ["pylitterbot==2022.9.3"], "codeowners": ["@natekspencer", "@tkdrob"], "dhcp": [{ "hostname": "litter-robot4" }], "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 2ec953b75b8..62affdd0244 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1674,7 +1674,7 @@ pylibrespot-java==0.1.0 pylitejet==0.3.0 # homeassistant.components.litterrobot -pylitterbot==2022.9.1 +pylitterbot==2022.9.3 # homeassistant.components.lutron_caseta pylutron-caseta==0.15.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8536fec55e4..4a2e6782dfc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1169,7 +1169,7 @@ pylibrespot-java==0.1.0 pylitejet==0.3.0 # homeassistant.components.litterrobot -pylitterbot==2022.9.1 +pylitterbot==2022.9.3 # homeassistant.components.lutron_caseta pylutron-caseta==0.15.1 From 047c3862d7ee3f7ee07b567865208c5426eaa68e Mon Sep 17 00:00:00 2001 From: On Freund Date: Fri, 16 Sep 2022 23:29:08 +0300 Subject: [PATCH 470/955] Bump pyrisco to v0.5.5 (#78566) Upgrade to pyrisco 0.5.5 --- homeassistant/components/risco/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/risco/manifest.json b/homeassistant/components/risco/manifest.json index 9703b5775bc..bd8bbfd715f 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.4"], + "requirements": ["pyrisco==0.5.5"], "codeowners": ["@OnFreund"], "quality_scale": "platinum", "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 62affdd0244..078098ff77b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1829,7 +1829,7 @@ pyrecswitch==1.0.2 pyrepetierng==0.1.0 # homeassistant.components.risco -pyrisco==0.5.4 +pyrisco==0.5.5 # homeassistant.components.rituals_perfume_genie pyrituals==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4a2e6782dfc..b3f76048da7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1279,7 +1279,7 @@ pyps4-2ndscreen==1.3.1 pyqwikswitch==0.93 # homeassistant.components.risco -pyrisco==0.5.4 +pyrisco==0.5.5 # homeassistant.components.rituals_perfume_genie pyrituals==0.0.6 From be17ba15db86323e6ebf63197e4b03721c41935d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 16 Sep 2022 23:04:29 +0200 Subject: [PATCH 471/955] Use attributes in demo media-player (#78461) * Use attributes in demo media-player * Use _attr_is_volume_muted * Use _attr_shuffle * Use _attr_sound_mode / _attr_sound_mode_list --- homeassistant/components/demo/media_player.py | 49 +++++-------------- 1 file changed, 13 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/demo/media_player.py b/homeassistant/components/demo/media_player.py index 8c4df015d18..8bbe380d9ec 100644 --- a/homeassistant/components/demo/media_player.py +++ b/homeassistant/components/demo/media_player.py @@ -105,6 +105,7 @@ NETFLIX_PLAYER_SUPPORT = ( class AbstractDemoPlayer(MediaPlayerEntity): """A demo media players.""" + _attr_sound_mode_list = SOUND_MODE_LIST _attr_should_poll = False # We only implement the methods that we support @@ -115,38 +116,12 @@ class AbstractDemoPlayer(MediaPlayerEntity): """Initialize the demo device.""" self._attr_name = name self._attr_state = MediaPlayerState.PLAYING - self._volume_level = 1.0 - self._volume_muted = False - self._shuffle = False - self._sound_mode_list = SOUND_MODE_LIST - self._sound_mode = DEFAULT_SOUND_MODE + self._attr_volume_level = 1.0 + self._attr_is_volume_muted = False + self._attr_shuffle = False + self._attr_sound_mode = DEFAULT_SOUND_MODE self._attr_device_class = device_class - @property - def volume_level(self) -> float: - """Return the volume level of the media player (0..1).""" - return self._volume_level - - @property - def is_volume_muted(self) -> bool: - """Return boolean if volume is currently muted.""" - return self._volume_muted - - @property - def shuffle(self) -> bool: - """Boolean if shuffling is enabled.""" - return self._shuffle - - @property - def sound_mode(self) -> str: - """Return the current sound mode.""" - return self._sound_mode - - @property - def sound_mode_list(self) -> list[str]: - """Return a list of available sound modes.""" - return self._sound_mode_list - def turn_on(self) -> None: """Turn the media player on.""" self._attr_state = MediaPlayerState.PLAYING @@ -159,22 +134,24 @@ class AbstractDemoPlayer(MediaPlayerEntity): def mute_volume(self, mute: bool) -> None: """Mute the volume.""" - self._volume_muted = mute + self._attr_is_volume_muted = mute self.schedule_update_ha_state() def volume_up(self) -> None: """Increase volume.""" - self._volume_level = min(1.0, self._volume_level + 0.1) + assert self.volume_level is not None + self._attr_volume_level = min(1.0, self.volume_level + 0.1) self.schedule_update_ha_state() def volume_down(self) -> None: """Decrease volume.""" - self._volume_level = max(0.0, self._volume_level - 0.1) + assert self.volume_level is not None + self._attr_volume_level = max(0.0, self.volume_level - 0.1) self.schedule_update_ha_state() def set_volume_level(self, volume: float) -> None: """Set the volume level, range 0..1.""" - self._volume_level = volume + self._attr_volume_level = volume self.schedule_update_ha_state() def media_play(self) -> None: @@ -194,12 +171,12 @@ class AbstractDemoPlayer(MediaPlayerEntity): def set_shuffle(self, shuffle: bool) -> None: """Enable/disable shuffle mode.""" - self._shuffle = shuffle + self._attr_shuffle = shuffle self.schedule_update_ha_state() def select_sound_mode(self, sound_mode: str) -> None: """Select sound mode.""" - self._sound_mode = sound_mode + self._attr_sound_mode = sound_mode self.schedule_update_ha_state() From 06178d3446c50be5ffef0f5f7fd72b1266a97e26 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Fri, 16 Sep 2022 17:04:55 -0400 Subject: [PATCH 472/955] Only redact zwave_js values that are worth redacting (#78420) * Only redact zwave_js values that are worth redacting * Tweak test * Use redacted fixture for test --- .../components/zwave_js/diagnostics.py | 3 + tests/components/zwave_js/conftest.py | 6 + .../config_entry_diagnostics_redacted.json | 1936 +++++++++++++++++ tests/components/zwave_js/test_diagnostics.py | 18 +- 4 files changed, 1951 insertions(+), 12 deletions(-) create mode 100644 tests/components/zwave_js/fixtures/config_entry_diagnostics_redacted.json diff --git a/homeassistant/components/zwave_js/diagnostics.py b/homeassistant/components/zwave_js/diagnostics.py index 2b3d72078b9..f9a30528863 100644 --- a/homeassistant/components/zwave_js/diagnostics.py +++ b/homeassistant/components/zwave_js/diagnostics.py @@ -51,6 +51,9 @@ VALUES_TO_REDACT = ( def redact_value_of_zwave_value(zwave_value: ValueDataType) -> ValueDataType: """Redact value of a Z-Wave value.""" + # If the value has no value, there is nothing to redact + if zwave_value.get("value") in (None, ""): + return zwave_value for value_to_redact in VALUES_TO_REDACT: command_class = None if "commandClass" in zwave_value: diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 6585deddbdb..68052aeaab1 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -264,6 +264,12 @@ def config_entry_diagnostics_fixture(): return json.loads(load_fixture("zwave_js/config_entry_diagnostics.json")) +@pytest.fixture(name="config_entry_diagnostics_redacted", scope="session") +def config_entry_diagnostics_redacted_fixture(): + """Load the redacted config entry diagnostics fixture data.""" + return json.loads(load_fixture("zwave_js/config_entry_diagnostics_redacted.json")) + + @pytest.fixture(name="multisensor_6_state", scope="session") def multisensor_6_state_fixture(): """Load the multisensor 6 node state fixture data.""" diff --git a/tests/components/zwave_js/fixtures/config_entry_diagnostics_redacted.json b/tests/components/zwave_js/fixtures/config_entry_diagnostics_redacted.json new file mode 100644 index 00000000000..1e68d82d586 --- /dev/null +++ b/tests/components/zwave_js/fixtures/config_entry_diagnostics_redacted.json @@ -0,0 +1,1936 @@ +[ + { + "type": "version", + "driverVersion": "8.11.6", + "serverVersion": "1.15.0", + "homeId": "**REDACTED**", + "minSchemaVersion": 0, + "maxSchemaVersion": 15 + }, + { + "type": "result", + "success": true, + "messageId": "api-schema-id", + "result": {} + }, + { + "type": "result", + "success": true, + "messageId": "listen-id", + "result": { + "state": { + "driver": { + "logConfig": { + "enabled": true, + "level": "info", + "logToFile": false, + "filename": "/data/store/zwavejs_%DATE%.log", + "forceConsole": true + }, + "statisticsEnabled": true + }, + "controller": { + "libraryVersion": "Z-Wave 6.07", + "type": 1, + "homeId": "**REDACTED**", + "ownNodeId": 1, + "isSecondary": false, + "isUsingHomeIdFromOtherNetwork": false, + "isSISPresent": true, + "wasRealPrimary": true, + "isStaticUpdateController": true, + "isSlave": false, + "serialApiVersion": "1.2", + "manufacturerId": 134, + "productType": 1, + "productId": 90, + "supportedFunctionTypes": [ + 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 16, 17, 18, 19, 20, 21, 22, 23, 28, + 32, 33, 34, 35, 36, 39, 40, 41, 42, 43, 44, 45, 46, 47, 55, 56, 57, + 58, 59, 60, 63, 65, 66, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 79, + 80, 81, 83, 84, 85, 86, 87, 88, 94, 95, 96, 97, 98, 99, 102, 103, + 120, 128, 144, 146, 147, 152, 161, 180, 182, 183, 184, 185, 186, + 189, 190, 191, 208, 209, 210, 211, 212, 238, 239 + ], + "sucNodeId": 1, + "supportsTimers": false, + "isHealNetworkActive": false, + "statistics": { + "messagesTX": 10, + "messagesRX": 734, + "messagesDroppedRX": 0, + "NAK": 0, + "CAN": 0, + "timeoutACK": 0, + "timeoutResponse": 0, + "timeoutCallback": 0, + "messagesDroppedTX": 0 + }, + "inclusionState": 0 + }, + "nodes": [ + { + "nodeId": 1, + "index": 0, + "status": 4, + "ready": true, + "isListening": true, + "isRouting": false, + "isSecure": "unknown", + "manufacturerId": 134, + "productId": 90, + "productType": 1, + "firmwareVersion": "1.2", + "deviceConfig": { + "filename": "/data/db/devices/0x0086/zw090.json", + "isEmbedded": true, + "manufacturer": "AEON Labs", + "manufacturerId": 134, + "label": "ZW090", + "description": "Z‐Stick Gen5 USB Controller", + "devices": [ + { + "productType": 1, + "productId": 90 + }, + { + "productType": 257, + "productId": 90 + }, + { + "productType": 513, + "productId": 90 + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + }, + "associations": {}, + "paramInformation": { + "_map": {} + }, + "metadata": { + "reset": "Use this procedure only in the event that the primary controller is missing or otherwise inoperable.\n\nPress and hold the Action Button on Z-Stick for 20 seconds and then release", + "manual": "https://products.z-wavealliance.org/ProductManual/File?folder=&filename=MarketCertificationFiles/1345/Z%20Stick%20Gen5%20manual%201.pdf" + } + }, + "label": "ZW090", + "interviewAttempts": 0, + "endpoints": [ + { + "nodeId": 1, + "index": 0, + "deviceClass": { + "basic": { + "key": 2, + "label": "Static Controller" + }, + "generic": { + "key": 2, + "label": "Static Controller" + }, + "specific": { + "key": 1, + "label": "PC Controller" + }, + "mandatorySupportedCCs": [], + "mandatoryControlledCCs": [32] + }, + "commandClasses": [] + } + ], + "values": [], + "isFrequentListening": false, + "maxDataRate": 40000, + "supportedDataRates": [40000], + "protocolVersion": 3, + "deviceClass": { + "basic": { + "key": 2, + "label": "Static Controller" + }, + "generic": { + "key": 2, + "label": "Static Controller" + }, + "specific": { + "key": 1, + "label": "PC Controller" + }, + "mandatorySupportedCCs": [], + "mandatoryControlledCCs": [32] + }, + "interviewStage": "Complete", + "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0086:0x0001:0x005a:1.2", + "statistics": { + "commandsTX": 0, + "commandsRX": 0, + "commandsDroppedRX": 0, + "commandsDroppedTX": 0, + "timeoutResponse": 0 + }, + "isControllerNode": true, + "keepAwake": false + }, + { + "nodeId": 29, + "index": 0, + "status": 4, + "ready": true, + "isListening": false, + "isRouting": true, + "isSecure": true, + "firmwareVersion": "113.22", + "name": "Front Door Lock", + "location": "**REDACTED**", + "interviewAttempts": 0, + "endpoints": [ + { + "nodeId": 29, + "index": 0, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 64, + "label": "Entry Control" + }, + "specific": { + "key": 3, + "label": "Secure Keypad Door Lock" + }, + "mandatorySupportedCCs": [32, 98, 99, 114, 134], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 98, + "name": "Door Lock", + "version": 1, + "isSecure": true + }, + { + "id": 99, + "name": "User Code", + "version": 1, + "isSecure": true + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": true + }, + { + "id": 113, + "name": "Notification", + "version": 1, + "isSecure": true + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 1, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 1, + "isSecure": false + }, + { + "id": 128, + "name": "Battery", + "version": 1, + "isSecure": true + }, + { + "id": 133, + "name": "Association", + "version": 1, + "isSecure": true + }, + { + "id": 134, + "name": "Version", + "version": 1, + "isSecure": false + }, + { + "id": 152, + "name": "Security", + "version": 1, + "isSecure": true + } + ] + } + ], + "values": [ + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "currentMode", + "propertyName": "currentMode", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current lock mode", + "min": 0, + "max": 255, + "states": { + "0": "Unsecured", + "1": "UnsecuredWithTimeout", + "16": "InsideUnsecured", + "17": "InsideUnsecuredWithTimeout", + "32": "OutsideUnsecured", + "33": "OutsideUnsecuredWithTimeout", + "254": "Unknown", + "255": "Secured" + } + }, + "value": 255 + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "targetMode", + "propertyName": "targetMode", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Target lock mode", + "min": 0, + "max": 255, + "states": { + "0": "Unsecured", + "1": "UnsecuredWithTimeout", + "16": "InsideUnsecured", + "17": "InsideUnsecuredWithTimeout", + "32": "OutsideUnsecured", + "33": "OutsideUnsecuredWithTimeout", + "254": "Unknown", + "255": "Secured" + } + }, + "value": 255 + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "outsideHandlesCanOpenDoor", + "propertyName": "outsideHandlesCanOpenDoor", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Which outside handles can open the door (actual status)" + }, + "value": [false, false, false, false] + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "insideHandlesCanOpenDoor", + "propertyName": "insideHandlesCanOpenDoor", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Which inside handles can open the door (actual status)" + }, + "value": [false, false, false, false] + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "latchStatus", + "propertyName": "latchStatus", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "The current status of the latch" + }, + "value": "open" + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "boltStatus", + "propertyName": "boltStatus", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "The current status of the bolt" + }, + "value": "locked" + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "doorStatus", + "propertyName": "doorStatus", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "The current status of the door" + }, + "value": "open" + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "lockTimeout", + "propertyName": "lockTimeout", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Seconds until lock mode times out" + } + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "operationType", + "propertyName": "operationType", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Lock operation type", + "min": 0, + "max": 255, + "states": { + "1": "Constant", + "2": "Timed" + } + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "outsideHandlesCanOpenDoorConfiguration", + "propertyName": "outsideHandlesCanOpenDoorConfiguration", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": true, + "label": "Which outside handles can open the door (configuration)" + }, + "value": [false, false, false, false] + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "insideHandlesCanOpenDoorConfiguration", + "propertyName": "insideHandlesCanOpenDoorConfiguration", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": true, + "label": "Which inside handles can open the door (configuration)" + }, + "value": [false, false, false, false] + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "lockTimeoutConfiguration", + "propertyName": "lockTimeoutConfiguration", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Duration of timed mode in seconds", + "min": 0, + "max": 65535 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 1, + "propertyName": "userIdStatus", + "propertyKeyName": "1", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (1)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 1, + "propertyName": "userCode", + "propertyKeyName": "1", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (1)", + "minLength": 4, + "maxLength": 10 + }, + "value": "**REDACTED**" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 2, + "propertyName": "userIdStatus", + "propertyKeyName": "2", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (2)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 2, + "propertyName": "userCode", + "propertyKeyName": "2", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (2)", + "minLength": 4, + "maxLength": 10 + }, + "value": "**REDACTED**" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 3, + "propertyName": "userIdStatus", + "propertyKeyName": "3", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (3)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 3, + "propertyName": "userCode", + "propertyKeyName": "3", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (3)", + "minLength": 4, + "maxLength": 10 + }, + "value": "**REDACTED**" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 4, + "propertyName": "userIdStatus", + "propertyKeyName": "4", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (4)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 4, + "propertyName": "userCode", + "propertyKeyName": "4", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (4)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 5, + "propertyName": "userIdStatus", + "propertyKeyName": "5", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (5)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 5, + "propertyName": "userCode", + "propertyKeyName": "5", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (5)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 6, + "propertyName": "userIdStatus", + "propertyKeyName": "6", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (6)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 6, + "propertyName": "userCode", + "propertyKeyName": "6", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (6)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 7, + "propertyName": "userIdStatus", + "propertyKeyName": "7", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (7)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 7, + "propertyName": "userCode", + "propertyKeyName": "7", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (7)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 8, + "propertyName": "userIdStatus", + "propertyKeyName": "8", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (8)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 8, + "propertyName": "userCode", + "propertyKeyName": "8", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (8)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 9, + "propertyName": "userIdStatus", + "propertyKeyName": "9", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (9)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 9, + "propertyName": "userCode", + "propertyKeyName": "9", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (9)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 10, + "propertyName": "userIdStatus", + "propertyKeyName": "10", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (10)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 10, + "propertyName": "userCode", + "propertyKeyName": "10", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (10)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 11, + "propertyName": "userIdStatus", + "propertyKeyName": "11", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (11)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 11, + "propertyName": "userCode", + "propertyKeyName": "11", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (11)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 12, + "propertyName": "userIdStatus", + "propertyKeyName": "12", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (12)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 12, + "propertyName": "userCode", + "propertyKeyName": "12", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (12)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 13, + "propertyName": "userIdStatus", + "propertyKeyName": "13", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (13)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 13, + "propertyName": "userCode", + "propertyKeyName": "13", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (13)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 14, + "propertyName": "userIdStatus", + "propertyKeyName": "14", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (14)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 14, + "propertyName": "userCode", + "propertyKeyName": "14", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (14)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 15, + "propertyName": "userIdStatus", + "propertyKeyName": "15", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (15)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 15, + "propertyName": "userCode", + "propertyKeyName": "15", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (15)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 16, + "propertyName": "userIdStatus", + "propertyKeyName": "16", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (16)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 16, + "propertyName": "userCode", + "propertyKeyName": "16", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (16)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 17, + "propertyName": "userIdStatus", + "propertyKeyName": "17", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (17)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 17, + "propertyName": "userCode", + "propertyKeyName": "17", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (17)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 18, + "propertyName": "userIdStatus", + "propertyKeyName": "18", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (18)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 18, + "propertyName": "userCode", + "propertyKeyName": "18", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (18)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 19, + "propertyName": "userIdStatus", + "propertyKeyName": "19", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (19)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 19, + "propertyName": "userCode", + "propertyKeyName": "19", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (19)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 20, + "propertyName": "userIdStatus", + "propertyKeyName": "20", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (20)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 20, + "propertyName": "userCode", + "propertyKeyName": "20", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (20)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 21, + "propertyName": "userIdStatus", + "propertyKeyName": "21", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (21)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 21, + "propertyName": "userCode", + "propertyKeyName": "21", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (21)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 22, + "propertyName": "userIdStatus", + "propertyKeyName": "22", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (22)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 22, + "propertyName": "userCode", + "propertyKeyName": "22", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (22)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 23, + "propertyName": "userIdStatus", + "propertyKeyName": "23", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (23)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 23, + "propertyName": "userCode", + "propertyKeyName": "23", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (23)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 24, + "propertyName": "userIdStatus", + "propertyKeyName": "24", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (24)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 24, + "propertyName": "userCode", + "propertyKeyName": "24", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (24)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 25, + "propertyName": "userIdStatus", + "propertyKeyName": "25", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (25)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 25, + "propertyName": "userCode", + "propertyKeyName": "25", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (25)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 26, + "propertyName": "userIdStatus", + "propertyKeyName": "26", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (26)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 26, + "propertyName": "userCode", + "propertyKeyName": "26", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (26)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 28, + "propertyName": "userIdStatus", + "propertyKeyName": "28", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (28)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 28, + "propertyName": "userCode", + "propertyKeyName": "28", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (28)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 29, + "propertyName": "userIdStatus", + "propertyKeyName": "29", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (29)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 29, + "propertyName": "userCode", + "propertyKeyName": "29", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (29)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 30, + "propertyName": "userIdStatus", + "propertyKeyName": "30", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (30)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 30, + "propertyName": "userCode", + "propertyKeyName": "30", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (30)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Access Control", + "propertyKey": "Lock state", + "propertyName": "Access Control", + "propertyKeyName": "Lock state", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Lock state", + "ccSpecific": { + "notificationType": 6 + }, + "min": 0, + "max": 255, + "states": { + "0": "idle", + "11": "Lock jammed" + } + }, + "value": 11 + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Access Control", + "propertyKey": "Keypad state", + "propertyName": "Access Control", + "propertyKeyName": "Keypad state", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Keypad state", + "ccSpecific": { + "notificationType": 6 + }, + "min": 0, + "max": 255, + "states": { + "0": "idle", + "16": "Keypad temporary disabled" + } + }, + "value": 16 + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "alarmType", + "propertyName": "alarmType", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Alarm Type", + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "alarmLevel", + "propertyName": "alarmLevel", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Alarm Level", + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "manufacturerId", + "propertyName": "manufacturerId", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Manufacturer ID", + "min": 0, + "max": 65535 + } + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productType", + "propertyName": "productType", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product type", + "min": 0, + "max": 65535 + } + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productId", + "propertyName": "productId", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product ID", + "min": 0, + "max": 65535 + } + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "level", + "propertyName": "level", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Battery level", + "min": 0, + "max": 100, + "unit": "%" + }, + "value": 89 + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "isLow", + "propertyName": "isLow", + "ccVersion": 0, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Low battery level" + }, + "value": false + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "libraryType", + "propertyName": "libraryType", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Library type", + "states": { + "0": "Unknown", + "1": "Static Controller", + "2": "Controller", + "3": "Enhanced Slave", + "4": "Slave", + "5": "Installer", + "6": "Routing Slave", + "7": "Bridge Controller", + "8": "Device under Test", + "9": "N/A", + "10": "AV Remote", + "11": "AV Device" + } + }, + "value": 6 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "protocolVersion", + "propertyName": "protocolVersion", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "3.42" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "ccVersion": 0, + "metadata": { + "type": "string[]", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + }, + "value": ["113.22"] + } + ], + "isFrequentListening": "1000ms", + "maxDataRate": 40000, + "supportedDataRates": [40000], + "protocolVersion": 3, + "supportsBeaming": true, + "supportsSecurity": false, + "nodeType": 1, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 64, + "label": "Entry Control" + }, + "specific": { + "key": 3, + "label": "Secure Keypad Door Lock" + }, + "mandatorySupportedCCs": [32, 98, 99, 114, 134], + "mandatoryControlledCCs": [] + }, + "interviewStage": "Complete", + "statistics": { + "commandsTX": 25, + "commandsRX": 42, + "commandsDroppedRX": 0, + "commandsDroppedTX": 0, + "timeoutResponse": 0 + }, + "highestSecurityClass": 7, + "isControllerNode": false, + "keepAwake": false + } + ] + } + } + } +] diff --git a/tests/components/zwave_js/test_diagnostics.py b/tests/components/zwave_js/test_diagnostics.py index 41505364111..64f27805243 100644 --- a/tests/components/zwave_js/test_diagnostics.py +++ b/tests/components/zwave_js/test_diagnostics.py @@ -4,7 +4,6 @@ from unittest.mock import patch import pytest from zwave_js_server.event import Event -from homeassistant.components.diagnostics.const import REDACTED from homeassistant.components.zwave_js.diagnostics import ( ZwaveValueMatcher, async_get_device_diagnostics, @@ -26,7 +25,11 @@ from tests.components.diagnostics import ( async def test_config_entry_diagnostics( - hass, hass_client, integration, config_entry_diagnostics + hass, + hass_client, + integration, + config_entry_diagnostics, + config_entry_diagnostics_redacted, ): """Test the config entry level diagnostics data dump.""" with patch( @@ -36,16 +39,7 @@ async def test_config_entry_diagnostics( diagnostics = await get_diagnostics_for_config_entry( hass, hass_client, integration ) - assert len(diagnostics) == 3 - assert diagnostics[0]["homeId"] == REDACTED - nodes = diagnostics[2]["result"]["state"]["nodes"] - for node in nodes: - assert "location" not in node or node["location"] == REDACTED - for value in node["values"]: - if value["commandClass"] == 99 and value["property"] == "userCode": - assert value["value"] == REDACTED - else: - assert value.get("value") != REDACTED + assert diagnostics == config_entry_diagnostics_redacted async def test_device_diagnostics( From 84cd0da26b2519c92c10ae4586389bcf0a9f06d2 Mon Sep 17 00:00:00 2001 From: "Kenneth J. Miller" Date: Fri, 16 Sep 2022 23:19:30 +0200 Subject: [PATCH 473/955] Add Airly gas sensors (#77908) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add support for gases queryable via Airly API: CO, NO₂, O₃, SO₂ * Add tests for above sensors and update test fixtures --- homeassistant/components/airly/const.py | 4 + homeassistant/components/airly/sensor.py | 60 + .../airly/fixtures/diagnostics_data.json | 36 +- .../airly/fixtures/valid_station.json | 4904 ++++++++++------- tests/components/airly/test_init.py | 2 +- tests/components/airly/test_sensor.py | 78 +- 6 files changed, 3041 insertions(+), 2043 deletions(-) diff --git a/homeassistant/components/airly/const.py b/homeassistant/components/airly/const.py index 8fddaea8ec2..76260699dbd 100644 --- a/homeassistant/components/airly/const.py +++ b/homeassistant/components/airly/const.py @@ -7,11 +7,15 @@ ATTR_API_ADVICE: Final = "ADVICE" ATTR_API_CAQI: Final = "CAQI" ATTR_API_CAQI_DESCRIPTION: Final = "DESCRIPTION" ATTR_API_CAQI_LEVEL: Final = "LEVEL" +ATTR_API_CO: Final = "CO" ATTR_API_HUMIDITY: Final = "HUMIDITY" +ATTR_API_NO2: Final = "NO2" +ATTR_API_O3: Final = "O3" ATTR_API_PM10: Final = "PM10" ATTR_API_PM1: Final = "PM1" ATTR_API_PM25: Final = "PM25" ATTR_API_PRESSURE: Final = "PRESSURE" +ATTR_API_SO2: Final = "SO2" ATTR_API_TEMPERATURE: Final = "TEMPERATURE" ATTR_ADVICE: Final = "advice" diff --git a/homeassistant/components/airly/sensor.py b/homeassistant/components/airly/sensor.py index a1c9f8a3057..9fb4e61dbdb 100644 --- a/homeassistant/components/airly/sensor.py +++ b/homeassistant/components/airly/sensor.py @@ -33,11 +33,15 @@ from .const import ( ATTR_API_CAQI, ATTR_API_CAQI_DESCRIPTION, ATTR_API_CAQI_LEVEL, + ATTR_API_CO, ATTR_API_HUMIDITY, + ATTR_API_NO2, + ATTR_API_O3, ATTR_API_PM1, ATTR_API_PM10, ATTR_API_PM25, ATTR_API_PRESSURE, + ATTR_API_SO2, ATTR_API_TEMPERATURE, ATTR_DESCRIPTION, ATTR_LEVEL, @@ -112,6 +116,34 @@ SENSOR_TYPES: tuple[AirlySensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, value=lambda value: round(value, 1), ), + AirlySensorEntityDescription( + key=ATTR_API_CO, + device_class=SensorDeviceClass.CO, + name=ATTR_API_CO, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + ), + AirlySensorEntityDescription( + key=ATTR_API_NO2, + device_class=SensorDeviceClass.NITROGEN_DIOXIDE, + name=ATTR_API_NO2, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + ), + AirlySensorEntityDescription( + key=ATTR_API_SO2, + device_class=SensorDeviceClass.SULPHUR_DIOXIDE, + name=ATTR_API_SO2, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + ), + AirlySensorEntityDescription( + key=ATTR_API_O3, + device_class=SensorDeviceClass.OZONE, + name=ATTR_API_O3, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + ), ) @@ -191,4 +223,32 @@ class AirlySensor(CoordinatorEntity[AirlyDataUpdateCoordinator], SensorEntity): self._attrs[ATTR_PERCENT] = round( self.coordinator.data[f"{ATTR_API_PM10}_{SUFFIX_PERCENT}"] ) + if self.entity_description.key == ATTR_API_CO: + self._attrs[ATTR_LIMIT] = self.coordinator.data[ + f"{ATTR_API_CO}_{SUFFIX_LIMIT}" + ] + self._attrs[ATTR_PERCENT] = round( + self.coordinator.data[f"{ATTR_API_CO}_{SUFFIX_PERCENT}"] + ) + if self.entity_description.key == ATTR_API_NO2: + self._attrs[ATTR_LIMIT] = self.coordinator.data[ + f"{ATTR_API_NO2}_{SUFFIX_LIMIT}" + ] + self._attrs[ATTR_PERCENT] = round( + self.coordinator.data[f"{ATTR_API_NO2}_{SUFFIX_PERCENT}"] + ) + if self.entity_description.key == ATTR_API_SO2: + self._attrs[ATTR_LIMIT] = self.coordinator.data[ + f"{ATTR_API_SO2}_{SUFFIX_LIMIT}" + ] + self._attrs[ATTR_PERCENT] = round( + self.coordinator.data[f"{ATTR_API_SO2}_{SUFFIX_PERCENT}"] + ) + if self.entity_description.key == ATTR_API_O3: + self._attrs[ATTR_LIMIT] = self.coordinator.data[ + f"{ATTR_API_O3}_{SUFFIX_LIMIT}" + ] + self._attrs[ATTR_PERCENT] = round( + self.coordinator.data[f"{ATTR_API_O3}_{SUFFIX_PERCENT}"] + ) return self._attrs diff --git a/tests/components/airly/fixtures/diagnostics_data.json b/tests/components/airly/fixtures/diagnostics_data.json index 46a3591501f..0f225fd4a20 100644 --- a/tests/components/airly/fixtures/diagnostics_data.json +++ b/tests/components/airly/fixtures/diagnostics_data.json @@ -1,16 +1,28 @@ { - "PM1": 9.23, - "PM25": 13.71, - "PM10": 18.58, - "PRESSURE": 1000.87, - "HUMIDITY": 92.84, - "TEMPERATURE": 14.23, - "PM25_LIMIT": 25.0, - "PM25_PERCENT": 54.84, - "PM10_LIMIT": 50.0, - "PM10_PERCENT": 37.17, - "CAQI": 22.85, + "PM1": 2.83, + "PM25": 4.37, + "PM10": 6.06, + "CO": 162.49, + "NO2": 16.04, + "O3": 41.52, + "SO2": 13.97, + "PRESSURE": 1019.86, + "HUMIDITY": 68.35, + "TEMPERATURE": 14.37, + "PM25_LIMIT": 15.0, + "PM25_PERCENT": 29.13, + "PM10_LIMIT": 45.0, + "PM10_PERCENT": 14.5, + "CO_LIMIT": 4000, + "CO_PERCENT": 4.06, + "NO2_LIMIT": 25, + "NO2_PERCENT": 64.17, + "O3_LIMIT": 100, + "O3_PERCENT": 41.52, + "SO2_LIMIT": 40, + "SO2_PERCENT": 34.93, + "CAQI": 7.29, "LEVEL": "very low", "DESCRIPTION": "Great air here today!", - "ADVICE": "Great air!" + "ADVICE": "Catch your breath!" } diff --git a/tests/components/airly/fixtures/valid_station.json b/tests/components/airly/fixtures/valid_station.json index c21c40b14a0..e0115a0470c 100644 --- a/tests/components/airly/fixtures/valid_station.json +++ b/tests/components/airly/fixtures/valid_station.json @@ -1,1740 +1,734 @@ { "current": { - "fromDateTime": "2019-10-02T05:54:57.204Z", - "tillDateTime": "2019-10-02T06:54:57.204Z", - "values": [ - { - "name": "PM1", - "value": 9.23 - }, - { - "name": "PM25", - "value": 13.71 - }, - { - "name": "PM10", - "value": 18.58 - }, - { - "name": "PRESSURE", - "value": 1000.87 - }, - { - "name": "HUMIDITY", - "value": 92.84 - }, - { - "name": "TEMPERATURE", - "value": 14.23 - } - ], + "fromDateTime": "2022-09-06T22:13:20.347Z", "indexes": [ { - "name": "AIRLY_CAQI", - "value": 22.85, - "level": "VERY_LOW", + "advice": "Catch your breath!", + "color": "#6BC926", "description": "Great air here today!", - "advice": "Great air!", - "color": "#6BC926" + "level": "VERY_LOW", + "name": "AIRLY_CAQI", + "value": 7.29 } ], "standards": [ { + "averaging": "24h", + "limit": 15, "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 54.84 + "percent": 29.13, + "pollutant": "PM25" + }, + { + "averaging": "24h", + "limit": 45, + "name": "WHO", + "percent": 14.5, + "pollutant": "PM10" + }, + { + "averaging": "24h", + "limit": 25, + "name": "WHO", + "percent": 64.17, + "pollutant": "NO2" + }, + { + "averaging": "8h", + "limit": 100, + "name": "WHO", + "percent": 41.52, + "pollutant": "O3" }, { "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 37.17 + "pollutant": "CO", + "limit": 4000, + "percent": 4.06, + "averaging": "24h" + }, + { + "name": "WHO", + "pollutant": "SO2", + "limit": 40, + "percent": 34.93, + "averaging": "24h" + } + ], + "tillDateTime": "2022-09-06T23:13:20.347Z", + "values": [ + { + "name": "PM1", + "value": 2.83 + }, + { + "name": "PM25", + "value": 4.37 + }, + { + "name": "PM10", + "value": 6.06 + }, + { + "name": "PRESSURE", + "value": 1019.86 + }, + { + "name": "HUMIDITY", + "value": 68.35 + }, + { + "name": "TEMPERATURE", + "value": 14.37 + }, + { + "name": "NO2", + "value": 16.04 + }, + { + "name": "O3", + "value": 41.52 + }, + { + "name": "SO2", + "value": 13.97 + }, + { + "name": "CO", + "value": 162.49 } ] }, - "history": [ - { - "fromDateTime": "2019-10-01T06:00:00.000Z", - "tillDateTime": "2019-10-01T07:00:00.000Z", - "values": [ - { - "name": "PM1", - "value": 5.95 - }, - { - "name": "PM25", - "value": 8.54 - }, - { - "name": "PM10", - "value": 11.46 - }, - { - "name": "PRESSURE", - "value": 1009.61 - }, - { - "name": "HUMIDITY", - "value": 97.6 - }, - { - "name": "TEMPERATURE", - "value": 9.71 - } - ], - "indexes": [ - { - "name": "AIRLY_CAQI", - "value": 14.24, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "Green equals clean!", - "color": "#6BC926" - } - ], - "standards": [ - { - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 34.18 - }, - { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 22.91 - } - ] - }, - { - "fromDateTime": "2019-10-01T07:00:00.000Z", - "tillDateTime": "2019-10-01T08:00:00.000Z", - "values": [ - { - "name": "PM1", - "value": 4.2 - }, - { - "name": "PM25", - "value": 5.88 - }, - { - "name": "PM10", - "value": 7.88 - }, - { - "name": "PRESSURE", - "value": 1009.13 - }, - { - "name": "HUMIDITY", - "value": 90.84 - }, - { - "name": "TEMPERATURE", - "value": 12.65 - } - ], - "indexes": [ - { - "name": "AIRLY_CAQI", - "value": 9.81, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "Dear me, how wonderful!", - "color": "#6BC926" - } - ], - "standards": [ - { - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 23.53 - }, - { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 15.75 - } - ] - }, - { - "fromDateTime": "2019-10-01T08:00:00.000Z", - "tillDateTime": "2019-10-01T09:00:00.000Z", - "values": [ - { - "name": "PM1", - "value": 3.63 - }, - { - "name": "PM25", - "value": 5.56 - }, - { - "name": "PM10", - "value": 7.71 - }, - { - "name": "PRESSURE", - "value": 1008.27 - }, - { - "name": "HUMIDITY", - "value": 84.61 - }, - { - "name": "TEMPERATURE", - "value": 15.57 - } - ], - "indexes": [ - { - "name": "AIRLY_CAQI", - "value": 9.26, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "Dear me, how wonderful!", - "color": "#6BC926" - } - ], - "standards": [ - { - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 22.23 - }, - { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 15.42 - } - ] - }, - { - "fromDateTime": "2019-10-01T09:00:00.000Z", - "tillDateTime": "2019-10-01T10:00:00.000Z", - "values": [ - { - "name": "PM1", - "value": 2.9 - }, - { - "name": "PM25", - "value": 3.93 - }, - { - "name": "PM10", - "value": 5.24 - }, - { - "name": "PRESSURE", - "value": 1007.57 - }, - { - "name": "HUMIDITY", - "value": 79.52 - }, - { - "name": "TEMPERATURE", - "value": 16.57 - } - ], - "indexes": [ - { - "name": "AIRLY_CAQI", - "value": 6.56, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "Breathe deep! The air is clean!", - "color": "#6BC926" - } - ], - "standards": [ - { - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 15.74 - }, - { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 10.48 - } - ] - }, - { - "fromDateTime": "2019-10-01T10:00:00.000Z", - "tillDateTime": "2019-10-01T11:00:00.000Z", - "values": [ - { - "name": "PM1", - "value": 2.45 - }, - { - "name": "PM25", - "value": 3.33 - }, - { - "name": "PM10", - "value": 4.52 - }, - { - "name": "PRESSURE", - "value": 1006.75 - }, - { - "name": "HUMIDITY", - "value": 74.09 - }, - { - "name": "TEMPERATURE", - "value": 16.95 - } - ], - "indexes": [ - { - "name": "AIRLY_CAQI", - "value": 5.55, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "The air is grand today. ;)", - "color": "#6BC926" - } - ], - "standards": [ - { - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 13.31 - }, - { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 9.04 - } - ] - }, - { - "fromDateTime": "2019-10-01T11:00:00.000Z", - "tillDateTime": "2019-10-01T12:00:00.000Z", - "values": [ - { - "name": "PM1", - "value": 2.0 - }, - { - "name": "PM25", - "value": 2.93 - }, - { - "name": "PM10", - "value": 3.98 - }, - { - "name": "PRESSURE", - "value": 1005.71 - }, - { - "name": "HUMIDITY", - "value": 69.06 - }, - { - "name": "TEMPERATURE", - "value": 17.31 - } - ], - "indexes": [ - { - "name": "AIRLY_CAQI", - "value": 4.89, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "Green equals clean!", - "color": "#6BC926" - } - ], - "standards": [ - { - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 11.74 - }, - { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 7.96 - } - ] - }, - { - "fromDateTime": "2019-10-01T12:00:00.000Z", - "tillDateTime": "2019-10-01T13:00:00.000Z", - "values": [ - { - "name": "PM1", - "value": 1.92 - }, - { - "name": "PM25", - "value": 2.69 - }, - { - "name": "PM10", - "value": 3.68 - }, - { - "name": "PRESSURE", - "value": 1005.03 - }, - { - "name": "HUMIDITY", - "value": 65.08 - }, - { - "name": "TEMPERATURE", - "value": 17.47 - } - ], - "indexes": [ - { - "name": "AIRLY_CAQI", - "value": 4.49, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "Enjoy life!", - "color": "#6BC926" - } - ], - "standards": [ - { - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 10.77 - }, - { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 7.36 - } - ] - }, - { - "fromDateTime": "2019-10-01T13:00:00.000Z", - "tillDateTime": "2019-10-01T14:00:00.000Z", - "values": [ - { - "name": "PM1", - "value": 1.79 - }, - { - "name": "PM25", - "value": 2.57 - }, - { - "name": "PM10", - "value": 3.53 - }, - { - "name": "PRESSURE", - "value": 1004.26 - }, - { - "name": "HUMIDITY", - "value": 63.72 - }, - { - "name": "TEMPERATURE", - "value": 17.91 - } - ], - "indexes": [ - { - "name": "AIRLY_CAQI", - "value": 4.29, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "Great air!", - "color": "#6BC926" - } - ], - "standards": [ - { - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 10.29 - }, - { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 7.06 - } - ] - }, - { - "fromDateTime": "2019-10-01T14:00:00.000Z", - "tillDateTime": "2019-10-01T15:00:00.000Z", - "values": [ - { - "name": "PM1", - "value": 2.06 - }, - { - "name": "PM25", - "value": 3.08 - }, - { - "name": "PM10", - "value": 4.23 - }, - { - "name": "PRESSURE", - "value": 1003.46 - }, - { - "name": "HUMIDITY", - "value": 64.44 - }, - { - "name": "TEMPERATURE", - "value": 17.84 - } - ], - "indexes": [ - { - "name": "AIRLY_CAQI", - "value": 5.14, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "The air is grand today. ;)", - "color": "#6BC926" - } - ], - "standards": [ - { - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 12.33 - }, - { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 8.47 - } - ] - }, - { - "fromDateTime": "2019-10-01T15:00:00.000Z", - "tillDateTime": "2019-10-01T16:00:00.000Z", - "values": [ - { - "name": "PM1", - "value": 3.17 - }, - { - "name": "PM25", - "value": 4.61 - }, - { - "name": "PM10", - "value": 6.25 - }, - { - "name": "PRESSURE", - "value": 1003.18 - }, - { - "name": "HUMIDITY", - "value": 65.32 - }, - { - "name": "TEMPERATURE", - "value": 18.08 - } - ], - "indexes": [ - { - "name": "AIRLY_CAQI", - "value": 7.68, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "Green, green, green!", - "color": "#6BC926" - } - ], - "standards": [ - { - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 18.44 - }, - { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 12.5 - } - ] - }, - { - "fromDateTime": "2019-10-01T16:00:00.000Z", - "tillDateTime": "2019-10-01T17:00:00.000Z", - "values": [ - { - "name": "PM1", - "value": 4.17 - }, - { - "name": "PM25", - "value": 5.91 - }, - { - "name": "PM10", - "value": 8.06 - }, - { - "name": "PRESSURE", - "value": 1003.05 - }, - { - "name": "HUMIDITY", - "value": 66.14 - }, - { - "name": "TEMPERATURE", - "value": 17.04 - } - ], - "indexes": [ - { - "name": "AIRLY_CAQI", - "value": 9.84, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "Enjoy life!", - "color": "#6BC926" - } - ], - "standards": [ - { - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 23.62 - }, - { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 16.11 - } - ] - }, - { - "fromDateTime": "2019-10-01T17:00:00.000Z", - "tillDateTime": "2019-10-01T18:00:00.000Z", - "values": [ - { - "name": "PM1", - "value": 6.4 - }, - { - "name": "PM25", - "value": 10.93 - }, - { - "name": "PM10", - "value": 15.7 - }, - { - "name": "PRESSURE", - "value": 1002.85 - }, - { - "name": "HUMIDITY", - "value": 68.31 - }, - { - "name": "TEMPERATURE", - "value": 16.33 - } - ], - "indexes": [ - { - "name": "AIRLY_CAQI", - "value": 18.22, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "It couldn't be better ;)", - "color": "#6BC926" - } - ], - "standards": [ - { - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 43.74 - }, - { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 31.4 - } - ] - }, - { - "fromDateTime": "2019-10-01T18:00:00.000Z", - "tillDateTime": "2019-10-01T19:00:00.000Z", - "values": [ - { - "name": "PM1", - "value": 4.79 - }, - { - "name": "PM25", - "value": 7.41 - }, - { - "name": "PM10", - "value": 10.31 - }, - { - "name": "PRESSURE", - "value": 1002.52 - }, - { - "name": "HUMIDITY", - "value": 69.88 - }, - { - "name": "TEMPERATURE", - "value": 15.98 - } - ], - "indexes": [ - { - "name": "AIRLY_CAQI", - "value": 12.35, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "Enjoy life!", - "color": "#6BC926" - } - ], - "standards": [ - { - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 29.65 - }, - { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 20.63 - } - ] - }, - { - "fromDateTime": "2019-10-01T19:00:00.000Z", - "tillDateTime": "2019-10-01T20:00:00.000Z", - "values": [ - { - "name": "PM1", - "value": 5.99 - }, - { - "name": "PM25", - "value": 9.45 - }, - { - "name": "PM10", - "value": 13.22 - }, - { - "name": "PRESSURE", - "value": 1002.32 - }, - { - "name": "HUMIDITY", - "value": 70.47 - }, - { - "name": "TEMPERATURE", - "value": 15.76 - } - ], - "indexes": [ - { - "name": "AIRLY_CAQI", - "value": 15.74, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "Breathe deeply!", - "color": "#6BC926" - } - ], - "standards": [ - { - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 37.78 - }, - { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 26.44 - } - ] - }, - { - "fromDateTime": "2019-10-01T20:00:00.000Z", - "tillDateTime": "2019-10-01T21:00:00.000Z", - "values": [ - { - "name": "PM1", - "value": 9.35 - }, - { - "name": "PM25", - "value": 14.67 - }, - { - "name": "PM10", - "value": 20.57 - }, - { - "name": "PRESSURE", - "value": 1002.46 - }, - { - "name": "HUMIDITY", - "value": 72.61 - }, - { - "name": "TEMPERATURE", - "value": 15.47 - } - ], - "indexes": [ - { - "name": "AIRLY_CAQI", - "value": 24.45, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "It couldn't be better ;)", - "color": "#6BC926" - } - ], - "standards": [ - { - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 58.68 - }, - { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 41.13 - } - ] - }, - { - "fromDateTime": "2019-10-01T21:00:00.000Z", - "tillDateTime": "2019-10-01T22:00:00.000Z", - "values": [ - { - "name": "PM1", - "value": 9.95 - }, - { - "name": "PM25", - "value": 15.37 - }, - { - "name": "PM10", - "value": 21.33 - }, - { - "name": "PRESSURE", - "value": 1002.59 - }, - { - "name": "HUMIDITY", - "value": 75.09 - }, - { - "name": "TEMPERATURE", - "value": 15.17 - } - ], - "indexes": [ - { - "name": "AIRLY_CAQI", - "value": 25.62, - "level": "LOW", - "description": "Air is quite good.", - "advice": "Take a breath!", - "color": "#D1CF1E" - } - ], - "standards": [ - { - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 61.48 - }, - { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 42.66 - } - ] - }, - { - "fromDateTime": "2019-10-01T22:00:00.000Z", - "tillDateTime": "2019-10-01T23:00:00.000Z", - "values": [ - { - "name": "PM1", - "value": 10.16 - }, - { - "name": "PM25", - "value": 15.78 - }, - { - "name": "PM10", - "value": 21.97 - }, - { - "name": "PRESSURE", - "value": 1002.59 - }, - { - "name": "HUMIDITY", - "value": 77.68 - }, - { - "name": "TEMPERATURE", - "value": 14.9 - } - ], - "indexes": [ - { - "name": "AIRLY_CAQI", - "value": 26.31, - "level": "LOW", - "description": "Air is quite good.", - "advice": "Great air for a walk to the park!", - "color": "#D1CF1E" - } - ], - "standards": [ - { - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 63.14 - }, - { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 43.93 - } - ] - }, - { - "fromDateTime": "2019-10-01T23:00:00.000Z", - "tillDateTime": "2019-10-02T00:00:00.000Z", - "values": [ - { - "name": "PM1", - "value": 9.86 - }, - { - "name": "PM25", - "value": 15.14 - }, - { - "name": "PM10", - "value": 21.07 - }, - { - "name": "PRESSURE", - "value": 1002.49 - }, - { - "name": "HUMIDITY", - "value": 79.86 - }, - { - "name": "TEMPERATURE", - "value": 14.56 - } - ], - "indexes": [ - { - "name": "AIRLY_CAQI", - "value": 25.24, - "level": "LOW", - "description": "Air is quite good.", - "advice": "Leave the mask at home today!", - "color": "#D1CF1E" - } - ], - "standards": [ - { - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 60.57 - }, - { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 42.14 - } - ] - }, - { - "fromDateTime": "2019-10-02T00:00:00.000Z", - "tillDateTime": "2019-10-02T01:00:00.000Z", - "values": [ - { - "name": "PM1", - "value": 9.77 - }, - { - "name": "PM25", - "value": 15.04 - }, - { - "name": "PM10", - "value": 20.97 - }, - { - "name": "PRESSURE", - "value": 1002.18 - }, - { - "name": "HUMIDITY", - "value": 81.77 - }, - { - "name": "TEMPERATURE", - "value": 14.13 - } - ], - "indexes": [ - { - "name": "AIRLY_CAQI", - "value": 25.07, - "level": "LOW", - "description": "Air is quite good.", - "advice": "Time for a walk with friends or activities with your family - because the air is clean!", - "color": "#D1CF1E" - } - ], - "standards": [ - { - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 60.18 - }, - { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 41.94 - } - ] - }, - { - "fromDateTime": "2019-10-02T01:00:00.000Z", - "tillDateTime": "2019-10-02T02:00:00.000Z", - "values": [ - { - "name": "PM1", - "value": 9.67 - }, - { - "name": "PM25", - "value": 14.9 - }, - { - "name": "PM10", - "value": 20.67 - }, - { - "name": "PRESSURE", - "value": 1002.01 - }, - { - "name": "HUMIDITY", - "value": 84.5 - }, - { - "name": "TEMPERATURE", - "value": 13.7 - } - ], - "indexes": [ - { - "name": "AIRLY_CAQI", - "value": 24.84, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "Great air!", - "color": "#6BC926" - } - ], - "standards": [ - { - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 59.62 - }, - { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 41.33 - } - ] - }, - { - "fromDateTime": "2019-10-02T02:00:00.000Z", - "tillDateTime": "2019-10-02T03:00:00.000Z", - "values": [ - { - "name": "PM1", - "value": 7.17 - }, - { - "name": "PM25", - "value": 10.7 - }, - { - "name": "PM10", - "value": 14.58 - }, - { - "name": "PRESSURE", - "value": 1001.56 - }, - { - "name": "HUMIDITY", - "value": 88.55 - }, - { - "name": "TEMPERATURE", - "value": 13.44 - } - ], - "indexes": [ - { - "name": "AIRLY_CAQI", - "value": 17.83, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "Catch your breath!", - "color": "#6BC926" - } - ], - "standards": [ - { - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 42.8 - }, - { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 29.17 - } - ] - }, - { - "fromDateTime": "2019-10-02T03:00:00.000Z", - "tillDateTime": "2019-10-02T04:00:00.000Z", - "values": [ - { - "name": "PM1", - "value": 6.99 - }, - { - "name": "PM25", - "value": 10.23 - }, - { - "name": "PM10", - "value": 13.66 - }, - { - "name": "PRESSURE", - "value": 1001.34 - }, - { - "name": "HUMIDITY", - "value": 90.82 - }, - { - "name": "TEMPERATURE", - "value": 13.3 - } - ], - "indexes": [ - { - "name": "AIRLY_CAQI", - "value": 17.05, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "Perfect air for exercising! Go for it!", - "color": "#6BC926" - } - ], - "standards": [ - { - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 40.91 - }, - { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 27.33 - } - ] - }, - { - "fromDateTime": "2019-10-02T04:00:00.000Z", - "tillDateTime": "2019-10-02T05:00:00.000Z", - "values": [ - { - "name": "PM1", - "value": 7.82 - }, - { - "name": "PM25", - "value": 11.59 - }, - { - "name": "PM10", - "value": 15.77 - }, - { - "name": "PRESSURE", - "value": 1000.92 - }, - { - "name": "HUMIDITY", - "value": 91.8 - }, - { - "name": "TEMPERATURE", - "value": 13.34 - } - ], - "indexes": [ - { - "name": "AIRLY_CAQI", - "value": 19.32, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "Dear me, how wonderful!", - "color": "#6BC926" - } - ], - "standards": [ - { - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 46.36 - }, - { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 31.54 - } - ] - }, - { - "fromDateTime": "2019-10-02T05:00:00.000Z", - "tillDateTime": "2019-10-02T06:00:00.000Z", - "values": [ - { - "name": "PM1", - "value": 10.16 - }, - { - "name": "PM25", - "value": 15.35 - }, - { - "name": "PM10", - "value": 21.45 - }, - { - "name": "PRESSURE", - "value": 1000.82 - }, - { - "name": "HUMIDITY", - "value": 92.15 - }, - { - "name": "TEMPERATURE", - "value": 13.74 - } - ], - "indexes": [ - { - "name": "AIRLY_CAQI", - "value": 25.59, - "level": "LOW", - "description": "Air is quite good.", - "advice": "How about going for a walk?", - "color": "#D1CF1E" - } - ], - "standards": [ - { - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 61.42 - }, - { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 42.9 - } - ] - } - ], "forecast": [ { - "fromDateTime": "2019-10-02T06:00:00.000Z", - "tillDateTime": "2019-10-02T07:00:00.000Z", - "values": [ - { - "name": "PM25", - "value": 13.28 - }, - { - "name": "PM10", - "value": 18.37 - } - ], + "fromDateTime": "2022-09-06T23:00:00.000Z", "indexes": [ { - "name": "AIRLY_CAQI", - "value": 22.14, - "level": "VERY_LOW", - "description": "Great air here today!", "advice": "It couldn't be better ;)", - "color": "#6BC926" + "color": "#6BC926", + "description": "Great air here today!", + "level": "VERY_LOW", + "name": "AIRLY_CAQI", + "value": 6.68 } ], "standards": [ { + "averaging": "24h", + "limit": 15, "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 53.13 + "percent": 26.71, + "pollutant": "PM25" }, { + "averaging": "24h", + "limit": 45, "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 36.73 + "percent": 11.47, + "pollutant": "PM10" + } + ], + "tillDateTime": "2022-09-07T00:00:00.000Z", + "values": [ + { + "name": "PM25", + "value": 4.01 + }, + { + "name": "PM10", + "value": 5.07 } ] }, { - "fromDateTime": "2019-10-02T07:00:00.000Z", - "tillDateTime": "2019-10-02T08:00:00.000Z", - "values": [ - { - "name": "PM25", - "value": 11.19 - }, - { - "name": "PM10", - "value": 15.65 - } - ], + "fromDateTime": "2022-09-07T00:00:00.000Z", "indexes": [ { - "name": "AIRLY_CAQI", - "value": 18.65, - "level": "VERY_LOW", + "advice": "Dear me, how wonderful!", + "color": "#6BC926", "description": "Great air here today!", - "advice": "Enjoy life!", - "color": "#6BC926" + "level": "VERY_LOW", + "name": "AIRLY_CAQI", + "value": 6.4 } ], "standards": [ { + "averaging": "24h", + "limit": 15, "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 44.76 + "percent": 25.57, + "pollutant": "PM25" }, { + "averaging": "24h", + "limit": 45, "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 31.31 + "percent": 10.79, + "pollutant": "PM10" + } + ], + "tillDateTime": "2022-09-07T01:00:00.000Z", + "values": [ + { + "name": "PM25", + "value": 3.84 + }, + { + "name": "PM10", + "value": 4.67 } ] }, { - "fromDateTime": "2019-10-02T08:00:00.000Z", - "tillDateTime": "2019-10-02T09:00:00.000Z", - "values": [ - { - "name": "PM25", - "value": 8.79 - }, - { - "name": "PM10", - "value": 12.8 - } - ], + "fromDateTime": "2022-09-07T01:00:00.000Z", "indexes": [ { - "name": "AIRLY_CAQI", - "value": 14.65, - "level": "VERY_LOW", + "advice": "Perfect air for exercising! Go for it!", + "color": "#6BC926", "description": "Great air here today!", - "advice": "Breathe deep! The air is clean!", - "color": "#6BC926" + "level": "VERY_LOW", + "name": "AIRLY_CAQI", + "value": 7.7 } ], "standards": [ { + "averaging": "24h", + "limit": 15, "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 35.15 + "percent": 30.78, + "pollutant": "PM25" }, { + "averaging": "24h", + "limit": 45, "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 25.59 + "percent": 12.51, + "pollutant": "PM10" + } + ], + "tillDateTime": "2022-09-07T02:00:00.000Z", + "values": [ + { + "name": "PM25", + "value": 4.62 + }, + { + "name": "PM10", + "value": 5.33 } ] }, { - "fromDateTime": "2019-10-02T09:00:00.000Z", - "tillDateTime": "2019-10-02T10:00:00.000Z", - "values": [ - { - "name": "PM25", - "value": 5.46 - }, - { - "name": "PM10", - "value": 8.91 - } - ], + "fromDateTime": "2022-09-07T02:00:00.000Z", "indexes": [ { - "name": "AIRLY_CAQI", - "value": 9.11, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "Breathe to fill your lungs!", - "color": "#6BC926" - } - ], - "standards": [ - { - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 21.86 - }, - { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 17.83 - } - ] - }, - { - "fromDateTime": "2019-10-02T10:00:00.000Z", - "tillDateTime": "2019-10-02T11:00:00.000Z", - "values": [ - { - "name": "PM25", - "value": 2.26 - }, - { - "name": "PM10", - "value": 5.02 - } - ], - "indexes": [ - { - "name": "AIRLY_CAQI", - "value": 5.02, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "Enjoy life!", - "color": "#6BC926" - } - ], - "standards": [ - { - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 9.06 - }, - { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 10.05 - } - ] - }, - { - "fromDateTime": "2019-10-02T11:00:00.000Z", - "tillDateTime": "2019-10-02T12:00:00.000Z", - "values": [ - { - "name": "PM25", - "value": 1.06 - }, - { - "name": "PM10", - "value": 2.52 - } - ], - "indexes": [ - { - "name": "AIRLY_CAQI", - "value": 2.52, - "level": "VERY_LOW", - "description": "Great air here today!", "advice": "The air is great!", - "color": "#6BC926" + "color": "#6BC926", + "description": "Great air here today!", + "level": "VERY_LOW", + "name": "AIRLY_CAQI", + "value": 9.05 } ], "standards": [ { + "averaging": "24h", + "limit": 15, "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 4.22 + "percent": 36.18, + "pollutant": "PM25" }, { + "averaging": "24h", + "limit": 45, "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 5.05 + "percent": 15.28, + "pollutant": "PM10" + } + ], + "tillDateTime": "2022-09-07T03:00:00.000Z", + "values": [ + { + "name": "PM25", + "value": 5.43 + }, + { + "name": "PM10", + "value": 6.43 } ] }, { - "fromDateTime": "2019-10-02T12:00:00.000Z", - "tillDateTime": "2019-10-02T13:00:00.000Z", - "values": [ - { - "name": "PM25", - "value": 0.48 - }, - { - "name": "PM10", - "value": 1.94 - } - ], + "fromDateTime": "2022-09-07T03:00:00.000Z", "indexes": [ { - "name": "AIRLY_CAQI", - "value": 1.94, - "level": "VERY_LOW", + "advice": "It couldn't be better ;)", + "color": "#6BC926", "description": "Great air here today!", - "advice": "Breathe as much as you can!", - "color": "#6BC926" + "level": "VERY_LOW", + "name": "AIRLY_CAQI", + "value": 11.18 } ], "standards": [ { + "averaging": "24h", + "limit": 15, "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 1.94 + "percent": 44.69, + "pollutant": "PM25" }, { + "averaging": "24h", + "limit": 45, "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 3.89 + "percent": 18.51, + "pollutant": "PM10" + } + ], + "tillDateTime": "2022-09-07T04:00:00.000Z", + "values": [ + { + "name": "PM25", + "value": 6.7 + }, + { + "name": "PM10", + "value": 7.69 } ] }, { - "fromDateTime": "2019-10-02T13:00:00.000Z", - "tillDateTime": "2019-10-02T14:00:00.000Z", + "fromDateTime": "2022-09-07T04:00:00.000Z", + "indexes": [ + { + "advice": "Zero dust - zero worries!", + "color": "#6BC926", + "description": "Great air here today!", + "level": "VERY_LOW", + "name": "AIRLY_CAQI", + "value": 13.08 + } + ], + "standards": [ + { + "averaging": "24h", + "limit": 15, + "name": "WHO", + "percent": 52.29, + "pollutant": "PM25" + }, + { + "averaging": "24h", + "limit": 45, + "name": "WHO", + "percent": 23.47, + "pollutant": "PM10" + } + ], + "tillDateTime": "2022-09-07T05:00:00.000Z", "values": [ { "name": "PM25", - "value": 0.63 + "value": 7.84 }, { "name": "PM10", - "value": 2.26 + "value": 9.89 } - ], + ] + }, + { + "fromDateTime": "2022-09-07T05:00:00.000Z", "indexes": [ { - "name": "AIRLY_CAQI", - "value": 2.26, - "level": "VERY_LOW", - "description": "Great air here today!", "advice": "Enjoy life!", - "color": "#6BC926" + "color": "#6BC926", + "description": "Great air here today!", + "level": "VERY_LOW", + "name": "AIRLY_CAQI", + "value": 14.05 } ], "standards": [ { + "averaging": "24h", + "limit": 15, "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 2.53 + "percent": 56.17, + "pollutant": "PM25" }, { + "averaging": "24h", + "limit": 45, "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 4.52 + "percent": 25.86, + "pollutant": "PM10" + } + ], + "tillDateTime": "2022-09-07T06:00:00.000Z", + "values": [ + { + "name": "PM25", + "value": 8.42 + }, + { + "name": "PM10", + "value": 11.09 } ] }, { - "fromDateTime": "2019-10-02T14:00:00.000Z", - "tillDateTime": "2019-10-02T15:00:00.000Z", - "values": [ - { - "name": "PM25", - "value": 1.47 - }, - { - "name": "PM10", - "value": 3.39 - } - ], + "fromDateTime": "2022-09-07T06:00:00.000Z", "indexes": [ { - "name": "AIRLY_CAQI", - "value": 3.39, - "level": "VERY_LOW", + "advice": "It couldn't be better ;)", + "color": "#6BC926", "description": "Great air here today!", - "advice": "Breathe as much as you can!", - "color": "#6BC926" + "level": "VERY_LOW", + "name": "AIRLY_CAQI", + "value": 13.22 } ], "standards": [ { + "averaging": "24h", + "limit": 15, "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 5.87 + "percent": 52.87, + "pollutant": "PM25" }, { + "averaging": "24h", + "limit": 45, "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 6.78 + "percent": 25.01, + "pollutant": "PM10" + } + ], + "tillDateTime": "2022-09-07T07:00:00.000Z", + "values": [ + { + "name": "PM25", + "value": 7.93 + }, + { + "name": "PM10", + "value": 10.83 } ] }, { - "fromDateTime": "2019-10-02T15:00:00.000Z", - "tillDateTime": "2019-10-02T16:00:00.000Z", + "fromDateTime": "2022-09-07T07:00:00.000Z", + "indexes": [ + { + "advice": "The air is great!", + "color": "#6BC926", + "description": "Great air here today!", + "level": "VERY_LOW", + "name": "AIRLY_CAQI", + "value": 11.54 + } + ], + "standards": [ + { + "averaging": "24h", + "limit": 15, + "name": "WHO", + "percent": 46.14, + "pollutant": "PM25" + }, + { + "averaging": "24h", + "limit": 45, + "name": "WHO", + "percent": 22.44, + "pollutant": "PM10" + } + ], + "tillDateTime": "2022-09-07T08:00:00.000Z", "values": [ { "name": "PM25", - "value": 2.62 + "value": 6.92 }, { "name": "PM10", - "value": 5.02 + "value": 9.71 } - ], + ] + }, + { + "fromDateTime": "2022-09-07T08:00:00.000Z", "indexes": [ { - "name": "AIRLY_CAQI", - "value": 5.02, - "level": "VERY_LOW", - "description": "Great air here today!", "advice": "Great air!", - "color": "#6BC926" + "color": "#6BC926", + "description": "Great air here today!", + "level": "VERY_LOW", + "name": "AIRLY_CAQI", + "value": 10.26 } ], "standards": [ { + "averaging": "24h", + "limit": 15, "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 10.5 + "percent": 41.03, + "pollutant": "PM25" }, { + "averaging": "24h", + "limit": 45, "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 10.05 + "percent": 19.04, + "pollutant": "PM10" + } + ], + "tillDateTime": "2022-09-07T09:00:00.000Z", + "values": [ + { + "name": "PM25", + "value": 6.15 + }, + { + "name": "PM10", + "value": 8.21 } ] }, { - "fromDateTime": "2019-10-02T16:00:00.000Z", - "tillDateTime": "2019-10-02T17:00:00.000Z", + "fromDateTime": "2022-09-07T09:00:00.000Z", + "indexes": [ + { + "advice": "Catch your breath!", + "color": "#6BC926", + "description": "Great air here today!", + "level": "VERY_LOW", + "name": "AIRLY_CAQI", + "value": 8.94 + } + ], + "standards": [ + { + "averaging": "24h", + "limit": 15, + "name": "WHO", + "percent": 35.72, + "pollutant": "PM25" + }, + { + "averaging": "24h", + "limit": 45, + "name": "WHO", + "percent": 17.02, + "pollutant": "PM10" + } + ], + "tillDateTime": "2022-09-07T10:00:00.000Z", + "values": [ + { + "name": "PM25", + "value": 5.36 + }, + { + "name": "PM10", + "value": 7.25 + } + ] + }, + { + "fromDateTime": "2022-09-07T10:00:00.000Z", + "indexes": [ + { + "advice": "Perfect air for exercising! Go for it!", + "color": "#6BC926", + "description": "Great air here today!", + "level": "VERY_LOW", + "name": "AIRLY_CAQI", + "value": 7.96 + } + ], + "standards": [ + { + "averaging": "24h", + "limit": 15, + "name": "WHO", + "percent": 31.81, + "pollutant": "PM25" + }, + { + "averaging": "24h", + "limit": 45, + "name": "WHO", + "percent": 16.22, + "pollutant": "PM10" + } + ], + "tillDateTime": "2022-09-07T11:00:00.000Z", + "values": [ + { + "name": "PM25", + "value": 4.77 + }, + { + "name": "PM10", + "value": 6.65 + } + ] + }, + { + "fromDateTime": "2022-09-07T11:00:00.000Z", + "indexes": [ + { + "advice": "The air is great!", + "color": "#6BC926", + "description": "Great air here today!", + "level": "VERY_LOW", + "name": "AIRLY_CAQI", + "value": 7.16 + } + ], + "standards": [ + { + "averaging": "24h", + "limit": 15, + "name": "WHO", + "percent": 28.61, + "pollutant": "PM25" + }, + { + "averaging": "24h", + "limit": 45, + "name": "WHO", + "percent": 15.74, + "pollutant": "PM10" + } + ], + "tillDateTime": "2022-09-07T12:00:00.000Z", + "values": [ + { + "name": "PM25", + "value": 4.29 + }, + { + "name": "PM10", + "value": 6.19 + } + ] + }, + { + "fromDateTime": "2022-09-07T12:00:00.000Z", + "indexes": [ + { + "advice": "Green equals clean!", + "color": "#6BC926", + "description": "Great air here today!", + "level": "VERY_LOW", + "name": "AIRLY_CAQI", + "value": 6.53 + } + ], + "standards": [ + { + "averaging": "24h", + "limit": 15, + "name": "WHO", + "percent": 26.08, + "pollutant": "PM25" + }, + { + "averaging": "24h", + "limit": 45, + "name": "WHO", + "percent": 14.64, + "pollutant": "PM10" + } + ], + "tillDateTime": "2022-09-07T13:00:00.000Z", + "values": [ + { + "name": "PM25", + "value": 3.91 + }, + { + "name": "PM10", + "value": 5.67 + } + ] + }, + { + "fromDateTime": "2022-09-07T13:00:00.000Z", + "indexes": [ + { + "advice": "Enjoy life!", + "color": "#6BC926", + "description": "Great air here today!", + "level": "VERY_LOW", + "name": "AIRLY_CAQI", + "value": 5.94 + } + ], + "standards": [ + { + "averaging": "24h", + "limit": 15, + "name": "WHO", + "percent": 23.72, + "pollutant": "PM25" + }, + { + "averaging": "24h", + "limit": 45, + "name": "WHO", + "percent": 13.03, + "pollutant": "PM10" + } + ], + "tillDateTime": "2022-09-07T14:00:00.000Z", + "values": [ + { + "name": "PM25", + "value": 3.56 + }, + { + "name": "PM10", + "value": 5.11 + } + ] + }, + { + "fromDateTime": "2022-09-07T14:00:00.000Z", + "indexes": [ + { + "advice": "Breathe as much as you can!", + "color": "#6BC926", + "description": "Great air here today!", + "level": "VERY_LOW", + "name": "AIRLY_CAQI", + "value": 6.49 + } + ], + "standards": [ + { + "averaging": "24h", + "limit": 15, + "name": "WHO", + "percent": 25.93, + "pollutant": "PM25" + }, + { + "averaging": "24h", + "limit": 45, + "name": "WHO", + "percent": 14.58, + "pollutant": "PM10" + } + ], + "tillDateTime": "2022-09-07T15:00:00.000Z", "values": [ { "name": "PM25", @@ -1742,525 +736,2393 @@ }, { "name": "PM10", - "value": 8.02 - } - ], - "indexes": [ - { - "name": "AIRLY_CAQI", - "value": 8.02, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "Dear me, how wonderful!", - "color": "#6BC926" - } - ], - "standards": [ - { - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 15.56 - }, - { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 16.04 + "value": 6.09 } ] }, { - "fromDateTime": "2019-10-02T17:00:00.000Z", - "tillDateTime": "2019-10-02T18:00:00.000Z", + "fromDateTime": "2022-09-07T15:00:00.000Z", + "indexes": [ + { + "advice": "The air is grand today. ;)", + "color": "#6BC926", + "description": "Great air here today!", + "level": "VERY_LOW", + "name": "AIRLY_CAQI", + "value": 8.73 + } + ], + "standards": [ + { + "averaging": "24h", + "limit": 15, + "name": "WHO", + "percent": 33.18, + "pollutant": "PM25" + }, + { + "averaging": "24h", + "limit": 45, + "name": "WHO", + "percent": 20, + "pollutant": "PM10" + } + ], + "tillDateTime": "2022-09-07T16:00:00.000Z", "values": [ { "name": "PM25", - "value": 6.26 + "value": 4.98 }, { "name": "PM10", - "value": 11.41 + "value": 8.72 } - ], + ] + }, + { + "fromDateTime": "2022-09-07T16:00:00.000Z", "indexes": [ { - "name": "AIRLY_CAQI", - "value": 11.41, - "level": "VERY_LOW", - "description": "Great air here today!", "advice": "The air is great!", - "color": "#6BC926" - } - ], - "standards": [ - { - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 25.05 - }, - { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 22.83 - } - ] - }, - { - "fromDateTime": "2019-10-02T18:00:00.000Z", - "tillDateTime": "2019-10-02T19:00:00.000Z", - "values": [ - { - "name": "PM25", - "value": 8.69 - }, - { - "name": "PM10", - "value": 14.48 - } - ], - "indexes": [ - { - "name": "AIRLY_CAQI", - "value": 14.48, - "level": "VERY_LOW", + "color": "#6BC926", "description": "Great air here today!", - "advice": "Zero dust - zero worries!", - "color": "#6BC926" - } - ], - "standards": [ - { - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 34.76 - }, - { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 28.96 - } - ] - }, - { - "fromDateTime": "2019-10-02T19:00:00.000Z", - "tillDateTime": "2019-10-02T20:00:00.000Z", - "values": [ - { - "name": "PM25", - "value": 10.78 - }, - { - "name": "PM10", - "value": 16.86 - } - ], - "indexes": [ - { - "name": "AIRLY_CAQI", - "value": 17.97, "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "Zero dust - zero worries!", - "color": "#6BC926" - } - ], - "standards": [ - { - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 43.13 - }, - { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 33.72 - } - ] - }, - { - "fromDateTime": "2019-10-02T20:00:00.000Z", - "tillDateTime": "2019-10-02T21:00:00.000Z", - "values": [ - { - "name": "PM25", - "value": 12.22 - }, - { - "name": "PM10", - "value": 18.19 - } - ], - "indexes": [ - { "name": "AIRLY_CAQI", - "value": 20.36, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "Breathe to fill your lungs!", - "color": "#6BC926" + "value": 13.68 } ], "standards": [ { + "averaging": "24h", + "limit": 15, "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 48.88 + "percent": 45.97, + "pollutant": "PM25" }, { + "averaging": "24h", + "limit": 45, "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 36.38 + "percent": 31.41, + "pollutant": "PM10" } - ] - }, - { - "fromDateTime": "2019-10-02T21:00:00.000Z", - "tillDateTime": "2019-10-02T22:00:00.000Z", + ], + "tillDateTime": "2022-09-07T17:00:00.000Z", "values": [ { "name": "PM25", - "value": 13.06 - }, - { - "name": "PM10", - "value": 18.62 - } - ], - "indexes": [ - { - "name": "AIRLY_CAQI", - "value": 21.77, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "Dear me, how wonderful!", - "color": "#6BC926" - } - ], - "standards": [ - { - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 52.25 - }, - { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 37.24 - } - ] - }, - { - "fromDateTime": "2019-10-02T22:00:00.000Z", - "tillDateTime": "2019-10-02T23:00:00.000Z", - "values": [ - { - "name": "PM25", - "value": 13.51 - }, - { - "name": "PM10", - "value": 18.49 - } - ], - "indexes": [ - { - "name": "AIRLY_CAQI", - "value": 22.52, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "The air is great!", - "color": "#6BC926" - } - ], - "standards": [ - { - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 54.06 - }, - { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 36.98 - } - ] - }, - { - "fromDateTime": "2019-10-02T23:00:00.000Z", - "tillDateTime": "2019-10-03T00:00:00.000Z", - "values": [ - { - "name": "PM25", - "value": 13.46 - }, - { - "name": "PM10", - "value": 17.63 - } - ], - "indexes": [ - { - "name": "AIRLY_CAQI", - "value": 22.44, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "Green, green, green!", - "color": "#6BC926" - } - ], - "standards": [ - { - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 53.85 - }, - { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 35.26 - } - ] - }, - { - "fromDateTime": "2019-10-03T00:00:00.000Z", - "tillDateTime": "2019-10-03T01:00:00.000Z", - "values": [ - { - "name": "PM25", - "value": 13.05 - }, - { - "name": "PM10", - "value": 16.36 - } - ], - "indexes": [ - { - "name": "AIRLY_CAQI", - "value": 21.74, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "Catch your breath!", - "color": "#6BC926" - } - ], - "standards": [ - { - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 52.19 - }, - { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 32.73 - } - ] - }, - { - "fromDateTime": "2019-10-03T01:00:00.000Z", - "tillDateTime": "2019-10-03T02:00:00.000Z", - "values": [ - { - "name": "PM25", - "value": 12.47 - }, - { - "name": "PM10", - "value": 15.16 - } - ], - "indexes": [ - { - "name": "AIRLY_CAQI", - "value": 20.79, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "Green, green, green!", - "color": "#6BC926" - } - ], - "standards": [ - { - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 49.9 - }, - { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 30.32 - } - ] - }, - { - "fromDateTime": "2019-10-03T02:00:00.000Z", - "tillDateTime": "2019-10-03T03:00:00.000Z", - "values": [ - { - "name": "PM25", - "value": 11.99 - }, - { - "name": "PM10", - "value": 14.07 - } - ], - "indexes": [ - { - "name": "AIRLY_CAQI", - "value": 19.98, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "Breathe as much as you can!", - "color": "#6BC926" - } - ], - "standards": [ - { - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 47.94 - }, - { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 28.14 - } - ] - }, - { - "fromDateTime": "2019-10-03T03:00:00.000Z", - "tillDateTime": "2019-10-03T04:00:00.000Z", - "values": [ - { - "name": "PM25", - "value": 11.74 + "value": 6.9 }, { "name": "PM10", "value": 13.67 } - ], + ] + }, + { + "fromDateTime": "2022-09-07T17:00:00.000Z", "indexes": [ { - "name": "AIRLY_CAQI", - "value": 19.56, - "level": "VERY_LOW", - "description": "Great air here today!", "advice": "Dear me, how wonderful!", - "color": "#6BC926" + "color": "#6BC926", + "description": "Great air here today!", + "level": "VERY_LOW", + "name": "AIRLY_CAQI", + "value": 18.49 } ], "standards": [ { + "averaging": "24h", + "limit": 15, "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 46.95 + "percent": 61.97, + "pollutant": "PM25" }, { + "averaging": "24h", + "limit": 45, "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 27.34 + "percent": 43.21, + "pollutant": "PM10" + } + ], + "tillDateTime": "2022-09-07T18:00:00.000Z", + "values": [ + { + "name": "PM25", + "value": 9.29 + }, + { + "name": "PM10", + "value": 18.48 } ] }, { - "fromDateTime": "2019-10-03T04:00:00.000Z", - "tillDateTime": "2019-10-03T05:00:00.000Z", + "fromDateTime": "2022-09-07T18:00:00.000Z", + "indexes": [ + { + "advice": "Breathe deep! The air is clean!", + "color": "#6BC926", + "description": "Great air here today!", + "level": "VERY_LOW", + "name": "AIRLY_CAQI", + "value": 21.48 + } + ], + "standards": [ + { + "averaging": "24h", + "limit": 15, + "name": "WHO", + "percent": 74.49, + "pollutant": "PM25" + }, + { + "averaging": "24h", + "limit": 45, + "name": "WHO", + "percent": 50.81, + "pollutant": "PM10" + } + ], + "tillDateTime": "2022-09-07T19:00:00.000Z", "values": [ { "name": "PM25", - "value": 11.44 + "value": 11.17 }, { "name": "PM10", - "value": 13.51 + "value": 21.47 } - ], + ] + }, + { + "fromDateTime": "2022-09-07T19:00:00.000Z", "indexes": [ { - "name": "AIRLY_CAQI", - "value": 19.06, - "level": "VERY_LOW", + "advice": "Great air!", + "color": "#6BC926", "description": "Great air here today!", + "level": "VERY_LOW", + "name": "AIRLY_CAQI", + "value": 22.85 + } + ], + "standards": [ + { + "averaging": "24h", + "limit": 15, + "name": "WHO", + "percent": 81.31, + "pollutant": "PM25" + }, + { + "averaging": "24h", + "limit": 45, + "name": "WHO", + "percent": 54.64, + "pollutant": "PM10" + } + ], + "tillDateTime": "2022-09-07T20:00:00.000Z", + "values": [ + { + "name": "PM25", + "value": 12.2 + }, + { + "name": "PM10", + "value": 22.84 + } + ] + }, + { + "fromDateTime": "2022-09-07T20:00:00.000Z", + "indexes": [ + { + "advice": "Enjoy life!", + "color": "#6BC926", + "description": "Great air here today!", + "level": "VERY_LOW", + "name": "AIRLY_CAQI", + "value": 22.1 + } + ], + "standards": [ + { + "averaging": "24h", + "limit": 15, + "name": "WHO", + "percent": 84.94, + "pollutant": "PM25" + }, + { + "averaging": "24h", + "limit": 45, + "name": "WHO", + "percent": 53.31, + "pollutant": "PM10" + } + ], + "tillDateTime": "2022-09-07T21:00:00.000Z", + "values": [ + { + "name": "PM25", + "value": 12.74 + }, + { + "name": "PM10", + "value": 22.1 + } + ] + }, + { + "fromDateTime": "2022-09-07T21:00:00.000Z", + "indexes": [ + { + "advice": "Enjoy life!", + "color": "#6BC926", + "description": "Great air here today!", + "level": "VERY_LOW", + "name": "AIRLY_CAQI", + "value": 21.47 + } + ], + "standards": [ + { + "averaging": "24h", + "limit": 15, + "name": "WHO", + "percent": 83.43, + "pollutant": "PM25" + }, + { + "averaging": "24h", + "limit": 45, + "name": "WHO", + "percent": 51.94, + "pollutant": "PM10" + } + ], + "tillDateTime": "2022-09-07T22:00:00.000Z", + "values": [ + { + "name": "PM25", + "value": 12.51 + }, + { + "name": "PM10", + "value": 21.46 + } + ] + }, + { + "fromDateTime": "2022-09-07T22:00:00.000Z", + "indexes": [ + { + "advice": "Breathe deep! The air is clean!", + "color": "#6BC926", + "description": "Great air here today!", + "level": "VERY_LOW", + "name": "AIRLY_CAQI", + "value": 20.47 + } + ], + "standards": [ + { + "averaging": "24h", + "limit": 15, + "name": "WHO", + "percent": 81.86, + "pollutant": "PM25" + }, + { + "averaging": "24h", + "limit": 45, + "name": "WHO", + "percent": 48.47, + "pollutant": "PM10" + } + ], + "tillDateTime": "2022-09-07T23:00:00.000Z", + "values": [ + { + "name": "PM25", + "value": 12.28 + }, + { + "name": "PM10", + "value": 19.92 + } + ] + } + ], + "history": [ + { + "fromDateTime": "2022-09-05T23:00:00.000Z", + "indexes": [ + { + "advice": "Great air!", + "color": "#6BC926", + "description": "Great air here today!", + "level": "VERY_LOW", + "name": "AIRLY_CAQI", + "value": 6.47 + } + ], + "standards": [ + { + "averaging": "24h", + "limit": 15, + "name": "WHO", + "percent": 25.85, + "pollutant": "PM25" + }, + { + "averaging": "24h", + "limit": 45, + "name": "WHO", + "percent": 13.87, + "pollutant": "PM10" + }, + { + "averaging": "24h", + "limit": 25, + "name": "WHO", + "percent": 55.29, + "pollutant": "NO2" + }, + { + "averaging": "8h", + "limit": 100, + "name": "WHO", + "percent": 44.3, + "pollutant": "O3" + } + ], + "tillDateTime": "2022-09-06T00:00:00.000Z", + "values": [ + { + "name": "PM1", + "value": 2.44 + }, + { + "name": "PM25", + "value": 3.88 + }, + { + "name": "PM10", + "value": 5.19 + }, + { + "name": "PRESSURE", + "value": 1023.88 + }, + { + "name": "HUMIDITY", + "value": 65.25 + }, + { + "name": "TEMPERATURE", + "value": 13.88 + }, + { + "name": "NO2", + "value": 13.82 + }, + { + "name": "O3", + "value": 44.3 + }, + { + "name": "SO2", + "value": 11.73 + }, + { + "name": "CO", + "value": 142.92 + } + ] + }, + { + "fromDateTime": "2022-09-06T00:00:00.000Z", + "indexes": [ + { + "advice": "Green equals clean!", + "color": "#6BC926", + "description": "Great air here today!", + "level": "VERY_LOW", + "name": "AIRLY_CAQI", + "value": 6.84 + } + ], + "standards": [ + { + "averaging": "24h", + "limit": 15, + "name": "WHO", + "percent": 27.35, + "pollutant": "PM25" + }, + { + "averaging": "24h", + "limit": 45, + "name": "WHO", + "percent": 12.86, + "pollutant": "PM10" + }, + { + "averaging": "24h", + "limit": 25, + "name": "WHO", + "percent": 65.53, + "pollutant": "NO2" + }, + { + "averaging": "8h", + "limit": 100, + "name": "WHO", + "percent": 42.53, + "pollutant": "O3" + } + ], + "tillDateTime": "2022-09-06T01:00:00.000Z", + "values": [ + { + "name": "PM1", + "value": 2.5 + }, + { + "name": "PM25", + "value": 4.1 + }, + { + "name": "PM10", + "value": 5.6 + }, + { + "name": "PRESSURE", + "value": 1023.68 + }, + { + "name": "HUMIDITY", + "value": 68.5 + }, + { + "name": "TEMPERATURE", + "value": 13.45 + }, + { + "name": "NO2", + "value": 16.38 + }, + { + "name": "O3", + "value": 42.53 + }, + { + "name": "SO2", + "value": 12.51 + }, + { + "name": "CO", + "value": 134.93 + } + ] + }, + { + "fromDateTime": "2022-09-06T01:00:00.000Z", + "indexes": [ + { + "advice": "It couldn't be better ;)", + "color": "#6BC926", + "description": "Great air here today!", + "level": "VERY_LOW", + "name": "AIRLY_CAQI", + "value": 7 + } + ], + "standards": [ + { + "averaging": "24h", + "limit": 15, + "name": "WHO", + "percent": 27.98, + "pollutant": "PM25" + }, + { + "averaging": "24h", + "limit": 45, + "name": "WHO", + "percent": 16.41, + "pollutant": "PM10" + }, + { + "averaging": "24h", + "limit": 25, + "name": "WHO", + "percent": 78.5, + "pollutant": "NO2" + }, + { + "averaging": "8h", + "limit": 100, + "name": "WHO", + "percent": 36.51, + "pollutant": "O3" + } + ], + "tillDateTime": "2022-09-06T02:00:00.000Z", + "values": [ + { + "name": "PM1", + "value": 2.81 + }, + { + "name": "PM25", + "value": 4.2 + }, + { + "name": "PM10", + "value": 5.8 + }, + { + "name": "PRESSURE", + "value": 1023.56 + }, + { + "name": "HUMIDITY", + "value": 71.13 + }, + { + "name": "TEMPERATURE", + "value": 12.81 + }, + { + "name": "NO2", + "value": 19.62 + }, + { + "name": "O3", + "value": 36.51 + }, + { + "name": "SO2", + "value": 14.27 + }, + { + "name": "CO", + "value": 151.9 + } + ] + }, + { + "fromDateTime": "2022-09-06T02:00:00.000Z", + "indexes": [ + { + "advice": "Enjoy life!", + "color": "#6BC926", + "description": "Great air here today!", + "level": "VERY_LOW", + "name": "AIRLY_CAQI", + "value": 7.12 + } + ], + "standards": [ + { + "averaging": "24h", + "limit": 15, + "name": "WHO", + "percent": 28.46, + "pollutant": "PM25" + }, + { + "averaging": "24h", + "limit": 45, + "name": "WHO", + "percent": 14.02, + "pollutant": "PM10" + }, + { + "averaging": "24h", + "limit": 25, + "name": "WHO", + "percent": 81.46, + "pollutant": "NO2" + }, + { + "averaging": "8h", + "limit": 100, + "name": "WHO", + "percent": 33.07, + "pollutant": "O3" + } + ], + "tillDateTime": "2022-09-06T03:00:00.000Z", + "values": [ + { + "name": "PM1", + "value": 2.78 + }, + { + "name": "PM25", + "value": 4.27 + }, + { + "name": "PM10", + "value": 5.69 + }, + { + "name": "PRESSURE", + "value": 1023.33 + }, + { + "name": "HUMIDITY", + "value": 73.75 + }, + { + "name": "TEMPERATURE", + "value": 11.99 + }, + { + "name": "NO2", + "value": 20.36 + }, + { + "name": "O3", + "value": 33.07 + }, + { + "name": "SO2", + "value": 14.3 + }, + { + "name": "CO", + "value": 181.88 + } + ] + }, + { + "fromDateTime": "2022-09-06T03:00:00.000Z", + "indexes": [ + { + "advice": "The air is great!", + "color": "#6BC926", + "description": "Great air here today!", + "level": "VERY_LOW", + "name": "AIRLY_CAQI", + "value": 9.02 + } + ], + "standards": [ + { + "averaging": "24h", + "limit": 15, + "name": "WHO", + "percent": 36.06, + "pollutant": "PM25" + }, + { + "averaging": "24h", + "limit": 45, + "name": "WHO", + "percent": 20.69, + "pollutant": "PM10" + }, + { + "averaging": "24h", + "limit": 25, + "name": "WHO", + "percent": 101.69, + "pollutant": "NO2" + }, + { + "averaging": "8h", + "limit": 100, + "name": "WHO", + "percent": 16.79, + "pollutant": "O3" + } + ], + "tillDateTime": "2022-09-06T04:00:00.000Z", + "values": [ + { + "name": "PM1", + "value": 3.51 + }, + { + "name": "PM25", + "value": 5.41 + }, + { + "name": "PM10", + "value": 7.27 + }, + { + "name": "PRESSURE", + "value": 1023.2 + }, + { + "name": "HUMIDITY", + "value": 76.5 + }, + { + "name": "TEMPERATURE", + "value": 11.35 + }, + { + "name": "NO2", + "value": 25.42 + }, + { + "name": "O3", + "value": 16.79 + }, + { + "name": "SO2", + "value": 15.46 + }, + { + "name": "CO", + "value": 192.34 + } + ] + }, + { + "fromDateTime": "2022-09-06T04:00:00.000Z", + "indexes": [ + { + "advice": "Zero dust - zero worries!", + "color": "#6BC926", + "description": "Great air here today!", + "level": "VERY_LOW", + "name": "AIRLY_CAQI", + "value": 9.2 + } + ], + "standards": [ + { + "averaging": "24h", + "limit": 15, + "name": "WHO", + "percent": 36.8, + "pollutant": "PM25" + }, + { + "averaging": "24h", + "limit": 45, + "name": "WHO", + "percent": 28.2, + "pollutant": "PM10" + }, + { + "averaging": "24h", + "limit": 25, + "name": "WHO", + "percent": 103.26, + "pollutant": "NO2" + }, + { + "averaging": "8h", + "limit": 100, + "name": "WHO", + "percent": 13.41, + "pollutant": "O3" + } + ], + "tillDateTime": "2022-09-06T05:00:00.000Z", + "values": [ + { + "name": "PM1", + "value": 3.62 + }, + { + "name": "PM25", + "value": 5.52 + }, + { + "name": "PM10", + "value": 7.25 + }, + { + "name": "PRESSURE", + "value": 1023.23 + }, + { + "name": "HUMIDITY", + "value": 80.5 + }, + { + "name": "TEMPERATURE", + "value": 11.1 + }, + { + "name": "NO2", + "value": 25.82 + }, + { + "name": "O3", + "value": 13.41 + }, + { + "name": "SO2", + "value": 15.91 + }, + { + "name": "CO", + "value": 207.15 + } + ] + }, + { + "fromDateTime": "2022-09-06T05:00:00.000Z", + "indexes": [ + { + "advice": "Green, green, green!", + "color": "#6BC926", + "description": "Great air here today!", + "level": "VERY_LOW", + "name": "AIRLY_CAQI", + "value": 9.38 + } + ], + "standards": [ + { + "averaging": "24h", + "limit": 15, + "name": "WHO", + "percent": 37.51, + "pollutant": "PM25" + }, + { + "averaging": "24h", + "limit": 45, + "name": "WHO", + "percent": 16.91, + "pollutant": "PM10" + }, + { + "averaging": "24h", + "limit": 25, + "name": "WHO", + "percent": 83.97, + "pollutant": "NO2" + }, + { + "averaging": "8h", + "limit": 100, + "name": "WHO", + "percent": 21.62, + "pollutant": "O3" + } + ], + "tillDateTime": "2022-09-06T06:00:00.000Z", + "values": [ + { + "name": "PM1", + "value": 3.63 + }, + { + "name": "PM25", + "value": 5.63 + }, + { + "name": "PM10", + "value": 7.63 + }, + { + "name": "PRESSURE", + "value": 1023.34 + }, + { + "name": "HUMIDITY", + "value": 81.63 + }, + { + "name": "TEMPERATURE", + "value": 11.83 + }, + { + "name": "NO2", + "value": 20.99 + }, + { + "name": "O3", + "value": 21.62 + }, + { + "name": "SO2", + "value": 15.27 + }, + { + "name": "CO", + "value": 218.33 + } + ] + }, + { + "fromDateTime": "2022-09-06T06:00:00.000Z", + "indexes": [ + { + "advice": "The air is great!", + "color": "#6BC926", + "description": "Great air here today!", + "level": "VERY_LOW", + "name": "AIRLY_CAQI", + "value": 7.89 + } + ], + "standards": [ + { + "averaging": "24h", + "limit": 15, + "name": "WHO", + "percent": 31.56, + "pollutant": "PM25" + }, + { + "averaging": "24h", + "limit": 45, + "name": "WHO", + "percent": 13, + "pollutant": "PM10" + }, + { + "averaging": "24h", + "limit": 25, + "name": "WHO", + "percent": 71.24, + "pollutant": "NO2" + }, + { + "averaging": "8h", + "limit": 100, + "name": "WHO", + "percent": 27.32, + "pollutant": "O3" + } + ], + "tillDateTime": "2022-09-06T07:00:00.000Z", + "values": [ + { + "name": "PM1", + "value": 3.03 + }, + { + "name": "PM25", + "value": 4.73 + }, + { + "name": "PM10", + "value": 6.5 + }, + { + "name": "PRESSURE", + "value": 1023.27 + }, + { + "name": "HUMIDITY", + "value": 79.69 + }, + { + "name": "TEMPERATURE", + "value": 13.34 + }, + { + "name": "NO2", + "value": 17.81 + }, + { + "name": "O3", + "value": 27.32 + }, + { + "name": "SO2", + "value": 15.01 + }, + { + "name": "CO", + "value": 190.22 + } + ] + }, + { + "fromDateTime": "2022-09-06T07:00:00.000Z", + "indexes": [ + { "advice": "Breathe to fill your lungs!", - "color": "#6BC926" + "color": "#6BC926", + "description": "Great air here today!", + "level": "VERY_LOW", + "name": "AIRLY_CAQI", + "value": 9.38 } ], "standards": [ { + "averaging": "24h", + "limit": 15, "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 45.74 + "percent": 37.49, + "pollutant": "PM25" }, { + "averaging": "24h", + "limit": 45, "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 27.02 + "percent": 14.34, + "pollutant": "PM10" + }, + { + "averaging": "24h", + "limit": 25, + "name": "WHO", + "percent": 67.99, + "pollutant": "NO2" + }, + { + "averaging": "8h", + "limit": 100, + "name": "WHO", + "percent": 28.73, + "pollutant": "O3" + } + ], + "tillDateTime": "2022-09-06T08:00:00.000Z", + "values": [ + { + "name": "PM1", + "value": 3.54 + }, + { + "name": "PM25", + "value": 5.62 + }, + { + "name": "PM10", + "value": 7.53 + }, + { + "name": "PRESSURE", + "value": 1023.56 + }, + { + "name": "HUMIDITY", + "value": 76.25 + }, + { + "name": "TEMPERATURE", + "value": 15.14 + }, + { + "name": "NO2", + "value": 17 + }, + { + "name": "O3", + "value": 28.73 + }, + { + "name": "SO2", + "value": 16.82 + }, + { + "name": "CO", + "value": 211.61 } ] }, { - "fromDateTime": "2019-10-03T05:00:00.000Z", - "tillDateTime": "2019-10-03T06:00:00.000Z", - "values": [ - { - "name": "PM25", - "value": 10.88 - }, - { - "name": "PM10", - "value": 13.38 - } - ], + "fromDateTime": "2022-09-06T08:00:00.000Z", "indexes": [ { - "name": "AIRLY_CAQI", - "value": 18.13, - "level": "VERY_LOW", + "advice": "Catch your breath!", + "color": "#6BC926", "description": "Great air here today!", - "advice": "Breathe as much as you can!", - "color": "#6BC926" + "level": "VERY_LOW", + "name": "AIRLY_CAQI", + "value": 9.69 } ], "standards": [ { + "averaging": "24h", + "limit": 15, "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 43.52 + "percent": 38.76, + "pollutant": "PM25" }, { + "averaging": "24h", + "limit": 45, "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 26.76 + "percent": 16.4, + "pollutant": "PM10" + }, + { + "averaging": "24h", + "limit": 25, + "name": "WHO", + "percent": 78.57, + "pollutant": "NO2" + }, + { + "averaging": "8h", + "limit": 100, + "name": "WHO", + "percent": 34.53, + "pollutant": "O3" + } + ], + "tillDateTime": "2022-09-06T09:00:00.000Z", + "values": [ + { + "name": "PM1", + "value": 3.8 + }, + { + "name": "PM25", + "value": 5.81 + }, + { + "name": "PM10", + "value": 7.72 + }, + { + "name": "PRESSURE", + "value": 1023.48 + }, + { + "name": "HUMIDITY", + "value": 73.56 + }, + { + "name": "TEMPERATURE", + "value": 16.47 + }, + { + "name": "NO2", + "value": 19.64 + }, + { + "name": "O3", + "value": 34.53 + }, + { + "name": "SO2", + "value": 18.21 + }, + { + "name": "CO", + "value": 223.41 + } + ] + }, + { + "fromDateTime": "2022-09-06T09:00:00.000Z", + "indexes": [ + { + "advice": "Perfect air for exercising! Go for it!", + "color": "#6BC926", + "description": "Great air here today!", + "level": "VERY_LOW", + "name": "AIRLY_CAQI", + "value": 7.51 + } + ], + "standards": [ + { + "averaging": "24h", + "limit": 15, + "name": "WHO", + "percent": 30.01, + "pollutant": "PM25" + }, + { + "averaging": "24h", + "limit": 45, + "name": "WHO", + "percent": 12.93, + "pollutant": "PM10" + }, + { + "averaging": "24h", + "limit": 25, + "name": "WHO", + "percent": 60.74, + "pollutant": "NO2" + }, + { + "averaging": "8h", + "limit": 100, + "name": "WHO", + "percent": 40.73, + "pollutant": "O3" + } + ], + "tillDateTime": "2022-09-06T10:00:00.000Z", + "values": [ + { + "name": "PM1", + "value": 2.9 + }, + { + "name": "PM25", + "value": 4.5 + }, + { + "name": "PM10", + "value": 6.04 + }, + { + "name": "PRESSURE", + "value": 1023.09 + }, + { + "name": "HUMIDITY", + "value": 70.38 + }, + { + "name": "TEMPERATURE", + "value": 18.16 + }, + { + "name": "NO2", + "value": 15.18 + }, + { + "name": "O3", + "value": 40.73 + }, + { + "name": "SO2", + "value": 14.7 + }, + { + "name": "CO", + "value": 217.37 + } + ] + }, + { + "fromDateTime": "2022-09-06T10:00:00.000Z", + "indexes": [ + { + "advice": "Catch your breath!", + "color": "#6BC926", + "description": "Great air here today!", + "level": "VERY_LOW", + "name": "AIRLY_CAQI", + "value": 6.19 + } + ], + "standards": [ + { + "averaging": "24h", + "limit": 15, + "name": "WHO", + "percent": 24.74, + "pollutant": "PM25" + }, + { + "averaging": "24h", + "limit": 45, + "name": "WHO", + "percent": 10.29, + "pollutant": "PM10" + }, + { + "averaging": "24h", + "limit": 25, + "name": "WHO", + "percent": 41.62, + "pollutant": "NO2" + }, + { + "averaging": "8h", + "limit": 100, + "name": "WHO", + "percent": 51.72, + "pollutant": "O3" + } + ], + "tillDateTime": "2022-09-06T11:00:00.000Z", + "values": [ + { + "name": "PM1", + "value": 2.41 + }, + { + "name": "PM25", + "value": 3.71 + }, + { + "name": "PM10", + "value": 4.83 + }, + { + "name": "PRESSURE", + "value": 1022.61 + }, + { + "name": "HUMIDITY", + "value": 67.13 + }, + { + "name": "TEMPERATURE", + "value": 20.64 + }, + { + "name": "NO2", + "value": 10.41 + }, + { + "name": "O3", + "value": 51.72 + }, + { + "name": "SO2", + "value": 7.57 + }, + { + "name": "CO", + "value": 190.58 + } + ] + }, + { + "fromDateTime": "2022-09-06T11:00:00.000Z", + "indexes": [ + { + "advice": "The air is grand today. ;)", + "color": "#6BC926", + "description": "Great air here today!", + "level": "VERY_LOW", + "name": "AIRLY_CAQI", + "value": 4.09 + } + ], + "standards": [ + { + "averaging": "24h", + "limit": 15, + "name": "WHO", + "percent": 16.35, + "pollutant": "PM25" + }, + { + "averaging": "24h", + "limit": 45, + "name": "WHO", + "percent": 8.2, + "pollutant": "PM10" + }, + { + "averaging": "24h", + "limit": 25, + "name": "WHO", + "percent": 57.45, + "pollutant": "NO2" + }, + { + "averaging": "8h", + "limit": 100, + "name": "WHO", + "percent": 59.87, + "pollutant": "O3" + } + ], + "tillDateTime": "2022-09-06T12:00:00.000Z", + "values": [ + { + "name": "PM1", + "value": 1.6 + }, + { + "name": "PM25", + "value": 2.45 + }, + { + "name": "PM10", + "value": 3.27 + }, + { + "name": "PRESSURE", + "value": 1022.35 + }, + { + "name": "HUMIDITY", + "value": 64.75 + }, + { + "name": "TEMPERATURE", + "value": 21.39 + }, + { + "name": "NO2", + "value": 14.36 + }, + { + "name": "O3", + "value": 59.87 + }, + { + "name": "SO2", + "value": 8.52 + }, + { + "name": "CO", + "value": 160.83 + } + ] + }, + { + "fromDateTime": "2022-09-06T12:00:00.000Z", + "indexes": [ + { + "advice": "It couldn't be better ;)", + "color": "#6BC926", + "description": "Great air here today!", + "level": "VERY_LOW", + "name": "AIRLY_CAQI", + "value": 5.42 + } + ], + "standards": [ + { + "averaging": "24h", + "limit": 15, + "name": "WHO", + "percent": 21.66, + "pollutant": "PM25" + }, + { + "averaging": "24h", + "limit": 45, + "name": "WHO", + "percent": 11.11, + "pollutant": "PM10" + }, + { + "averaging": "24h", + "limit": 25, + "name": "WHO", + "percent": 63.14, + "pollutant": "NO2" + }, + { + "averaging": "8h", + "limit": 100, + "name": "WHO", + "percent": 57.67, + "pollutant": "O3" + } + ], + "tillDateTime": "2022-09-06T13:00:00.000Z", + "values": [ + { + "name": "PM1", + "value": 2.21 + }, + { + "name": "PM25", + "value": 3.25 + }, + { + "name": "PM10", + "value": 4.37 + }, + { + "name": "PRESSURE", + "value": 1021.72 + }, + { + "name": "HUMIDITY", + "value": 61 + }, + { + "name": "TEMPERATURE", + "value": 21.62 + }, + { + "name": "NO2", + "value": 15.79 + }, + { + "name": "O3", + "value": 57.67 + }, + { + "name": "SO2", + "value": 13.43 + }, + { + "name": "CO", + "value": 171.22 + } + ] + }, + { + "fromDateTime": "2022-09-06T13:00:00.000Z", + "indexes": [ + { + "advice": "Zero dust - zero worries!", + "color": "#6BC926", + "description": "Great air here today!", + "level": "VERY_LOW", + "name": "AIRLY_CAQI", + "value": 6.88 + } + ], + "standards": [ + { + "averaging": "24h", + "limit": 15, + "name": "WHO", + "percent": 27.49, + "pollutant": "PM25" + }, + { + "averaging": "24h", + "limit": 45, + "name": "WHO", + "percent": 14.55, + "pollutant": "PM10" + }, + { + "averaging": "24h", + "limit": 25, + "name": "WHO", + "percent": 79.87, + "pollutant": "NO2" + }, + { + "averaging": "8h", + "limit": 100, + "name": "WHO", + "percent": 51.47, + "pollutant": "O3" + } + ], + "tillDateTime": "2022-09-06T14:00:00.000Z", + "values": [ + { + "name": "PM1", + "value": 2.75 + }, + { + "name": "PM25", + "value": 4.12 + }, + { + "name": "PM10", + "value": 5.42 + }, + { + "name": "PRESSURE", + "value": 1021.36 + }, + { + "name": "HUMIDITY", + "value": 59.75 + }, + { + "name": "TEMPERATURE", + "value": 21.42 + }, + { + "name": "NO2", + "value": 19.97 + }, + { + "name": "O3", + "value": 51.47 + }, + { + "name": "SO2", + "value": 15.42 + }, + { + "name": "CO", + "value": 199.3 + } + ] + }, + { + "fromDateTime": "2022-09-06T14:00:00.000Z", + "indexes": [ + { + "advice": "Zero dust - zero worries!", + "color": "#6BC926", + "description": "Great air here today!", + "level": "VERY_LOW", + "name": "AIRLY_CAQI", + "value": 6.5 + } + ], + "standards": [ + { + "averaging": "24h", + "limit": 15, + "name": "WHO", + "percent": 25.97, + "pollutant": "PM25" + }, + { + "averaging": "24h", + "limit": 45, + "name": "WHO", + "percent": 11.74, + "pollutant": "PM10" + }, + { + "averaging": "24h", + "limit": 25, + "name": "WHO", + "percent": 17.42, + "pollutant": "NO2" + }, + { + "averaging": "8h", + "limit": 100, + "name": "WHO", + "percent": 56.69, + "pollutant": "O3" + } + ], + "tillDateTime": "2022-09-06T15:00:00.000Z", + "values": [ + { + "name": "PM1", + "value": 2.68 + }, + { + "name": "PM25", + "value": 3.89 + }, + { + "name": "PM10", + "value": 5.23 + }, + { + "name": "PRESSURE", + "value": 1020.96 + }, + { + "name": "HUMIDITY", + "value": 61 + }, + { + "name": "TEMPERATURE", + "value": 21.49 + }, + { + "name": "NO2", + "value": 4.36 + }, + { + "name": "O3", + "value": 56.69 + }, + { + "name": "SO2", + "value": 13.25 + }, + { + "name": "CO", + "value": 180.53 + } + ] + }, + { + "fromDateTime": "2022-09-06T15:00:00.000Z", + "indexes": [ + { + "advice": "Breathe deeply!", + "color": "#6BC926", + "description": "Great air here today!", + "level": "VERY_LOW", + "name": "AIRLY_CAQI", + "value": 6.95 + } + ], + "standards": [ + { + "averaging": "24h", + "limit": 15, + "name": "WHO", + "percent": 27.79, + "pollutant": "PM25" + }, + { + "averaging": "24h", + "limit": 45, + "name": "WHO", + "percent": 10.09, + "pollutant": "PM10" + }, + { + "averaging": "24h", + "limit": 25, + "name": "WHO", + "percent": 46.43, + "pollutant": "NO2" + }, + { + "averaging": "8h", + "limit": 100, + "name": "WHO", + "percent": 85.39, + "pollutant": "O3" + } + ], + "tillDateTime": "2022-09-06T16:00:00.000Z", + "values": [ + { + "name": "PM1", + "value": 2.68 + }, + { + "name": "PM25", + "value": 4.17 + }, + { + "name": "PM10", + "value": 5.59 + }, + { + "name": "PRESSURE", + "value": 1020.77 + }, + { + "name": "HUMIDITY", + "value": 62.49 + }, + { + "name": "TEMPERATURE", + "value": 21.04 + }, + { + "name": "NO2", + "value": 11.61 + }, + { + "name": "O3", + "value": 85.39 + }, + { + "name": "SO2", + "value": 12.77 + }, + { + "name": "CO", + "value": 159.08 + } + ] + }, + { + "fromDateTime": "2022-09-06T16:00:00.000Z", + "indexes": [ + { + "advice": "Dear me, how wonderful!", + "color": "#6BC926", + "description": "Great air here today!", + "level": "VERY_LOW", + "name": "AIRLY_CAQI", + "value": 7.11 + } + ], + "standards": [ + { + "averaging": "24h", + "limit": 15, + "name": "WHO", + "percent": 28.43, + "pollutant": "PM25" + }, + { + "averaging": "24h", + "limit": 45, + "name": "WHO", + "percent": 11.77, + "pollutant": "PM10" + }, + { + "averaging": "24h", + "limit": 25, + "name": "WHO", + "percent": 126.91, + "pollutant": "NO2" + }, + { + "averaging": "8h", + "limit": 100, + "name": "WHO", + "percent": 80.2, + "pollutant": "O3" + } + ], + "tillDateTime": "2022-09-06T17:00:00.000Z", + "values": [ + { + "name": "PM1", + "value": 2.88 + }, + { + "name": "PM25", + "value": 4.26 + }, + { + "name": "PM10", + "value": 5.66 + }, + { + "name": "PRESSURE", + "value": 1020.5 + }, + { + "name": "HUMIDITY", + "value": 66.22 + }, + { + "name": "TEMPERATURE", + "value": 20.08 + }, + { + "name": "NO2", + "value": 31.73 + }, + { + "name": "O3", + "value": 80.2 + }, + { + "name": "SO2", + "value": 15.05 + }, + { + "name": "CO", + "value": 186.52 + } + ] + }, + { + "fromDateTime": "2022-09-06T17:00:00.000Z", + "indexes": [ + { + "advice": "Zero dust - zero worries!", + "color": "#6BC926", + "description": "Great air here today!", + "level": "VERY_LOW", + "name": "AIRLY_CAQI", + "value": 9.81 + } + ], + "standards": [ + { + "averaging": "24h", + "limit": 15, + "name": "WHO", + "percent": 39.22, + "pollutant": "PM25" + }, + { + "averaging": "24h", + "limit": 45, + "name": "WHO", + "percent": 18.8, + "pollutant": "PM10" + }, + { + "averaging": "24h", + "limit": 25, + "name": "WHO", + "percent": 133.19, + "pollutant": "NO2" + }, + { + "averaging": "8h", + "limit": 100, + "name": "WHO", + "percent": 54.46, + "pollutant": "O3" + } + ], + "tillDateTime": "2022-09-06T18:00:00.000Z", + "values": [ + { + "name": "PM1", + "value": 4.13 + }, + { + "name": "PM25", + "value": 5.88 + }, + { + "name": "PM10", + "value": 7.74 + }, + { + "name": "PRESSURE", + "value": 1020.37 + }, + { + "name": "HUMIDITY", + "value": 68.88 + }, + { + "name": "TEMPERATURE", + "value": 18.69 + }, + { + "name": "NO2", + "value": 33.3 + }, + { + "name": "O3", + "value": 54.46 + }, + { + "name": "SO2", + "value": 17.85 + }, + { + "name": "CO", + "value": 219.51 + } + ] + }, + { + "fromDateTime": "2022-09-06T18:00:00.000Z", + "indexes": [ + { + "advice": "Enjoy life!", + "color": "#6BC926", + "description": "Great air here today!", + "level": "VERY_LOW", + "name": "AIRLY_CAQI", + "value": 12.18 + } + ], + "standards": [ + { + "averaging": "24h", + "limit": 15, + "name": "WHO", + "percent": 48.71, + "pollutant": "PM25" + }, + { + "averaging": "24h", + "limit": 45, + "name": "WHO", + "percent": 25.59, + "pollutant": "PM10" + }, + { + "averaging": "24h", + "limit": 25, + "name": "WHO", + "percent": 97.71, + "pollutant": "NO2" + }, + { + "averaging": "8h", + "limit": 100, + "name": "WHO", + "percent": 49.24, + "pollutant": "O3" + } + ], + "tillDateTime": "2022-09-06T19:00:00.000Z", + "values": [ + { + "name": "PM1", + "value": 4.8 + }, + { + "name": "PM25", + "value": 7.31 + }, + { + "name": "PM10", + "value": 9.8 + }, + { + "name": "PRESSURE", + "value": 1020.54 + }, + { + "name": "HUMIDITY", + "value": 69.8 + }, + { + "name": "TEMPERATURE", + "value": 17.38 + }, + { + "name": "NO2", + "value": 24.43 + }, + { + "name": "O3", + "value": 49.24 + }, + { + "name": "SO2", + "value": 16.35 + }, + { + "name": "CO", + "value": 229.63 + } + ] + }, + { + "fromDateTime": "2022-09-06T19:00:00.000Z", + "indexes": [ + { + "advice": "Green, green, green!", + "color": "#6BC926", + "description": "Great air here today!", + "level": "VERY_LOW", + "name": "AIRLY_CAQI", + "value": 15.31 + } + ], + "standards": [ + { + "averaging": "24h", + "limit": 15, + "name": "WHO", + "percent": 61.21, + "pollutant": "PM25" + }, + { + "averaging": "24h", + "limit": 45, + "name": "WHO", + "percent": 24.56, + "pollutant": "PM10" + }, + { + "averaging": "24h", + "limit": 25, + "name": "WHO", + "percent": 84.31, + "pollutant": "NO2" + }, + { + "averaging": "8h", + "limit": 100, + "name": "WHO", + "percent": 45.35, + "pollutant": "O3" + } + ], + "tillDateTime": "2022-09-06T20:00:00.000Z", + "values": [ + { + "name": "PM1", + "value": 5.9 + }, + { + "name": "PM25", + "value": 9.18 + }, + { + "name": "PM10", + "value": 12.2 + }, + { + "name": "PRESSURE", + "value": 1020.5 + }, + { + "name": "HUMIDITY", + "value": 68.85 + }, + { + "name": "TEMPERATURE", + "value": 16.59 + }, + { + "name": "NO2", + "value": 21.08 + }, + { + "name": "O3", + "value": 45.35 + }, + { + "name": "SO2", + "value": 16 + }, + { + "name": "CO", + "value": 249.34 + } + ] + }, + { + "fromDateTime": "2022-09-06T20:00:00.000Z", + "indexes": [ + { + "advice": "Enjoy life!", + "color": "#6BC926", + "description": "Great air here today!", + "level": "VERY_LOW", + "name": "AIRLY_CAQI", + "value": 11.65 + } + ], + "standards": [ + { + "averaging": "24h", + "limit": 15, + "name": "WHO", + "percent": 46.58, + "pollutant": "PM25" + }, + { + "averaging": "24h", + "limit": 45, + "name": "WHO", + "percent": 22.75, + "pollutant": "PM10" + }, + { + "averaging": "24h", + "limit": 25, + "name": "WHO", + "percent": 75.91, + "pollutant": "NO2" + }, + { + "averaging": "8h", + "limit": 100, + "name": "WHO", + "percent": 42.98, + "pollutant": "O3" + } + ], + "tillDateTime": "2022-09-06T21:00:00.000Z", + "values": [ + { + "name": "PM1", + "value": 4.36 + }, + { + "name": "PM25", + "value": 6.99 + }, + { + "name": "PM10", + "value": 9.35 + }, + { + "name": "PRESSURE", + "value": 1020.2 + }, + { + "name": "HUMIDITY", + "value": 67.31 + }, + { + "name": "TEMPERATURE", + "value": 15.79 + }, + { + "name": "NO2", + "value": 18.98 + }, + { + "name": "O3", + "value": 42.98 + }, + { + "name": "SO2", + "value": 14.47 + }, + { + "name": "CO", + "value": 197.43 + } + ] + }, + { + "fromDateTime": "2022-09-06T21:00:00.000Z", + "indexes": [ + { + "advice": "Breathe deeply!", + "color": "#6BC926", + "description": "Great air here today!", + "level": "VERY_LOW", + "name": "AIRLY_CAQI", + "value": 10.28 + } + ], + "standards": [ + { + "averaging": "24h", + "limit": 15, + "name": "WHO", + "percent": 41.1, + "pollutant": "PM25" + }, + { + "averaging": "24h", + "limit": 45, + "name": "WHO", + "percent": 17.55, + "pollutant": "PM10" + }, + { + "averaging": "24h", + "limit": 25, + "name": "WHO", + "percent": 68.82, + "pollutant": "NO2" + }, + { + "averaging": "8h", + "limit": 100, + "name": "WHO", + "percent": 41.88, + "pollutant": "O3" + } + ], + "tillDateTime": "2022-09-06T22:00:00.000Z", + "values": [ + { + "name": "PM1", + "value": 3.91 + }, + { + "name": "PM25", + "value": 6.17 + }, + { + "name": "PM10", + "value": 8.55 + }, + { + "name": "PRESSURE", + "value": 1020.17 + }, + { + "name": "HUMIDITY", + "value": 66.84 + }, + { + "name": "TEMPERATURE", + "value": 15.16 + }, + { + "name": "NO2", + "value": 17.21 + }, + { + "name": "O3", + "value": 41.88 + }, + { + "name": "SO2", + "value": 14.74 + }, + { + "name": "CO", + "value": 172.13 + } + ] + }, + { + "fromDateTime": "2022-09-06T22:00:00.000Z", + "indexes": [ + { + "advice": "Zero dust - zero worries!", + "color": "#6BC926", + "description": "Great air here today!", + "level": "VERY_LOW", + "name": "AIRLY_CAQI", + "value": 7.5 + } + ], + "standards": [ + { + "averaging": "24h", + "limit": 15, + "name": "WHO", + "percent": 29.99, + "pollutant": "PM25" + }, + { + "averaging": "24h", + "limit": 45, + "name": "WHO", + "percent": 13.83, + "pollutant": "PM10" + }, + { + "averaging": "24h", + "limit": 25, + "name": "WHO", + "percent": 63.67, + "pollutant": "NO2" + }, + { + "averaging": "8h", + "limit": 100, + "name": "WHO", + "percent": 42.24, + "pollutant": "O3" + } + ], + "tillDateTime": "2022-09-06T23:00:00.000Z", + "values": [ + { + "name": "PM1", + "value": 2.96 + }, + { + "name": "PM25", + "value": 4.5 + }, + { + "name": "PM10", + "value": 6.26 + }, + { + "name": "PRESSURE", + "value": 1019.87 + }, + { + "name": "HUMIDITY", + "value": 67.94 + }, + { + "name": "TEMPERATURE", + "value": 14.54 + }, + { + "name": "NO2", + "value": 15.92 + }, + { + "name": "O3", + "value": 42.24 + }, + { + "name": "SO2", + "value": 13.63 + }, + { + "name": "CO", + "value": 168.23 } ] } diff --git a/tests/components/airly/test_init.py b/tests/components/airly/test_init.py index c8ad6782ce8..7e1d0797b20 100644 --- a/tests/components/airly/test_init.py +++ b/tests/components/airly/test_init.py @@ -29,7 +29,7 @@ async def test_async_setup_entry(hass, aioclient_mock): state = hass.states.get("sensor.home_pm2_5") assert state is not None assert state.state != STATE_UNAVAILABLE - assert state.state == "14" + assert state.state == "4" async def test_config_not_ready(hass, aioclient_mock): diff --git a/tests/components/airly/test_sensor.py b/tests/components/airly/test_sensor.py index 3e8206aa76e..d7717e3886a 100644 --- a/tests/components/airly/test_sensor.py +++ b/tests/components/airly/test_sensor.py @@ -35,7 +35,7 @@ async def test_sensor(hass, aioclient_mock): state = hass.states.get("sensor.home_caqi") assert state - assert state.state == "23" + assert state.state == "7" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "CAQI" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.AQI @@ -46,7 +46,7 @@ async def test_sensor(hass, aioclient_mock): state = hass.states.get("sensor.home_humidity") assert state - assert state.state == "92.8" + assert state.state == "68.3" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.HUMIDITY @@ -58,7 +58,7 @@ async def test_sensor(hass, aioclient_mock): state = hass.states.get("sensor.home_pm1") assert state - assert state.state == "9" + assert state.state == "3" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) @@ -73,7 +73,7 @@ async def test_sensor(hass, aioclient_mock): state = hass.states.get("sensor.home_pm2_5") assert state - assert state.state == "14" + assert state.state == "4" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) @@ -88,7 +88,7 @@ async def test_sensor(hass, aioclient_mock): state = hass.states.get("sensor.home_pm10") assert state - assert state.state == "19" + assert state.state == "6" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) @@ -101,9 +101,69 @@ async def test_sensor(hass, aioclient_mock): assert entry assert entry.unique_id == "123-456-pm10" + state = hass.states.get("sensor.home_co") + assert state + assert state.state == "162" + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + ) + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.CO + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + + entry = registry.async_get("sensor.home_co") + assert entry + assert entry.unique_id == "123-456-co" + + state = hass.states.get("sensor.home_no2") + assert state + assert state.state == "16" + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + ) + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.NITROGEN_DIOXIDE + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + + entry = registry.async_get("sensor.home_no2") + assert entry + assert entry.unique_id == "123-456-no2" + + state = hass.states.get("sensor.home_o3") + assert state + assert state.state == "42" + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + ) + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.OZONE + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + + entry = registry.async_get("sensor.home_o3") + assert entry + assert entry.unique_id == "123-456-o3" + + state = hass.states.get("sensor.home_so2") + assert state + assert state.state == "14" + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + ) + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.SULPHUR_DIOXIDE + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + + entry = registry.async_get("sensor.home_so2") + assert entry + assert entry.unique_id == "123-456-so2" + state = hass.states.get("sensor.home_pressure") assert state - assert state.state == "1001" + assert state.state == "1020" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PRESSURE_HPA assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PRESSURE @@ -115,7 +175,7 @@ async def test_sensor(hass, aioclient_mock): state = hass.states.get("sensor.home_temperature") assert state - assert state.state == "14.2" + assert state.state == "14.4" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE @@ -133,7 +193,7 @@ async def test_availability(hass, aioclient_mock): state = hass.states.get("sensor.home_humidity") assert state assert state.state != STATE_UNAVAILABLE - assert state.state == "92.8" + assert state.state == "68.3" aioclient_mock.clear_requests() aioclient_mock.get(API_POINT_URL, exc=ConnectionError()) @@ -154,7 +214,7 @@ async def test_availability(hass, aioclient_mock): state = hass.states.get("sensor.home_humidity") assert state assert state.state != STATE_UNAVAILABLE - assert state.state == "92.8" + assert state.state == "68.3" async def test_manual_update_entity(hass, aioclient_mock): From b4356a432e50f0c91534087ff35a8e389058fc8c Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 16 Sep 2022 16:41:17 -0600 Subject: [PATCH 474/955] Replace SimpliSafe `clear_notifications` service with a button (#75283) * Replace SimpliSafe `clear_notifications` service with a button * Better log message * Coverage * Docstring * Add repairs item * Better repairs strings * Mark issue as fixable * Add issue registry through helper * Update deprecation version --- .coveragerc | 1 + .../components/simplisafe/__init__.py | 50 +++++++++- homeassistant/components/simplisafe/button.py | 93 +++++++++++++++++++ .../components/simplisafe/services.yaml | 12 --- .../components/simplisafe/strings.json | 6 ++ .../simplisafe/translations/en.json | 27 ++---- 6 files changed, 155 insertions(+), 34 deletions(-) create mode 100644 homeassistant/components/simplisafe/button.py diff --git a/.coveragerc b/.coveragerc index e2eda11cb1e..811528b2911 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1110,6 +1110,7 @@ omit = homeassistant/components/simplisafe/__init__.py homeassistant/components/simplisafe/alarm_control_panel.py homeassistant/components/simplisafe/binary_sensor.py + homeassistant/components/simplisafe/button.py homeassistant/components/simplisafe/lock.py homeassistant/components/simplisafe/sensor.py homeassistant/components/simulated/sensor.py diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index c28a3740694..53aa9e84054 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -71,6 +71,7 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_send, ) from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.service import ( async_register_admin_service, verify_domain_control, @@ -126,6 +127,7 @@ EVENT_SIMPLISAFE_NOTIFICATION = "SIMPLISAFE_NOTIFICATION" PLATFORMS = [ Platform.ALARM_CONTROL_PANEL, Platform.BINARY_SENSOR, + Platform.BUTTON, Platform.LOCK, Platform.SENSOR, ] @@ -257,6 +259,45 @@ def _async_get_system_for_service_call( raise ValueError(f"No system for device ID: {device_id}") +@callback +def _async_log_deprecated_service_call( + hass: HomeAssistant, + call: ServiceCall, + alternate_service: str, + alternate_target: str, + breaks_in_ha_version: str, +) -> None: + """Log a warning about a deprecated service call.""" + deprecated_service = f"{call.domain}.{call.service}" + + async_create_issue( + hass, + DOMAIN, + f"deprecated_service_{deprecated_service}", + breaks_in_ha_version=breaks_in_ha_version, + is_fixable=True, + is_persistent=True, + severity=IssueSeverity.WARNING, + translation_key="deprecated_service", + translation_placeholders={ + "alternate_service": alternate_service, + "alternate_target": alternate_target, + "deprecated_service": deprecated_service, + }, + ) + + LOGGER.warning( + ( + 'The "%s" service is deprecated and will be removed in %s; use the "%s" ' + 'service and pass it a target entity ID of "%s"' + ), + deprecated_service, + breaks_in_ha_version, + alternate_service, + alternate_target, + ) + + @callback def _async_register_base_station( hass: HomeAssistant, entry: ConfigEntry, system: SystemType @@ -347,6 +388,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @extract_system async def async_clear_notifications(call: ServiceCall, system: SystemType) -> None: """Clear all active notifications.""" + _async_log_deprecated_service_call( + hass, + call, + "button.press", + "button.alarm_control_panel_clear_notifications", + "2022.12.0", + ) await system.async_clear_notifications() @_verify_domain_control @@ -839,9 +887,7 @@ class SimpliSafeEntity(CoordinatorEntity): @callback def async_update_from_rest_api(self) -> None: """Update the entity when new data comes from the REST API.""" - raise NotImplementedError() @callback def async_update_from_websocket_event(self, event: WebsocketEvent) -> None: """Update the entity when new data comes from the websocket.""" - raise NotImplementedError() diff --git a/homeassistant/components/simplisafe/button.py b/homeassistant/components/simplisafe/button.py new file mode 100644 index 00000000000..a10d958a94a --- /dev/null +++ b/homeassistant/components/simplisafe/button.py @@ -0,0 +1,93 @@ +"""Buttons for the SimpliSafe integration.""" +from __future__ import annotations + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass + +from simplipy.errors import SimplipyError +from simplipy.system import System + +from homeassistant.components.button import ButtonEntity, ButtonEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import SimpliSafe, SimpliSafeEntity +from .const import DOMAIN +from .typing import SystemType + + +@dataclass +class SimpliSafeButtonDescriptionMixin: + """Define an entity description mixin for SimpliSafe buttons.""" + + push_action: Callable[[System], Awaitable] + + +@dataclass +class SimpliSafeButtonDescription( + ButtonEntityDescription, SimpliSafeButtonDescriptionMixin +): + """Describe a SimpliSafe button entity.""" + + +BUTTON_KIND_CLEAR_NOTIFICATIONS = "clear_notifications" + + +async def _async_clear_notifications(system: System) -> None: + """Reboot the SimpliSafe.""" + await system.async_clear_notifications() + + +BUTTON_DESCRIPTIONS = ( + SimpliSafeButtonDescription( + key=BUTTON_KIND_CLEAR_NOTIFICATIONS, + name="Clear notifications", + push_action=_async_clear_notifications, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up SimpliSafe buttons based on a config entry.""" + simplisafe = hass.data[DOMAIN][entry.entry_id] + + async_add_entities( + [ + SimpliSafeButton(simplisafe, system, description) + for system in simplisafe.systems.values() + for description in BUTTON_DESCRIPTIONS + ] + ) + + +class SimpliSafeButton(SimpliSafeEntity, ButtonEntity): + """Define a SimpliSafe button.""" + + _attr_entity_category = EntityCategory.CONFIG + + entity_description: SimpliSafeButtonDescription + + def __init__( + self, + simplisafe: SimpliSafe, + system: SystemType, + description: SimpliSafeButtonDescription, + ) -> None: + """Initialize the SimpliSafe alarm.""" + super().__init__(simplisafe, system) + + self.entity_description = description + + async def async_press(self) -> None: + """Send out a restart command.""" + try: + await self.entity_description.push_action(self._system) + except SimplipyError as err: + raise HomeAssistantError( + f'Error while pressing button "{self.entity_id}": {err}' + ) from err diff --git a/homeassistant/components/simplisafe/services.yaml b/homeassistant/components/simplisafe/services.yaml index 9875d5baa37..6f9cedc77cb 100644 --- a/homeassistant/components/simplisafe/services.yaml +++ b/homeassistant/components/simplisafe/services.yaml @@ -1,16 +1,4 @@ # Describes the format for available SimpliSafe services -clear_notifications: - name: Clear notifications - description: Clear any active SimpliSafe notifications - fields: - device_id: - name: System - description: The system to remove the PIN from - required: true - selector: - device: - integration: simplisafe - model: alarm_control_panel remove_pin: name: Remove PIN description: Remove a PIN by its label or value. diff --git a/homeassistant/components/simplisafe/strings.json b/homeassistant/components/simplisafe/strings.json index 618c21566f7..bc03ab4c514 100644 --- a/homeassistant/components/simplisafe/strings.json +++ b/homeassistant/components/simplisafe/strings.json @@ -29,5 +29,11 @@ } } } + }, + "issues": { + "deprecated_service": { + "title": "The {deprecated_service} service is being removed", + "description": "Update any automations or scripts that use this service to instead use the `{alternate_service}` service with a target entity ID of `{alternate_target}`. Then, click SUBMIT below to mark this issue as resolved." + } } } diff --git a/homeassistant/components/simplisafe/translations/en.json b/homeassistant/components/simplisafe/translations/en.json index baa167ca951..724e3b5af28 100644 --- a/homeassistant/components/simplisafe/translations/en.json +++ b/homeassistant/components/simplisafe/translations/en.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "This SimpliSafe account is already in use.", - "email_2fa_timed_out": "Timed out while waiting for email-based two-factor authentication.", "reauth_successful": "Re-authentication was successful", "wrong_account": "The user credentials provided do not match this SimpliSafe account." }, @@ -12,33 +11,21 @@ "invalid_auth_code_length": "SimpliSafe authorization codes are 45 characters in length", "unknown": "Unexpected error" }, - "progress": { - "email_2fa": "Check your email for a verification link from Simplisafe." - }, "step": { - "reauth_confirm": { - "data": { - "password": "Password" - }, - "description": "Please re-enter the password for {username}.", - "title": "Reauthenticate Integration" - }, - "sms_2fa": { - "data": { - "code": "Code" - }, - "description": "Input the two-factor authentication code sent to you via SMS." - }, "user": { "data": { - "auth_code": "Authorization Code", - "password": "Password", - "username": "Username" + "auth_code": "Authorization Code" }, "description": "SimpliSafe authenticates users via its web app. Due to technical limitations, there is a manual step at the end of this process; please ensure that you read the [documentation](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) before starting.\n\nWhen you are ready, click [here]({url}) to open the SimpliSafe web app and input your credentials. If you've already logged into SimpliSafe in your browser, you may want to open a new tab, then copy/paste the above URL into that tab.\n\nWhen the process is complete, return here and input the authorization code from the `com.simplisafe.mobile` URL." } } }, + "issues": { + "deprecated_service": { + "description": "Update any automations or scripts that use this service to instead use the `{alternate_service}` service with a target entity ID of `{alternate_target}`. Then, click SUBMIT below to mark this issue as resolved.", + "title": "The {deprecated_service} service is being removed" + } + }, "options": { "step": { "init": { From 38b087d2209dbe26f90676ecebf90fc1b97836e3 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 17 Sep 2022 00:27:28 +0000 Subject: [PATCH 475/955] [ci skip] Translation update --- .../google_sheets/translations/el.json | 31 ++++++++++++ .../google_sheets/translations/et.json | 16 +++++++ .../components/lametric/translations/ca.json | 3 +- .../components/lametric/translations/de.json | 3 +- .../components/lametric/translations/el.json | 3 +- .../components/lametric/translations/fr.json | 3 +- .../components/lametric/translations/id.json | 3 +- .../components/lametric/translations/it.json | 3 +- .../lametric/translations/zh-Hant.json | 3 +- .../components/lcn/translations/id.json | 3 +- .../simplisafe/translations/en.json | 21 +++++++- .../simplisafe/translations/fr.json | 5 ++ .../components/switchbee/translations/en.json | 48 +++++++++++-------- 13 files changed, 117 insertions(+), 28 deletions(-) create mode 100644 homeassistant/components/google_sheets/translations/el.json create mode 100644 homeassistant/components/google_sheets/translations/et.json diff --git a/homeassistant/components/google_sheets/translations/el.json b/homeassistant/components/google_sheets/translations/el.json new file mode 100644 index 00000000000..e7527dcb7d3 --- /dev/null +++ b/homeassistant/components/google_sheets/translations/el.json @@ -0,0 +1,31 @@ +{ + "application_credentials": { + "description": "\u0391\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 [\u03bf\u03b4\u03b7\u03b3\u03af\u03b5\u03c2]({more_info_url}) \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd [\u03bf\u03b8\u03cc\u03bd\u03b7 \u03c3\u03c5\u03bd\u03b1\u03af\u03bd\u03b5\u03c3\u03b7\u03c2 OAuth]({oauth_consent_url}) \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03ce\u03c3\u03b5\u03c4\u03b5 \u03c3\u03c4\u03bf\u03bd \u0392\u03bf\u03b7\u03b8\u03cc Home \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7 \u03c3\u03c4\u03b1 \u03a6\u03cd\u03bb\u03bb\u03b1 Google \u03c3\u03b1\u03c2. \u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03b5\u03c0\u03af\u03c3\u03b7\u03c2 \u03bd\u03b1 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03b5\u03c4\u03b5 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b4\u03b5\u03bc\u03ad\u03bd\u03b1 \u03bc\u03b5 \u03c4\u03bf\u03bd \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc \u03c3\u03b1\u03c2:\n 1. \u039c\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b1 [\u0394\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1] ({oauth_creds_url}) \u03ba\u03b1\u03b9 \u03ba\u03ac\u03bd\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03bf **\u0394\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03b7\u03c1\u03af\u03c9\u03bd**.\n 1. \u0391\u03c0\u03cc \u03c4\u03b7\u03bd \u03b1\u03bd\u03b1\u03c0\u03c4\u03c5\u03c3\u03c3\u03cc\u03bc\u03b5\u03bd\u03b7 \u03bb\u03af\u03c3\u03c4\u03b1 \u03b5\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 **OAuth \u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c0\u03b5\u03bb\u03ac\u03c4\u03b7**.\n 1. \u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 **\u0395\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae \u0399\u03c3\u03c4\u03bf\u03cd** \u03b3\u03b9\u03b1 \u03c4\u03bf\u03bd \u03a4\u03cd\u03c0\u03bf \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2. \n\n" + }, + "config": { + "abort": { + "already_configured": "\u039f \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "already_in_progress": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "create_spreadsheet_failure": "\u03a3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03b9\u03ba\u03bf\u03cd \u03c6\u03cd\u03bb\u03bb\u03bf\u03c5, \u03b4\u03b5\u03af\u03c4\u03b5 \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf \u03ba\u03b1\u03c4\u03b1\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2 \u03c3\u03c6\u03b1\u03bb\u03bc\u03ac\u03c4\u03c9\u03bd \u03b3\u03b9\u03b1 \u03bb\u03b5\u03c0\u03c4\u03bf\u03bc\u03ad\u03c1\u03b5\u03b9\u03b5\u03c2", + "invalid_access_token": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "missing_configuration": "\u03a4\u03bf \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03bf \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af. \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7.", + "oauth_error": "\u039b\u03ae\u03c6\u03b8\u03b7\u03ba\u03b1\u03bd \u03bc\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b1 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03b1 \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03bf\u03cd.", + "open_spreadsheet_failure": "\u03a3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03ba\u03b1\u03c4\u03ac \u03c4\u03bf \u03ac\u03bd\u03bf\u03b9\u03b3\u03bc\u03b1 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03b9\u03ba\u03bf\u03cd \u03c6\u03cd\u03bb\u03bb\u03bf\u03c5, \u03b4\u03b5\u03af\u03c4\u03b5 \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf \u03ba\u03b1\u03c4\u03b1\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2 \u03c3\u03c6\u03b1\u03bb\u03bc\u03ac\u03c4\u03c9\u03bd \u03b3\u03b9\u03b1 \u03bb\u03b5\u03c0\u03c4\u03bf\u03bc\u03ad\u03c1\u03b5\u03b9\u03b5\u03c2", + "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2", + "timeout_connect": "\u03a7\u03c1\u03bf\u03bd\u03b9\u03ba\u03cc \u03cc\u03c1\u03b9\u03bf \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "create_entry": { + "default": "\u0395\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ba\u03b1\u03b9 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03c6\u03cd\u03bb\u03bb\u03bf\u03c5 \u03b5\u03c1\u03b3\u03b1\u03c3\u03af\u03b1\u03c2 \u03c3\u03c4\u03bf: {url}" + }, + "step": { + "auth": { + "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd Google" + }, + "pick_implementation": { + "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03bc\u03b5\u03b8\u03cc\u03b4\u03bf\u03c5 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_sheets/translations/et.json b/homeassistant/components/google_sheets/translations/et.json new file mode 100644 index 00000000000..6373e7d5b63 --- /dev/null +++ b/homeassistant/components/google_sheets/translations/et.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "create_spreadsheet_failure": "Viga arvutustabeli loomisel, vt vigade logi \u00fcksikasjadeks", + "open_spreadsheet_failure": "Viga arvutustabelite avamisel, vt vigade logi \u00fcksikasjade kohta" + }, + "create_entry": { + "default": "Autentimine \u00f5nnestus ja arvutustabel loodud aadressil: {url}" + }, + "step": { + "auth": { + "title": "Google'i konto linkimine" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lametric/translations/ca.json b/homeassistant/components/lametric/translations/ca.json index 14066cdeab5..0c7a30099e5 100644 --- a/homeassistant/components/lametric/translations/ca.json +++ b/homeassistant/components/lametric/translations/ca.json @@ -7,7 +7,8 @@ "link_local_address": "L'enlla\u00e7 amb adreces locals no est\u00e0 perm\u00e8s", "missing_configuration": "La integraci\u00f3 LaMetric no est\u00e0 configurada. Consulta la documentaci\u00f3.", "no_devices": "L'usuari autoritzat no t\u00e9 cap dispositiu LaMetric", - "no_url_available": "No hi ha cap URL disponible. Per a m\u00e9s informaci\u00f3 sobre aquest error, [consulta la secci\u00f3 d'ajuda]({docs_url})" + "no_url_available": "No hi ha cap URL disponible. Per a m\u00e9s informaci\u00f3 sobre aquest error, [consulta la secci\u00f3 d'ajuda]({docs_url})", + "unknown": "Error inesperat" }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", diff --git a/homeassistant/components/lametric/translations/de.json b/homeassistant/components/lametric/translations/de.json index dab436c4b2d..8f347963182 100644 --- a/homeassistant/components/lametric/translations/de.json +++ b/homeassistant/components/lametric/translations/de.json @@ -7,7 +7,8 @@ "link_local_address": "Lokale Linkadressen werden nicht unterst\u00fctzt", "missing_configuration": "Die LaMetric-Integration ist nicht konfiguriert. Bitte folge der Dokumentation.", "no_devices": "Der autorisierte Benutzer hat keine LaMetric Ger\u00e4te", - "no_url_available": "Keine URL verf\u00fcgbar. Informationen zu diesem Fehler findest du [im Hilfebereich]({docs_url})." + "no_url_available": "Keine URL verf\u00fcgbar. Informationen zu diesem Fehler findest du [im Hilfebereich]({docs_url}).", + "unknown": "Unerwarteter Fehler" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", diff --git a/homeassistant/components/lametric/translations/el.json b/homeassistant/components/lametric/translations/el.json index 964bfc4b212..8733a19e690 100644 --- a/homeassistant/components/lametric/translations/el.json +++ b/homeassistant/components/lametric/translations/el.json @@ -7,7 +7,8 @@ "link_local_address": "\u039f\u03b9 \u03c4\u03bf\u03c0\u03b9\u03ba\u03ad\u03c2 \u03b4\u03b9\u03b5\u03c5\u03b8\u03cd\u03bd\u03c3\u03b5\u03b9\u03c2 \u03c3\u03c5\u03bd\u03b4\u03ad\u03c3\u03bc\u03bf\u03c5 \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03bf\u03bd\u03c4\u03b1\u03b9", "missing_configuration": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 LaMetric \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af. \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7.", "no_devices": "\u039f \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03bf\u03c4\u03b7\u03bc\u03ad\u03bd\u03bf\u03c2 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7\u03c2 \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 LaMetric", - "no_url_available": "\u0394\u03b5\u03bd \u03c5\u03c0\u03ac\u03c1\u03c7\u03b5\u03b9 \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL. \u0393\u03b9\u03b1 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03c3\u03c7\u03b5\u03c4\u03b9\u03ba\u03ac \u03bc\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1, [\u03b5\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03bd\u03cc\u03c4\u03b7\u03c4\u03b1 \u03b2\u03bf\u03ae\u03b8\u03b5\u03b9\u03b1\u03c2] ( {docs_url} )" + "no_url_available": "\u0394\u03b5\u03bd \u03c5\u03c0\u03ac\u03c1\u03c7\u03b5\u03b9 \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL. \u0393\u03b9\u03b1 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03c3\u03c7\u03b5\u03c4\u03b9\u03ba\u03ac \u03bc\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1, [\u03b5\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03bd\u03cc\u03c4\u03b7\u03c4\u03b1 \u03b2\u03bf\u03ae\u03b8\u03b5\u03b9\u03b1\u03c2] ( {docs_url} )", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "error": { "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", diff --git a/homeassistant/components/lametric/translations/fr.json b/homeassistant/components/lametric/translations/fr.json index 5bd4a5899f6..fd0125f68c9 100644 --- a/homeassistant/components/lametric/translations/fr.json +++ b/homeassistant/components/lametric/translations/fr.json @@ -7,7 +7,8 @@ "link_local_address": "Les adresses de liaison locale ne sont pas prises en charge", "missing_configuration": "L'int\u00e9gration LaMetric n'est pas configur\u00e9e\u00a0; veuillez suivre la documentation.", "no_devices": "L'utilisateur autoris\u00e9 ne poss\u00e8de aucun appareil LaMetric", - "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide]({docs_url})" + "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide]({docs_url})", + "unknown": "Erreur inattendue" }, "error": { "cannot_connect": "\u00c9chec de connexion", diff --git a/homeassistant/components/lametric/translations/id.json b/homeassistant/components/lametric/translations/id.json index 69f776e636c..e668efa0403 100644 --- a/homeassistant/components/lametric/translations/id.json +++ b/homeassistant/components/lametric/translations/id.json @@ -7,7 +7,8 @@ "link_local_address": "Tautan alamat lokal tidak didukung", "missing_configuration": "Integrasi LaMetric tidak dikonfigurasi. Silakan ikuti dokumentasi.", "no_devices": "Pengguna yang diotorisasi tidak memiliki perangkat LaMetric", - "no_url_available": "Tidak ada URL yang tersedia. Untuk informasi tentang kesalahan ini, [lihat bagian bantuan]({docs_url})" + "no_url_available": "Tidak ada URL yang tersedia. Untuk informasi tentang kesalahan ini, [lihat bagian bantuan]({docs_url})", + "unknown": "Kesalahan yang tidak diharapkan" }, "error": { "cannot_connect": "Gagal terhubung", diff --git a/homeassistant/components/lametric/translations/it.json b/homeassistant/components/lametric/translations/it.json index e41d40d7605..c3496022701 100644 --- a/homeassistant/components/lametric/translations/it.json +++ b/homeassistant/components/lametric/translations/it.json @@ -7,7 +7,8 @@ "link_local_address": "Gli indirizzi locali di collegamento non sono supportati", "missing_configuration": "L'integrazione LaMetric non \u00e8 configurata. Segui la documentazione.", "no_devices": "L'utente autorizzato non dispone di dispositivi LaMetric", - "no_url_available": "Nessun URL disponibile. Per informazioni su questo errore, [controlla la sezione della guida]({docs_url})" + "no_url_available": "Nessun URL disponibile. Per informazioni su questo errore, [controlla la sezione della guida]({docs_url})", + "unknown": "Errore imprevisto" }, "error": { "cannot_connect": "Impossibile connettersi", diff --git a/homeassistant/components/lametric/translations/zh-Hant.json b/homeassistant/components/lametric/translations/zh-Hant.json index e9c2835756c..bcaa67ed4ad 100644 --- a/homeassistant/components/lametric/translations/zh-Hant.json +++ b/homeassistant/components/lametric/translations/zh-Hant.json @@ -7,7 +7,8 @@ "link_local_address": "\u4e0d\u652f\u63f4\u9023\u7d50\u672c\u5730\u7aef\u4f4d\u5740", "missing_configuration": "LaMetric \u6574\u5408\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002", "no_devices": "\u8a8d\u8b49\u4f7f\u7528\u8005\u6c92\u6709\u4efb\u4f55 LaMetric \u88dd\u7f6e", - "no_url_available": "\u6c92\u6709\u53ef\u7528\u7684\u7db2\u5740\u3002\u95dc\u65bc\u6b64\u932f\u8aa4\u66f4\u8a73\u7d30\u8a0a\u606f\uff0c[\u9ede\u9078\u5354\u52a9\u7ae0\u7bc0]({docs_url})" + "no_url_available": "\u6c92\u6709\u53ef\u7528\u7684\u7db2\u5740\u3002\u95dc\u65bc\u6b64\u932f\u8aa4\u66f4\u8a73\u7d30\u8a0a\u606f\uff0c[\u9ede\u9078\u5354\u52a9\u7ae0\u7bc0]({docs_url})", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/lcn/translations/id.json b/homeassistant/components/lcn/translations/id.json index e265e2e357b..f728f67fdfd 100644 --- a/homeassistant/components/lcn/translations/id.json +++ b/homeassistant/components/lcn/translations/id.json @@ -1,8 +1,9 @@ { "device_automation": { "trigger_type": { + "codelock": "kode kunci diterima", "fingerprint": "kode sidik jari diterima", - "send_keys": "kode dikirim diterima", + "send_keys": "kunci kirim diterima", "transmitter": "kode pemancar diterima", "transponder": "kode transponder diterima" } diff --git a/homeassistant/components/simplisafe/translations/en.json b/homeassistant/components/simplisafe/translations/en.json index 724e3b5af28..60e641a597c 100644 --- a/homeassistant/components/simplisafe/translations/en.json +++ b/homeassistant/components/simplisafe/translations/en.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "This SimpliSafe account is already in use.", + "email_2fa_timed_out": "Timed out while waiting for email-based two-factor authentication.", "reauth_successful": "Re-authentication was successful", "wrong_account": "The user credentials provided do not match this SimpliSafe account." }, @@ -11,10 +12,28 @@ "invalid_auth_code_length": "SimpliSafe authorization codes are 45 characters in length", "unknown": "Unexpected error" }, + "progress": { + "email_2fa": "Check your email for a verification link from Simplisafe." + }, "step": { + "reauth_confirm": { + "data": { + "password": "Password" + }, + "description": "Please re-enter the password for {username}.", + "title": "Reauthenticate Integration" + }, + "sms_2fa": { + "data": { + "code": "Code" + }, + "description": "Input the two-factor authentication code sent to you via SMS." + }, "user": { "data": { - "auth_code": "Authorization Code" + "auth_code": "Authorization Code", + "password": "Password", + "username": "Username" }, "description": "SimpliSafe authenticates users via its web app. Due to technical limitations, there is a manual step at the end of this process; please ensure that you read the [documentation](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) before starting.\n\nWhen you are ready, click [here]({url}) to open the SimpliSafe web app and input your credentials. If you've already logged into SimpliSafe in your browser, you may want to open a new tab, then copy/paste the above URL into that tab.\n\nWhen the process is complete, return here and input the authorization code from the `com.simplisafe.mobile` URL." } diff --git a/homeassistant/components/simplisafe/translations/fr.json b/homeassistant/components/simplisafe/translations/fr.json index 15ad31ce463..8f7f3a2dfcd 100644 --- a/homeassistant/components/simplisafe/translations/fr.json +++ b/homeassistant/components/simplisafe/translations/fr.json @@ -37,6 +37,11 @@ } } }, + "issues": { + "deprecated_service": { + "title": "Le service {deprecated_service} sera bient\u00f4t supprim\u00e9" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/switchbee/translations/en.json b/homeassistant/components/switchbee/translations/en.json index 8cbe7b4c56a..41f9ee6a043 100644 --- a/homeassistant/components/switchbee/translations/en.json +++ b/homeassistant/components/switchbee/translations/en.json @@ -1,22 +1,32 @@ { - "config": { - "abort": { - "already_configured_device": "Device is already configured" - }, - "error": { - "cannot_connect": "Failed to connect", - "invalid_auth": "Failed to Authenticate with the Central Unit", - "unknown": "Unexpected error" - }, - "step": { - "user": { - "description": "Setup SwitchBee integration with Home Assistant.", - "data": { - "host": "Central Unit IP address", - "username": "User (e-mail)", - "password": "Password" + "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", + "password": "Password", + "switch_as_light": "Initialize switches as light entities", + "username": "Username" + }, + "description": "Setup SwitchBee integration with Home Assistant." } - } - } - } + } + }, + "options": { + "step": { + "init": { + "data": { + "devices": "Devices to include" + } + } + } + } } \ No newline at end of file From f59c8d985d85b9d45773bd095be6ab8aa7d344c8 Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Sat, 17 Sep 2022 06:03:18 +0200 Subject: [PATCH 476/955] Correct unit for here_travel_time distance sensor (#78303) Signed-off-by: Kevin Stillhammer Signed-off-by: Kevin Stillhammer --- .../components/here_travel_time/sensor.py | 36 +++++++++++++++---- .../here_travel_time/test_sensor.py | 15 +++++++- 2 files changed, 44 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/here_travel_time/sensor.py b/homeassistant/components/here_travel_time/sensor.py index e7bc88f2210..6bb4052dd9f 100644 --- a/homeassistant/components/here_travel_time/sensor.py +++ b/homeassistant/components/here_travel_time/sensor.py @@ -23,6 +23,9 @@ from homeassistant.const import ( CONF_MODE, CONF_NAME, CONF_UNIT_SYSTEM, + CONF_UNIT_SYSTEM_IMPERIAL, + LENGTH_KILOMETERS, + LENGTH_MILES, TIME_MINUTES, ) from homeassistant.core import HomeAssistant @@ -139,12 +142,6 @@ def sensor_descriptions(travel_mode: str) -> tuple[SensorEntityDescription, ...] state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=TIME_MINUTES, ), - SensorEntityDescription( - name="Distance", - icon=ICONS.get(travel_mode, ICON_CAR), - key=ATTR_DISTANCE, - state_class=SensorStateClass.MEASUREMENT, - ), SensorEntityDescription( name="Route", icon="mdi:directions", @@ -198,6 +195,7 @@ async def async_setup_entry( ) sensors.append(OriginSensor(entry_id, name, coordinator)) sensors.append(DestinationSensor(entry_id, name, coordinator)) + sensors.append(DistanceSensor(entry_id, name, coordinator)) async_add_entities(sensors) @@ -301,3 +299,29 @@ class DestinationSensor(HERETravelTimeSensor): ATTR_LONGITUDE: self.coordinator.data[ATTR_DESTINATION].split(",")[1], } return None + + +class DistanceSensor(HERETravelTimeSensor): + """Sensor holding information about the distance.""" + + def __init__( + self, + unique_id_prefix: str, + name: str, + coordinator: HereTravelTimeDataUpdateCoordinator, + ) -> None: + """Initialize the sensor.""" + sensor_description = SensorEntityDescription( + name="Distance", + icon=ICONS.get(coordinator.config.travel_mode, ICON_CAR), + key=ATTR_DISTANCE, + state_class=SensorStateClass.MEASUREMENT, + ) + super().__init__(unique_id_prefix, name, sensor_description, coordinator) + + @property + def native_unit_of_measurement(self) -> str | None: + """Return the unit of measurement of the sensor.""" + if self.coordinator.config.units == CONF_UNIT_SYSTEM_IMPERIAL: + return LENGTH_MILES + return LENGTH_KILOMETERS diff --git a/tests/components/here_travel_time/test_sensor.py b/tests/components/here_travel_time/test_sensor.py index 70669a63da5..849c123e72e 100644 --- a/tests/components/here_travel_time/test_sensor.py +++ b/tests/components/here_travel_time/test_sensor.py @@ -37,10 +37,13 @@ from homeassistant.const import ( ATTR_ICON, ATTR_LATITUDE, ATTR_LONGITUDE, + ATTR_UNIT_OF_MEASUREMENT, CONF_API_KEY, CONF_MODE, CONF_NAME, EVENT_HOMEASSISTANT_START, + LENGTH_KILOMETERS, + LENGTH_MILES, TIME_MINUTES, ) from homeassistant.core import HomeAssistant @@ -58,7 +61,7 @@ from tests.common import MockConfigEntry @pytest.mark.parametrize( - "mode,icon,unit_system,arrival_time,departure_time,expected_duration,expected_distance,expected_duration_in_traffic", + "mode,icon,unit_system,arrival_time,departure_time,expected_duration,expected_distance,expected_duration_in_traffic,expected_distance_unit", [ ( TRAVEL_MODE_CAR, @@ -69,6 +72,7 @@ from tests.common import MockConfigEntry "30", 23.903, "31", + LENGTH_KILOMETERS, ), ( TRAVEL_MODE_BICYCLE, @@ -79,6 +83,7 @@ from tests.common import MockConfigEntry "30", 23.903, "30", + LENGTH_KILOMETERS, ), ( TRAVEL_MODE_PEDESTRIAN, @@ -89,6 +94,7 @@ from tests.common import MockConfigEntry "30", 14.85263, "30", + LENGTH_MILES, ), ( TRAVEL_MODE_PUBLIC_TIME_TABLE, @@ -99,6 +105,7 @@ from tests.common import MockConfigEntry "30", 14.85263, "30", + LENGTH_MILES, ), ( TRAVEL_MODE_TRUCK, @@ -109,6 +116,7 @@ from tests.common import MockConfigEntry "30", 23.903, "31", + LENGTH_KILOMETERS, ), ], ) @@ -123,6 +131,7 @@ async def test_sensor( expected_duration, expected_distance, expected_duration_in_traffic, + expected_distance_unit, ): """Test that sensor works.""" entry = MockConfigEntry( @@ -166,6 +175,10 @@ async def test_sensor( assert float(hass.states.get("sensor.test_distance").state) == pytest.approx( expected_distance ) + assert ( + hass.states.get("sensor.test_distance").attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == expected_distance_unit + ) assert hass.states.get("sensor.test_route").state == ( "US-29 - K St NW; US-29 - Whitehurst Fwy; " "I-495 N - Capital Beltway; MD-187 S - Old Georgetown Rd" From 5cde3ca4e17caafa8f120c3b40d4639cab6b9d4c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 17 Sep 2022 01:34:44 -0500 Subject: [PATCH 477/955] Switch emulated_hue to use async_timeout instead of asyncio.wait_for (#78608) --- homeassistant/components/emulated_hue/hue_api.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index 687a7d5fbe6..272645909a5 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -11,6 +11,7 @@ import time from typing import Any from aiohttp import web +import async_timeout from homeassistant import core from homeassistant.components import ( @@ -871,7 +872,8 @@ async def wait_for_state_change_or_timeout( unsub = async_track_state_change_event(hass, [entity_id], _async_event_changed) try: - await asyncio.wait_for(ev.wait(), timeout=STATE_CHANGE_WAIT_TIMEOUT) + async with async_timeout.timeout(STATE_CHANGE_WAIT_TIMEOUT): + await ev.wait() except asyncio.TimeoutError: pass finally: From 82407ca433b592159d459568cfa1a00bd71dfa49 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 17 Sep 2022 01:35:53 -0500 Subject: [PATCH 478/955] Switch yeelight to use async_timeout instead of asyncio.wait_for (#78606) --- homeassistant/components/yeelight/scanner.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/yeelight/scanner.py b/homeassistant/components/yeelight/scanner.py index 0b33512e315..4c0b0f69310 100644 --- a/homeassistant/components/yeelight/scanner.py +++ b/homeassistant/components/yeelight/scanner.py @@ -9,6 +9,7 @@ from ipaddress import IPv4Address import logging from urllib.parse import urlparse +import async_timeout from async_upnp_client.search import SsdpSearchListener from async_upnp_client.utils import CaseInsensitiveDict @@ -154,7 +155,8 @@ class YeelightScanner: listener.async_search((host, SSDP_TARGET[1])) with contextlib.suppress(asyncio.TimeoutError): - await asyncio.wait_for(host_event.wait(), timeout=DISCOVERY_TIMEOUT) + async with async_timeout.timeout(DISCOVERY_TIMEOUT): + await host_event.wait() self._host_discovered_events[host].remove(host_event) return self._host_capabilities.get(host) From 98dd84f53543cdfd2351abf620006cb4ab659b90 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 17 Sep 2022 03:26:02 -0500 Subject: [PATCH 479/955] Ensure bluetooth callbacks are only fired when advertisement data changes (#78609) --- homeassistant/components/bluetooth/manager.py | 13 +++- .../test_active_update_coordinator.py | 19 +++-- tests/components/bluetooth/test_init.py | 75 +++++++++++++++---- .../test_passive_update_processor.py | 43 ++++++++--- 4 files changed, 120 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index 47941c8d5c1..edc9bb1edde 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -323,12 +323,23 @@ class BluetoothManager: return self._history[address] = service_info - source = service_info.source if connectable: self._connectable_history[address] = service_info # Bleak callbacks must get a connectable device + # If the advertisement data is the same as the last time we saw it, we + # don't need to do anything else. + if old_service_info and not ( + service_info.manufacturer_data != old_service_info.manufacturer_data + or service_info.service_data != old_service_info.service_data + or service_info.service_uuids != old_service_info.service_uuids + ): + return + + source = service_info.source + if connectable: + # Bleak callbacks must get a connectable device for callback_filters in self._bleak_callbacks: _dispatch_bleak_callback(*callback_filters, device, advertisement_data) diff --git a/tests/components/bluetooth/test_active_update_coordinator.py b/tests/components/bluetooth/test_active_update_coordinator.py index ecec5f12171..4dbe32d69d2 100644 --- a/tests/components/bluetooth/test_active_update_coordinator.py +++ b/tests/components/bluetooth/test_active_update_coordinator.py @@ -36,6 +36,15 @@ GENERIC_BLUETOOTH_SERVICE_INFO = BluetoothServiceInfo( service_uuids=[], source="local", ) +GENERIC_BLUETOOTH_SERVICE_INFO_2 = BluetoothServiceInfo( + name="Generic", + address="aa:bb:cc:dd:ee:ff", + rssi=-95, + manufacturer_data={1: b"\x01\x01\x01\x01\x01\x01\x01\x01", 2: b"\x02"}, + service_data={}, + service_uuids=[], + source="local", +) async def test_basic_usage(hass: HomeAssistant, mock_bleak_scanner_start): @@ -128,7 +137,7 @@ async def test_poll_can_be_skipped(hass: HomeAssistant, mock_bleak_scanner_start flag = False - inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) + inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO_2) await hass.async_block_till_done() assert async_handle_update.mock_calls[-1] == call({"testdata": None}) @@ -196,7 +205,7 @@ async def test_bleak_error_and_recover( # Second poll works flag = False - inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) + inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO_2) await hass.async_block_till_done() assert async_handle_update.mock_calls[-1] == call({"testdata": False}) @@ -251,7 +260,7 @@ async def test_poll_failure_and_recover(hass: HomeAssistant, mock_bleak_scanner_ # Second poll works flag = False - inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) + inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO_2) await hass.async_block_till_done() assert async_handle_update.mock_calls[-1] == call({"testdata": False}) @@ -297,7 +306,7 @@ async def test_second_poll_needed(hass: HomeAssistant, mock_bleak_scanner_start) # First poll gets queued inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) # Second poll gets stuck behind first poll - inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) + inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO_2) await hass.async_block_till_done() assert async_handle_update.mock_calls[-1] == call({"testdata": 1}) @@ -343,7 +352,7 @@ async def test_rate_limit(hass: HomeAssistant, mock_bleak_scanner_start): # First poll gets queued inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) # Second poll gets stuck behind first poll - inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) + inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO_2) # Third poll gets stuck behind first poll doesn't get queued inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index 527ab879164..33627f6a787 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -475,7 +475,9 @@ async def test_discovery_match_by_local_name( assert len(mock_config_flow.mock_calls) == 0 switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") - switchbot_adv = AdvertisementData(local_name="wohand", service_uuids=[]) + switchbot_adv = AdvertisementData( + local_name="wohand", service_uuids=[], manufacturer_data={1: b"\x01"} + ) inject_advertisement(hass, switchbot_device, switchbot_adv) await hass.async_block_till_done() @@ -875,7 +877,11 @@ async def test_rediscovery(hass, mock_bleak_scanner_start, enable_bluetooth): switchbot_adv = AdvertisementData( local_name="wohand", service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"] ) - + switchbot_adv_2 = AdvertisementData( + local_name="wohand", + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + manufacturer_data={1: b"\x01"}, + ) inject_advertisement(hass, switchbot_device, switchbot_adv) await hass.async_block_till_done() @@ -887,7 +893,7 @@ async def test_rediscovery(hass, mock_bleak_scanner_start, enable_bluetooth): async_rediscover_address(hass, "44:44:33:11:23:45") - inject_advertisement(hass, switchbot_device, switchbot_adv) + inject_advertisement(hass, switchbot_device, switchbot_adv_2) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 2 @@ -1876,7 +1882,12 @@ async def test_register_callback_survives_reload( manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, ) - + switchbot_adv_2 = AdvertisementData( + local_name="wohand", + service_uuids=["zba20d00-224d-11e6-9fb8-0002a5d5c51b"], + manufacturer_data={89: b"\xd8.\xad\xcd\r\x84"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, + ) inject_advertisement(hass, switchbot_device, switchbot_adv) assert len(callbacks) == 1 service_info: BluetoothServiceInfo = callbacks[0][0] @@ -1888,7 +1899,7 @@ async def test_register_callback_survives_reload( await hass.config_entries.async_reload(entry.entry_id) await hass.async_block_till_done() - inject_advertisement(hass, switchbot_device, switchbot_adv) + inject_advertisement(hass, switchbot_device, switchbot_adv_2) assert len(callbacks) == 2 service_info: BluetoothServiceInfo = callbacks[1][0] assert service_info.name == "wohand" @@ -1950,6 +1961,12 @@ async def test_process_advertisements_ignore_bad_advertisement( manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, service_data={"00000d00-0000-1000-8000-00805f9b34fa": b""}, ) + adv2 = AdvertisementData( + local_name="wohand", + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51a"], + manufacturer_data={89: b"\xd8.\xad\xcd\r\x84"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fa": b""}, + ) def _callback(service_info: BluetoothServiceInfo) -> bool: done.set() @@ -1969,6 +1986,7 @@ async def test_process_advertisements_ignore_bad_advertisement( # callback that returns False while not done.is_set(): inject_advertisement(hass, device, adv) + inject_advertisement(hass, device, adv2) await asyncio.sleep(0) # Set the return value and mutate the advertisement @@ -1976,6 +1994,7 @@ async def test_process_advertisements_ignore_bad_advertisement( return_value.set() adv.service_data["00000d00-0000-1000-8000-00805f9b34fa"] = b"H\x10c" inject_advertisement(hass, device, adv) + inject_advertisement(hass, device, adv2) await asyncio.sleep(0) result = await handle @@ -2024,6 +2043,12 @@ async def test_wrapped_instance_with_filter( manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, ) + switchbot_adv_2 = AdvertisementData( + local_name="wohand", + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + manufacturer_data={89: b"\xd8.\xad\xcd\r\x84"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, + ) empty_device = BLEDevice("11:22:33:44:55:66", "empty") empty_adv = AdvertisementData(local_name="empty") @@ -2033,7 +2058,7 @@ async def test_wrapped_instance_with_filter( ) scanner.register_detection_callback(_device_detected) - inject_advertisement(hass, switchbot_device, switchbot_adv) + inject_advertisement(hass, switchbot_device, switchbot_adv_2) await hass.async_block_till_done() discovered = await scanner.discover(timeout=0) @@ -2090,6 +2115,12 @@ async def test_wrapped_instance_with_service_uuids( manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, ) + switchbot_adv_2 = AdvertisementData( + local_name="wohand", + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + manufacturer_data={89: b"\xd8.\xad\xcd\r\x84"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, + ) empty_device = BLEDevice("11:22:33:44:55:66", "empty") empty_adv = AdvertisementData(local_name="empty") @@ -2099,9 +2130,10 @@ async def test_wrapped_instance_with_service_uuids( ) scanner.register_detection_callback(_device_detected) - for _ in range(2): - inject_advertisement(hass, switchbot_device, switchbot_adv) - await hass.async_block_till_done() + inject_advertisement(hass, switchbot_device, switchbot_adv) + inject_advertisement(hass, switchbot_device, switchbot_adv_2) + + await hass.async_block_till_done() assert len(detected) == 2 @@ -2182,6 +2214,12 @@ async def test_wrapped_instance_changes_uuids( manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, ) + switchbot_adv_2 = AdvertisementData( + local_name="wohand", + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + manufacturer_data={89: b"\xd8.\xad\xcd\r\x84"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, + ) empty_device = BLEDevice("11:22:33:44:55:66", "empty") empty_adv = AdvertisementData(local_name="empty") @@ -2192,9 +2230,9 @@ async def test_wrapped_instance_changes_uuids( ) scanner.register_detection_callback(_device_detected) - for _ in range(2): - inject_advertisement(hass, switchbot_device, switchbot_adv) - await hass.async_block_till_done() + inject_advertisement(hass, switchbot_device, switchbot_adv) + inject_advertisement(hass, switchbot_device, switchbot_adv_2) + await hass.async_block_till_done() assert len(detected) == 2 @@ -2231,6 +2269,12 @@ async def test_wrapped_instance_changes_filters( manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, ) + switchbot_adv_2 = AdvertisementData( + local_name="wohand", + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + manufacturer_data={89: b"\xd8.\xad\xcd\r\x84"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, + ) empty_device = BLEDevice("11:22:33:44:55:62", "empty") empty_adv = AdvertisementData(local_name="empty") @@ -2241,9 +2285,10 @@ async def test_wrapped_instance_changes_filters( ) scanner.register_detection_callback(_device_detected) - for _ in range(2): - inject_advertisement(hass, switchbot_device, switchbot_adv) - await hass.async_block_till_done() + inject_advertisement(hass, switchbot_device, switchbot_adv) + inject_advertisement(hass, switchbot_device, switchbot_adv_2) + + await hass.async_block_till_done() assert len(detected) == 2 diff --git a/tests/components/bluetooth/test_passive_update_processor.py b/tests/components/bluetooth/test_passive_update_processor.py index 99b16131ddc..e7260d22e4b 100644 --- a/tests/components/bluetooth/test_passive_update_processor.py +++ b/tests/components/bluetooth/test_passive_update_processor.py @@ -56,6 +56,18 @@ GENERIC_BLUETOOTH_SERVICE_INFO = BluetoothServiceInfo( service_uuids=[], source="local", ) +GENERIC_BLUETOOTH_SERVICE_INFO_2 = BluetoothServiceInfo( + name="Generic", + address="aa:bb:cc:dd:ee:ff", + rssi=-95, + manufacturer_data={ + 1: b"\x01\x01\x01\x01\x01\x01\x01\x01", + 2: b"\x02", + }, + service_data={}, + service_uuids=[], + source="local", +) GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( devices={ @@ -156,7 +168,7 @@ async def test_basic_usage(hass, mock_bleak_scanner_start): # There should be 4 calls to create entities assert len(mock_entity.mock_calls) == 2 - inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) + inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO_2) # Each listener should receive the same data # since both match @@ -325,7 +337,7 @@ async def test_no_updates_once_stopping(hass, mock_bleak_scanner_start): hass.state = CoreState.stopping # We should stop processing events once hass is stopping - inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) + inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO_2) assert len(all_events) == 1 unregister_processor() cancel_coordinator() @@ -383,7 +395,7 @@ async def test_exception_from_update_method(hass, caplog, mock_bleak_scanner_sta assert processor.available is True # We should go unavailable once we get an exception - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO_2, BluetoothChange.ADVERTISEMENT) assert "Test exception" in caplog.text assert processor.available is False @@ -447,7 +459,7 @@ async def test_bad_data_from_update_method(hass, mock_bleak_scanner_start): # We should go unavailable once we get bad data with pytest.raises(ValueError): - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO_2, BluetoothChange.ADVERTISEMENT) assert processor.available is False @@ -796,7 +808,7 @@ async def test_integration_with_entity(hass, mock_bleak_scanner_start): # First call with just the remote sensor entities results in them being added assert len(mock_add_entities.mock_calls) == 1 - inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) + inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO_2) # Second call with just the remote sensor entities does not add them again assert len(mock_add_entities.mock_calls) == 1 @@ -804,7 +816,7 @@ async def test_integration_with_entity(hass, mock_bleak_scanner_start): # Third call with primary and remote sensor entities adds the primary sensor entities assert len(mock_add_entities.mock_calls) == 2 - inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) + inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO_2) # Forth call with both primary and remote sensor entities does not add them again assert len(mock_add_entities.mock_calls) == 2 @@ -840,6 +852,19 @@ NO_DEVICES_BLUETOOTH_SERVICE_INFO = BluetoothServiceInfo( service_uuids=[], source="local", ) + +NO_DEVICES_BLUETOOTH_SERVICE_INFO_2 = BluetoothServiceInfo( + name="Generic", + address="aa:bb:cc:dd:ee:ff", + rssi=-95, + manufacturer_data={ + 1: b"\x01\x01\x01\x01\x01\x01\x01\x01", + 2: b"\x02", + }, + service_data={}, + service_uuids=[], + source="local", +) NO_DEVICES_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( devices={}, entity_data={ @@ -905,7 +930,7 @@ async def test_integration_with_entity_without_a_device(hass, mock_bleak_scanner # First call with just the remote sensor entities results in them being added assert len(mock_add_entities.mock_calls) == 1 - inject_bluetooth_service_info(hass, NO_DEVICES_BLUETOOTH_SERVICE_INFO) + inject_bluetooth_service_info(hass, NO_DEVICES_BLUETOOTH_SERVICE_INFO_2) # Second call with just the remote sensor entities does not add them again assert len(mock_add_entities.mock_calls) == 1 @@ -967,7 +992,7 @@ async def test_passive_bluetooth_entity_with_entity_platform( ) inject_bluetooth_service_info(hass, NO_DEVICES_BLUETOOTH_SERVICE_INFO) await hass.async_block_till_done() - inject_bluetooth_service_info(hass, NO_DEVICES_BLUETOOTH_SERVICE_INFO) + inject_bluetooth_service_info(hass, NO_DEVICES_BLUETOOTH_SERVICE_INFO_2) await hass.async_block_till_done() assert ( hass.states.get("test_domain.test_platform_aa_bb_cc_dd_ee_ff_temperature") @@ -1157,7 +1182,7 @@ async def test_exception_from_coordinator_update_method( assert processor.available is True # We should go unavailable once we get an exception - inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) + inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO_2) assert "Test exception" in caplog.text assert processor.available is False From 0b4e4e81d47b8501f4811c32f264f9b8e792f779 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Sat, 17 Sep 2022 10:26:59 +0200 Subject: [PATCH 480/955] Handle connection issues with Traccar (#78624) --- homeassistant/components/traccar/device_tracker.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/traccar/device_tracker.py b/homeassistant/components/traccar/device_tracker.py index b668d2fff33..1f6b0b828bd 100644 --- a/homeassistant/components/traccar/device_tracker.py +++ b/homeassistant/components/traccar/device_tracker.py @@ -11,6 +11,7 @@ from pytraccar import ( GeofenceModel, PositionModel, TraccarAuthenticationException, + TraccarConnectionException, TraccarException, ) from stringcase import camelcase @@ -238,6 +239,9 @@ class TraccarScanner: except TraccarAuthenticationException: _LOGGER.error("Authentication for Traccar failed") return False + except TraccarConnectionException as exception: + _LOGGER.error("Connection with Traccar failed - %s", exception) + return False await self._async_update() async_track_time_interval(self._hass, self._async_update, self._scan_interval) From cc51052be55cdcd32f22f4d2118b438c4845ca22 Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Sat, 17 Sep 2022 03:29:56 -0600 Subject: [PATCH 481/955] Change litterrobot integration to cloud_push (#77741) Co-authored-by: J. Nick Koston --- .../components/litterrobot/__init__.py | 2 +- .../components/litterrobot/entity.py | 112 ++---------------- homeassistant/components/litterrobot/hub.py | 7 +- .../components/litterrobot/manifest.json | 2 +- .../components/litterrobot/select.py | 19 ++- .../components/litterrobot/switch.py | 21 ++-- .../components/litterrobot/vacuum.py | 37 ++++-- tests/components/litterrobot/test_init.py | 2 +- tests/components/litterrobot/test_select.py | 8 -- tests/components/litterrobot/test_switch.py | 27 ++--- tests/components/litterrobot/test_vacuum.py | 7 -- 11 files changed, 77 insertions(+), 167 deletions(-) diff --git a/homeassistant/components/litterrobot/__init__.py b/homeassistant/components/litterrobot/__init__.py index 742e9dcb9c7..d4a4f3bfe91 100644 --- a/homeassistant/components/litterrobot/__init__.py +++ b/homeassistant/components/litterrobot/__init__.py @@ -37,7 +37,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Litter-Robot from a config entry.""" hass.data.setdefault(DOMAIN, {}) hub = hass.data[DOMAIN][entry.entry_id] = LitterRobotHub(hass, entry.data) - await hub.login(load_robots=True) + await hub.login(load_robots=True, subscribe_for_updates=True) if platforms := get_platforms_for_robots(hub.account.robots): await hass.config_entries.async_forward_entry_setups(entry, platforms) diff --git a/homeassistant/components/litterrobot/entity.py b/homeassistant/components/litterrobot/entity.py index 9716793f70e..3ad21b1aeb7 100644 --- a/homeassistant/components/litterrobot/entity.py +++ b/homeassistant/components/litterrobot/entity.py @@ -1,33 +1,24 @@ """Litter-Robot entities for common data and methods.""" from __future__ import annotations -from collections.abc import Callable, Coroutine, Iterable -from datetime import time -import logging -from typing import Any, Generic, TypeVar +from collections.abc import Iterable +from typing import Generic, TypeVar from pylitterbot import Robot -from pylitterbot.exceptions import InvalidCommandException -from typing_extensions import ParamSpec +from pylitterbot.robot import EVENT_UPDATE -from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback -from homeassistant.helpers.entity import DeviceInfo, EntityCategory, EntityDescription +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo, EntityDescription import homeassistant.helpers.entity_registry as er -from homeassistant.helpers.event import async_call_later from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, ) -import homeassistant.util.dt as dt_util from .const import DOMAIN from .hub import LitterRobotHub -_P = ParamSpec("_P") _RobotT = TypeVar("_RobotT", bound=Robot) -_LOGGER = logging.getLogger(__name__) - -REFRESH_WAIT_TIME_SECONDS = 8 class LitterRobotEntity( @@ -62,95 +53,10 @@ class LitterRobotEntity( sw_version=getattr(self.robot, "firmware", None), ) - -class LitterRobotControlEntity(LitterRobotEntity[_RobotT]): - """A Litter-Robot entity that can control the unit.""" - - def __init__( - self, robot: _RobotT, hub: LitterRobotHub, description: EntityDescription - ) -> None: - """Init a Litter-Robot control entity.""" - super().__init__(robot=robot, hub=hub, description=description) - self._refresh_callback: CALLBACK_TYPE | None = None - - async def perform_action_and_refresh( - self, - action: Callable[_P, Coroutine[Any, Any, bool]], - *args: _P.args, - **kwargs: _P.kwargs, - ) -> bool: - """Perform an action and initiates a refresh of the robot data after a few seconds.""" - success = False - - try: - success = await action(*args, **kwargs) - except InvalidCommandException as ex: # pragma: no cover - # this exception should only occur if the underlying API for commands changes - _LOGGER.error(ex) - success = False - - if success: - self.async_cancel_refresh_callback() - self._refresh_callback = async_call_later( - self.hass, REFRESH_WAIT_TIME_SECONDS, self.async_call_later_callback - ) - return success - - async def async_call_later_callback(self, *_: Any) -> None: - """Perform refresh request on callback.""" - self._refresh_callback = None - await self.coordinator.async_request_refresh() - - async def async_will_remove_from_hass(self) -> None: - """Cancel refresh callback when entity is being removed from hass.""" - self.async_cancel_refresh_callback() - - @callback - def async_cancel_refresh_callback(self) -> None: - """Clear the refresh callback if it has not already fired.""" - if self._refresh_callback is not None: - self._refresh_callback() - self._refresh_callback = None - - @staticmethod - def parse_time_at_default_timezone(time_str: str | None) -> time | None: - """Parse a time string and add default timezone.""" - if time_str is None: - return None - - if (parsed_time := dt_util.parse_time(time_str)) is None: # pragma: no cover - return None - - return ( - dt_util.start_of_local_day() - .replace( - hour=parsed_time.hour, - minute=parsed_time.minute, - second=parsed_time.second, - ) - .timetz() - ) - - -class LitterRobotConfigEntity(LitterRobotControlEntity[_RobotT]): - """A Litter-Robot entity that can control configuration of the unit.""" - - _attr_entity_category = EntityCategory.CONFIG - - def __init__( - self, robot: _RobotT, hub: LitterRobotHub, description: EntityDescription - ) -> None: - """Init a Litter-Robot control entity.""" - super().__init__(robot=robot, hub=hub, description=description) - self._assumed_state: bool | None = None - - async def perform_action_and_assume_state( - self, action: Callable[[bool], Coroutine[Any, Any, bool]], assumed_state: bool - ) -> None: - """Perform an action and assume the state passed in if call is successful.""" - if await self.perform_action_and_refresh(action, assumed_state): - self._assumed_state = assumed_state - self.async_write_ha_state() + async def async_added_to_hass(self) -> None: + """Set up a listener for the entity.""" + await super().async_added_to_hass() + self.async_on_remove(self.robot.on(EVENT_UPDATE, self.async_write_ha_state)) def async_update_unique_id( diff --git a/homeassistant/components/litterrobot/hub.py b/homeassistant/components/litterrobot/hub.py index 8fab3346cec..5dc8098a8df 100644 --- a/homeassistant/components/litterrobot/hub.py +++ b/homeassistant/components/litterrobot/hub.py @@ -19,7 +19,7 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -UPDATE_INTERVAL_SECONDS = 20 +UPDATE_INTERVAL_SECONDS = 60 * 5 class LitterRobotHub: @@ -43,13 +43,16 @@ class LitterRobotHub: update_interval=timedelta(seconds=UPDATE_INTERVAL_SECONDS), ) - async def login(self, load_robots: bool = False) -> None: + async def login( + self, load_robots: bool = False, subscribe_for_updates: bool = False + ) -> None: """Login to Litter-Robot.""" try: await self.account.connect( username=self._data[CONF_USERNAME], password=self._data[CONF_PASSWORD], load_robots=load_robots, + subscribe_for_updates=subscribe_for_updates, ) return except LitterRobotLoginException as ex: diff --git a/homeassistant/components/litterrobot/manifest.json b/homeassistant/components/litterrobot/manifest.json index cb9b67210fb..fb26f0ca685 100644 --- a/homeassistant/components/litterrobot/manifest.json +++ b/homeassistant/components/litterrobot/manifest.json @@ -6,6 +6,6 @@ "requirements": ["pylitterbot==2022.9.3"], "codeowners": ["@natekspencer", "@tkdrob"], "dhcp": [{ "hostname": "litter-robot4" }], - "iot_class": "cloud_polling", + "iot_class": "cloud_push", "loggers": ["pylitterbot"] } diff --git a/homeassistant/components/litterrobot/select.py b/homeassistant/components/litterrobot/select.py index 9ec784db8f2..d384e94a092 100644 --- a/homeassistant/components/litterrobot/select.py +++ b/homeassistant/components/litterrobot/select.py @@ -16,10 +16,11 @@ from homeassistant.components.select import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import TIME_MINUTES from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .entity import LitterRobotConfigEntity, _RobotT, async_update_unique_id +from .entity import LitterRobotEntity, _RobotT, async_update_unique_id from .hub import LitterRobotHub _CastTypeT = TypeVar("_CastTypeT", int, float) @@ -31,10 +32,7 @@ class RequiredKeysMixin(Generic[_RobotT, _CastTypeT]): current_fn: Callable[[_RobotT], _CastTypeT] options_fn: Callable[[_RobotT], list[_CastTypeT]] - select_fn: Callable[ - [_RobotT, str], - tuple[Callable[[_CastTypeT], Coroutine[Any, Any, bool]], _CastTypeT], - ] + select_fn: Callable[[_RobotT, str], Coroutine[Any, Any, bool]] @dataclass @@ -43,6 +41,8 @@ class RobotSelectEntityDescription( ): """A class that describes robot select entities.""" + entity_category: EntityCategory = EntityCategory.CONFIG + LITTER_ROBOT_SELECT = RobotSelectEntityDescription[LitterRobot, int]( key="cycle_delay", @@ -51,7 +51,7 @@ LITTER_ROBOT_SELECT = RobotSelectEntityDescription[LitterRobot, int]( unit_of_measurement=TIME_MINUTES, current_fn=lambda robot: robot.clean_cycle_wait_time_minutes, options_fn=lambda robot: robot.VALID_WAIT_TIMES, - select_fn=lambda robot, option: (robot.set_wait_time, int(option)), + select_fn=lambda robot, option: robot.set_wait_time(int(option)), ) FEEDER_ROBOT_SELECT = RobotSelectEntityDescription[FeederRobot, float]( key="meal_insert_size", @@ -60,7 +60,7 @@ FEEDER_ROBOT_SELECT = RobotSelectEntityDescription[FeederRobot, float]( unit_of_measurement="cups", current_fn=lambda robot: robot.meal_insert_size, options_fn=lambda robot: robot.VALID_MEAL_INSERT_SIZES, - select_fn=lambda robot, option: (robot.set_meal_insert_size, float(option)), + select_fn=lambda robot, option: robot.set_meal_insert_size(float(option)), ) @@ -88,7 +88,7 @@ async def async_setup_entry( class LitterRobotSelect( - LitterRobotConfigEntity[_RobotT], SelectEntity, Generic[_RobotT, _CastTypeT] + LitterRobotEntity[_RobotT], SelectEntity, Generic[_RobotT, _CastTypeT] ): """Litter-Robot Select.""" @@ -112,5 +112,4 @@ class LitterRobotSelect( async def async_select_option(self, option: str) -> None: """Change the selected option.""" - action, adjusted_option = self.entity_description.select_fn(self.robot, option) - await self.perform_action_and_refresh(action, adjusted_option) + await self.entity_description.select_fn(self.robot, option) diff --git a/homeassistant/components/litterrobot/switch.py b/homeassistant/components/litterrobot/switch.py index 779ee699b41..af690f30501 100644 --- a/homeassistant/components/litterrobot/switch.py +++ b/homeassistant/components/litterrobot/switch.py @@ -14,10 +14,11 @@ from homeassistant.components.switch import ( ) 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 .const import DOMAIN -from .entity import LitterRobotConfigEntity, _RobotT, async_update_unique_id +from .entity import LitterRobotEntity, _RobotT, async_update_unique_id from .hub import LitterRobotHub @@ -26,31 +27,33 @@ class RequiredKeysMixin(Generic[_RobotT]): """A class that describes robot switch entity required keys.""" icons: tuple[str, str] - set_fn: Callable[[_RobotT], Callable[[bool], Coroutine[Any, Any, bool]]] + set_fn: Callable[[_RobotT, bool], Coroutine[Any, Any, bool]] @dataclass class RobotSwitchEntityDescription(SwitchEntityDescription, RequiredKeysMixin[_RobotT]): """A class that describes robot switch entities.""" + entity_category: EntityCategory = EntityCategory.CONFIG + ROBOT_SWITCHES = [ RobotSwitchEntityDescription[Union[LitterRobot, FeederRobot]]( key="night_light_mode_enabled", name="Night Light Mode", icons=("mdi:lightbulb-on", "mdi:lightbulb-off"), - set_fn=lambda robot: robot.set_night_light, + set_fn=lambda robot, value: robot.set_night_light(value), ), RobotSwitchEntityDescription[Union[LitterRobot, FeederRobot]]( key="panel_lock_enabled", name="Panel Lockout", icons=("mdi:lock", "mdi:lock-open"), - set_fn=lambda robot: robot.set_panel_lockout, + set_fn=lambda robot, value: robot.set_panel_lockout(value), ), ] -class RobotSwitchEntity(LitterRobotConfigEntity[_RobotT], SwitchEntity): +class RobotSwitchEntity(LitterRobotEntity[_RobotT], SwitchEntity): """Litter-Robot switch entity.""" entity_description: RobotSwitchEntityDescription[_RobotT] @@ -58,8 +61,6 @@ class RobotSwitchEntity(LitterRobotConfigEntity[_RobotT], SwitchEntity): @property def is_on(self) -> bool | None: """Return true if switch is on.""" - if self._refresh_callback is not None: - return self._assumed_state return bool(getattr(self.robot, self.entity_description.key)) @property @@ -70,13 +71,11 @@ class RobotSwitchEntity(LitterRobotConfigEntity[_RobotT], SwitchEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" - set_fn = self.entity_description.set_fn - await self.perform_action_and_assume_state(set_fn(self.robot), True) + await self.entity_description.set_fn(self.robot, True) async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" - set_fn = self.entity_description.set_fn - await self.perform_action_and_assume_state(set_fn(self.robot), False) + await self.entity_description.set_fn(self.robot, False) async def async_setup_entry( diff --git a/homeassistant/components/litterrobot/vacuum.py b/homeassistant/components/litterrobot/vacuum.py index 27cd3e6758a..55f0a182959 100644 --- a/homeassistant/components/litterrobot/vacuum.py +++ b/homeassistant/components/litterrobot/vacuum.py @@ -1,6 +1,7 @@ """Support for Litter-Robot "Vacuum".""" from __future__ import annotations +from datetime import time from typing import Any from pylitterbot import LitterRobot @@ -22,9 +23,10 @@ from homeassistant.const import STATE_OFF from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback +import homeassistant.util.dt as dt_util from .const import DOMAIN -from .entity import LitterRobotControlEntity, async_update_unique_id +from .entity import LitterRobotEntity, async_update_unique_id from .hub import LitterRobotHub SERVICE_SET_SLEEP_MODE = "set_sleep_mode" @@ -70,7 +72,7 @@ async def async_setup_entry( ) -class LitterRobotCleaner(LitterRobotControlEntity[LitterRobot], StateVacuumEntity): +class LitterRobotCleaner(LitterRobotEntity[LitterRobot], StateVacuumEntity): """Litter-Robot "Vacuum" Cleaner.""" _attr_supported_features = ( @@ -95,24 +97,41 @@ class LitterRobotCleaner(LitterRobotControlEntity[LitterRobot], StateVacuumEntit async def async_turn_on(self, **kwargs: Any) -> None: """Turn the cleaner on, starting a clean cycle.""" - await self.perform_action_and_refresh(self.robot.set_power_status, True) + await self.robot.set_power_status(True) async def async_turn_off(self, **kwargs: Any) -> None: """Turn the unit off, stopping any cleaning in progress as is.""" - await self.perform_action_and_refresh(self.robot.set_power_status, False) + await self.robot.set_power_status(False) async def async_start(self) -> None: """Start a clean cycle.""" - await self.perform_action_and_refresh(self.robot.start_cleaning) + await self.robot.start_cleaning() async def async_set_sleep_mode( self, enabled: bool, start_time: str | None = None ) -> None: """Set the sleep mode.""" - await self.perform_action_and_refresh( - self.robot.set_sleep_mode, - enabled, - self.parse_time_at_default_timezone(start_time), + await self.robot.set_sleep_mode( + enabled, self.parse_time_at_default_timezone(start_time) + ) + + @staticmethod + def parse_time_at_default_timezone(time_str: str | None) -> time | None: + """Parse a time string and add default timezone.""" + if time_str is None: + return None + + if (parsed_time := dt_util.parse_time(time_str)) is None: # pragma: no cover + return None + + return ( + dt_util.start_of_local_day() + .replace( + hour=parsed_time.hour, + minute=parsed_time.minute, + second=parsed_time.second, + ) + .timetz() ) @property diff --git a/tests/components/litterrobot/test_init.py b/tests/components/litterrobot/test_init.py index d8ca690d965..610dab04a90 100644 --- a/tests/components/litterrobot/test_init.py +++ b/tests/components/litterrobot/test_init.py @@ -56,7 +56,7 @@ async def test_entry_not_setup(hass, side_effect, expected_state): entry.add_to_hass(hass) with patch( - "pylitterbot.Account.connect", + "homeassistant.components.litterrobot.hub.Account.connect", side_effect=side_effect, ): await hass.config_entries.async_setup(entry.entry_id) diff --git a/tests/components/litterrobot/test_select.py b/tests/components/litterrobot/test_select.py index eda59216718..3cde7a5d23b 100644 --- a/tests/components/litterrobot/test_select.py +++ b/tests/components/litterrobot/test_select.py @@ -1,10 +1,7 @@ """Test the Litter-Robot select entity.""" -from datetime import timedelta - from pylitterbot import LitterRobot3 import pytest -from homeassistant.components.litterrobot.entity import REFRESH_WAIT_TIME_SECONDS from homeassistant.components.select import ( ATTR_OPTION, DOMAIN as PLATFORM_DOMAIN, @@ -14,12 +11,9 @@ from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry from homeassistant.helpers.entity import EntityCategory -from homeassistant.util.dt import utcnow from .conftest import setup_integration -from tests.common import async_fire_time_changed - SELECT_ENTITY_ID = "select.test_clean_cycle_wait_time_minutes" @@ -49,8 +43,6 @@ async def test_wait_time_select(hass: HomeAssistant, mock_account): blocking=True, ) - future = utcnow() + timedelta(seconds=REFRESH_WAIT_TIME_SECONDS) - async_fire_time_changed(hass, future) assert mock_account.robots[0].set_wait_time.call_count == count diff --git a/tests/components/litterrobot/test_switch.py b/tests/components/litterrobot/test_switch.py index 540a1c92810..8adc7cdc6eb 100644 --- a/tests/components/litterrobot/test_switch.py +++ b/tests/components/litterrobot/test_switch.py @@ -1,10 +1,9 @@ """Test the Litter-Robot switch entity.""" -from datetime import timedelta from unittest.mock import MagicMock +from pylitterbot import Robot import pytest -from homeassistant.components.litterrobot.entity import REFRESH_WAIT_TIME_SECONDS from homeassistant.components.switch import ( DOMAIN as PLATFORM_DOMAIN, SERVICE_TURN_OFF, @@ -14,12 +13,9 @@ from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry from homeassistant.helpers.entity import EntityCategory -from homeassistant.util.dt import utcnow from .conftest import setup_integration -from tests.common import async_fire_time_changed - NIGHT_LIGHT_MODE_ENTITY_ID = "switch.test_night_light_mode" PANEL_LOCKOUT_ENTITY_ID = "switch.test_panel_lockout" @@ -39,17 +35,22 @@ async def test_switch(hass: HomeAssistant, mock_account: MagicMock): @pytest.mark.parametrize( - "entity_id,robot_command", + "entity_id,robot_command,updated_field", [ - (NIGHT_LIGHT_MODE_ENTITY_ID, "set_night_light"), - (PANEL_LOCKOUT_ENTITY_ID, "set_panel_lockout"), + (NIGHT_LIGHT_MODE_ENTITY_ID, "set_night_light", "nightLightActive"), + (PANEL_LOCKOUT_ENTITY_ID, "set_panel_lockout", "panelLockActive"), ], ) async def test_on_off_commands( - hass: HomeAssistant, mock_account: MagicMock, entity_id: str, robot_command: str + hass: HomeAssistant, + mock_account: MagicMock, + entity_id: str, + robot_command: str, + updated_field: str, ): """Test sending commands to the switch.""" await setup_integration(hass, mock_account, PLATFORM_DOMAIN) + robot: Robot = mock_account.robots[0] state = hass.states.get(entity_id) assert state @@ -57,19 +58,17 @@ async def test_on_off_commands( data = {ATTR_ENTITY_ID: entity_id} count = 0 - for service in [SERVICE_TURN_ON, SERVICE_TURN_OFF]: + for service in (SERVICE_TURN_ON, SERVICE_TURN_OFF): count += 1 - await hass.services.async_call( PLATFORM_DOMAIN, service, data, blocking=True, ) + robot._update_data({updated_field: 1 if service == SERVICE_TURN_ON else 0}) - future = utcnow() + timedelta(seconds=REFRESH_WAIT_TIME_SECONDS) - async_fire_time_changed(hass, future) - assert getattr(mock_account.robots[0], robot_command).call_count == count + assert getattr(robot, robot_command).call_count == count state = hass.states.get(entity_id) assert state assert state.state == STATE_ON if service == SERVICE_TURN_ON else STATE_OFF diff --git a/tests/components/litterrobot/test_vacuum.py b/tests/components/litterrobot/test_vacuum.py index 08aa8b2399b..f288ebc4c87 100644 --- a/tests/components/litterrobot/test_vacuum.py +++ b/tests/components/litterrobot/test_vacuum.py @@ -1,14 +1,12 @@ """Test the Litter-Robot vacuum entity.""" from __future__ import annotations -from datetime import timedelta from typing import Any from unittest.mock import MagicMock import pytest from homeassistant.components.litterrobot import DOMAIN -from homeassistant.components.litterrobot.entity import REFRESH_WAIT_TIME_SECONDS from homeassistant.components.litterrobot.vacuum import SERVICE_SET_SLEEP_MODE from homeassistant.components.vacuum import ( ATTR_STATUS, @@ -22,13 +20,10 @@ from homeassistant.components.vacuum import ( from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant import homeassistant.helpers.entity_registry as er -from homeassistant.util.dt import utcnow from .common import VACUUM_ENTITY_ID from .conftest import setup_integration -from tests.common import async_fire_time_changed - VACUUM_UNIQUE_ID_OLD = "LR3C012345-Litter Box" VACUUM_UNIQUE_ID_NEW = "LR3C012345-litter_box" @@ -141,7 +136,5 @@ async def test_commands( data, blocking=True, ) - future = utcnow() + timedelta(seconds=REFRESH_WAIT_TIME_SECONDS) - async_fire_time_changed(hass, future) getattr(mock_account.robots[0], command).assert_called_once() assert (f"'{DOMAIN}.{service}' service is deprecated" in caplog.text) is deprecated From c3ca9f3ad1744e8b3c074ff9959502fcafec2167 Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Sat, 17 Sep 2022 04:03:12 -0600 Subject: [PATCH 482/955] Add litter level sensor for Litter-Robot 4 (#78602) Co-authored-by: J. Nick Koston --- homeassistant/components/litterrobot/sensor.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/litterrobot/sensor.py b/homeassistant/components/litterrobot/sensor.py index c904335d23f..1a8f066f54b 100644 --- a/homeassistant/components/litterrobot/sensor.py +++ b/homeassistant/components/litterrobot/sensor.py @@ -100,12 +100,18 @@ ROBOT_SENSOR_MAP: dict[type[Robot], list[RobotSensorEntityDescription]] = { ), ], LitterRobot4: [ + RobotSensorEntityDescription[LitterRobot4]( + key="litter_level", + name="Litter level", + native_unit_of_measurement=PERCENTAGE, + icon_fn=lambda state: icon_for_gauge_level(state, 10), + ), RobotSensorEntityDescription[LitterRobot4]( key="pet_weight", name="Pet weight", icon="mdi:scale", native_unit_of_measurement=MASS_POUNDS, - ) + ), ], FeederRobot: [ RobotSensorEntityDescription[FeederRobot]( From cd8a5ea1e265af51c875f966a56964a651afe63b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 17 Sep 2022 05:13:33 -0500 Subject: [PATCH 483/955] Fix reconnect race in HomeKit Controller (#78629) --- homeassistant/components/homekit_controller/connection.py | 2 +- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index b4aaab5acf0..8afbe6a70e4 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -423,7 +423,7 @@ class HKDevice: if self._polling_interval_remover: self._polling_interval_remover() - await self.pairing.close() + await self.pairing.shutdown() await self.hass.config_entries.async_unload_platforms( self.config_entry, self.platforms diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 338015d8c40..d9e5bfb854b 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==1.5.8"], + "requirements": ["aiohomekit==1.5.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 078098ff77b..6f5adcee759 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==1.5.8 +aiohomekit==1.5.9 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b3f76048da7..9f5ce58d3d2 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==1.5.8 +aiohomekit==1.5.9 # homeassistant.components.emulated_hue # homeassistant.components.http From 9acea07d31aaf08cb1d08a8d225d62def03b9bad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Conde=20G=C3=B3mez?= Date: Sat, 17 Sep 2022 13:59:29 +0200 Subject: [PATCH 484/955] Bump qingping-ble to 0.7.0 (#78630) --- CODEOWNERS | 4 ++-- homeassistant/components/qingping/manifest.json | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 4c3ebac5bf1..314c43f418e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -868,8 +868,8 @@ build.json @home-assistant/supervisor /homeassistant/components/pvpc_hourly_pricing/ @azogue /tests/components/pvpc_hourly_pricing/ @azogue /homeassistant/components/qbittorrent/ @geoffreylagaisse -/homeassistant/components/qingping/ @bdraco -/tests/components/qingping/ @bdraco +/homeassistant/components/qingping/ @bdraco @skgsergio +/tests/components/qingping/ @bdraco @skgsergio /homeassistant/components/qld_bushfire/ @exxamalte /tests/components/qld_bushfire/ @exxamalte /homeassistant/components/qnap_qsw/ @Noltari diff --git a/homeassistant/components/qingping/manifest.json b/homeassistant/components/qingping/manifest.json index 4e1189f3782..85df751bfc7 100644 --- a/homeassistant/components/qingping/manifest.json +++ b/homeassistant/components/qingping/manifest.json @@ -11,8 +11,8 @@ "connectable": false } ], - "requirements": ["qingping-ble==0.6.0"], + "requirements": ["qingping-ble==0.7.0"], "dependencies": ["bluetooth"], - "codeowners": ["@bdraco"], + "codeowners": ["@bdraco", "@skgsergio"], "iot_class": "local_push" } diff --git a/requirements_all.txt b/requirements_all.txt index 6f5adcee759..a2e4ba1ef01 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2106,7 +2106,7 @@ pyzbar==0.1.7 pyzerproc==0.4.8 # homeassistant.components.qingping -qingping-ble==0.6.0 +qingping-ble==0.7.0 # homeassistant.components.qnap qnapstats==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9f5ce58d3d2..22d68919575 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1448,7 +1448,7 @@ pyws66i==1.1 pyzerproc==0.4.8 # homeassistant.components.qingping -qingping-ble==0.6.0 +qingping-ble==0.7.0 # homeassistant.components.rachio rachiopy==1.0.3 From 24df3574bc1c11db4201d05608bc5d26ece5a861 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 17 Sep 2022 15:04:09 +0200 Subject: [PATCH 485/955] Automatically set up Awair during onboarding (#78632) --- homeassistant/components/awair/config_flow.py | 4 +-- tests/components/awair/test_config_flow.py | 26 +++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/awair/config_flow.py b/homeassistant/components/awair/config_flow.py index 99b1545e792..c68d46f7d39 100644 --- a/homeassistant/components/awair/config_flow.py +++ b/homeassistant/components/awair/config_flow.py @@ -10,7 +10,7 @@ from python_awair.exceptions import AuthError, AwairError from python_awair.user import AwairUser import voluptuous as vol -from homeassistant.components import zeroconf +from homeassistant.components import onboarding, zeroconf from homeassistant.config_entries import SOURCE_ZEROCONF, ConfigFlow from homeassistant.const import CONF_ACCESS_TOKEN, CONF_DEVICE, CONF_HOST from homeassistant.core import callback @@ -60,7 +60,7 @@ class AwairFlowHandler(ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Confirm discovery.""" - if user_input is not None: + if user_input is not None or not onboarding.async_is_onboarded(self.hass): title = f"{self._device.model} ({self._device.device_id})" return self.async_create_entry( title=title, diff --git a/tests/components/awair/test_config_flow.py b/tests/components/awair/test_config_flow.py index b939b62641a..aeb7b7d5d8a 100644 --- a/tests/components/awair/test_config_flow.py +++ b/tests/components/awair/test_config_flow.py @@ -399,3 +399,29 @@ async def test_zeroconf_discovery_update_configuration( assert config_entry.data[CONF_HOST] == ZEROCONF_DISCOVERY.host assert mock_setup_entry.call_count == 0 + + +async def test_zeroconf_during_onboarding( + hass: HomeAssistant, local_devices: Any +) -> None: + """Test the zeroconf creates an entry during onboarding.""" + with patch( + "homeassistant.components.awair.async_setup_entry", + return_value=True, + ) as mock_setup_entry, patch( + "python_awair.AwairClient.query", side_effect=[local_devices] + ), patch( + "homeassistant.components.onboarding.async_is_onboarded", + return_value=False, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_ZEROCONF}, data=ZEROCONF_DISCOVERY + ) + + assert result.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result.get("title") == "Awair Element (24947)" + assert "data" in result + assert result["data"][CONF_HOST] == ZEROCONF_DISCOVERY.host + assert "result" in result + assert result["result"].unique_id == LOCAL_UNIQUE_ID + assert len(mock_setup_entry.mock_calls) == 1 From 5a6a474bbe673ff02c37d7816c745baf5a1704c8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 17 Sep 2022 17:15:11 +0200 Subject: [PATCH 486/955] Update demetriek to 0.2.4 (#78646) --- homeassistant/components/lametric/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lametric/manifest.json b/homeassistant/components/lametric/manifest.json index 251378860be..cddf28e5487 100644 --- a/homeassistant/components/lametric/manifest.json +++ b/homeassistant/components/lametric/manifest.json @@ -2,7 +2,7 @@ "domain": "lametric", "name": "LaMetric", "documentation": "https://www.home-assistant.io/integrations/lametric", - "requirements": ["demetriek==0.2.2"], + "requirements": ["demetriek==0.2.4"], "codeowners": ["@robbiet480", "@frenck"], "iot_class": "local_polling", "dependencies": ["application_credentials"], diff --git a/requirements_all.txt b/requirements_all.txt index a2e4ba1ef01..ee14e5ce731 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -555,7 +555,7 @@ defusedxml==0.7.1 deluge-client==1.7.1 # homeassistant.components.lametric -demetriek==0.2.2 +demetriek==0.2.4 # homeassistant.components.denonavr denonavr==0.10.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 22d68919575..ade27067dc7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -426,7 +426,7 @@ defusedxml==0.7.1 deluge-client==1.7.1 # homeassistant.components.lametric -demetriek==0.2.2 +demetriek==0.2.4 # homeassistant.components.denonavr denonavr==0.10.11 From 35221e9a618d94ab7412741486c687af87f1e9af Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sat, 17 Sep 2022 17:27:22 +0200 Subject: [PATCH 487/955] Correct return typing for `catch_log_exception` (#78399) --- homeassistant/util/logging.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/homeassistant/util/logging.py b/homeassistant/util/logging.py index 76df4bb17b6..e493a3378fd 100644 --- a/homeassistant/util/logging.py +++ b/homeassistant/util/logging.py @@ -120,27 +120,30 @@ def log_exception(format_err: Callable[..., Any], *args: Any) -> None: def catch_log_exception( func: Callable[..., Coroutine[Any, Any, Any]], format_err: Callable[..., Any] ) -> Callable[..., Coroutine[Any, Any, None]]: - """Overload for Callables that return a Coroutine.""" + """Overload for Coroutine that returns a Coroutine.""" @overload def catch_log_exception( func: Callable[..., Any], format_err: Callable[..., Any] -) -> Callable[..., None | Coroutine[Any, Any, None]]: - """Overload for Callables that return Any.""" +) -> Callable[..., None] | Callable[..., Coroutine[Any, Any, None]]: + """Overload for a callback that returns a callback.""" def catch_log_exception( func: Callable[..., Any], format_err: Callable[..., Any] -) -> Callable[..., None | Coroutine[Any, Any, None]]: - """Decorate a callback to catch and log exceptions.""" +) -> Callable[..., None] | Callable[..., Coroutine[Any, Any, None]]: + """Decorate a function func to catch and log exceptions. + If func is a coroutine function, a coroutine function will be returned. + If func is a callback, a callback will be returned. + """ # Check for partials to properly determine if coroutine function check_func = func while isinstance(check_func, partial): check_func = check_func.func - wrapper_func: Callable[..., None | Coroutine[Any, Any, None]] + wrapper_func: Callable[..., None] | Callable[..., Coroutine[Any, Any, None]] if asyncio.iscoroutinefunction(check_func): async_func = cast(Callable[..., Coroutine[Any, Any, None]], func) From bbf54e6f443c4184bbf216d7e31edfb1f0171653 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 17 Sep 2022 17:37:39 +0200 Subject: [PATCH 488/955] Improve light typing (#78641) --- homeassistant/components/light/__init__.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index e179672f45a..7d34c607b1f 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -444,6 +444,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: # If a color is specified, convert to the color space supported by the light # Backwards compatibility: Fall back to hs color if light.supported_color_modes # is not implemented + rgb_color: tuple[int, int, int] | None + rgbww_color: tuple[int, int, int, int, int] | None if not supported_color_modes: if (rgb_color := params.pop(ATTR_RGB_COLOR, None)) is not None: params[ATTR_HS_COLOR] = color_util.color_RGB_to_hs(*rgb_color) @@ -453,6 +455,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: rgb_color = color_util.color_rgbw_to_rgb(*rgbw_color) params[ATTR_HS_COLOR] = color_util.color_RGB_to_hs(*rgb_color) elif (rgbww_color := params.pop(ATTR_RGBWW_COLOR, None)) is not None: + # https://github.com/python/mypy/issues/13673 rgb_color = color_util.color_rgbww_to_rgb( # type: ignore[call-arg] *rgbww_color, light.min_mireds, light.max_mireds ) @@ -466,16 +469,17 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: params[ATTR_RGBW_COLOR] = color_util.color_rgb_to_rgbw(*rgb_color) elif ColorMode.RGBWW in supported_color_modes: rgb_color = color_util.color_hs_to_RGB(*hs_color) - params[ATTR_RGBWW_COLOR] = color_util.color_rgb_to_rgbww( # type: ignore[call-arg] + params[ATTR_RGBWW_COLOR] = color_util.color_rgb_to_rgbww( *rgb_color, light.min_mireds, light.max_mireds ) elif ColorMode.XY in supported_color_modes: params[ATTR_XY_COLOR] = color_util.color_hs_to_xy(*hs_color) elif ATTR_RGB_COLOR in params and ColorMode.RGB not in supported_color_modes: - rgb_color = params.pop(ATTR_RGB_COLOR) + assert (rgb_color := params.pop(ATTR_RGB_COLOR)) is not None if ColorMode.RGBW in supported_color_modes: params[ATTR_RGBW_COLOR] = color_util.color_rgb_to_rgbw(*rgb_color) elif ColorMode.RGBWW in supported_color_modes: + # https://github.com/python/mypy/issues/13673 params[ATTR_RGBWW_COLOR] = color_util.color_rgb_to_rgbww( # type: ignore[call-arg] *rgb_color, light.min_mireds, light.max_mireds ) @@ -494,7 +498,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: params[ATTR_RGBW_COLOR] = color_util.color_rgb_to_rgbw(*rgb_color) elif ColorMode.RGBWW in supported_color_modes: rgb_color = color_util.color_xy_to_RGB(*xy_color) - params[ATTR_RGBWW_COLOR] = color_util.color_rgb_to_rgbww( # type: ignore[call-arg] + params[ATTR_RGBWW_COLOR] = color_util.color_rgb_to_rgbww( *rgb_color, light.min_mireds, light.max_mireds ) elif ATTR_RGBW_COLOR in params and ColorMode.RGBW not in supported_color_modes: @@ -503,7 +507,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: if ColorMode.RGB in supported_color_modes: params[ATTR_RGB_COLOR] = rgb_color elif ColorMode.RGBWW in supported_color_modes: - params[ATTR_RGBWW_COLOR] = color_util.color_rgb_to_rgbww( # type: ignore[call-arg] + params[ATTR_RGBWW_COLOR] = color_util.color_rgb_to_rgbww( *rgb_color, light.min_mireds, light.max_mireds ) elif ColorMode.HS in supported_color_modes: @@ -513,7 +517,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: elif ( ATTR_RGBWW_COLOR in params and ColorMode.RGBWW not in supported_color_modes ): - rgbww_color = params.pop(ATTR_RGBWW_COLOR) + assert (rgbww_color := params.pop(ATTR_RGBWW_COLOR)) is not None + # https://github.com/python/mypy/issues/13673 rgb_color = color_util.color_rgbww_to_rgb( # type: ignore[call-arg] *rgbww_color, light.min_mireds, light.max_mireds ) From 64988521bbf3a40abc42861cf037476c8a6b51ad Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 17 Sep 2022 18:18:53 +0200 Subject: [PATCH 489/955] Make use of generic EntityComponent (part 2) (#78494) --- homeassistant/components/air_quality/__init__.py | 8 +++++--- .../components/alarm_control_panel/__init__.py | 8 +++++--- homeassistant/components/binary_sensor/__init__.py | 8 +++++--- homeassistant/components/button/__init__.py | 8 +++++--- homeassistant/components/calendar/__init__.py | 12 +++++++----- homeassistant/components/calendar/trigger.py | 4 +++- homeassistant/components/climate/__init__.py | 8 +++++--- homeassistant/components/cover/__init__.py | 8 +++++--- .../components/device_tracker/config_entry.py | 10 +++++++--- homeassistant/components/fan/__init__.py | 8 +++++--- homeassistant/components/geo_location/__init__.py | 8 +++++--- homeassistant/components/humidifier/__init__.py | 8 +++++--- homeassistant/components/lock/__init__.py | 8 +++++--- homeassistant/components/scene/__init__.py | 8 +++++--- homeassistant/components/select/__init__.py | 8 +++++--- homeassistant/components/sensor/__init__.py | 8 +++++--- homeassistant/components/siren/__init__.py | 9 ++++++--- homeassistant/components/switch/__init__.py | 8 +++++--- homeassistant/components/vacuum/__init__.py | 8 +++++--- homeassistant/components/water_heater/__init__.py | 8 +++++--- homeassistant/components/weather/__init__.py | 8 +++++--- 21 files changed, 108 insertions(+), 63 deletions(-) diff --git a/homeassistant/components/air_quality/__init__.py b/homeassistant/components/air_quality/__init__.py index d0aa1fd4a76..c2992cc804b 100644 --- a/homeassistant/components/air_quality/__init__.py +++ b/homeassistant/components/air_quality/__init__.py @@ -50,10 +50,12 @@ PROP_TO_ATTR: Final[dict[str, str]] = { "sulphur_dioxide": ATTR_SO2, } +# mypy: disallow-any-generics + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the air quality component.""" - component = hass.data[DOMAIN] = EntityComponent( + component = hass.data[DOMAIN] = EntityComponent[AirQualityEntity]( _LOGGER, DOMAIN, hass, SCAN_INTERVAL ) await component.async_setup(config) @@ -62,13 +64,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[AirQualityEntity] = hass.data[DOMAIN] return await component.async_setup_entry(entry) async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[AirQualityEntity] = hass.data[DOMAIN] return await component.async_unload_entry(entry) diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py index d89dbd280ef..4d74a39d977 100644 --- a/homeassistant/components/alarm_control_panel/__init__.py +++ b/homeassistant/components/alarm_control_panel/__init__.py @@ -55,10 +55,12 @@ ALARM_SERVICE_SCHEMA: Final = make_entity_service_schema( PLATFORM_SCHEMA: Final = cv.PLATFORM_SCHEMA PLATFORM_SCHEMA_BASE: Final = cv.PLATFORM_SCHEMA_BASE +# mypy: disallow-any-generics + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Track states and offer events for sensors.""" - component = hass.data[DOMAIN] = EntityComponent( + component = hass.data[DOMAIN] = EntityComponent[AlarmControlPanelEntity]( _LOGGER, DOMAIN, hass, SCAN_INTERVAL ) @@ -109,13 +111,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[AlarmControlPanelEntity] = hass.data[DOMAIN] return await component.async_setup_entry(entry) async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[AlarmControlPanelEntity] = hass.data[DOMAIN] return await component.async_unload_entry(entry) diff --git a/homeassistant/components/binary_sensor/__init__.py b/homeassistant/components/binary_sensor/__init__.py index 82e903ce877..46107938ddf 100644 --- a/homeassistant/components/binary_sensor/__init__.py +++ b/homeassistant/components/binary_sensor/__init__.py @@ -150,10 +150,12 @@ DEVICE_CLASS_UPDATE = BinarySensorDeviceClass.UPDATE.value DEVICE_CLASS_VIBRATION = BinarySensorDeviceClass.VIBRATION.value DEVICE_CLASS_WINDOW = BinarySensorDeviceClass.WINDOW.value +# mypy: disallow-any-generics + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Track states and offer events for binary sensors.""" - component = hass.data[DOMAIN] = EntityComponent( + component = hass.data[DOMAIN] = EntityComponent[BinarySensorEntity]( logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL ) @@ -163,13 +165,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[BinarySensorEntity] = hass.data[DOMAIN] return await component.async_setup_entry(entry) async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[BinarySensorEntity] = hass.data[DOMAIN] return await component.async_unload_entry(entry) diff --git a/homeassistant/components/button/__init__.py b/homeassistant/components/button/__init__.py index d0e27662d41..99fe02f7a9d 100644 --- a/homeassistant/components/button/__init__.py +++ b/homeassistant/components/button/__init__.py @@ -41,10 +41,12 @@ class ButtonDeviceClass(StrEnum): DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.Coerce(ButtonDeviceClass)) +# mypy: disallow-any-generics + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up Button entities.""" - component = hass.data[DOMAIN] = EntityComponent( + component = hass.data[DOMAIN] = EntityComponent[ButtonEntity]( _LOGGER, DOMAIN, hass, SCAN_INTERVAL ) await component.async_setup(config) @@ -60,13 +62,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[ButtonEntity] = hass.data[DOMAIN] return await component.async_setup_entry(entry) async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[ButtonEntity] = hass.data[DOMAIN] return await component.async_unload_entry(entry) diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py index ee1bb866e6a..cfbe038c251 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -32,10 +32,12 @@ DOMAIN = "calendar" ENTITY_ID_FORMAT = DOMAIN + ".{}" SCAN_INTERVAL = datetime.timedelta(seconds=60) +# mypy: disallow-any-generics + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Track states and offer events for calendars.""" - component = hass.data[DOMAIN] = EntityComponent( + component = hass.data[DOMAIN] = EntityComponent[CalendarEntity]( _LOGGER, DOMAIN, hass, SCAN_INTERVAL ) @@ -52,13 +54,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[CalendarEntity] = hass.data[DOMAIN] return await component.async_setup_entry(entry) async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[CalendarEntity] = hass.data[DOMAIN] return await component.async_unload_entry(entry) @@ -243,7 +245,7 @@ class CalendarEventView(http.HomeAssistantView): url = "/api/calendars/{entity_id}" name = "api:calendars:calendar" - def __init__(self, component: EntityComponent) -> None: + def __init__(self, component: EntityComponent[CalendarEntity]) -> None: """Initialize calendar view.""" self.component = component @@ -294,7 +296,7 @@ class CalendarListView(http.HomeAssistantView): url = "/api/calendars" name = "api:calendars" - def __init__(self, component: EntityComponent) -> None: + def __init__(self, component: EntityComponent[CalendarEntity]) -> None: """Initialize calendar view.""" self.component = component diff --git a/homeassistant/components/calendar/trigger.py b/homeassistant/components/calendar/trigger.py index 74be0f7e71d..0fdb7259c9d 100644 --- a/homeassistant/components/calendar/trigger.py +++ b/homeassistant/components/calendar/trigger.py @@ -38,6 +38,8 @@ TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( } ) +# mypy: disallow-any-generics + class CalendarEventListener: """Helper class to listen to calendar events.""" @@ -172,7 +174,7 @@ async def async_attach_trigger( event_type = config[CONF_EVENT] offset = config[CONF_OFFSET] - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[CalendarEntity] = hass.data[DOMAIN] if not (entity := component.get_entity(entity_id)) or not isinstance( entity, CalendarEntity ): diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index 0f3e5666bc6..4ed875f2d31 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -129,10 +129,12 @@ SET_TEMPERATURE_SCHEMA = vol.All( ), ) +# mypy: disallow-any-generics + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up climate entities.""" - component = hass.data[DOMAIN] = EntityComponent( + component = hass.data[DOMAIN] = EntityComponent[ClimateEntity]( _LOGGER, DOMAIN, hass, SCAN_INTERVAL ) await component.async_setup(config) @@ -189,13 +191,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[ClimateEntity] = hass.data[DOMAIN] return await component.async_setup_entry(entry) async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[ClimateEntity] = hass.data[DOMAIN] return await component.async_unload_entry(entry) diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index b66398b3491..57187c56819 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -83,6 +83,8 @@ DEVICE_CLASS_SHADE = CoverDeviceClass.SHADE.value DEVICE_CLASS_SHUTTER = CoverDeviceClass.SHUTTER.value DEVICE_CLASS_WINDOW = CoverDeviceClass.WINDOW.value +# mypy: disallow-any-generics + class CoverEntityFeature(IntEnum): """Supported features of the cover entity.""" @@ -122,7 +124,7 @@ def is_closed(hass: HomeAssistant, entity_id: str) -> bool: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Track states and offer events for covers.""" - component = hass.data[DOMAIN] = EntityComponent( + component = hass.data[DOMAIN] = EntityComponent[CoverEntity]( _LOGGER, DOMAIN, hass, SCAN_INTERVAL ) @@ -202,13 +204,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[CoverEntity] = hass.data[DOMAIN] return await component.async_setup_entry(entry) async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[CoverEntity] = hass.data[DOMAIN] return await component.async_unload_entry(entry) diff --git a/homeassistant/components/device_tracker/config_entry.py b/homeassistant/components/device_tracker/config_entry.py index b587f17d58e..64ad55aee37 100644 --- a/homeassistant/components/device_tracker/config_entry.py +++ b/homeassistant/components/device_tracker/config_entry.py @@ -33,15 +33,19 @@ from .const import ( SourceType, ) +# mypy: disallow-any-generics + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up an entry.""" - component: EntityComponent | None = hass.data.get(DOMAIN) + component: EntityComponent[BaseTrackerEntity] | None = hass.data.get(DOMAIN) if component is not None: return await component.async_setup_entry(entry) - component = hass.data[DOMAIN] = EntityComponent(LOGGER, DOMAIN, hass) + component = hass.data[DOMAIN] = EntityComponent[BaseTrackerEntity]( + LOGGER, DOMAIN, hass + ) # Clean up old devices created by device tracker entities in the past. # Can be removed after 2022.6 @@ -70,7 +74,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload an entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[BaseTrackerEntity] = hass.data[DOMAIN] return await component.async_unload_entry(entry) diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index 8f6585f6535..d44335fad07 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -74,6 +74,8 @@ ATTR_DIRECTION = "direction" ATTR_PRESET_MODE = "preset_mode" ATTR_PRESET_MODES = "preset_modes" +# mypy: disallow-any-generics + class NotValidPresetModeError(ValueError): """Exception class when the preset_mode in not in the preset_modes list.""" @@ -89,7 +91,7 @@ def is_on(hass: HomeAssistant, entity_id: str) -> bool: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Expose fan control via statemachine and services.""" - component = hass.data[DOMAIN] = EntityComponent( + component = hass.data[DOMAIN] = EntityComponent[FanEntity]( _LOGGER, DOMAIN, hass, SCAN_INTERVAL ) @@ -163,13 +165,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[FanEntity] = hass.data[DOMAIN] return await component.async_setup_entry(entry) async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[FanEntity] = hass.data[DOMAIN] return await component.async_unload_entry(entry) diff --git a/homeassistant/components/geo_location/__init__.py b/homeassistant/components/geo_location/__init__.py index cb737b896eb..af64443ca28 100644 --- a/homeassistant/components/geo_location/__init__.py +++ b/homeassistant/components/geo_location/__init__.py @@ -27,10 +27,12 @@ ENTITY_ID_FORMAT = DOMAIN + ".{}" SCAN_INTERVAL = timedelta(seconds=60) +# mypy: disallow-any-generics + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Geolocation component.""" - component = hass.data[DOMAIN] = EntityComponent( + component = hass.data[DOMAIN] = EntityComponent[GeolocationEvent]( _LOGGER, DOMAIN, hass, SCAN_INTERVAL ) await component.async_setup(config) @@ -39,13 +41,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[GeolocationEvent] = hass.data[DOMAIN] return await component.async_setup_entry(entry) async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[GeolocationEvent] = hass.data[DOMAIN] return await component.async_unload_entry(entry) diff --git a/homeassistant/components/humidifier/__init__.py b/homeassistant/components/humidifier/__init__.py index 051e326fa53..1077e133b3a 100644 --- a/homeassistant/components/humidifier/__init__.py +++ b/homeassistant/components/humidifier/__init__.py @@ -68,6 +68,8 @@ DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.Coerce(HumidifierDeviceClass)) # use the HumidifierDeviceClass enum instead. DEVICE_CLASSES = [cls.value for cls in HumidifierDeviceClass] +# mypy: disallow-any-generics + @bind_hass def is_on(hass, entity_id): @@ -80,7 +82,7 @@ def is_on(hass, entity_id): async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up humidifier devices.""" - component = hass.data[DOMAIN] = EntityComponent( + component = hass.data[DOMAIN] = EntityComponent[HumidifierEntity]( _LOGGER, DOMAIN, hass, SCAN_INTERVAL ) await component.async_setup(config) @@ -109,13 +111,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[HumidifierEntity] = hass.data[DOMAIN] return await component.async_setup_entry(entry) async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[HumidifierEntity] = hass.data[DOMAIN] return await component.async_unload_entry(entry) diff --git a/homeassistant/components/lock/__init__.py b/homeassistant/components/lock/__init__.py index b94cd33a015..d241d57e128 100644 --- a/homeassistant/components/lock/__init__.py +++ b/homeassistant/components/lock/__init__.py @@ -60,10 +60,12 @@ SUPPORT_OPEN = 1 PROP_TO_ATTR = {"changed_by": ATTR_CHANGED_BY, "code_format": ATTR_CODE_FORMAT} +# mypy: disallow-any-generics + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Track states and offer events for locks.""" - component = hass.data[DOMAIN] = EntityComponent( + component = hass.data[DOMAIN] = EntityComponent[LockEntity]( _LOGGER, DOMAIN, hass, SCAN_INTERVAL ) @@ -84,13 +86,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[LockEntity] = hass.data[DOMAIN] return await component.async_setup_entry(entry) async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[LockEntity] = hass.data[DOMAIN] return await component.async_unload_entry(entry) diff --git a/homeassistant/components/scene/__init__.py b/homeassistant/components/scene/__init__.py index 813f5138ed1..3c8adbd0502 100644 --- a/homeassistant/components/scene/__init__.py +++ b/homeassistant/components/scene/__init__.py @@ -56,10 +56,12 @@ PLATFORM_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) +# mypy: disallow-any-generics + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the scenes.""" - component = hass.data[DOMAIN] = EntityComponent( + component = hass.data[DOMAIN] = EntityComponent[Scene]( logging.getLogger(__name__), DOMAIN, hass ) @@ -77,13 +79,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[Scene] = hass.data[DOMAIN] return await component.async_setup_entry(entry) async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[Scene] = hass.data[DOMAIN] return await component.async_unload_entry(entry) diff --git a/homeassistant/components/select/__init__.py b/homeassistant/components/select/__init__.py index 9a7bfa62cdf..56ac28ae39e 100644 --- a/homeassistant/components/select/__init__.py +++ b/homeassistant/components/select/__init__.py @@ -29,10 +29,12 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) _LOGGER = logging.getLogger(__name__) +# mypy: disallow-any-generics + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up Select entities.""" - component = hass.data[DOMAIN] = EntityComponent( + component = hass.data[DOMAIN] = EntityComponent[SelectEntity]( _LOGGER, DOMAIN, hass, SCAN_INTERVAL ) await component.async_setup(config) @@ -56,13 +58,13 @@ async def async_select_option(entity: SelectEntity, service_call: ServiceCall) - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[SelectEntity] = hass.data[DOMAIN] return await component.async_setup_entry(entry) async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[SelectEntity] = hass.data[DOMAIN] return await component.async_unload_entry(entry) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index d4f4aa3c37d..a6bfd4189f8 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -226,10 +226,12 @@ VALID_UNITS: dict[str, tuple[str, ...]] = { SensorDeviceClass.TEMPERATURE: temperature_util.VALID_UNITS, } +# mypy: disallow-any-generics + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Track states and offer events for sensors.""" - component = hass.data[DOMAIN] = EntityComponent( + component = hass.data[DOMAIN] = EntityComponent[SensorEntity]( _LOGGER, DOMAIN, hass, SCAN_INTERVAL ) @@ -239,13 +241,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - component = cast(EntityComponent, hass.data[DOMAIN]) + component: EntityComponent[SensorEntity] = hass.data[DOMAIN] return await component.async_setup_entry(entry) async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - component = cast(EntityComponent, hass.data[DOMAIN]) + component: EntityComponent[SensorEntity] = hass.data[DOMAIN] return await component.async_unload_entry(entry) diff --git a/homeassistant/components/siren/__init__.py b/homeassistant/components/siren/__init__.py index 42dfd0ed3df..920865c41f3 100644 --- a/homeassistant/components/siren/__init__.py +++ b/homeassistant/components/siren/__init__.py @@ -53,6 +53,9 @@ class SirenTurnOnServiceParameters(TypedDict, total=False): volume_level: float +# mypy: disallow-any-generics + + def process_turn_on_params( siren: SirenEntity, params: SirenTurnOnServiceParameters ) -> SirenTurnOnServiceParameters: @@ -99,7 +102,7 @@ def process_turn_on_params( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up siren devices.""" - component = hass.data[DOMAIN] = EntityComponent( + component = hass.data[DOMAIN] = EntityComponent[SirenEntity]( _LOGGER, DOMAIN, hass, SCAN_INTERVAL ) await component.async_setup(config) @@ -138,13 +141,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[SirenEntity] = hass.data[DOMAIN] return await component.async_setup_entry(entry) async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[SirenEntity] = hass.data[DOMAIN] return await component.async_unload_entry(entry) diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index 7ef4a754b55..7387685187a 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -51,6 +51,8 @@ DEVICE_CLASSES = [cls.value for cls in SwitchDeviceClass] DEVICE_CLASS_OUTLET = SwitchDeviceClass.OUTLET.value DEVICE_CLASS_SWITCH = SwitchDeviceClass.SWITCH.value +# mypy: disallow-any-generics + @bind_hass def is_on(hass: HomeAssistant, entity_id: str) -> bool: @@ -63,7 +65,7 @@ def is_on(hass: HomeAssistant, entity_id: str) -> bool: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Track states and offer events for switches.""" - component = hass.data[DOMAIN] = EntityComponent( + component = hass.data[DOMAIN] = EntityComponent[SwitchEntity]( _LOGGER, DOMAIN, hass, SCAN_INTERVAL ) await component.async_setup(config) @@ -77,13 +79,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[SwitchEntity] = hass.data[DOMAIN] return await component.async_setup_entry(entry) async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[SwitchEntity] = hass.data[DOMAIN] return await component.async_unload_entry(entry) diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index dc5c9cc225c..2942078a875 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -110,6 +110,8 @@ SUPPORT_MAP = 2048 SUPPORT_STATE = 4096 SUPPORT_START = 8192 +# mypy: disallow-any-generics + @bind_hass def is_on(hass: HomeAssistant, entity_id: str) -> bool: @@ -119,7 +121,7 @@ def is_on(hass: HomeAssistant, entity_id: str) -> bool: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the vacuum component.""" - component = hass.data[DOMAIN] = EntityComponent( + component = hass.data[DOMAIN] = EntityComponent[_BaseVacuum]( _LOGGER, DOMAIN, hass, SCAN_INTERVAL ) @@ -158,13 +160,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[_BaseVacuum] = hass.data[DOMAIN] return await component.async_setup_entry(entry) async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[_BaseVacuum] = hass.data[DOMAIN] return await component.async_unload_entry(entry) diff --git a/homeassistant/components/water_heater/__init__.py b/homeassistant/components/water_heater/__init__.py index c7ece01a93d..8881b3babd4 100644 --- a/homeassistant/components/water_heater/__init__.py +++ b/homeassistant/components/water_heater/__init__.py @@ -107,10 +107,12 @@ SET_OPERATION_MODE_SCHEMA = vol.Schema( } ) +# mypy: disallow-any-generics + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up water_heater devices.""" - component = hass.data[DOMAIN] = EntityComponent( + component = hass.data[DOMAIN] = EntityComponent[WaterHeaterEntity]( _LOGGER, DOMAIN, hass, SCAN_INTERVAL ) await component.async_setup(config) @@ -138,13 +140,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[WaterHeaterEntity] = hass.data[DOMAIN] return await component.async_setup_entry(entry) async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[WaterHeaterEntity] = hass.data[DOMAIN] return await component.async_unload_entry(entry) diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index 9f8c1b9c5ce..b28cd143b20 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -141,6 +141,8 @@ VALID_UNITS: dict[str, tuple[str, ...]] = { ATTR_WEATHER_WIND_SPEED_UNIT: VALID_UNITS_WIND_SPEED, } +# mypy: disallow-any-generics + def round_temperature(temperature: float | None, precision: float) -> float | None: """Convert temperature into preferred precision for display.""" @@ -183,7 +185,7 @@ class Forecast(TypedDict, total=False): async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the weather component.""" - component = hass.data[DOMAIN] = EntityComponent( + component = hass.data[DOMAIN] = EntityComponent[WeatherEntity]( _LOGGER, DOMAIN, hass, SCAN_INTERVAL ) await component.async_setup(config) @@ -192,13 +194,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[WeatherEntity] = hass.data[DOMAIN] return await component.async_setup_entry(entry) async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[WeatherEntity] = hass.data[DOMAIN] return await component.async_unload_entry(entry) From 51b01fcd80d48776f72789b13b034c21b7d22b74 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 17 Sep 2022 19:38:35 +0200 Subject: [PATCH 490/955] Bump smhi-pkg to 1.0.16 (#78639) --- homeassistant/components/smhi/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/smhi/manifest.json b/homeassistant/components/smhi/manifest.json index d1030cb7868..7f47dbfce33 100644 --- a/homeassistant/components/smhi/manifest.json +++ b/homeassistant/components/smhi/manifest.json @@ -3,7 +3,7 @@ "name": "SMHI", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/smhi", - "requirements": ["smhi-pkg==1.0.15"], + "requirements": ["smhi-pkg==1.0.16"], "codeowners": ["@gjohansson-ST"], "iot_class": "cloud_polling", "loggers": ["smhi"] diff --git a/requirements_all.txt b/requirements_all.txt index ee14e5ce731..9df89439043 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2254,7 +2254,7 @@ slixmpp==1.8.2 smart-meter-texas==0.4.7 # homeassistant.components.smhi -smhi-pkg==1.0.15 +smhi-pkg==1.0.16 # homeassistant.components.snapcast snapcast==2.1.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ade27067dc7..733342febdb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1539,7 +1539,7 @@ slackclient==2.5.0 smart-meter-texas==0.4.7 # homeassistant.components.smhi -smhi-pkg==1.0.15 +smhi-pkg==1.0.16 # homeassistant.components.sonos soco==0.28.0 From 6e4a0b9fdceba5ec4dc0b701e1fc564f4a482e21 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 17 Sep 2022 12:40:10 -0500 Subject: [PATCH 491/955] Switch recorder to use async_timeout instead of asyncio.wait_for (#78607) --- homeassistant/components/recorder/core.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 8706bc1c7a8..88b63e93733 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -13,6 +13,7 @@ import threading import time from typing import Any, TypeVar, cast +import async_timeout from awesomeversion import AwesomeVersion from lru import LRU # pylint: disable=no-name-in-module from sqlalchemy import create_engine, event as sqlalchemy_event, exc, func, select @@ -1030,7 +1031,8 @@ class Recorder(threading.Thread): task = DatabaseLockTask(database_locked, threading.Event(), False) self.queue_task(task) try: - await asyncio.wait_for(database_locked.wait(), timeout=DB_LOCK_TIMEOUT) + async with async_timeout.timeout(DB_LOCK_TIMEOUT): + await database_locked.wait() except asyncio.TimeoutError as err: task.database_unlock.set() raise TimeoutError( From 01acc3d1e5771048a13727213b58509a885b04de Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sat, 17 Sep 2022 13:43:35 -0400 Subject: [PATCH 492/955] Fix zwave_js update entity startup state (#78563) * Fix update entity startup state * Only write state if there is a change * Add test to show that when an existing entity gets recreated, skipped version does not reset * Remove unused blocks * Update homeassistant/components/zwave_js/update.py Co-authored-by: Franck Nijhof Co-authored-by: Franck Nijhof --- homeassistant/components/zwave_js/update.py | 34 ++++++----- tests/components/zwave_js/test_update.py | 63 ++++++++++++++++++++- 2 files changed, 82 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/zwave_js/update.py b/homeassistant/components/zwave_js/update.py index 022c4dc3f3c..08a5b90b42d 100644 --- a/homeassistant/components/zwave_js/update.py +++ b/homeassistant/components/zwave_js/update.py @@ -99,7 +99,7 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): self._attr_name = "Firmware" self._base_unique_id = get_valueless_base_unique_id(driver, node) self._attr_unique_id = f"{self._base_unique_id}.firmware_update" - self._attr_installed_version = self._attr_latest_version = node.firmware_version + self._attr_installed_version = node.firmware_version # device may not be precreated in main handler yet self._attr_device_info = get_device_info(driver, node) @@ -187,20 +187,26 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): err, ) else: - if available_firmware_updates: - self._latest_version_firmware = latest_firmware = max( - available_firmware_updates, - key=lambda x: AwesomeVersion(x.version), + # If we have an available firmware update that is a higher version than + # what's on the node, we should advertise it, otherwise the installed + # version is the latest. + if ( + available_firmware_updates + and ( + latest_firmware := max( + available_firmware_updates, + key=lambda x: AwesomeVersion(x.version), + ) ) - - # If we have an available firmware update that is a higher version than - # what's on the node, we should advertise it, otherwise there is - # nothing to do. - new_version = latest_firmware.version - current_version = self.node.firmware_version - if AwesomeVersion(new_version) > AwesomeVersion(current_version): - self._attr_latest_version = new_version - self.async_write_ha_state() + and AwesomeVersion(latest_firmware.version) + > AwesomeVersion(self.node.firmware_version) + ): + self._latest_version_firmware = latest_firmware + self._attr_latest_version = latest_firmware.version + self.async_write_ha_state() + elif self._attr_latest_version != self._attr_installed_version: + self._attr_latest_version = self._attr_installed_version + self.async_write_ha_state() finally: self._poll_unsub = async_call_later( self.hass, timedelta(days=1), self._async_update diff --git a/tests/components/zwave_js/test_update.py b/tests/components/zwave_js/test_update.py index 0b567d93106..a5b3059e705 100644 --- a/tests/components/zwave_js/test_update.py +++ b/tests/components/zwave_js/test_update.py @@ -13,8 +13,10 @@ from homeassistant.components.update.const import ( ATTR_INSTALLED_VERSION, ATTR_LATEST_VERSION, ATTR_RELEASE_URL, + ATTR_SKIPPED_VERSION, DOMAIN as UPDATE_DOMAIN, SERVICE_INSTALL, + SERVICE_SKIP, ) from homeassistant.components.zwave_js.const import DOMAIN, SERVICE_REFRESH_VALUE from homeassistant.components.zwave_js.helpers import get_valueless_base_unique_id @@ -64,7 +66,6 @@ async def test_update_entity_states( ): """Test update entity states.""" ws_client = await hass_ws_client(hass) - await hass.async_block_till_done() assert hass.states.get(UPDATE_ENTITY).state == STATE_OFF @@ -453,3 +454,63 @@ async def test_update_entity_install_failed( # validate that the install task failed with pytest.raises(HomeAssistantError): await install_task + + +async def test_update_entity_reload( + hass, + client, + climate_radio_thermostat_ct100_plus_different_endpoints, + integration, +): + """Test update entity maintains state after reload.""" + assert hass.states.get(UPDATE_ENTITY).state == STATE_OFF + + client.async_send_command.return_value = {"updates": []} + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(days=1)) + await hass.async_block_till_done() + + state = hass.states.get(UPDATE_ENTITY) + assert state + assert state.state == STATE_OFF + + client.async_send_command.return_value = FIRMWARE_UPDATES + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(days=2)) + await hass.async_block_till_done() + + state = hass.states.get(UPDATE_ENTITY) + assert state + assert state.state == STATE_ON + attrs = state.attributes + assert not attrs[ATTR_AUTO_UPDATE] + assert attrs[ATTR_INSTALLED_VERSION] == "10.7" + assert not attrs[ATTR_IN_PROGRESS] + assert attrs[ATTR_LATEST_VERSION] == "11.2.4" + assert attrs[ATTR_RELEASE_URL] is None + + await hass.services.async_call( + UPDATE_DOMAIN, + SERVICE_SKIP, + { + ATTR_ENTITY_ID: UPDATE_ENTITY, + }, + blocking=True, + ) + + state = hass.states.get(UPDATE_ENTITY) + assert state + assert state.state == STATE_OFF + assert state.attributes[ATTR_SKIPPED_VERSION] == "11.2.4" + + await hass.config_entries.async_reload(integration.entry_id) + await hass.async_block_till_done() + + # Trigger another update and make sure the skipped version is still skipped + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(days=4)) + await hass.async_block_till_done() + + state = hass.states.get(UPDATE_ENTITY) + assert state + assert state.state == STATE_OFF + assert state.attributes[ATTR_SKIPPED_VERSION] == "11.2.4" From 8cae33a730c7307225d0fa7741c0c4c2634edd6e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 17 Sep 2022 12:44:24 -0500 Subject: [PATCH 493/955] Fix rachio not being able to be ignored (#78636) Fixes #77272 --- .../components/rachio/config_flow.py | 1 + tests/components/rachio/test_config_flow.py | 26 +++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/homeassistant/components/rachio/config_flow.py b/homeassistant/components/rachio/config_flow.py index 00f31003ba6..477abcb3694 100644 --- a/homeassistant/components/rachio/config_flow.py +++ b/homeassistant/components/rachio/config_flow.py @@ -90,6 +90,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id( discovery_info.properties[zeroconf.ATTR_PROPERTIES_ID] ) + self._abort_if_unique_id_configured() return await self.async_step_user() @staticmethod diff --git a/tests/components/rachio/test_config_flow.py b/tests/components/rachio/test_config_flow.py index 00445f23c01..07965fca693 100644 --- a/tests/components/rachio/test_config_flow.py +++ b/tests/components/rachio/test_config_flow.py @@ -149,3 +149,29 @@ async def test_form_homekit(hass): ) assert result["type"] == "abort" assert result["reason"] == "already_configured" + + +async def test_form_homekit_ignored(hass): + """Test that we abort from homekit if rachio is ignored.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="AA:BB:CC:DD:EE:FF", + source=config_entries.SOURCE_IGNORE, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_HOMEKIT}, + data=zeroconf.ZeroconfServiceInfo( + host="mock_host", + addresses=["mock_host"], + hostname="mock_hostname", + name="mock_name", + port=None, + properties={zeroconf.ATTR_PROPERTIES_ID: "AA:BB:CC:DD:EE:FF"}, + type="mock_type", + ), + ) + assert result["type"] == "abort" + assert result["reason"] == "already_configured" From 943fe657c577e4a1018e04fd6ff580bb9e46f10a Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Sat, 17 Sep 2022 11:50:47 -0600 Subject: [PATCH 494/955] Add additional status sensor state strings for Litter-Robot 4 (#78652) --- homeassistant/components/litterrobot/strings.sensor.json | 3 +++ .../components/litterrobot/translations/sensor.en.json | 3 +++ 2 files changed, 6 insertions(+) diff --git a/homeassistant/components/litterrobot/strings.sensor.json b/homeassistant/components/litterrobot/strings.sensor.json index d9ad141cf21..0c901704b02 100644 --- a/homeassistant/components/litterrobot/strings.sensor.json +++ b/homeassistant/components/litterrobot/strings.sensor.json @@ -4,6 +4,7 @@ "br": "Bonnet Removed", "ccc": "Clean Cycle Complete", "ccp": "Clean Cycle In Progress", + "cd": "Cat Detected", "csf": "Cat Sensor Fault", "csi": "Cat Sensor Interrupted", "cst": "Cat Sensor Timing", @@ -19,6 +20,8 @@ "otf": "Over Torque Fault", "p": "[%key:common::state::paused%]", "pd": "Pinch Detect", + "pwrd": "Powering Down", + "pwru": "Powering Up", "rdy": "Ready", "scf": "Cat Sensor Fault At Startup", "sdf": "Drawer Full At Startup", diff --git a/homeassistant/components/litterrobot/translations/sensor.en.json b/homeassistant/components/litterrobot/translations/sensor.en.json index 5491a6a835f..65cf08e980e 100644 --- a/homeassistant/components/litterrobot/translations/sensor.en.json +++ b/homeassistant/components/litterrobot/translations/sensor.en.json @@ -4,6 +4,7 @@ "br": "Bonnet Removed", "ccc": "Clean Cycle Complete", "ccp": "Clean Cycle In Progress", + "cd": "Cat Detected", "csf": "Cat Sensor Fault", "csi": "Cat Sensor Interrupted", "cst": "Cat Sensor Timing", @@ -19,6 +20,8 @@ "otf": "Over Torque Fault", "p": "Paused", "pd": "Pinch Detect", + "pwrd": "Powering Down", + "pwru": "Powering Up", "rdy": "Ready", "scf": "Cat Sensor Fault At Startup", "sdf": "Drawer Full At Startup", From 74f7ae409be003e99abad4b9259477a08a26cc02 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 17 Sep 2022 12:52:28 -0500 Subject: [PATCH 495/955] Add a helpful message to the config_entries.OperationNotAllowed exception (#78631) We only expect this exception to be raised as a result of an implementation problem. When it is raised during production it is currently hard to trace down why its happening See #75835 --- homeassistant/config_entries.py | 10 ++++++++-- tests/test_config_entries.py | 16 +++++++++++----- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index e7960646ecb..8e618deb3d2 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -1039,7 +1039,10 @@ class ConfigEntries: raise UnknownEntry if entry.state is not ConfigEntryState.NOT_LOADED: - raise OperationNotAllowed + raise OperationNotAllowed( + f"The config entry {entry.title} ({entry.domain}) with entry_id {entry.entry_id}" + f" cannot be setup because is already loaded in the {entry.state} state" + ) # Setup Component if not set up yet if entry.domain in self.hass.config.components: @@ -1061,7 +1064,10 @@ class ConfigEntries: raise UnknownEntry if not entry.state.recoverable: - raise OperationNotAllowed + raise OperationNotAllowed( + f"The config entry {entry.title} ({entry.domain}) with entry_id " + f"{entry.entry_id} cannot be unloaded because it is not in a recoverable state ({entry.state})" + ) return await entry.async_unload(self.hass) diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index b923e37b636..d2d4ffe1134 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -1141,7 +1141,7 @@ async def test_entry_setup_invalid_state(hass, manager, state): MockModule("comp", async_setup=mock_setup, async_setup_entry=mock_setup_entry), ) - with pytest.raises(config_entries.OperationNotAllowed): + with pytest.raises(config_entries.OperationNotAllowed, match=str(state)): assert await manager.async_setup(entry.entry_id) assert len(mock_setup.mock_calls) == 0 @@ -1201,7 +1201,7 @@ async def test_entry_unload_invalid_state(hass, manager, state): mock_integration(hass, MockModule("comp", async_unload_entry=async_unload_entry)) - with pytest.raises(config_entries.OperationNotAllowed): + with pytest.raises(config_entries.OperationNotAllowed, match=str(state)): assert await manager.async_unload(entry.entry_id) assert len(async_unload_entry.mock_calls) == 0 @@ -1296,7 +1296,7 @@ async def test_entry_reload_error(hass, manager, state): ), ) - with pytest.raises(config_entries.OperationNotAllowed): + with pytest.raises(config_entries.OperationNotAllowed, match=str(state)): assert await manager.async_reload(entry.entry_id) assert len(async_unload_entry.mock_calls) == 0 @@ -1370,7 +1370,10 @@ async def test_entry_disable_without_reload_support(hass, manager): assert entry.state is config_entries.ConfigEntryState.FAILED_UNLOAD # Enable - with pytest.raises(config_entries.OperationNotAllowed): + with pytest.raises( + config_entries.OperationNotAllowed, + match=str(config_entries.ConfigEntryState.FAILED_UNLOAD), + ): await manager.async_set_disabled_by(entry.entry_id, None) assert len(async_setup.mock_calls) == 0 assert len(async_setup_entry.mock_calls) == 0 @@ -3270,7 +3273,10 @@ async def test_disallow_entry_reload_with_setup_in_progresss(hass, manager): ) entry.add_to_hass(hass) - with pytest.raises(config_entries.OperationNotAllowed): + with pytest.raises( + config_entries.OperationNotAllowed, + match=str(config_entries.ConfigEntryState.SETUP_IN_PROGRESS), + ): assert await manager.async_reload(entry.entry_id) assert entry.state is config_entries.ConfigEntryState.SETUP_IN_PROGRESS From b51fd7f6886508130edf07e342ff5e630930f678 Mon Sep 17 00:00:00 2001 From: Kevin Addeman Date: Sat, 17 Sep 2022 14:26:04 -0400 Subject: [PATCH 496/955] Fix lutron_caseta get_triggers() raising error for non-button devices (caseta and ra3/hwqsx) (#78397) --- .../lutron_caseta/device_trigger.py | 3 +- .../lutron_caseta/test_device_trigger.py | 29 ++++++++++++++----- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/lutron_caseta/device_trigger.py b/homeassistant/components/lutron_caseta/device_trigger.py index b355c3dcc3f..b9fe89edf7f 100644 --- a/homeassistant/components/lutron_caseta/device_trigger.py +++ b/homeassistant/components/lutron_caseta/device_trigger.py @@ -405,7 +405,8 @@ async def async_get_triggers( triggers = [] if not (device := get_button_device_by_dr_id(hass, device_id)): - raise InvalidDeviceAutomationConfig(f"Device not found: {device_id}") + # Check if device is a valid button device. Return empty if not. + return [] valid_buttons = DEVICE_TYPE_SUBTYPE_MAP_TO_LEAP.get( _lutron_model_to_device_type(device["model"], device["type"]), {} diff --git a/tests/components/lutron_caseta/test_device_trigger.py b/tests/components/lutron_caseta/test_device_trigger.py index b8c655a23bd..161f5cf357f 100644 --- a/tests/components/lutron_caseta/test_device_trigger.py +++ b/tests/components/lutron_caseta/test_device_trigger.py @@ -5,9 +5,6 @@ import pytest from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType -from homeassistant.components.device_automation.exceptions import ( - InvalidDeviceAutomationConfig, -) from homeassistant.components.lutron_caseta import ( ATTR_ACTION, ATTR_AREA_NAME, @@ -140,6 +137,7 @@ async def test_get_triggers(hass, device_reg): triggers = await async_get_device_automations( hass, DeviceAutomationType.TRIGGER, device_id ) + assert_lists_same(triggers, expected_triggers) @@ -152,10 +150,27 @@ async def test_get_triggers_for_invalid_device_id(hass, device_reg): connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) - with pytest.raises(InvalidDeviceAutomationConfig): - await async_get_device_automations( - hass, DeviceAutomationType.TRIGGER, invalid_device.id - ) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, invalid_device.id + ) + + assert triggers == [] + + +async def test_get_triggers_for_non_button_device(hass, device_reg): + """Test error raised for invalid lutron device_id.""" + config_entry_id = await _async_setup_lutron_with_picos(hass, device_reg) + + invalid_device = device_reg.async_get_or_create( + config_entry_id=config_entry_id, + identifiers={(DOMAIN, "invdevserial")}, + ) + + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, invalid_device.id + ) + + assert triggers == [] async def test_if_fires_on_button_event(hass, calls, device_reg): From 391d8954260608e7d16e04bbdbd0d08c9a9d5456 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 17 Sep 2022 21:23:16 +0200 Subject: [PATCH 497/955] Enable disallow-any-generics in media-player (#78498) Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> --- .../components/media_player/__init__.py | 50 +++++++++++++------ 1 file changed, 34 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 408f33bbecc..8c89288b41d 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -12,7 +12,7 @@ import hashlib from http import HTTPStatus import logging import secrets -from typing import Any, cast, final +from typing import Any, Final, TypedDict, final from urllib.parse import quote, urlparse from aiohttp import web @@ -136,12 +136,11 @@ _LOGGER = logging.getLogger(__name__) ENTITY_ID_FORMAT = DOMAIN + ".{}" -CACHE_IMAGES = "images" -CACHE_MAXSIZE = "maxsize" -CACHE_LOCK = "lock" -CACHE_URL = "url" -CACHE_CONTENT = "content" -ENTITY_IMAGE_CACHE = {CACHE_IMAGES: collections.OrderedDict(), CACHE_MAXSIZE: 16} +CACHE_IMAGES: Final = "images" +CACHE_MAXSIZE: Final = "maxsize" +CACHE_LOCK: Final = "lock" +CACHE_URL: Final = "url" +CACHE_CONTENT: Final = "content" SCAN_INTERVAL = dt.timedelta(seconds=10) @@ -214,6 +213,25 @@ ATTR_TO_PROPERTY = [ ATTR_MEDIA_REPEAT, ] +# mypy: disallow-any-generics + + +class _CacheImage(TypedDict, total=False): + """Class to hold a cached image.""" + + lock: asyncio.Lock + content: tuple[bytes | None, str | None] + + +class _ImageCache(TypedDict): + """Class to hold a cached image.""" + + images: collections.OrderedDict[str, _CacheImage] + maxsize: int + + +_ENTITY_IMAGE_CACHE = _ImageCache(images=collections.OrderedDict(), maxsize=16) + @bind_hass def is_on(hass: HomeAssistant, entity_id: str | None = None) -> bool: @@ -248,7 +266,7 @@ def _rename_keys(**keys: Any) -> Callable[[dict[str, Any]], dict[str, Any]]: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Track states and offer events for media_players.""" - component = hass.data[DOMAIN] = EntityComponent( + component = hass.data[DOMAIN] = EntityComponent[MediaPlayerEntity]( logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL ) @@ -420,13 +438,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[MediaPlayerEntity] = hass.data[DOMAIN] return await component.async_setup_entry(entry) async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[MediaPlayerEntity] = hass.data[DOMAIN] return await component.async_unload_entry(entry) @@ -1060,8 +1078,8 @@ class MediaPlayerEntity(Entity): Images are cached in memory (the images are typically 10-100kB in size). """ - cache_images = cast(collections.OrderedDict, ENTITY_IMAGE_CACHE[CACHE_IMAGES]) - cache_maxsize = cast(int, ENTITY_IMAGE_CACHE[CACHE_MAXSIZE]) + cache_images = _ENTITY_IMAGE_CACHE[CACHE_IMAGES] + cache_maxsize = _ENTITY_IMAGE_CACHE[CACHE_MAXSIZE] if urlparse(url).hostname is None: url = f"{get_url(self.hass)}{url}" @@ -1071,7 +1089,7 @@ class MediaPlayerEntity(Entity): async with cache_images[url][CACHE_LOCK]: if CACHE_CONTENT in cache_images[url]: - return cache_images[url][CACHE_CONTENT] # type:ignore[no-any-return] + return cache_images[url][CACHE_CONTENT] (content, content_type) = await self._async_fetch_image(url) @@ -1120,7 +1138,7 @@ class MediaPlayerImageView(HomeAssistantView): + "/browse_media/{media_content_type}/{media_content_id:.+}", ] - def __init__(self, component: EntityComponent) -> None: + def __init__(self, component: EntityComponent[MediaPlayerEntity]) -> None: """Initialize a media player view.""" self.component = component @@ -1191,8 +1209,8 @@ async def websocket_browse_media( To use, media_player integrations can implement MediaPlayerEntity.async_browse_media() """ - component = hass.data[DOMAIN] - player: MediaPlayerEntity | None = component.get_entity(msg["entity_id"]) + component: EntityComponent[MediaPlayerEntity] = hass.data[DOMAIN] + player = component.get_entity(msg["entity_id"]) if player is None: connection.send_error(msg["id"], "entity_not_found", "Entity not found") From 1f410e884abc948f21505227902e689913bdc97a Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sat, 17 Sep 2022 21:43:42 +0200 Subject: [PATCH 498/955] Make hass.data["mqtt"] an instance of a DataClass (#77972) * Use dataclass to reference hass.data globals * Add discovery_registry_hooks to dataclass * Move discovery registry hooks to dataclass * Add device triggers to dataclass * Cleanup DEVICE_TRIGGERS const * Add last_discovery to data_class * Simplify typing for class `Subscription` * Follow up on comment * Redo suggested typing change to sasisfy mypy * Restore typing * Add mypy version to CI check logging * revert changes to ci.yaml * Add docstr for protocol Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> * Mypy update after merging #78399 * Remove mypy ignore * Correct return type Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> --- homeassistant/components/mqtt/__init__.py | 104 ++++++++---------- homeassistant/components/mqtt/client.py | 45 +++++--- homeassistant/components/mqtt/config_flow.py | 25 +++-- homeassistant/components/mqtt/const.py | 8 -- .../components/mqtt/device_trigger.py | 35 +++--- homeassistant/components/mqtt/diagnostics.py | 2 +- homeassistant/components/mqtt/discovery.py | 15 ++- homeassistant/components/mqtt/mixins.py | 61 ++++++---- tests/common.py | 2 +- tests/components/mqtt/test_config_flow.py | 2 +- tests/components/mqtt/test_discovery.py | 2 +- tests/components/mqtt/test_init.py | 10 +- 12 files changed, 174 insertions(+), 137 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 842e5b6405f..315f116ed92 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -20,13 +20,7 @@ from homeassistant.const import ( CONF_USERNAME, SERVICE_RELOAD, ) -from homeassistant.core import ( - CALLBACK_TYPE, - HassJob, - HomeAssistant, - ServiceCall, - callback, -) +from homeassistant.core import HassJob, HomeAssistant, ServiceCall, callback from homeassistant.exceptions import TemplateError, Unauthorized from homeassistant.helpers import ( config_validation as cv, @@ -71,15 +65,7 @@ from .const import ( # noqa: F401 CONF_TLS_VERSION, CONF_TOPIC, CONF_WILL_MESSAGE, - CONFIG_ENTRY_IS_SETUP, DATA_MQTT, - DATA_MQTT_CONFIG, - DATA_MQTT_DISCOVERY_REGISTRY_HOOKS, - DATA_MQTT_RELOAD_DISPATCHERS, - DATA_MQTT_RELOAD_ENTRY, - DATA_MQTT_RELOAD_NEEDED, - DATA_MQTT_SUBSCRIPTIONS_TO_RESTORE, - DATA_MQTT_UPDATED_CONFIG, DEFAULT_ENCODING, DEFAULT_QOS, DEFAULT_RETAIN, @@ -89,7 +75,7 @@ from .const import ( # noqa: F401 PLATFORMS, RELOADABLE_PLATFORMS, ) -from .mixins import async_discover_yaml_entities +from .mixins import MqttData, async_discover_yaml_entities from .models import ( # noqa: F401 MqttCommandTemplate, MqttValueTemplate, @@ -177,6 +163,8 @@ async def _async_setup_discovery( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Start the MQTT protocol service.""" + mqtt_data: MqttData = hass.data.setdefault(DATA_MQTT, MqttData()) + conf: ConfigType | None = config.get(DOMAIN) websocket_api.async_register_command(hass, websocket_subscribe) @@ -185,7 +173,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if conf: conf = dict(conf) - hass.data[DATA_MQTT_CONFIG] = conf + mqtt_data.config = conf if (mqtt_entry_status := mqtt_config_entry_enabled(hass)) is None: # Create an import flow if the user has yaml configured entities etc. @@ -197,12 +185,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, data={}, ) - hass.data[DATA_MQTT_RELOAD_NEEDED] = True + mqtt_data.reload_needed = True elif mqtt_entry_status is False: _LOGGER.info( "MQTT will be not available until the config entry is enabled", ) - hass.data[DATA_MQTT_RELOAD_NEEDED] = True + mqtt_data.reload_needed = True return True @@ -260,33 +248,34 @@ async def _async_config_entry_updated(hass: HomeAssistant, entry: ConfigEntry) - Causes for this is config entry options changing. """ - mqtt_client = hass.data[DATA_MQTT] + mqtt_data: MqttData = hass.data[DATA_MQTT] + assert (client := mqtt_data.client) is not None - if (conf := hass.data.get(DATA_MQTT_CONFIG)) is None: + if (conf := mqtt_data.config) is None: conf = CONFIG_SCHEMA_BASE(dict(entry.data)) - mqtt_client.conf = _merge_extended_config(entry, conf) - await mqtt_client.async_disconnect() - mqtt_client.init_client() - await mqtt_client.async_connect() + mqtt_data.config = _merge_extended_config(entry, conf) + await client.async_disconnect() + client.init_client() + await client.async_connect() await discovery.async_stop(hass) - if mqtt_client.conf.get(CONF_DISCOVERY): - await _async_setup_discovery(hass, mqtt_client.conf, entry) + if client.conf.get(CONF_DISCOVERY): + await _async_setup_discovery(hass, cast(ConfigType, mqtt_data.config), entry) async def async_fetch_config(hass: HomeAssistant, entry: ConfigEntry) -> dict | None: """Fetch fresh MQTT yaml config from the hass config when (re)loading the entry.""" - if DATA_MQTT_RELOAD_ENTRY in hass.data: + mqtt_data: MqttData = hass.data[DATA_MQTT] + if mqtt_data.reload_entry: hass_config = await conf_util.async_hass_config_yaml(hass) - mqtt_config = CONFIG_SCHEMA_BASE(hass_config.get(DOMAIN, {})) - hass.data[DATA_MQTT_CONFIG] = mqtt_config + mqtt_data.config = CONFIG_SCHEMA_BASE(hass_config.get(DOMAIN, {})) # Remove unknown keys from config entry data _filter_entry_config(hass, entry) # Merge basic configuration, and add missing defaults for basic options - _merge_basic_config(hass, entry, hass.data.get(DATA_MQTT_CONFIG, {})) + _merge_basic_config(hass, entry, mqtt_data.config or {}) # Bail out if broker setting is missing if CONF_BROKER not in entry.data: _LOGGER.error("MQTT broker is not configured, please configure it") @@ -294,7 +283,7 @@ async def async_fetch_config(hass: HomeAssistant, entry: ConfigEntry) -> dict | # If user doesn't have configuration.yaml config, generate default values # for options not in config entry data - if (conf := hass.data.get(DATA_MQTT_CONFIG)) is None: + if (conf := mqtt_data.config) is None: conf = CONFIG_SCHEMA_BASE(dict(entry.data)) # User has configuration.yaml config, warn about config entry overrides @@ -317,21 +306,20 @@ async def async_fetch_config(hass: HomeAssistant, entry: ConfigEntry) -> dict | async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Load a config entry.""" + mqtt_data: MqttData = hass.data.setdefault(DATA_MQTT, MqttData()) + # Merge basic configuration, and add missing defaults for basic options if (conf := await async_fetch_config(hass, entry)) is None: # Bail out return False - - hass.data[DATA_MQTT_DISCOVERY_REGISTRY_HOOKS] = {} - hass.data[DATA_MQTT] = MQTT(hass, entry, conf) + mqtt_data.client = MQTT(hass, entry, conf) # Restore saved subscriptions - if DATA_MQTT_SUBSCRIPTIONS_TO_RESTORE in hass.data: - hass.data[DATA_MQTT].subscriptions = hass.data.pop( - DATA_MQTT_SUBSCRIPTIONS_TO_RESTORE - ) + if mqtt_data.subscriptions_to_restore: + mqtt_data.client.subscriptions = mqtt_data.subscriptions_to_restore + mqtt_data.subscriptions_to_restore = [] entry.add_update_listener(_async_config_entry_updated) - await hass.data[DATA_MQTT].async_connect() + await mqtt_data.client.async_connect() async def async_publish_service(call: ServiceCall) -> None: """Handle MQTT publish service calls.""" @@ -380,7 +368,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) return - await hass.data[DATA_MQTT].async_publish(msg_topic, payload, qos, retain) + assert mqtt_data.client is not None and msg_topic is not None + await mqtt_data.client.async_publish(msg_topic, payload, qos, retain) hass.services.async_register( DOMAIN, SERVICE_PUBLISH, async_publish_service, schema=MQTT_PUBLISH_SCHEMA @@ -421,7 +410,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) # setup platforms and discovery - hass.data[CONFIG_ENTRY_IS_SETUP] = set() async def async_setup_reload_service() -> None: """Create the reload service for the MQTT domain.""" @@ -435,7 +423,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Reload the modern yaml platforms config_yaml = await async_integration_yaml_config(hass, DOMAIN) or {} - hass.data[DATA_MQTT_UPDATED_CONFIG] = config_yaml.get(DOMAIN, {}) + mqtt_data.updated_config = config_yaml.get(DOMAIN, {}) await asyncio.gather( *( [ @@ -476,13 +464,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Setup reload service after all platforms have loaded await async_setup_reload_service() # When the entry is reloaded, also reload manual set up items to enable MQTT - if DATA_MQTT_RELOAD_ENTRY in hass.data: - hass.data.pop(DATA_MQTT_RELOAD_ENTRY) + if mqtt_data.reload_entry: + mqtt_data.reload_entry = False reload_manual_setup = True # When the entry was disabled before, reload manual set up items to enable MQTT again - if DATA_MQTT_RELOAD_NEEDED in hass.data: - hass.data.pop(DATA_MQTT_RELOAD_NEEDED) + if mqtt_data.reload_needed: + mqtt_data.reload_needed = False reload_manual_setup = True if reload_manual_setup: @@ -592,7 +580,9 @@ def async_subscribe_connection_status( def is_connected(hass: HomeAssistant) -> bool: """Return if MQTT client is connected.""" - return hass.data[DATA_MQTT].connected + mqtt_data: MqttData = hass.data[DATA_MQTT] + assert mqtt_data.client is not None + return mqtt_data.client.connected async def async_remove_config_entry_device( @@ -608,6 +598,10 @@ async def async_remove_config_entry_device( async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload MQTT dump and publish service when the config entry is unloaded.""" + mqtt_data: MqttData = hass.data[DATA_MQTT] + assert mqtt_data.client is not None + mqtt_client = mqtt_data.client + # Unload publish and dump services. hass.services.async_remove( DOMAIN, @@ -620,7 +614,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Stop the discovery await discovery.async_stop(hass) - mqtt_client: MQTT = hass.data[DATA_MQTT] # Unload the platforms await asyncio.gather( *( @@ -630,26 +623,23 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) await hass.async_block_till_done() # Unsubscribe reload dispatchers - while reload_dispatchers := hass.data.setdefault(DATA_MQTT_RELOAD_DISPATCHERS, []): + while reload_dispatchers := mqtt_data.reload_dispatchers: reload_dispatchers.pop()() - hass.data[CONFIG_ENTRY_IS_SETUP] = set() # Cleanup listeners mqtt_client.cleanup() # Trigger reload manual MQTT items at entry setup if (mqtt_entry_status := mqtt_config_entry_enabled(hass)) is False: # The entry is disabled reload legacy manual items when the entry is enabled again - hass.data[DATA_MQTT_RELOAD_NEEDED] = True + mqtt_data.reload_needed = True elif mqtt_entry_status is True: # The entry is reloaded: # Trigger re-fetching the yaml config at entry setup - hass.data[DATA_MQTT_RELOAD_ENTRY] = True + mqtt_data.reload_entry = True # Reload the legacy yaml platform to make entities unavailable await async_reload_integration_platforms(hass, DOMAIN, RELOADABLE_PLATFORMS) # Cleanup entity registry hooks - registry_hooks: dict[tuple, CALLBACK_TYPE] = hass.data[ - DATA_MQTT_DISCOVERY_REGISTRY_HOOKS - ] + registry_hooks = mqtt_data.discovery_registry_hooks while registry_hooks: registry_hooks.popitem()[1]() # Wait for all ACKs and stop the loop @@ -657,6 +647,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Store remaining subscriptions to be able to restore or reload them # when the entry is set up again if mqtt_client.subscriptions: - hass.data[DATA_MQTT_SUBSCRIPTIONS_TO_RESTORE] = mqtt_client.subscriptions + mqtt_data.subscriptions_to_restore = mqtt_client.subscriptions return True diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index 884e589ba05..28887818133 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -2,7 +2,7 @@ from __future__ import annotations import asyncio -from collections.abc import Awaitable, Callable, Coroutine, Iterable +from collections.abc import Callable, Coroutine, Iterable from functools import lru_cache, partial, wraps import inspect from itertools import groupby @@ -17,6 +17,7 @@ import attr import certifi from paho.mqtt.client import MQTTMessage +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_CLIENT_ID, CONF_PASSWORD, @@ -52,7 +53,6 @@ from .const import ( MQTT_DISCONNECTED, PROTOCOL_31, ) -from .discovery import LAST_DISCOVERY from .models import ( AsyncMessageCallbackType, MessageCallbackType, @@ -68,6 +68,9 @@ if TYPE_CHECKING: # because integrations should be able to optionally rely on MQTT. import paho.mqtt.client as mqtt + from .mixins import MqttData + + _LOGGER = logging.getLogger(__name__) DISCOVERY_COOLDOWN = 2 @@ -97,8 +100,12 @@ async def async_publish( encoding: str | None = DEFAULT_ENCODING, ) -> None: """Publish message to a MQTT topic.""" + # Local import to avoid circular dependencies + # pylint: disable-next=import-outside-toplevel + from .mixins import MqttData - if DATA_MQTT not in hass.data or not mqtt_config_entry_enabled(hass): + mqtt_data: MqttData = hass.data.setdefault(DATA_MQTT, MqttData()) + if mqtt_data.client is None or not mqtt_config_entry_enabled(hass): raise HomeAssistantError( f"Cannot publish to topic '{topic}', MQTT is not enabled" ) @@ -126,11 +133,13 @@ async def async_publish( ) return - await hass.data[DATA_MQTT].async_publish(topic, outgoing_payload, qos, retain) + await mqtt_data.client.async_publish( + topic, outgoing_payload, qos or 0, retain or False + ) AsyncDeprecatedMessageCallbackType = Callable[ - [str, ReceivePayloadType, int], Awaitable[None] + [str, ReceivePayloadType, int], Coroutine[Any, Any, None] ] DeprecatedMessageCallbackType = Callable[[str, ReceivePayloadType, int], None] @@ -175,13 +184,18 @@ async def async_subscribe( | DeprecatedMessageCallbackType | AsyncDeprecatedMessageCallbackType, qos: int = DEFAULT_QOS, - encoding: str | None = "utf-8", + encoding: str | None = DEFAULT_ENCODING, ): """Subscribe to an MQTT topic. Call the return value to unsubscribe. """ - if DATA_MQTT not in hass.data or not mqtt_config_entry_enabled(hass): + # Local import to avoid circular dependencies + # pylint: disable-next=import-outside-toplevel + from .mixins import MqttData + + mqtt_data: MqttData = hass.data.setdefault(DATA_MQTT, MqttData()) + if mqtt_data.client is None or not mqtt_config_entry_enabled(hass): raise HomeAssistantError( f"Cannot subscribe to topic '{topic}', MQTT is not enabled" ) @@ -206,7 +220,7 @@ async def async_subscribe( cast(DeprecatedMessageCallbackType, msg_callback) ) - async_remove = await hass.data[DATA_MQTT].async_subscribe( + async_remove = await mqtt_data.client.async_subscribe( topic, catch_log_exception( wrapped_msg_callback, @@ -309,15 +323,17 @@ class MQTT: def __init__( self, - hass, - config_entry, - conf, + hass: HomeAssistant, + config_entry: ConfigEntry, + conf: ConfigType, ) -> None: """Initialize Home Assistant MQTT client.""" # We don't import on the top because some integrations # should be able to optionally rely on MQTT. import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel + self._mqtt_data: MqttData = hass.data[DATA_MQTT] + self.hass = hass self.config_entry = config_entry self.conf = conf @@ -635,7 +651,6 @@ class MQTT: subscription.job, ) continue - self.hass.async_run_hass_job( subscription.job, ReceiveMessage( @@ -695,10 +710,10 @@ class MQTT: async def _discovery_cooldown(self): now = time.time() # Reset discovery and subscribe cooldowns - self.hass.data[LAST_DISCOVERY] = now + self._mqtt_data.last_discovery = now self._last_subscribe = now - last_discovery = self.hass.data[LAST_DISCOVERY] + last_discovery = self._mqtt_data.last_discovery last_subscribe = self._last_subscribe wait_until = max( last_discovery + DISCOVERY_COOLDOWN, last_subscribe + DISCOVERY_COOLDOWN @@ -706,7 +721,7 @@ class MQTT: while now < wait_until: await asyncio.sleep(wait_until - now) now = time.time() - last_discovery = self.hass.data[LAST_DISCOVERY] + last_discovery = self._mqtt_data.last_discovery last_subscribe = self._last_subscribe wait_until = max( last_discovery + DISCOVERY_COOLDOWN, last_subscribe + DISCOVERY_COOLDOWN diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index 538c12d258c..12d97b41a74 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -18,7 +18,7 @@ from homeassistant.const import ( CONF_PROTOCOL, CONF_USERNAME, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult from .client import MqttClientSetup @@ -30,12 +30,13 @@ from .const import ( CONF_BIRTH_MESSAGE, CONF_BROKER, CONF_WILL_MESSAGE, - DATA_MQTT_CONFIG, + DATA_MQTT, DEFAULT_BIRTH, DEFAULT_DISCOVERY, DEFAULT_WILL, DOMAIN, ) +from .mixins import MqttData from .util import MQTT_WILL_BIRTH_SCHEMA MQTT_TIMEOUT = 5 @@ -164,9 +165,10 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Manage the MQTT broker configuration.""" + mqtt_data: MqttData = self.hass.data.setdefault(DATA_MQTT, MqttData()) errors = {} current_config = self.config_entry.data - yaml_config = self.hass.data.get(DATA_MQTT_CONFIG, {}) + yaml_config = mqtt_data.config or {} if user_input is not None: can_connect = await self.hass.async_add_executor_job( try_connection, @@ -214,9 +216,10 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Manage the MQTT options.""" + mqtt_data: MqttData = self.hass.data.setdefault(DATA_MQTT, MqttData()) errors = {} current_config = self.config_entry.data - yaml_config = self.hass.data.get(DATA_MQTT_CONFIG, {}) + yaml_config = mqtt_data.config or {} options_config: dict[str, Any] = {} if user_input is not None: bad_birth = False @@ -334,14 +337,22 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow): ) -def try_connection(hass, broker, port, username, password, protocol="3.1"): +def try_connection( + hass: HomeAssistant, + broker: str, + port: int, + username: str | None, + password: str | None, + protocol: str = "3.1", +) -> bool: """Test if we can connect to an MQTT broker.""" # We don't import on the top because some integrations # should be able to optionally rely on MQTT. import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel # Get the config from configuration.yaml - yaml_config = hass.data.get(DATA_MQTT_CONFIG, {}) + mqtt_data: MqttData = hass.data.setdefault(DATA_MQTT, MqttData()) + yaml_config = mqtt_data.config or {} entry_config = { CONF_BROKER: broker, CONF_PORT: port, @@ -351,7 +362,7 @@ def try_connection(hass, broker, port, username, password, protocol="3.1"): } client = MqttClientSetup({**yaml_config, **entry_config}).client - result = queue.Queue(maxsize=1) + result: queue.Queue[bool] = queue.Queue(maxsize=1) def on_connect(client_, userdata, flags, result_code): """Handle connection result.""" diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index c8af58862e0..93410f0c792 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -30,16 +30,8 @@ CONF_CLIENT_CERT = "client_cert" CONF_TLS_INSECURE = "tls_insecure" CONF_TLS_VERSION = "tls_version" -CONFIG_ENTRY_IS_SETUP = "mqtt_config_entry_is_setup" DATA_MQTT = "mqtt" -DATA_MQTT_SUBSCRIPTIONS_TO_RESTORE = "mqtt_client_subscriptions" -DATA_MQTT_DISCOVERY_REGISTRY_HOOKS = "mqtt_discovery_registry_hooks" -DATA_MQTT_CONFIG = "mqtt_config" MQTT_DATA_DEVICE_TRACKER_LEGACY = "mqtt_device_tracker_legacy" -DATA_MQTT_RELOAD_DISPATCHERS = "mqtt_reload_dispatchers" -DATA_MQTT_RELOAD_ENTRY = "mqtt_reload_entry" -DATA_MQTT_RELOAD_NEEDED = "mqtt_reload_needed" -DATA_MQTT_UPDATED_CONFIG = "mqtt_updated_config" DEFAULT_PREFIX = "homeassistant" DEFAULT_BIRTH_WILL_TOPIC = DEFAULT_PREFIX + "/status" diff --git a/homeassistant/components/mqtt/device_trigger.py b/homeassistant/components/mqtt/device_trigger.py index 30d6fdea05f..7e37ed72821 100644 --- a/homeassistant/components/mqtt/device_trigger.py +++ b/homeassistant/components/mqtt/device_trigger.py @@ -33,11 +33,13 @@ from .const import ( CONF_PAYLOAD, CONF_QOS, CONF_TOPIC, + DATA_MQTT, DOMAIN, ) from .discovery import MQTT_DISCOVERY_DONE from .mixins import ( MQTT_ENTITY_DEVICE_INFO_SCHEMA, + MqttData, MqttDiscoveryDeviceUpdate, send_discovery_done, update_device, @@ -81,8 +83,6 @@ TRIGGER_DISCOVERY_SCHEMA = MQTT_BASE_SCHEMA.extend( extra=vol.REMOVE_EXTRA, ) -DEVICE_TRIGGERS = "mqtt_device_triggers" - LOG_NAME = "Device trigger" @@ -203,6 +203,7 @@ class MqttDeviceTrigger(MqttDiscoveryDeviceUpdate): self.device_id = device_id self.discovery_data = discovery_data self.hass = hass + self._mqtt_data: MqttData = hass.data[DATA_MQTT] MqttDiscoveryDeviceUpdate.__init__( self, @@ -217,8 +218,8 @@ class MqttDeviceTrigger(MqttDiscoveryDeviceUpdate): """Initialize the device trigger.""" discovery_hash = self.discovery_data[ATTR_DISCOVERY_HASH] discovery_id = discovery_hash[1] - if discovery_id not in self.hass.data.setdefault(DEVICE_TRIGGERS, {}): - self.hass.data[DEVICE_TRIGGERS][discovery_id] = Trigger( + if discovery_id not in self._mqtt_data.device_triggers: + self._mqtt_data.device_triggers[discovery_id] = Trigger( hass=self.hass, device_id=self.device_id, discovery_data=self.discovery_data, @@ -230,7 +231,7 @@ class MqttDeviceTrigger(MqttDiscoveryDeviceUpdate): value_template=self._config[CONF_VALUE_TEMPLATE], ) else: - await self.hass.data[DEVICE_TRIGGERS][discovery_id].update_trigger( + await self._mqtt_data.device_triggers[discovery_id].update_trigger( self._config ) debug_info.add_trigger_discovery_data( @@ -246,16 +247,16 @@ class MqttDeviceTrigger(MqttDiscoveryDeviceUpdate): ) config = TRIGGER_DISCOVERY_SCHEMA(discovery_data) update_device(self.hass, self._config_entry, config) - device_trigger: Trigger = self.hass.data[DEVICE_TRIGGERS][discovery_id] + device_trigger: Trigger = self._mqtt_data.device_triggers[discovery_id] await device_trigger.update_trigger(config) async def async_tear_down(self) -> None: """Cleanup device trigger.""" discovery_hash = self.discovery_data[ATTR_DISCOVERY_HASH] discovery_id = discovery_hash[1] - if discovery_id in self.hass.data[DEVICE_TRIGGERS]: + if discovery_id in self._mqtt_data.device_triggers: _LOGGER.info("Removing trigger: %s", discovery_hash) - trigger: Trigger = self.hass.data[DEVICE_TRIGGERS][discovery_id] + trigger: Trigger = self._mqtt_data.device_triggers[discovery_id] trigger.detach_trigger() debug_info.remove_trigger_discovery_data(self.hass, discovery_hash) @@ -280,11 +281,10 @@ async def async_setup_trigger( async def async_removed_from_device(hass: HomeAssistant, device_id: str) -> None: """Handle Mqtt removed from a device.""" + mqtt_data: MqttData = hass.data[DATA_MQTT] triggers = await async_get_triggers(hass, device_id) for trig in triggers: - device_trigger: Trigger = hass.data[DEVICE_TRIGGERS].pop( - trig[CONF_DISCOVERY_ID] - ) + device_trigger: Trigger = mqtt_data.device_triggers.pop(trig[CONF_DISCOVERY_ID]) if device_trigger: device_trigger.detach_trigger() discovery_data = cast(dict, device_trigger.discovery_data) @@ -296,12 +296,13 @@ async def async_get_triggers( hass: HomeAssistant, device_id: str ) -> list[dict[str, str]]: """List device triggers for MQTT devices.""" + mqtt_data: MqttData = hass.data[DATA_MQTT] triggers: list[dict[str, str]] = [] - if DEVICE_TRIGGERS not in hass.data: + if not mqtt_data.device_triggers: return triggers - for discovery_id, trig in hass.data[DEVICE_TRIGGERS].items(): + for discovery_id, trig in mqtt_data.device_triggers.items(): if trig.device_id != device_id or trig.topic is None: continue @@ -324,12 +325,12 @@ async def async_attach_trigger( trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" - hass.data.setdefault(DEVICE_TRIGGERS, {}) + mqtt_data: MqttData = hass.data[DATA_MQTT] device_id = config[CONF_DEVICE_ID] discovery_id = config[CONF_DISCOVERY_ID] - if discovery_id not in hass.data[DEVICE_TRIGGERS]: - hass.data[DEVICE_TRIGGERS][discovery_id] = Trigger( + if discovery_id not in mqtt_data.device_triggers: + mqtt_data.device_triggers[discovery_id] = Trigger( hass=hass, device_id=device_id, discovery_data=None, @@ -340,6 +341,6 @@ async def async_attach_trigger( qos=None, value_template=None, ) - return await hass.data[DEVICE_TRIGGERS][discovery_id].add_trigger( + return await mqtt_data.device_triggers[discovery_id].add_trigger( action, trigger_info ) diff --git a/homeassistant/components/mqtt/diagnostics.py b/homeassistant/components/mqtt/diagnostics.py index ea490783fc0..2a6322cac63 100644 --- a/homeassistant/components/mqtt/diagnostics.py +++ b/homeassistant/components/mqtt/diagnostics.py @@ -43,7 +43,7 @@ def _async_get_diagnostics( device: DeviceEntry | None = None, ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - mqtt_instance: MQTT = hass.data[DATA_MQTT] + mqtt_instance: MQTT = hass.data[DATA_MQTT].client redacted_config = async_redact_data(mqtt_instance.conf, REDACT_CONFIG) diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index 8a4c4d0c542..65051ce54fc 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -7,6 +7,7 @@ import functools import logging import re import time +from typing import TYPE_CHECKING from homeassistant.const import CONF_DEVICE, CONF_PLATFORM from homeassistant.core import HomeAssistant @@ -28,9 +29,13 @@ from .const import ( ATTR_DISCOVERY_TOPIC, CONF_AVAILABILITY, CONF_TOPIC, + DATA_MQTT, DOMAIN, ) +if TYPE_CHECKING: + from .mixins import MqttData + _LOGGER = logging.getLogger(__name__) TOPIC_MATCHER = re.compile( @@ -69,7 +74,6 @@ INTEGRATION_UNSUBSCRIBE = "mqtt_integration_discovery_unsubscribe" MQTT_DISCOVERY_UPDATED = "mqtt_discovery_updated_{}" MQTT_DISCOVERY_NEW = "mqtt_discovery_new_{}_{}" MQTT_DISCOVERY_DONE = "mqtt_discovery_done_{}" -LAST_DISCOVERY = "mqtt_last_discovery" TOPIC_BASE = "~" @@ -80,12 +84,12 @@ class MQTTConfig(dict): discovery_data: dict -def clear_discovery_hash(hass: HomeAssistant, discovery_hash: tuple) -> None: +def clear_discovery_hash(hass: HomeAssistant, discovery_hash: tuple[str, str]) -> None: """Clear entry in ALREADY_DISCOVERED list.""" del hass.data[ALREADY_DISCOVERED][discovery_hash] -def set_discovery_hash(hass: HomeAssistant, discovery_hash: tuple): +def set_discovery_hash(hass: HomeAssistant, discovery_hash: tuple[str, str]): """Clear entry in ALREADY_DISCOVERED list.""" hass.data[ALREADY_DISCOVERED][discovery_hash] = {} @@ -94,11 +98,12 @@ async def async_start( # noqa: C901 hass: HomeAssistant, discovery_topic, config_entry=None ) -> None: """Start MQTT Discovery.""" + mqtt_data: MqttData = hass.data[DATA_MQTT] mqtt_integrations = {} async def async_discovery_message_received(msg): """Process the received message.""" - hass.data[LAST_DISCOVERY] = time.time() + mqtt_data.last_discovery = time.time() payload = msg.payload topic = msg.topic topic_trimmed = topic.replace(f"{discovery_topic}/", "", 1) @@ -253,7 +258,7 @@ async def async_start( # noqa: C901 ) ) - hass.data[LAST_DISCOVERY] = time.time() + mqtt_data.last_discovery = time.time() mqtt_integrations = await async_get_mqtt(hass) hass.data[INTEGRATION_UNSUBSCRIBE] = {} diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index fddbe838303..a16394667d8 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -4,9 +4,10 @@ from __future__ import annotations from abc import abstractmethod import asyncio from collections.abc import Callable, Coroutine +from dataclasses import dataclass, field from functools import partial import logging -from typing import Any, Protocol, cast, final +from typing import TYPE_CHECKING, Any, Protocol, cast, final import voluptuous as vol @@ -60,7 +61,7 @@ from homeassistant.helpers.json import json_loads from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import debug_info, subscription -from .client import async_publish +from .client import MQTT, Subscription, async_publish from .const import ( ATTR_DISCOVERY_HASH, ATTR_DISCOVERY_PAYLOAD, @@ -70,11 +71,6 @@ from .const import ( CONF_QOS, CONF_TOPIC, DATA_MQTT, - DATA_MQTT_CONFIG, - DATA_MQTT_DISCOVERY_REGISTRY_HOOKS, - DATA_MQTT_RELOAD_DISPATCHERS, - DATA_MQTT_RELOAD_ENTRY, - DATA_MQTT_UPDATED_CONFIG, DEFAULT_ENCODING, DEFAULT_PAYLOAD_AVAILABLE, DEFAULT_PAYLOAD_NOT_AVAILABLE, @@ -98,6 +94,9 @@ from .subscription import ( ) from .util import mqtt_config_entry_enabled, valid_subscribe_topic +if TYPE_CHECKING: + from .device_trigger import Trigger + _LOGGER = logging.getLogger(__name__) AVAILABILITY_ALL = "all" @@ -274,6 +273,24 @@ def warn_for_legacy_schema(domain: str) -> Callable: return validator +@dataclass +class MqttData: + """Keep the MQTT entry data.""" + + client: MQTT | None = None + config: ConfigType | None = None + device_triggers: dict[str, Trigger] = field(default_factory=dict) + discovery_registry_hooks: dict[tuple[str, str], CALLBACK_TYPE] = field( + default_factory=dict + ) + last_discovery: float = 0.0 + reload_dispatchers: list[CALLBACK_TYPE] = field(default_factory=list) + reload_entry: bool = False + reload_needed: bool = False + subscriptions_to_restore: list[Subscription] = field(default_factory=list) + updated_config: ConfigType = field(default_factory=dict) + + class SetupEntity(Protocol): """Protocol type for async_setup_entities.""" @@ -292,11 +309,12 @@ async def async_discover_yaml_entities( hass: HomeAssistant, platform_domain: str ) -> None: """Discover entities for a platform.""" - if DATA_MQTT_UPDATED_CONFIG in hass.data: + mqtt_data: MqttData = hass.data[DATA_MQTT] + if mqtt_data.updated_config: # The platform has been reloaded - config_yaml = hass.data[DATA_MQTT_UPDATED_CONFIG] + config_yaml = mqtt_data.updated_config else: - config_yaml = hass.data.get(DATA_MQTT_CONFIG, {}) + config_yaml = mqtt_data.config or {} if not config_yaml: return if platform_domain not in config_yaml: @@ -318,8 +336,9 @@ async def async_get_platform_config_from_yaml( ) -> list[ConfigType]: """Return a list of validated configurations for the domain.""" + mqtt_data: MqttData = hass.data[DATA_MQTT] if config_yaml is None: - config_yaml = hass.data.get(DATA_MQTT_CONFIG) + config_yaml = mqtt_data.config if not config_yaml: return [] if not (platform_configs := config_yaml.get(platform_domain)): @@ -334,6 +353,7 @@ async def async_setup_entry_helper( schema: vol.Schema, ) -> None: """Set up entity, automation or tag creation dynamically through MQTT discovery.""" + mqtt_data: MqttData = hass.data[DATA_MQTT] async def async_discover(discovery_payload): """Discover and add an MQTT entity, automation or tag.""" @@ -357,7 +377,7 @@ async def async_setup_entry_helper( ) raise - hass.data.setdefault(DATA_MQTT_RELOAD_DISPATCHERS, []).append( + mqtt_data.reload_dispatchers.append( async_dispatcher_connect( hass, MQTT_DISCOVERY_NEW.format(domain, "mqtt"), async_discover ) @@ -372,7 +392,8 @@ async def async_setup_platform_helper( async_setup_entities: SetupEntity, ) -> None: """Help to set up the platform for manual configured MQTT entities.""" - if DATA_MQTT_RELOAD_ENTRY in hass.data: + mqtt_data: MqttData = hass.data[DATA_MQTT] + if mqtt_data.reload_entry: _LOGGER.debug( "MQTT integration is %s, skipping setup of manually configured MQTT items while unloading the config entry", platform_domain, @@ -597,7 +618,10 @@ class MqttAvailability(Entity): @property def available(self) -> bool: """Return if the device is available.""" - if not self.hass.data[DATA_MQTT].connected and not self.hass.is_stopping: + mqtt_data: MqttData = self.hass.data[DATA_MQTT] + assert mqtt_data.client is not None + client = mqtt_data.client + if not client.connected and not self.hass.is_stopping: return False if not self._avail_topics: return True @@ -632,7 +656,7 @@ async def cleanup_device_registry( ) -def get_discovery_hash(discovery_data: dict) -> tuple: +def get_discovery_hash(discovery_data: dict) -> tuple[str, str]: """Get the discovery hash from the discovery data.""" return discovery_data[ATTR_DISCOVERY_HASH] @@ -817,9 +841,8 @@ class MqttDiscoveryUpdate(Entity): self._removed_from_hass = False if discovery_data is None: return - self._registry_hooks: dict[tuple, CALLBACK_TYPE] = hass.data[ - DATA_MQTT_DISCOVERY_REGISTRY_HOOKS - ] + mqtt_data: MqttData = hass.data[DATA_MQTT] + self._registry_hooks = mqtt_data.discovery_registry_hooks discovery_hash: tuple[str, str] = discovery_data[ATTR_DISCOVERY_HASH] if discovery_hash in self._registry_hooks: self._registry_hooks.pop(discovery_hash)() @@ -897,7 +920,7 @@ class MqttDiscoveryUpdate(Entity): def add_to_platform_abort(self) -> None: """Abort adding an entity to a platform.""" if self._discovery_data is not None: - discovery_hash: tuple = self._discovery_data[ATTR_DISCOVERY_HASH] + discovery_hash: tuple[str, str] = self._discovery_data[ATTR_DISCOVERY_HASH] if self.registry_entry is not None: self._registry_hooks[ discovery_hash diff --git a/tests/common.py b/tests/common.py index 232701bd746..cc2bc454810 100644 --- a/tests/common.py +++ b/tests/common.py @@ -369,7 +369,7 @@ def async_fire_mqtt_message(hass, topic, payload, qos=0, retain=False): if isinstance(payload, str): payload = payload.encode("utf-8") msg = ReceiveMessage(topic, payload, qos, retain) - hass.data["mqtt"]._mqtt_handle_message(msg) + hass.data["mqtt"].client._mqtt_handle_message(msg) fire_mqtt_message = threadsafe_callback_factory(async_fire_mqtt_message) diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index e40397fd1d4..dba06e5cd5b 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -155,7 +155,7 @@ async def test_manual_config_set( assert await async_setup_component(hass, "mqtt", {"mqtt": {"broker": "bla"}}) await hass.async_block_till_done() # do not try to reload - del hass.data["mqtt_reload_needed"] + hass.data["mqtt"].reload_needed = False assert len(mock_finish_setup.mock_calls) == 0 mock_try_connection.return_value = True diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index c625d0a21f9..a9ac66f8851 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -1438,7 +1438,7 @@ async def test_clean_up_registry_monitoring( ): """Test registry monitoring hook is removed after a reload.""" await mqtt_mock_entry_no_yaml_config() - hooks: dict = hass.data[mqtt.const.DATA_MQTT_DISCOVERY_REGISTRY_HOOKS] + hooks: dict = hass.data["mqtt"].discovery_registry_hooks # discover an entity that is not enabled by default config1 = { "name": "sbfspot_12345", diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index b76979cc990..46649bf703f 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -1776,14 +1776,14 @@ async def test_delayed_birth_message( await hass.async_block_till_done() mqtt_component_mock = MagicMock( - return_value=hass.data["mqtt"], - spec_set=hass.data["mqtt"], - wraps=hass.data["mqtt"], + return_value=hass.data["mqtt"].client, + spec_set=hass.data["mqtt"].client, + wraps=hass.data["mqtt"].client, ) mqtt_component_mock._mqttc = mqtt_client_mock - hass.data["mqtt"] = mqtt_component_mock - mqtt_mock = hass.data["mqtt"] + hass.data["mqtt"].client = mqtt_component_mock + mqtt_mock = hass.data["mqtt"].client mqtt_mock.reset_mock() async def wait_birth(topic, payload, qos): From 13d3f4c3b2ec7f2a40c8316e7bf41d3148582d44 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 17 Sep 2022 15:01:57 -0600 Subject: [PATCH 499/955] Replace Guardian `disable_ap` and `enable_ap` services with a switch (#75034) * Starter buttons * Ready to go * Replace Guardian `disable_ap` and `enable_ap` services with a switch * Clean up how actions are stored * Make similar to buttons * Remove service definitions * Docstring * Docstring * flake8 * Add repairs item * Add a repairs issue to notify of removed entity * Add entity replacement strategy * Fix repairs import * Update deprecation version * Remove breaking change * Include future breaking change version * Naming --- homeassistant/components/guardian/__init__.py | 14 +++ .../components/guardian/binary_sensor.py | 22 +++- .../components/guardian/services.yaml | 44 ++++---- .../components/guardian/strings.json | 13 ++- homeassistant/components/guardian/switch.py | 103 ++++++++++++++---- .../components/guardian/translations/en.json | 13 ++- homeassistant/components/guardian/util.py | 61 ++++++++++- 7 files changed, 220 insertions(+), 50 deletions(-) diff --git a/homeassistant/components/guardian/__init__.py b/homeassistant/components/guardian/__init__.py index a3cc7a0031b..eccf845fcce 100644 --- a/homeassistant/components/guardian/__init__.py +++ b/homeassistant/components/guardian/__init__.py @@ -238,11 +238,25 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @call_with_data async def async_disable_ap(call: ServiceCall, data: GuardianData) -> None: """Disable the onboard AP.""" + async_log_deprecated_service_call( + hass, + call, + "switch.turn_off", + f"switch.guardian_valve_controller_{entry.data[CONF_UID]}_onboard_ap", + "2022.12.0", + ) await data.client.wifi.disable_ap() @call_with_data async def async_enable_ap(call: ServiceCall, data: GuardianData) -> None: """Enable the onboard AP.""" + async_log_deprecated_service_call( + hass, + call, + "switch.turn_on", + f"switch.guardian_valve_controller_{entry.data[CONF_UID]}_onboard_ap", + "2022.12.0", + ) await data.client.wifi.enable_ap() @call_with_data diff --git a/homeassistant/components/guardian/binary_sensor.py b/homeassistant/components/guardian/binary_sensor.py index 766e5d961e8..6425ecd46a6 100644 --- a/homeassistant/components/guardian/binary_sensor.py +++ b/homeassistant/components/guardian/binary_sensor.py @@ -4,6 +4,7 @@ from __future__ import annotations from dataclasses import dataclass from homeassistant.components.binary_sensor import ( + DOMAIN as BINARY_SENSOR_DOMAIN, BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, @@ -27,7 +28,11 @@ from .const import ( DOMAIN, SIGNAL_PAIRED_SENSOR_COORDINATOR_ADDED, ) -from .util import GuardianDataUpdateCoordinator +from .util import ( + EntityDomainReplacementStrategy, + GuardianDataUpdateCoordinator, + async_finish_entity_domain_replacements, +) ATTR_CONNECTED_CLIENTS = "connected_clients" @@ -79,6 +84,21 @@ async def async_setup_entry( ) -> None: """Set up Guardian switches based on a config entry.""" data: GuardianData = hass.data[DOMAIN][entry.entry_id] + uid = entry.data[CONF_UID] + + async_finish_entity_domain_replacements( + hass, + entry, + ( + EntityDomainReplacementStrategy( + BINARY_SENSOR_DOMAIN, + f"{uid}_ap_enabled", + f"switch.guardian_valve_controller_{uid}_onboard_ap", + "2022.12.0", + remove_old_entity=False, + ), + ), + ) @callback def add_new_paired_sensor(uid: str) -> None: diff --git a/homeassistant/components/guardian/services.yaml b/homeassistant/components/guardian/services.yaml index 61cf709a31c..0707abb6978 100644 --- a/homeassistant/components/guardian/services.yaml +++ b/homeassistant/components/guardian/services.yaml @@ -1,26 +1,4 @@ # Describes the format for available Elexa Guardians services -disable_ap: - name: Disable AP - description: Disable the device's onboard access point. - fields: - device_id: - name: Valve Controller - description: The valve controller whose AP should be disabled - required: true - selector: - device: - integration: guardian -enable_ap: - name: Enable AP - description: Enable the device's onboard access point. - fields: - device_id: - name: Valve Controller - description: The valve controller whose AP should be enabled - required: true - selector: - device: - integration: guardian pair_sensor: name: Pair Sensor description: Add a new paired sensor to the valve controller. @@ -39,6 +17,28 @@ pair_sensor: example: 5410EC688BCF selector: text: +reboot: + name: Reboot + description: Reboot the device. + fields: + device_id: + name: Valve Controller + description: The valve controller to reboot + required: true + selector: + device: + integration: guardian +reset_valve_diagnostics: + name: Reset Valve Diagnostics + description: Fully (and irrecoverably) reset all valve diagnostics. + fields: + device_id: + name: Valve Controller + description: The valve controller whose diagnostics should be reset + required: true + selector: + device: + integration: guardian unpair_sensor: name: Unpair Sensor description: Remove a paired sensor from the valve controller. diff --git a/homeassistant/components/guardian/strings.json b/homeassistant/components/guardian/strings.json index 1665cf9f678..b173051a860 100644 --- a/homeassistant/components/guardian/strings.json +++ b/homeassistant/components/guardian/strings.json @@ -25,7 +25,18 @@ "step": { "confirm": { "title": "The {deprecated_service} service is being removed", - "description": "Update any automations or scripts that use this service to instead use the `{alternate_service}` service with a target entity ID of `{alternate_target}`. Then, click SUBMIT below to mark this issue as resolved." + "description": "Update any automations or scripts that use this service to instead use the `{alternate_service}` service with a target entity ID of `{alternate_target}`." + } + } + } + }, + "replaced_old_entity": { + "title": "The {old_entity_id} entity will be removed", + "fix_flow": { + "step": { + "confirm": { + "title": "The {old_entity_id} entity will be removed", + "description": "This entity has been replaced by `{replacement_entity_id}`." } } } diff --git a/homeassistant/components/guardian/switch.py b/homeassistant/components/guardian/switch.py index 4e100ce4fe4..5471d2471e8 100644 --- a/homeassistant/components/guardian/switch.py +++ b/homeassistant/components/guardian/switch.py @@ -1,41 +1,86 @@ """Switches for the Elexa Guardian integration.""" from __future__ import annotations +from collections.abc import Awaitable, Callable from dataclasses import dataclass from typing import Any +from aioguardian import Client from aioguardian.errors import GuardianError from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import GuardianData, ValveControllerEntity, ValveControllerEntityDescription -from .const import API_VALVE_STATUS, DOMAIN +from .const import API_VALVE_STATUS, API_WIFI_STATUS, DOMAIN ATTR_AVG_CURRENT = "average_current" +ATTR_CONNECTED_CLIENTS = "connected_clients" ATTR_INST_CURRENT = "instantaneous_current" ATTR_INST_CURRENT_DDT = "instantaneous_current_ddt" +ATTR_STATION_CONNECTED = "station_connected" ATTR_TRAVEL_COUNT = "travel_count" +SWITCH_KIND_ONBOARD_AP = "onboard_ap" SWITCH_KIND_VALVE = "valve" +@dataclass +class SwitchDescriptionMixin: + """Define an entity description mixin for Guardian switches.""" + + off_action: Callable[[Client], Awaitable] + on_action: Callable[[Client], Awaitable] + + @dataclass class ValveControllerSwitchDescription( - SwitchEntityDescription, ValveControllerEntityDescription + SwitchEntityDescription, ValveControllerEntityDescription, SwitchDescriptionMixin ): """Describe a Guardian valve controller switch.""" +async def _async_disable_ap(client: Client) -> None: + """Disable the onboard AP.""" + await client.wifi.disable_ap() + + +async def _async_enable_ap(client: Client) -> None: + """Enable the onboard AP.""" + await client.wifi.enable_ap() + + +async def _async_close_valve(client: Client) -> None: + """Close the valve.""" + await client.valve.close() + + +async def _async_open_valve(client: Client) -> None: + """Open the valve.""" + await client.valve.open() + + VALVE_CONTROLLER_DESCRIPTIONS = ( + ValveControllerSwitchDescription( + key=SWITCH_KIND_ONBOARD_AP, + name="Onboard AP", + icon="mdi:wifi", + entity_category=EntityCategory.CONFIG, + api_category=API_WIFI_STATUS, + off_action=_async_disable_ap, + on_action=_async_enable_ap, + ), ValveControllerSwitchDescription( key=SWITCH_KIND_VALVE, name="Valve controller", icon="mdi:water", api_category=API_VALVE_STATUS, + off_action=_async_close_valve, + on_action=_async_open_valve, ), ) @@ -53,9 +98,7 @@ async def async_setup_entry( class ValveControllerSwitch(ValveControllerEntity, SwitchEntity): - """Define a switch to open/close the Guardian valve.""" - - entity_description: ValveControllerSwitchDescription + """Define a switch related to a Guardian valve controller.""" ON_STATES = { "start_opening", @@ -64,6 +107,8 @@ class ValveControllerSwitch(ValveControllerEntity, SwitchEntity): "opened", } + entity_description: ValveControllerSwitchDescription + def __init__( self, entry: ConfigEntry, @@ -73,42 +118,54 @@ class ValveControllerSwitch(ValveControllerEntity, SwitchEntity): """Initialize.""" super().__init__(entry, data.valve_controller_coordinators, description) - self._attr_is_on = True self._client = data.client @callback def _async_update_from_latest_data(self) -> None: """Update the entity.""" - self._attr_is_on = self.coordinator.data["state"] in self.ON_STATES - self._attr_extra_state_attributes.update( - { - ATTR_AVG_CURRENT: self.coordinator.data["average_current"], - ATTR_INST_CURRENT: self.coordinator.data["instantaneous_current"], - ATTR_INST_CURRENT_DDT: self.coordinator.data[ - "instantaneous_current_ddt" - ], - ATTR_TRAVEL_COUNT: self.coordinator.data["travel_count"], - } - ) + if self.entity_description.key == SWITCH_KIND_ONBOARD_AP: + self._attr_extra_state_attributes.update( + { + ATTR_CONNECTED_CLIENTS: self.coordinator.data.get("ap_clients"), + ATTR_STATION_CONNECTED: self.coordinator.data["station_connected"], + } + ) + self._attr_is_on = self.coordinator.data["ap_enabled"] + elif self.entity_description.key == SWITCH_KIND_VALVE: + self._attr_is_on = self.coordinator.data["state"] in self.ON_STATES + self._attr_extra_state_attributes.update( + { + ATTR_AVG_CURRENT: self.coordinator.data["average_current"], + ATTR_INST_CURRENT: self.coordinator.data["instantaneous_current"], + ATTR_INST_CURRENT_DDT: self.coordinator.data[ + "instantaneous_current_ddt" + ], + ATTR_TRAVEL_COUNT: self.coordinator.data["travel_count"], + } + ) async def async_turn_off(self, **kwargs: Any) -> None: - """Turn the valve off (closed).""" + """Turn the switch off.""" try: async with self._client: - await self._client.valve.close() + await self.entity_description.off_action(self._client) except GuardianError as err: - raise HomeAssistantError(f"Error while closing the valve: {err}") from err + raise HomeAssistantError( + f'Error while turning "{self.entity_id}" off: {err}' + ) from err self._attr_is_on = False self.async_write_ha_state() async def async_turn_on(self, **kwargs: Any) -> None: - """Turn the valve on (open).""" + """Turn the switch on.""" try: async with self._client: - await self._client.valve.open() + await self.entity_description.on_action(self._client) except GuardianError as err: - raise HomeAssistantError(f"Error while opening the valve: {err}") from err + raise HomeAssistantError( + f'Error while turning "{self.entity_id}" on: {err}' + ) from err self._attr_is_on = True self.async_write_ha_state() diff --git a/homeassistant/components/guardian/translations/en.json b/homeassistant/components/guardian/translations/en.json index ad6d0a4b7dc..99bf2e67b4e 100644 --- a/homeassistant/components/guardian/translations/en.json +++ b/homeassistant/components/guardian/translations/en.json @@ -23,12 +23,23 @@ "fix_flow": { "step": { "confirm": { - "description": "Update any automations or scripts that use this service to instead use the `{alternate_service}` service with a target entity ID of `{alternate_target}`. Then, click SUBMIT below to mark this issue as resolved.", + "description": "Update any automations or scripts that use this service to instead use the `{alternate_service}` service with a target entity ID of `{alternate_target}`.", "title": "The {deprecated_service} service is being removed" } } }, "title": "The {deprecated_service} service is being removed" + }, + "replaced_old_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "This entity has been replaced by `{replacement_entity_id}`.", + "title": "The {old_entity_id} entity will be removed" + } + } + }, + "title": "The {old_entity_id} entity will be removed" } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/util.py b/homeassistant/components/guardian/util.py index c88d6762e51..9966435e7b0 100644 --- a/homeassistant/components/guardian/util.py +++ b/homeassistant/components/guardian/util.py @@ -2,7 +2,8 @@ from __future__ import annotations import asyncio -from collections.abc import Awaitable, Callable +from collections.abc import Awaitable, Callable, Iterable +from dataclasses import dataclass from datetime import timedelta from typing import Any, cast @@ -11,16 +12,72 @@ from aioguardian.errors import GuardianError from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import LOGGER +from .const import DOMAIN, LOGGER DEFAULT_UPDATE_INTERVAL = timedelta(seconds=30) SIGNAL_REBOOT_REQUESTED = "guardian_reboot_requested_{0}" +@dataclass +class EntityDomainReplacementStrategy: + """Define an entity replacement.""" + + old_domain: str + old_unique_id: str + replacement_entity_id: str + breaks_in_ha_version: str + remove_old_entity: bool = True + + +@callback +def async_finish_entity_domain_replacements( + hass: HomeAssistant, + entry: ConfigEntry, + entity_replacement_strategies: Iterable[EntityDomainReplacementStrategy], +) -> None: + """Remove old entities and create a repairs issue with info on their replacement.""" + ent_reg = entity_registry.async_get(hass) + for strategy in entity_replacement_strategies: + try: + [registry_entry] = [ + registry_entry + for registry_entry in ent_reg.entities.values() + if registry_entry.config_entry_id == entry.entry_id + and registry_entry.domain == strategy.old_domain + and registry_entry.unique_id == strategy.old_unique_id + ] + except ValueError: + continue + + old_entity_id = registry_entry.entity_id + translation_key = "replaced_old_entity" + + async_create_issue( + hass, + DOMAIN, + f"{translation_key}_{old_entity_id}", + breaks_in_ha_version=strategy.breaks_in_ha_version, + is_fixable=True, + is_persistent=True, + severity=IssueSeverity.WARNING, + translation_key=translation_key, + translation_placeholders={ + "old_entity_id": old_entity_id, + "replacement_entity_id": strategy.replacement_entity_id, + }, + ) + + if strategy.remove_old_entity: + LOGGER.info('Removing old entity: "%s"', old_entity_id) + ent_reg.async_remove(old_entity_id) + + class GuardianDataUpdateCoordinator(DataUpdateCoordinator[dict]): """Define an extended DataUpdateCoordinator with some Guardian goodies.""" From 18eef5da1f1c542142e15796ab21460b7a18a992 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 17 Sep 2022 16:58:19 -0500 Subject: [PATCH 500/955] Restore history from bluetooth stack at startup (#78612) --- .../components/bluetooth/__init__.py | 2 +- homeassistant/components/bluetooth/manager.py | 11 ++++-- .../components/bluetooth/manifest.json | 2 +- homeassistant/components/bluetooth/scanner.py | 20 +---------- homeassistant/components/bluetooth/util.py | 33 ++++++++++++++++++ homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/bluetooth/conftest.py | 16 +++++++-- .../test_active_update_coordinator.py | 22 ++++++++---- .../components/bluetooth/test_config_flow.py | 22 +++++++++--- tests/components/bluetooth/test_init.py | 2 +- tests/components/bluetooth/test_manager.py | 24 +++++++++++++ .../test_passive_update_coordinator.py | 10 +++--- .../test_passive_update_processor.py | 34 +++++++++++++------ tests/conftest.py | 2 ++ 16 files changed, 151 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 2132e1c8b66..387615cdc29 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -228,7 +228,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: integration_matcher = IntegrationMatcher(await async_get_bluetooth(hass)) integration_matcher.async_setup() manager = BluetoothManager(hass, integration_matcher) - manager.async_setup() + await manager.async_setup() hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, manager.async_stop) hass.data[DATA_MANAGER] = models.MANAGER = manager adapters = await manager.async_get_bluetooth_adapters() diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index edc9bb1edde..533867496bf 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -45,7 +45,7 @@ from .models import ( BluetoothServiceInfoBleak, ) from .usage import install_multiple_bleak_catcher, uninstall_multiple_bleak_catcher -from .util import async_get_bluetooth_adapters +from .util import async_get_bluetooth_adapters, async_load_history_from_system if TYPE_CHECKING: from bleak.backends.device import BLEDevice @@ -213,10 +213,15 @@ class BluetoothManager: self._adapters = await async_get_bluetooth_adapters() return self._find_adapter_by_address(address) - @hass_callback - def async_setup(self) -> None: + async def async_setup(self) -> None: """Set up the bluetooth manager.""" install_multiple_bleak_catcher() + history = await async_load_history_from_system() + # Everything is connectable so it fall into both + # buckets since the host system can only provide + # connectable devices + self._history = history.copy() + self._connectable_history = history.copy() self.async_setup_unavailable_tracking() @hass_callback diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 1b1ec016e82..9c368ebf82a 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -7,7 +7,7 @@ "requirements": [ "bleak==0.17.0", "bleak-retry-connector==1.17.1", - "bluetooth-adapters==0.4.1", + "bluetooth-adapters==0.5.1", "bluetooth-auto-recovery==0.3.3", "dbus-fast==1.4.0" ], diff --git a/homeassistant/components/bluetooth/scanner.py b/homeassistant/components/bluetooth/scanner.py index 184e8775f07..f9bfcf7bb79 100644 --- a/homeassistant/components/bluetooth/scanner.py +++ b/homeassistant/components/bluetooth/scanner.py @@ -19,13 +19,7 @@ from bleak.backends.device import BLEDevice from bleak.backends.scanner import AdvertisementData from dbus_fast import InvalidMessageError -from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.core import ( - CALLBACK_TYPE, - Event, - HomeAssistant, - callback as hass_callback, -) +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.package import is_docker_env @@ -133,7 +127,6 @@ class HaScanner(BaseHaScanner): self.scanner = scanner self.adapter = adapter self._start_stop_lock = asyncio.Lock() - self._cancel_stop: CALLBACK_TYPE | None = None self._cancel_watchdog: CALLBACK_TYPE | None = None self._last_detection = 0.0 self._start_time = 0.0 @@ -318,9 +311,6 @@ class HaScanner(BaseHaScanner): break self._async_setup_scanner_watchdog() - self._cancel_stop = self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, self._async_hass_stopping - ) @hass_callback def _async_setup_scanner_watchdog(self) -> None: @@ -368,11 +358,6 @@ class HaScanner(BaseHaScanner): exc_info=True, ) - async def _async_hass_stopping(self, event: Event) -> None: - """Stop the Bluetooth integration at shutdown.""" - self._cancel_stop = None - await self.async_stop() - async def _async_reset_adapter(self) -> None: """Reset the adapter.""" # There is currently nothing the user can do to fix this @@ -396,9 +381,6 @@ class HaScanner(BaseHaScanner): async def _async_stop_scanner(self) -> None: """Stop bluetooth discovery under the lock.""" - if self._cancel_stop: - self._cancel_stop() - self._cancel_stop = None _LOGGER.debug("%s: Stopping bluetooth discovery", self.name) try: await self.scanner.stop() # type: ignore[no-untyped-call] diff --git a/homeassistant/components/bluetooth/util.py b/homeassistant/components/bluetooth/util.py index 3f6c862e53d..d04685f34d9 100644 --- a/homeassistant/components/bluetooth/util.py +++ b/homeassistant/components/bluetooth/util.py @@ -2,6 +2,7 @@ from __future__ import annotations import platform +import time from bluetooth_auto_recovery import recover_adapter @@ -15,6 +16,38 @@ from .const import ( WINDOWS_DEFAULT_BLUETOOTH_ADAPTER, AdapterDetails, ) +from .models import BluetoothServiceInfoBleak + + +async def async_load_history_from_system() -> dict[str, BluetoothServiceInfoBleak]: + """Load the device and advertisement_data history if available on the current system.""" + if platform.system() != "Linux": + return {} + from bluetooth_adapters import ( # pylint: disable=import-outside-toplevel + BlueZDBusObjects, + ) + + bluez_dbus = BlueZDBusObjects() + await bluez_dbus.load() + now = time.monotonic() + return { + address: BluetoothServiceInfoBleak( + name=history.advertisement_data.local_name + or history.device.name + or history.device.address, + address=history.device.address, + rssi=history.device.rssi, + manufacturer_data=history.advertisement_data.manufacturer_data, + service_data=history.advertisement_data.service_data, + service_uuids=history.advertisement_data.service_uuids, + source=history.source, + device=history.device, + advertisement=history.advertisement_data, + connectable=False, + time=now, + ) + for address, history in bluez_dbus.history.items() + } async def async_get_bluetooth_adapters() -> dict[str, AdapterDetails]: diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 8f6ffbf2cbd..065bd8469c2 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==1.17.1 bleak==0.17.0 -bluetooth-adapters==0.4.1 +bluetooth-adapters==0.5.1 bluetooth-auto-recovery==0.3.3 certifi>=2021.5.30 ciso8601==2.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 9df89439043..8ef6d6c1ee7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -430,7 +430,7 @@ bluemaestro-ble==0.2.0 # bluepy==1.3.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.4.1 +bluetooth-adapters==0.5.1 # homeassistant.components.bluetooth bluetooth-auto-recovery==0.3.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 733342febdb..90eeabcba37 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -341,7 +341,7 @@ blinkpy==0.19.2 bluemaestro-ble==0.2.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.4.1 +bluetooth-adapters==0.5.1 # homeassistant.components.bluetooth bluetooth-auto-recovery==0.3.3 diff --git a/tests/components/bluetooth/conftest.py b/tests/components/bluetooth/conftest.py index 3447012ace5..58b6e596629 100644 --- a/tests/components/bluetooth/conftest.py +++ b/tests/components/bluetooth/conftest.py @@ -1,10 +1,20 @@ """Tests for the bluetooth component.""" -from unittest.mock import patch +from unittest.mock import AsyncMock, MagicMock, patch import pytest +@pytest.fixture(name="bluez_dbus_mock") +def bluez_dbus_mock(): + """Fixture that mocks out the bluez dbus calls.""" + # Must patch directly since this is loaded on demand only + with patch( + "bluetooth_adapters.BlueZDBusObjects", return_value=MagicMock(load=AsyncMock()) + ): + yield + + @pytest.fixture(name="macos_adapter") def macos_adapter(): """Fixture that mocks the macos adapter.""" @@ -25,7 +35,7 @@ def windows_adapter(): @pytest.fixture(name="one_adapter") -def one_adapter_fixture(): +def one_adapter_fixture(bluez_dbus_mock): """Fixture that mocks one adapter on Linux.""" with patch( "homeassistant.components.bluetooth.platform.system", return_value="Linux" @@ -54,7 +64,7 @@ def one_adapter_fixture(): @pytest.fixture(name="two_adapters") -def two_adapters_fixture(): +def two_adapters_fixture(bluez_dbus_mock): """Fixture that mocks two adapters on Linux.""" with patch( "homeassistant.components.bluetooth.platform.system", return_value="Linux" diff --git a/tests/components/bluetooth/test_active_update_coordinator.py b/tests/components/bluetooth/test_active_update_coordinator.py index 4dbe32d69d2..318934d77b7 100644 --- a/tests/components/bluetooth/test_active_update_coordinator.py +++ b/tests/components/bluetooth/test_active_update_coordinator.py @@ -47,7 +47,9 @@ GENERIC_BLUETOOTH_SERVICE_INFO_2 = BluetoothServiceInfo( ) -async def test_basic_usage(hass: HomeAssistant, mock_bleak_scanner_start): +async def test_basic_usage( + hass: HomeAssistant, mock_bleak_scanner_start, mock_bluetooth_adapters +): """Test basic usage of the ActiveBluetoothProcessorCoordinator.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) @@ -92,7 +94,9 @@ async def test_basic_usage(hass: HomeAssistant, mock_bleak_scanner_start): cancel() -async def test_poll_can_be_skipped(hass: HomeAssistant, mock_bleak_scanner_start): +async def test_poll_can_be_skipped( + hass: HomeAssistant, mock_bleak_scanner_start, mock_bluetooth_adapters +): """Test need_poll callback works and can skip a poll if its not needed.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) @@ -151,7 +155,7 @@ async def test_poll_can_be_skipped(hass: HomeAssistant, mock_bleak_scanner_start async def test_bleak_error_and_recover( - hass: HomeAssistant, mock_bleak_scanner_start, caplog + hass: HomeAssistant, mock_bleak_scanner_start, mock_bluetooth_adapters, caplog ): """Test bleak error handling and recovery.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) @@ -212,7 +216,9 @@ async def test_bleak_error_and_recover( cancel() -async def test_poll_failure_and_recover(hass: HomeAssistant, mock_bleak_scanner_start): +async def test_poll_failure_and_recover( + hass: HomeAssistant, mock_bleak_scanner_start, mock_bluetooth_adapters +): """Test error handling and recovery.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) @@ -267,7 +273,9 @@ async def test_poll_failure_and_recover(hass: HomeAssistant, mock_bleak_scanner_ cancel() -async def test_second_poll_needed(hass: HomeAssistant, mock_bleak_scanner_start): +async def test_second_poll_needed( + hass: HomeAssistant, mock_bleak_scanner_start, mock_bluetooth_adapters +): """If a poll is queued, by the time it starts it may no longer be needed.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) @@ -314,7 +322,9 @@ async def test_second_poll_needed(hass: HomeAssistant, mock_bleak_scanner_start) cancel() -async def test_rate_limit(hass: HomeAssistant, mock_bleak_scanner_start): +async def test_rate_limit( + hass: HomeAssistant, mock_bleak_scanner_start, mock_bluetooth_adapters +): """Test error handling and recovery.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) diff --git a/tests/components/bluetooth/test_config_flow.py b/tests/components/bluetooth/test_config_flow.py index aa40666c80a..4c1e8f660b3 100644 --- a/tests/components/bluetooth/test_config_flow.py +++ b/tests/components/bluetooth/test_config_flow.py @@ -18,7 +18,11 @@ from tests.common import MockConfigEntry async def test_options_flow_disabled_not_setup( - hass, hass_ws_client, mock_bleak_scanner_start, macos_adapter + hass, + hass_ws_client, + mock_bleak_scanner_start, + mock_bluetooth_adapters, + macos_adapter, ): """Test options are disabled if the integration has not been setup.""" await async_setup_component(hass, "config", {}) @@ -38,6 +42,7 @@ async def test_options_flow_disabled_not_setup( ) response = await ws_client.receive_json() assert response["result"][0]["supports_options"] is False + await hass.config_entries.async_unload(entry.entry_id) async def test_async_step_user_macos(hass, macos_adapter): @@ -262,7 +267,9 @@ async def test_async_step_integration_discovery_already_exists(hass): assert result["reason"] == "already_configured" -async def test_options_flow_linux(hass, mock_bleak_scanner_start, one_adapter): +async def test_options_flow_linux( + hass, mock_bleak_scanner_start, mock_bluetooth_adapters, one_adapter +): """Test options on Linux.""" entry = MockConfigEntry( domain=DOMAIN, @@ -308,10 +315,15 @@ async def test_options_flow_linux(hass, mock_bleak_scanner_start, one_adapter): assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"][CONF_PASSIVE] is False + await hass.config_entries.async_unload(entry.entry_id) async def test_options_flow_disabled_macos( - hass, hass_ws_client, mock_bleak_scanner_start, macos_adapter + hass, + hass_ws_client, + mock_bleak_scanner_start, + mock_bluetooth_adapters, + macos_adapter, ): """Test options are disabled on MacOS.""" await async_setup_component(hass, "config", {}) @@ -334,10 +346,11 @@ async def test_options_flow_disabled_macos( ) response = await ws_client.receive_json() assert response["result"][0]["supports_options"] is False + await hass.config_entries.async_unload(entry.entry_id) async def test_options_flow_enabled_linux( - hass, hass_ws_client, mock_bleak_scanner_start, one_adapter + hass, hass_ws_client, mock_bleak_scanner_start, mock_bluetooth_adapters, one_adapter ): """Test options are enabled on Linux.""" await async_setup_component(hass, "config", {}) @@ -363,3 +376,4 @@ async def test_options_flow_enabled_linux( ) response = await ws_client.receive_json() assert response["result"][0]["supports_options"] is True + await hass.config_entries.async_unload(entry.entry_id) diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index 33627f6a787..1c3c58bc7ab 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -2446,7 +2446,7 @@ async def test_auto_detect_bluetooth_adapters_linux_multiple(hass, two_adapters) assert len(hass.config_entries.flow.async_progress(bluetooth.DOMAIN)) == 2 -async def test_auto_detect_bluetooth_adapters_linux_none_found(hass): +async def test_auto_detect_bluetooth_adapters_linux_none_found(hass, bluez_dbus_mock): """Test we auto detect bluetooth adapters on linux with no adapters found.""" with patch( "bluetooth_adapters.get_bluetooth_adapter_details", return_value={} diff --git a/tests/components/bluetooth/test_manager.py b/tests/components/bluetooth/test_manager.py index 9ce5985318b..c05b9424508 100644 --- a/tests/components/bluetooth/test_manager.py +++ b/tests/components/bluetooth/test_manager.py @@ -1,10 +1,13 @@ """Tests for the Bluetooth integration manager.""" +from unittest.mock import AsyncMock, MagicMock, patch from bleak.backends.scanner import AdvertisementData, BLEDevice +from bluetooth_adapters import AdvertisementHistory from homeassistant.components import bluetooth from homeassistant.components.bluetooth.manager import STALE_ADVERTISEMENT_SECONDS +from homeassistant.setup import async_setup_component from . import ( inject_advertisement_with_source, @@ -176,3 +179,24 @@ async def test_switching_adapters_based_on_stale(hass, enable_bluetooth): bluetooth.async_ble_device_from_address(hass, address) is switchbot_device_poor_signal_hci1 ) + + +async def test_restore_history_from_dbus(hass, one_adapter): + """Test we can restore history from dbus.""" + address = "AA:BB:CC:CC:CC:FF" + + ble_device = BLEDevice(address, "name") + history = { + address: AdvertisementHistory( + ble_device, AdvertisementData(local_name="name"), "hci0" + ) + } + + with patch( + "bluetooth_adapters.BlueZDBusObjects", + return_value=MagicMock(load=AsyncMock(), history=history), + ): + assert await async_setup_component(hass, bluetooth.DOMAIN, {}) + await hass.async_block_till_done() + + assert bluetooth.async_ble_device_from_address(hass, address) is ble_device diff --git a/tests/components/bluetooth/test_passive_update_coordinator.py b/tests/components/bluetooth/test_passive_update_coordinator.py index bf7c0a48467..15845abc5ac 100644 --- a/tests/components/bluetooth/test_passive_update_coordinator.py +++ b/tests/components/bluetooth/test_passive_update_coordinator.py @@ -59,7 +59,7 @@ class MyCoordinator(PassiveBluetoothDataUpdateCoordinator): super()._async_handle_bluetooth_event(service_info, change) -async def test_basic_usage(hass, mock_bleak_scanner_start): +async def test_basic_usage(hass, mock_bleak_scanner_start, mock_bluetooth_adapters): """Test basic usage of the PassiveBluetoothDataUpdateCoordinator.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) coordinator = MyCoordinator( @@ -88,7 +88,7 @@ async def test_basic_usage(hass, mock_bleak_scanner_start): async def test_context_compatiblity_with_data_update_coordinator( - hass, mock_bleak_scanner_start + hass, mock_bleak_scanner_start, mock_bluetooth_adapters ): """Test contexts can be passed for compatibility with DataUpdateCoordinator.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) @@ -124,7 +124,7 @@ async def test_context_compatiblity_with_data_update_coordinator( async def test_unavailable_callbacks_mark_the_coordinator_unavailable( - hass, mock_bleak_scanner_start + hass, mock_bleak_scanner_start, mock_bluetooth_adapters ): """Test that the coordinator goes unavailable when the bluetooth stack no longer sees the device.""" with patch( @@ -165,7 +165,9 @@ async def test_unavailable_callbacks_mark_the_coordinator_unavailable( assert coordinator.available is False -async def test_passive_bluetooth_coordinator_entity(hass, mock_bleak_scanner_start): +async def test_passive_bluetooth_coordinator_entity( + hass, mock_bleak_scanner_start, mock_bluetooth_adapters +): """Test integration of PassiveBluetoothDataUpdateCoordinator with PassiveBluetoothCoordinatorEntity.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) coordinator = MyCoordinator( diff --git a/tests/components/bluetooth/test_passive_update_processor.py b/tests/components/bluetooth/test_passive_update_processor.py index e7260d22e4b..482f3b3a94f 100644 --- a/tests/components/bluetooth/test_passive_update_processor.py +++ b/tests/components/bluetooth/test_passive_update_processor.py @@ -98,7 +98,7 @@ GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( ) -async def test_basic_usage(hass, mock_bleak_scanner_start): +async def test_basic_usage(hass, mock_bleak_scanner_start, mock_bluetooth_adapters): """Test basic usage of the PassiveBluetoothProcessorCoordinator.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) @@ -196,7 +196,9 @@ async def test_basic_usage(hass, mock_bleak_scanner_start): cancel_coordinator() -async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start): +async def test_unavailable_after_no_data( + hass, mock_bleak_scanner_start, mock_bluetooth_adapters +): """Test that the coordinator is unavailable after no data for a while.""" with patch( "bleak.BleakScanner.discovered_devices", # Must patch before we setup @@ -290,7 +292,9 @@ async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start): cancel_coordinator() -async def test_no_updates_once_stopping(hass, mock_bleak_scanner_start): +async def test_no_updates_once_stopping( + hass, mock_bleak_scanner_start, mock_bluetooth_adapters +): """Test updates are ignored once hass is stopping.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) @@ -343,7 +347,9 @@ async def test_no_updates_once_stopping(hass, mock_bleak_scanner_start): cancel_coordinator() -async def test_exception_from_update_method(hass, caplog, mock_bleak_scanner_start): +async def test_exception_from_update_method( + hass, caplog, mock_bleak_scanner_start, mock_bluetooth_adapters +): """Test we handle exceptions from the update method.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) @@ -406,7 +412,9 @@ async def test_exception_from_update_method(hass, caplog, mock_bleak_scanner_sta cancel_coordinator() -async def test_bad_data_from_update_method(hass, mock_bleak_scanner_start): +async def test_bad_data_from_update_method( + hass, mock_bleak_scanner_start, mock_bluetooth_adapters +): """Test we handle bad data from the update method.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) @@ -758,7 +766,9 @@ GOVEE_B5178_PRIMARY_AND_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE = ( ) -async def test_integration_with_entity(hass, mock_bleak_scanner_start): +async def test_integration_with_entity( + hass, mock_bleak_scanner_start, mock_bluetooth_adapters +): """Test integration of PassiveBluetoothProcessorCoordinator with PassiveBluetoothCoordinatorEntity.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) @@ -888,7 +898,9 @@ NO_DEVICES_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( ) -async def test_integration_with_entity_without_a_device(hass, mock_bleak_scanner_start): +async def test_integration_with_entity_without_a_device( + hass, mock_bleak_scanner_start, mock_bluetooth_adapters +): """Test integration with PassiveBluetoothCoordinatorEntity with no device.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) @@ -950,7 +962,7 @@ async def test_integration_with_entity_without_a_device(hass, mock_bleak_scanner async def test_passive_bluetooth_entity_with_entity_platform( - hass, mock_bleak_scanner_start + hass, mock_bleak_scanner_start, mock_bluetooth_adapters ): """Test with a mock entity platform.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) @@ -1048,7 +1060,9 @@ BINARY_SENSOR_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( ) -async def test_integration_multiple_entity_platforms(hass, mock_bleak_scanner_start): +async def test_integration_multiple_entity_platforms( + hass, mock_bleak_scanner_start, mock_bluetooth_adapters +): """Test integration of PassiveBluetoothProcessorCoordinator with multiple platforms.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) @@ -1138,7 +1152,7 @@ async def test_integration_multiple_entity_platforms(hass, mock_bleak_scanner_st async def test_exception_from_coordinator_update_method( - hass, caplog, mock_bleak_scanner_start + hass, caplog, mock_bleak_scanner_start, mock_bluetooth_adapters ): """Test we handle exceptions from the update method.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) diff --git a/tests/conftest.py b/tests/conftest.py index 4a02083610b..2c95770974b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -991,6 +991,8 @@ def mock_bluetooth_adapters(): """Fixture to mock bluetooth adapters.""" with patch( "homeassistant.components.bluetooth.util.platform.system", return_value="Linux" + ), patch( + "bluetooth_adapters.BlueZDBusObjects", return_value=MagicMock(load=AsyncMock()) ), patch( "bluetooth_adapters.get_bluetooth_adapter_details", return_value={ From 08e6e27a3b74f50247f12d8db47300215076cb0c Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 17 Sep 2022 16:09:40 -0600 Subject: [PATCH 501/955] Remove deprecated Guardian services (scheduled for 2022.10.0) (#78663) Remove deprecated Guardian services (schedule for 2022.10.0) --- homeassistant/components/guardian/__init__.py | 36 ------------------- .../components/guardian/services.yaml | 22 ------------ 2 files changed, 58 deletions(-) diff --git a/homeassistant/components/guardian/__init__.py b/homeassistant/components/guardian/__init__.py index eccf845fcce..63fc66f685d 100644 --- a/homeassistant/components/guardian/__init__.py +++ b/homeassistant/components/guardian/__init__.py @@ -47,8 +47,6 @@ DATA_PAIRED_SENSOR_MANAGER = "paired_sensor_manager" SERVICE_NAME_DISABLE_AP = "disable_ap" SERVICE_NAME_ENABLE_AP = "enable_ap" SERVICE_NAME_PAIR_SENSOR = "pair_sensor" -SERVICE_NAME_REBOOT = "reboot" -SERVICE_NAME_RESET_VALVE_DIAGNOSTICS = "reset_valve_diagnostics" SERVICE_NAME_UNPAIR_SENSOR = "unpair_sensor" SERVICE_NAME_UPGRADE_FIRMWARE = "upgrade_firmware" @@ -56,8 +54,6 @@ SERVICES = ( SERVICE_NAME_DISABLE_AP, SERVICE_NAME_ENABLE_AP, SERVICE_NAME_PAIR_SENSOR, - SERVICE_NAME_REBOOT, - SERVICE_NAME_RESET_VALVE_DIAGNOSTICS, SERVICE_NAME_UNPAIR_SENSOR, SERVICE_NAME_UPGRADE_FIRMWARE, ) @@ -266,32 +262,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await data.client.sensor.pair_sensor(uid) await data.paired_sensor_manager.async_pair_sensor(uid) - @call_with_data - async def async_reboot(call: ServiceCall, data: GuardianData) -> None: - """Reboot the valve controller.""" - async_log_deprecated_service_call( - hass, - call, - "button.press", - f"button.guardian_valve_controller_{data.entry.data[CONF_UID]}_reboot", - "2022.10.0", - ) - await data.client.system.reboot() - - @call_with_data - async def async_reset_valve_diagnostics( - call: ServiceCall, data: GuardianData - ) -> None: - """Fully reset system motor diagnostics.""" - async_log_deprecated_service_call( - hass, - call, - "button.press", - f"button.guardian_valve_controller_{data.entry.data[CONF_UID]}_reset_valve_diagnostics", - "2022.10.0", - ) - await data.client.valve.reset() - @call_with_data async def async_unpair_sensor(call: ServiceCall, data: GuardianData) -> None: """Remove a paired sensor.""" @@ -316,12 +286,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: SERVICE_PAIR_UNPAIR_SENSOR_SCHEMA, async_pair_sensor, ), - (SERVICE_NAME_REBOOT, SERVICE_BASE_SCHEMA, async_reboot), - ( - SERVICE_NAME_RESET_VALVE_DIAGNOSTICS, - SERVICE_BASE_SCHEMA, - async_reset_valve_diagnostics, - ), ( SERVICE_NAME_UNPAIR_SENSOR, SERVICE_PAIR_UNPAIR_SENSOR_SCHEMA, diff --git a/homeassistant/components/guardian/services.yaml b/homeassistant/components/guardian/services.yaml index 0707abb6978..7415ac626a9 100644 --- a/homeassistant/components/guardian/services.yaml +++ b/homeassistant/components/guardian/services.yaml @@ -17,28 +17,6 @@ pair_sensor: example: 5410EC688BCF selector: text: -reboot: - name: Reboot - description: Reboot the device. - fields: - device_id: - name: Valve Controller - description: The valve controller to reboot - required: true - selector: - device: - integration: guardian -reset_valve_diagnostics: - name: Reset Valve Diagnostics - description: Fully (and irrecoverably) reset all valve diagnostics. - fields: - device_id: - name: Valve Controller - description: The valve controller whose diagnostics should be reset - required: true - selector: - device: - integration: guardian unpair_sensor: name: Unpair Sensor description: Remove a paired sensor from the valve controller. From b87c452106f3595427b08da1d2e928db601af2b0 Mon Sep 17 00:00:00 2001 From: Justin Vanderhooft Date: Sat, 17 Sep 2022 18:19:19 -0400 Subject: [PATCH 502/955] Bump melnor-bluetooth to v0.0.20 (#78642) --- homeassistant/components/melnor/manifest.json | 2 +- homeassistant/components/melnor/sensor.py | 49 ++++++++++--------- homeassistant/components/melnor/switch.py | 43 +++++++--------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/melnor/conftest.py | 38 ++++++++++++-- 6 files changed, 79 insertions(+), 57 deletions(-) diff --git a/homeassistant/components/melnor/manifest.json b/homeassistant/components/melnor/manifest.json index a59758f705b..c57549aa647 100644 --- a/homeassistant/components/melnor/manifest.json +++ b/homeassistant/components/melnor/manifest.json @@ -12,5 +12,5 @@ "documentation": "https://www.home-assistant.io/integrations/melnor", "iot_class": "local_polling", "name": "Melnor Bluetooth", - "requirements": ["melnor-bluetooth==0.0.15"] + "requirements": ["melnor-bluetooth==0.0.20"] } diff --git a/homeassistant/components/melnor/sensor.py b/homeassistant/components/melnor/sensor.py index bda70dfee3b..e642df4f9c3 100644 --- a/homeassistant/components/melnor/sensor.py +++ b/homeassistant/components/melnor/sensor.py @@ -38,38 +38,39 @@ class MelnorSensorEntityDescription( """Describes Melnor sensor entity.""" +sensors = [ + MelnorSensorEntityDescription( + device_class=SensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, + key="battery", + name="Battery", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + state_fn=lambda device: device.battery_level, + ), + MelnorSensorEntityDescription( + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + key="rssi", + name="RSSI", + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + state_class=SensorStateClass.MEASUREMENT, + state_fn=lambda device: device.rssi, + ), +] + + async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, - async_add_devices: AddEntitiesCallback, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up the sensor platform.""" coordinator: MelnorDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] - sensors: list[MelnorSensorEntityDescription] = [ - MelnorSensorEntityDescription( - device_class=SensorDeviceClass.BATTERY, - entity_category=EntityCategory.DIAGNOSTIC, - key="battery", - name="Battery", - native_unit_of_measurement=PERCENTAGE, - state_class=SensorStateClass.MEASUREMENT, - state_fn=lambda device: device.battery_level, - ), - MelnorSensorEntityDescription( - device_class=SensorDeviceClass.SIGNAL_STRENGTH, - entity_category=EntityCategory.DIAGNOSTIC, - entity_registry_enabled_default=False, - key="rssi", - name="RSSI", - native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, - state_class=SensorStateClass.MEASUREMENT, - state_fn=lambda device: device.rssi, - ), - ] - - async_add_devices( + async_add_entities( MelnorSensorEntity( coordinator, description, diff --git a/homeassistant/components/melnor/switch.py b/homeassistant/components/melnor/switch.py index 34e0ca331b1..20d95ad99a6 100644 --- a/homeassistant/components/melnor/switch.py +++ b/homeassistant/components/melnor/switch.py @@ -2,7 +2,7 @@ from __future__ import annotations -from collections.abc import Callable +from collections.abc import Callable, Coroutine from dataclasses import dataclass from typing import Any @@ -21,16 +21,11 @@ from .const import DOMAIN from .models import MelnorDataUpdateCoordinator, MelnorZoneEntity -def set_is_watering(valve: Valve, value: bool) -> None: - """Set the is_watering state of a valve.""" - valve.is_watering = value - - @dataclass class MelnorSwitchEntityDescriptionMixin: """Mixin for required keys.""" - on_off_fn: Callable[[Valve, bool], Any] + on_off_fn: Callable[[Valve, bool], Coroutine[Any, Any, None]] state_fn: Callable[[Valve], Any] @@ -41,6 +36,18 @@ class MelnorSwitchEntityDescription( """Describes Melnor switch entity.""" +switches = [ + MelnorSwitchEntityDescription( + device_class=SwitchDeviceClass.SWITCH, + icon="mdi:sprinkler", + key="manual", + name="Manual", + on_off_fn=lambda valve, bool: valve.set_is_watering(bool), + state_fn=lambda valve: valve.is_watering, + ) +] + + async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -56,20 +63,8 @@ async def async_setup_entry( valve = coordinator.data[f"zone{i}"] if valve is not None: - entities.append( - MelnorZoneSwitch( - coordinator, - valve, - MelnorSwitchEntityDescription( - device_class=SwitchDeviceClass.SWITCH, - icon="mdi:sprinkler", - key="manual", - name="Manual", - on_off_fn=set_is_watering, - state_fn=lambda valve: valve.is_watering, - ), - ) - ) + for description in switches: + entities.append(MelnorZoneSwitch(coordinator, valve, description)) async_add_devices(entities) @@ -98,12 +93,10 @@ class MelnorZoneSwitch(MelnorZoneEntity, SwitchEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" - self.entity_description.on_off_fn(self._valve, True) - await self._device.push_state() + await self.entity_description.on_off_fn(self._valve, True) self.async_write_ha_state() async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" - self.entity_description.on_off_fn(self._valve, False) - await self._device.push_state() + await self.entity_description.on_off_fn(self._valve, False) self.async_write_ha_state() diff --git a/requirements_all.txt b/requirements_all.txt index 8ef6d6c1ee7..2bfe314040f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1046,7 +1046,7 @@ mcstatus==6.0.0 meater-python==0.0.8 # homeassistant.components.melnor -melnor-bluetooth==0.0.15 +melnor-bluetooth==0.0.20 # homeassistant.components.message_bird messagebird==1.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 90eeabcba37..2ff915966f0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -748,7 +748,7 @@ mcstatus==6.0.0 meater-python==0.0.8 # homeassistant.components.melnor -melnor-bluetooth==0.0.15 +melnor-bluetooth==0.0.20 # homeassistant.components.meteo_france meteofrance-api==1.0.2 diff --git a/tests/components/melnor/conftest.py b/tests/components/melnor/conftest.py index 5aee6264501..554349109ef 100644 --- a/tests/components/melnor/conftest.py +++ b/tests/components/melnor/conftest.py @@ -6,7 +6,7 @@ from unittest.mock import AsyncMock, patch from bleak.backends.device import BLEDevice from bleak.backends.scanner import AdvertisementData -from melnor_bluetooth.device import Device, Valve +from melnor_bluetooth.device import Device from homeassistant.components.bluetooth.models import BluetoothServiceInfoBleak from homeassistant.components.melnor.const import DOMAIN @@ -52,6 +52,34 @@ FAKE_SERVICE_INFO_2 = BluetoothServiceInfoBleak( ) +class MockedValve: + """Mocked class for a Valve.""" + + _id: int + _is_watering: bool + _manual_watering_minutes: int + + def __init__(self, identifier: int) -> None: + """Initialize a mocked valve.""" + self._id = identifier + self._is_watering = False + self._manual_watering_minutes = 0 + + @property + def id(self) -> int: + """Return the valve id.""" + return self._id + + @property + def is_watering(self): + """Return true if the valve is currently watering.""" + return self._is_watering + + async def set_is_watering(self, is_watering: bool): + """Set the valve to manual watering.""" + self._is_watering = is_watering + + def mock_config_entry(hass: HomeAssistant): """Return a mock config entry.""" @@ -83,10 +111,10 @@ def mock_melnor_device(): device.name = "test_melnor" device.rssi = -50 - device.zone1 = Valve(0, device) - device.zone2 = Valve(1, device) - device.zone3 = Valve(2, device) - device.zone4 = Valve(3, device) + device.zone1 = MockedValve(0) + device.zone2 = MockedValve(1) + device.zone3 = MockedValve(2) + device.zone4 = MockedValve(3) device.__getitem__.side_effect = lambda key: getattr(device, key) From ca5a9c945649f7de9cf58a09f41a33f5ba89b037 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 17 Sep 2022 17:56:45 -0600 Subject: [PATCH 503/955] Allow multiple instances of OpenUV via the `homeassistant.update_entity` service (#76878) * Allow for multiple instances of the OpenUV integration * Docstring * Remove Repairs * Fix tests * Slightly faster OpenUV object lookup * Entity update service * Remove service descriptions * hassfest * Simplify strings * Don't add UI instructions to Repairs item * Add a throttle to entity update * Update homeassistant/components/openuv/__init__.py Co-authored-by: Paulus Schoutsen * Switch from Throttle to Debouncer(s) * Keep dispatcher for services * Reduce change surface area * Duplicate method * Add issue registry through helper * Update deprecation version * Use config entry selector * Remove device/service info * Remove commented out method * Correct entity IDs and better verbiage * Fix tests * Handle missing config entry ID in service calls * Remove unhelpful comment * Remove unused constants Co-authored-by: Paulus Schoutsen Co-authored-by: J. Nick Koston --- homeassistant/components/openuv/__init__.py | 228 +++++++++++++++--- .../components/openuv/binary_sensor.py | 8 + homeassistant/components/openuv/sensor.py | 8 + homeassistant/components/openuv/services.yaml | 24 ++ homeassistant/components/openuv/strings.json | 10 + .../components/openuv/translations/en.json | 10 + tests/components/openuv/conftest.py | 2 + tests/components/openuv/test_diagnostics.py | 5 +- 8 files changed, 267 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/openuv/__init__.py b/homeassistant/components/openuv/__init__.py index 29b13ff5258..bef5bb23059 100644 --- a/homeassistant/components/openuv/__init__.py +++ b/homeassistant/components/openuv/__init__.py @@ -2,12 +2,14 @@ from __future__ import annotations import asyncio +from collections.abc import Callable from typing import Any from pyopenuv import Client from pyopenuv.errors import OpenUvError +import voluptuous as vol -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.const import ( CONF_API_KEY, CONF_BINARY_SENSORS, @@ -19,12 +21,18 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError -from homeassistant.helpers import aiohttp_client +from homeassistant.helpers import ( + aiohttp_client, + config_validation as cv, + entity_registry, +) +from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) from homeassistant.helpers.entity import Entity, EntityDescription +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.service import verify_domain_control from .const import ( @@ -38,15 +46,81 @@ from .const import ( LOGGER, ) -DEFAULT_ATTRIBUTION = "Data provided by OpenUV" +CONF_ENTRY_ID = "entry_id" -NOTIFICATION_ID = "openuv_notification" -NOTIFICATION_TITLE = "OpenUV Component Setup" +DEFAULT_DEBOUNCER_COOLDOWN_SECONDS = 15 * 60 TOPIC_UPDATE = f"{DOMAIN}_data_update" PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] +SERVICE_NAME_UPDATE_DATA = "update_data" +SERVICE_NAME_UPDATE_PROTECTION_DATA = "update_protection_data" +SERVICE_NAME_UPDATE_UV_INDEX_DATA = "update_uv_index_data" + +SERVICES = ( + SERVICE_NAME_UPDATE_DATA, + SERVICE_NAME_UPDATE_PROTECTION_DATA, + SERVICE_NAME_UPDATE_UV_INDEX_DATA, +) + + +@callback +def async_get_entity_id_from_unique_id_suffix( + hass: HomeAssistant, entry: ConfigEntry, unique_id_suffix: str +) -> str: + """Get the entity ID for a config entry based on unique ID suffix.""" + ent_reg = entity_registry.async_get(hass) + [registry_entry] = [ + registry_entry + for registry_entry in ent_reg.entities.values() + if registry_entry.config_entry_id == entry.entry_id + and registry_entry.unique_id.endswith(unique_id_suffix) + ] + return registry_entry.entity_id + + +@callback +def async_log_deprecated_service_call( + hass: HomeAssistant, + call: ServiceCall, + alternate_service: str, + alternate_targets: list[str], + breaks_in_ha_version: str, +) -> None: + """Log a warning about a deprecated service call.""" + deprecated_service = f"{call.domain}.{call.service}" + + if len(alternate_targets) > 1: + translation_key = "deprecated_service_multiple_alternate_targets" + else: + translation_key = "deprecated_service_single_alternate_target" + + async_create_issue( + hass, + DOMAIN, + f"deprecated_service_{deprecated_service}", + breaks_in_ha_version=breaks_in_ha_version, + is_fixable=False, + is_persistent=True, + severity=IssueSeverity.WARNING, + translation_key=translation_key, + translation_placeholders={ + "alternate_service": alternate_service, + "alternate_targets": ", ".join(alternate_targets), + "deprecated_service": deprecated_service, + }, + ) + + LOGGER.warning( + ( + 'The "%s" service is deprecated and will be removed in %s; review the ' + "Repairs item in the UI for more information" + ), + deprecated_service, + breaks_in_ha_version, + ) + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up OpenUV as config entry.""" @@ -54,6 +128,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: websession = aiohttp_client.async_get_clientsession(hass) openuv = OpenUV( + hass, entry, Client( entry.data[CONF_API_KEY], @@ -82,33 +157,90 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + @callback + def extract_openuv(func: Callable) -> Callable: + """Define a decorator to get the correct OpenUV object for a service call.""" + + async def wrapper(call: ServiceCall) -> None: + """Wrap the service function.""" + openuv: OpenUV = hass.data[DOMAIN][call.data[CONF_ENTRY_ID]] + + try: + await func(call, openuv) + except OpenUvError as err: + raise HomeAssistantError( + f'Error while executing "{call.service}": {err}' + ) from err + + return wrapper + + # We determine entity IDs needed to help the user migrate from deprecated services: + current_uv_index_entity_id = async_get_entity_id_from_unique_id_suffix( + hass, entry, "current_uv_index" + ) + protection_window_entity_id = async_get_entity_id_from_unique_id_suffix( + hass, entry, "protection_window" + ) + @_verify_domain_control - async def update_data(_: ServiceCall) -> None: + @extract_openuv + async def update_data(call: ServiceCall, openuv: OpenUV) -> None: """Refresh all OpenUV data.""" LOGGER.debug("Refreshing all OpenUV data") + async_log_deprecated_service_call( + hass, + call, + "homeassistant.update_entity", + [protection_window_entity_id, current_uv_index_entity_id], + "2022.12.0", + ) await openuv.async_update() async_dispatcher_send(hass, TOPIC_UPDATE) @_verify_domain_control - async def update_uv_index_data(_: ServiceCall) -> None: + @extract_openuv + async def update_uv_index_data(call: ServiceCall, openuv: OpenUV) -> None: """Refresh OpenUV UV index data.""" LOGGER.debug("Refreshing OpenUV UV index data") + async_log_deprecated_service_call( + hass, + call, + "homeassistant.update_entity", + [current_uv_index_entity_id], + "2022.12.0", + ) await openuv.async_update_uv_index_data() async_dispatcher_send(hass, TOPIC_UPDATE) @_verify_domain_control - async def update_protection_data(_: ServiceCall) -> None: + @extract_openuv + async def update_protection_data(call: ServiceCall, openuv: OpenUV) -> None: """Refresh OpenUV protection window data.""" LOGGER.debug("Refreshing OpenUV protection window data") + async_log_deprecated_service_call( + hass, + call, + "homeassistant.update_entity", + [protection_window_entity_id], + "2022.12.0", + ) await openuv.async_update_protection_data() async_dispatcher_send(hass, TOPIC_UPDATE) + service_schema = vol.Schema( + { + vol.Optional(CONF_ENTRY_ID, default=entry.entry_id): cv.string, + } + ) + for service, method in ( - ("update_data", update_data), - ("update_uv_index_data", update_uv_index_data), - ("update_protection_data", update_protection_data), + (SERVICE_NAME_UPDATE_DATA, update_data), + (SERVICE_NAME_UPDATE_UV_INDEX_DATA, update_uv_index_data), + (SERVICE_NAME_UPDATE_PROTECTION_DATA, update_protection_data), ): - hass.services.async_register(DOMAIN, service, method) + if hass.services.has_service(DOMAIN, service): + continue + hass.services.async_register(DOMAIN, service, method, schema=service_schema) return True @@ -119,6 +251,17 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) + loaded_entries = [ + entry + for entry in hass.config_entries.async_entries(DOMAIN) + if entry.state == ConfigEntryState.LOADED + ] + if len(loaded_entries) == 1: + # If this is the last loaded instance of OpenUV, deregister any services + # defined during integration setup: + for service_name in SERVICES: + hass.services.async_remove(DOMAIN, service_name) + return unload_ok @@ -143,13 +286,29 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class OpenUV: """Define a generic OpenUV object.""" - def __init__(self, entry: ConfigEntry, client: Client) -> None: + def __init__(self, hass: HomeAssistant, entry: ConfigEntry, client: Client) -> None: """Initialize.""" + self._update_protection_data_debouncer = Debouncer( + hass, + LOGGER, + cooldown=DEFAULT_DEBOUNCER_COOLDOWN_SECONDS, + immediate=True, + function=self._async_update_protection_data, + ) + + self._update_uv_index_data_debouncer = Debouncer( + hass, + LOGGER, + cooldown=DEFAULT_DEBOUNCER_COOLDOWN_SECONDS, + immediate=True, + function=self._async_update_uv_index_data, + ) + self._entry = entry self.client = client self.data: dict[str, Any] = {DATA_PROTECTION_WINDOW: {}, DATA_UV: {}} - async def async_update_protection_data(self) -> None: + async def _async_update_protection_data(self) -> None: """Update binary sensor (protection window) data.""" low = self._entry.options.get(CONF_FROM_WINDOW, DEFAULT_FROM_WINDOW) high = self._entry.options.get(CONF_TO_WINDOW, DEFAULT_TO_WINDOW) @@ -163,7 +322,7 @@ class OpenUV: self.data[DATA_PROTECTION_WINDOW] = data.get("result") - async def async_update_uv_index_data(self) -> None: + async def _async_update_uv_index_data(self) -> None: """Update sensor (uv index, etc) data.""" try: data = await self.client.uv_index() @@ -174,6 +333,14 @@ class OpenUV: self.data[DATA_UV] = data.get("result") + async def async_update_protection_data(self) -> None: + """Update binary sensor (protection window) data with a debouncer.""" + await self._update_protection_data_debouncer.async_call() + + async def async_update_uv_index_data(self) -> None: + """Update sensor (uv index, etc) data with a debouncer.""" + await self._update_uv_index_data_debouncer.async_call() + async def async_update(self) -> None: """Update sensor/binary sensor data.""" tasks = [self.async_update_protection_data(), self.async_update_uv_index_data()] @@ -187,26 +354,33 @@ class OpenUvEntity(Entity): def __init__(self, openuv: OpenUV, description: EntityDescription) -> None: """Initialize.""" + coordinates = f"{openuv.client.latitude}, {openuv.client.longitude}" self._attr_extra_state_attributes = {} self._attr_should_poll = False - self._attr_unique_id = ( - f"{openuv.client.latitude}_{openuv.client.longitude}_{description.key}" - ) + self._attr_unique_id = f"{coordinates}_{description.key}" self.entity_description = description self.openuv = openuv + @callback + def async_update_state(self) -> None: + """Update the state.""" + self.update_from_latest_data() + self.async_write_ha_state() + async def async_added_to_hass(self) -> None: """Register callbacks.""" - - @callback - def update() -> None: - """Update the state.""" - self.update_from_latest_data() - self.async_write_ha_state() - - self.async_on_remove(async_dispatcher_connect(self.hass, TOPIC_UPDATE, update)) - self.update_from_latest_data() + self.async_on_remove( + async_dispatcher_connect(self.hass, TOPIC_UPDATE, self.async_update_state) + ) + + async def async_update(self) -> None: + """Update the entity. + + Only used by the generic entity update service. Should be implemented by each + OpenUV platform. + """ + raise NotImplementedError def update_from_latest_data(self) -> None: """Update the sensor using the latest data.""" diff --git a/homeassistant/components/openuv/binary_sensor.py b/homeassistant/components/openuv/binary_sensor.py index 757f0479e01..b1c962932b7 100644 --- a/homeassistant/components/openuv/binary_sensor.py +++ b/homeassistant/components/openuv/binary_sensor.py @@ -36,6 +36,14 @@ async def async_setup_entry( class OpenUvBinarySensor(OpenUvEntity, BinarySensorEntity): """Define a binary sensor for OpenUV.""" + async def async_update(self) -> None: + """Update the entity. + + Only used by the generic entity update service. + """ + await self.openuv.async_update_protection_data() + self.async_update_state() + @callback def update_from_latest_data(self) -> None: """Update the state.""" diff --git a/homeassistant/components/openuv/sensor.py b/homeassistant/components/openuv/sensor.py index 3a5bd3c2a47..ff28062da37 100644 --- a/homeassistant/components/openuv/sensor.py +++ b/homeassistant/components/openuv/sensor.py @@ -131,6 +131,14 @@ async def async_setup_entry( class OpenUvSensor(OpenUvEntity, SensorEntity): """Define a binary sensor for OpenUV.""" + async def async_update(self) -> None: + """Update the entity. + + Only used by the generic entity update service. + """ + await self.openuv.async_update_uv_index_data() + self.async_update_state() + @callback def update_from_latest_data(self) -> None: """Update the state.""" diff --git a/homeassistant/components/openuv/services.yaml b/homeassistant/components/openuv/services.yaml index e4886dfa7d8..3e2e6ab0087 100644 --- a/homeassistant/components/openuv/services.yaml +++ b/homeassistant/components/openuv/services.yaml @@ -2,11 +2,35 @@ update_data: name: Update data description: Request new data from OpenUV. Consumes two API calls. + fields: + entry_id: + name: Config Entry + description: The configured instance of the OpenUV integration to use + required: true + selector: + config_entry: + integration: openuv update_uv_index_data: name: Update UV index data description: Request new UV index data from OpenUV. + fields: + entry_id: + name: Config Entry + description: The configured instance of the OpenUV integration to use + required: true + selector: + config_entry: + integration: openuv update_protection_data: name: Update protection data description: Request new protection window data from OpenUV. + fields: + entry_id: + name: Config Entry + description: The configured instance of the OpenUV integration to use + required: true + selector: + config_entry: + integration: openuv diff --git a/homeassistant/components/openuv/strings.json b/homeassistant/components/openuv/strings.json index cd9ec36d93a..84a093280f3 100644 --- a/homeassistant/components/openuv/strings.json +++ b/homeassistant/components/openuv/strings.json @@ -28,5 +28,15 @@ } } } + }, + "issues": { + "deprecated_service_multiple_alternate_targets": { + "title": "The {deprecated_service} service is being removed", + "description": "Update any automations or scripts that use this service to instead use the `{alternate_service}` service with one of these entity IDs as the target: `{alternate_targets}`." + }, + "deprecated_service_single_alternate_target": { + "title": "The {deprecated_service} service is being removed", + "description": "Update any automations or scripts that use this service to instead use the `{alternate_service}` service with `{alternate_targets}` as the target." + } } } diff --git a/homeassistant/components/openuv/translations/en.json b/homeassistant/components/openuv/translations/en.json index 92ca71cd46f..3879a4d7d44 100644 --- a/homeassistant/components/openuv/translations/en.json +++ b/homeassistant/components/openuv/translations/en.json @@ -18,6 +18,16 @@ } } }, + "issues": { + "deprecated_service_multiple_alternate_targets": { + "description": "Update any automations or scripts that use this service to instead use the `{alternate_service}` service with one of these entity IDs as the target: `{alternate_targets}`.", + "title": "The {deprecated_service} service is being removed" + }, + "deprecated_service_single_alternate_target": { + "description": "Update any automations or scripts that use this service to instead use the `{alternate_service}` service with `{alternate_targets}` as the target.", + "title": "The {deprecated_service} service is being removed" + } + }, "options": { "step": { "init": { diff --git a/tests/components/openuv/conftest.py b/tests/components/openuv/conftest.py index 859769f2b58..c39a84b8b4c 100644 --- a/tests/components/openuv/conftest.py +++ b/tests/components/openuv/conftest.py @@ -56,6 +56,8 @@ def data_uv_index_fixture(): async def setup_openuv_fixture(hass, config, data_protection_window, data_uv_index): """Define a fixture to set up OpenUV.""" with patch( + "homeassistant.components.openuv.async_get_entity_id_from_unique_id_suffix", + ), patch( "homeassistant.components.openuv.Client.uv_index", return_value=data_uv_index ), patch( "homeassistant.components.openuv.Client.uv_protection_window", diff --git a/tests/components/openuv/test_diagnostics.py b/tests/components/openuv/test_diagnostics.py index 4999e5d2132..1196045300b 100644 --- a/tests/components/openuv/test_diagnostics.py +++ b/tests/components/openuv/test_diagnostics.py @@ -1,12 +1,15 @@ """Test OpenUV diagnostics.""" from homeassistant.components.diagnostics import REDACTED +from homeassistant.components.openuv import CONF_ENTRY_ID from tests.components.diagnostics import get_diagnostics_for_config_entry async def test_entry_diagnostics(hass, config_entry, hass_client, setup_openuv): """Test config entry diagnostics.""" - await hass.services.async_call("openuv", "update_data") + await hass.services.async_call( + "openuv", "update_data", service_data={CONF_ENTRY_ID: "test_entry_id"} + ) assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { "entry": { "data": { From e9eb5dc3389c4f69cbe7abd9ec5b743a068c0560 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 18 Sep 2022 00:29:50 +0000 Subject: [PATCH 504/955] [ci skip] Translation update --- .../amberelectric/translations/ja.json | 5 ++++ .../android_ip_webcam/translations/fr.json | 5 ++++ .../components/anthemav/translations/fr.json | 5 ++++ .../bluemaestro/translations/ja.json | 15 ++++++++++ .../components/fibaro/translations/ja.json | 9 ++++-- .../components/google/translations/fr.json | 5 ++++ .../google_sheets/translations/ja.json | 23 ++++++++++++++ .../components/guardian/translations/ca.json | 2 +- .../components/guardian/translations/de.json | 13 +++++++- .../components/guardian/translations/fr.json | 24 +++++++++++++++ .../components/lametric/translations/ja.json | 3 +- .../lametric/translations/pt-BR.json | 3 +- .../components/lametric/translations/ru.json | 3 +- .../litterrobot/translations/sensor.de.json | 3 ++ .../litterrobot/translations/sensor.fr.json | 3 ++ .../components/mqtt/translations/fr.json | 5 ++++ .../components/nest/translations/fr.json | 2 +- .../openexchangerates/translations/fr.json | 5 ++++ .../components/openuv/translations/de.json | 10 +++++++ .../components/pushover/translations/fr.json | 5 ++++ .../radiotherm/translations/fr.json | 5 ++++ .../simplepush/translations/fr.json | 2 +- .../simplisafe/translations/ca.json | 6 ++++ .../simplisafe/translations/de.json | 6 ++++ .../simplisafe/translations/fr.json | 1 + .../simplisafe/translations/pt-BR.json | 6 ++++ .../simplisafe/translations/ru.json | 6 ++++ .../simplisafe/translations/uk.json | 6 ++++ .../simplisafe/translations/zh-Hant.json | 6 ++++ .../soundtouch/translations/fr.json | 2 +- .../speedtestdotnet/translations/fr.json | 13 ++++++++ .../components/switchbee/translations/ja.json | 30 +++++++++++++++++++ .../components/switchbee/translations/ko.json | 21 +++++++++++++ .../components/tilt_ble/translations/ja.json | 21 +++++++++++++ .../volvooncall/translations/fr.json | 5 ++++ .../components/xbox/translations/fr.json | 2 +- .../components/zha/translations/ja.json | 12 ++++---- 37 files changed, 282 insertions(+), 16 deletions(-) create mode 100644 homeassistant/components/google_sheets/translations/ja.json create mode 100644 homeassistant/components/switchbee/translations/ja.json create mode 100644 homeassistant/components/switchbee/translations/ko.json create mode 100644 homeassistant/components/tilt_ble/translations/ja.json diff --git a/homeassistant/components/amberelectric/translations/ja.json b/homeassistant/components/amberelectric/translations/ja.json index 0c061b26112..bc39623d399 100644 --- a/homeassistant/components/amberelectric/translations/ja.json +++ b/homeassistant/components/amberelectric/translations/ja.json @@ -1,5 +1,10 @@ { "config": { + "error": { + "invalid_api_token": "\u7121\u52b9\u306aAPI\u30ad\u30fc", + "no_site": "\u30b5\u30a4\u30c8\u306e\u63d0\u4f9b\u306a\u3057", + "unknown_error": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "site": { "data": { diff --git a/homeassistant/components/android_ip_webcam/translations/fr.json b/homeassistant/components/android_ip_webcam/translations/fr.json index 19f42be4376..6381ce09051 100644 --- a/homeassistant/components/android_ip_webcam/translations/fr.json +++ b/homeassistant/components/android_ip_webcam/translations/fr.json @@ -17,5 +17,10 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "title": "La configuration YAML pour webcam IP Android sera bient\u00f4t supprim\u00e9e" + } } } \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/fr.json b/homeassistant/components/anthemav/translations/fr.json index faf417552ce..d08ef70d6b7 100644 --- a/homeassistant/components/anthemav/translations/fr.json +++ b/homeassistant/components/anthemav/translations/fr.json @@ -15,5 +15,10 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "title": "La configuration YAML pour les r\u00e9cepteurs A/V Anthem sera bient\u00f4t supprim\u00e9e" + } } } \ No newline at end of file diff --git a/homeassistant/components/bluemaestro/translations/ja.json b/homeassistant/components/bluemaestro/translations/ja.json index bab0033cc32..7e4f5db8e3b 100644 --- a/homeassistant/components/bluemaestro/translations/ja.json +++ b/homeassistant/components/bluemaestro/translations/ja.json @@ -1,7 +1,22 @@ { "config": { "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", + "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", "not_supported": "\u30c7\u30d0\u30a4\u30b9\u304c\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "{name} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" + }, + "user": { + "data": { + "address": "\u30c7\u30d0\u30a4\u30b9" + }, + "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3059\u308b\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044" + } } } } \ No newline at end of file diff --git a/homeassistant/components/fibaro/translations/ja.json b/homeassistant/components/fibaro/translations/ja.json index a2b94a4bb17..e99a5645343 100644 --- a/homeassistant/components/fibaro/translations/ja.json +++ b/homeassistant/components/fibaro/translations/ja.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", @@ -10,7 +11,11 @@ }, "step": { "reauth_confirm": { - "description": "{username}\u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u66f4\u65b0\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + }, + "description": "{username}\u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u66f4\u65b0\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "user": { "data": { diff --git a/homeassistant/components/google/translations/fr.json b/homeassistant/components/google/translations/fr.json index 389f769cdb9..b404fbb29be 100644 --- a/homeassistant/components/google/translations/fr.json +++ b/homeassistant/components/google/translations/fr.json @@ -33,6 +33,11 @@ } } }, + "issues": { + "deprecated_yaml": { + "title": "La configuration YAML pour Google\u00a0Agenda sera bient\u00f4t supprim\u00e9e" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/google_sheets/translations/ja.json b/homeassistant/components/google_sheets/translations/ja.json new file mode 100644 index 00000000000..e37b4517358 --- /dev/null +++ b/homeassistant/components/google_sheets/translations/ja.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "invalid_access_token": "\u7121\u52b9\u306a\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3", + "missing_configuration": "\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", + "oauth_error": "\u7121\u52b9\u306a\u30c8\u30fc\u30af\u30f3\u30c7\u30fc\u30bf\u3092\u53d7\u4fe1\u3057\u307e\u3057\u305f\u3002", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", + "timeout_connect": "\u63a5\u7d9a\u78ba\u7acb\u6642\u306b\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "step": { + "auth": { + "title": "Google\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u30ea\u30f3\u30af\u3059\u308b" + }, + "pick_implementation": { + "title": "\u8a8d\u8a3c\u65b9\u6cd5\u306e\u9078\u629e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/ca.json b/homeassistant/components/guardian/translations/ca.json index a338db67446..cfe8675b70c 100644 --- a/homeassistant/components/guardian/translations/ca.json +++ b/homeassistant/components/guardian/translations/ca.json @@ -23,7 +23,7 @@ "fix_flow": { "step": { "confirm": { - "description": "Actualitza totes les automatitzacions o 'scripts' que utilitzin aquest servei perqu\u00e8 passin a utilitzar el servei `{alternate_service}` amb un ID d'entitat objectiu o 'target' `{alternate_target}`. Despr\u00e9s, fes clic a ENVIAR per marcar aquest problema com a resolt.", + "description": "Actualitza totes les automatitzacions o 'scripts' que utilitzin aquest servei perqu\u00e8 passin a utilitzar el servei `{alternate_service}` amb un ID d'entitat objectiu o 'target' `{alternate_target}`. Despr\u00e9s, fes clic a ENVIA per marcar aquest problema com a resolt.", "title": "El servei {deprecated_service} est\u00e0 sent eliminat" } } diff --git a/homeassistant/components/guardian/translations/de.json b/homeassistant/components/guardian/translations/de.json index 33d2973317f..76c18651998 100644 --- a/homeassistant/components/guardian/translations/de.json +++ b/homeassistant/components/guardian/translations/de.json @@ -23,12 +23,23 @@ "fix_flow": { "step": { "confirm": { - "description": "Aktualisiere alle Automatisierungen oder Skripte, die diesen Dienst verwenden, um stattdessen den Dienst `{alternate_service}` mit einer Zielentit\u00e4ts-ID von `{alternate_target}` zu verwenden. Dr\u00fccke dann unten auf SENDEN, um dieses Problem als behoben zu markieren.", + "description": "Aktualisiere alle Automatisierungen oder Skripte, die diesen Dienst verwenden, um stattdessen den Dienst `{alternate_service}` mit einer Ziel-Entit\u00e4ts-ID von `{alternate_target}` zu verwenden.", "title": "Der Dienst {deprecated_service} wird entfernt" } } }, "title": "Der Dienst {deprecated_service} wird entfernt" + }, + "replaced_old_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "Diese Entit\u00e4t wurde durch `{replacement_entity_id}` ersetzt.", + "title": "Die Entit\u00e4t {old_entity_id} wird entfernt" + } + } + }, + "title": "Die Entit\u00e4t {old_entity_id} wird entfernt" } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/fr.json b/homeassistant/components/guardian/translations/fr.json index d76ec4886e3..1cb1436cae2 100644 --- a/homeassistant/components/guardian/translations/fr.json +++ b/homeassistant/components/guardian/translations/fr.json @@ -17,5 +17,29 @@ "description": "Configurez un appareil Elexa Guardian local." } } + }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Modifiez tout script ou automatisation utilisant ce service afin qu'ils utilisent \u00e0 la place le service `{alternate_service}` avec pour ID d'entit\u00e9 cible `{alternate_target}`.", + "title": "Le service {deprecated_service} sera bient\u00f4t supprim\u00e9" + } + } + }, + "title": "Le service {deprecated_service} sera bient\u00f4t supprim\u00e9" + }, + "replaced_old_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "Cette entit\u00e9 a \u00e9t\u00e9 remplac\u00e9e par `{replacement_entity_id}`.", + "title": "L'entit\u00e9 {old_entity_id} sera supprim\u00e9e" + } + } + }, + "title": "L'entit\u00e9 {old_entity_id} sera supprim\u00e9e" + } } } \ No newline at end of file diff --git a/homeassistant/components/lametric/translations/ja.json b/homeassistant/components/lametric/translations/ja.json index 5fc913a12b7..c3f0da2866c 100644 --- a/homeassistant/components/lametric/translations/ja.json +++ b/homeassistant/components/lametric/translations/ja.json @@ -7,7 +7,8 @@ "link_local_address": "\u30ed\u30fc\u30ab\u30eb\u30a2\u30c9\u30ec\u30b9\u306e\u30ea\u30f3\u30af\u306b\u306f\u5bfe\u5fdc\u3057\u3066\u3044\u307e\u305b\u3093", "missing_configuration": "LaMetric\u306e\u7d71\u5408\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", "no_devices": "\u8a31\u53ef\u3055\u308c\u305f\u30e6\u30fc\u30b6\u30fc\u306f\u3001LaMetric\u30c7\u30d0\u30a4\u30b9\u3092\u6301\u3063\u3066\u3044\u307e\u305b\u3093", - "no_url_available": "\u4f7f\u7528\u53ef\u80fd\u306aURL\u304c\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u306e\u30a8\u30e9\u30fc\u306e\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001[\u30d8\u30eb\u30d7\u30bb\u30af\u30b7\u30e7\u30f3\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044]({docs_url})" + "no_url_available": "\u4f7f\u7528\u53ef\u80fd\u306aURL\u304c\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u306e\u30a8\u30e9\u30fc\u306e\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001[\u30d8\u30eb\u30d7\u30bb\u30af\u30b7\u30e7\u30f3\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044]({docs_url})", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", diff --git a/homeassistant/components/lametric/translations/pt-BR.json b/homeassistant/components/lametric/translations/pt-BR.json index 7349baeb8dc..b9834dbfa2f 100644 --- a/homeassistant/components/lametric/translations/pt-BR.json +++ b/homeassistant/components/lametric/translations/pt-BR.json @@ -7,7 +7,8 @@ "link_local_address": "Endere\u00e7os locais de links n\u00e3o s\u00e3o suportados", "missing_configuration": "A integra\u00e7\u00e3o LaMetric n\u00e3o est\u00e1 configurada. Por favor, siga a documenta\u00e7\u00e3o.", "no_devices": "O usu\u00e1rio autorizado n\u00e3o possui dispositivos LaMetric", - "no_url_available": "N\u00e3o h\u00e1 URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre esse erro, [verifique a se\u00e7\u00e3o de ajuda]({docs_url})" + "no_url_available": "N\u00e3o h\u00e1 URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre esse erro, [verifique a se\u00e7\u00e3o de ajuda]({docs_url})", + "unknown": "Erro inesperado" }, "error": { "cannot_connect": "Falha ao conectar", diff --git a/homeassistant/components/lametric/translations/ru.json b/homeassistant/components/lametric/translations/ru.json index 34a1bb58a62..cf9324b9abe 100644 --- a/homeassistant/components/lametric/translations/ru.json +++ b/homeassistant/components/lametric/translations/ru.json @@ -7,7 +7,8 @@ "link_local_address": "\u0421\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0435 \u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f.", "missing_configuration": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f LaMetric \u043d\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438.", "no_devices": "\u0423 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u043e\u0432\u0430\u043d\u043d\u043e\u0433\u043e \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u043d\u0435\u0442 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 LaMetric.", - "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d. \u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e\u0431 \u044d\u0442\u043e\u0439 \u043e\u0448\u0438\u0431\u043a\u0435." + "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d. \u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e\u0431 \u044d\u0442\u043e\u0439 \u043e\u0448\u0438\u0431\u043a\u0435.", + "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.", diff --git a/homeassistant/components/litterrobot/translations/sensor.de.json b/homeassistant/components/litterrobot/translations/sensor.de.json index 2901b0e5c55..078faa79d6a 100644 --- a/homeassistant/components/litterrobot/translations/sensor.de.json +++ b/homeassistant/components/litterrobot/translations/sensor.de.json @@ -4,6 +4,7 @@ "br": "Haube entfernt", "ccc": "Reinigungszyklus abgeschlossen", "ccp": "Reinigungszyklus l\u00e4uft", + "cd": "Katze erkannt", "csf": "Katzensensor Fehler", "csi": "Katzensensor unterbrochen", "cst": "Katzensensor Timing", @@ -19,6 +20,8 @@ "otf": "\u00dcberdrehungsfehler", "p": "Pausiert", "pd": "Einklemmen erkennen", + "pwrd": "F\u00e4hrt herunter", + "pwru": "F\u00e4hrt hoch", "rdy": "Bereit", "scf": "Katzen-Sensorfehler beim Start", "sdf": "Schublade voll beim Start", diff --git a/homeassistant/components/litterrobot/translations/sensor.fr.json b/homeassistant/components/litterrobot/translations/sensor.fr.json index fba0796e2b0..ab21dd9b3e4 100644 --- a/homeassistant/components/litterrobot/translations/sensor.fr.json +++ b/homeassistant/components/litterrobot/translations/sensor.fr.json @@ -4,6 +4,7 @@ "br": "Capot retir\u00e9", "ccc": "Cycle de nettoyage termin\u00e9", "ccp": "Cycle de nettoyage en cours", + "cd": "Chat d\u00e9tect\u00e9", "csf": "D\u00e9faut du capteur de chat", "csi": "Interruption du capteur de chat", "cst": "Minutage du capteur de chat", @@ -19,6 +20,8 @@ "otf": "D\u00e9faut de surcouple", "p": "En pause", "pd": "D\u00e9tection de pincement", + "pwrd": "Mise hors tension", + "pwru": "Mise sous tension", "rdy": "Pr\u00eat", "scf": "D\u00e9faut du capteur de chat au d\u00e9marrage", "sdf": "Tiroir plein au d\u00e9marrage", diff --git a/homeassistant/components/mqtt/translations/fr.json b/homeassistant/components/mqtt/translations/fr.json index b84aec15a80..87ce8c2bdee 100644 --- a/homeassistant/components/mqtt/translations/fr.json +++ b/homeassistant/components/mqtt/translations/fr.json @@ -49,6 +49,11 @@ "button_triple_press": "\u00ab\u00a0{subtype}\u00a0\u00bb triple-cliqu\u00e9" } }, + "issues": { + "deprecated_yaml": { + "title": "Un\u00b7e ou plusieurs {plateform}\u00b7e\u00b7s MQTT configur\u00e9\u00b7e\u00b7s manuellement requi\u00e8rent votre attention" + } + }, "options": { "error": { "bad_birth": "Sujet de la naissance non valide.", diff --git a/homeassistant/components/nest/translations/fr.json b/homeassistant/components/nest/translations/fr.json index 7cbed195e0c..4d7f2d35f06 100644 --- a/homeassistant/components/nest/translations/fr.json +++ b/homeassistant/components/nest/translations/fr.json @@ -91,7 +91,7 @@ }, "issues": { "deprecated_yaml": { - "title": "La configuration YAML pour Nest est en cours de suppression" + "title": "La configuration YAML pour Nest sera bient\u00f4t supprim\u00e9e" } } } \ No newline at end of file diff --git a/homeassistant/components/openexchangerates/translations/fr.json b/homeassistant/components/openexchangerates/translations/fr.json index a6b5929245a..2e8b1c42cac 100644 --- a/homeassistant/components/openexchangerates/translations/fr.json +++ b/homeassistant/components/openexchangerates/translations/fr.json @@ -23,5 +23,10 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "title": "La configuration YAML pour Open Exchange Rates sera bient\u00f4t supprim\u00e9e" + } } } \ No newline at end of file diff --git a/homeassistant/components/openuv/translations/de.json b/homeassistant/components/openuv/translations/de.json index abc32f68f02..94d8b49b7d5 100644 --- a/homeassistant/components/openuv/translations/de.json +++ b/homeassistant/components/openuv/translations/de.json @@ -18,6 +18,16 @@ } } }, + "issues": { + "deprecated_service_multiple_alternate_targets": { + "description": "Aktualisiere alle Automatisierungen oder Skripte, die diesen Dienst verwenden, um stattdessen den Dienst `{alternate_service}` mit einer dieser Entit\u00e4ts-IDs als Ziel zu verwenden: `{alternate_targets}`.", + "title": "Der Dienst {deprecated_service} wird entfernt" + }, + "deprecated_service_single_alternate_target": { + "description": "Aktualisiere alle Automatisierungen oder Skripte, die diesen Dienst verwenden, um stattdessen den Dienst `{alternate_service}` mit `{alternate_targets}` als Ziel zu verwenden.", + "title": "Der Dienst {deprecated_service} wird entfernt" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/pushover/translations/fr.json b/homeassistant/components/pushover/translations/fr.json index a8d7a213d64..2982b17064c 100644 --- a/homeassistant/components/pushover/translations/fr.json +++ b/homeassistant/components/pushover/translations/fr.json @@ -24,5 +24,10 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "title": "La configuration YAML pour Pushover sera bient\u00f4t supprim\u00e9e" + } } } \ No newline at end of file diff --git a/homeassistant/components/radiotherm/translations/fr.json b/homeassistant/components/radiotherm/translations/fr.json index 47705ce5138..ed56ecb8ac6 100644 --- a/homeassistant/components/radiotherm/translations/fr.json +++ b/homeassistant/components/radiotherm/translations/fr.json @@ -19,6 +19,11 @@ } } }, + "issues": { + "deprecated_yaml": { + "title": "La configuration YAML pour Radio\u00a0Thermostat sera bient\u00f4t supprim\u00e9e" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/simplepush/translations/fr.json b/homeassistant/components/simplepush/translations/fr.json index e92ef0263d0..508d31437cb 100644 --- a/homeassistant/components/simplepush/translations/fr.json +++ b/homeassistant/components/simplepush/translations/fr.json @@ -20,7 +20,7 @@ }, "issues": { "deprecated_yaml": { - "title": "La configuration YAML pour Simplepush est en cours de suppression" + "title": "La configuration YAML pour Simplepush sera bient\u00f4t supprim\u00e9e" }, "removed_yaml": { "title": "La configuration YAML pour Simplepush a \u00e9t\u00e9 supprim\u00e9e" diff --git a/homeassistant/components/simplisafe/translations/ca.json b/homeassistant/components/simplisafe/translations/ca.json index 13ec9c79d38..159b46eeefb 100644 --- a/homeassistant/components/simplisafe/translations/ca.json +++ b/homeassistant/components/simplisafe/translations/ca.json @@ -39,6 +39,12 @@ } } }, + "issues": { + "deprecated_service": { + "description": "Actualitza totes les automatitzacions o 'scripts' que utilitzin aquest servei perqu\u00e8 passin a utilitzar el servei `{alternate_service}` amb un ID d'entitat objectiu o 'target' `{alternate_target}`. Despr\u00e9s, fes clic a ENVIA per marcar aquest problema com a resolt.", + "title": "El servei {deprecated_service} est\u00e0 sent eliminat" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/simplisafe/translations/de.json b/homeassistant/components/simplisafe/translations/de.json index 8f0bdda6baa..9ada40c252d 100644 --- a/homeassistant/components/simplisafe/translations/de.json +++ b/homeassistant/components/simplisafe/translations/de.json @@ -39,6 +39,12 @@ } } }, + "issues": { + "deprecated_service": { + "description": "Aktualisiere alle Automatisierungen oder Skripte, die diesen Dienst verwenden, um stattdessen den Dienst `{alternate_service}` mit einer Ziel-Entit\u00e4ts-ID von `{alternate_target}` zu verwenden. Dr\u00fccke dann unten auf SENDEN, um dieses Problem als behoben zu markieren.", + "title": "Der Dienst {deprecated_service} wird entfernt" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/simplisafe/translations/fr.json b/homeassistant/components/simplisafe/translations/fr.json index 8f7f3a2dfcd..ed355f5faf8 100644 --- a/homeassistant/components/simplisafe/translations/fr.json +++ b/homeassistant/components/simplisafe/translations/fr.json @@ -39,6 +39,7 @@ }, "issues": { "deprecated_service": { + "description": "Modifiez tout script ou automatisation utilisant ce service afin qu'ils utilisent \u00e0 la place le service `{alternate_service}` avec pour ID d'entit\u00e9 cible `{alternate_target}`. Cliquez ensuite sur VALIDER ci-dessous afin de marquer ce probl\u00e8me comme r\u00e9solu.", "title": "Le service {deprecated_service} sera bient\u00f4t supprim\u00e9" } }, diff --git a/homeassistant/components/simplisafe/translations/pt-BR.json b/homeassistant/components/simplisafe/translations/pt-BR.json index 87e3c6f0303..4dadf67720f 100644 --- a/homeassistant/components/simplisafe/translations/pt-BR.json +++ b/homeassistant/components/simplisafe/translations/pt-BR.json @@ -39,6 +39,12 @@ } } }, + "issues": { + "deprecated_service": { + "description": "Atualize quaisquer automa\u00e7\u00f5es ou scripts que usam este servi\u00e7o para usar o servi\u00e7o `{alternate_service}` com um ID de entidade de destino de `{alternate_target}`. Em seguida, clique em ENVIAR abaixo para marcar este problema como resolvido.", + "title": "O servi\u00e7o {deprecated_service} est\u00e1 sendo removido" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/simplisafe/translations/ru.json b/homeassistant/components/simplisafe/translations/ru.json index 0d09bcd0faf..327cb296ff3 100644 --- a/homeassistant/components/simplisafe/translations/ru.json +++ b/homeassistant/components/simplisafe/translations/ru.json @@ -39,6 +39,12 @@ } } }, + "issues": { + "deprecated_service": { + "description": "\u0412\u043c\u0435\u0441\u0442\u043e \u044d\u0442\u043e\u0439 \u0441\u043b\u0443\u0436\u0431\u044b \u0442\u0435\u043f\u0435\u0440\u044c \u0441\u043b\u0435\u0434\u0443\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0441\u043b\u0443\u0436\u0431\u0443 `{alternate_service}` \u0441 \u0446\u0435\u043b\u0435\u0432\u044b\u043c \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u043c `{alternate_target}`. \u041e\u0442\u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u0443\u0439\u0442\u0435 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0438 \u0438 \u0441\u043a\u0440\u0438\u043f\u0442\u044b \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 \u041f\u041e\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u042c, \u0447\u0442\u043e\u0431\u044b \u043e\u0442\u043c\u0435\u0442\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443 \u043a\u0430\u043a \u0443\u0441\u0442\u0440\u0430\u043d\u0451\u043d\u043d\u0443\u044e.", + "title": "\u0421\u043b\u0443\u0436\u0431\u0430 {deprecated_service} \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/simplisafe/translations/uk.json b/homeassistant/components/simplisafe/translations/uk.json index 76e0fb397cb..1df0e00c6cc 100644 --- a/homeassistant/components/simplisafe/translations/uk.json +++ b/homeassistant/components/simplisafe/translations/uk.json @@ -24,6 +24,12 @@ } } }, + "issues": { + "deprecated_service": { + "description": "\u041e\u043d\u043e\u0432\u0456\u0442\u044c \u0431\u0443\u0434\u044c-\u044f\u043a\u0456 \u0437\u0430\u0441\u043e\u0431\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0456\u0457 \u0430\u0431\u043e \u0441\u0446\u0435\u043d\u0430\u0440\u0456\u0457, \u044f\u043a\u0456 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u044e\u0442\u044c \u0446\u044e \u0441\u043b\u0443\u0436\u0431\u0443, \u0449\u043e\u0431 \u043d\u0430\u0442\u043e\u043c\u0456\u0441\u0442\u044c \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438 \u0441\u043b\u0443\u0436\u0431\u0443 ` {alternate_service} ` \u0437 \u0456\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440\u043e\u043c \u0446\u0456\u043b\u044c\u043e\u0432\u043e\u0457 \u0441\u0443\u0442\u043d\u043e\u0441\u0442\u0456 ` {alternate_target} `. \u041f\u043e\u0442\u0456\u043c \u043d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c \u00ab\u041d\u0410\u0414\u0406\u0421\u041b\u0410\u0422\u0418\u00bb \u043d\u0438\u0436\u0447\u0435, \u0449\u043e\u0431 \u043f\u043e\u0437\u043d\u0430\u0447\u0438\u0442\u0438 \u0446\u044e \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443 \u044f\u043a \u0432\u0438\u0440\u0456\u0448\u0435\u043d\u0443.", + "title": "\u0421\u043b\u0443\u0436\u0431\u0430 {deprecated_service} \u0432\u0438\u0434\u0430\u043b\u044f\u0454\u0442\u044c\u0441\u044f" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/simplisafe/translations/zh-Hant.json b/homeassistant/components/simplisafe/translations/zh-Hant.json index fe6a3fa5ce0..199e96e6792 100644 --- a/homeassistant/components/simplisafe/translations/zh-Hant.json +++ b/homeassistant/components/simplisafe/translations/zh-Hant.json @@ -39,6 +39,12 @@ } } }, + "issues": { + "deprecated_service": { + "description": "\u4f7f\u7528\u6b64\u670d\u52d9\u4ee5\u66f4\u65b0\u4efb\u4f55\u81ea\u52d5\u5316\u6216\u8173\u672c\u3001\u4ee5\u53d6\u4ee3\u4f7f\u7528\u76ee\u6a19\u5be6\u9ad4 ID \u70ba `{alternate_target}` \u4e4b `{alternate_service}` \u670d\u52d9\uff0c\u7136\u5f8c\u9ede\u9078\u50b3\u9001\u4ee5\u6a19\u793a\u554f\u984c\u5df2\u89e3\u6c7a\u3002", + "title": "{deprecated_service} \u670d\u52d9\u5373\u5c07\u79fb\u9664" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/soundtouch/translations/fr.json b/homeassistant/components/soundtouch/translations/fr.json index 94cbd1ed69e..ad7cd421893 100644 --- a/homeassistant/components/soundtouch/translations/fr.json +++ b/homeassistant/components/soundtouch/translations/fr.json @@ -20,7 +20,7 @@ }, "issues": { "deprecated_yaml": { - "title": "La configuration YAML pour Bose SoundTouch est en cours de suppression" + "title": "La configuration YAML pour Bose SoundTouch sera bient\u00f4t supprim\u00e9e" } } } \ No newline at end of file diff --git a/homeassistant/components/speedtestdotnet/translations/fr.json b/homeassistant/components/speedtestdotnet/translations/fr.json index d2efebd0eb1..a89b7e89977 100644 --- a/homeassistant/components/speedtestdotnet/translations/fr.json +++ b/homeassistant/components/speedtestdotnet/translations/fr.json @@ -9,6 +9,19 @@ } } }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Modifiez tout script ou automatisation utilisant ce service afin qu'ils utilisent \u00e0 la place le service `homeassistant.update_entity` avec l'entity_id d'un Speedtest pour cible. Cliquez ensuite sur VALIDER ci-dessous afin de marquer ce probl\u00e8me comme r\u00e9solu.", + "title": "Le service speedtest sera bient\u00f4t supprim\u00e9" + } + } + }, + "title": "Le service speedtest sera bient\u00f4t supprim\u00e9" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/switchbee/translations/ja.json b/homeassistant/components/switchbee/translations/ja.json new file mode 100644 index 00000000000..b0bdd860094 --- /dev/null +++ b/homeassistant/components/switchbee/translations/ja.json @@ -0,0 +1,30 @@ +{ + "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", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "devices": "\u542b\u3081\u308b\u30c7\u30d0\u30a4\u30b9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switchbee/translations/ko.json b/homeassistant/components/switchbee/translations/ko.json new file mode 100644 index 00000000000..5af63d34313 --- /dev/null +++ b/homeassistant/components/switchbee/translations/ko.json @@ -0,0 +1,21 @@ +{ + "config": { + "step": { + "user": { + "data": { + "switch_as_light": "\uc2a4\uc704\uce58\ub97c \uc870\uba85 \uac1c\uccb4\ub85c \ucd08\uae30\ud654", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "devices": "\ud3ec\ud568\ud560 \uc7a5\uce58" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tilt_ble/translations/ja.json b/homeassistant/components/tilt_ble/translations/ja.json new file mode 100644 index 00000000000..38f862bd2f6 --- /dev/null +++ b/homeassistant/components/tilt_ble/translations/ja.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", + "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "{name} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" + }, + "user": { + "data": { + "address": "\u30c7\u30d0\u30a4\u30b9" + }, + "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3059\u308b\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/volvooncall/translations/fr.json b/homeassistant/components/volvooncall/translations/fr.json index cc35aa10fbb..bb18d166012 100644 --- a/homeassistant/components/volvooncall/translations/fr.json +++ b/homeassistant/components/volvooncall/translations/fr.json @@ -19,5 +19,10 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "title": "La configuration YAML pour Volvo\u00a0On\u00a0Call sera bient\u00f4t supprim\u00e9e" + } } } \ No newline at end of file diff --git a/homeassistant/components/xbox/translations/fr.json b/homeassistant/components/xbox/translations/fr.json index ee3af75d401..e411dbfd764 100644 --- a/homeassistant/components/xbox/translations/fr.json +++ b/homeassistant/components/xbox/translations/fr.json @@ -16,7 +16,7 @@ }, "issues": { "deprecated_yaml": { - "title": "La configuration YAML pour XBox est en cours de suppression" + "title": "La configuration YAML pour XBox sera bient\u00f4t supprim\u00e9e" } } } \ No newline at end of file diff --git a/homeassistant/components/zha/translations/ja.json b/homeassistant/components/zha/translations/ja.json index 9c92cc50398..5cf05092122 100644 --- a/homeassistant/components/zha/translations/ja.json +++ b/homeassistant/components/zha/translations/ja.json @@ -59,10 +59,10 @@ }, "maybe_confirm_ezsp_restore": { "data": { - "overwrite_coordinator_ieee": "\u7121\u7ddaIEEE \u30c9\u30ec\u30b9\u3092\u5b8c\u5168\u306b\u7f6e\u304d\u63db\u3048\u308b" + "overwrite_coordinator_ieee": "\u7121\u7dda\u306eIEEE\u30a2\u30c9\u30ec\u30b9\u3092\u5b8c\u5168\u306b\u7f6e\u304d\u63db\u3048\u308b" }, - "description": "\u30d0\u30c3\u30af\u30a2\u30c3\u30d7\u306b\u306f\u3001\u7121\u7dda\u3068\u306f\u7570\u306a\u308b IEEE \u30a2\u30c9\u30ec\u30b9\u304c\u3042\u308a\u307e\u3059\u3002\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u304c\u6b63\u3057\u304f\u6a5f\u80fd\u3059\u308b\u306b\u306f\u3001\u7121\u7dda\u306e IEEE \u30a2\u30c9\u30ec\u30b9\u3082\u5909\u66f4\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002 \n\n\u3053\u308c\u306f\u6052\u4e45\u7684\u306a\u64cd\u4f5c\u3067\u3059\u3002", - "title": "\u7121\u7ddaIEEE\u30a2\u30c9\u30ec\u30b9\u306e\u4e0a\u66f8\u304d" + "description": "\u30d0\u30c3\u30af\u30a2\u30c3\u30d7\u306b\u306f\u3001\u7121\u7dda\u3068\u306f\u7570\u306a\u308bIEEE\u30a2\u30c9\u30ec\u30b9\u304c\u3042\u308a\u307e\u3059\u3002\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u304c\u6b63\u3057\u304f\u6a5f\u80fd\u3059\u308b\u306b\u306f\u3001\u7121\u7dda\u306eIEEE\u30a2\u30c9\u30ec\u30b9\u3082\u5909\u66f4\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002 \n\n\u3053\u308c\u306f\u6052\u4e45\u7684\u306a\u64cd\u4f5c\u3067\u3059\u3002", + "title": "\u7121\u7dda\u306eIEEE\u30a2\u30c9\u30ec\u30b9\u306e\u4e0a\u66f8\u304d" }, "pick_radio": { "data": { @@ -228,14 +228,16 @@ }, "maybe_confirm_ezsp_restore": { "data": { - "overwrite_coordinator_ieee": "\u7121\u7ddaIEEE \u30c9\u30ec\u30b9\u3092\u5b8c\u5168\u306b\u7f6e\u304d\u63db\u3048\u308b" + "overwrite_coordinator_ieee": "\u7121\u7dda\u306eIEEE\u30a2\u30c9\u30ec\u30b9\u3092\u5b8c\u5168\u306b\u7f6e\u304d\u63db\u3048\u308b" }, - "title": "\u7121\u7ddaIEEE\u30a2\u30c9\u30ec\u30b9\u306e\u4e0a\u66f8\u304d" + "description": "\u30d0\u30c3\u30af\u30a2\u30c3\u30d7\u306b\u306f\u3001\u7121\u7dda\u3068\u306f\u7570\u306a\u308bIEEE\u30a2\u30c9\u30ec\u30b9\u304c\u3042\u308a\u307e\u3059\u3002\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u304c\u6b63\u3057\u304f\u6a5f\u80fd\u3059\u308b\u306b\u306f\u3001\u7121\u7dda\u306eIEEE\u30a2\u30c9\u30ec\u30b9\u3082\u5909\u66f4\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002 \n\n\u3053\u308c\u306f\u6052\u4e45\u7684\u306a\u64cd\u4f5c\u3067\u3059\u3002", + "title": "\u7121\u7dda\u306eIEEE\u30a2\u30c9\u30ec\u30b9\u306e\u4e0a\u66f8\u304d" }, "upload_manual_backup": { "data": { "uploaded_backup_file": "\u30d5\u30a1\u30a4\u30eb\u3092\u30a2\u30c3\u30d7\u30ed\u30fc\u30c9\u3059\u308b" }, + "description": "\u30a2\u30c3\u30d7\u30ed\u30fc\u30c9\u3055\u308c\u305f\u30d0\u30c3\u30af\u30a2\u30c3\u30d7 JSON \u30d5\u30a1\u30a4\u30eb\u304b\u3089\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u8a2d\u5b9a\u3092\u5fa9\u5143\u3057\u307e\u3059\u3002 **Network Settings** \u304b\u3089\u5225\u306e ZHA \u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u304b\u3089\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u3059\u308b\u304b\u3001Zigbee2MQTT `coordinator_backup.json` \u30d5\u30a1\u30a4\u30eb\u3092\u4f7f\u7528\u3067\u304d\u307e\u3059\u3002", "title": "\u624b\u52d5\u30d0\u30c3\u30af\u30a2\u30c3\u30d7\u3092\u30a2\u30c3\u30d7\u30ed\u30fc\u30c9\u3059\u308b" } } From dbc02707a9bde8aa06019053568c92c97e87aa2d Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 18 Sep 2022 03:08:17 +0200 Subject: [PATCH 505/955] Improve media_player typing (#78666) --- homeassistant/components/media_player/__init__.py | 3 ++- homeassistant/components/media_player/device_condition.py | 2 +- homeassistant/components/media_player/reproduce_state.py | 4 +--- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 8c89288b41d..5771e1b6938 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -19,6 +19,7 @@ from aiohttp import web from aiohttp.hdrs import CACHE_CONTROL, CONTENT_TYPE from aiohttp.typedefs import LooseHeaders import async_timeout +from typing_extensions import Required import voluptuous as vol from yarl import URL @@ -219,7 +220,7 @@ ATTR_TO_PROPERTY = [ class _CacheImage(TypedDict, total=False): """Class to hold a cached image.""" - lock: asyncio.Lock + lock: Required[asyncio.Lock] content: tuple[bytes | None, str | None] diff --git a/homeassistant/components/media_player/device_condition.py b/homeassistant/components/media_player/device_condition.py index 2f398790ac3..3bf6c5956fa 100644 --- a/homeassistant/components/media_player/device_condition.py +++ b/homeassistant/components/media_player/device_condition.py @@ -46,7 +46,7 @@ async def async_get_conditions( ) -> list[dict[str, str]]: """List device conditions for Media player devices.""" registry = entity_registry.async_get(hass) - conditions = [] + conditions: list[dict[str, str]] = [] # Get all the integrations entities for this device for entry in entity_registry.async_entries_for_device(registry, device_id): diff --git a/homeassistant/components/media_player/reproduce_state.py b/homeassistant/components/media_player/reproduce_state.py index bdfc0bf3acb..387792a1b60 100644 --- a/homeassistant/components/media_player/reproduce_state.py +++ b/homeassistant/components/media_player/reproduce_state.py @@ -37,8 +37,6 @@ from .const import ( MediaPlayerEntityFeature, ) -# mypy: allow-untyped-defs - async def _async_reproduce_states( hass: HomeAssistant, @@ -51,7 +49,7 @@ async def _async_reproduce_states( cur_state = hass.states.get(state.entity_id) features = cur_state.attributes[ATTR_SUPPORTED_FEATURES] if cur_state else 0 - async def call_service(service: str, keys: Iterable) -> None: + async def call_service(service: str, keys: Iterable[str]) -> None: """Call service with set of attributes given.""" data = {"entity_id": state.entity_id} for key in keys: From f7ef9eb91b55589a93d7adbbc92719110401559f Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 18 Sep 2022 03:08:55 +0200 Subject: [PATCH 506/955] Remove low level call from fritzbox_callmonitor (#78668) --- .../components/fritzbox_callmonitor/config_flow.py | 11 ++++------- .../components/fritzbox_callmonitor/const.py | 2 -- .../fritzbox_callmonitor/test_config_flow.py | 13 ++++++++++--- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/fritzbox_callmonitor/config_flow.py b/homeassistant/components/fritzbox_callmonitor/config_flow.py index 5df1e7fc215..982d888d22a 100644 --- a/homeassistant/components/fritzbox_callmonitor/config_flow.py +++ b/homeassistant/components/fritzbox_callmonitor/config_flow.py @@ -5,6 +5,7 @@ from typing import Any, cast from fritzconnection import FritzConnection from fritzconnection.core.exceptions import FritzConnectionException, FritzSecurityError +from fritzconnection.lib.fritzstatus import FritzStatus from requests.exceptions import ConnectionError as RequestsConnectionError import voluptuous as vol @@ -29,10 +30,7 @@ from .const import ( DEFAULT_PORT, DEFAULT_USERNAME, DOMAIN, - FRITZ_ACTION_GET_INFO, FRITZ_ATTR_NAME, - FRITZ_ATTR_SERIAL_NUMBER, - FRITZ_SERVICE_DEVICE_INFO, SERIAL_NUMBER, ) @@ -104,10 +102,9 @@ class FritzBoxCallMonitorConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): fritz_connection = FritzConnection( address=self._host, user=self._username, password=self._password ) - device_info = fritz_connection.call_action( - FRITZ_SERVICE_DEVICE_INFO, FRITZ_ACTION_GET_INFO - ) - self._serial_number = device_info[FRITZ_ATTR_SERIAL_NUMBER] + fritz_status = FritzStatus(fc=fritz_connection) + device_info = fritz_status.get_device_info() + self._serial_number = device_info.serial_number return ConnectResult.SUCCESS except RequestsConnectionError: diff --git a/homeassistant/components/fritzbox_callmonitor/const.py b/homeassistant/components/fritzbox_callmonitor/const.py index 9939a73bc18..ccc5a45e61f 100644 --- a/homeassistant/components/fritzbox_callmonitor/const.py +++ b/homeassistant/components/fritzbox_callmonitor/const.py @@ -18,10 +18,8 @@ ICON_PHONE: Final = "mdi:phone" ATTR_PREFIXES = "prefixes" -FRITZ_ACTION_GET_INFO = "GetInfo" FRITZ_ATTR_NAME = "name" FRITZ_ATTR_SERIAL_NUMBER = "NewSerialNumber" -FRITZ_SERVICE_DEVICE_INFO = "DeviceInfo" UNKNOWN_NAME = "unknown" SERIAL_NUMBER = "serial_number" diff --git a/tests/components/fritzbox_callmonitor/test_config_flow.py b/tests/components/fritzbox_callmonitor/test_config_flow.py index 94d5bdc8eeb..2aa68af0758 100644 --- a/tests/components/fritzbox_callmonitor/test_config_flow.py +++ b/tests/components/fritzbox_callmonitor/test_config_flow.py @@ -2,6 +2,7 @@ from unittest.mock import PropertyMock from fritzconnection.core.exceptions import FritzConnectionException, FritzSecurityError +from fritzconnection.lib.fritztools import ArgumentNamespace from requests.exceptions import ConnectionError as RequestsConnectionError from homeassistant.components.fritzbox_callmonitor.config_flow import ConnectResult @@ -58,7 +59,7 @@ MOCK_YAML_CONFIG = { CONF_PHONEBOOK: MOCK_PHONEBOOK_ID, CONF_NAME: MOCK_NAME, } -MOCK_DEVICE_INFO = {FRITZ_ATTR_SERIAL_NUMBER: MOCK_SERIAL_NUMBER} +MOCK_DEVICE_INFO = ArgumentNamespace({FRITZ_ATTR_SERIAL_NUMBER: MOCK_SERIAL_NUMBER}) MOCK_PHONEBOOK_INFO_1 = {FRITZ_ATTR_NAME: MOCK_PHONEBOOK_NAME_1} MOCK_PHONEBOOK_INFO_2 = {FRITZ_ATTR_NAME: MOCK_PHONEBOOK_NAME_2} MOCK_UNIQUE_ID = f"{MOCK_SERIAL_NUMBER}-{MOCK_PHONEBOOK_ID}" @@ -90,7 +91,10 @@ async def test_setup_one_phonebook(hass: HomeAssistant) -> None: "homeassistant.components.fritzbox_callmonitor.config_flow.FritzConnection.__init__", return_value=None, ), patch( - "homeassistant.components.fritzbox_callmonitor.config_flow.FritzConnection.call_action", + "homeassistant.components.fritzbox_callmonitor.config_flow.FritzStatus.__init__", + return_value=None, + ), patch( + "homeassistant.components.fritzbox_callmonitor.config_flow.FritzStatus.get_device_info", return_value=MOCK_DEVICE_INFO, ), patch( "homeassistant.components.fritzbox_callmonitor.async_setup_entry", @@ -126,7 +130,10 @@ async def test_setup_multiple_phonebooks(hass: HomeAssistant) -> None: "homeassistant.components.fritzbox_callmonitor.config_flow.FritzConnection.__init__", return_value=None, ), patch( - "homeassistant.components.fritzbox_callmonitor.config_flow.FritzConnection.call_action", + "homeassistant.components.fritzbox_callmonitor.config_flow.FritzStatus.__init__", + return_value=None, + ), patch( + "homeassistant.components.fritzbox_callmonitor.config_flow.FritzStatus.get_device_info", return_value=MOCK_DEVICE_INFO, ), patch( "homeassistant.components.fritzbox_callmonitor.base.FritzPhonebook.phonebook_info", From b8ccf53799615f6ecf5381da63d89236c4cdad52 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 17 Sep 2022 22:45:04 -0500 Subject: [PATCH 507/955] Fix switchbot not accepting the first advertisement (#78610) --- homeassistant/components/switchbot/__init__.py | 2 +- homeassistant/components/switchbot/coordinator.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/switchbot/__init__.py b/homeassistant/components/switchbot/__init__.py index 7307187bf54..58e77dfe1bf 100644 --- a/homeassistant/components/switchbot/__init__.py +++ b/homeassistant/components/switchbot/__init__.py @@ -110,7 +110,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) entry.async_on_unload(coordinator.async_start()) if not await coordinator.async_wait_ready(): - raise ConfigEntryNotReady(f"Switchbot {sensor_type} with {address} not ready") + raise ConfigEntryNotReady(f"{address} is not advertising state") entry.async_on_unload(entry.add_update_listener(_async_update_listener)) await hass.config_entries.async_forward_entry_setups( diff --git a/homeassistant/components/switchbot/coordinator.py b/homeassistant/components/switchbot/coordinator.py index 94018c1b46b..ee93c74af37 100644 --- a/homeassistant/components/switchbot/coordinator.py +++ b/homeassistant/components/switchbot/coordinator.py @@ -73,7 +73,7 @@ class SwitchbotDataUpdateCoordinator(PassiveBluetoothDataUpdateCoordinator): if adv := switchbot.parse_advertisement_data( service_info.device, service_info.advertisement ): - if "modelName" in self.data: + 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): From 1f7c90baa04144da2d554c023cb991d56469796a Mon Sep 17 00:00:00 2001 From: Pete Date: Sun, 18 Sep 2022 05:45:31 +0200 Subject: [PATCH 508/955] Fix fan speed regression for some xiaomi fans (#78406) Co-authored-by: Martin Hjelmare --- homeassistant/components/xiaomi_miio/const.py | 9 -- homeassistant/components/xiaomi_miio/fan.py | 82 +++++++++++++------ 2 files changed, 58 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py index 11922956c25..c0711a02a36 100644 --- a/homeassistant/components/xiaomi_miio/const.py +++ b/homeassistant/components/xiaomi_miio/const.py @@ -112,15 +112,6 @@ MODELS_FAN_MIOT = [ MODEL_FAN_ZA5, ] -# number of speed levels each fan has -SPEEDS_FAN_MIOT = { - MODEL_FAN_1C: 3, - MODEL_FAN_P10: 4, - MODEL_FAN_P11: 4, - MODEL_FAN_P9: 4, - MODEL_FAN_ZA5: 4, -} - MODELS_PURIFIER_MIOT = [ MODEL_AIRPURIFIER_3, MODEL_AIRPURIFIER_3C, diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index 901211d1d2d..ddbd45bff08 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -85,7 +85,6 @@ from .const import ( MODELS_PURIFIER_MIOT, SERVICE_RESET_FILTER, SERVICE_SET_EXTRA_FEATURES, - SPEEDS_FAN_MIOT, ) from .device import XiaomiCoordinatedMiioEntity @@ -235,13 +234,11 @@ async def async_setup_entry( elif model in MODELS_FAN_MIIO: entity = XiaomiFan(device, config_entry, unique_id, coordinator) elif model == MODEL_FAN_ZA5: - speed_count = SPEEDS_FAN_MIOT[model] - entity = XiaomiFanZA5(device, config_entry, unique_id, coordinator, speed_count) + entity = XiaomiFanZA5(device, config_entry, unique_id, coordinator) + elif model == MODEL_FAN_1C: + entity = XiaomiFan1C(device, config_entry, unique_id, coordinator) elif model in MODELS_FAN_MIOT: - speed_count = SPEEDS_FAN_MIOT[model] - entity = XiaomiFanMiot( - device, config_entry, unique_id, coordinator, speed_count - ) + entity = XiaomiFanMiot(device, config_entry, unique_id, coordinator) else: return @@ -1049,11 +1046,6 @@ class XiaomiFanP5(XiaomiGenericFan): class XiaomiFanMiot(XiaomiGenericFan): """Representation of a Xiaomi Fan Miot.""" - def __init__(self, device, entry, unique_id, coordinator, speed_count): - """Initialize MIOT fan with speed count.""" - super().__init__(device, entry, unique_id, coordinator) - self._speed_count = speed_count - @property def operation_mode_class(self): """Hold operation mode class.""" @@ -1071,9 +1063,7 @@ class XiaomiFanMiot(XiaomiGenericFan): self._preset_mode = self.coordinator.data.mode.name self._oscillating = self.coordinator.data.oscillate if self.coordinator.data.is_on: - self._percentage = ranged_value_to_percentage( - (1, self._speed_count), self.coordinator.data.speed - ) + self._percentage = self.coordinator.data.speed else: self._percentage = 0 @@ -1092,6 +1082,59 @@ class XiaomiFanMiot(XiaomiGenericFan): self._preset_mode = preset_mode self.async_write_ha_state() + async def async_set_percentage(self, percentage: int) -> None: + """Set the percentage of the fan.""" + if percentage == 0: + self._percentage = 0 + await self.async_turn_off() + return + + result = await self._try_command( + "Setting fan speed percentage of the miio device failed.", + self._device.set_speed, + percentage, + ) + if result: + self._percentage = percentage + + if not self.is_on: + await self.async_turn_on() + elif result: + self.async_write_ha_state() + + +class XiaomiFanZA5(XiaomiFanMiot): + """Representation of a Xiaomi Fan ZA5.""" + + @property + def operation_mode_class(self): + """Hold operation mode class.""" + return FanZA5OperationMode + + +class XiaomiFan1C(XiaomiFanMiot): + """Representation of a Xiaomi Fan 1C (Standing Fan 2 Lite).""" + + def __init__(self, device, entry, unique_id, coordinator): + """Initialize MIOT fan with speed count.""" + super().__init__(device, entry, unique_id, coordinator) + self._speed_count = 3 + + @callback + def _handle_coordinator_update(self): + """Fetch state from the device.""" + self._state = self.coordinator.data.is_on + self._preset_mode = self.coordinator.data.mode.name + self._oscillating = self.coordinator.data.oscillate + if self.coordinator.data.is_on: + self._percentage = ranged_value_to_percentage( + (1, self._speed_count), self.coordinator.data.speed + ) + else: + self._percentage = 0 + + self.async_write_ha_state() + async def async_set_percentage(self, percentage: int) -> None: """Set the percentage of the fan.""" if percentage == 0: @@ -1116,12 +1159,3 @@ class XiaomiFanMiot(XiaomiGenericFan): if result: self._percentage = ranged_value_to_percentage((1, self._speed_count), speed) self.async_write_ha_state() - - -class XiaomiFanZA5(XiaomiFanMiot): - """Representation of a Xiaomi Fan ZA5.""" - - @property - def operation_mode_class(self): - """Hold operation mode class.""" - return FanZA5OperationMode From 69c5d910d46de513e5a59e9ae5fbc4931471865b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Sun, 18 Sep 2022 09:58:14 +0200 Subject: [PATCH 509/955] Remove deprecated update binary sensor from Supervisor (#78664) --- .../components/hassio/binary_sensor.py | 62 ++++--------------- homeassistant/components/hassio/const.py | 1 - tests/components/hassio/test_binary_sensor.py | 3 - 3 files changed, 12 insertions(+), 54 deletions(-) diff --git a/homeassistant/components/hassio/binary_sensor.py b/homeassistant/components/hassio/binary_sensor.py index 6ddc15e7725..16845e6f76c 100644 --- a/homeassistant/components/hassio/binary_sensor.py +++ b/homeassistant/components/hassio/binary_sensor.py @@ -13,14 +13,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import ADDONS_COORDINATOR -from .const import ( - ATTR_STARTED, - ATTR_STATE, - ATTR_UPDATE_AVAILABLE, - DATA_KEY_ADDONS, - DATA_KEY_OS, -) -from .entity import HassioAddonEntity, HassioOSEntity +from .const import ATTR_STARTED, ATTR_STATE, DATA_KEY_ADDONS +from .entity import HassioAddonEntity @dataclass @@ -30,17 +24,7 @@ class HassioBinarySensorEntityDescription(BinarySensorEntityDescription): target: str | None = None -COMMON_ENTITY_DESCRIPTIONS = ( - HassioBinarySensorEntityDescription( - # Deprecated, scheduled to be removed in 2022.6 - device_class=BinarySensorDeviceClass.UPDATE, - entity_registry_enabled_default=False, - key=ATTR_UPDATE_AVAILABLE, - name="Update available", - ), -) - -ADDON_ENTITY_DESCRIPTIONS = COMMON_ENTITY_DESCRIPTIONS + ( +ADDON_ENTITY_DESCRIPTIONS = ( HassioBinarySensorEntityDescription( device_class=BinarySensorDeviceClass.RUNNING, entity_registry_enabled_default=False, @@ -59,28 +43,15 @@ async def async_setup_entry( """Binary sensor set up for Hass.io config entry.""" coordinator = hass.data[ADDONS_COORDINATOR] - entities: list[HassioAddonBinarySensor | HassioOSBinarySensor] = [] - - for entity_description in ADDON_ENTITY_DESCRIPTIONS: - for addon in coordinator.data[DATA_KEY_ADDONS].values(): - entities.append( - HassioAddonBinarySensor( - addon=addon, - coordinator=coordinator, - entity_description=entity_description, - ) - ) - - if coordinator.is_hass_os: - for entity_description in COMMON_ENTITY_DESCRIPTIONS: - entities.append( - HassioOSBinarySensor( - coordinator=coordinator, - entity_description=entity_description, - ) - ) - - async_add_entities(entities) + async_add_entities( + HassioAddonBinarySensor( + addon=addon, + coordinator=coordinator, + entity_description=entity_description, + ) + for addon in coordinator.data[DATA_KEY_ADDONS].values() + for entity_description in ADDON_ENTITY_DESCRIPTIONS + ) class HassioAddonBinarySensor(HassioAddonEntity, BinarySensorEntity): @@ -97,12 +68,3 @@ class HassioAddonBinarySensor(HassioAddonEntity, BinarySensorEntity): if self.entity_description.target is None: return value return value == self.entity_description.target - - -class HassioOSBinarySensor(HassioOSEntity, BinarySensorEntity): - """Binary sensor to track whether an update is available for Hass.io OS.""" - - @property - def is_on(self) -> bool: - """Return true if the binary sensor is on.""" - return self.coordinator.data[DATA_KEY_OS][self.entity_description.key] diff --git a/homeassistant/components/hassio/const.py b/homeassistant/components/hassio/const.py index e4991e5fc03..e37a31ddbd6 100644 --- a/homeassistant/components/hassio/const.py +++ b/homeassistant/components/hassio/const.py @@ -42,7 +42,6 @@ EVENT_SUPERVISOR_EVENT = "supervisor_event" ATTR_AUTO_UPDATE = "auto_update" ATTR_VERSION = "version" ATTR_VERSION_LATEST = "version_latest" -ATTR_UPDATE_AVAILABLE = "update_available" ATTR_CPU_PERCENT = "cpu_percent" ATTR_CHANGELOG = "changelog" ATTR_MEMORY_PERCENT = "memory_percent" diff --git a/tests/components/hassio/test_binary_sensor.py b/tests/components/hassio/test_binary_sensor.py index ba9bcb2afdf..31667efadc6 100644 --- a/tests/components/hassio/test_binary_sensor.py +++ b/tests/components/hassio/test_binary_sensor.py @@ -137,9 +137,6 @@ def mock_all(aioclient_mock, request): @pytest.mark.parametrize( "entity_id,expected", [ - ("binary_sensor.home_assistant_operating_system_update_available", "off"), - ("binary_sensor.test_update_available", "on"), - ("binary_sensor.test2_update_available", "off"), ("binary_sensor.test_running", "on"), ("binary_sensor.test2_running", "off"), ], From 87f8ebceb2d9e7be997dfcb90d7088686698acfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Sun, 18 Sep 2022 11:40:42 +0200 Subject: [PATCH 510/955] Limit Github event subscription if polling is disabled (#78662) --- homeassistant/components/github/__init__.py | 4 +- tests/components/github/test_init.py | 41 ++++++++++++++++++++- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/github/__init__.py b/homeassistant/components/github/__init__.py index 53b8cd67871..bc2bab49bdb 100644 --- a/homeassistant/components/github/__init__.py +++ b/homeassistant/components/github/__init__.py @@ -38,7 +38,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) await coordinator.async_config_entry_first_refresh() - await coordinator.subscribe() + + if not entry.pref_disable_polling: + await coordinator.subscribe() hass.data[DOMAIN][repository] = coordinator diff --git a/tests/components/github/test_init.py b/tests/components/github/test_init.py index 8abb6ffda92..aba6c98db16 100644 --- a/tests/components/github/test_init.py +++ b/tests/components/github/test_init.py @@ -1,7 +1,7 @@ """Test the GitHub init file.""" from pytest import LogCaptureFixture -from homeassistant.components.github.const import CONF_REPOSITORIES +from homeassistant.components.github import CONF_REPOSITORIES from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr @@ -44,3 +44,42 @@ async def test_device_registry_cleanup( ) assert len(devices) == 0 + + +async def test_subscription_setup( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + aioclient_mock: AiohttpClientMocker, +) -> None: + """Test that we setup event subscription.""" + mock_config_entry.options = {CONF_REPOSITORIES: ["home-assistant/core"]} + mock_config_entry.pref_disable_polling = False + await setup_github_integration(hass, mock_config_entry, aioclient_mock) + assert ( + "https://api.github.com/repos/home-assistant/core/events" in x[1] + for x in aioclient_mock.mock_calls + ) + + +async def test_subscription_setup_polling_disabled( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + aioclient_mock: AiohttpClientMocker, +) -> None: + """Test that we do not setup event subscription if polling is disabled.""" + mock_config_entry.options = {CONF_REPOSITORIES: ["home-assistant/core"]} + mock_config_entry.pref_disable_polling = True + await setup_github_integration(hass, mock_config_entry, aioclient_mock) + assert ( + "https://api.github.com/repos/home-assistant/core/events" not in x[1] + for x in aioclient_mock.mock_calls + ) + + # Prove that we subscribed if the user enabled polling again + mock_config_entry.pref_disable_polling = False + assert await hass.config_entries.async_reload(mock_config_entry.entry_id) + await hass.async_block_till_done() + assert ( + "https://api.github.com/repos/home-assistant/core/events" in x[1] + for x in aioclient_mock.mock_calls + ) From 59aef20e994a82d40d7d16d3ee763adb77a3aa20 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 18 Sep 2022 11:46:43 +0200 Subject: [PATCH 511/955] Add missing typing met config flow (#78645) --- homeassistant/components/met/config_flow.py | 27 +++++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/met/config_flow.py b/homeassistant/components/met/config_flow.py index 6c4c4d33d5b..baf7269a81d 100644 --- a/homeassistant/components/met/config_flow.py +++ b/homeassistant/components/met/config_flow.py @@ -1,11 +1,14 @@ """Config flow to configure Met component.""" from __future__ import annotations +from typing import Any + import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv from .const import ( @@ -18,7 +21,7 @@ from .const import ( @callback -def configured_instances(hass): +def configured_instances(hass: HomeAssistant) -> set[str]: """Return a set of configured SimpliSafe instances.""" entries = [] for entry in hass.config_entries.async_entries(DOMAIN): @@ -36,11 +39,13 @@ class MetFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - def __init__(self): + def __init__(self) -> None: """Init MetFlowHandler.""" - self._errors = {} + self._errors: dict[str, Any] = {} - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a flow initialized by the user.""" self._errors = {} @@ -62,8 +67,12 @@ class MetFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) async def _show_config_form( - self, name=None, latitude=None, longitude=None, elevation=None - ): + self, + name: str | None = None, + latitude: float | None = None, + longitude: float | None = None, + elevation: int | None = None, + ) -> FlowResult: """Show the configuration form to edit location data.""" return self.async_show_form( step_id="user", @@ -78,7 +87,9 @@ class MetFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=self._errors, ) - async def async_step_onboarding(self, data=None): + async def async_step_onboarding( + self, data: dict[str, Any] | None = None + ) -> FlowResult: """Handle a flow initialized by onboarding.""" # Don't create entry if latitude or longitude isn't set. # Also, filters out our onboarding default location. From 8dbbd0ded057479f5cb3171a30f5c0858348bcaf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 18 Sep 2022 04:48:04 -0500 Subject: [PATCH 512/955] Cache template regex compiles (#78529) --- homeassistant/helpers/template.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 3999231eb68..5a4d631a8c8 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -1719,7 +1719,13 @@ def regex_match(value, find="", ignorecase=False): if not isinstance(value, str): value = str(value) flags = re.I if ignorecase else 0 - return bool(re.match(find, value, flags)) + return bool(_regex_cache(find, flags).match(value)) + + +@lru_cache(maxsize=128) +def _regex_cache(find: str, flags: int) -> re.Pattern: + """Cache compiled regex.""" + return re.compile(find, flags) def regex_replace(value="", find="", replace="", ignorecase=False): @@ -1727,8 +1733,7 @@ def regex_replace(value="", find="", replace="", ignorecase=False): if not isinstance(value, str): value = str(value) flags = re.I if ignorecase else 0 - regex = re.compile(find, flags) - return regex.sub(replace, value) + return _regex_cache(find, flags).sub(replace, value) def regex_search(value, find="", ignorecase=False): @@ -1736,7 +1741,7 @@ def regex_search(value, find="", ignorecase=False): if not isinstance(value, str): value = str(value) flags = re.I if ignorecase else 0 - return bool(re.search(find, value, flags)) + return bool(_regex_cache(find, flags).search(value)) def regex_findall_index(value, find="", index=0, ignorecase=False): @@ -1749,7 +1754,7 @@ def regex_findall(value, find="", ignorecase=False): if not isinstance(value, str): value = str(value) flags = re.I if ignorecase else 0 - return re.findall(find, value, flags) + return _regex_cache(find, flags).findall(value) def bitwise_and(first_value, second_value): From 2eb265f28b6907964a6761542e6e2e37d8ab51a5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 18 Sep 2022 12:17:28 +0200 Subject: [PATCH 513/955] Remove mDNS iteration from Plugwise unique ID (#78680) * Remove mDNS iteration from Plugwise unique ID * Add iteration to tests --- homeassistant/components/plugwise/config_flow.py | 2 +- tests/components/plugwise/test_config_flow.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/plugwise/config_flow.py b/homeassistant/components/plugwise/config_flow.py index 3fee1445758..8cf2456c0b4 100644 --- a/homeassistant/components/plugwise/config_flow.py +++ b/homeassistant/components/plugwise/config_flow.py @@ -91,7 +91,7 @@ class PlugwiseConfigFlow(ConfigFlow, domain=DOMAIN): self.discovery_info = discovery_info _properties = discovery_info.properties - unique_id = discovery_info.hostname.split(".")[0] + unique_id = discovery_info.hostname.split(".")[0].split("-")[0] if config_entry := await self.async_set_unique_id(unique_id): try: await validate_gw_input( diff --git a/tests/components/plugwise/test_config_flow.py b/tests/components/plugwise/test_config_flow.py index 4dbe1d2615f..84d2335b16f 100644 --- a/tests/components/plugwise/test_config_flow.py +++ b/tests/components/plugwise/test_config_flow.py @@ -36,7 +36,8 @@ TEST_USERNAME2 = "stretch" TEST_DISCOVERY = ZeroconfServiceInfo( host=TEST_HOST, addresses=[TEST_HOST], - hostname=f"{TEST_HOSTNAME}.local.", + # The added `-2` is to simulate mDNS collision + hostname=f"{TEST_HOSTNAME}-2.local.", name="mock_name", port=DEFAULT_PORT, properties={ From 4d6151666e3e398757d076b1859767891bfba8fb Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sun, 18 Sep 2022 08:56:46 -0400 Subject: [PATCH 514/955] Handle multiple files properly in zwave_js update entity (#78658) * Handle multiple files properly in zwave_js update entity * Until we have progress, set in progress to true. And fix when we write state * fix tests * Assert we set in progress to true before we get progress * Fix tests * Comment --- homeassistant/components/zwave_js/update.py | 16 +- tests/components/zwave_js/test_update.py | 159 +++++++++++++++++++- 2 files changed, 168 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/zwave_js/update.py b/homeassistant/components/zwave_js/update.py index 08a5b90b42d..52c7e0d46e1 100644 --- a/homeassistant/components/zwave_js/update.py +++ b/homeassistant/components/zwave_js/update.py @@ -138,7 +138,7 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): @callback def _unsub_firmware_events_and_reset_progress( - self, write_state: bool = False + self, write_state: bool = True ) -> None: """Unsubscribe from firmware events and reset update install progress.""" if self._progress_unsub: @@ -224,12 +224,14 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): """Install an update.""" firmware = self._latest_version_firmware assert firmware - self._unsub_firmware_events_and_reset_progress(True) + self._unsub_firmware_events_and_reset_progress(False) + self._attr_in_progress = True + self.async_write_ha_state() self._progress_unsub = self.node.on( "firmware update progress", self._update_progress ) - self._finished_unsub = self.node.once( + self._finished_unsub = self.node.on( "firmware update finished", self._update_finished ) @@ -244,6 +246,8 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): # We need to block until we receive the `firmware update finished` event await self._finished_event.wait() + # Clear the event so that a second firmware update blocks again + self._finished_event.clear() assert self._finished_status is not None # If status is not OK, we should throw an error to let the user know @@ -262,8 +266,12 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): self._attr_in_progress = floor( 100 * self._num_files_installed / len(firmware.files) ) + + # Clear the status so we can get a new one + self._finished_status = None self.async_write_ha_state() + # If we get here, all files were installed successfully self._attr_installed_version = self._attr_latest_version = firmware.version self._latest_version_firmware = None self._unsub_firmware_events_and_reset_progress() @@ -313,4 +321,4 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): self._poll_unsub() self._poll_unsub = None - self._unsub_firmware_events_and_reset_progress() + self._unsub_firmware_events_and_reset_progress(False) diff --git a/tests/components/zwave_js/test_update.py b/tests/components/zwave_js/test_update.py index a5b3059e705..b2517c3dd34 100644 --- a/tests/components/zwave_js/test_update.py +++ b/tests/components/zwave_js/test_update.py @@ -7,7 +7,7 @@ from zwave_js_server.event import Event from zwave_js_server.exceptions import FailedZWaveCommand from zwave_js_server.model.firmware import FirmwareUpdateStatus -from homeassistant.components.update.const import ( +from homeassistant.components.update import ( ATTR_AUTO_UPDATE, ATTR_IN_PROGRESS, ATTR_INSTALLED_VERSION, @@ -54,6 +54,19 @@ FIRMWARE_UPDATES = { ] } +FIRMWARE_UPDATE_MULTIPLE_FILES = { + "updates": [ + { + "version": "11.2.4", + "changelog": "blah 2", + "files": [ + {"target": 0, "url": "https://example2.com", "integrity": "sha2"}, + {"target": 1, "url": "https://example4.com", "integrity": "sha4"}, + ], + }, + ] +} + async def test_update_entity_states( hass, @@ -328,6 +341,11 @@ async def test_update_entity_progress( # Sleep so that task starts await asyncio.sleep(0.1) + state = hass.states.get(UPDATE_ENTITY) + assert state + attrs = state.attributes + assert attrs[ATTR_IN_PROGRESS] is True + event = Event( type="firmware update progress", data={ @@ -363,7 +381,142 @@ async def test_update_entity_progress( state = hass.states.get(UPDATE_ENTITY) assert state attrs = state.attributes - assert attrs[ATTR_IN_PROGRESS] is False + assert attrs[ATTR_IN_PROGRESS] == 0 + assert attrs[ATTR_INSTALLED_VERSION] == "11.2.4" + assert attrs[ATTR_LATEST_VERSION] == "11.2.4" + assert state.state == STATE_OFF + + await install_task + + +async def test_update_entity_progress_multiple( + hass, + client, + climate_radio_thermostat_ct100_plus_different_endpoints, + integration, +): + """Test update entity progress with multiple files.""" + node = climate_radio_thermostat_ct100_plus_different_endpoints + client.async_send_command.return_value = FIRMWARE_UPDATE_MULTIPLE_FILES + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(days=1)) + await hass.async_block_till_done() + + state = hass.states.get(UPDATE_ENTITY) + assert state + assert state.state == STATE_ON + attrs = state.attributes + assert attrs[ATTR_INSTALLED_VERSION] == "10.7" + assert attrs[ATTR_LATEST_VERSION] == "11.2.4" + + client.async_send_command.reset_mock() + client.async_send_command.return_value = None + + # Test successful install call without a version + install_task = hass.async_create_task( + hass.services.async_call( + UPDATE_DOMAIN, + SERVICE_INSTALL, + { + ATTR_ENTITY_ID: UPDATE_ENTITY, + }, + blocking=True, + ) + ) + + # Sleep so that task starts + await asyncio.sleep(0.1) + + state = hass.states.get(UPDATE_ENTITY) + assert state + attrs = state.attributes + assert attrs[ATTR_IN_PROGRESS] is True + + node.receive_event( + Event( + type="firmware update progress", + data={ + "source": "node", + "event": "firmware update progress", + "nodeId": node.node_id, + "sentFragments": 1, + "totalFragments": 20, + }, + ) + ) + + # Block so HA can do its thing + await asyncio.sleep(0) + + # Validate that the progress is updated (two files means progress is 50% of 5) + state = hass.states.get(UPDATE_ENTITY) + assert state + attrs = state.attributes + assert attrs[ATTR_IN_PROGRESS] == 2 + + node.receive_event( + Event( + type="firmware update finished", + data={ + "source": "node", + "event": "firmware update finished", + "nodeId": node.node_id, + "status": FirmwareUpdateStatus.OK_NO_RESTART, + }, + ) + ) + + # Block so HA can do its thing + await asyncio.sleep(0) + + # One file done, progress should be 50% + state = hass.states.get(UPDATE_ENTITY) + assert state + attrs = state.attributes + assert attrs[ATTR_IN_PROGRESS] == 50 + + node.receive_event( + Event( + type="firmware update progress", + data={ + "source": "node", + "event": "firmware update progress", + "nodeId": node.node_id, + "sentFragments": 1, + "totalFragments": 20, + }, + ) + ) + + # Block so HA can do its thing + await asyncio.sleep(0) + + # Validate that the progress is updated (50% + 50% of 5) + state = hass.states.get(UPDATE_ENTITY) + assert state + attrs = state.attributes + assert attrs[ATTR_IN_PROGRESS] == 52 + + node.receive_event( + Event( + type="firmware update finished", + data={ + "source": "node", + "event": "firmware update finished", + "nodeId": node.node_id, + "status": FirmwareUpdateStatus.OK_NO_RESTART, + }, + ) + ) + + # Block so HA can do its thing + await asyncio.sleep(0) + + # Validate that progress is reset and entity reflects new version + state = hass.states.get(UPDATE_ENTITY) + assert state + attrs = state.attributes + assert attrs[ATTR_IN_PROGRESS] == 0 assert attrs[ATTR_INSTALLED_VERSION] == "11.2.4" assert attrs[ATTR_LATEST_VERSION] == "11.2.4" assert state.state == STATE_OFF @@ -446,7 +599,7 @@ async def test_update_entity_install_failed( state = hass.states.get(UPDATE_ENTITY) assert state attrs = state.attributes - assert attrs[ATTR_IN_PROGRESS] is False + assert attrs[ATTR_IN_PROGRESS] == 0 assert attrs[ATTR_INSTALLED_VERSION] == "10.7" assert attrs[ATTR_LATEST_VERSION] == "11.2.4" assert state.state == STATE_ON From d4181aa9112ddd02679901937b065c4146503d12 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 18 Sep 2022 10:22:54 -0500 Subject: [PATCH 515/955] Fix bluetooth callback matchers when only matching on connectable (#78687) --- homeassistant/components/bluetooth/manager.py | 4 +- homeassistant/components/bluetooth/match.py | 53 ++++++++++++------ tests/components/bluetooth/test_init.py | 55 +++++++++++++++++++ 3 files changed, 93 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index 533867496bf..0dcf11fd1e2 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -410,11 +410,11 @@ class BluetoothManager: callback_matcher[CONNECTABLE] = matcher.get(CONNECTABLE, True) connectable = callback_matcher[CONNECTABLE] - self._callback_index.add_with_address(callback_matcher) + self._callback_index.add_callback_matcher(callback_matcher) @hass_callback def _async_remove_callback() -> None: - self._callback_index.remove_with_address(callback_matcher) + self._callback_index.remove_callback_matcher(callback_matcher) # If we have history for the subscriber, we can trigger the callback # immediately with the last packet so the subscriber can see the diff --git a/homeassistant/components/bluetooth/match.py b/homeassistant/components/bluetooth/match.py index dd1c9c1fa3c..1a59ee6fe4c 100644 --- a/homeassistant/components/bluetooth/match.py +++ b/homeassistant/components/bluetooth/match.py @@ -173,7 +173,7 @@ class BluetoothMatcherIndexBase(Generic[_T]): self.service_data_uuid_set: set[str] = set() self.manufacturer_id_set: set[int] = set() - def add(self, matcher: _T) -> None: + def add(self, matcher: _T) -> bool: """Add a matcher to the index. Matchers must end up only in one bucket. @@ -185,26 +185,28 @@ class BluetoothMatcherIndexBase(Generic[_T]): self.local_name.setdefault( _local_name_to_index_key(matcher[LOCAL_NAME]), [] ).append(matcher) - return + return True # Manufacturer data is 2nd cheapest since its all ints if MANUFACTURER_ID in matcher: self.manufacturer_id.setdefault(matcher[MANUFACTURER_ID], []).append( matcher ) - return + return True if SERVICE_UUID in matcher: self.service_uuid.setdefault(matcher[SERVICE_UUID], []).append(matcher) - return + return True if SERVICE_DATA_UUID in matcher: self.service_data_uuid.setdefault(matcher[SERVICE_DATA_UUID], []).append( matcher ) - return + return True - def remove(self, matcher: _T) -> None: + return False + + def remove(self, matcher: _T) -> bool: """Remove a matcher from the index. Matchers only end up in one bucket, so once we have @@ -214,19 +216,21 @@ class BluetoothMatcherIndexBase(Generic[_T]): self.local_name[_local_name_to_index_key(matcher[LOCAL_NAME])].remove( matcher ) - return + return True if MANUFACTURER_ID in matcher: self.manufacturer_id[matcher[MANUFACTURER_ID]].remove(matcher) - return + return True if SERVICE_UUID in matcher: self.service_uuid[matcher[SERVICE_UUID]].remove(matcher) - return + return True if SERVICE_DATA_UUID in matcher: self.service_data_uuid[matcher[SERVICE_DATA_UUID]].remove(matcher) - return + return True + + return False def build(self) -> None: """Rebuild the index sets.""" @@ -284,8 +288,11 @@ class BluetoothCallbackMatcherIndex( """Initialize the matcher index.""" super().__init__() self.address: dict[str, list[BluetoothCallbackMatcherWithCallback]] = {} + self.connectable: list[BluetoothCallbackMatcherWithCallback] = [] - def add_with_address(self, matcher: BluetoothCallbackMatcherWithCallback) -> None: + def add_callback_matcher( + self, matcher: BluetoothCallbackMatcherWithCallback + ) -> None: """Add a matcher to the index. Matchers must end up only in one bucket. @@ -296,10 +303,15 @@ class BluetoothCallbackMatcherIndex( self.address.setdefault(matcher[ADDRESS], []).append(matcher) return - super().add(matcher) - self.build() + if super().add(matcher): + self.build() + return - def remove_with_address( + if CONNECTABLE in matcher: + self.connectable.append(matcher) + return + + def remove_callback_matcher( self, matcher: BluetoothCallbackMatcherWithCallback ) -> None: """Remove a matcher from the index. @@ -311,8 +323,13 @@ class BluetoothCallbackMatcherIndex( self.address[matcher[ADDRESS]].remove(matcher) return - super().remove(matcher) - self.build() + if super().remove(matcher): + self.build() + return + + if CONNECTABLE in matcher: + self.connectable.remove(matcher) + return def match_callbacks( self, service_info: BluetoothServiceInfoBleak @@ -322,6 +339,9 @@ class BluetoothCallbackMatcherIndex( for matcher in self.address.get(service_info.address, []): if ble_device_matches(matcher, service_info): matches.append(matcher) + for matcher in self.connectable: + if ble_device_matches(matcher, service_info): + matches.append(matcher) return matches @@ -355,7 +375,6 @@ def ble_device_matches( # Don't check address here since all callers already # check the address and we don't want to double check # since it would result in an unreachable reject case. - if matcher.get(CONNECTABLE, True) and not service_info.connectable: return False diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index 1c3c58bc7ab..32feb3d7b0f 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -1327,6 +1327,61 @@ async def test_register_callback_by_manufacturer_id( assert service_info.manufacturer_id == 21 +async def test_register_callback_by_connectable( + hass, mock_bleak_scanner_start, enable_bluetooth +): + """Test registering a callback by connectable.""" + mock_bt = [] + callbacks = [] + + def _fake_subscriber( + service_info: BluetoothServiceInfo, change: BluetoothChange + ) -> None: + """Fake subscriber for the BleakScanner.""" + callbacks.append((service_info, change)) + + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ): + await async_setup_with_default_adapter(hass) + + with patch.object(hass.config_entries.flow, "async_init"): + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + cancel = bluetooth.async_register_callback( + hass, + _fake_subscriber, + {CONNECTABLE: False}, + BluetoothScanningMode.ACTIVE, + ) + + assert len(mock_bleak_scanner_start.mock_calls) == 1 + + apple_device = BLEDevice("44:44:33:11:23:45", "rtx") + apple_adv = AdvertisementData( + local_name="rtx", + manufacturer_data={7676: b"\xd8.\xad\xcd\r\x85"}, + ) + + inject_advertisement(hass, apple_device, apple_adv) + + empty_device = BLEDevice("11:22:33:44:55:66", "empty") + empty_adv = AdvertisementData(local_name="empty") + + inject_advertisement(hass, empty_device, empty_adv) + await hass.async_block_till_done() + + cancel() + + assert len(callbacks) == 2 + + service_info: BluetoothServiceInfo = callbacks[0][0] + assert service_info.name == "rtx" + service_info: BluetoothServiceInfo = callbacks[1][0] + assert service_info.name == "empty" + + async def test_not_filtering_wanted_apple_devices( hass, mock_bleak_scanner_start, enable_bluetooth ): From a282e41d683859ac00a4078734984bc3d55659c1 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 18 Sep 2022 09:24:13 -0600 Subject: [PATCH 516/955] Revert unintended OpenUV unique ID change (#78691) --- homeassistant/components/openuv/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/openuv/__init__.py b/homeassistant/components/openuv/__init__.py index bef5bb23059..365a3ab247a 100644 --- a/homeassistant/components/openuv/__init__.py +++ b/homeassistant/components/openuv/__init__.py @@ -354,10 +354,11 @@ class OpenUvEntity(Entity): def __init__(self, openuv: OpenUV, description: EntityDescription) -> None: """Initialize.""" - coordinates = f"{openuv.client.latitude}, {openuv.client.longitude}" self._attr_extra_state_attributes = {} self._attr_should_poll = False - self._attr_unique_id = f"{coordinates}_{description.key}" + self._attr_unique_id = ( + f"{openuv.client.latitude}_{openuv.client.longitude}_{description.key}" + ) self.entity_description = description self.openuv = openuv From 49ead219a5484d6b0d00eadefb42de4dd2232abf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 18 Sep 2022 11:47:13 -0500 Subject: [PATCH 517/955] Bump thermobeacon-ble to 0.3.2 (#78693) --- homeassistant/components/thermobeacon/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/thermobeacon/manifest.json b/homeassistant/components/thermobeacon/manifest.json index eb13b68a7e2..639d2362026 100644 --- a/homeassistant/components/thermobeacon/manifest.json +++ b/homeassistant/components/thermobeacon/manifest.json @@ -24,7 +24,7 @@ }, { "local_name": "ThermoBeacon", "connectable": false } ], - "requirements": ["thermobeacon-ble==0.3.1"], + "requirements": ["thermobeacon-ble==0.3.2"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 2bfe314040f..daaa91a5704 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2375,7 +2375,7 @@ tesla-wall-connector==1.0.2 # tf-models-official==2.5.0 # homeassistant.components.thermobeacon -thermobeacon-ble==0.3.1 +thermobeacon-ble==0.3.2 # homeassistant.components.thermopro thermopro-ble==0.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2ff915966f0..2bae9a1d8ca 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1621,7 +1621,7 @@ tesla-powerwall==0.3.18 tesla-wall-connector==1.0.2 # homeassistant.components.thermobeacon -thermobeacon-ble==0.3.1 +thermobeacon-ble==0.3.2 # homeassistant.components.thermopro thermopro-ble==0.4.3 From 354411feed482105d296824d052b4217164a132b Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sun, 18 Sep 2022 18:55:31 +0200 Subject: [PATCH 518/955] Link manually added MQTT entities the the MQTT config entry (#78547) Co-authored-by: Erik --- homeassistant/components/mqtt/__init__.py | 17 ++++- .../components/mqtt/alarm_control_panel.py | 4 -- .../components/mqtt/binary_sensor.py | 4 -- homeassistant/components/mqtt/button.py | 4 -- homeassistant/components/mqtt/camera.py | 4 -- homeassistant/components/mqtt/climate.py | 4 -- homeassistant/components/mqtt/cover.py | 4 -- homeassistant/components/mqtt/fan.py | 4 -- homeassistant/components/mqtt/humidifier.py | 4 -- .../components/mqtt/light/__init__.py | 4 -- homeassistant/components/mqtt/lock.py | 4 -- homeassistant/components/mqtt/mixins.py | 57 ++++++++-------- homeassistant/components/mqtt/number.py | 4 -- homeassistant/components/mqtt/scene.py | 4 -- homeassistant/components/mqtt/select.py | 4 -- homeassistant/components/mqtt/sensor.py | 12 ++-- homeassistant/components/mqtt/siren.py | 4 -- homeassistant/components/mqtt/switch.py | 4 -- .../components/mqtt/vacuum/__init__.py | 9 +-- tests/components/mqtt/test_init.py | 67 +++++++++++++++++++ 20 files changed, 117 insertions(+), 105 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 315f116ed92..c14266e296f 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -30,6 +30,7 @@ from homeassistant.helpers import ( ) from homeassistant.helpers.device_registry import DeviceEntry from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import async_get_platforms from homeassistant.helpers.reload import ( async_integration_yaml_config, async_reload_integration_platforms, @@ -75,7 +76,7 @@ from .const import ( # noqa: F401 PLATFORMS, RELOADABLE_PLATFORMS, ) -from .mixins import MqttData, async_discover_yaml_entities +from .mixins import MqttData from .models import ( # noqa: F401 MqttCommandTemplate, MqttValueTemplate, @@ -422,13 +423,25 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await async_reload_integration_platforms(hass, DOMAIN, RELOADABLE_PLATFORMS) # Reload the modern yaml platforms + mqtt_platforms = async_get_platforms(hass, DOMAIN) + tasks = [ + entity.async_remove() + for mqtt_platform in mqtt_platforms + for entity in mqtt_platform.entities.values() + if not entity._discovery_data # type: ignore[attr-defined] # pylint: disable=protected-access + if mqtt_platform.config_entry + and mqtt_platform.domain in RELOADABLE_PLATFORMS + ] + await asyncio.gather(*tasks) + config_yaml = await async_integration_yaml_config(hass, DOMAIN) or {} mqtt_data.updated_config = config_yaml.get(DOMAIN, {}) await asyncio.gather( *( [ - async_discover_yaml_entities(hass, component) + mqtt_data.reload_handlers[component]() for component in RELOADABLE_PLATFORMS + if component in mqtt_data.reload_handlers ] ) ) diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index cf7262f9468..c3502cd8e64 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -44,7 +44,6 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, - async_discover_yaml_entities, async_setup_entry_helper, async_setup_platform_helper, warn_for_legacy_schema, @@ -146,9 +145,6 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up MQTT alarm control panel through configuration.yaml and dynamically through MQTT discovery.""" - # load and initialize platform config from configuration.yaml - await async_discover_yaml_entities(hass, alarm.DOMAIN) - # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index adb4c12daf9..f0e5ecc9df8 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -42,7 +42,6 @@ from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttAvailability, MqttEntity, - async_discover_yaml_entities, async_setup_entry_helper, async_setup_platform_helper, warn_for_legacy_schema, @@ -102,9 +101,6 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up MQTT binary sensor through configuration.yaml and dynamically through MQTT discovery.""" - # load and initialize platform config from configuration.yaml - await async_discover_yaml_entities(hass, binary_sensor.DOMAIN) - # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/button.py b/homeassistant/components/mqtt/button.py index 0881b963b04..a14bf87c3be 100644 --- a/homeassistant/components/mqtt/button.py +++ b/homeassistant/components/mqtt/button.py @@ -25,7 +25,6 @@ from .const import ( from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, - async_discover_yaml_entities, async_setup_entry_helper, async_setup_platform_helper, warn_for_legacy_schema, @@ -81,9 +80,6 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up MQTT button through configuration.yaml and dynamically through MQTT discovery.""" - # load and initialize platform config from configuration.yaml - await async_discover_yaml_entities(hass, button.DOMAIN) - # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py index 61c87e86888..f6039251882 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -23,7 +23,6 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, - async_discover_yaml_entities, async_setup_entry_helper, async_setup_platform_helper, warn_for_legacy_schema, @@ -105,9 +104,6 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up MQTT camera through configuration.yaml and dynamically through MQTT discovery.""" - # load and initialize platform config from configuration.yaml - await async_discover_yaml_entities(hass, camera.DOMAIN) - # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 3f0f8a89b3e..96c7ca3665b 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -50,7 +50,6 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, - async_discover_yaml_entities, async_setup_entry_helper, async_setup_platform_helper, warn_for_legacy_schema, @@ -350,9 +349,6 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up MQTT climate device through configuration.yaml and dynamically through MQTT discovery.""" - # load and initialize platform config from configuration.yaml - await async_discover_yaml_entities(hass, climate.DOMAIN) - # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index fd96fe524d9..1f5d26c3a78 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -46,7 +46,6 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, - async_discover_yaml_entities, async_setup_entry_helper, async_setup_platform_helper, warn_for_legacy_schema, @@ -242,9 +241,6 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up MQTT cover through configuration.yaml and dynamically through MQTT discovery.""" - # load and initialize platform config from configuration.yaml - await async_discover_yaml_entities(hass, cover.DOMAIN) - # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index fab748d2bfc..584df08e7d7 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -50,7 +50,6 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, - async_discover_yaml_entities, async_setup_entry_helper, async_setup_platform_helper, warn_for_legacy_schema, @@ -241,9 +240,6 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up MQTT fan through configuration.yaml and dynamically through MQTT discovery.""" - # load and initialize platform config from configuration.yaml - await async_discover_yaml_entities(hass, fan.DOMAIN) - # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/humidifier.py b/homeassistant/components/mqtt/humidifier.py index 3a1271ea2c9..837bbb8b909 100644 --- a/homeassistant/components/mqtt/humidifier.py +++ b/homeassistant/components/mqtt/humidifier.py @@ -46,7 +46,6 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, - async_discover_yaml_entities, async_setup_entry_helper, async_setup_platform_helper, warn_for_legacy_schema, @@ -187,9 +186,6 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up MQTT humidifier through configuration.yaml and dynamically through MQTT discovery.""" - # load and initialize platform config from configuration.yaml - await async_discover_yaml_entities(hass, humidifier.DOMAIN) - # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/light/__init__.py b/homeassistant/components/mqtt/light/__init__.py index 76c2980e63b..b7d52919d5e 100644 --- a/homeassistant/components/mqtt/light/__init__.py +++ b/homeassistant/components/mqtt/light/__init__.py @@ -14,7 +14,6 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from ..mixins import ( - async_discover_yaml_entities, async_setup_entry_helper, async_setup_platform_helper, warn_for_legacy_schema, @@ -111,9 +110,6 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up MQTT lights configured under the light platform key (deprecated).""" - # load and initialize platform config from configuration.yaml - await async_discover_yaml_entities(hass, light.DOMAIN) - # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index 4910eafae75..dca02f909dc 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -28,7 +28,6 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, - async_discover_yaml_entities, async_setup_entry_helper, async_setup_platform_helper, warn_for_legacy_schema, @@ -102,9 +101,6 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up MQTT lock through configuration.yaml and dynamically through MQTT discovery.""" - # load and initialize platform config from configuration.yaml - await async_discover_yaml_entities(hass, lock.DOMAIN) - # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index a16394667d8..477be399e26 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -39,7 +39,6 @@ from homeassistant.core import ( from homeassistant.helpers import ( config_validation as cv, device_registry as dr, - discovery, entity_registry as er, ) from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED @@ -286,6 +285,9 @@ class MqttData: last_discovery: float = 0.0 reload_dispatchers: list[CALLBACK_TYPE] = field(default_factory=list) reload_entry: bool = False + reload_handlers: dict[str, Callable[[], Coroutine[Any, Any, None]]] = field( + default_factory=dict + ) reload_needed: bool = False subscriptions_to_restore: list[Subscription] = field(default_factory=list) updated_config: ConfigType = field(default_factory=dict) @@ -305,30 +307,6 @@ class SetupEntity(Protocol): """Define setup_entities type.""" -async def async_discover_yaml_entities( - hass: HomeAssistant, platform_domain: str -) -> None: - """Discover entities for a platform.""" - mqtt_data: MqttData = hass.data[DATA_MQTT] - if mqtt_data.updated_config: - # The platform has been reloaded - config_yaml = mqtt_data.updated_config - else: - config_yaml = mqtt_data.config or {} - if not config_yaml: - return - if platform_domain not in config_yaml: - return - await asyncio.gather( - *( - discovery.async_load_platform(hass, platform_domain, DOMAIN, config, {}) - for config in await async_get_platform_config_from_yaml( - hass, platform_domain, config_yaml - ) - ) - ) - - async def async_get_platform_config_from_yaml( hass: HomeAssistant, platform_domain: str, @@ -350,7 +328,7 @@ async def async_setup_entry_helper( hass: HomeAssistant, domain: str, async_setup: partial[Coroutine[HomeAssistant, str, None]], - schema: vol.Schema, + discovery_schema: vol.Schema, ) -> None: """Set up entity, automation or tag creation dynamically through MQTT discovery.""" mqtt_data: MqttData = hass.data[DATA_MQTT] @@ -367,7 +345,7 @@ async def async_setup_entry_helper( return discovery_data = discovery_payload.discovery_data try: - config = schema(discovery_payload) + config = discovery_schema(discovery_payload) await async_setup(config, discovery_data=discovery_data) except Exception: discovery_hash = discovery_data[ATTR_DISCOVERY_HASH] @@ -383,6 +361,31 @@ async def async_setup_entry_helper( ) ) + async def _async_setup_entities() -> None: + """Set up MQTT items from configuration.yaml.""" + mqtt_data: MqttData = hass.data[DATA_MQTT] + if mqtt_data.updated_config: + # The platform has been reloaded + config_yaml = mqtt_data.updated_config + else: + config_yaml = mqtt_data.config or {} + if not config_yaml: + return + if domain not in config_yaml: + return + await asyncio.gather( + *[ + async_setup(config) + for config in await async_get_platform_config_from_yaml( + hass, domain, config_yaml + ) + ] + ) + + # discover manual configured MQTT items + mqtt_data.reload_handlers[domain] = _async_setup_entities + await _async_setup_entities() + async def async_setup_platform_helper( hass: HomeAssistant, diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index eeac406f668..09f9d122b98 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -44,7 +44,6 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, - async_discover_yaml_entities, async_setup_entry_helper, async_setup_platform_helper, warn_for_legacy_schema, @@ -138,9 +137,6 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up MQTT number through configuration.yaml and dynamically through MQTT discovery.""" - # load and initialize platform config from configuration.yaml - await async_discover_yaml_entities(hass, number.DOMAIN) - # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/scene.py b/homeassistant/components/mqtt/scene.py index 70a4cad7f37..e237d70e903 100644 --- a/homeassistant/components/mqtt/scene.py +++ b/homeassistant/components/mqtt/scene.py @@ -23,7 +23,6 @@ from .mixins import ( CONF_OBJECT_ID, MQTT_AVAILABILITY_SCHEMA, MqttEntity, - async_discover_yaml_entities, async_setup_entry_helper, async_setup_platform_helper, warn_for_legacy_schema, @@ -79,9 +78,6 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up MQTT scene through configuration.yaml and dynamically through MQTT discovery.""" - # load and initialize platform config from configuration.yaml - await async_discover_yaml_entities(hass, scene.DOMAIN) - # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/select.py b/homeassistant/components/mqtt/select.py index ec88b1732d4..a6de0495690 100644 --- a/homeassistant/components/mqtt/select.py +++ b/homeassistant/components/mqtt/select.py @@ -30,7 +30,6 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, - async_discover_yaml_entities, async_setup_entry_helper, async_setup_platform_helper, warn_for_legacy_schema, @@ -93,9 +92,6 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up MQTT select through configuration.yaml and dynamically through MQTT discovery.""" - # load and initialize platform config from configuration.yaml - await async_discover_yaml_entities(hass, select.DOMAIN) - # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index b361ce06a81..7c98fdf51b7 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 timedelta +from datetime import datetime, timedelta import functools import logging @@ -30,7 +30,7 @@ from homeassistant.core import HomeAssistant, 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 -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType from homeassistant.util import dt as dt_util from . import subscription @@ -41,7 +41,6 @@ from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttAvailability, MqttEntity, - async_discover_yaml_entities, async_setup_entry_helper, async_setup_platform_helper, warn_for_legacy_schema, @@ -146,9 +145,6 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up MQTT sensor through configuration.yaml and dynamically through MQTT discovery.""" - # load and initialize platform config from configuration.yaml - await async_discover_yaml_entities(hass, sensor.DOMAIN) - # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) @@ -347,12 +343,12 @@ class MqttSensor(MqttEntity, RestoreSensor): self.async_write_ha_state() @property - def native_unit_of_measurement(self): + 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): + def native_value(self) -> StateType | datetime: """Return the state of the entity.""" return self._state diff --git a/homeassistant/components/mqtt/siren.py b/homeassistant/components/mqtt/siren.py index 1f5111a5499..c8332046092 100644 --- a/homeassistant/components/mqtt/siren.py +++ b/homeassistant/components/mqtt/siren.py @@ -49,7 +49,6 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, - async_discover_yaml_entities, async_setup_entry_helper, async_setup_platform_helper, warn_for_legacy_schema, @@ -140,9 +139,6 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up MQTT siren through configuration.yaml and dynamically through MQTT discovery.""" - # load and initialize platform config from configuration.yaml - await async_discover_yaml_entities(hass, siren.DOMAIN) - # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index b5c7ab13dfc..af16b14bea1 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -42,7 +42,6 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, - async_discover_yaml_entities, async_setup_entry_helper, async_setup_platform_helper, warn_for_legacy_schema, @@ -101,9 +100,6 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up MQTT switch through configuration.yaml and dynamically through MQTT discovery.""" - # load and initialize platform config from configuration.yaml - await async_discover_yaml_entities(hass, switch.DOMAIN) - # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/vacuum/__init__.py b/homeassistant/components/mqtt/vacuum/__init__.py index cdd14e6d8e3..abab55c632c 100644 --- a/homeassistant/components/mqtt/vacuum/__init__.py +++ b/homeassistant/components/mqtt/vacuum/__init__.py @@ -11,11 +11,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from ..mixins import ( - async_discover_yaml_entities, - async_setup_entry_helper, - async_setup_platform_helper, -) +from ..mixins import async_setup_entry_helper, async_setup_platform_helper from .schema import CONF_SCHEMA, LEGACY, MQTT_VACUUM_SCHEMA, STATE from .schema_legacy import ( DISCOVERY_SCHEMA_LEGACY, @@ -90,9 +86,6 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up MQTT vacuum through configuration.yaml and dynamically through MQTT discovery.""" - # load and initialize platform config from configuration.yaml - await async_discover_yaml_entities(hass, vacuum.DOMAIN) - # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 46649bf703f..90df45b65a1 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -29,11 +29,13 @@ from homeassistant.core import CoreState, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, template from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import async_get_platforms from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow from .test_common import ( help_test_entry_reload_with_new_config, + help_test_reload_with_config, help_test_setup_manual_entity_from_yaml, ) @@ -2986,3 +2988,68 @@ async def test_remove_unknown_conf_entry_options(hass, mqtt_client_mock, caplog) "MQTT config entry: {'protocol'}. Add them to configuration.yaml if they " "are needed" ) in caplog.text + + +@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.LIGHT]) +async def test_link_config_entry(hass, tmp_path, caplog): + """Test manual and dynamically setup entities are linked to the config entry.""" + config_manual = { + "mqtt": { + "light": [ + { + "name": "test_manual", + "unique_id": "test_manual_unique_id123", + "command_topic": "test-topic_manual", + } + ] + } + } + config_discovery = { + "name": "test_discovery", + "unique_id": "test_discovery_unique456", + "command_topic": "test-topic_discovery", + } + + # set up manual item + await help_test_setup_manual_entity_from_yaml(hass, config_manual) + + # set up item through discovery + async_fire_mqtt_message( + hass, "homeassistant/light/bla/config", json.dumps(config_discovery) + ) + await hass.async_block_till_done() + + assert hass.states.get("light.test_manual") is not None + assert hass.states.get("light.test_discovery") is not None + entity_names = ["test_manual", "test_discovery"] + + # Check if both entities were linked to the MQTT config entry + mqtt_config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] + mqtt_platforms = async_get_platforms(hass, mqtt.DOMAIN) + + def _check_entities(): + entities = [] + for mqtt_platform in mqtt_platforms: + assert mqtt_platform.config_entry is mqtt_config_entry + entities += (entity for entity in mqtt_platform.entities.values()) + + for entity in entities: + assert entity.name in entity_names + return len(entities) + + assert _check_entities() == 2 + + # reload entry and assert again + await help_test_entry_reload_with_new_config(hass, tmp_path, config_manual) + # manual set up item should remain + assert _check_entities() == 1 + # set up item through discovery + async_fire_mqtt_message( + hass, "homeassistant/light/bla/config", json.dumps(config_discovery) + ) + await hass.async_block_till_done() + assert _check_entities() == 2 + + # reload manual configured items and assert again + await help_test_reload_with_config(hass, caplog, tmp_path, config_manual) + assert _check_entities() == 2 From 6094c0070570c78a8d01ca0bffcefb970faae61d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sun, 18 Sep 2022 19:50:43 +0200 Subject: [PATCH 519/955] Warn user if Tasmota devices are configured with invalid MQTT topics (#77640) --- homeassistant/components/tasmota/discovery.py | 135 ++++++++++++- .../components/tasmota/manifest.json | 2 +- homeassistant/components/tasmota/strings.json | 10 + .../components/tasmota/translations/en.json | 10 + requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/tasmota/test_discovery.py | 182 +++++++++++++++++- 7 files changed, 337 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/tasmota/discovery.py b/homeassistant/components/tasmota/discovery.py index da9e809bd8b..8afac859b88 100644 --- a/homeassistant/components/tasmota/discovery.py +++ b/homeassistant/components/tasmota/discovery.py @@ -3,7 +3,9 @@ from __future__ import annotations from collections.abc import Awaitable, Callable import logging +from typing import TypedDict, cast +from hatasmota import const as tasmota_const from hatasmota.discovery import ( TasmotaDiscovery, get_device_config as tasmota_get_device_config, @@ -17,11 +19,16 @@ from hatasmota.entity import TasmotaEntityConfig from hatasmota.models import DiscoveryHashType, TasmotaDeviceConfig from hatasmota.mqtt import TasmotaMQTTClient from hatasmota.sensor import TasmotaBaseSensorConfig +from hatasmota.utils import get_topic_command, get_topic_stat from homeassistant.components import sensor from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers import ( + device_registry as dr, + entity_registry as er, + issue_registry as ir, +) from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity_registry import async_entries_for_device @@ -30,10 +37,13 @@ from .const import DOMAIN, PLATFORMS _LOGGER = logging.getLogger(__name__) ALREADY_DISCOVERED = "tasmota_discovered_components" +DISCOVERY_DATA = "tasmota_discovery_data" TASMOTA_DISCOVERY_ENTITY_NEW = "tasmota_discovery_entity_new_{}" TASMOTA_DISCOVERY_ENTITY_UPDATED = "tasmota_discovery_entity_updated_{}_{}_{}_{}" TASMOTA_DISCOVERY_INSTANCE = "tasmota_discovery_instance" +MQTT_TOPIC_URL = "https://tasmota.github.io/docs/Home-Assistant/#tasmota-integration" + SetupDeviceCallback = Callable[[TasmotaDeviceConfig, str], Awaitable[None]] @@ -52,7 +62,64 @@ def set_discovery_hash(hass: HomeAssistant, discovery_hash: DiscoveryHashType) - hass.data[ALREADY_DISCOVERED][discovery_hash] = {} -async def async_start( +def warn_if_topic_duplicated( + hass: HomeAssistant, + command_topic: str, + own_mac: str | None, + own_device_config: TasmotaDeviceConfig, +) -> bool: + """Log and create repairs issue if several devices share the same topic.""" + duplicated = False + offenders = [] + for other_mac, other_config in hass.data[DISCOVERY_DATA].items(): + if own_mac and other_mac == own_mac: + continue + if command_topic == get_topic_command(other_config): + offenders.append((other_mac, tasmota_get_device_config(other_config))) + issue_id = f"topic_duplicated_{command_topic}" + if offenders: + if own_mac: + offenders.append((own_mac, own_device_config)) + offender_strings = [ + f"'{cfg[tasmota_const.CONF_NAME]}' ({cfg[tasmota_const.CONF_IP]})" + for _, cfg in offenders + ] + _LOGGER.warning( + "Multiple Tasmota devices are sharing the same topic '%s'. Offending devices: %s", + command_topic, + ", ".join(offender_strings), + ) + ir.async_create_issue( + hass, + DOMAIN, + issue_id, + data={ + "key": "topic_duplicated", + "mac": " ".join([mac for mac, _ in offenders]), + "topic": command_topic, + }, + is_fixable=False, + learn_more_url=MQTT_TOPIC_URL, + severity=ir.IssueSeverity.ERROR, + translation_key="topic_duplicated", + translation_placeholders={ + "topic": command_topic, + "offenders": "\n\n* " + "\n\n* ".join(offender_strings), + }, + ) + duplicated = True + return duplicated + + +class DuplicatedTopicIssueData(TypedDict): + """Typed result dict.""" + + key: str + mac: str + topic: str + + +async def async_start( # noqa: C901 hass: HomeAssistant, discovery_topic: str, config_entry: ConfigEntry, @@ -121,9 +188,72 @@ async def async_start( tasmota_device_config = tasmota_get_device_config(payload) await setup_device(tasmota_device_config, mac) + hass.data[DISCOVERY_DATA][mac] = payload + + add_entities = True + + command_topic = get_topic_command(payload) if payload else None + state_topic = get_topic_stat(payload) if payload else None + + # Create or clear issue if topic is missing prefix + issue_id = f"topic_no_prefix_{mac}" + if payload and command_topic == state_topic: + _LOGGER.warning( + "Tasmota device '%s' with IP %s doesn't set %%prefix%% in its topic", + tasmota_device_config[tasmota_const.CONF_NAME], + tasmota_device_config[tasmota_const.CONF_IP], + ) + ir.async_create_issue( + hass, + DOMAIN, + issue_id, + data={"key": "topic_no_prefix"}, + is_fixable=False, + learn_more_url=MQTT_TOPIC_URL, + severity=ir.IssueSeverity.ERROR, + translation_key="topic_no_prefix", + translation_placeholders={ + "name": tasmota_device_config[tasmota_const.CONF_NAME], + "ip": tasmota_device_config[tasmota_const.CONF_IP], + }, + ) + add_entities = False + else: + ir.async_delete_issue(hass, DOMAIN, issue_id) + + # Clear previous issues caused by duplicated topic + issue_reg = ir.async_get(hass) + tasmota_issues = [ + issue for key, issue in issue_reg.issues.items() if key[0] == DOMAIN + ] + for issue in tasmota_issues: + if issue.data and issue.data["key"] == "topic_duplicated": + issue_data: DuplicatedTopicIssueData = cast( + DuplicatedTopicIssueData, issue.data + ) + macs = issue_data["mac"].split() + if mac not in macs: + continue + if payload and command_topic == issue_data["topic"]: + continue + if len(macs) > 2: + # This device is no longer duplicated, update the issue + warn_if_topic_duplicated(hass, issue_data["topic"], None, {}) + continue + ir.async_delete_issue(hass, DOMAIN, issue.issue_id) + if not payload: return + # Warn and add issues if there are duplicated topics + if warn_if_topic_duplicated(hass, command_topic, mac, tasmota_device_config): + add_entities = False + + if not add_entities: + # Add the device entry so the user can identify the device, but do not add + # entities or triggers + return + tasmota_triggers = tasmota_get_triggers(payload) for trigger_config in tasmota_triggers: discovery_hash: DiscoveryHashType = ( @@ -194,6 +324,7 @@ async def async_start( entity_registry.async_remove(entity_id) hass.data[ALREADY_DISCOVERED] = {} + hass.data[DISCOVERY_DATA] = {} tasmota_discovery = TasmotaDiscovery(discovery_topic, tasmota_mqtt) await tasmota_discovery.start_discovery( diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json index d6ba4cf90cc..6e3e69f59fe 100644 --- a/homeassistant/components/tasmota/manifest.json +++ b/homeassistant/components/tasmota/manifest.json @@ -3,7 +3,7 @@ "name": "Tasmota", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tasmota", - "requirements": ["hatasmota==0.6.0"], + "requirements": ["hatasmota==0.6.1"], "dependencies": ["mqtt"], "mqtt": ["tasmota/discovery/#"], "codeowners": ["@emontnemery"], diff --git a/homeassistant/components/tasmota/strings.json b/homeassistant/components/tasmota/strings.json index 2a23912b80c..22af3304297 100644 --- a/homeassistant/components/tasmota/strings.json +++ b/homeassistant/components/tasmota/strings.json @@ -16,5 +16,15 @@ "error": { "invalid_discovery_topic": "Invalid discovery topic prefix." } + }, + "issues": { + "topic_duplicated": { + "title": "Several Tasmota devices are sharing the same topic", + "description": "Several Tasmota devices are sharing the topic {topic}.\n\n Tasmota devices with this problem: {offenders}." + }, + "topic_no_prefix": { + "title": "Tasmota device {name} has an invalid MQTT topic", + "description": "Tasmota device {name} with IP {ip} does not include `%prefix%` in its fulltopic.\n\nEntities for this devices are disabled until the configuration has been corrected." + } } } diff --git a/homeassistant/components/tasmota/translations/en.json b/homeassistant/components/tasmota/translations/en.json index 3e8b0b43bce..bb4d174a04d 100644 --- a/homeassistant/components/tasmota/translations/en.json +++ b/homeassistant/components/tasmota/translations/en.json @@ -16,5 +16,15 @@ "description": "Do you want to set up Tasmota?" } } + }, + "issues": { + "topic_duplicated": { + "description": "Several Tasmota devices are sharing the topic {topic}.\n\n Tasmota devices with this problem: {offenders}.", + "title": "Several Tasmota devices are sharing the same topic" + }, + "topic_no_prefix": { + "description": "Tasmota device {name} with IP {ip} does not include `%prefix%` in its fulltopic.\n\nEntities for this devices are disabled until the configuration has been corrected.", + "title": "Tasmota device {name} has an invalid MQTT topic" + } } } \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index daaa91a5704..8d0bd4ff995 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -830,7 +830,7 @@ hass-nabucasa==0.55.0 hass_splunk==0.1.1 # homeassistant.components.tasmota -hatasmota==0.6.0 +hatasmota==0.6.1 # homeassistant.components.jewish_calendar hdate==0.10.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2bae9a1d8ca..bdba84bb811 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -613,7 +613,7 @@ hangups==0.4.18 hass-nabucasa==0.55.0 # homeassistant.components.tasmota -hatasmota==0.6.0 +hatasmota==0.6.1 # homeassistant.components.jewish_calendar hdate==0.10.4 diff --git a/tests/components/tasmota/test_discovery.py b/tests/components/tasmota/test_discovery.py index a97b877d819..55bb385817d 100644 --- a/tests/components/tasmota/test_discovery.py +++ b/tests/components/tasmota/test_discovery.py @@ -5,7 +5,11 @@ from unittest.mock import ANY, patch from homeassistant.components.tasmota.const import DEFAULT_PREFIX from homeassistant.components.tasmota.discovery import ALREADY_DISCOVERED -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import ( + device_registry as dr, + entity_registry as er, + issue_registry as ir, +) from homeassistant.setup import async_setup_component from .conftest import setup_tasmota_helper @@ -495,3 +499,179 @@ async def test_entity_duplicate_removal(hass, mqtt_mock, caplog, setup_tasmota): async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{mac}/config", json.dumps(config)) await hass.async_block_till_done() assert "Removing entity: switch" not in caplog.text + + +async def test_same_topic( + hass, mqtt_mock, caplog, device_reg, entity_reg, setup_tasmota +): + """Test detecting devices with same topic.""" + configs = [ + copy.deepcopy(DEFAULT_CONFIG), + copy.deepcopy(DEFAULT_CONFIG), + copy.deepcopy(DEFAULT_CONFIG), + ] + configs[0]["rl"][0] = 1 + configs[1]["rl"][0] = 1 + configs[2]["rl"][0] = 1 + configs[0]["mac"] = "000000000001" + configs[1]["mac"] = "000000000002" + configs[2]["mac"] = "000000000003" + + for config in configs[0:2]: + async_fire_mqtt_message( + hass, + f"{DEFAULT_PREFIX}/{config['mac']}/config", + json.dumps(config), + ) + await hass.async_block_till_done() + + # Verify device registry entries are created for both devices + for config in configs[0:2]: + device_entry = device_reg.async_get_device( + set(), {(dr.CONNECTION_NETWORK_MAC, config["mac"])} + ) + assert device_entry is not None + assert device_entry.configuration_url == f"http://{config['ip']}/" + assert device_entry.manufacturer == "Tasmota" + assert device_entry.model == config["md"] + assert device_entry.name == config["dn"] + assert device_entry.sw_version == config["sw"] + + # Verify entities are created only for the first device + device_entry = device_reg.async_get_device( + set(), {(dr.CONNECTION_NETWORK_MAC, configs[0]["mac"])} + ) + assert len(er.async_entries_for_device(entity_reg, device_entry.id, True)) == 1 + device_entry = device_reg.async_get_device( + set(), {(dr.CONNECTION_NETWORK_MAC, configs[1]["mac"])} + ) + assert len(er.async_entries_for_device(entity_reg, device_entry.id, True)) == 0 + + # Verify a repairs issue was created + issue_id = "topic_duplicated_tasmota_49A3BC/cmnd/" + issue_registry = ir.async_get(hass) + issue = issue_registry.async_get_issue("tasmota", issue_id) + assert issue.data["mac"] == " ".join(config["mac"] for config in configs[0:2]) + + # Discover a 3rd device with same topic + async_fire_mqtt_message( + hass, + f"{DEFAULT_PREFIX}/{configs[2]['mac']}/config", + json.dumps(configs[2]), + ) + await hass.async_block_till_done() + + # Verify device registry entries was created + device_entry = device_reg.async_get_device( + set(), {(dr.CONNECTION_NETWORK_MAC, configs[2]["mac"])} + ) + assert device_entry is not None + assert device_entry.configuration_url == f"http://{configs[2]['ip']}/" + assert device_entry.manufacturer == "Tasmota" + assert device_entry.model == configs[2]["md"] + assert device_entry.name == configs[2]["dn"] + assert device_entry.sw_version == configs[2]["sw"] + + # Verify no entities were created + device_entry = device_reg.async_get_device( + set(), {(dr.CONNECTION_NETWORK_MAC, configs[2]["mac"])} + ) + assert len(er.async_entries_for_device(entity_reg, device_entry.id, True)) == 0 + + # Verify the repairs issue has been updated + issue = issue_registry.async_get_issue("tasmota", issue_id) + assert issue.data["mac"] == " ".join(config["mac"] for config in configs[0:3]) + + # Rediscover 3rd device with fixed config + configs[2]["t"] = "unique_topic_2" + async_fire_mqtt_message( + hass, + f"{DEFAULT_PREFIX}/{configs[2]['mac']}/config", + json.dumps(configs[2]), + ) + await hass.async_block_till_done() + + # Verify entities are created also for the third device + device_entry = device_reg.async_get_device( + set(), {(dr.CONNECTION_NETWORK_MAC, configs[2]["mac"])} + ) + assert len(er.async_entries_for_device(entity_reg, device_entry.id, True)) == 1 + + # Verify the repairs issue has been updated + issue = issue_registry.async_get_issue("tasmota", issue_id) + assert issue.data["mac"] == " ".join(config["mac"] for config in configs[0:2]) + + # Rediscover 2nd device with fixed config + configs[1]["t"] = "unique_topic_1" + async_fire_mqtt_message( + hass, + f"{DEFAULT_PREFIX}/{configs[1]['mac']}/config", + json.dumps(configs[1]), + ) + await hass.async_block_till_done() + + # Verify entities are created also for the second device + device_entry = device_reg.async_get_device( + set(), {(dr.CONNECTION_NETWORK_MAC, configs[1]["mac"])} + ) + assert len(er.async_entries_for_device(entity_reg, device_entry.id, True)) == 1 + + # Verify the repairs issue has been removed + assert issue_registry.async_get_issue("tasmota", issue_id) is None + + +async def test_topic_no_prefix( + hass, mqtt_mock, caplog, device_reg, entity_reg, setup_tasmota +): + """Test detecting devices with same topic.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["rl"][0] = 1 + config["ft"] = "%topic%/blah/" + + async_fire_mqtt_message( + hass, + f"{DEFAULT_PREFIX}/{config['mac']}/config", + json.dumps(config), + ) + await hass.async_block_till_done() + + # Verify device registry entry is created + device_entry = device_reg.async_get_device( + set(), {(dr.CONNECTION_NETWORK_MAC, config["mac"])} + ) + assert device_entry is not None + assert device_entry.configuration_url == f"http://{config['ip']}/" + assert device_entry.manufacturer == "Tasmota" + assert device_entry.model == config["md"] + assert device_entry.name == config["dn"] + assert device_entry.sw_version == config["sw"] + + # Verify entities are not created + device_entry = device_reg.async_get_device( + set(), {(dr.CONNECTION_NETWORK_MAC, config["mac"])} + ) + assert len(er.async_entries_for_device(entity_reg, device_entry.id, True)) == 0 + + # Verify a repairs issue was created + issue_id = "topic_no_prefix_00000049A3BC" + issue_registry = ir.async_get(hass) + assert ("tasmota", issue_id) in issue_registry.issues + + # Rediscover device with fixed config + config["ft"] = "%topic%/%prefix%/" + async_fire_mqtt_message( + hass, + f"{DEFAULT_PREFIX}/{config['mac']}/config", + json.dumps(config), + ) + await hass.async_block_till_done() + + # Verify entities are created + device_entry = device_reg.async_get_device( + set(), {(dr.CONNECTION_NETWORK_MAC, config["mac"])} + ) + assert len(er.async_entries_for_device(entity_reg, device_entry.id, True)) == 1 + + # Verify the repairs issue has been removed + issue_registry = ir.async_get(hass) + assert ("tasmota", issue_id) not in issue_registry.issues From b03de1c92f2788dd5e8914cb17a5d5741c47b117 Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Sun, 18 Sep 2022 13:21:24 -0600 Subject: [PATCH 520/955] Address code review from litterrobot PR (#78699) Address code review --- tests/components/litterrobot/test_switch.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/tests/components/litterrobot/test_switch.py b/tests/components/litterrobot/test_switch.py index 8adc7cdc6eb..e7ca5747fb1 100644 --- a/tests/components/litterrobot/test_switch.py +++ b/tests/components/litterrobot/test_switch.py @@ -58,17 +58,14 @@ async def test_on_off_commands( data = {ATTR_ENTITY_ID: entity_id} count = 0 - for service in (SERVICE_TURN_ON, SERVICE_TURN_OFF): + services = ((SERVICE_TURN_ON, STATE_ON, "1"), (SERVICE_TURN_OFF, STATE_OFF, "0")) + for service, new_state, new_value in services: count += 1 - await hass.services.async_call( - PLATFORM_DOMAIN, - service, - data, - blocking=True, + await hass.services.async_call(PLATFORM_DOMAIN, service, data, blocking=True) + robot._update_data( # pylint:disable=protected-access + {updated_field: new_value}, partial=True ) - robot._update_data({updated_field: 1 if service == SERVICE_TURN_ON else 0}) assert getattr(robot, robot_command).call_count == count - state = hass.states.get(entity_id) - assert state - assert state.state == STATE_ON if service == SERVICE_TURN_ON else STATE_OFF + assert (state := hass.states.get(entity_id)) + assert state.state == new_state From d74f5c6ee07febb2827c1587d552a07852dab8dd Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 18 Sep 2022 13:24:21 -0600 Subject: [PATCH 521/955] Make Guardian Repairs strings more consistent (and instructive) (#78694) Make Guardian Repairs strings consistently helpful --- homeassistant/components/guardian/strings.json | 6 +++--- homeassistant/components/guardian/translations/en.json | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/guardian/strings.json b/homeassistant/components/guardian/strings.json index b173051a860..33ddcf637a4 100644 --- a/homeassistant/components/guardian/strings.json +++ b/homeassistant/components/guardian/strings.json @@ -20,11 +20,11 @@ }, "issues": { "deprecated_service": { - "title": "The {deprecated_service} service is being removed", + "title": "The {deprecated_service} service will be removed", "fix_flow": { "step": { "confirm": { - "title": "The {deprecated_service} service is being removed", + "title": "The {deprecated_service} service will be removed", "description": "Update any automations or scripts that use this service to instead use the `{alternate_service}` service with a target entity ID of `{alternate_target}`." } } @@ -36,7 +36,7 @@ "step": { "confirm": { "title": "The {old_entity_id} entity will be removed", - "description": "This entity has been replaced by `{replacement_entity_id}`." + "description": "Update any automations or scripts that use this entity to instead use `{replacement_entity_id}`." } } } diff --git a/homeassistant/components/guardian/translations/en.json b/homeassistant/components/guardian/translations/en.json index 99bf2e67b4e..ac87ae36506 100644 --- a/homeassistant/components/guardian/translations/en.json +++ b/homeassistant/components/guardian/translations/en.json @@ -24,17 +24,17 @@ "step": { "confirm": { "description": "Update any automations or scripts that use this service to instead use the `{alternate_service}` service with a target entity ID of `{alternate_target}`.", - "title": "The {deprecated_service} service is being removed" + "title": "The {deprecated_service} service will be removed" } } }, - "title": "The {deprecated_service} service is being removed" + "title": "The {deprecated_service} service will be removed" }, "replaced_old_entity": { "fix_flow": { "step": { "confirm": { - "description": "This entity has been replaced by `{replacement_entity_id}`.", + "description": "Update any automations or scripts that use this entity to instead use `{replacement_entity_id}`.", "title": "The {old_entity_id} entity will be removed" } } From 4fbf44cced8cdfe4eb08c0bb84740ef305343c87 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sun, 18 Sep 2022 15:25:37 -0400 Subject: [PATCH 522/955] Create repair issue if zwave-js-server is too old (#78670) * Create repair issue if zwave-js-server is too old * Switch is_fixable to false * review comments --- homeassistant/components/zwave_js/__init__.py | 15 +++++++ .../components/zwave_js/strings.json | 6 +++ tests/components/zwave_js/test_init.py | 45 ++++++++++++++++++- 3 files changed, 64 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 066bc5101ae..d676a40d32f 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -34,6 +34,11 @@ from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry, entity_registry from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.issue_registry import ( + IssueSeverity, + async_create_issue, + async_delete_issue, +) from homeassistant.helpers.typing import UNDEFINED, ConfigType from .addon import AddonError, AddonManager, AddonState, get_addon_manager @@ -133,10 +138,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except InvalidServerVersion as err: if use_addon: async_ensure_addon_updated(hass) + else: + async_create_issue( + hass, + DOMAIN, + "invalid_server_version", + is_fixable=False, + severity=IssueSeverity.ERROR, + translation_key="invalid_server_version", + ) raise ConfigEntryNotReady(f"Invalid server version: {err}") from err except (asyncio.TimeoutError, BaseZwaveJSServerError) as err: raise ConfigEntryNotReady(f"Failed to connect: {err}") from err else: + async_delete_issue(hass, DOMAIN, "invalid_server_version") LOGGER.info("Connected to Zwave JS Server") dev_reg = device_registry.async_get(hass) diff --git a/homeassistant/components/zwave_js/strings.json b/homeassistant/components/zwave_js/strings.json index 91a6eae6ab6..19587cf0c0f 100644 --- a/homeassistant/components/zwave_js/strings.json +++ b/homeassistant/components/zwave_js/strings.json @@ -144,5 +144,11 @@ "ping": "Ping device", "reset_meter": "Reset meters on {subtype}" } + }, + "issues": { + "invalid_server_version": { + "title": "Newer version of Z-Wave JS Server needed", + "description": "The version of Z-Wave JS Server you are currently running is too old for this version of Home Assistant. Please update the Z-Wave JS Server to the latest version to fix this issue." + } } } diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index 5929657f595..c69c5a09f89 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -1,6 +1,7 @@ """Test the Z-Wave JS init module.""" +import asyncio from copy import deepcopy -from unittest.mock import call, patch +from unittest.mock import AsyncMock, call, patch import pytest from zwave_js_server.client import Client @@ -9,7 +10,7 @@ from zwave_js_server.exceptions import BaseZwaveJSServerError, InvalidServerVers from zwave_js_server.model.node import Node from homeassistant.components.hassio.handler import HassioAPIError -from homeassistant.components.zwave_js.const import DOMAIN +from homeassistant.components.zwave_js import DOMAIN from homeassistant.components.zwave_js.helpers import get_device_id from homeassistant.config_entries import ConfigEntryDisabler, ConfigEntryState from homeassistant.const import STATE_UNAVAILABLE @@ -18,6 +19,7 @@ from homeassistant.helpers import ( area_registry as ar, device_registry as dr, entity_registry as er, + issue_registry as ir, ) from .common import AIR_TEMPERATURE_SENSOR, EATON_RF9640_ENTITY @@ -696,6 +698,45 @@ async def test_update_addon( assert update_addon.call_count == update_calls +async def test_issue_registry(hass, client, version_state): + """Test issue registry.""" + device = "/test" + network_key = "abc123" + + client.connect.side_effect = InvalidServerVersion("Invalid version") + + entry = MockConfigEntry( + domain=DOMAIN, + title="Z-Wave JS", + data={ + "url": "ws://host1:3001", + "use_addon": False, + "usb_path": device, + "network_key": network_key, + }, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state is ConfigEntryState.SETUP_RETRY + + issue_reg = ir.async_get(hass) + assert issue_reg.async_get_issue(DOMAIN, "invalid_server_version") + + async def connect(): + await asyncio.sleep(0) + client.connected = True + + client.connect = AsyncMock(side_effect=connect) + + await hass.config_entries.async_reload(entry.entry_id) + await hass.async_block_till_done() + assert entry.state is ConfigEntryState.LOADED + assert not issue_reg.async_get_issue(DOMAIN, "invalid_server_version") + + @pytest.mark.parametrize( "stop_addon_side_effect, entry_state", [ From fa7f04c34ba2927151af0a9b42c044677b1c5d1a Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Mon, 19 Sep 2022 05:27:09 +1000 Subject: [PATCH 523/955] Code Quality Improvements for Advantage Air (#77695) Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- .../components/advantage_air/__init__.py | 7 +-- .../components/advantage_air/binary_sensor.py | 33 ++++++------ .../components/advantage_air/climate.py | 45 ++++++++-------- .../components/advantage_air/config_flow.py | 9 +++- .../components/advantage_air/cover.py | 23 ++++---- .../components/advantage_air/entity.py | 20 +++---- .../components/advantage_air/light.py | 22 ++++---- .../components/advantage_air/select.py | 13 +++-- .../components/advantage_air/sensor.py | 54 ++++++++++--------- .../components/advantage_air/switch.py | 17 +++--- .../components/advantage_air/update.py | 8 +-- .../components/advantage_air/test_climate.py | 18 ++++--- 12 files changed, 148 insertions(+), 121 deletions(-) diff --git a/homeassistant/components/advantage_air/__init__.py b/homeassistant/components/advantage_air/__init__.py index 3e07a8fbcef..8620a228edf 100644 --- a/homeassistant/components/advantage_air/__init__.py +++ b/homeassistant/components/advantage_air/__init__.py @@ -7,6 +7,7 @@ from advantage_air import ApiError, advantage_air from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT, Platform from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -60,7 +61,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if await func(param): await coordinator.async_refresh() except ApiError as err: - _LOGGER.warning(err) + raise HomeAssistantError(err) from err return error_handle @@ -69,8 +70,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = { "coordinator": coordinator, - "async_change": error_handle_factory(api.aircon.async_set), - "async_set_light": error_handle_factory(api.lights.async_set), + "aircon": error_handle_factory(api.aircon.async_set), + "lights": error_handle_factory(api.lights.async_set), } await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/advantage_air/binary_sensor.py b/homeassistant/components/advantage_air/binary_sensor.py index c0934239fe7..f3e021855fa 100644 --- a/homeassistant/components/advantage_air/binary_sensor.py +++ b/homeassistant/components/advantage_air/binary_sensor.py @@ -1,6 +1,8 @@ """Binary Sensor platform for Advantage Air integration.""" from __future__ import annotations +from typing import Any + from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, @@ -26,15 +28,16 @@ async def async_setup_entry( instance = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id] entities: list[BinarySensorEntity] = [] - for ac_key, ac_device in instance["coordinator"].data["aircons"].items(): - entities.append(AdvantageAirFilter(instance, ac_key)) - for zone_key, zone in ac_device["zones"].items(): - # Only add motion sensor when motion is enabled - if zone["motionConfig"] >= 2: - entities.append(AdvantageAirZoneMotion(instance, ac_key, zone_key)) - # Only add MyZone if it is available - if zone["type"] != 0: - entities.append(AdvantageAirZoneMyZone(instance, ac_key, zone_key)) + if aircons := instance["coordinator"].data.get("aircons"): + for ac_key, ac_device in aircons.items(): + entities.append(AdvantageAirFilter(instance, ac_key)) + for zone_key, zone in ac_device["zones"].items(): + # Only add motion sensor when motion is enabled + if zone["motionConfig"] >= 2: + entities.append(AdvantageAirZoneMotion(instance, ac_key, zone_key)) + # Only add MyZone if it is available + if zone["type"] != 0: + entities.append(AdvantageAirZoneMyZone(instance, ac_key, zone_key)) async_add_entities(entities) @@ -45,13 +48,13 @@ class AdvantageAirFilter(AdvantageAirAcEntity, BinarySensorEntity): _attr_entity_category = EntityCategory.DIAGNOSTIC _attr_name = "Filter" - def __init__(self, instance, ac_key): + def __init__(self, instance: dict[str, Any], ac_key: str) -> None: """Initialize an Advantage Air Filter sensor.""" super().__init__(instance, ac_key) self._attr_unique_id += "-filter" @property - def is_on(self): + def is_on(self) -> bool: """Return if filter needs cleaning.""" return self._ac["filterCleanStatus"] @@ -61,14 +64,14 @@ class AdvantageAirZoneMotion(AdvantageAirZoneEntity, BinarySensorEntity): _attr_device_class = BinarySensorDeviceClass.MOTION - def __init__(self, instance, ac_key, zone_key): + def __init__(self, instance: dict[str, Any], ac_key: str, zone_key: str) -> None: """Initialize an Advantage Air Zone Motion sensor.""" super().__init__(instance, ac_key, zone_key) self._attr_name = f'{self._zone["name"]} motion' self._attr_unique_id += "-motion" @property - def is_on(self): + def is_on(self) -> bool: """Return if motion is detect.""" return self._zone["motion"] == 20 @@ -79,13 +82,13 @@ class AdvantageAirZoneMyZone(AdvantageAirZoneEntity, BinarySensorEntity): _attr_entity_registry_enabled_default = False _attr_entity_category = EntityCategory.DIAGNOSTIC - def __init__(self, instance, ac_key, zone_key): + def __init__(self, instance: dict[str, Any], ac_key: str, zone_key: str) -> None: """Initialize an Advantage Air Zone MyZone sensor.""" super().__init__(instance, ac_key, zone_key) self._attr_name = f'{self._zone["name"]} myZone' self._attr_unique_id += "-myzone" @property - def is_on(self): + def is_on(self) -> bool: """Return if this zone is the myZone.""" return self._zone["number"] == self._ac["myZone"] diff --git a/homeassistant/components/advantage_air/climate.py b/homeassistant/components/advantage_air/climate.py index 1925b519a38..fdba46cde76 100644 --- a/homeassistant/components/advantage_air/climate.py +++ b/homeassistant/components/advantage_air/climate.py @@ -70,12 +70,13 @@ async def async_setup_entry( instance = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id] entities: list[ClimateEntity] = [] - for ac_key, ac_device in instance["coordinator"].data["aircons"].items(): - entities.append(AdvantageAirAC(instance, ac_key)) - for zone_key, zone in ac_device["zones"].items(): - # Only add zone climate control when zone is in temperature control - if zone["type"] != 0: - entities.append(AdvantageAirZone(instance, ac_key, zone_key)) + if aircons := instance["coordinator"].data.get("aircons"): + for ac_key, ac_device in aircons.items(): + entities.append(AdvantageAirAC(instance, ac_key)) + for zone_key, zone in ac_device["zones"].items(): + # Only add zone climate control when zone is in temperature control + if zone["type"] != 0: + entities.append(AdvantageAirZone(instance, ac_key, zone_key)) async_add_entities(entities) @@ -92,37 +93,37 @@ class AdvantageAirAC(AdvantageAirAcEntity, ClimateEntity): ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE ) - def __init__(self, instance, ac_key): + def __init__(self, instance: dict[str, Any], ac_key: str) -> None: """Initialize an AdvantageAir AC unit.""" super().__init__(instance, ac_key) if self._ac.get("myAutoModeEnabled"): self._attr_hvac_modes = AC_HVAC_MODES + [HVACMode.AUTO] @property - def target_temperature(self): + def target_temperature(self) -> float: """Return the current target temperature.""" return self._ac["setTemp"] @property - def hvac_mode(self): + def hvac_mode(self) -> HVACMode | None: """Return the current HVAC modes.""" if self._ac["state"] == ADVANTAGE_AIR_STATE_ON: return ADVANTAGE_AIR_HVAC_MODES.get(self._ac["mode"]) return HVACMode.OFF @property - def fan_mode(self): + def fan_mode(self) -> str | None: """Return the current fan modes.""" return ADVANTAGE_AIR_FAN_MODES.get(self._ac["fan"]) async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set the HVAC Mode and State.""" if hvac_mode == HVACMode.OFF: - await self.async_change( + await self.aircon( {self.ac_key: {"info": {"state": ADVANTAGE_AIR_STATE_OFF}}} ) else: - await self.async_change( + await self.aircon( { self.ac_key: { "info": { @@ -135,14 +136,14 @@ class AdvantageAirAC(AdvantageAirAcEntity, ClimateEntity): async def async_set_fan_mode(self, fan_mode: str) -> None: """Set the Fan Mode.""" - await self.async_change( + await self.aircon( {self.ac_key: {"info": {"fan": HASS_FAN_MODES.get(fan_mode)}}} ) async def async_set_temperature(self, **kwargs: Any) -> None: """Set the Temperature.""" temp = kwargs.get(ATTR_TEMPERATURE) - await self.async_change({self.ac_key: {"info": {"setTemp": temp}}}) + await self.aircon({self.ac_key: {"info": {"setTemp": temp}}}) class AdvantageAirZone(AdvantageAirZoneEntity, ClimateEntity): @@ -155,7 +156,7 @@ class AdvantageAirZone(AdvantageAirZoneEntity, ClimateEntity): _attr_hvac_modes = ZONE_HVAC_MODES _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE - def __init__(self, instance, ac_key, zone_key): + def __init__(self, instance: dict[str, Any], ac_key: str, zone_key: str) -> None: """Initialize an AdvantageAir Zone control.""" super().__init__(instance, ac_key, zone_key) self._attr_name = self._zone["name"] @@ -164,26 +165,26 @@ class AdvantageAirZone(AdvantageAirZoneEntity, ClimateEntity): ) @property - def hvac_mode(self): + def hvac_mode(self) -> HVACMode: """Return the current state as HVAC mode.""" if self._zone["state"] == ADVANTAGE_AIR_STATE_OPEN: return HVACMode.HEAT_COOL return HVACMode.OFF @property - def current_temperature(self): + def current_temperature(self) -> float: """Return the current temperature.""" return self._zone["measuredTemp"] @property - def target_temperature(self): + def target_temperature(self) -> float: """Return the target temperature.""" return self._zone["setTemp"] async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set the HVAC Mode and State.""" if hvac_mode == HVACMode.OFF: - await self.async_change( + await self.aircon( { self.ac_key: { "zones": {self.zone_key: {"state": ADVANTAGE_AIR_STATE_CLOSE}} @@ -191,7 +192,7 @@ class AdvantageAirZone(AdvantageAirZoneEntity, ClimateEntity): } ) else: - await self.async_change( + await self.aircon( { self.ac_key: { "zones": {self.zone_key: {"state": ADVANTAGE_AIR_STATE_OPEN}} @@ -202,6 +203,4 @@ class AdvantageAirZone(AdvantageAirZoneEntity, ClimateEntity): async def async_set_temperature(self, **kwargs: Any) -> None: """Set the Temperature.""" temp = kwargs.get(ATTR_TEMPERATURE) - await self.async_change( - {self.ac_key: {"zones": {self.zone_key: {"setTemp": temp}}}} - ) + await self.aircon({self.ac_key: {"zones": {self.zone_key: {"setTemp": temp}}}}) diff --git a/homeassistant/components/advantage_air/config_flow.py b/homeassistant/components/advantage_air/config_flow.py index b13ab1e9b21..7b5acab55f0 100644 --- a/homeassistant/components/advantage_air/config_flow.py +++ b/homeassistant/components/advantage_air/config_flow.py @@ -1,9 +1,14 @@ """Config Flow for Advantage Air integration.""" +from __future__ import annotations + +from typing import Any + from advantage_air import ApiError, advantage_air import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import ADVANTAGE_AIR_RETRY, DOMAIN @@ -25,7 +30,9 @@ class AdvantageAirConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): DOMAIN = DOMAIN - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Get configuration from the user.""" errors = {} if user_input: diff --git a/homeassistant/components/advantage_air/cover.py b/homeassistant/components/advantage_air/cover.py index 4b3f371f52e..8d05f7e2e63 100644 --- a/homeassistant/components/advantage_air/cover.py +++ b/homeassistant/components/advantage_air/cover.py @@ -30,12 +30,13 @@ async def async_setup_entry( instance = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id] - entities = [] - for ac_key, ac_device in instance["coordinator"].data["aircons"].items(): - for zone_key, zone in ac_device["zones"].items(): - # Only add zone vent controls when zone in vent control mode. - if zone["type"] == 0: - entities.append(AdvantageAirZoneVent(instance, ac_key, zone_key)) + entities: list[CoverEntity] = [] + if aircons := instance["coordinator"].data.get("aircons"): + for ac_key, ac_device in aircons.items(): + for zone_key, zone in ac_device["zones"].items(): + # Only add zone vent controls when zone in vent control mode. + if zone["type"] == 0: + entities.append(AdvantageAirZoneVent(instance, ac_key, zone_key)) async_add_entities(entities) @@ -49,7 +50,7 @@ class AdvantageAirZoneVent(AdvantageAirZoneEntity, CoverEntity): | CoverEntityFeature.SET_POSITION ) - def __init__(self, instance, ac_key, zone_key): + def __init__(self, instance: dict[str, Any], ac_key: str, zone_key: str) -> None: """Initialize an Advantage Air Zone Vent.""" super().__init__(instance, ac_key, zone_key) self._attr_name = self._zone["name"] @@ -68,7 +69,7 @@ class AdvantageAirZoneVent(AdvantageAirZoneEntity, CoverEntity): async def async_open_cover(self, **kwargs: Any) -> None: """Fully open zone vent.""" - await self.async_change( + await self.aircon( { self.ac_key: { "zones": { @@ -80,7 +81,7 @@ class AdvantageAirZoneVent(AdvantageAirZoneEntity, CoverEntity): async def async_close_cover(self, **kwargs: Any) -> None: """Fully close zone vent.""" - await self.async_change( + await self.aircon( { self.ac_key: { "zones": {self.zone_key: {"state": ADVANTAGE_AIR_STATE_CLOSE}} @@ -92,7 +93,7 @@ class AdvantageAirZoneVent(AdvantageAirZoneEntity, CoverEntity): """Change vent position.""" position = round(kwargs[ATTR_POSITION] / 5) * 5 if position == 0: - await self.async_change( + await self.aircon( { self.ac_key: { "zones": {self.zone_key: {"state": ADVANTAGE_AIR_STATE_CLOSE}} @@ -100,7 +101,7 @@ class AdvantageAirZoneVent(AdvantageAirZoneEntity, CoverEntity): } ) else: - await self.async_change( + await self.aircon( { self.ac_key: { "zones": { diff --git a/homeassistant/components/advantage_air/entity.py b/homeassistant/components/advantage_air/entity.py index 375bfa255c4..aaaa4ff5813 100644 --- a/homeassistant/components/advantage_air/entity.py +++ b/homeassistant/components/advantage_air/entity.py @@ -1,5 +1,7 @@ """Advantage Air parent entity class.""" +from typing import Any + from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -11,20 +13,20 @@ class AdvantageAirEntity(CoordinatorEntity): _attr_has_entity_name = True - def __init__(self, instance): + def __init__(self, instance: dict[str, Any]) -> None: """Initialize common aspects of an Advantage Air entity.""" super().__init__(instance["coordinator"]) - self._attr_unique_id = self.coordinator.data["system"]["rid"] + self._attr_unique_id: str = self.coordinator.data["system"]["rid"] class AdvantageAirAcEntity(AdvantageAirEntity): """Parent class for Advantage Air AC Entities.""" - def __init__(self, instance, ac_key): + def __init__(self, instance: dict[str, Any], ac_key: str) -> None: """Initialize common aspects of an Advantage Air ac entity.""" super().__init__(instance) - self.async_change = instance["async_change"] - self.ac_key = ac_key + self.aircon = instance["aircon"] + self.ac_key: str = ac_key self._attr_unique_id += f"-{ac_key}" self._attr_device_info = DeviceInfo( @@ -36,19 +38,19 @@ class AdvantageAirAcEntity(AdvantageAirEntity): ) @property - def _ac(self): + def _ac(self) -> dict[str, Any]: return self.coordinator.data["aircons"][self.ac_key]["info"] class AdvantageAirZoneEntity(AdvantageAirAcEntity): """Parent class for Advantage Air Zone Entities.""" - def __init__(self, instance, ac_key, zone_key): + def __init__(self, instance: dict[str, Any], ac_key: str, zone_key: str) -> None: """Initialize common aspects of an Advantage Air zone entity.""" super().__init__(instance, ac_key) - self.zone_key = zone_key + self.zone_key: str = zone_key self._attr_unique_id += f"-{zone_key}" @property - def _zone(self): + def _zone(self) -> dict[str, Any]: return self.coordinator.data["aircons"][self.ac_key]["zones"][self.zone_key] diff --git a/homeassistant/components/advantage_air/light.py b/homeassistant/components/advantage_air/light.py index b1c8495edf8..f0ae669acde 100644 --- a/homeassistant/components/advantage_air/light.py +++ b/homeassistant/components/advantage_air/light.py @@ -24,9 +24,9 @@ async def async_setup_entry( instance = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id] - entities = [] - if "myLights" in instance["coordinator"].data: - for light in instance["coordinator"].data["myLights"]["lights"].values(): + entities: list[LightEntity] = [] + if my_lights := instance["coordinator"].data.get("myLights"): + for light in my_lights["lights"].values(): if light.get("relay"): entities.append(AdvantageAirLight(instance, light)) else: @@ -39,11 +39,11 @@ class AdvantageAirLight(AdvantageAirEntity, LightEntity): _attr_supported_color_modes = {ColorMode.ONOFF} - def __init__(self, instance, light): + def __init__(self, instance: dict[str, Any], light: dict[str, Any]) -> None: """Initialize an Advantage Air Light.""" super().__init__(instance) - self.async_set_light = instance["async_set_light"] - self._id = light["id"] + self.lights = instance["lights"] + self._id: str = light["id"] self._attr_unique_id += f"-{self._id}" self._attr_device_info = DeviceInfo( identifiers={(ADVANTAGE_AIR_DOMAIN, self._attr_unique_id)}, @@ -54,7 +54,7 @@ class AdvantageAirLight(AdvantageAirEntity, LightEntity): ) @property - def _light(self): + def _light(self) -> dict[str, Any]: """Return the light object.""" return self.coordinator.data["myLights"]["lights"][self._id] @@ -65,11 +65,11 @@ class AdvantageAirLight(AdvantageAirEntity, LightEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" - await self.async_set_light({"id": self._id, "state": ADVANTAGE_AIR_STATE_ON}) + await self.lights({"id": self._id, "state": ADVANTAGE_AIR_STATE_ON}) async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" - await self.async_set_light({"id": self._id, "state": ADVANTAGE_AIR_STATE_OFF}) + await self.lights({"id": self._id, "state": ADVANTAGE_AIR_STATE_OFF}) class AdvantageAirLightDimmable(AdvantageAirLight): @@ -84,7 +84,7 @@ class AdvantageAirLightDimmable(AdvantageAirLight): async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on and optionally set the brightness.""" - data = {"id": self._id, "state": ADVANTAGE_AIR_STATE_ON} + data: dict[str, Any] = {"id": self._id, "state": ADVANTAGE_AIR_STATE_ON} if ATTR_BRIGHTNESS in kwargs: data["value"] = round(kwargs[ATTR_BRIGHTNESS] * 100 / 255) - await self.async_set_light(data) + await self.lights(data) diff --git a/homeassistant/components/advantage_air/select.py b/homeassistant/components/advantage_air/select.py index f6c46cb7f87..742ce810011 100644 --- a/homeassistant/components/advantage_air/select.py +++ b/homeassistant/components/advantage_air/select.py @@ -1,4 +1,6 @@ """Select platform for Advantage Air integration.""" +from typing import Any + from homeassistant.components.select import SelectEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -19,9 +21,10 @@ async def async_setup_entry( instance = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id] - entities = [] - for ac_key in instance["coordinator"].data["aircons"]: - entities.append(AdvantageAirMyZone(instance, ac_key)) + entities: list[SelectEntity] = [] + if aircons := instance["coordinator"].data.get("aircons"): + for ac_key in aircons: + entities.append(AdvantageAirMyZone(instance, ac_key)) async_add_entities(entities) @@ -31,7 +34,7 @@ class AdvantageAirMyZone(AdvantageAirAcEntity, SelectEntity): _attr_icon = "mdi:home-thermometer" _attr_name = "MyZone" - def __init__(self, instance, ac_key): + def __init__(self, instance: dict[str, Any], ac_key: str) -> None: """Initialize an Advantage Air MyZone control.""" super().__init__(instance, ac_key) self._attr_unique_id += "-myzone" @@ -52,6 +55,6 @@ class AdvantageAirMyZone(AdvantageAirAcEntity, SelectEntity): async def async_select_option(self, option: str) -> None: """Set the MyZone.""" - await self.async_change( + await self.aircon( {self.ac_key: {"info": {"myZone": self._name_to_number[option]}}} ) diff --git a/homeassistant/components/advantage_air/sensor.py b/homeassistant/components/advantage_air/sensor.py index b110294b2fd..60e640d36e9 100644 --- a/homeassistant/components/advantage_air/sensor.py +++ b/homeassistant/components/advantage_air/sensor.py @@ -1,6 +1,9 @@ """Sensor platform for Advantage Air integration.""" from __future__ import annotations +from decimal import Decimal +from typing import Any + import voluptuous as vol from homeassistant.components.sensor import ( @@ -35,17 +38,18 @@ async def async_setup_entry( instance = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id] entities: list[SensorEntity] = [] - for ac_key, ac_device in instance["coordinator"].data["aircons"].items(): - entities.append(AdvantageAirTimeTo(instance, ac_key, "On")) - entities.append(AdvantageAirTimeTo(instance, ac_key, "Off")) - for zone_key, zone in ac_device["zones"].items(): - # Only show damper and temp sensors when zone is in temperature control - if zone["type"] != 0: - entities.append(AdvantageAirZoneVent(instance, ac_key, zone_key)) - entities.append(AdvantageAirZoneTemp(instance, ac_key, zone_key)) - # Only show wireless signal strength sensors when using wireless sensors - if zone["rssi"] > 0: - entities.append(AdvantageAirZoneSignal(instance, ac_key, zone_key)) + if aircons := instance["coordinator"].data.get("aircons"): + for ac_key, ac_device in aircons.items(): + entities.append(AdvantageAirTimeTo(instance, ac_key, "On")) + entities.append(AdvantageAirTimeTo(instance, ac_key, "Off")) + for zone_key, zone in ac_device["zones"].items(): + # Only show damper and temp sensors when zone is in temperature control + if zone["type"] != 0: + entities.append(AdvantageAirZoneVent(instance, ac_key, zone_key)) + entities.append(AdvantageAirZoneTemp(instance, ac_key, zone_key)) + # Only show wireless signal strength sensors when using wireless sensors + if zone["rssi"] > 0: + entities.append(AdvantageAirZoneSignal(instance, ac_key, zone_key)) async_add_entities(entities) platform = entity_platform.async_get_current_platform() @@ -62,7 +66,7 @@ class AdvantageAirTimeTo(AdvantageAirAcEntity, SensorEntity): _attr_native_unit_of_measurement = ADVANTAGE_AIR_SET_COUNTDOWN_UNIT _attr_entity_category = EntityCategory.DIAGNOSTIC - def __init__(self, instance, ac_key, action): + def __init__(self, instance: dict[str, Any], ac_key: str, action: str) -> None: """Initialize the Advantage Air timer control.""" super().__init__(instance, ac_key) self.action = action @@ -71,21 +75,21 @@ class AdvantageAirTimeTo(AdvantageAirAcEntity, SensorEntity): self._attr_unique_id += f"-timeto{action}" @property - def native_value(self): + def native_value(self) -> Decimal: """Return the current value.""" return self._ac[self._time_key] @property - def icon(self): + def icon(self) -> str: """Return a representative icon of the timer.""" if self._ac[self._time_key] > 0: return "mdi:timer-outline" return "mdi:timer-off-outline" - async def set_time_to(self, **kwargs): + async def set_time_to(self, **kwargs: Any) -> None: """Set the timer value.""" value = min(720, max(0, int(kwargs[ADVANTAGE_AIR_SET_COUNTDOWN_VALUE]))) - await self.async_change({self.ac_key: {"info": {self._time_key: value}}}) + await self.aircon({self.ac_key: {"info": {self._time_key: value}}}) class AdvantageAirZoneVent(AdvantageAirZoneEntity, SensorEntity): @@ -95,21 +99,21 @@ class AdvantageAirZoneVent(AdvantageAirZoneEntity, SensorEntity): _attr_state_class = SensorStateClass.MEASUREMENT _attr_entity_category = EntityCategory.DIAGNOSTIC - def __init__(self, instance, ac_key, zone_key): + def __init__(self, instance: dict[str, Any], ac_key: str, zone_key: str) -> None: """Initialize an Advantage Air Zone Vent Sensor.""" super().__init__(instance, ac_key, zone_key=zone_key) self._attr_name = f'{self._zone["name"]} vent' self._attr_unique_id += "-vent" @property - def native_value(self): + def native_value(self) -> Decimal: """Return the current value of the air vent.""" if self._zone["state"] == ADVANTAGE_AIR_STATE_OPEN: return self._zone["value"] - return 0 + return Decimal(0) @property - def icon(self): + def icon(self) -> str: """Return a representative icon.""" if self._zone["state"] == ADVANTAGE_AIR_STATE_OPEN: return "mdi:fan" @@ -123,19 +127,19 @@ class AdvantageAirZoneSignal(AdvantageAirZoneEntity, SensorEntity): _attr_state_class = SensorStateClass.MEASUREMENT _attr_entity_category = EntityCategory.DIAGNOSTIC - def __init__(self, instance, ac_key, zone_key): + def __init__(self, instance: dict[str, Any], ac_key: str, zone_key: str) -> None: """Initialize an Advantage Air Zone wireless signal sensor.""" super().__init__(instance, ac_key, zone_key) self._attr_name = f'{self._zone["name"]} signal' self._attr_unique_id += "-signal" @property - def native_value(self): + def native_value(self) -> Decimal: """Return the current value of the wireless signal.""" return self._zone["rssi"] @property - def icon(self): + def icon(self) -> str: """Return a representative icon.""" if self._zone["rssi"] >= 80: return "mdi:wifi-strength-4" @@ -157,13 +161,13 @@ class AdvantageAirZoneTemp(AdvantageAirZoneEntity, SensorEntity): _attr_entity_registry_enabled_default = False _attr_entity_category = EntityCategory.DIAGNOSTIC - def __init__(self, instance, ac_key, zone_key): + def __init__(self, instance: dict[str, Any], ac_key: str, zone_key: str) -> None: """Initialize an Advantage Air Zone Temp Sensor.""" super().__init__(instance, ac_key, zone_key) self._attr_name = f'{self._zone["name"]} temperature' self._attr_unique_id += "-temp" @property - def native_value(self): + def native_value(self) -> Decimal: """Return the current value of the measured temperature.""" return self._zone["measuredTemp"] diff --git a/homeassistant/components/advantage_air/switch.py b/homeassistant/components/advantage_air/switch.py index 52992ae9531..e3504ab7624 100644 --- a/homeassistant/components/advantage_air/switch.py +++ b/homeassistant/components/advantage_air/switch.py @@ -23,10 +23,11 @@ async def async_setup_entry( instance = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id] - entities = [] - for ac_key, ac_device in instance["coordinator"].data["aircons"].items(): - if ac_device["info"]["freshAirStatus"] != "none": - entities.append(AdvantageAirFreshAir(instance, ac_key)) + entities: list[SwitchEntity] = [] + if aircons := instance["coordinator"].data.get("aircons"): + for ac_key, ac_device in aircons.items(): + if ac_device["info"]["freshAirStatus"] != "none": + entities.append(AdvantageAirFreshAir(instance, ac_key)) async_add_entities(entities) @@ -36,24 +37,24 @@ class AdvantageAirFreshAir(AdvantageAirAcEntity, SwitchEntity): _attr_icon = "mdi:air-filter" _attr_name = "Fresh air" - def __init__(self, instance, ac_key): + def __init__(self, instance: dict[str, Any], ac_key: str) -> None: """Initialize an Advantage Air fresh air control.""" super().__init__(instance, ac_key) self._attr_unique_id += "-freshair" @property - def is_on(self): + def is_on(self) -> bool: """Return the fresh air status.""" return self._ac["freshAirStatus"] == ADVANTAGE_AIR_STATE_ON async def async_turn_on(self, **kwargs: Any) -> None: """Turn fresh air on.""" - await self.async_change( + await self.aircon( {self.ac_key: {"info": {"freshAirStatus": ADVANTAGE_AIR_STATE_ON}}} ) async def async_turn_off(self, **kwargs: Any) -> None: """Turn fresh air off.""" - await self.async_change( + await self.aircon( {self.ac_key: {"info": {"freshAirStatus": ADVANTAGE_AIR_STATE_OFF}}} ) diff --git a/homeassistant/components/advantage_air/update.py b/homeassistant/components/advantage_air/update.py index 9294ecab238..404fcad7447 100644 --- a/homeassistant/components/advantage_air/update.py +++ b/homeassistant/components/advantage_air/update.py @@ -1,4 +1,6 @@ """Advantage Air Update platform.""" +from typing import Any + from homeassistant.components.update import UpdateEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -26,7 +28,7 @@ class AdvantageAirApp(AdvantageAirEntity, UpdateEntity): _attr_name = "App" - def __init__(self, instance): + def __init__(self, instance: dict[str, Any]) -> None: """Initialize the Advantage Air App.""" super().__init__(instance) self._attr_device_info = DeviceInfo( @@ -40,12 +42,12 @@ class AdvantageAirApp(AdvantageAirEntity, UpdateEntity): ) @property - def installed_version(self): + def installed_version(self) -> str: """Return the current app version.""" return self.coordinator.data["system"]["myAppRev"] @property - def latest_version(self): + def latest_version(self) -> str: """Return if there is an update.""" if self.coordinator.data["system"]["needsUpdate"]: return "Needs Update" diff --git a/tests/components/advantage_air/test_climate.py b/tests/components/advantage_air/test_climate.py index f1b118ab3b3..39999ba4ed9 100644 --- a/tests/components/advantage_air/test_climate.py +++ b/tests/components/advantage_air/test_climate.py @@ -1,6 +1,8 @@ """Test the Advantage Air Climate Platform.""" from json import loads +import pytest + from homeassistant.components.advantage_air.climate import ( HASS_FAN_MODES, HASS_HVAC_MODES, @@ -20,9 +22,10 @@ from homeassistant.components.climate.const import ( HVACMode, ) from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er -from tests.components.advantage_air import ( +from . import ( TEST_SET_RESPONSE, TEST_SET_URL, TEST_SYSTEM_DATA, @@ -184,12 +187,13 @@ async def test_climate_async_failed_update(hass, aioclient_mock): assert len(aioclient_mock.mock_calls) == 1 - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_TEMPERATURE, - {ATTR_ENTITY_ID: ["climate.ac_one"], ATTR_TEMPERATURE: 25}, - blocking=True, - ) + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: ["climate.ac_one"], ATTR_TEMPERATURE: 25}, + blocking=True, + ) assert len(aioclient_mock.mock_calls) == 2 assert aioclient_mock.mock_calls[-1][0] == "GET" assert aioclient_mock.mock_calls[-1][1].path == "/setAircon" From 8fd18cda303a48f7489482d4d6168079f030639d Mon Sep 17 00:00:00 2001 From: Arto Jantunen Date: Sun, 18 Sep 2022 22:35:19 +0300 Subject: [PATCH 524/955] Fix Vallox extract and supply fan speed measurement units (#77692) --- homeassistant/components/vallox/sensor.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/vallox/sensor.py b/homeassistant/components/vallox/sensor.py index d3357e50ad2..2e00452fdf2 100644 --- a/homeassistant/components/vallox/sensor.py +++ b/homeassistant/components/vallox/sensor.py @@ -13,7 +13,6 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, - FREQUENCY_HERTZ, PERCENTAGE, TEMP_CELSIUS, ) @@ -157,7 +156,7 @@ SENSOR_ENTITIES: tuple[ValloxSensorEntityDescription, ...] = ( metric_key="A_CYC_EXTR_FAN_SPEED", icon="mdi:fan", state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=FREQUENCY_HERTZ, + native_unit_of_measurement="RPM", entity_type=ValloxFanSpeedSensor, entity_registry_enabled_default=False, ), @@ -167,7 +166,7 @@ SENSOR_ENTITIES: tuple[ValloxSensorEntityDescription, ...] = ( metric_key="A_CYC_SUPP_FAN_SPEED", icon="mdi:fan", state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=FREQUENCY_HERTZ, + native_unit_of_measurement="RPM", entity_type=ValloxFanSpeedSensor, entity_registry_enabled_default=False, ), From 3655958d2685b4d39d20534e6981d98cddf6931f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Sun, 18 Sep 2022 21:38:27 +0200 Subject: [PATCH 525/955] Seperate timeout errors in rest requests (#78710) --- homeassistant/components/rest/data.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/rest/data.py b/homeassistant/components/rest/data.py index 7c8fd61e688..86219634027 100644 --- a/homeassistant/components/rest/data.py +++ b/homeassistant/components/rest/data.py @@ -69,6 +69,12 @@ class RestData: ) self.data = response.text self.headers = response.headers + except httpx.TimeoutException as ex: + if log_errors: + _LOGGER.error("Timeout while fetching data: %s", self._resource) + self.last_exception = ex + self.data = None + self.headers = None except httpx.RequestError as ex: if log_errors: _LOGGER.error( From 3c6c673a2074c6093219ae9ff442610db062f578 Mon Sep 17 00:00:00 2001 From: Orad SA Date: Sun, 18 Sep 2022 23:34:53 +0300 Subject: [PATCH 526/955] Add state_class to Waze travel time for statistics support (#77386) --- homeassistant/components/waze_travel_time/sensor.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/waze_travel_time/sensor.py b/homeassistant/components/waze_travel_time/sensor.py index 4992fc07db5..153ada11349 100644 --- a/homeassistant/components/waze_travel_time/sensor.py +++ b/homeassistant/components/waze_travel_time/sensor.py @@ -6,7 +6,11 @@ import logging from WazeRouteCalculator import WazeRouteCalculator, WRCError -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorStateClass, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ATTRIBUTION, @@ -107,6 +111,8 @@ class WazeTravelTime(SensorEntity): """Representation of a Waze travel time sensor.""" _attr_native_unit_of_measurement = TIME_MINUTES + _attr_device_class = SensorDeviceClass.DURATION + _attr_state_class = SensorStateClass.MEASUREMENT _attr_device_info = DeviceInfo( entry_type=DeviceEntryType.SERVICE, name="Waze", From 721fddc0167e55425925d1364e80793aad8afb9e Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 18 Sep 2022 23:28:17 +0200 Subject: [PATCH 527/955] Bump `brother` backend library (#78072) * Update integration for a new library * Update tests * Add unique_id migration * Update integration and tests for lib 2.0.0 * Improve test coverage * Improve typing in tests --- homeassistant/components/brother/__init__.py | 24 +++---- .../components/brother/config_flow.py | 8 ++- .../components/brother/diagnostics.py | 4 +- .../components/brother/manifest.json | 2 +- homeassistant/components/brother/sensor.py | 27 +++++++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/brother/__init__.py | 7 +- .../brother/fixtures/diagnostics_data.json | 23 ++++++- tests/components/brother/test_config_flow.py | 65 +++++++++++-------- tests/components/brother/test_diagnostics.py | 12 +++- tests/components/brother/test_init.py | 30 +++++++-- tests/components/brother/test_sensor.py | 38 +++++++++-- 13 files changed, 182 insertions(+), 62 deletions(-) diff --git a/homeassistant/components/brother/__init__.py b/homeassistant/components/brother/__init__.py index 7c3d3003ea6..c1dbfd5bf0a 100644 --- a/homeassistant/components/brother/__init__.py +++ b/homeassistant/components/brother/__init__.py @@ -5,12 +5,12 @@ from datetime import timedelta import logging import async_timeout -from brother import Brother, DictToObj, SnmpError, UnsupportedModel -import pysnmp.hlapi.asyncio as SnmpEngine +from brother import Brother, BrotherSensors, SnmpError, UnsupportedModel from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_TYPE, Platform from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DATA_CONFIG_ENTRY, DOMAIN, SNMP @@ -26,13 +26,17 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Brother from a config entry.""" host = entry.data[CONF_HOST] - kind = entry.data[CONF_TYPE] + printer_type = entry.data[CONF_TYPE] snmp_engine = get_snmp_engine(hass) + try: + brother = await Brother.create( + host, printer_type=printer_type, snmp_engine=snmp_engine + ) + except (ConnectionError, SnmpError) as error: + raise ConfigEntryNotReady from error - coordinator = BrotherDataUpdateCoordinator( - hass, host=host, kind=kind, snmp_engine=snmp_engine - ) + coordinator = BrotherDataUpdateCoordinator(hass, brother) await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {}) @@ -61,11 +65,9 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class BrotherDataUpdateCoordinator(DataUpdateCoordinator): """Class to manage fetching Brother data from the printer.""" - def __init__( - self, hass: HomeAssistant, host: str, kind: str, snmp_engine: SnmpEngine - ) -> None: + def __init__(self, hass: HomeAssistant, brother: Brother) -> None: """Initialize.""" - self.brother = Brother(host, kind=kind, snmp_engine=snmp_engine) + self.brother = brother super().__init__( hass, @@ -74,7 +76,7 @@ class BrotherDataUpdateCoordinator(DataUpdateCoordinator): update_interval=SCAN_INTERVAL, ) - async def _async_update_data(self) -> DictToObj: + async def _async_update_data(self) -> BrotherSensors: """Update data via library.""" try: async with async_timeout.timeout(20): diff --git a/homeassistant/components/brother/config_flow.py b/homeassistant/components/brother/config_flow.py index 27f3c73cd63..48c73d0c4d1 100644 --- a/homeassistant/components/brother/config_flow.py +++ b/homeassistant/components/brother/config_flow.py @@ -46,7 +46,9 @@ class BrotherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): snmp_engine = get_snmp_engine(self.hass) - brother = Brother(user_input[CONF_HOST], snmp_engine=snmp_engine) + brother = await Brother.create( + user_input[CONF_HOST], snmp_engine=snmp_engine + ) await brother.async_update() await self.async_set_unique_id(brother.serial.lower()) @@ -80,7 +82,9 @@ class BrotherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): model = discovery_info.properties.get("product") try: - self.brother = Brother(self.host, snmp_engine=snmp_engine, model=model) + self.brother = await Brother.create( + self.host, snmp_engine=snmp_engine, model=model + ) await self.brother.async_update() except UnsupportedModel: return self.async_abort(reason="unsupported_model") diff --git a/homeassistant/components/brother/diagnostics.py b/homeassistant/components/brother/diagnostics.py index 9ff515004cb..239d4916d6b 100644 --- a/homeassistant/components/brother/diagnostics.py +++ b/homeassistant/components/brother/diagnostics.py @@ -1,6 +1,8 @@ """Diagnostics support for Brother.""" from __future__ import annotations +from dataclasses import asdict + from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -18,7 +20,7 @@ async def async_get_config_entry_diagnostics( diagnostics_data = { "info": dict(config_entry.data), - "data": coordinator.data, + "data": asdict(coordinator.data), } return diagnostics_data diff --git a/homeassistant/components/brother/manifest.json b/homeassistant/components/brother/manifest.json index e14079f6dd9..61b1d8bcdc9 100644 --- a/homeassistant/components/brother/manifest.json +++ b/homeassistant/components/brother/manifest.json @@ -3,7 +3,7 @@ "name": "Brother Printer", "documentation": "https://www.home-assistant.io/integrations/brother", "codeowners": ["@bieniu"], - "requirements": ["brother==1.2.3"], + "requirements": ["brother==2.0.0"], "zeroconf": [ { "type": "_printer._tcp.local.", diff --git a/homeassistant/components/brother/sensor.py b/homeassistant/components/brother/sensor.py index b6af96087af..86f1d2d40ec 100644 --- a/homeassistant/components/brother/sensor.py +++ b/homeassistant/components/brother/sensor.py @@ -3,9 +3,11 @@ from __future__ import annotations from dataclasses import dataclass from datetime import datetime +import logging from typing import Any, cast from homeassistant.components.sensor import ( + DOMAIN as PLATFORM, SensorDeviceClass, SensorEntity, SensorEntityDescription, @@ -14,6 +16,7 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, PERCENTAGE from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType @@ -28,7 +31,7 @@ ATTR_BLACK_DRUM_REMAINING_LIFE = "black_drum_remaining_life" ATTR_BLACK_DRUM_REMAINING_PAGES = "black_drum_remaining_pages" ATTR_BLACK_INK_REMAINING = "black_ink_remaining" ATTR_BLACK_TONER_REMAINING = "black_toner_remaining" -ATTR_BW_COUNTER = "b/w_counter" +ATTR_BW_COUNTER = "bw_counter" ATTR_COLOR_COUNTER = "color_counter" ATTR_COUNTER = "counter" ATTR_CYAN_DRUM_COUNTER = "cyan_drum_counter" @@ -82,6 +85,8 @@ ATTRS_MAP: dict[str, tuple[str, str]] = { ), } +_LOGGER = logging.getLogger(__name__) + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback @@ -89,6 +94,22 @@ async def async_setup_entry( """Add Brother entities from a config_entry.""" coordinator = hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id] + # Due to the change of the attribute name of one sensor, it is necessary to migrate + # the unique_id to the new one. + entity_registry = er.async_get(hass) + old_unique_id = f"{coordinator.data.serial.lower()}_b/w_counter" + if entity_id := entity_registry.async_get_entity_id( + PLATFORM, DOMAIN, old_unique_id + ): + new_unique_id = f"{coordinator.data.serial.lower()}_bw_counter" + _LOGGER.debug( + "Migrating entity %s from old unique ID '%s' to new unique ID '%s'", + entity_id, + old_unique_id, + new_unique_id, + ) + entity_registry.async_update_entity(entity_id, new_unique_id=new_unique_id) + sensors = [] device_info = DeviceInfo( @@ -97,11 +118,11 @@ async def async_setup_entry( manufacturer=ATTR_MANUFACTURER, model=coordinator.data.model, name=coordinator.data.model, - sw_version=getattr(coordinator.data, "firmware", None), + sw_version=coordinator.data.firmware, ) for description in SENSOR_TYPES: - if description.key in coordinator.data: + if getattr(coordinator.data, description.key) is not None: sensors.append( description.entity_class(coordinator, description, device_info) ) diff --git a/requirements_all.txt b/requirements_all.txt index 8d0bd4ff995..32ac27e2fbb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -449,7 +449,7 @@ boto3==1.20.24 broadlink==0.18.2 # homeassistant.components.brother -brother==1.2.3 +brother==2.0.0 # homeassistant.components.brottsplatskartan brottsplatskartan==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bdba84bb811..8ad195aafc6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -356,7 +356,7 @@ boschshcpy==0.2.30 broadlink==0.18.2 # homeassistant.components.brother -brother==1.2.3 +brother==2.0.0 # homeassistant.components.brunt brunt==1.2.0 diff --git a/tests/components/brother/__init__.py b/tests/components/brother/__init__.py index 57cbe8b71de..8e24c2d8058 100644 --- a/tests/components/brother/__init__.py +++ b/tests/components/brother/__init__.py @@ -4,11 +4,14 @@ from unittest.mock import patch from homeassistant.components.brother.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_TYPE +from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry, load_fixture -async def init_integration(hass, skip_setup=False) -> MockConfigEntry: +async def init_integration( + hass: HomeAssistant, skip_setup: bool = False +) -> MockConfigEntry: """Set up the Brother integration in Home Assistant.""" entry = MockConfigEntry( domain=DOMAIN, @@ -20,7 +23,7 @@ async def init_integration(hass, skip_setup=False) -> MockConfigEntry: entry.add_to_hass(hass) if not skip_setup: - with patch( + with patch("brother.Brother.initialize"), patch( "brother.Brother._get_data", return_value=json.loads(load_fixture("printer_data.json", "brother")), ): diff --git a/tests/components/brother/fixtures/diagnostics_data.json b/tests/components/brother/fixtures/diagnostics_data.json index 0199acdd722..fd22f861e8d 100644 --- a/tests/components/brother/fixtures/diagnostics_data.json +++ b/tests/components/brother/fixtures/diagnostics_data.json @@ -1,5 +1,10 @@ { - "b/w_counter": 709, + "black_counter": null, + "black_ink": null, + "black_ink_remaining": null, + "black_ink_status": null, + "cyan_counter": null, + "bw_counter": 709, "belt_unit_remaining_life": 97, "belt_unit_remaining_pages": 48436, "black_drum_counter": 1611, @@ -12,6 +17,9 @@ "cyan_drum_counter": 1611, "cyan_drum_remaining_life": 92, "cyan_drum_remaining_pages": 16389, + "cyan_ink": null, + "cyan_ink_remaining": null, + "cyan_ink_status": null, "cyan_toner": 10, "cyan_toner_remaining": 10, "cyan_toner_status": 1, @@ -22,10 +30,17 @@ "duplex_unit_pages_counter": 538, "firmware": "1.17", "fuser_remaining_life": 97, + "fuser_unit_remaining_pages": null, + "image_counter": null, + "laser_remaining_life": null, "laser_unit_remaining_pages": 48389, + "magenta_counter": null, "magenta_drum_counter": 1611, "magenta_drum_remaining_life": 92, "magenta_drum_remaining_pages": 16389, + "magenta_ink": null, + "magenta_ink_remaining": null, + "magenta_ink_status": null, "magenta_toner": 10, "magenta_toner_remaining": 8, "magenta_toner_status": 2, @@ -33,12 +48,18 @@ "page_counter": 986, "pf_kit_1_remaining_life": 98, "pf_kit_1_remaining_pages": 48741, + "pf_kit_mp_remaining_life": null, + "pf_kit_mp_remaining_pages": null, "serial": "0123456789", "status": "waiting", "uptime": "2019-09-24T12:14:56+00:00", + "yellow_counter": null, "yellow_drum_counter": 1611, "yellow_drum_remaining_life": 92, "yellow_drum_remaining_pages": 16389, + "yellow_ink": null, + "yellow_ink_remaining": null, + "yellow_ink_status": null, "yellow_toner": 10, "yellow_toner_remaining": 2, "yellow_toner_status": 2 diff --git a/tests/components/brother/test_config_flow.py b/tests/components/brother/test_config_flow.py index 60f29dbd901..5a145edf030 100644 --- a/tests/components/brother/test_config_flow.py +++ b/tests/components/brother/test_config_flow.py @@ -9,13 +9,14 @@ from homeassistant.components import zeroconf from homeassistant.components.brother.const import DOMAIN from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_HOST, CONF_TYPE +from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry, load_fixture CONFIG = {CONF_HOST: "127.0.0.1", CONF_TYPE: "laser"} -async def test_show_form(hass): +async def test_show_form(hass: HomeAssistant) -> None: """Test that the form is served with no input.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} @@ -25,9 +26,9 @@ async def test_show_form(hass): assert result["step_id"] == SOURCE_USER -async def test_create_entry_with_hostname(hass): +async def test_create_entry_with_hostname(hass: HomeAssistant) -> None: """Test that the user step works with printer hostname.""" - with patch( + with patch("brother.Brother.initialize"), patch( "brother.Brother._get_data", return_value=json.loads(load_fixture("printer_data.json", "brother")), ): @@ -43,9 +44,9 @@ async def test_create_entry_with_hostname(hass): assert result["data"][CONF_TYPE] == "laser" -async def test_create_entry_with_ipv4_address(hass): +async def test_create_entry_with_ipv4_address(hass: HomeAssistant) -> None: """Test that the user step works with printer IPv4 address.""" - with patch( + with patch("brother.Brother.initialize"), patch( "brother.Brother._get_data", return_value=json.loads(load_fixture("printer_data.json", "brother")), ): @@ -59,9 +60,9 @@ async def test_create_entry_with_ipv4_address(hass): assert result["data"][CONF_TYPE] == "laser" -async def test_create_entry_with_ipv6_address(hass): +async def test_create_entry_with_ipv6_address(hass: HomeAssistant) -> None: """Test that the user step works with printer IPv6 address.""" - with patch( + with patch("brother.Brother.initialize"), patch( "brother.Brother._get_data", return_value=json.loads(load_fixture("printer_data.json", "brother")), ): @@ -77,7 +78,7 @@ async def test_create_entry_with_ipv6_address(hass): assert result["data"][CONF_TYPE] == "laser" -async def test_invalid_hostname(hass): +async def test_invalid_hostname(hass: HomeAssistant) -> None: """Test invalid hostname in user_input.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -88,9 +89,11 @@ async def test_invalid_hostname(hass): assert result["errors"] == {CONF_HOST: "wrong_host"} -async def test_connection_error(hass): +async def test_connection_error(hass: HomeAssistant) -> None: """Test connection to host error.""" - with patch("brother.Brother._get_data", side_effect=ConnectionError()): + with patch("brother.Brother.initialize"), patch( + "brother.Brother._get_data", side_effect=ConnectionError() + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONFIG ) @@ -98,9 +101,11 @@ async def test_connection_error(hass): assert result["errors"] == {"base": "cannot_connect"} -async def test_snmp_error(hass): +async def test_snmp_error(hass: HomeAssistant) -> None: """Test SNMP error.""" - with patch("brother.Brother._get_data", side_effect=SnmpError("error")): + with patch("brother.Brother.initialize"), patch( + "brother.Brother._get_data", side_effect=SnmpError("error") + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONFIG ) @@ -108,9 +113,11 @@ async def test_snmp_error(hass): assert result["errors"] == {"base": "snmp_error"} -async def test_unsupported_model_error(hass): +async def test_unsupported_model_error(hass: HomeAssistant) -> None: """Test unsupported printer model error.""" - with patch("brother.Brother._get_data", side_effect=UnsupportedModel("error")): + with patch("brother.Brother.initialize"), patch( + "brother.Brother._get_data", side_effect=UnsupportedModel("error") + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONFIG @@ -120,9 +127,9 @@ async def test_unsupported_model_error(hass): assert result["reason"] == "unsupported_model" -async def test_device_exists_abort(hass): +async def test_device_exists_abort(hass: HomeAssistant) -> None: """Test we abort config flow if Brother printer already configured.""" - with patch( + with patch("brother.Brother.initialize"), patch( "brother.Brother._get_data", return_value=json.loads(load_fixture("printer_data.json", "brother")), ): @@ -137,9 +144,11 @@ async def test_device_exists_abort(hass): assert result["reason"] == "already_configured" -async def test_zeroconf_snmp_error(hass): +async def test_zeroconf_snmp_error(hass: HomeAssistant) -> None: """Test we abort zeroconf flow on SNMP error.""" - with patch("brother.Brother._get_data", side_effect=SnmpError("error")): + with patch("brother.Brother.initialize"), patch( + "brother.Brother._get_data", side_effect=SnmpError("error") + ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -159,9 +168,11 @@ async def test_zeroconf_snmp_error(hass): assert result["reason"] == "cannot_connect" -async def test_zeroconf_unsupported_model(hass): +async def test_zeroconf_unsupported_model(hass: HomeAssistant) -> None: """Test unsupported printer model error.""" - with patch("brother.Brother._get_data") as mock_get_data: + with patch("brother.Brother.initialize"), patch( + "brother.Brother._get_data" + ) as mock_get_data: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, @@ -181,9 +192,9 @@ async def test_zeroconf_unsupported_model(hass): assert len(mock_get_data.mock_calls) == 0 -async def test_zeroconf_device_exists_abort(hass): +async def test_zeroconf_device_exists_abort(hass: HomeAssistant) -> None: """Test we abort zeroconf flow if Brother printer already configured.""" - with patch( + with patch("brother.Brother.initialize"), patch( "brother.Brother._get_data", return_value=json.loads(load_fixture("printer_data.json", "brother")), ): @@ -215,11 +226,13 @@ async def test_zeroconf_device_exists_abort(hass): assert entry.data["host"] == "127.0.0.1" -async def test_zeroconf_no_probe_existing_device(hass): +async def test_zeroconf_no_probe_existing_device(hass: HomeAssistant) -> None: """Test we do not probe the device is the host is already configured.""" entry = MockConfigEntry(domain=DOMAIN, unique_id="0123456789", data=CONFIG) entry.add_to_hass(hass) - with patch("brother.Brother._get_data") as mock_get_data: + with patch("brother.Brother.initialize"), patch( + "brother.Brother._get_data" + ) as mock_get_data: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, @@ -240,9 +253,9 @@ async def test_zeroconf_no_probe_existing_device(hass): assert len(mock_get_data.mock_calls) == 0 -async def test_zeroconf_confirm_create_entry(hass): +async def test_zeroconf_confirm_create_entry(hass: HomeAssistant) -> None: """Test zeroconf confirmation and create config entry.""" - with patch( + with patch("brother.Brother.initialize"), patch( "brother.Brother._get_data", return_value=json.loads(load_fixture("printer_data.json", "brother")), ): diff --git a/tests/components/brother/test_diagnostics.py b/tests/components/brother/test_diagnostics.py index 7e25ffaa4f0..73b3b6dda7a 100644 --- a/tests/components/brother/test_diagnostics.py +++ b/tests/components/brother/test_diagnostics.py @@ -1,8 +1,12 @@ """Test Brother diagnostics.""" +from collections.abc import Awaitable, Callable from datetime import datetime import json from unittest.mock import Mock, patch +from aiohttp import ClientSession + +from homeassistant.core import HomeAssistant from homeassistant.util.dt import UTC from tests.common import load_fixture @@ -10,13 +14,17 @@ from tests.components.brother import init_integration from tests.components.diagnostics import get_diagnostics_for_config_entry -async def test_entry_diagnostics(hass, hass_client): +async def test_entry_diagnostics( + hass: HomeAssistant, hass_client: Callable[..., Awaitable[ClientSession]] +) -> None: """Test config entry diagnostics.""" entry = await init_integration(hass, skip_setup=True) diagnostics_data = json.loads(load_fixture("diagnostics_data.json", "brother")) test_time = datetime(2019, 11, 11, 9, 10, 32, tzinfo=UTC) - with patch("brother.datetime", utcnow=Mock(return_value=test_time)), patch( + with patch("brother.Brother.initialize"), patch( + "brother.datetime", utcnow=Mock(return_value=test_time) + ), patch( "brother.Brother._get_data", return_value=json.loads(load_fixture("printer_data.json", "brother")), ): diff --git a/tests/components/brother/test_init.py b/tests/components/brother/test_init.py index 76b999c3e54..b034259974a 100644 --- a/tests/components/brother/test_init.py +++ b/tests/components/brother/test_init.py @@ -1,15 +1,19 @@ """Test init of Brother integration.""" from unittest.mock import patch +from brother import SnmpError +import pytest + from homeassistant.components.brother.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_HOST, CONF_TYPE, STATE_UNAVAILABLE +from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry from tests.components.brother import init_integration -async def test_async_setup_entry(hass): +async def test_async_setup_entry(hass: HomeAssistant) -> None: """Test a successful setup entry.""" await init_integration(hass) @@ -19,7 +23,7 @@ async def test_async_setup_entry(hass): assert state.state == "waiting" -async def test_config_not_ready(hass): +async def test_config_not_ready(hass: HomeAssistant) -> None: """Test for setup failure if connection to broker is missing.""" entry = MockConfigEntry( domain=DOMAIN, @@ -28,13 +32,31 @@ async def test_config_not_ready(hass): data={CONF_HOST: "localhost", CONF_TYPE: "laser"}, ) - with patch("brother.Brother._get_data", side_effect=ConnectionError()): + with patch("brother.Brother.initialize"), patch( + "brother.Brother._get_data", side_effect=ConnectionError() + ): entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) assert entry.state is ConfigEntryState.SETUP_RETRY -async def test_unload_entry(hass): +@pytest.mark.parametrize("exc", [(SnmpError("SNMP Error")), (ConnectionError)]) +async def test_error_on_init(hass: HomeAssistant, exc: Exception) -> None: + """Test for error on init.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="HL-L2340DW 0123456789", + unique_id="0123456789", + data={CONF_HOST: "localhost", CONF_TYPE: "laser"}, + ) + + with patch("brother.Brother.initialize", side_effect=exc): + entry.add_to_hass(hass) + 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 successful unload of entry.""" entry = await init_integration(hass) diff --git a/tests/components/brother/test_sensor.py b/tests/components/brother/test_sensor.py index 7ef87a2e396..f50104c0b3a 100644 --- a/tests/components/brother/test_sensor.py +++ b/tests/components/brother/test_sensor.py @@ -19,6 +19,7 @@ from homeassistant.const import ( PERCENTAGE, STATE_UNAVAILABLE, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component from homeassistant.util.dt import UTC, utcnow @@ -30,7 +31,7 @@ ATTR_REMAINING_PAGES = "remaining_pages" ATTR_COUNTER = "counter" -async def test_sensors(hass): +async def test_sensors(hass: HomeAssistant) -> None: """Test states of the sensors.""" entry = await init_integration(hass, skip_setup=True) @@ -45,7 +46,9 @@ async def test_sensors(hass): disabled_by=None, ) test_time = datetime(2019, 11, 11, 9, 10, 32, tzinfo=UTC) - with patch("brother.datetime", utcnow=Mock(return_value=test_time)), patch( + with patch("brother.Brother.initialize"), patch( + "brother.datetime", utcnow=Mock(return_value=test_time) + ), patch( "brother.Brother._get_data", return_value=json.loads(load_fixture("printer_data.json", "brother")), ): @@ -235,7 +238,7 @@ async def test_sensors(hass): entry = registry.async_get("sensor.hl_l2340dw_b_w_counter") assert entry - assert entry.unique_id == "0123456789_b/w_counter" + assert entry.unique_id == "0123456789_bw_counter" state = hass.states.get("sensor.hl_l2340dw_color_counter") assert state @@ -276,7 +279,7 @@ async def test_disabled_by_default_sensors(hass): assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION -async def test_availability(hass): +async def test_availability(hass: HomeAssistant) -> None: """Ensure that we mark the entities unavailable correctly when device is offline.""" await init_integration(hass) @@ -286,7 +289,9 @@ async def test_availability(hass): assert state.state == "waiting" future = utcnow() + timedelta(minutes=5) - with patch("brother.Brother._get_data", side_effect=ConnectionError()): + with patch("brother.Brother.initialize"), patch( + "brother.Brother._get_data", side_effect=ConnectionError() + ): async_fire_time_changed(hass, future) await hass.async_block_till_done() @@ -295,7 +300,7 @@ async def test_availability(hass): assert state.state == STATE_UNAVAILABLE future = utcnow() + timedelta(minutes=10) - with patch( + with patch("brother.Brother.initialize"), patch( "brother.Brother._get_data", return_value=json.loads(load_fixture("printer_data.json", "brother")), ): @@ -308,7 +313,7 @@ async def test_availability(hass): assert state.state == "waiting" -async def test_manual_update_entity(hass): +async def test_manual_update_entity(hass: HomeAssistant) -> None: """Test manual update entity via service homeassistant/update_entity.""" await init_integration(hass) @@ -326,3 +331,22 @@ async def test_manual_update_entity(hass): ) assert len(mock_update.mock_calls) == 1 + + +async def test_unique_id_migration(hass: HomeAssistant) -> None: + """Test states of the unique_id migration.""" + registry = er.async_get(hass) + + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "0123456789_b/w_counter", + suggested_object_id="hl_l2340dw_b_w_counter", + disabled_by=None, + ) + + await init_integration(hass) + + entry = registry.async_get("sensor.hl_l2340dw_b_w_counter") + assert entry + assert entry.unique_id == "0123456789_bw_counter" From d33cc2c83e112797ce9fec5bb76d2a74550d103c Mon Sep 17 00:00:00 2001 From: Tom Brien Date: Sun, 18 Sep 2022 22:30:09 +0100 Subject: [PATCH 528/955] Add GALA currency to Coinbase (#78708) Add GALA currency --- homeassistant/components/coinbase/const.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/coinbase/const.py b/homeassistant/components/coinbase/const.py index 08773745f09..48d4b1a6307 100644 --- a/homeassistant/components/coinbase/const.py +++ b/homeassistant/components/coinbase/const.py @@ -110,6 +110,7 @@ WALLETS = { "FJD": "FJD", "FKP": "FKP", "FORTH": "FORTH", + "GALA": "GALA", "GBP": "GBP", "GBX": "GBX", "GEL": "GEL", From a1735b742c299638f5e6586c573bc2f3d39096cc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 18 Sep 2022 16:36:50 -0500 Subject: [PATCH 529/955] Drop PARALLEL_UPDATES from switchbot (#78713) --- homeassistant/components/switchbot/binary_sensor.py | 2 +- homeassistant/components/switchbot/cover.py | 2 +- homeassistant/components/switchbot/light.py | 2 +- homeassistant/components/switchbot/manifest.json | 2 +- homeassistant/components/switchbot/sensor.py | 2 +- homeassistant/components/switchbot/switch.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/switchbot/binary_sensor.py b/homeassistant/components/switchbot/binary_sensor.py index bf071d64a2d..a5378028264 100644 --- a/homeassistant/components/switchbot/binary_sensor.py +++ b/homeassistant/components/switchbot/binary_sensor.py @@ -15,7 +15,7 @@ from .const import DOMAIN from .coordinator import SwitchbotDataUpdateCoordinator from .entity import SwitchbotEntity -PARALLEL_UPDATES = 1 +PARALLEL_UPDATES = 0 BINARY_SENSOR_TYPES: dict[str, BinarySensorEntityDescription] = { "calibration": BinarySensorEntityDescription( diff --git a/homeassistant/components/switchbot/cover.py b/homeassistant/components/switchbot/cover.py index df716be6ff3..696c9455f28 100644 --- a/homeassistant/components/switchbot/cover.py +++ b/homeassistant/components/switchbot/cover.py @@ -24,7 +24,7 @@ from .entity import SwitchbotEntity # Initialize the logger _LOGGER = logging.getLogger(__name__) -PARALLEL_UPDATES = 1 +PARALLEL_UPDATES = 0 async def async_setup_entry( diff --git a/homeassistant/components/switchbot/light.py b/homeassistant/components/switchbot/light.py index e55f5fff9b1..0b4f748f1b2 100644 --- a/homeassistant/components/switchbot/light.py +++ b/homeassistant/components/switchbot/light.py @@ -29,7 +29,7 @@ SWITCHBOT_COLOR_MODE_TO_HASS = { SwitchBotColorMode.COLOR_TEMP: ColorMode.COLOR_TEMP, } -PARALLEL_UPDATES = 1 +PARALLEL_UPDATES = 0 async def async_setup_entry( diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index a321c964edc..bb670cc72d3 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.19.9"], + "requirements": ["PySwitchbot==0.19.10"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/homeassistant/components/switchbot/sensor.py b/homeassistant/components/switchbot/sensor.py index 9658c1ed9c8..e435e71efbd 100644 --- a/homeassistant/components/switchbot/sensor.py +++ b/homeassistant/components/switchbot/sensor.py @@ -21,7 +21,7 @@ from .const import DOMAIN from .coordinator import SwitchbotDataUpdateCoordinator from .entity import SwitchbotEntity -PARALLEL_UPDATES = 1 +PARALLEL_UPDATES = 0 SENSOR_TYPES: dict[str, SensorEntityDescription] = { "rssi": SensorEntityDescription( diff --git a/homeassistant/components/switchbot/switch.py b/homeassistant/components/switchbot/switch.py index d524a7100f0..c4bbc2af1e0 100644 --- a/homeassistant/components/switchbot/switch.py +++ b/homeassistant/components/switchbot/switch.py @@ -19,7 +19,7 @@ from .entity import SwitchbotEntity # Initialize the logger _LOGGER = logging.getLogger(__name__) -PARALLEL_UPDATES = 1 +PARALLEL_UPDATES = 0 async def async_setup_entry( diff --git a/requirements_all.txt b/requirements_all.txt index 32ac27e2fbb..8abdbd3a8f9 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.19.9 +PySwitchbot==0.19.10 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8ad195aafc6..0fd9feb6b19 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.19.9 +PySwitchbot==0.19.10 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From ba74f00fb50f6a57cbad6a5fd1a12a209d5dd1d0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 18 Sep 2022 16:49:12 -0500 Subject: [PATCH 530/955] Add tests for switchbot sensor platform (#78611) --- tests/components/switchbot/__init__.py | 4 +- .../components/switchbot/test_config_flow.py | 4 +- tests/components/switchbot/test_sensor.py | 58 +++++++++++++++++++ 3 files changed, 62 insertions(+), 4 deletions(-) create mode 100644 tests/components/switchbot/test_sensor.py diff --git a/tests/components/switchbot/__init__.py b/tests/components/switchbot/__init__.py index 3235a9e8cd3..f30f72892ba 100644 --- a/tests/components/switchbot/__init__.py +++ b/tests/components/switchbot/__init__.py @@ -59,7 +59,7 @@ WOHAND_SERVICE_INFO = BluetoothServiceInfoBleak( manufacturer_data={89: b"\xfd`0U\x92W"}, service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x90\xd9"}, service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], - address="aa:bb:cc:dd:ee:ff", + address="AA:BB:CC:DD:EE:FF", rssi=-60, source="local", advertisement=AdvertisementData( @@ -68,7 +68,7 @@ WOHAND_SERVICE_INFO = BluetoothServiceInfoBleak( service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x90\xd9"}, service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], ), - device=BLEDevice("aa:bb:cc:dd:ee:ff", "WoHand"), + device=BLEDevice("AA:BB:CC:DD:EE:FF", "WoHand"), time=0, connectable=True, ) diff --git a/tests/components/switchbot/test_config_flow.py b/tests/components/switchbot/test_config_flow.py index 71b7018a6b3..66d0874809f 100644 --- a/tests/components/switchbot/test_config_flow.py +++ b/tests/components/switchbot/test_config_flow.py @@ -45,7 +45,7 @@ async def test_bluetooth_discovery(hass): assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "Bot EEFF" assert result["data"] == { - CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", + CONF_ADDRESS: "AA:BB:CC:DD:EE:FF", CONF_SENSOR_TYPE: "bot", } @@ -148,7 +148,7 @@ async def test_user_setup_wohand(hass): assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "Bot EEFF" assert result["data"] == { - CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", + CONF_ADDRESS: "AA:BB:CC:DD:EE:FF", CONF_SENSOR_TYPE: "bot", } diff --git a/tests/components/switchbot/test_sensor.py b/tests/components/switchbot/test_sensor.py new file mode 100644 index 00000000000..ae77c5a8de4 --- /dev/null +++ b/tests/components/switchbot/test_sensor.py @@ -0,0 +1,58 @@ +"""Test the switchbot sensors.""" + + +from homeassistant.components.sensor import ATTR_STATE_CLASS +from homeassistant.components.switchbot.const import DOMAIN +from homeassistant.const import ( + ATTR_FRIENDLY_NAME, + ATTR_UNIT_OF_MEASUREMENT, + CONF_ADDRESS, + CONF_NAME, + CONF_PASSWORD, + CONF_SENSOR_TYPE, +) +from homeassistant.setup import async_setup_component + +from . import WOHAND_SERVICE_INFO + +from tests.common import MockConfigEntry +from tests.components.bluetooth import inject_bluetooth_service_info + + +async def test_sensors(hass, entity_registry_enabled_by_default): + """Test setting up creates the sensors.""" + await async_setup_component(hass, DOMAIN, {}) + inject_bluetooth_service_info(hass, WOHAND_SERVICE_INFO) + + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", + CONF_NAME: "test-name", + CONF_PASSWORD: "test-password", + CONF_SENSOR_TYPE: "bot", + }, + unique_id="aabbccddeeff", + ) + 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")) == 2 + + battery_sensor = hass.states.get("sensor.test_name_battery") + battery_sensor_attrs = battery_sensor.attributes + assert battery_sensor.state == "89" + assert battery_sensor_attrs[ATTR_FRIENDLY_NAME] == "test-name Battery" + assert battery_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == "%" + assert battery_sensor_attrs[ATTR_STATE_CLASS] == "measurement" + + rssi_sensor = hass.states.get("sensor.test_name_rssi") + rssi_sensor_attrs = rssi_sensor.attributes + assert rssi_sensor.state == "-60" + assert rssi_sensor_attrs[ATTR_FRIENDLY_NAME] == "test-name Rssi" + assert rssi_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == "dBm" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() From 1f928042111c9368d5899badb70052689229ed38 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 19 Sep 2022 00:27:46 +0000 Subject: [PATCH 531/955] [ci skip] Translation update --- .../airvisual/translations/sensor.ja.json | 2 +- .../components/awair/translations/nl.json | 10 +++++++- .../components/bthome/translations/nl.json | 3 +++ .../components/cover/translations/hu.json | 2 +- .../components/demo/translations/nl.json | 12 ++++++++++ .../deutsche_bahn/translations/nl.json | 7 ++++++ .../components/ecowitt/translations/nl.json | 7 ++++++ .../flunearyou/translations/nl.json | 11 +++++++++ .../google_sheets/translations/et.json | 17 +++++++++++++- .../google_sheets/translations/nl.json | 23 +++++++++++++++++++ .../components/group/translations/hu.json | 2 +- .../components/guardian/translations/et.json | 11 +++++++++ .../components/guardian/translations/hu.json | 13 ++++++++++- .../guardian/translations/pt-BR.json | 13 ++++++++++- .../components/guardian/translations/ru.json | 13 ++++++++++- .../guardian/translations/zh-Hant.json | 13 ++++++++++- .../translations/sensor.nl.json | 12 ++++++++++ .../components/icloud/translations/nl.json | 6 ++++- .../justnimbus/translations/nl.json | 4 +++- .../components/lametric/translations/et.json | 3 ++- .../components/lametric/translations/hu.json | 3 ++- .../components/lametric/translations/nl.json | 3 ++- .../components/led_ble/translations/nl.json | 23 +++++++++++++++++++ .../litterrobot/translations/nl.json | 9 +++++++- .../litterrobot/translations/sensor.et.json | 3 +++ .../litterrobot/translations/sensor.hu.json | 3 +++ .../litterrobot/translations/sensor.nl.json | 3 +++ .../translations/sensor.pt-BR.json | 3 +++ .../litterrobot/translations/sensor.ru.json | 3 +++ .../translations/sensor.zh-Hant.json | 3 +++ .../components/mysensors/translations/nl.json | 7 ++++++ .../nam/translations/sensor.nl.json | 11 +++++++++ .../components/openuv/translations/et.json | 10 ++++++++ .../components/openuv/translations/fr.json | 8 +++++++ .../components/openuv/translations/hu.json | 10 ++++++++ .../components/openuv/translations/pt-BR.json | 10 ++++++++ .../components/openuv/translations/ru.json | 10 ++++++++ .../openuv/translations/zh-Hant.json | 10 ++++++++ .../components/prusalink/translations/nl.json | 18 +++++++++++++++ .../prusalink/translations/sensor.nl.json | 9 ++++++++ .../components/qingping/translations/nl.json | 22 ++++++++++++++++++ .../components/rfxtrx/translations/hu.json | 2 +- .../components/schedule/translations/nl.json | 3 ++- .../components/sensorpro/translations/nl.json | 22 ++++++++++++++++++ .../simplisafe/translations/et.json | 6 +++++ .../simplisafe/translations/hu.json | 6 +++++ .../somfy_mylink/translations/hu.json | 2 +- .../switch_as_x/translations/hu.json | 2 +- .../components/tasmota/translations/hu.json | 10 ++++++++ .../tomorrowio/translations/sensor.ja.json | 2 +- .../volvooncall/translations/nl.json | 2 ++ .../components/weather/translations/ja.json | 2 +- .../xiaomi_miio/translations/select.nl.json | 5 ++++ .../yalexs_ble/translations/nl.json | 11 +++++++++ .../components/zha/translations/nl.json | 13 +++++++++++ .../components/zwave_js/translations/en.json | 6 +++++ 56 files changed, 438 insertions(+), 21 deletions(-) create mode 100644 homeassistant/components/deutsche_bahn/translations/nl.json create mode 100644 homeassistant/components/ecowitt/translations/nl.json create mode 100644 homeassistant/components/google_sheets/translations/nl.json create mode 100644 homeassistant/components/homekit_controller/translations/sensor.nl.json create mode 100644 homeassistant/components/led_ble/translations/nl.json create mode 100644 homeassistant/components/nam/translations/sensor.nl.json create mode 100644 homeassistant/components/prusalink/translations/nl.json create mode 100644 homeassistant/components/prusalink/translations/sensor.nl.json create mode 100644 homeassistant/components/qingping/translations/nl.json create mode 100644 homeassistant/components/sensorpro/translations/nl.json diff --git a/homeassistant/components/airvisual/translations/sensor.ja.json b/homeassistant/components/airvisual/translations/sensor.ja.json index 91bd016b0ac..718fa0ed158 100644 --- a/homeassistant/components/airvisual/translations/sensor.ja.json +++ b/homeassistant/components/airvisual/translations/sensor.ja.json @@ -11,7 +11,7 @@ "airvisual__pollutant_level": { "good": "\u826f\u597d", "hazardous": "\u5371\u967a", - "moderate": "\u9069\u5ea6", + "moderate": "\u4e2d\u7a0b\u5ea6", "unhealthy": "\u4e0d\u5065\u5eb7", "unhealthy_sensitive": "\u654f\u611f\u306a\u30b0\u30eb\u30fc\u30d7\u306b\u3068\u3063\u3066\u306f\u4e0d\u5065\u5eb7", "very_unhealthy": "\u3068\u3066\u3082\u4e0d\u5065\u5eb7" diff --git a/homeassistant/components/awair/translations/nl.json b/homeassistant/components/awair/translations/nl.json index 1f002a5984b..6a42bca2372 100644 --- a/homeassistant/components/awair/translations/nl.json +++ b/homeassistant/components/awair/translations/nl.json @@ -26,6 +26,11 @@ "host": "IP-adres" } }, + "local_pick": { + "data": { + "device": "Apparaat" + } + }, "reauth": { "data": { "access_token": "Toegangstoken", @@ -44,7 +49,10 @@ "access_token": "Toegangstoken", "email": "E-mail" }, - "description": "U moet zich registreren voor een Awair-toegangstoken voor ontwikkelaars op: https://developer.getawair.com/onboard/login" + "description": "U moet zich registreren voor een Awair-toegangstoken voor ontwikkelaars op: https://developer.getawair.com/onboard/login", + "menu_options": { + "cloud": "Verbinden via de cloud" + } } } } diff --git a/homeassistant/components/bthome/translations/nl.json b/homeassistant/components/bthome/translations/nl.json index 9a4e727ef2e..6b79e0311de 100644 --- a/homeassistant/components/bthome/translations/nl.json +++ b/homeassistant/components/bthome/translations/nl.json @@ -12,6 +12,9 @@ "description": "Wilt u {name} instellen?" }, "user": { + "data": { + "address": "Apparaat" + }, "description": "Kies een apparaat om in te stellen" } } diff --git a/homeassistant/components/cover/translations/hu.json b/homeassistant/components/cover/translations/hu.json index 2155907cae2..e2f24e28af7 100644 --- a/homeassistant/components/cover/translations/hu.json +++ b/homeassistant/components/cover/translations/hu.json @@ -35,5 +35,5 @@ "stopped": "Meg\u00e1llt" } }, - "title": "Bor\u00edt\u00f3" + "title": "\u00c1rny\u00e9kol\u00f3" } \ No newline at end of file diff --git a/homeassistant/components/demo/translations/nl.json b/homeassistant/components/demo/translations/nl.json index 37b23b2aaac..08aeec0fcd0 100644 --- a/homeassistant/components/demo/translations/nl.json +++ b/homeassistant/components/demo/translations/nl.json @@ -1,4 +1,16 @@ { + "issues": { + "bad_psu": { + "fix_flow": { + "step": { + "confirm": { + "title": "De voeding moet worden vervangen" + } + } + }, + "title": "De voeding is niet stabiel" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/deutsche_bahn/translations/nl.json b/homeassistant/components/deutsche_bahn/translations/nl.json new file mode 100644 index 00000000000..6aabd5b3a6d --- /dev/null +++ b/homeassistant/components/deutsche_bahn/translations/nl.json @@ -0,0 +1,7 @@ +{ + "issues": { + "pending_removal": { + "title": "De Deutsche Bahn-integratie wordt verwijderd" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecowitt/translations/nl.json b/homeassistant/components/ecowitt/translations/nl.json new file mode 100644 index 00000000000..7e198e836d7 --- /dev/null +++ b/homeassistant/components/ecowitt/translations/nl.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "unknown": "Onverwachte fout" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/nl.json b/homeassistant/components/flunearyou/translations/nl.json index 0938bd45206..b38da0e9901 100644 --- a/homeassistant/components/flunearyou/translations/nl.json +++ b/homeassistant/components/flunearyou/translations/nl.json @@ -16,5 +16,16 @@ "title": "Configureer \nFlu Near You" } } + }, + "issues": { + "integration_removal": { + "fix_flow": { + "step": { + "confirm": { + "title": "Flu Near You verwijderen" + } + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/google_sheets/translations/et.json b/homeassistant/components/google_sheets/translations/et.json index 6373e7d5b63..e1e88192389 100644 --- a/homeassistant/components/google_sheets/translations/et.json +++ b/homeassistant/components/google_sheets/translations/et.json @@ -1,8 +1,20 @@ { + "application_credentials": { + "description": "J\u00e4rgi [instructions]({more_info_url}) v\u00e4ljal [OAuth consent screen]({oauth_consent_url}), et anda koduabilisele juurdep\u00e4\u00e4s oma Google'i arvutustabelitele. Samuti pead looma oma kontoga lingitud rakenduse identimisteabe.\n1. Mine aadressile [Credentials]({oauth_creds_url}) ja kl\u00f5psa **Create Credentials**.\n1. Vali ripploendist **OAuthi kliendi ID**.\n1. Vali rakenduse t\u00fc\u00fcbiks **Veebirakendus**.\n\n" + }, "config": { "abort": { + "already_configured": "Konto on juba h\u00e4\u00e4lestatud", + "already_in_progress": "Seadistamine on juba k\u00e4imas", + "cannot_connect": "\u00dchendamine nurjus", "create_spreadsheet_failure": "Viga arvutustabeli loomisel, vt vigade logi \u00fcksikasjadeks", - "open_spreadsheet_failure": "Viga arvutustabelite avamisel, vt vigade logi \u00fcksikasjade kohta" + "invalid_access_token": "Vigane juurdep\u00e4\u00e4suluba", + "missing_configuration": "See osis on seadistamata. Vaata teavet.", + "oauth_error": "Saadi vigane luba", + "open_spreadsheet_failure": "Viga arvutustabelite avamisel, vt vigade logi \u00fcksikasjade kohta", + "reauth_successful": "Taastuvastamine \u00f5nnestus", + "timeout_connect": "\u00dchenduse loomise ajal\u00f5pp", + "unknown": "Ootamatu t\u00f5rge" }, "create_entry": { "default": "Autentimine \u00f5nnestus ja arvutustabel loodud aadressil: {url}" @@ -10,6 +22,9 @@ "step": { "auth": { "title": "Google'i konto linkimine" + }, + "pick_implementation": { + "title": "Vali tuvastusmeetod" } } } diff --git a/homeassistant/components/google_sheets/translations/nl.json b/homeassistant/components/google_sheets/translations/nl.json new file mode 100644 index 00000000000..bf3a9a5db0a --- /dev/null +++ b/homeassistant/components/google_sheets/translations/nl.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Account is al geconfigureerd", + "already_in_progress": "De configuratie is momenteel al bezig", + "cannot_connect": "Kan geen verbinding maken", + "invalid_access_token": "Ongeldig toegangstoken", + "missing_configuration": "Integratie niet geconfigureerd. Raadpleeg de documentatie.", + "oauth_error": "Ongeldige tokengegevens ontvangen.", + "reauth_successful": "Herauthenticatie geslaagd", + "timeout_connect": "Time-out bij het maken van verbinding", + "unknown": "Onverwachte fout" + }, + "step": { + "auth": { + "title": "Google-account koppelen" + }, + "pick_implementation": { + "title": "Kies een authenticatie methode" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/group/translations/hu.json b/homeassistant/components/group/translations/hu.json index dfe4e0dcfee..37e6567c517 100644 --- a/homeassistant/components/group/translations/hu.json +++ b/homeassistant/components/group/translations/hu.json @@ -63,7 +63,7 @@ "description": "A csoportok lehet\u0151v\u00e9 teszik egy \u00faj entit\u00e1s l\u00e9trehoz\u00e1s\u00e1t, amely t\u00f6bb azonos t\u00edpus\u00fa entit\u00e1st k\u00e9pvisel.", "menu_options": { "binary_sensor": "Bin\u00e1ris \u00e9rz\u00e9kel\u0151 csoport", - "cover": "Red\u0151ny csoport", + "cover": "\u00c1rny\u00e9kol\u00f3 csoport", "fan": "Ventil\u00e1tor csoport", "light": "L\u00e1mpa csoport", "lock": "Z\u00e1r csoport", diff --git a/homeassistant/components/guardian/translations/et.json b/homeassistant/components/guardian/translations/et.json index 49172263d9c..37eee5acf33 100644 --- a/homeassistant/components/guardian/translations/et.json +++ b/homeassistant/components/guardian/translations/et.json @@ -29,6 +29,17 @@ } }, "title": "Teenus {deprecated_service} eemaldatakse" + }, + "replaced_old_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "See olem on asendatud olemiga \"{replacement_entity_id}\".", + "title": "Olem {old_entity_id} eemaldatakse" + } + } + }, + "title": "Olem {old_entity_id} eemaldatakse" } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/hu.json b/homeassistant/components/guardian/translations/hu.json index 35787e95524..38873657dda 100644 --- a/homeassistant/components/guardian/translations/hu.json +++ b/homeassistant/components/guardian/translations/hu.json @@ -23,12 +23,23 @@ "fix_flow": { "step": { "confirm": { - "description": "Friss\u00edtsen minden olyan automatiz\u00e1l\u00e1st vagy szkriptet, amely ezt a szolg\u00e1ltat\u00e1st haszn\u00e1lja, hogy helyette a(z) `{alternate_service}` szolg\u00e1ltat\u00e1st haszn\u00e1lja a(z) `{alternate_target}` entit\u00e1ssal. Ezut\u00e1n kattintson az al\u00e1bbi MEHET gombra a probl\u00e9ma megoldottk\u00e9nt val\u00f3 megjel\u00f6l\u00e9s\u00e9hez.", + "description": "Friss\u00edtse a szolg\u00e1ltat\u00e1st haszn\u00e1l\u00f3 automatizmusokat vagy szkripteket, hogy helyette az `{alternate_service}` szolg\u00e1ltat\u00e1st haszn\u00e1lhassa `{alternate_target}` c\u00e9lentit\u00e1s-azonos\u00edt\u00f3val.", "title": "{deprecated_service} szolg\u00e1ltat\u00e1s elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" } } }, "title": "{deprecated_service} szolg\u00e1ltat\u00e1s elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + }, + "replaced_old_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "Ezt az entit\u00e1st a `{replacement_entity_id}} v\u00e1ltotta fel.", + "title": "{old_entity_id} entit\u00e1s el lesz t\u00e1vol\u00edtva" + } + } + }, + "title": "{old_entity_id} entit\u00e1s el lesz t\u00e1vol\u00edtva" } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/pt-BR.json b/homeassistant/components/guardian/translations/pt-BR.json index 2a4514f4968..1166d53009f 100644 --- a/homeassistant/components/guardian/translations/pt-BR.json +++ b/homeassistant/components/guardian/translations/pt-BR.json @@ -23,12 +23,23 @@ "fix_flow": { "step": { "confirm": { - "description": "Atualize quaisquer automa\u00e7\u00f5es ou scripts que usam este servi\u00e7o para usar o servi\u00e7o `{alternate_service}` com um ID de entidade de destino de `{alternate_target}`. Em seguida, clique em ENVIAR abaixo para marcar este problema como resolvido.", + "description": "Atualize quaisquer automa\u00e7\u00f5es ou scripts que usam este servi\u00e7o para usar o servi\u00e7o `{alternate_service}` com um ID de entidade de destino de `{alternate_target}`.", "title": "O servi\u00e7o {deprecated_service} est\u00e1 sendo removido" } } }, "title": "O servi\u00e7o {deprecated_service} est\u00e1 sendo removido" + }, + "replaced_old_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "Esta entidade foi substitu\u00edda por `{replacement_entity_id}`.", + "title": "A entidade {old_entity_id} ser\u00e1 removida" + } + } + }, + "title": "A entidade {old_entity_id} ser\u00e1 removida" } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/ru.json b/homeassistant/components/guardian/translations/ru.json index 068787b5e86..4d256258e71 100644 --- a/homeassistant/components/guardian/translations/ru.json +++ b/homeassistant/components/guardian/translations/ru.json @@ -23,12 +23,23 @@ "fix_flow": { "step": { "confirm": { - "description": "\u0412\u043c\u0435\u0441\u0442\u043e \u044d\u0442\u043e\u0439 \u0441\u043b\u0443\u0436\u0431\u044b \u0442\u0435\u043f\u0435\u0440\u044c \u0441\u043b\u0435\u0434\u0443\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0441\u043b\u0443\u0436\u0431\u0443 `{alternate_service}` \u0441 \u0446\u0435\u043b\u0435\u0432\u044b\u043c \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u043c `{alternate_target}`. \u041e\u0442\u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u0443\u0439\u0442\u0435 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0438 \u0438 \u0441\u043a\u0440\u0438\u043f\u0442\u044b \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 \u041f\u041e\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u042c, \u0447\u0442\u043e\u0431\u044b \u043e\u0442\u043c\u0435\u0442\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443 \u043a\u0430\u043a \u0443\u0441\u0442\u0440\u0430\u043d\u0451\u043d\u043d\u0443\u044e.", + "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, \u0442\u0435\u043f\u0435\u0440\u044c \u0441\u043b\u0435\u0434\u0443\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0441\u043b\u0443\u0436\u0431\u0443 `{alternate_service}` \u0441 \u0446\u0435\u043b\u0435\u0432\u044b\u043c \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u043c `{alternate_target}`.", "title": "\u0421\u043b\u0443\u0436\u0431\u0430 {deprecated_service} \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" } } }, "title": "\u0421\u043b\u0443\u0436\u0431\u0430 {deprecated_service} \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + }, + "replaced_old_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u042d\u0442\u043e\u0442 \u043e\u0431\u044a\u0435\u043a\u0442 \u0431\u044b\u043b \u0437\u0430\u043c\u0435\u043d\u0435\u043d \u043d\u0430 `{replacement_entity_id}`.", + "title": "\u041e\u0431\u044a\u0435\u043a\u0442 {old_entity_id} \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d" + } + } + }, + "title": "\u041e\u0431\u044a\u0435\u043a\u0442 {old_entity_id} \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d" } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/zh-Hant.json b/homeassistant/components/guardian/translations/zh-Hant.json index baa0e477b4c..9870758f067 100644 --- a/homeassistant/components/guardian/translations/zh-Hant.json +++ b/homeassistant/components/guardian/translations/zh-Hant.json @@ -23,12 +23,23 @@ "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\u4f7f\u7528\u76ee\u6a19\u5be6\u9ad4 ID \u70ba `{alternate_target}` \u4e4b `{alternate_service}` \u670d\u52d9\uff0c\u7136\u5f8c\u9ede\u9078\u50b3\u9001\u4ee5\u6a19\u793a\u554f\u984c\u5df2\u89e3\u6c7a\u3002", + "description": "\u4f7f\u7528\u6b64\u670d\u52d9\u4ee5\u66f4\u65b0\u4efb\u4f55\u81ea\u52d5\u5316\u6216\u8173\u672c\u3001\u4ee5\u53d6\u4ee3\u4f7f\u7528\u76ee\u6a19\u5be6\u9ad4 ID \u70ba `{alternate_target}` \u4e4b `{alternate_service}` \u670d\u52d9\u3002", "title": "{deprecated_service} \u670d\u52d9\u5373\u5c07\u79fb\u9664" } } }, "title": "{deprecated_service} \u670d\u52d9\u5373\u5c07\u79fb\u9664" + }, + "replaced_old_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u5be6\u9ad4\u5c07\u7531 `{replacement_entity_id}` \u6240\u53d6\u4ee3\u3002", + "title": "{old_entity_id} \u5be6\u9ad4\u5c07\u9032\u884c\u79fb\u9664" + } + } + }, + "title": "{old_entity_id} \u5be6\u9ad4\u5c07\u9032\u884c\u79fb\u9664" } } } \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/sensor.nl.json b/homeassistant/components/homekit_controller/translations/sensor.nl.json new file mode 100644 index 00000000000..be137558599 --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/sensor.nl.json @@ -0,0 +1,12 @@ +{ + "state": { + "homekit_controller__thread_node_capabilities": { + "none": "Geen" + }, + "homekit_controller__thread_status": { + "detached": "Ontkoppeld", + "disabled": "Uitgeschakeld", + "router": "Router" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/translations/nl.json b/homeassistant/components/icloud/translations/nl.json index 30f0410fd60..6c831c2f65b 100644 --- a/homeassistant/components/icloud/translations/nl.json +++ b/homeassistant/components/icloud/translations/nl.json @@ -19,7 +19,11 @@ "title": "Integratie herauthenticeren" }, "reauth_confirm": { - "description": "Je eerder ingevoerde wachtwoord voor {username} werkt niet meer. Update je wachtwoord om deze integratie te blijven gebruiken." + "data": { + "password": "Wachtwoord" + }, + "description": "Je eerder ingevoerde wachtwoord voor {username} werkt niet meer. Update je wachtwoord om deze integratie te blijven gebruiken.", + "title": "Integratie herauthenticeren" }, "trusted_device": { "data": { diff --git a/homeassistant/components/justnimbus/translations/nl.json b/homeassistant/components/justnimbus/translations/nl.json index 70d636c953e..87e2082c011 100644 --- a/homeassistant/components/justnimbus/translations/nl.json +++ b/homeassistant/components/justnimbus/translations/nl.json @@ -4,7 +4,9 @@ "already_configured": "Apparaat is al geconfigureerd" }, "error": { - "cannot_connect": "Kan geen verbinding maken" + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" } } } \ No newline at end of file diff --git a/homeassistant/components/lametric/translations/et.json b/homeassistant/components/lametric/translations/et.json index 62340afca03..f2ec397481d 100644 --- a/homeassistant/components/lametric/translations/et.json +++ b/homeassistant/components/lametric/translations/et.json @@ -7,7 +7,8 @@ "link_local_address": "Kohtv\u00f5rgu linke ei toetata", "missing_configuration": "LaMetricu integratsioon pole konfigureeritud. Palun j\u00e4rgige dokumentatsiooni.", "no_devices": "Volitatud kasutajal pole LaMetricu seadmeid", - "no_url_available": "URL pole saadaval. Teavet selle veateate kohta saab [check the help section]({docs_url})" + "no_url_available": "URL pole saadaval. Teavet selle veateate kohta saab [check the help section]({docs_url})", + "unknown": "Ootamatu t\u00f5rge" }, "error": { "cannot_connect": "\u00dchendamine nurjus", diff --git a/homeassistant/components/lametric/translations/hu.json b/homeassistant/components/lametric/translations/hu.json index c27848279e7..0e326d4b4e8 100644 --- a/homeassistant/components/lametric/translations/hu.json +++ b/homeassistant/components/lametric/translations/hu.json @@ -7,7 +7,8 @@ "link_local_address": "A linklocal c\u00edmek nem t\u00e1mogatottak", "missing_configuration": "A LaMetric integr\u00e1ci\u00f3 nincs konfigur\u00e1lva. K\u00e9rj\u00fck, k\u00f6vesse a dokument\u00e1ci\u00f3t.", "no_devices": "A jogosult felhaszn\u00e1l\u00f3 nem rendelkezik LaMetric-eszk\u00f6z\u00f6kkel", - "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3 [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lhat\u00f3." + "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3 [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lhat\u00f3.", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", diff --git a/homeassistant/components/lametric/translations/nl.json b/homeassistant/components/lametric/translations/nl.json index 6cbebff75e3..de776229764 100644 --- a/homeassistant/components/lametric/translations/nl.json +++ b/homeassistant/components/lametric/translations/nl.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "Apparaat is al geconfigureerd", "authorize_url_timeout": "Time-out bij het genereren van autorisatie-URL.", - "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [raadpleeg de documentatie]({docs_url})" + "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [raadpleeg de documentatie]({docs_url})", + "unknown": "Onverwachte fout" }, "error": { "cannot_connect": "Kan geen verbinding maken", diff --git a/homeassistant/components/led_ble/translations/nl.json b/homeassistant/components/led_ble/translations/nl.json new file mode 100644 index 00000000000..14d596f2d6b --- /dev/null +++ b/homeassistant/components/led_ble/translations/nl.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "already_in_progress": "De configuratie is momenteel al bezig", + "no_devices_found": "Geen apparaten gevonden op het netwerk", + "no_unconfigured_devices": "Geen niet-geconfigureerde apparaten gevonden.", + "not_supported": "Apparaat is niet ondersteund" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "unknown": "Onverwachte fout" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "Bluetooth-adres" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/nl.json b/homeassistant/components/litterrobot/translations/nl.json index 50b4c3f2fe6..09d9c66090d 100644 --- a/homeassistant/components/litterrobot/translations/nl.json +++ b/homeassistant/components/litterrobot/translations/nl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Apparaat is al geconfigureerd" + "already_configured": "Apparaat is al geconfigureerd", + "reauth_successful": "Herauthenticatie geslaagd" }, "error": { "cannot_connect": "Kan geen verbinding maken", @@ -9,6 +10,12 @@ "unknown": "Onverwachte fout" }, "step": { + "reauth_confirm": { + "data": { + "password": "Wachtwoord" + }, + "title": "Integratie herauthenticeren" + }, "user": { "data": { "password": "Wachtwoord", diff --git a/homeassistant/components/litterrobot/translations/sensor.et.json b/homeassistant/components/litterrobot/translations/sensor.et.json index 98b04dfd207..ea2399dd0e2 100644 --- a/homeassistant/components/litterrobot/translations/sensor.et.json +++ b/homeassistant/components/litterrobot/translations/sensor.et.json @@ -4,6 +4,7 @@ "br": "Kaast eemaldatud", "ccc": "Puhastusts\u00fckkel on l\u00f5ppenud", "ccp": "Puhastusts\u00fckkel on pooleli", + "cd": "Kassi on m\u00e4rgatud", "csf": "Kassianduri viga", "csi": "Kassianduri h\u00e4iring", "cst": "Kassianduri ajastus", @@ -19,6 +20,8 @@ "otf": "\u00dclekoornuse viga", "p": "Ootel", "pd": "Pinch Detect", + "pwrd": "Sulgumine", + "pwru": "K\u00e4ivitumine", "rdy": "Valmis", "scf": "Kassianduri viga k\u00e4ivitamisel", "sdf": "Sahtel t\u00e4is k\u00e4ivitamisel", diff --git a/homeassistant/components/litterrobot/translations/sensor.hu.json b/homeassistant/components/litterrobot/translations/sensor.hu.json index 6c68a231bf4..85709bd209d 100644 --- a/homeassistant/components/litterrobot/translations/sensor.hu.json +++ b/homeassistant/components/litterrobot/translations/sensor.hu.json @@ -4,6 +4,7 @@ "br": "Tet\u0151 elt\u00e1vol\u00edtva", "ccc": "Tiszt\u00edt\u00e1s befejez\u0151d\u00f6tt", "ccp": "Tiszt\u00edt\u00e1s folyamatban", + "cd": "Macska \u00e9szlelve", "csf": "Macska\u00e9rz\u00e9kel\u0151 hiba", "csi": "Macska\u00e9rz\u00e9kel\u0151 megszak\u00edtva", "cst": "Macska\u00e9rz\u00e9kel\u0151 id\u0151z\u00edt\u00e9se", @@ -19,6 +20,8 @@ "otf": "T\u00falzott nyomat\u00e9k hiba", "p": "Sz\u00fcnetel", "pd": "Pinch Detect", + "pwrd": "Kikapcsol\u00e1s", + "pwru": "Bekapcsol\u00e1s", "rdy": "K\u00e9sz", "scf": "Macska\u00e9rz\u00e9kel\u0151 hiba ind\u00edt\u00e1skor", "sdf": "Ind\u00edt\u00e1skor megtelt a fi\u00f3k", diff --git a/homeassistant/components/litterrobot/translations/sensor.nl.json b/homeassistant/components/litterrobot/translations/sensor.nl.json index 6a383627aee..cf0d1537cdf 100644 --- a/homeassistant/components/litterrobot/translations/sensor.nl.json +++ b/homeassistant/components/litterrobot/translations/sensor.nl.json @@ -4,6 +4,7 @@ "br": "Motorkap verwijderd", "ccc": "Reinigingscyclus voltooid", "ccp": "Reinigingscyclus in uitvoering", + "cd": "Kat gedetecteerd", "csf": "Kattensensor fout", "csi": "Kattensensor onderbroken", "cst": "Timing kattensensor", @@ -17,6 +18,8 @@ "off": "Uit", "offline": "Offline", "p": "Gepauzeerd", + "pwrd": "Uitschakelen", + "pwru": "Opstarten", "rdy": "Gereed", "scf": "Kattensensorfout bij opstarten", "sdf": "Lade vol bij opstarten" diff --git a/homeassistant/components/litterrobot/translations/sensor.pt-BR.json b/homeassistant/components/litterrobot/translations/sensor.pt-BR.json index 9eb1dd8be8b..cf949118f54 100644 --- a/homeassistant/components/litterrobot/translations/sensor.pt-BR.json +++ b/homeassistant/components/litterrobot/translations/sensor.pt-BR.json @@ -4,6 +4,7 @@ "br": "Cap\u00f4 removido", "ccc": "Ciclo de limpeza conclu\u00eddo", "ccp": "Ciclo de limpeza em andamento", + "cd": "Gato detectado", "csf": "Falha do Sensor Cat", "csi": "Sensor Cat interrompido", "cst": "Sincroniza\u00e7\u00e3o do Sensor Cat", @@ -19,6 +20,8 @@ "otf": "Falha de sobretorque", "p": "Pausado", "pd": "Detec\u00e7\u00e3o de pin\u00e7a", + "pwrd": "Desligando", + "pwru": "Ligando", "rdy": "Pronto", "scf": "Falha do sensor Cat na inicializa\u00e7\u00e3o", "sdf": "Gaveta cheia na inicializa\u00e7\u00e3o", diff --git a/homeassistant/components/litterrobot/translations/sensor.ru.json b/homeassistant/components/litterrobot/translations/sensor.ru.json index 3a28cfe08ec..dfc5d9a9b33 100644 --- a/homeassistant/components/litterrobot/translations/sensor.ru.json +++ b/homeassistant/components/litterrobot/translations/sensor.ru.json @@ -4,6 +4,7 @@ "br": "\u041a\u043e\u0436\u0443\u0445 \u0441\u043d\u044f\u0442", "ccc": "\u0426\u0438\u043a\u043b \u043e\u0447\u0438\u0441\u0442\u043a\u0438 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d", "ccp": "\u0412\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f \u0446\u0438\u043a\u043b \u043e\u0447\u0438\u0441\u0442\u043a\u0438", + "cd": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d \u043a\u043e\u0442", "csf": "\u041d\u0435\u0438\u0441\u043f\u0440\u0430\u0432\u043d\u043e\u0441\u0442\u044c \u0434\u0430\u0442\u0447\u0438\u043a\u0430", "csi": "\u0414\u0430\u0442\u0447\u0438\u043a \u043f\u0435\u0440\u0435\u043a\u0440\u044b\u0442", "cst": "\u0412\u0440\u0435\u043c\u044f \u0441\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u043d\u0438\u044f \u0434\u0430\u0442\u0447\u0438\u043a\u0430", @@ -19,6 +20,8 @@ "otf": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0435\u0432\u044b\u0448\u0435\u043d\u0438\u044f \u043a\u0440\u0443\u0442\u044f\u0449\u0435\u0433\u043e \u043c\u043e\u043c\u0435\u043d\u0442\u0430", "p": "\u041f\u0430\u0443\u0437\u0430", "pd": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0435 \u0437\u0430\u0449\u0435\u043c\u043b\u0435\u043d\u0438\u044f", + "pwrd": "\u041e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043f\u0438\u0442\u0430\u043d\u0438\u044f", + "pwru": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043f\u0438\u0442\u0430\u043d\u0438\u044f", "rdy": "\u0413\u043e\u0442\u043e\u0432", "scf": "\u041d\u0435\u0438\u0441\u043f\u0440\u0430\u0432\u043d\u043e\u0441\u0442\u044c \u0434\u0430\u0442\u0447\u0438\u043a\u0430 \u043f\u0440\u0438 \u0437\u0430\u043f\u0443\u0441\u043a\u0435", "sdf": "\u042f\u0449\u0438\u043a \u043f\u043e\u043b\u043d\u044b\u0439 \u043f\u0440\u0438 \u0437\u0430\u043f\u0443\u0441\u043a\u0435", diff --git a/homeassistant/components/litterrobot/translations/sensor.zh-Hant.json b/homeassistant/components/litterrobot/translations/sensor.zh-Hant.json index f51c03f9c8c..e070bb69f51 100644 --- a/homeassistant/components/litterrobot/translations/sensor.zh-Hant.json +++ b/homeassistant/components/litterrobot/translations/sensor.zh-Hant.json @@ -4,6 +4,7 @@ "br": "\u4e0a\u84cb\u906d\u958b\u555f", "ccc": "\u8c93\u7802\u6e05\u7406\u5b8c\u6210", "ccp": "\u8c93\u7802\u6e05\u7406\u4e2d", + "cd": "\u5075\u6e2c\u5230\u8c93\u54aa", "csf": "\u8c93\u54aa\u611f\u6e2c\u5668\u6545\u969c", "csi": "\u8c93\u54aa\u611f\u6e2c\u5668\u906d\u4e2d\u65b7", "cst": "\u8c93\u54aa\u611f\u6e2c\u5668\u8a08\u6642", @@ -19,6 +20,8 @@ "otf": "\u8f49\u52d5\u5931\u6557", "p": "\u5df2\u66ab\u505c", "pd": "\u7570\u7269\u5075\u6e2c", + "pwrd": "\u95dc\u6a5f\u4e2d", + "pwru": "\u555f\u52d5\u4e2d", "rdy": "\u6e96\u5099\u5c31\u7dd2", "scf": "\u555f\u52d5\u6642\u8c93\u54aa\u611f\u6e2c\u5668\u5931\u6548", "sdf": "\u555f\u52d5\u6642\u6392\u5ee2\u76d2\u5df2\u6eff", diff --git a/homeassistant/components/mysensors/translations/nl.json b/homeassistant/components/mysensors/translations/nl.json index 8293e815d8c..44bd06022b9 100644 --- a/homeassistant/components/mysensors/translations/nl.json +++ b/homeassistant/components/mysensors/translations/nl.json @@ -14,6 +14,7 @@ "invalid_serial": "Ongeldige seri\u00eble poort", "invalid_subscribe_topic": "Ongeldig abonneeronderwerp", "invalid_version": "Ongeldige MySensors-versie", + "mqtt_required": "De MQTT-integratie is niet ingesteld", "not_a_number": "Voer een nummer in", "port_out_of_range": "Poortnummer moet minimaal 1 en maximaal 65535 zijn", "same_topic": "De topics abonneren en publiceren zijn hetzelfde", @@ -68,6 +69,12 @@ }, "description": "Ethernet gateway instellen" }, + "select_gateway_type": { + "menu_options": { + "gw_mqtt": "Configureer een MQTT-gateway", + "gw_serial": "Configureer een seri\u00eble gateway" + } + }, "user": { "data": { "gateway_type": "Gateway type" diff --git a/homeassistant/components/nam/translations/sensor.nl.json b/homeassistant/components/nam/translations/sensor.nl.json new file mode 100644 index 00000000000..b607f922b98 --- /dev/null +++ b/homeassistant/components/nam/translations/sensor.nl.json @@ -0,0 +1,11 @@ +{ + "state": { + "nam__caqi_level": { + "high": "Hoog", + "low": "Laag", + "medium": "Medium", + "very high": "Heel hoog", + "very low": "Heel laag" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openuv/translations/et.json b/homeassistant/components/openuv/translations/et.json index 89bfcd38318..76145f40ed0 100644 --- a/homeassistant/components/openuv/translations/et.json +++ b/homeassistant/components/openuv/translations/et.json @@ -18,6 +18,16 @@ } } }, + "issues": { + "deprecated_service_multiple_alternate_targets": { + "description": "V\u00e4rskenda k\u00f5iki automaatikaid v\u00f5i skripte, mis seda teenust kasutavad, et kasutada selle asemel teenust '{alternate_service}', mille sihtm\u00e4rgiks on \u00fcks neist olemi ID-dest: '{alternate_targets}'.", + "title": "Teenus {deprecated_service} eemaldatakse" + }, + "deprecated_service_single_alternate_target": { + "description": "Uuenda k\u00f5iki seda teenust kasutavaid automatiseerimisi v\u00f5i skripte, et need kasutaksid selle asemel teenust `{alternate_service}}, mille sihtm\u00e4rgiks on `{alternate_targets}}.", + "title": "Teenus {deprecated_service} eemaldatakse" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/openuv/translations/fr.json b/homeassistant/components/openuv/translations/fr.json index 99a616427a7..527d4d3348e 100644 --- a/homeassistant/components/openuv/translations/fr.json +++ b/homeassistant/components/openuv/translations/fr.json @@ -18,6 +18,14 @@ } } }, + "issues": { + "deprecated_service_multiple_alternate_targets": { + "title": "Le service {deprecated_service} sera bient\u00f4t supprim\u00e9" + }, + "deprecated_service_single_alternate_target": { + "title": "Le service {deprecated_service} sera bient\u00f4t supprim\u00e9" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/openuv/translations/hu.json b/homeassistant/components/openuv/translations/hu.json index e94d076782d..f6493247a99 100644 --- a/homeassistant/components/openuv/translations/hu.json +++ b/homeassistant/components/openuv/translations/hu.json @@ -18,6 +18,16 @@ } } }, + "issues": { + "deprecated_service_multiple_alternate_targets": { + "description": "Friss\u00edtsen minden olyan automatizmust vagy szkriptet, amely ezt a szolg\u00e1ltat\u00e1st haszn\u00e1lja, hogy ehelyett az `{alternate_service}` szolg\u00e1ltat\u00e1st haszn\u00e1lja c\u00e9lpontk\u00e9nt az al\u00e1bbi entit\u00e1sazonos\u00edt\u00f3k egyik\u00e9vel: `{alternate_targets}`.", + "title": "A {deprecated_service} szolg\u00e1ltat\u00e1s elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + }, + "deprecated_service_single_alternate_target": { + "description": "Friss\u00edtsen minden olyan automatizmust vagy szkriptet, amely ezt a szolg\u00e1ltat\u00e1st haszn\u00e1lja, hogy helyette az `{alternate_service}` szolg\u00e1ltat\u00e1st haszn\u00e1lja, a `{alternate_targets}` c\u00e9lpontk\u00e9nt.", + "title": "{deprecated_service} szolg\u00e1ltat\u00e1s elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/openuv/translations/pt-BR.json b/homeassistant/components/openuv/translations/pt-BR.json index 1fe9216bdad..9d0c6dd7e8a 100644 --- a/homeassistant/components/openuv/translations/pt-BR.json +++ b/homeassistant/components/openuv/translations/pt-BR.json @@ -18,6 +18,16 @@ } } }, + "issues": { + "deprecated_service_multiple_alternate_targets": { + "description": "Atualize quaisquer automa\u00e7\u00f5es ou scripts que usam este servi\u00e7o para usar o servi\u00e7o `{alternate_service}` com um destes IDs de entidade como destino: `{alternate_targets}`.", + "title": "O servi\u00e7o {deprecated_service} est\u00e1 sendo removido" + }, + "deprecated_service_single_alternate_target": { + "description": "Atualize quaisquer automa\u00e7\u00f5es ou scripts que usam este servi\u00e7o para usar o servi\u00e7o `{alternate_service}` com `{alternate_targets}` como destino.", + "title": "O servi\u00e7o {deprecated_service} est\u00e1 sendo removido" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/openuv/translations/ru.json b/homeassistant/components/openuv/translations/ru.json index e4a2fe9f49c..ce7da6af147 100644 --- a/homeassistant/components/openuv/translations/ru.json +++ b/homeassistant/components/openuv/translations/ru.json @@ -18,6 +18,16 @@ } } }, + "issues": { + "deprecated_service_multiple_alternate_targets": { + "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, \u0442\u0435\u043f\u0435\u0440\u044c \u0441\u043b\u0435\u0434\u0443\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0441\u043b\u0443\u0436\u0431\u0443 `{alternate_service}` \u0441 \u0446\u0435\u043b\u0435\u0432\u044b\u043c \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u043c `{alternate_targets}`.", + "title": "\u0421\u043b\u0443\u0436\u0431\u0430 {deprecated_service} \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + }, + "deprecated_service_single_alternate_target": { + "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, \u0442\u0435\u043f\u0435\u0440\u044c \u0441\u043b\u0435\u0434\u0443\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0441\u043b\u0443\u0436\u0431\u0443 `{alternate_service}` \u0441 \u0446\u0435\u043b\u0435\u0432\u044b\u043c \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u043c `{alternate_targets}`.", + "title": "\u0421\u043b\u0443\u0436\u0431\u0430 {deprecated_service} \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/openuv/translations/zh-Hant.json b/homeassistant/components/openuv/translations/zh-Hant.json index 16d083da340..eaeeec74e3c 100644 --- a/homeassistant/components/openuv/translations/zh-Hant.json +++ b/homeassistant/components/openuv/translations/zh-Hant.json @@ -18,6 +18,16 @@ } } }, + "issues": { + "deprecated_service_multiple_alternate_targets": { + "description": "\u4f7f\u7528\u6b64\u670d\u52d9\u4ee5\u66f4\u65b0\u4efb\u4f55\u81ea\u52d5\u5316\u6216\u8173\u672c\u3001\u4ee5\u53d6\u4ee3\u4f7f\u7528\u76ee\u6a19\u5be6\u9ad4 ID \u4e4b\u4e00\u70ba `{alternate_target}` \u4e4b `{alternate_service}` \u670d\u52d9\u3002", + "title": "{deprecated_service} \u670d\u52d9\u5373\u5c07\u79fb\u9664" + }, + "deprecated_service_single_alternate_target": { + "description": "\u4f7f\u7528\u6b64\u670d\u52d9\u4ee5\u66f4\u65b0\u4efb\u4f55\u81ea\u52d5\u5316\u6216\u8173\u672c\u3001\u4ee5\u53d6\u4ee3\u76ee\u6a19\u70ba `{alternate_target}` \u4e4b `{alternate_service}` \u670d\u52d9\u3002", + "title": "{deprecated_service} \u670d\u52d9\u5373\u5c07\u79fb\u9664" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/prusalink/translations/nl.json b/homeassistant/components/prusalink/translations/nl.json new file mode 100644 index 00000000000..2a250129c18 --- /dev/null +++ b/homeassistant/components/prusalink/translations/nl.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "not_supported": "Alleen PrusaLink API V2 wordt ondersteund", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "api_key": "API-sleutel", + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/sensor.nl.json b/homeassistant/components/prusalink/translations/sensor.nl.json new file mode 100644 index 00000000000..cd02b3cefd8 --- /dev/null +++ b/homeassistant/components/prusalink/translations/sensor.nl.json @@ -0,0 +1,9 @@ +{ + "state": { + "prusalink__printer_state": { + "cancelling": "Annuleren", + "idle": "Inactief", + "paused": "Gepauzeerd" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qingping/translations/nl.json b/homeassistant/components/qingping/translations/nl.json new file mode 100644 index 00000000000..281d6feff46 --- /dev/null +++ b/homeassistant/components/qingping/translations/nl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "already_in_progress": "De configuratie is momenteel al bezig", + "no_devices_found": "Geen apparaten gevonden op het netwerk", + "not_supported": "Apparaat is niet ondersteund" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Wilt u {name} instellen?" + }, + "user": { + "data": { + "address": "Apparaat" + }, + "description": "Kies een apparaat om in te stellen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rfxtrx/translations/hu.json b/homeassistant/components/rfxtrx/translations/hu.json index ebdde5c9428..10a659ff3c2 100644 --- a/homeassistant/components/rfxtrx/translations/hu.json +++ b/homeassistant/components/rfxtrx/translations/hu.json @@ -78,7 +78,7 @@ "off_delay": "Kikapcsol\u00e1si k\u00e9sleltet\u00e9s", "off_delay_enabled": "Kikapcsol\u00e1si k\u00e9sleltet\u00e9s enged\u00e9lyez\u00e9se", "replace_device": "V\u00e1lassza ki a cser\u00e9lni k\u00edv\u00e1nt eszk\u00f6zt", - "venetian_blind_mode": "Velencei red\u0151ny \u00fczemm\u00f3d" + "venetian_blind_mode": "Reluxa m\u00f3d" }, "title": "Konfigur\u00e1lja az eszk\u00f6z be\u00e1ll\u00edt\u00e1sait" } diff --git a/homeassistant/components/schedule/translations/nl.json b/homeassistant/components/schedule/translations/nl.json index ea585424fdb..38c6968ce14 100644 --- a/homeassistant/components/schedule/translations/nl.json +++ b/homeassistant/components/schedule/translations/nl.json @@ -4,5 +4,6 @@ "off": "Uit", "on": "Aan" } - } + }, + "title": "Schema" } \ No newline at end of file diff --git a/homeassistant/components/sensorpro/translations/nl.json b/homeassistant/components/sensorpro/translations/nl.json new file mode 100644 index 00000000000..281d6feff46 --- /dev/null +++ b/homeassistant/components/sensorpro/translations/nl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "already_in_progress": "De configuratie is momenteel al bezig", + "no_devices_found": "Geen apparaten gevonden op het netwerk", + "not_supported": "Apparaat is niet ondersteund" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Wilt u {name} instellen?" + }, + "user": { + "data": { + "address": "Apparaat" + }, + "description": "Kies een apparaat om in te stellen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/et.json b/homeassistant/components/simplisafe/translations/et.json index 03356fc7d6e..d1403a88303 100644 --- a/homeassistant/components/simplisafe/translations/et.json +++ b/homeassistant/components/simplisafe/translations/et.json @@ -39,6 +39,12 @@ } } }, + "issues": { + "deprecated_service": { + "description": "Uuenda k\u00f5iki seda teenust kasutavaid automatiseerimisi v\u00f5i skripte, et need kasutaksid selle asemel teenust `{alternate_service}}, mille siht\u00fcksuse ID on `{alternate_target}}. Seej\u00e4rel kl\u00f5psa allpool nuppu ESITA, et m\u00e4rkida see probleem lahendatuks.", + "title": "Teenus {deprecated_service} eemaldatakse" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/simplisafe/translations/hu.json b/homeassistant/components/simplisafe/translations/hu.json index 78c32522a3d..1ac24d8468c 100644 --- a/homeassistant/components/simplisafe/translations/hu.json +++ b/homeassistant/components/simplisafe/translations/hu.json @@ -39,6 +39,12 @@ } } }, + "issues": { + "deprecated_service": { + "description": "Friss\u00edtsen minden olyan automatiz\u00e1l\u00e1st vagy szkriptet, amely ezt a szolg\u00e1ltat\u00e1st haszn\u00e1lja, hogy helyette a(z) `{alternate_service}` szolg\u00e1ltat\u00e1st haszn\u00e1lja a(z) `{alternate_target}` entit\u00e1ssal. Ezut\u00e1n kattintson az al\u00e1bbi MEHET gombra a probl\u00e9ma megoldottk\u00e9nt val\u00f3 megjel\u00f6l\u00e9s\u00e9hez.", + "title": "A {deprecated_service} szolg\u00e1ltat\u00e1s elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/somfy_mylink/translations/hu.json b/homeassistant/components/somfy_mylink/translations/hu.json index c3348b1f628..666f274001f 100644 --- a/homeassistant/components/somfy_mylink/translations/hu.json +++ b/homeassistant/components/somfy_mylink/translations/hu.json @@ -33,7 +33,7 @@ }, "target_config": { "data": { - "reverse": "A bor\u00edt\u00f3 megfordult" + "reverse": "Ford\u00edtott \u00e1rny\u00e9kol\u00f3" }, "description": "`{target_name}` be\u00e1ll\u00edt\u00e1sainak konfigur\u00e1l\u00e1sa", "title": "MyLink \u00e1rny\u00e9kol\u00f3 konfigur\u00e1l\u00e1sa" diff --git a/homeassistant/components/switch_as_x/translations/hu.json b/homeassistant/components/switch_as_x/translations/hu.json index 9f3d49ec348..09f7fbaa89b 100644 --- a/homeassistant/components/switch_as_x/translations/hu.json +++ b/homeassistant/components/switch_as_x/translations/hu.json @@ -6,7 +6,7 @@ "entity_id": "Kapcsol\u00f3", "target_domain": "\u00daj t\u00edpus" }, - "description": "V\u00e1lassza ki azt a kapcsol\u00f3t, amelyet meg szeretne jelen\u00edteni a Home Assistantban l\u00e1mpak\u00e9nt, red\u0151nyk\u00e9nt vagy b\u00e1rmi m\u00e1sk\u00e9nt. Az eredeti kapcsol\u00f3 el lesz rejtve." + "description": "V\u00e1lassza ki azt a kapcsol\u00f3t, amelyet meg szeretne jelen\u00edteni a Home Assistantban l\u00e1mpak\u00e9nt, \u00e1rny\u00e9kol\u00f3k\u00e9nt vagy b\u00e1rmi m\u00e1sk\u00e9nt. Az eredeti kapcsol\u00f3 el lesz rejtve." } } }, diff --git a/homeassistant/components/tasmota/translations/hu.json b/homeassistant/components/tasmota/translations/hu.json index 990bde11d58..6316dadeb13 100644 --- a/homeassistant/components/tasmota/translations/hu.json +++ b/homeassistant/components/tasmota/translations/hu.json @@ -16,5 +16,15 @@ "description": "Szeretn\u00e9 b\u00e1ll\u00edtani a Tasmota-t?" } } + }, + "issues": { + "topic_duplicated": { + "description": "T\u00f6bb Tasmota eszk\u00f6z haszn\u00e1lja a {topic} topikot.\n\nTasmota eszk\u00f6z\u00f6k ezzel a probl\u00e9m\u00e1val: {offenders}.", + "title": "T\u00f6bb Tasmota eszk\u00f6z is ugyanazt a topikot haszn\u00e1lja" + }, + "topic_no_prefix": { + "description": "A {ip} IP-c\u00edm\u0171 Tasmota eszk\u00f6z {name} nem tartalmazza a `%prefix%`elemet a fulltopikban. \n\nAz eszk\u00f6z\u00f6k entit\u00e1sai a konfigur\u00e1ci\u00f3 kijav\u00edt\u00e1s\u00e1ig le vannak tiltva.", + "title": "{name} Tasmota eszk\u00f6z \u00e9rv\u00e9nytelen MQTT topikkal van be\u00e1llitva" + } } } \ No newline at end of file diff --git a/homeassistant/components/tomorrowio/translations/sensor.ja.json b/homeassistant/components/tomorrowio/translations/sensor.ja.json index 286be339435..bc5d7ef1f1c 100644 --- a/homeassistant/components/tomorrowio/translations/sensor.ja.json +++ b/homeassistant/components/tomorrowio/translations/sensor.ja.json @@ -3,7 +3,7 @@ "tomorrowio__health_concern": { "good": "\u826f\u3044", "hazardous": "\u5371\u967a", - "moderate": "\u9069\u5ea6", + "moderate": "\u4e2d\u7a0b\u5ea6", "unhealthy": "\u4e0d\u5065\u5eb7", "unhealthy_for_sensitive_groups": "\u654f\u611f\u306a\u30b0\u30eb\u30fc\u30d7\u306b\u3068\u3063\u3066\u4e0d\u5065\u5eb7", "very_unhealthy": "\u3068\u3066\u3082\u4e0d\u5065\u5eb7" diff --git a/homeassistant/components/volvooncall/translations/nl.json b/homeassistant/components/volvooncall/translations/nl.json index 1e942c8694c..c9aa1582d52 100644 --- a/homeassistant/components/volvooncall/translations/nl.json +++ b/homeassistant/components/volvooncall/translations/nl.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Account is al geconfigureerd", "reauth_successful": "Herauthenticatie geslaagd" }, "error": { @@ -11,6 +12,7 @@ "user": { "data": { "password": "Wachtwoord", + "region": "Regio", "username": "Gebruikersnaam" } } diff --git a/homeassistant/components/weather/translations/ja.json b/homeassistant/components/weather/translations/ja.json index 787791be8cd..02f2dd848a3 100644 --- a/homeassistant/components/weather/translations/ja.json +++ b/homeassistant/components/weather/translations/ja.json @@ -7,7 +7,7 @@ "fog": "\u9727", "hail": "\u96f9", "lightning": "\u96f7", - "lightning-rainy": "\u96f7\u96e8", + "lightning-rainy": "\u96f7\u3001\u96e8", "partlycloudy": "\u6674\u308c\u6642\u3005\u66c7\u308a", "pouring": "\u5927\u96e8", "rainy": "\u96e8", diff --git a/homeassistant/components/xiaomi_miio/translations/select.nl.json b/homeassistant/components/xiaomi_miio/translations/select.nl.json index eaa69b3170c..0e93df956e9 100644 --- a/homeassistant/components/xiaomi_miio/translations/select.nl.json +++ b/homeassistant/components/xiaomi_miio/translations/select.nl.json @@ -4,6 +4,11 @@ "bright": "Helder", "dim": "Dim", "off": "Uit" + }, + "xiaomi_miio__ptc_level": { + "high": "Hoog", + "low": "Laag", + "medium": "Medium" } } } \ No newline at end of file diff --git a/homeassistant/components/yalexs_ble/translations/nl.json b/homeassistant/components/yalexs_ble/translations/nl.json index 04ee25942f6..f8cb3d26757 100644 --- a/homeassistant/components/yalexs_ble/translations/nl.json +++ b/homeassistant/components/yalexs_ble/translations/nl.json @@ -1,9 +1,20 @@ { "config": { + "abort": { + "already_in_progress": "De configuratie is momenteel al bezig" + }, "error": { "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie", "unknown": "Onverwachte fout" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "Bluetooth-adres" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/zha/translations/nl.json b/homeassistant/components/zha/translations/nl.json index 1de0e24def3..2955326add4 100644 --- a/homeassistant/components/zha/translations/nl.json +++ b/homeassistant/components/zha/translations/nl.json @@ -29,6 +29,9 @@ "description": "Voer poortspecifieke instellingen in", "title": "Instellingen" }, + "upload_manual_backup": { + "title": "Upload een handmatige back-up" + }, "user": { "data": { "path": "Serieel apparaatpad" @@ -108,5 +111,15 @@ "remote_button_short_release": "\"{subtype}\" knop losgelaten", "remote_button_triple_press": "\" {subtype} \" knop driemaal geklikt" } + }, + "options": { + "abort": { + "not_zha_device": "Dit apparaat is niet een zha-apparaat.", + "usb_probe_failed": "Kon het USB apparaat niet onderzoeken" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken" + }, + "flow_title": "{name}" } } \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/en.json b/homeassistant/components/zwave_js/translations/en.json index 843f2aaf284..9224e27d90b 100644 --- a/homeassistant/components/zwave_js/translations/en.json +++ b/homeassistant/components/zwave_js/translations/en.json @@ -91,6 +91,12 @@ "zwave_js.value_updated.value": "Value change on a Z-Wave JS Value" } }, + "issues": { + "invalid_server_version": { + "description": "The version of Z-Wave JS Server you are currently running is too old for this version of Home Assistant. Please update the Z-Wave JS Server to the latest version to fix this issue.", + "title": "Newer version of Z-Wave JS Server needed" + } + }, "options": { "abort": { "addon_get_discovery_info_failed": "Failed to get Z-Wave JS add-on discovery info.", From 9655f30146aab006b76755283267719b658d21a6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 18 Sep 2022 20:09:45 -0500 Subject: [PATCH 532/955] Handle Modalias missing from the bluetooth adapter details on older BlueZ (#78715) --- homeassistant/components/bluetooth/const.py | 2 +- homeassistant/components/bluetooth/util.py | 2 +- tests/components/bluetooth/conftest.py | 24 +++++++++++ tests/components/bluetooth/test_init.py | 47 +++++++++++++++++++++ 4 files changed, 73 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/bluetooth/const.py b/homeassistant/components/bluetooth/const.py index 3174603f08e..891e6d8be82 100644 --- a/homeassistant/components/bluetooth/const.py +++ b/homeassistant/components/bluetooth/const.py @@ -58,7 +58,7 @@ class AdapterDetails(TypedDict, total=False): address: str sw_version: str - hw_version: str + hw_version: str | None passive_scan: bool diff --git a/homeassistant/components/bluetooth/util.py b/homeassistant/components/bluetooth/util.py index d04685f34d9..860428a6106 100644 --- a/homeassistant/components/bluetooth/util.py +++ b/homeassistant/components/bluetooth/util.py @@ -79,7 +79,7 @@ async def async_get_bluetooth_adapters() -> dict[str, AdapterDetails]: adapters[adapter] = AdapterDetails( address=adapter1["Address"], sw_version=adapter1["Name"], # This is actually the BlueZ version - hw_version=adapter1["Modalias"], + hw_version=adapter1.get("Modalias"), passive_scan="org.bluez.AdvertisementMonitorManager1" in details, ) return adapters diff --git a/tests/components/bluetooth/conftest.py b/tests/components/bluetooth/conftest.py index 58b6e596629..857af1e7eaa 100644 --- a/tests/components/bluetooth/conftest.py +++ b/tests/components/bluetooth/conftest.py @@ -97,3 +97,27 @@ def two_adapters_fixture(bluez_dbus_mock): }, ): yield + + +@pytest.fixture(name="one_adapter_old_bluez") +def one_adapter_old_bluez(bluez_dbus_mock): + """Fixture that mocks two adapters on Linux.""" + with patch( + "homeassistant.components.bluetooth.platform.system", return_value="Linux" + ), patch( + "homeassistant.components.bluetooth.scanner.platform.system", + return_value="Linux", + ), 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.43", + } + }, + }, + ): + yield diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index 32feb3d7b0f..a3045291286 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -116,6 +116,53 @@ async def test_setup_and_stop_passive(hass, mock_bleak_scanner_start, one_adapte } +async def test_setup_and_stop_old_bluez( + hass, mock_bleak_scanner_start, one_adapter_old_bluez +): + """Test we and setup and stop the scanner the passive scanner with older bluez.""" + entry = MockConfigEntry( + domain=bluetooth.DOMAIN, + data={}, + options={}, + unique_id="00:00:00:00:00:01", + ) + entry.add_to_hass(hass) + init_kwargs = None + + class MockBleakScanner: + def __init__(self, *args, **kwargs): + """Init the scanner.""" + nonlocal init_kwargs + init_kwargs = kwargs + + async def start(self, *args, **kwargs): + """Start the scanner.""" + + async def stop(self, *args, **kwargs): + """Stop the scanner.""" + + def register_detection_callback(self, *args, **kwargs): + """Register a callback.""" + + with patch( + "homeassistant.components.bluetooth.scanner.OriginalBleakScanner", + MockBleakScanner, + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + + assert init_kwargs == { + "adapter": "hci0", + "scanning_mode": "active", + } + + async def test_setup_and_stop_no_bluetooth(hass, caplog, macos_adapter): """Test we fail gracefully when bluetooth is not available.""" mock_bt = [ From aa0cbf0afefedafa5266b78261909c543e678a40 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Mon, 19 Sep 2022 00:12:38 -0400 Subject: [PATCH 533/955] Add tests for LitterRobot sensors (#78638) --- tests/components/litterrobot/common.py | 122 +++++++++++++++++++- tests/components/litterrobot/conftest.py | 35 +++++- tests/components/litterrobot/test_sensor.py | 43 ++++++- 3 files changed, 191 insertions(+), 9 deletions(-) diff --git a/tests/components/litterrobot/common.py b/tests/components/litterrobot/common.py index 19a6b5617c7..f5b4e32a1e1 100644 --- a/tests/components/litterrobot/common.py +++ b/tests/components/litterrobot/common.py @@ -1,6 +1,4 @@ """Common utils for Litter-Robot tests.""" -from datetime import datetime - from homeassistant.components.litterrobot import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME @@ -11,7 +9,7 @@ ROBOT_NAME = "Test" ROBOT_SERIAL = "LR3C012345" ROBOT_DATA = { "powerStatus": "AC", - "lastSeen": datetime.now().isoformat(), + "lastSeen": "2022-09-17T13:06:37.884Z", "cleanCycleWaitTimeMinutes": "7", "unitStatus": "RDY", "litterRobotNickname": ROBOT_NAME, @@ -24,5 +22,123 @@ ROBOT_DATA = { "nightLightActive": "1", "sleepModeActive": "112:50:19", } +ROBOT_4_DATA = { + "name": ROBOT_NAME, + "serial": "LR4C010001", + "userId": "1234567", + "espFirmware": "1.1.50", + "picFirmwareVersion": "10512.2560.2.53", + "laserBoardFirmwareVersion": "4.0.65.4", + "wifiRssi": -53.0, + "unitPowerType": "AC", + "catWeight": 12.0, + "unitTimezone": "America/New_York", + "unitTime": None, + "cleanCycleWaitTime": 15, + "isKeypadLockout": False, + "nightLightMode": "OFF", + "nightLightBrightness": 85, + "isPanelSleepMode": False, + "panelSleepTime": 0, + "panelWakeTime": 0, + "weekdaySleepModeEnabled": { + "Sunday": {"sleepTime": 0, "wakeTime": 0, "isEnabled": False}, + "Monday": {"sleepTime": 0, "wakeTime": 180, "isEnabled": True}, + "Tuesday": {"sleepTime": 0, "wakeTime": 180, "isEnabled": True}, + "Wednesday": {"sleepTime": 0, "wakeTime": 180, "isEnabled": True}, + "Thursday": {"sleepTime": 0, "wakeTime": 180, "isEnabled": True}, + "Friday": {"sleepTime": 0, "wakeTime": 180, "isEnabled": True}, + "Saturday": {"sleepTime": 0, "wakeTime": 0, "isEnabled": False}, + }, + "unitPowerStatus": "ON", + "sleepStatus": "WAKE", + "robotStatus": "ROBOT_IDLE", + "globeMotorFaultStatus": "FAULT_CLEAR", + "pinchStatus": "CLEAR", + "catDetect": "CAT_DETECT_CLEAR", + "isBonnetRemoved": False, + "isNightLightLEDOn": False, + "odometerPowerCycles": 8, + "odometerCleanCycles": 158, + "odometerEmptyCycles": 1, + "odometerFilterCycles": 0, + "isDFIResetPending": False, + "DFINumberOfCycles": 104, + "DFILevelPercent": 76, + "isDFIFull": True, + "DFIFullCounter": 3, + "DFITriggerCount": 42, + "litterLevel": 460, + "DFILevelMM": 115, + "isCatDetectPending": False, + "globeMotorRetractFaultStatus": "FAULT_CLEAR", + "robotCycleStatus": "CYCLE_IDLE", + "robotCycleState": "CYCLE_STATE_WAIT_ON", + "weightSensor": -3.0, + "isOnline": True, + "isOnboarded": True, + "isProvisioned": True, + "isDebugModeActive": False, + "lastSeen": "2022-09-17T12:06:37.884Z", + "sessionId": "abcdef12-e358-4b6c-9022-012345678912", + "setupDateTime": "2022-08-28T17:01:12.644Z", + "isFirmwareUpdateTriggered": False, + "firmwareUpdateStatus": "NONE", + "wifiModeStatus": "ROUTER_CONNECTED", + "isUSBPowerOn": True, + "USBFaultStatus": "CLEAR", + "isDFIPartialFull": True, +} +FEEDER_ROBOT_DATA = { + "id": 1, + "name": ROBOT_NAME, + "serial": "RF1C000001", + "timezone": "America/Denver", + "isEighthCupEnabled": False, + "created_at": "2021-12-15T06:45:00.000000+00:00", + "household_id": 1, + "state": { + "id": 1, + "info": { + "level": 2, + "power": True, + "online": True, + "acPower": True, + "dcPower": False, + "gravity": False, + "chuteFull": False, + "fwVersion": "1.0.0", + "onBoarded": True, + "unitMeals": 0, + "motorJammed": False, + "chuteFullExt": False, + "panelLockout": False, + "unitPortions": 0, + "autoNightMode": True, + "mealInsertSize": 1, + }, + "updated_at": "2022-09-08T15:07:00.000000+00:00", + }, + "feeding_snack": [ + {"timestamp": "2022-09-04T03:03:00.000000+00:00", "amount": 0.125}, + {"timestamp": "2022-08-30T16:34:00.000000+00:00", "amount": 0.25}, + ], + "feeding_meal": [ + { + "timestamp": "2022-09-08T18:00:00.000000+00:00", + "amount": 0.125, + "meal_name": "Lunch", + "meal_number": 2, + "meal_total_portions": 2, + }, + { + "timestamp": "2022-09-08T12:00:00.000000+00:00", + "amount": 0.125, + "meal_name": "Breakfast", + "meal_number": 1, + "meal_total_portions": 1, + }, + ], +} VACUUM_ENTITY_ID = "vacuum.test_litter_box" diff --git a/tests/components/litterrobot/conftest.py b/tests/components/litterrobot/conftest.py index 34132ec66d6..ce80471797d 100644 --- a/tests/components/litterrobot/conftest.py +++ b/tests/components/litterrobot/conftest.py @@ -4,26 +4,35 @@ from __future__ import annotations from typing import Any from unittest.mock import AsyncMock, MagicMock, patch -from pylitterbot import Account, LitterRobot3, Robot +from pylitterbot import Account, FeederRobot, LitterRobot3, LitterRobot4, Robot from pylitterbot.exceptions import InvalidCommandException import pytest from homeassistant.components import litterrobot from homeassistant.core import HomeAssistant -from .common import CONFIG, ROBOT_DATA +from .common import CONFIG, FEEDER_ROBOT_DATA, ROBOT_4_DATA, ROBOT_DATA from tests.common import MockConfigEntry def create_mock_robot( - robot_data: dict | None, account: Account, side_effect: Any | None = None + robot_data: dict | None, + account: Account, + v4: bool, + feeder: bool, + side_effect: Any | None = None, ) -> Robot: """Create a mock Litter-Robot device.""" if not robot_data: robot_data = {} - robot = LitterRobot3(data={**ROBOT_DATA, **robot_data}, account=account) + if v4: + robot = LitterRobot4(data={**ROBOT_4_DATA, **robot_data}, account=account) + elif feeder: + robot = FeederRobot(data={**FEEDER_ROBOT_DATA, **robot_data}, account=account) + else: + robot = LitterRobot3(data={**ROBOT_DATA, **robot_data}, account=account) robot.start_cleaning = AsyncMock(side_effect=side_effect) robot.set_power_status = AsyncMock(side_effect=side_effect) robot.reset_waste_drawer = AsyncMock(side_effect=side_effect) @@ -39,13 +48,17 @@ def create_mock_account( robot_data: dict | None = None, side_effect: Any | None = None, skip_robots: bool = False, + v4: bool = False, + feeder: bool = False, ) -> MagicMock: """Create a mock Litter-Robot account.""" account = MagicMock(spec=Account) account.connect = AsyncMock() account.refresh_robots = AsyncMock() account.robots = ( - [] if skip_robots else [create_mock_robot(robot_data, account, side_effect)] + [] + if skip_robots + else [create_mock_robot(robot_data, account, v4, feeder, side_effect)] ) return account @@ -56,6 +69,18 @@ def mock_account() -> MagicMock: return create_mock_account() +@pytest.fixture +def mock_account_with_litterrobot_4() -> MagicMock: + """Mock account with Litter-Robot 4.""" + return create_mock_account(v4=True) + + +@pytest.fixture +def mock_account_with_feederrobot() -> MagicMock: + """Mock account with Feeder-Robot.""" + return create_mock_account(feeder=True) + + @pytest.fixture def mock_account_with_no_robots() -> MagicMock: """Mock a Litter-Robot account.""" diff --git a/tests/components/litterrobot/test_sensor.py b/tests/components/litterrobot/test_sensor.py index ce91541f0d7..77668a9e592 100644 --- a/tests/components/litterrobot/test_sensor.py +++ b/tests/components/litterrobot/test_sensor.py @@ -2,12 +2,13 @@ from unittest.mock import MagicMock from homeassistant.components.sensor import DOMAIN as PLATFORM_DOMAIN, SensorDeviceClass -from homeassistant.const import PERCENTAGE, STATE_UNKNOWN +from homeassistant.const import MASS_POUNDS, PERCENTAGE, STATE_UNKNOWN from homeassistant.core import HomeAssistant from .conftest import setup_integration WASTE_DRAWER_ENTITY_ID = "sensor.test_waste_drawer" +SLEEP_END_TIME_ENTITY_ID = "sensor.test_sleep_mode_end_time" SLEEP_START_TIME_ENTITY_ID = "sensor.test_sleep_mode_start_time" @@ -36,6 +37,10 @@ async def test_sleep_time_sensor_with_sleep_disabled( assert sensor.state == STATE_UNKNOWN assert sensor.attributes["device_class"] == SensorDeviceClass.TIMESTAMP + sensor = hass.states.get(SLEEP_END_TIME_ENTITY_ID) + assert sensor.state == STATE_UNKNOWN + assert sensor.attributes["device_class"] == SensorDeviceClass.TIMESTAMP + async def test_gauge_icon() -> None: """Test icon generator for gauge sensor.""" @@ -59,3 +64,39 @@ async def test_gauge_icon() -> None: assert icon_for_gauge_level(40, 10) == GAUGE_LOW assert icon_for_gauge_level(80, 10) == GAUGE assert icon_for_gauge_level(100, 10) == GAUGE_FULL + + +async def test_litter_robot_sensor( + hass: HomeAssistant, mock_account_with_litterrobot_4: MagicMock +) -> None: + """Tests Litter-Robot sensors.""" + await setup_integration(hass, mock_account_with_litterrobot_4, PLATFORM_DOMAIN) + + sensor = hass.states.get(SLEEP_START_TIME_ENTITY_ID) + assert sensor.state == "2022-09-19T04:00:00+00:00" + assert sensor.attributes["device_class"] == SensorDeviceClass.TIMESTAMP + sensor = hass.states.get(SLEEP_END_TIME_ENTITY_ID) + assert sensor.state == "2022-09-16T07:00:00+00:00" + assert sensor.attributes["device_class"] == SensorDeviceClass.TIMESTAMP + sensor = hass.states.get("sensor.test_last_seen") + assert sensor.state == "2022-09-17T12:06:37+00:00" + assert sensor.attributes["device_class"] == SensorDeviceClass.TIMESTAMP + sensor = hass.states.get("sensor.test_status_code") + assert sensor.state == "dfs" + assert sensor.attributes["device_class"] == "litterrobot__status_code" + sensor = hass.states.get("sensor.test_litter_level") + assert sensor.state == "70.0" + assert sensor.attributes["unit_of_measurement"] == PERCENTAGE + sensor = hass.states.get("sensor.test_pet_weight") + assert sensor.state == "12.0" + assert sensor.attributes["unit_of_measurement"] == MASS_POUNDS + + +async def test_feeder_robot_sensor( + hass: HomeAssistant, mock_account_with_feederrobot: MagicMock +) -> None: + """Tests Feeder-Robot sensors.""" + await setup_integration(hass, mock_account_with_feederrobot, PLATFORM_DOMAIN) + sensor = hass.states.get("sensor.test_food_level") + assert sensor.state == "20" + assert sensor.attributes["unit_of_measurement"] == PERCENTAGE From a809e18559ab345733e127c6a6624dc0437e3350 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 19 Sep 2022 09:31:57 +0200 Subject: [PATCH 534/955] Apply hass-relative-import to tests (d-h) (#78730) --- tests/components/dexcom/test_config_flow.py | 3 ++- tests/components/dexcom/test_init.py | 3 ++- tests/components/dexcom/test_sensor.py | 2 +- tests/components/directv/test_config_flow.py | 3 ++- tests/components/directv/test_init.py | 3 ++- tests/components/directv/test_media_player.py | 3 ++- tests/components/directv/test_remote.py | 3 ++- tests/components/elmax/conftest.py | 3 ++- tests/components/elmax/test_config_flow.py | 5 +++-- tests/components/freedompro/conftest.py | 3 ++- tests/components/freedompro/test_binary_sensor.py | 3 ++- tests/components/freedompro/test_climate.py | 3 ++- tests/components/freedompro/test_config_flow.py | 2 +- tests/components/freedompro/test_cover.py | 3 ++- tests/components/freedompro/test_fan.py | 3 ++- tests/components/freedompro/test_lock.py | 3 ++- tests/components/freedompro/test_sensor.py | 3 ++- tests/components/freedompro/test_switch.py | 3 ++- tests/components/gdacs/test_geo_location.py | 3 ++- tests/components/gdacs/test_sensor.py | 3 ++- tests/components/geonetnz_quakes/test_geo_location.py | 3 ++- tests/components/geonetnz_quakes/test_sensor.py | 3 ++- tests/components/geonetnz_volcano/test_sensor.py | 3 ++- tests/components/gios/test_config_flow.py | 3 ++- tests/components/gios/test_diagnostics.py | 3 ++- tests/components/gios/test_init.py | 3 +-- tests/components/gios/test_sensor.py | 3 ++- tests/components/google_travel_time/test_config_flow.py | 2 +- tests/components/group/test_init.py | 3 ++- tests/components/home_plus_control/test_config_flow.py | 7 ++----- tests/components/home_plus_control/test_init.py | 6 +----- tests/components/home_plus_control/test_switch.py | 7 ++----- tests/components/huisbaasje/test_init.py | 3 ++- tests/components/huisbaasje/test_sensor.py | 6 ++---- 34 files changed, 64 insertions(+), 51 deletions(-) diff --git a/tests/components/dexcom/test_config_flow.py b/tests/components/dexcom/test_config_flow.py index b20321277fb..471c71acaba 100644 --- a/tests/components/dexcom/test_config_flow.py +++ b/tests/components/dexcom/test_config_flow.py @@ -7,8 +7,9 @@ from homeassistant import config_entries, data_entry_flow from homeassistant.components.dexcom.const import DOMAIN, MG_DL, MMOL_L from homeassistant.const import CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME +from . import CONFIG + from tests.common import MockConfigEntry -from tests.components.dexcom import CONFIG async def test_form(hass): diff --git a/tests/components/dexcom/test_init.py b/tests/components/dexcom/test_init.py index 2509ba25f33..79727f79ff6 100644 --- a/tests/components/dexcom/test_init.py +++ b/tests/components/dexcom/test_init.py @@ -6,8 +6,9 @@ from pydexcom import AccountError, SessionError from homeassistant.components.dexcom.const import DOMAIN from homeassistant.config_entries import ConfigEntryState +from . import CONFIG, init_integration + from tests.common import MockConfigEntry -from tests.components.dexcom import CONFIG, init_integration async def test_setup_entry_account_error(hass): diff --git a/tests/components/dexcom/test_sensor.py b/tests/components/dexcom/test_sensor.py index ff1256a9bc0..95d3b2f51a5 100644 --- a/tests/components/dexcom/test_sensor.py +++ b/tests/components/dexcom/test_sensor.py @@ -12,7 +12,7 @@ from homeassistant.const import ( ) from homeassistant.helpers.entity_component import async_update_entity -from tests.components.dexcom import GLUCOSE_READING, init_integration +from . import GLUCOSE_READING, init_integration async def test_sensors(hass): diff --git a/tests/components/directv/test_config_flow.py b/tests/components/directv/test_config_flow.py index 7ef6bdde69d..8017dc290c9 100644 --- a/tests/components/directv/test_config_flow.py +++ b/tests/components/directv/test_config_flow.py @@ -11,7 +11,7 @@ from homeassistant.const import CONF_HOST, CONF_NAME, CONF_SOURCE from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from tests.components.directv import ( +from . import ( HOST, MOCK_SSDP_DISCOVERY_INFO, MOCK_USER_INPUT, @@ -20,6 +20,7 @@ from tests.components.directv import ( mock_connection, setup_integration, ) + from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/directv/test_init.py b/tests/components/directv/test_init.py index 3ef151f4257..cdfc9b1bcad 100644 --- a/tests/components/directv/test_init.py +++ b/tests/components/directv/test_init.py @@ -3,7 +3,8 @@ from homeassistant.components.directv.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant -from tests.components.directv import setup_integration +from . import setup_integration + from tests.test_util.aiohttp import AiohttpClientMocker # pylint: disable=redefined-outer-name diff --git a/tests/components/directv/test_media_player.py b/tests/components/directv/test_media_player.py index cfade8bc4e7..1c03758af72 100644 --- a/tests/components/directv/test_media_player.py +++ b/tests/components/directv/test_media_player.py @@ -58,7 +58,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.util import dt as dt_util -from tests.components.directv import setup_integration +from . import setup_integration + from tests.test_util.aiohttp import AiohttpClientMocker ATTR_UNIQUE_ID = "unique_id" diff --git a/tests/components/directv/test_remote.py b/tests/components/directv/test_remote.py index 37eec1324c0..1dcd6c88047 100644 --- a/tests/components/directv/test_remote.py +++ b/tests/components/directv/test_remote.py @@ -10,7 +10,8 @@ from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_O from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from tests.components.directv import setup_integration +from . import setup_integration + from tests.test_util.aiohttp import AiohttpClientMocker ATTR_UNIQUE_ID = "unique_id" diff --git a/tests/components/elmax/conftest.py b/tests/components/elmax/conftest.py index 17ad58b6292..70e3af76702 100644 --- a/tests/components/elmax/conftest.py +++ b/tests/components/elmax/conftest.py @@ -11,8 +11,9 @@ from httpx import Response import pytest import respx +from . import MOCK_PANEL_ID, MOCK_PANEL_PIN + from tests.common import load_fixture -from tests.components.elmax import MOCK_PANEL_ID, MOCK_PANEL_PIN @pytest.fixture(autouse=True) diff --git a/tests/components/elmax/test_config_flow.py b/tests/components/elmax/test_config_flow.py index e4e9889aadd..c2c91bc5a35 100644 --- a/tests/components/elmax/test_config_flow.py +++ b/tests/components/elmax/test_config_flow.py @@ -14,8 +14,7 @@ from homeassistant.components.elmax.const import ( ) from homeassistant.config_entries import SOURCE_REAUTH -from tests.common import MockConfigEntry -from tests.components.elmax import ( +from . import ( MOCK_PANEL_ID, MOCK_PANEL_NAME, MOCK_PANEL_PIN, @@ -23,6 +22,8 @@ from tests.components.elmax import ( MOCK_USERNAME, ) +from tests.common import MockConfigEntry + CONF_POLLING = "polling" diff --git a/tests/components/freedompro/conftest.py b/tests/components/freedompro/conftest.py index 804dc6d1933..bc3d7311058 100644 --- a/tests/components/freedompro/conftest.py +++ b/tests/components/freedompro/conftest.py @@ -9,8 +9,9 @@ import pytest from homeassistant.components.freedompro.const import DOMAIN +from .const import DEVICES, DEVICES_STATE + from tests.common import MockConfigEntry -from tests.components.freedompro.const import DEVICES, DEVICES_STATE @pytest.fixture(autouse=True) diff --git a/tests/components/freedompro/test_binary_sensor.py b/tests/components/freedompro/test_binary_sensor.py index 459484dbc45..f43857038ef 100644 --- a/tests/components/freedompro/test_binary_sensor.py +++ b/tests/components/freedompro/test_binary_sensor.py @@ -8,8 +8,9 @@ from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.util.dt import utcnow +from .conftest import get_states_response_for_uid + from tests.common import async_fire_time_changed -from tests.components.freedompro.conftest import get_states_response_for_uid @pytest.mark.parametrize( diff --git a/tests/components/freedompro/test_climate.py b/tests/components/freedompro/test_climate.py index 40e68f1e03b..da65abffeed 100644 --- a/tests/components/freedompro/test_climate.py +++ b/tests/components/freedompro/test_climate.py @@ -20,8 +20,9 @@ from homeassistant.const import ATTR_ENTITY_ID from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.util.dt import utcnow +from .conftest import get_states_response_for_uid + from tests.common import async_fire_time_changed -from tests.components.freedompro.conftest import get_states_response_for_uid uid = "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*TWMYQKL3UVED4HSIIB9GXJWJZBQCXG-9VE-N2IUAIWI" diff --git a/tests/components/freedompro/test_config_flow.py b/tests/components/freedompro/test_config_flow.py index d1804437a59..d80bd22736c 100644 --- a/tests/components/freedompro/test_config_flow.py +++ b/tests/components/freedompro/test_config_flow.py @@ -6,7 +6,7 @@ from homeassistant.components.freedompro.const import DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_API_KEY -from tests.components.freedompro.const import DEVICES +from .const import DEVICES VALID_CONFIG = { CONF_API_KEY: "ksdjfgslkjdfksjdfksjgfksjd", diff --git a/tests/components/freedompro/test_cover.py b/tests/components/freedompro/test_cover.py index ed14da90789..ecec83cd411 100644 --- a/tests/components/freedompro/test_cover.py +++ b/tests/components/freedompro/test_cover.py @@ -17,8 +17,9 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity_component import async_update_entity from homeassistant.util.dt import utcnow +from .conftest import get_states_response_for_uid + from tests.common import async_fire_time_changed -from tests.components.freedompro.conftest import get_states_response_for_uid @pytest.mark.parametrize( diff --git a/tests/components/freedompro/test_fan.py b/tests/components/freedompro/test_fan.py index 3192398a323..75b798ea13a 100644 --- a/tests/components/freedompro/test_fan.py +++ b/tests/components/freedompro/test_fan.py @@ -13,8 +13,9 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity_component import async_update_entity from homeassistant.util.dt import utcnow +from .conftest import get_states_response_for_uid + from tests.common import async_fire_time_changed -from tests.components.freedompro.conftest import get_states_response_for_uid uid = "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*ILYH1E3DWZOVMNEUIMDYMNLOW-LFRQFDPWWJOVHVDOS" diff --git a/tests/components/freedompro/test_lock.py b/tests/components/freedompro/test_lock.py index 44811faffe8..1680b7f4f35 100644 --- a/tests/components/freedompro/test_lock.py +++ b/tests/components/freedompro/test_lock.py @@ -12,8 +12,9 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity_component import async_update_entity from homeassistant.util.dt import utcnow +from .conftest import get_states_response_for_uid + from tests.common import async_fire_time_changed -from tests.components.freedompro.conftest import get_states_response_for_uid uid = "2WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*2VAS3HTWINNZ5N6HVEIPDJ6NX85P2-AM-GSYWUCNPU0" diff --git a/tests/components/freedompro/test_sensor.py b/tests/components/freedompro/test_sensor.py index d73506e2c6f..0405c5393ce 100644 --- a/tests/components/freedompro/test_sensor.py +++ b/tests/components/freedompro/test_sensor.py @@ -7,8 +7,9 @@ import pytest from homeassistant.helpers import entity_registry as er from homeassistant.util.dt import utcnow +from .conftest import get_states_response_for_uid + from tests.common import async_fire_time_changed -from tests.components.freedompro.conftest import get_states_response_for_uid @pytest.mark.parametrize( diff --git a/tests/components/freedompro/test_switch.py b/tests/components/freedompro/test_switch.py index f71ec4b896f..3e26276282a 100644 --- a/tests/components/freedompro/test_switch.py +++ b/tests/components/freedompro/test_switch.py @@ -8,8 +8,9 @@ from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_component import async_update_entity from homeassistant.util.dt import utcnow +from .conftest import get_states_response_for_uid + from tests.common import async_fire_time_changed -from tests.components.freedompro.conftest import get_states_response_for_uid uid = "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*1JKU1MVWHQL-Z9SCUS85VFXMRGNDCDNDDUVVDKBU31W" diff --git a/tests/components/gdacs/test_geo_location.py b/tests/components/gdacs/test_geo_location.py index 8fcd5db24a6..d55141ebe56 100644 --- a/tests/components/gdacs/test_geo_location.py +++ b/tests/components/gdacs/test_geo_location.py @@ -36,8 +36,9 @@ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import IMPERIAL_SYSTEM +from . import _generate_mock_feed_entry + from tests.common import async_fire_time_changed -from tests.components.gdacs import _generate_mock_feed_entry CONFIG = {gdacs.DOMAIN: {CONF_RADIUS: 200}} diff --git a/tests/components/gdacs/test_sensor.py b/tests/components/gdacs/test_sensor.py index 11898d79877..8aafbcf864e 100644 --- a/tests/components/gdacs/test_sensor.py +++ b/tests/components/gdacs/test_sensor.py @@ -22,8 +22,9 @@ from homeassistant.const import ( from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from . import _generate_mock_feed_entry + from tests.common import async_fire_time_changed -from tests.components.gdacs import _generate_mock_feed_entry CONFIG = {gdacs.DOMAIN: {CONF_RADIUS: 200}} diff --git a/tests/components/geonetnz_quakes/test_geo_location.py b/tests/components/geonetnz_quakes/test_geo_location.py index 0690da5bf7b..130964b4eeb 100644 --- a/tests/components/geonetnz_quakes/test_geo_location.py +++ b/tests/components/geonetnz_quakes/test_geo_location.py @@ -30,8 +30,9 @@ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import IMPERIAL_SYSTEM +from . import _generate_mock_feed_entry + from tests.common import async_fire_time_changed -from tests.components.geonetnz_quakes import _generate_mock_feed_entry CONFIG = {geonetnz_quakes.DOMAIN: {CONF_RADIUS: 200}} diff --git a/tests/components/geonetnz_quakes/test_sensor.py b/tests/components/geonetnz_quakes/test_sensor.py index a88a878a03c..9221c2fe848 100644 --- a/tests/components/geonetnz_quakes/test_sensor.py +++ b/tests/components/geonetnz_quakes/test_sensor.py @@ -23,8 +23,9 @@ from homeassistant.const import ( from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from . import _generate_mock_feed_entry + from tests.common import async_fire_time_changed -from tests.components.geonetnz_quakes import _generate_mock_feed_entry CONFIG = {geonetnz_quakes.DOMAIN: {CONF_RADIUS: 200}} diff --git a/tests/components/geonetnz_volcano/test_sensor.py b/tests/components/geonetnz_volcano/test_sensor.py index d3d2d5dcc2e..dbb6834c596 100644 --- a/tests/components/geonetnz_volcano/test_sensor.py +++ b/tests/components/geonetnz_volcano/test_sensor.py @@ -25,8 +25,9 @@ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import IMPERIAL_SYSTEM +from . import _generate_mock_feed_entry + from tests.common import async_fire_time_changed -from tests.components.geonetnz_volcano import _generate_mock_feed_entry CONFIG = {geonetnz_volcano.DOMAIN: {CONF_RADIUS: 200}} diff --git a/tests/components/gios/test_config_flow.py b/tests/components/gios/test_config_flow.py index 8ebb514a4d3..3a0d57ce0d4 100644 --- a/tests/components/gios/test_config_flow.py +++ b/tests/components/gios/test_config_flow.py @@ -9,8 +9,9 @@ from homeassistant.components.gios import config_flow from homeassistant.components.gios.const import CONF_STATION_ID from homeassistant.const import CONF_NAME +from . import STATIONS + from tests.common import load_fixture -from tests.components.gios import STATIONS CONFIG = { CONF_NAME: "Foo", diff --git a/tests/components/gios/test_diagnostics.py b/tests/components/gios/test_diagnostics.py index e72880feed7..2168457f628 100644 --- a/tests/components/gios/test_diagnostics.py +++ b/tests/components/gios/test_diagnostics.py @@ -1,9 +1,10 @@ """Test GIOS diagnostics.""" import json +from . import init_integration + from tests.common import load_fixture from tests.components.diagnostics import get_diagnostics_for_config_entry -from tests.components.gios import init_integration async def test_entry_diagnostics(hass, hass_client): diff --git a/tests/components/gios/test_init.py b/tests/components/gios/test_init.py index f0b3f660e8d..f3a628160f0 100644 --- a/tests/components/gios/test_init.py +++ b/tests/components/gios/test_init.py @@ -8,10 +8,9 @@ from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_UNAVAILABLE from homeassistant.helpers import entity_registry as er -from . import STATIONS +from . import STATIONS, init_integration from tests.common import MockConfigEntry, load_fixture, mock_device_registry -from tests.components.gios import init_integration async def test_async_setup_entry(hass): diff --git a/tests/components/gios/test_sensor.py b/tests/components/gios/test_sensor.py index 6603f67e359..4cdb7208dd3 100644 --- a/tests/components/gios/test_sensor.py +++ b/tests/components/gios/test_sensor.py @@ -28,8 +28,9 @@ from homeassistant.const import ( from homeassistant.helpers import entity_registry as er from homeassistant.util.dt import utcnow +from . import init_integration + from tests.common import async_fire_time_changed, load_fixture -from tests.components.gios import init_integration async def test_sensor(hass): diff --git a/tests/components/google_travel_time/test_config_flow.py b/tests/components/google_travel_time/test_config_flow.py index 7f6371c1446..4fe5f797d45 100644 --- a/tests/components/google_travel_time/test_config_flow.py +++ b/tests/components/google_travel_time/test_config_flow.py @@ -27,7 +27,7 @@ from homeassistant.const import ( CONF_UNIT_SYSTEM_IMPERIAL, ) -from tests.components.google_travel_time.const import MOCK_CONFIG +from .const import MOCK_CONFIG @pytest.mark.usefixtures("validate_config_entry", "bypass_setup") diff --git a/tests/components/group/test_init.py b/tests/components/group/test_init.py index 945f6555789..ec5732503e8 100644 --- a/tests/components/group/test_init.py +++ b/tests/components/group/test_init.py @@ -26,8 +26,9 @@ from homeassistant.helpers import entity_registry as er from homeassistant.helpers.event import TRACK_STATE_CHANGE_CALLBACKS from homeassistant.setup import async_setup_component +from . import common + from tests.common import MockConfigEntry, assert_setup_component -from tests.components.group import common async def test_setup_group_with_mixed_groupable_states(hass): diff --git a/tests/components/home_plus_control/test_config_flow.py b/tests/components/home_plus_control/test_config_flow.py index e86362c3e11..f4f7e1143a5 100644 --- a/tests/components/home_plus_control/test_config_flow.py +++ b/tests/components/home_plus_control/test_config_flow.py @@ -12,12 +12,9 @@ from homeassistant.components.home_plus_control.const import ( from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.helpers import config_entry_oauth2_flow +from .conftest import CLIENT_ID, CLIENT_SECRET, SUBSCRIPTION_KEY + from tests.common import MockConfigEntry -from tests.components.home_plus_control.conftest import ( - CLIENT_ID, - CLIENT_SECRET, - SUBSCRIPTION_KEY, -) async def test_full_flow( diff --git a/tests/components/home_plus_control/test_init.py b/tests/components/home_plus_control/test_init.py index 4da913047a2..bed3dcdce18 100644 --- a/tests/components/home_plus_control/test_init.py +++ b/tests/components/home_plus_control/test_init.py @@ -8,11 +8,7 @@ from homeassistant.components.home_plus_control.const import ( ) from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET -from tests.components.home_plus_control.conftest import ( - CLIENT_ID, - CLIENT_SECRET, - SUBSCRIPTION_KEY, -) +from .conftest import CLIENT_ID, CLIENT_SECRET, SUBSCRIPTION_KEY async def test_loading(hass, mock_config_entry): diff --git a/tests/components/home_plus_control/test_switch.py b/tests/components/home_plus_control/test_switch.py index 44a969392bf..f788e7fa4b4 100644 --- a/tests/components/home_plus_control/test_switch.py +++ b/tests/components/home_plus_control/test_switch.py @@ -18,12 +18,9 @@ from homeassistant.const import ( ) from homeassistant.helpers import device_registry as dr, entity_registry as er +from .conftest import CLIENT_ID, CLIENT_SECRET, SUBSCRIPTION_KEY + from tests.common import async_fire_time_changed -from tests.components.home_plus_control.conftest import ( - CLIENT_ID, - CLIENT_SECRET, - SUBSCRIPTION_KEY, -) def entity_assertions( diff --git a/tests/components/huisbaasje/test_init.py b/tests/components/huisbaasje/test_init.py index ba2022f7583..859cfc4df83 100644 --- a/tests/components/huisbaasje/test_init.py +++ b/tests/components/huisbaasje/test_init.py @@ -9,8 +9,9 @@ from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME, STATE_UNA from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from .test_data import MOCK_CURRENT_MEASUREMENTS + from tests.common import MockConfigEntry -from tests.components.huisbaasje.test_data import MOCK_CURRENT_MEASUREMENTS async def test_setup(hass: HomeAssistant): diff --git a/tests/components/huisbaasje/test_sensor.py b/tests/components/huisbaasje/test_sensor.py index 4418127a0d1..84e1f71071c 100644 --- a/tests/components/huisbaasje/test_sensor.py +++ b/tests/components/huisbaasje/test_sensor.py @@ -21,11 +21,9 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant +from .test_data import MOCK_CURRENT_MEASUREMENTS, MOCK_LIMITED_CURRENT_MEASUREMENTS + from tests.common import MockConfigEntry -from tests.components.huisbaasje.test_data import ( - MOCK_CURRENT_MEASUREMENTS, - MOCK_LIMITED_CURRENT_MEASUREMENTS, -) async def test_setup_entry(hass: HomeAssistant): From 00dd27ef1b5ee2279694cf6a61fb2163def041db Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 19 Sep 2022 09:46:59 +0200 Subject: [PATCH 535/955] Apply hass-relative-import to tests (i-r) (#78732) --- tests/components/iaqualink/test_init.py | 3 ++- tests/components/iaqualink/test_utils.py | 2 +- tests/components/image_processing/test_init.py | 3 ++- tests/components/intellifire/test_config_flow.py | 3 ++- tests/components/ipp/test_init.py | 3 ++- tests/components/ipp/test_sensor.py | 3 ++- tests/components/knx/test_diagnostic.py | 3 ++- tests/components/minio/test_minio.py | 2 +- tests/components/modern_forms/test_binary_sensor.py | 3 ++- tests/components/modern_forms/test_fan.py | 3 ++- tests/components/modern_forms/test_init.py | 6 ++---- tests/components/modern_forms/test_light.py | 3 ++- tests/components/modern_forms/test_sensor.py | 3 ++- tests/components/modern_forms/test_switch.py | 3 ++- tests/components/nam/test_button.py | 2 +- tests/components/nam/test_diagnostics.py | 3 ++- tests/components/nam/test_init.py | 3 ++- tests/components/nam/test_sensor.py | 3 +-- tests/components/nextdns/test_diagnostics.py | 3 ++- tests/components/nightscout/test_config_flow.py | 7 ++----- tests/components/nightscout/test_init.py | 3 ++- tests/components/nightscout/test_sensor.py | 2 +- tests/components/nws/conftest.py | 2 +- tests/components/nws/test_init.py | 3 ++- tests/components/nws/test_sensor.py | 5 +++-- tests/components/nws/test_weather.py | 5 +++-- tests/components/renault/test_services.py | 3 ++- tests/components/rflink/test_binary_sensor.py | 3 ++- tests/components/rflink/test_cover.py | 3 ++- tests/components/rflink/test_light.py | 3 ++- tests/components/rflink/test_sensor.py | 2 +- tests/components/rflink/test_switch.py | 3 ++- tests/components/rfxtrx/test_binary_sensor.py | 3 ++- tests/components/rfxtrx/test_cover.py | 3 ++- tests/components/rfxtrx/test_device_action.py | 3 ++- tests/components/rfxtrx/test_device_trigger.py | 3 ++- tests/components/rfxtrx/test_init.py | 3 ++- tests/components/rfxtrx/test_light.py | 3 ++- tests/components/rfxtrx/test_sensor.py | 3 ++- tests/components/rfxtrx/test_switch.py | 3 ++- tests/components/roku/test_binary_sensor.py | 3 ++- tests/components/roku/test_config_flow.py | 5 +++-- tests/components/roku/test_remote.py | 3 ++- tests/components/roku/test_sensor.py | 3 ++- tests/components/ruckus_unleashed/test_config_flow.py | 3 ++- tests/components/ruckus_unleashed/test_device_tracker.py | 5 +++-- tests/components/ruckus_unleashed/test_init.py | 2 +- 47 files changed, 90 insertions(+), 59 deletions(-) diff --git a/tests/components/iaqualink/test_init.py b/tests/components/iaqualink/test_init.py index 3a35804f447..bd2e072d213 100644 --- a/tests/components/iaqualink/test_init.py +++ b/tests/components/iaqualink/test_init.py @@ -24,8 +24,9 @@ from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ATTR_ASSUMED_STATE, STATE_ON, STATE_UNAVAILABLE from homeassistant.util import dt as dt_util +from .conftest import get_aqualink_device, get_aqualink_system + from tests.common import async_fire_time_changed -from tests.components.iaqualink.conftest import get_aqualink_device, get_aqualink_system async def _ffwd_next_update_interval(hass): diff --git a/tests/components/iaqualink/test_utils.py b/tests/components/iaqualink/test_utils.py index 56b239c0d9f..b4462d26dbc 100644 --- a/tests/components/iaqualink/test_utils.py +++ b/tests/components/iaqualink/test_utils.py @@ -6,7 +6,7 @@ import pytest from homeassistant.components.iaqualink.utils import await_or_reraise from homeassistant.exceptions import HomeAssistantError -from tests.components.iaqualink.conftest import async_raises, async_returns +from .conftest import async_raises, async_returns async def test_await_or_reraise(hass): diff --git a/tests/components/image_processing/test_init.py b/tests/components/image_processing/test_init.py index ed8d49e8ddb..9953df041ae 100644 --- a/tests/components/image_processing/test_init.py +++ b/tests/components/image_processing/test_init.py @@ -9,8 +9,9 @@ from homeassistant.const import ATTR_ENTITY_PICTURE from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component +from . import common + from tests.common import assert_setup_component, async_capture_events -from tests.components.image_processing import common @pytest.fixture diff --git a/tests/components/intellifire/test_config_flow.py b/tests/components/intellifire/test_config_flow.py index 95e5d735c35..9e5d5627a94 100644 --- a/tests/components/intellifire/test_config_flow.py +++ b/tests/components/intellifire/test_config_flow.py @@ -11,8 +11,9 @@ from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PASSWORD, CONF_USE from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from .conftest import mock_api_connection_error + from tests.common import MockConfigEntry -from tests.components.intellifire.conftest import mock_api_connection_error @patch.multiple( diff --git a/tests/components/ipp/test_init.py b/tests/components/ipp/test_init.py index 5caffc62d7b..32060b4df86 100644 --- a/tests/components/ipp/test_init.py +++ b/tests/components/ipp/test_init.py @@ -3,7 +3,8 @@ from homeassistant.components.ipp.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant -from tests.components.ipp import init_integration +from . import init_integration + from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/ipp/test_sensor.py b/tests/components/ipp/test_sensor.py index 5531259c597..d772c6e9163 100644 --- a/tests/components/ipp/test_sensor.py +++ b/tests/components/ipp/test_sensor.py @@ -9,7 +9,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.util import dt as dt_util -from tests.components.ipp import init_integration, mock_connection +from . import init_integration, mock_connection + from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/knx/test_diagnostic.py b/tests/components/knx/test_diagnostic.py index e2d93f17498..5f4dd01bcb6 100644 --- a/tests/components/knx/test_diagnostic.py +++ b/tests/components/knx/test_diagnostic.py @@ -22,9 +22,10 @@ from homeassistant.components.knx.const import ( ) from homeassistant.core import HomeAssistant +from .conftest import KNXTestKit + from tests.common import MockConfigEntry from tests.components.diagnostics import get_diagnostics_for_config_entry -from tests.components.knx.conftest import KNXTestKit async def test_diagnostics( diff --git a/tests/components/minio/test_minio.py b/tests/components/minio/test_minio.py index d6668470b55..cebca17b24d 100644 --- a/tests/components/minio/test_minio.py +++ b/tests/components/minio/test_minio.py @@ -19,7 +19,7 @@ from homeassistant.components.minio import ( from homeassistant.core import callback from homeassistant.setup import async_setup_component -from tests.components.minio.common import TEST_EVENT +from .common import TEST_EVENT @pytest.fixture(name="minio_client") diff --git a/tests/components/modern_forms/test_binary_sensor.py b/tests/components/modern_forms/test_binary_sensor.py index bc32e309958..6b64beb4f1a 100644 --- a/tests/components/modern_forms/test_binary_sensor.py +++ b/tests/components/modern_forms/test_binary_sensor.py @@ -5,7 +5,8 @@ from homeassistant.const import ATTR_ICON from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from tests.components.modern_forms import init_integration +from . import init_integration + from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/modern_forms/test_fan.py b/tests/components/modern_forms/test_fan.py index c9b6c66bb62..b445946706f 100644 --- a/tests/components/modern_forms/test_fan.py +++ b/tests/components/modern_forms/test_fan.py @@ -28,7 +28,8 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from tests.components.modern_forms import init_integration +from . import init_integration + from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/modern_forms/test_init.py b/tests/components/modern_forms/test_init.py index 518355ac18b..fd6ff495470 100644 --- a/tests/components/modern_forms/test_init.py +++ b/tests/components/modern_forms/test_init.py @@ -8,10 +8,8 @@ from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from tests.components.modern_forms import ( - init_integration, - modern_forms_no_light_call_mock, -) +from . import init_integration, modern_forms_no_light_call_mock + from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/modern_forms/test_light.py b/tests/components/modern_forms/test_light.py index 29725ab4bcd..2d1fc1ca56d 100644 --- a/tests/components/modern_forms/test_light.py +++ b/tests/components/modern_forms/test_light.py @@ -21,7 +21,8 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from tests.components.modern_forms import init_integration +from . import init_integration + from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/modern_forms/test_sensor.py b/tests/components/modern_forms/test_sensor.py index 638b3ddbacf..7e3914cd7d9 100644 --- a/tests/components/modern_forms/test_sensor.py +++ b/tests/components/modern_forms/test_sensor.py @@ -6,7 +6,8 @@ from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_ICON from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from tests.components.modern_forms import init_integration, modern_forms_timers_set_mock +from . import init_integration, modern_forms_timers_set_mock + from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/modern_forms/test_switch.py b/tests/components/modern_forms/test_switch.py index 963264e3ca1..56f7edebcdd 100644 --- a/tests/components/modern_forms/test_switch.py +++ b/tests/components/modern_forms/test_switch.py @@ -15,7 +15,8 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from tests.components.modern_forms import init_integration +from . import init_integration + from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/nam/test_button.py b/tests/components/nam/test_button.py index c8ed00b3376..5037d9c8720 100644 --- a/tests/components/nam/test_button.py +++ b/tests/components/nam/test_button.py @@ -6,7 +6,7 @@ from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, STATE_UNKNOWN from homeassistant.helpers import entity_registry as er from homeassistant.util import dt as dt_util -from tests.components.nam import init_integration +from . import init_integration async def test_button(hass): diff --git a/tests/components/nam/test_diagnostics.py b/tests/components/nam/test_diagnostics.py index ce7b6d59e78..321b1785038 100644 --- a/tests/components/nam/test_diagnostics.py +++ b/tests/components/nam/test_diagnostics.py @@ -1,9 +1,10 @@ """Test NAM diagnostics.""" import json +from . import init_integration + from tests.common import load_fixture from tests.components.diagnostics import get_diagnostics_for_config_entry -from tests.components.nam import init_integration async def test_entry_diagnostics(hass, hass_client): diff --git a/tests/components/nam/test_init.py b/tests/components/nam/test_init.py index 9eac901d693..b6f278d4e94 100644 --- a/tests/components/nam/test_init.py +++ b/tests/components/nam/test_init.py @@ -9,8 +9,9 @@ from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_UNAVAILABLE from homeassistant.helpers import entity_registry as er +from . import init_integration + from tests.common import MockConfigEntry -from tests.components.nam import init_integration async def test_async_setup_entry(hass): diff --git a/tests/components/nam/test_sensor.py b/tests/components/nam/test_sensor.py index 28eaea54186..bee4c515cd0 100644 --- a/tests/components/nam/test_sensor.py +++ b/tests/components/nam/test_sensor.py @@ -28,10 +28,9 @@ from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow -from . import INCOMPLETE_NAM_DATA, nam_data +from . import INCOMPLETE_NAM_DATA, init_integration, nam_data from tests.common import async_fire_time_changed -from tests.components.nam import init_integration async def test_sensor(hass): diff --git a/tests/components/nextdns/test_diagnostics.py b/tests/components/nextdns/test_diagnostics.py index 21c030274cc..6e79ac5c92a 100644 --- a/tests/components/nextdns/test_diagnostics.py +++ b/tests/components/nextdns/test_diagnostics.py @@ -7,9 +7,10 @@ from aiohttp import ClientSession from homeassistant.components.diagnostics import REDACTED from homeassistant.core import HomeAssistant +from . import init_integration + from tests.common import load_fixture from tests.components.diagnostics import get_diagnostics_for_config_entry -from tests.components.nextdns import init_integration async def test_entry_diagnostics( diff --git a/tests/components/nightscout/test_config_flow.py b/tests/components/nightscout/test_config_flow.py index 9a2b070c20c..7439f0b6b1c 100644 --- a/tests/components/nightscout/test_config_flow.py +++ b/tests/components/nightscout/test_config_flow.py @@ -9,12 +9,9 @@ from homeassistant.components.nightscout.const import DOMAIN from homeassistant.components.nightscout.utils import hash_from_url from homeassistant.const import CONF_URL +from . import GLUCOSE_READINGS, SERVER_STATUS, SERVER_STATUS_STATUS_ONLY + from tests.common import MockConfigEntry -from tests.components.nightscout import ( - GLUCOSE_READINGS, - SERVER_STATUS, - SERVER_STATUS_STATUS_ONLY, -) CONFIG = {CONF_URL: "https://some.url:1234"} diff --git a/tests/components/nightscout/test_init.py b/tests/components/nightscout/test_init.py index 04824139e20..782f90ea8d9 100644 --- a/tests/components/nightscout/test_init.py +++ b/tests/components/nightscout/test_init.py @@ -7,8 +7,9 @@ from homeassistant.components.nightscout.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_URL +from . import init_integration + from tests.common import MockConfigEntry -from tests.components.nightscout import init_integration async def test_unload_entry(hass): diff --git a/tests/components/nightscout/test_sensor.py b/tests/components/nightscout/test_sensor.py index 5e73c75d93c..cd0c39ac61e 100644 --- a/tests/components/nightscout/test_sensor.py +++ b/tests/components/nightscout/test_sensor.py @@ -7,7 +7,7 @@ from homeassistant.components.nightscout.const import ( ) from homeassistant.const import ATTR_DATE, ATTR_ICON, STATE_UNAVAILABLE -from tests.components.nightscout import ( +from . import ( GLUCOSE_READINGS, init_integration, init_integration_empty_response, diff --git a/tests/components/nws/conftest.py b/tests/components/nws/conftest.py index 98ac9191e0d..3ec9b3d6867 100644 --- a/tests/components/nws/conftest.py +++ b/tests/components/nws/conftest.py @@ -3,7 +3,7 @@ from unittest.mock import AsyncMock, patch import pytest -from tests.components.nws.const import DEFAULT_FORECAST, DEFAULT_OBSERVATION +from .const import DEFAULT_FORECAST, DEFAULT_OBSERVATION @pytest.fixture() diff --git a/tests/components/nws/test_init.py b/tests/components/nws/test_init.py index 01a203aa07b..f6b5fef9c4a 100644 --- a/tests/components/nws/test_init.py +++ b/tests/components/nws/test_init.py @@ -3,8 +3,9 @@ from homeassistant.components.nws.const import DOMAIN from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN from homeassistant.const import STATE_UNAVAILABLE +from .const import NWS_CONFIG + from tests.common import MockConfigEntry -from tests.components.nws.const import NWS_CONFIG async def test_unload_entry(hass, mock_simple_nws): diff --git a/tests/components/nws/test_sensor.py b/tests/components/nws/test_sensor.py index 5a55907e53b..78d39575522 100644 --- a/tests/components/nws/test_sensor.py +++ b/tests/components/nws/test_sensor.py @@ -8,8 +8,7 @@ from homeassistant.helpers import entity_registry as er from homeassistant.util import slugify from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM -from tests.common import MockConfigEntry -from tests.components.nws.const import ( +from .const import ( EXPECTED_FORECAST_IMPERIAL, EXPECTED_FORECAST_METRIC, NONE_OBSERVATION, @@ -18,6 +17,8 @@ from tests.components.nws.const import ( SENSOR_EXPECTED_OBSERVATION_METRIC, ) +from tests.common import MockConfigEntry + @pytest.mark.parametrize( "units,result_observation,result_forecast", diff --git a/tests/components/nws/test_weather.py b/tests/components/nws/test_weather.py index 78ab7eb4ac5..3f3e9a649f3 100644 --- a/tests/components/nws/test_weather.py +++ b/tests/components/nws/test_weather.py @@ -17,8 +17,7 @@ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM -from tests.common import MockConfigEntry, async_fire_time_changed -from tests.components.nws.const import ( +from .const import ( EXPECTED_FORECAST_IMPERIAL, EXPECTED_FORECAST_METRIC, NONE_FORECAST, @@ -28,6 +27,8 @@ from tests.components.nws.const import ( WEATHER_EXPECTED_OBSERVATION_METRIC, ) +from tests.common import MockConfigEntry, async_fire_time_changed + @pytest.mark.parametrize( "units,result_observation,result_forecast", diff --git a/tests/components/renault/test_services.py b/tests/components/renault/test_services.py index b7748cafb5d..b45273c581b 100644 --- a/tests/components/renault/test_services.py +++ b/tests/components/renault/test_services.py @@ -29,8 +29,9 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr +from .const import MOCK_VEHICLES + from tests.common import load_fixture -from tests.components.renault.const import MOCK_VEHICLES pytestmark = pytest.mark.usefixtures("patch_renault_account", "patch_get_vehicles") diff --git a/tests/components/rflink/test_binary_sensor.py b/tests/components/rflink/test_binary_sensor.py index c8d701f300f..d806e51a833 100644 --- a/tests/components/rflink/test_binary_sensor.py +++ b/tests/components/rflink/test_binary_sensor.py @@ -19,8 +19,9 @@ from homeassistant.const import ( from homeassistant.core import CoreState, State, callback import homeassistant.util.dt as dt_util +from .test_init import mock_rflink + from tests.common import async_fire_time_changed, mock_restore_cache -from tests.components.rflink.test_init import mock_rflink DOMAIN = "binary_sensor" diff --git a/tests/components/rflink/test_cover.py b/tests/components/rflink/test_cover.py index 1dac064d778..56ceab05091 100644 --- a/tests/components/rflink/test_cover.py +++ b/tests/components/rflink/test_cover.py @@ -14,8 +14,9 @@ from homeassistant.const import ( ) from homeassistant.core import CoreState, State, callback +from .test_init import mock_rflink + from tests.common import mock_restore_cache -from tests.components.rflink.test_init import mock_rflink DOMAIN = "cover" diff --git a/tests/components/rflink/test_light.py b/tests/components/rflink/test_light.py index 5f9672ac9fc..662f6affd18 100644 --- a/tests/components/rflink/test_light.py +++ b/tests/components/rflink/test_light.py @@ -15,8 +15,9 @@ from homeassistant.const import ( ) from homeassistant.core import CoreState, State, callback +from .test_init import mock_rflink + from tests.common import mock_restore_cache -from tests.components.rflink.test_init import mock_rflink DOMAIN = "light" diff --git a/tests/components/rflink/test_sensor.py b/tests/components/rflink/test_sensor.py index d18076a372a..13d7e4b300d 100644 --- a/tests/components/rflink/test_sensor.py +++ b/tests/components/rflink/test_sensor.py @@ -19,7 +19,7 @@ from homeassistant.const import ( TEMP_CELSIUS, ) -from tests.components.rflink.test_init import mock_rflink +from .test_init import mock_rflink DOMAIN = "sensor" diff --git a/tests/components/rflink/test_switch.py b/tests/components/rflink/test_switch.py index ef6bac55f21..7598301e5c9 100644 --- a/tests/components/rflink/test_switch.py +++ b/tests/components/rflink/test_switch.py @@ -15,8 +15,9 @@ from homeassistant.const import ( ) from homeassistant.core import CoreState, State, callback +from .test_init import mock_rflink + from tests.common import mock_restore_cache -from tests.components.rflink.test_init import mock_rflink DOMAIN = "switch" diff --git a/tests/components/rfxtrx/test_binary_sensor.py b/tests/components/rfxtrx/test_binary_sensor.py index 175b455da6b..a6643d159d0 100644 --- a/tests/components/rfxtrx/test_binary_sensor.py +++ b/tests/components/rfxtrx/test_binary_sensor.py @@ -6,8 +6,9 @@ from homeassistant.components.rfxtrx.const import ATTR_EVENT from homeassistant.const import STATE_UNKNOWN from homeassistant.core import State +from .conftest import create_rfx_test_cfg + from tests.common import MockConfigEntry, mock_restore_cache -from tests.components.rfxtrx.conftest import create_rfx_test_cfg EVENT_SMOKE_DETECTOR_PANIC = "08200300a109000670" EVENT_SMOKE_DETECTOR_NO_PANIC = "08200300a109000770" diff --git a/tests/components/rfxtrx/test_cover.py b/tests/components/rfxtrx/test_cover.py index 3be41d9233e..32e3f4d160c 100644 --- a/tests/components/rfxtrx/test_cover.py +++ b/tests/components/rfxtrx/test_cover.py @@ -7,8 +7,9 @@ from homeassistant.components.rfxtrx import DOMAIN from homeassistant.core import State from homeassistant.exceptions import HomeAssistantError +from .conftest import create_rfx_test_cfg + from tests.common import MockConfigEntry, mock_restore_cache -from tests.components.rfxtrx.conftest import create_rfx_test_cfg async def test_one_cover(hass, rfxtrx): diff --git a/tests/components/rfxtrx/test_device_action.py b/tests/components/rfxtrx/test_device_action.py index 6b61e166b2a..7dda64bbe62 100644 --- a/tests/components/rfxtrx/test_device_action.py +++ b/tests/components/rfxtrx/test_device_action.py @@ -12,6 +12,8 @@ from homeassistant.components.rfxtrx import DOMAIN from homeassistant.helpers.device_registry import DeviceRegistry from homeassistant.setup import async_setup_component +from .conftest import create_rfx_test_cfg + from tests.common import ( MockConfigEntry, assert_lists_same, @@ -19,7 +21,6 @@ from tests.common import ( mock_device_registry, mock_registry, ) -from tests.components.rfxtrx.conftest import create_rfx_test_cfg @pytest.fixture(name="device_reg") diff --git a/tests/components/rfxtrx/test_device_trigger.py b/tests/components/rfxtrx/test_device_trigger.py index 8e5ee27504b..b783d10ed27 100644 --- a/tests/components/rfxtrx/test_device_trigger.py +++ b/tests/components/rfxtrx/test_device_trigger.py @@ -11,6 +11,8 @@ from homeassistant.components.rfxtrx import DOMAIN from homeassistant.helpers.device_registry import DeviceRegistry from homeassistant.setup import async_setup_component +from .conftest import create_rfx_test_cfg + from tests.common import ( MockConfigEntry, assert_lists_same, @@ -18,7 +20,6 @@ from tests.common import ( async_mock_service, mock_device_registry, ) -from tests.components.rfxtrx.conftest import create_rfx_test_cfg class EventTestData(NamedTuple): diff --git a/tests/components/rfxtrx/test_init.py b/tests/components/rfxtrx/test_init.py index aff4a25770a..390856ffb44 100644 --- a/tests/components/rfxtrx/test_init.py +++ b/tests/components/rfxtrx/test_init.py @@ -11,8 +11,9 @@ from homeassistant.core import callback from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component +from .conftest import create_rfx_test_cfg, setup_rfx_test_cfg + from tests.common import MockConfigEntry -from tests.components.rfxtrx.conftest import create_rfx_test_cfg, setup_rfx_test_cfg SOME_PROTOCOLS = ["ac", "arc"] diff --git a/tests/components/rfxtrx/test_light.py b/tests/components/rfxtrx/test_light.py index a6ff96662fb..e72c4738ea3 100644 --- a/tests/components/rfxtrx/test_light.py +++ b/tests/components/rfxtrx/test_light.py @@ -8,8 +8,9 @@ from homeassistant.components.rfxtrx import DOMAIN from homeassistant.const import STATE_UNKNOWN from homeassistant.core import State +from .conftest import create_rfx_test_cfg + from tests.common import MockConfigEntry, mock_restore_cache -from tests.components.rfxtrx.conftest import create_rfx_test_cfg async def test_one_light(hass, rfxtrx): diff --git a/tests/components/rfxtrx/test_sensor.py b/tests/components/rfxtrx/test_sensor.py index 8ba0104cf50..cd562037713 100644 --- a/tests/components/rfxtrx/test_sensor.py +++ b/tests/components/rfxtrx/test_sensor.py @@ -11,8 +11,9 @@ from homeassistant.const import ( ) from homeassistant.core import State +from .conftest import create_rfx_test_cfg + from tests.common import MockConfigEntry, mock_restore_cache -from tests.components.rfxtrx.conftest import create_rfx_test_cfg async def test_default_config(hass, rfxtrx): diff --git a/tests/components/rfxtrx/test_switch.py b/tests/components/rfxtrx/test_switch.py index 4d92c6fa332..40086b1fb0a 100644 --- a/tests/components/rfxtrx/test_switch.py +++ b/tests/components/rfxtrx/test_switch.py @@ -8,8 +8,9 @@ from homeassistant.components.rfxtrx import DOMAIN from homeassistant.const import STATE_UNKNOWN from homeassistant.core import State +from .conftest import create_rfx_test_cfg + from tests.common import MockConfigEntry, mock_restore_cache -from tests.components.rfxtrx.conftest import create_rfx_test_cfg EVENT_RFY_ENABLE_SUN_AUTO = "0C1a0000030101011300000003" EVENT_RFY_DISABLE_SUN_AUTO = "0C1a0000030101011400000003" diff --git a/tests/components/roku/test_binary_sensor.py b/tests/components/roku/test_binary_sensor.py index 706b1eceddb..9b8ca5268c4 100644 --- a/tests/components/roku/test_binary_sensor.py +++ b/tests/components/roku/test_binary_sensor.py @@ -11,8 +11,9 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity import EntityCategory +from . import UPNP_SERIAL + from tests.common import MockConfigEntry -from tests.components.roku import UPNP_SERIAL async def test_roku_binary_sensors( diff --git a/tests/components/roku/test_config_flow.py b/tests/components/roku/test_config_flow.py index bac6d7456a3..e86084e7718 100644 --- a/tests/components/roku/test_config_flow.py +++ b/tests/components/roku/test_config_flow.py @@ -11,8 +11,7 @@ from homeassistant.const import CONF_HOST, CONF_NAME, CONF_SOURCE from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from tests.common import MockConfigEntry -from tests.components.roku import ( +from . import ( HOMEKIT_HOST, HOST, MOCK_HOMEKIT_DISCOVERY_INFO, @@ -21,6 +20,8 @@ from tests.components.roku import ( UPNP_FRIENDLY_NAME, ) +from tests.common import MockConfigEntry + async def test_duplicate_error( hass: HomeAssistant, diff --git a/tests/components/roku/test_remote.py b/tests/components/roku/test_remote.py index f1685609563..df36558e22e 100644 --- a/tests/components/roku/test_remote.py +++ b/tests/components/roku/test_remote.py @@ -10,8 +10,9 @@ from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_O from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +from . import UPNP_SERIAL + from tests.common import MockConfigEntry -from tests.components.roku import UPNP_SERIAL MAIN_ENTITY_ID = f"{REMOTE_DOMAIN}.my_roku_3" diff --git a/tests/components/roku/test_sensor.py b/tests/components/roku/test_sensor.py index 983455255fa..1942be9b687 100644 --- a/tests/components/roku/test_sensor.py +++ b/tests/components/roku/test_sensor.py @@ -14,8 +14,9 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity import EntityCategory +from . import UPNP_SERIAL + from tests.common import MockConfigEntry -from tests.components.roku import UPNP_SERIAL async def test_roku_sensors( diff --git a/tests/components/ruckus_unleashed/test_config_flow.py b/tests/components/ruckus_unleashed/test_config_flow.py index 9a93dcf78a7..b6816a710fc 100644 --- a/tests/components/ruckus_unleashed/test_config_flow.py +++ b/tests/components/ruckus_unleashed/test_config_flow.py @@ -8,8 +8,9 @@ from homeassistant import config_entries from homeassistant.components.ruckus_unleashed.const import DOMAIN from homeassistant.util import utcnow +from . import CONFIG, DEFAULT_SYSTEM_INFO, DEFAULT_TITLE + from tests.common import async_fire_time_changed -from tests.components.ruckus_unleashed import CONFIG, DEFAULT_SYSTEM_INFO, DEFAULT_TITLE async def test_form(hass): diff --git a/tests/components/ruckus_unleashed/test_device_tracker.py b/tests/components/ruckus_unleashed/test_device_tracker.py index b6a991f4c75..12dae721c90 100644 --- a/tests/components/ruckus_unleashed/test_device_tracker.py +++ b/tests/components/ruckus_unleashed/test_device_tracker.py @@ -8,8 +8,7 @@ from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_component import async_update_entity from homeassistant.util import utcnow -from tests.common import async_fire_time_changed -from tests.components.ruckus_unleashed import ( +from . import ( DEFAULT_AP_INFO, DEFAULT_SYSTEM_INFO, DEFAULT_TITLE, @@ -20,6 +19,8 @@ from tests.components.ruckus_unleashed import ( mock_config_entry, ) +from tests.common import async_fire_time_changed + async def test_client_connected(hass): """Test client connected.""" diff --git a/tests/components/ruckus_unleashed/test_init.py b/tests/components/ruckus_unleashed/test_init.py index d72856aa542..b4ebd7b1f00 100644 --- a/tests/components/ruckus_unleashed/test_init.py +++ b/tests/components/ruckus_unleashed/test_init.py @@ -18,7 +18,7 @@ from homeassistant.config_entries import ConfigEntryState from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC -from tests.components.ruckus_unleashed import ( +from . import ( DEFAULT_AP_INFO, DEFAULT_SYSTEM_INFO, DEFAULT_TITLE, From c5a56dab422ef2703b4d049b10d07d1c25e76eb7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 19 Sep 2022 09:48:25 +0200 Subject: [PATCH 536/955] Apply hass-relative-import to tests (a-c) (#78728) --- tests/components/accuweather/test_diagnostics.py | 3 ++- tests/components/accuweather/test_init.py | 3 ++- tests/components/accuweather/test_sensor.py | 3 ++- tests/components/accuweather/test_weather.py | 3 ++- tests/components/advantage_air/test_binary_sensor.py | 5 +++-- tests/components/advantage_air/test_config_flow.py | 2 +- tests/components/advantage_air/test_cover.py | 2 +- tests/components/advantage_air/test_init.py | 6 +----- tests/components/advantage_air/test_light.py | 2 +- tests/components/advantage_air/test_select.py | 2 +- tests/components/advantage_air/test_sensor.py | 5 +++-- tests/components/advantage_air/test_switch.py | 2 +- tests/components/advantage_air/test_update.py | 3 ++- tests/components/agent_dvr/test_init.py | 3 +-- tests/components/airly/test_diagnostics.py | 3 ++- tests/components/airly/test_init.py | 3 +-- tests/components/airly/test_sensor.py | 3 +-- tests/components/amberelectric/test_binary_sensor.py | 7 ++----- tests/components/amberelectric/test_coordinator.py | 2 +- tests/components/amberelectric/test_sensor.py | 5 +++-- tests/components/atag/test_climate.py | 3 ++- tests/components/atag/test_sensors.py | 3 ++- tests/components/atag/test_water_heater.py | 3 ++- tests/components/august/test_binary_sensor.py | 5 +++-- tests/components/august/test_button.py | 5 +---- tests/components/august/test_camera.py | 5 +---- tests/components/august/test_diagnostics.py | 3 ++- tests/components/august/test_gateway.py | 2 +- tests/components/august/test_init.py | 5 +++-- tests/components/august/test_lock.py | 5 +++-- tests/components/august/test_sensor.py | 2 +- tests/components/brother/test_diagnostics.py | 3 ++- tests/components/brother/test_init.py | 3 ++- tests/components/brother/test_sensor.py | 3 ++- tests/components/bsblan/test_init.py | 3 ++- tests/components/coinbase/test_diagnostics.py | 5 +---- tests/components/counter/test_init.py | 7 ++----- 37 files changed, 65 insertions(+), 67 deletions(-) diff --git a/tests/components/accuweather/test_diagnostics.py b/tests/components/accuweather/test_diagnostics.py index 1936de5fad7..4e8ad3d1ea8 100644 --- a/tests/components/accuweather/test_diagnostics.py +++ b/tests/components/accuweather/test_diagnostics.py @@ -1,8 +1,9 @@ """Test AccuWeather diagnostics.""" import json +from . import init_integration + from tests.common import load_fixture -from tests.components.accuweather import init_integration from tests.components.diagnostics import get_diagnostics_for_config_entry diff --git a/tests/components/accuweather/test_init.py b/tests/components/accuweather/test_init.py index d6f76f113b3..ee91fa4d219 100644 --- a/tests/components/accuweather/test_init.py +++ b/tests/components/accuweather/test_init.py @@ -10,8 +10,9 @@ from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_UNAVAILABLE from homeassistant.util.dt import utcnow +from . import init_integration + from tests.common import MockConfigEntry, async_fire_time_changed, load_fixture -from tests.components.accuweather import init_integration async def test_async_setup_entry(hass): diff --git a/tests/components/accuweather/test_sensor.py b/tests/components/accuweather/test_sensor.py index a1c454ad0b7..8612a805980 100644 --- a/tests/components/accuweather/test_sensor.py +++ b/tests/components/accuweather/test_sensor.py @@ -32,8 +32,9 @@ from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow from homeassistant.util.unit_system import IMPERIAL_SYSTEM +from . import init_integration + from tests.common import async_fire_time_changed, load_fixture -from tests.components.accuweather import init_integration async def test_sensor_without_forecast(hass): diff --git a/tests/components/accuweather/test_weather.py b/tests/components/accuweather/test_weather.py index 97f588cb477..041ccd687ac 100644 --- a/tests/components/accuweather/test_weather.py +++ b/tests/components/accuweather/test_weather.py @@ -27,8 +27,9 @@ from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow +from . import init_integration + from tests.common import async_fire_time_changed, load_fixture -from tests.components.accuweather import init_integration async def test_weather_without_forecast(hass): diff --git a/tests/components/advantage_air/test_binary_sensor.py b/tests/components/advantage_air/test_binary_sensor.py index 4bd792808fb..ff09131a77d 100644 --- a/tests/components/advantage_air/test_binary_sensor.py +++ b/tests/components/advantage_air/test_binary_sensor.py @@ -6,8 +6,7 @@ from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.helpers import entity_registry as er from homeassistant.util import dt -from tests.common import async_fire_time_changed -from tests.components.advantage_air import ( +from . import ( TEST_SET_RESPONSE, TEST_SET_URL, TEST_SYSTEM_DATA, @@ -15,6 +14,8 @@ from tests.components.advantage_air import ( add_mock_config, ) +from tests.common import async_fire_time_changed + async def test_binary_sensor_async_setup_entry(hass, aioclient_mock): """Test binary sensor setup.""" diff --git a/tests/components/advantage_air/test_config_flow.py b/tests/components/advantage_air/test_config_flow.py index 533cf2a17a4..cc9d8358d93 100644 --- a/tests/components/advantage_air/test_config_flow.py +++ b/tests/components/advantage_air/test_config_flow.py @@ -5,7 +5,7 @@ from unittest.mock import patch from homeassistant import config_entries, data_entry_flow from homeassistant.components.advantage_air.const import DOMAIN -from tests.components.advantage_air import TEST_SYSTEM_DATA, TEST_SYSTEM_URL, USER_INPUT +from . import TEST_SYSTEM_DATA, TEST_SYSTEM_URL, USER_INPUT async def test_form(hass, aioclient_mock): diff --git a/tests/components/advantage_air/test_cover.py b/tests/components/advantage_air/test_cover.py index 90cdf9a2168..e272ea15de3 100644 --- a/tests/components/advantage_air/test_cover.py +++ b/tests/components/advantage_air/test_cover.py @@ -17,7 +17,7 @@ from homeassistant.components.cover import ( from homeassistant.const import ATTR_ENTITY_ID, STATE_OPEN from homeassistant.helpers import entity_registry as er -from tests.components.advantage_air import ( +from . import ( TEST_SET_RESPONSE, TEST_SET_URL, TEST_SYSTEM_DATA, diff --git a/tests/components/advantage_air/test_init.py b/tests/components/advantage_air/test_init.py index ca8ecff359e..88591922295 100644 --- a/tests/components/advantage_air/test_init.py +++ b/tests/components/advantage_air/test_init.py @@ -2,11 +2,7 @@ from homeassistant.config_entries import ConfigEntryState -from tests.components.advantage_air import ( - TEST_SYSTEM_DATA, - TEST_SYSTEM_URL, - add_mock_config, -) +from . import TEST_SYSTEM_DATA, TEST_SYSTEM_URL, add_mock_config async def test_async_setup_entry(hass, aioclient_mock): diff --git a/tests/components/advantage_air/test_light.py b/tests/components/advantage_air/test_light.py index 85223700dbf..a259c1dc01b 100644 --- a/tests/components/advantage_air/test_light.py +++ b/tests/components/advantage_air/test_light.py @@ -14,7 +14,7 @@ from homeassistant.components.light import ( from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF from homeassistant.helpers import entity_registry as er -from tests.components.advantage_air import ( +from . import ( TEST_SET_LIGHT_URL, TEST_SET_RESPONSE, TEST_SYSTEM_DATA, diff --git a/tests/components/advantage_air/test_select.py b/tests/components/advantage_air/test_select.py index 20d42fdcffc..b8493c8ec45 100644 --- a/tests/components/advantage_air/test_select.py +++ b/tests/components/advantage_air/test_select.py @@ -9,7 +9,7 @@ from homeassistant.components.select.const import ( from homeassistant.const import ATTR_ENTITY_ID from homeassistant.helpers import entity_registry as er -from tests.components.advantage_air import ( +from . import ( TEST_SET_RESPONSE, TEST_SET_URL, TEST_SYSTEM_DATA, diff --git a/tests/components/advantage_air/test_sensor.py b/tests/components/advantage_air/test_sensor.py index 4dc2f1baaff..2489a336451 100644 --- a/tests/components/advantage_air/test_sensor.py +++ b/tests/components/advantage_air/test_sensor.py @@ -13,8 +13,7 @@ from homeassistant.const import ATTR_ENTITY_ID from homeassistant.helpers import entity_registry as er from homeassistant.util import dt -from tests.common import async_fire_time_changed -from tests.components.advantage_air import ( +from . import ( TEST_SET_RESPONSE, TEST_SET_URL, TEST_SYSTEM_DATA, @@ -22,6 +21,8 @@ from tests.components.advantage_air import ( add_mock_config, ) +from tests.common import async_fire_time_changed + async def test_sensor_platform(hass, aioclient_mock): """Test sensor platform.""" diff --git a/tests/components/advantage_air/test_switch.py b/tests/components/advantage_air/test_switch.py index be78edf8ffe..6e16d8d743a 100644 --- a/tests/components/advantage_air/test_switch.py +++ b/tests/components/advantage_air/test_switch.py @@ -13,7 +13,7 @@ from homeassistant.components.switch import ( from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF from homeassistant.helpers import entity_registry as er -from tests.components.advantage_air import ( +from . import ( TEST_SET_RESPONSE, TEST_SET_URL, TEST_SYSTEM_DATA, diff --git a/tests/components/advantage_air/test_update.py b/tests/components/advantage_air/test_update.py index 2fef887997f..d70f59b2bbb 100644 --- a/tests/components/advantage_air/test_update.py +++ b/tests/components/advantage_air/test_update.py @@ -3,8 +3,9 @@ from homeassistant.const import STATE_ON from homeassistant.helpers import entity_registry as er +from . import TEST_SYSTEM_URL, add_mock_config + from tests.common import load_fixture -from tests.components.advantage_air import TEST_SYSTEM_URL, add_mock_config async def test_update_platform(hass, aioclient_mock): diff --git a/tests/components/agent_dvr/test_init.py b/tests/components/agent_dvr/test_init.py index 27b3652a446..9166d51675b 100644 --- a/tests/components/agent_dvr/test_init.py +++ b/tests/components/agent_dvr/test_init.py @@ -7,9 +7,8 @@ from homeassistant.components.agent_dvr.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant -from . import CONF_DATA, create_entry +from . import CONF_DATA, create_entry, init_integration -from tests.components.agent_dvr import init_integration from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/airly/test_diagnostics.py b/tests/components/airly/test_diagnostics.py index fabe4f262ea..82be698311a 100644 --- a/tests/components/airly/test_diagnostics.py +++ b/tests/components/airly/test_diagnostics.py @@ -3,8 +3,9 @@ import json from homeassistant.components.diagnostics import REDACTED +from . import init_integration + from tests.common import load_fixture -from tests.components.airly import init_integration from tests.components.diagnostics import get_diagnostics_for_config_entry diff --git a/tests/components/airly/test_init.py b/tests/components/airly/test_init.py index 7e1d0797b20..5e76e96305e 100644 --- a/tests/components/airly/test_init.py +++ b/tests/components/airly/test_init.py @@ -11,7 +11,7 @@ from homeassistant.const import STATE_UNAVAILABLE from homeassistant.helpers import entity_registry as er from homeassistant.util.dt import utcnow -from . import API_POINT_URL +from . import API_POINT_URL, init_integration from tests.common import ( MockConfigEntry, @@ -19,7 +19,6 @@ from tests.common import ( load_fixture, mock_device_registry, ) -from tests.components.airly import init_integration async def test_async_setup_entry(hass, aioclient_mock): diff --git a/tests/components/airly/test_sensor.py b/tests/components/airly/test_sensor.py index d7717e3886a..d6bf4970130 100644 --- a/tests/components/airly/test_sensor.py +++ b/tests/components/airly/test_sensor.py @@ -22,10 +22,9 @@ from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow -from . import API_POINT_URL +from . import API_POINT_URL, init_integration from tests.common import async_fire_time_changed, load_fixture -from tests.components.airly import init_integration async def test_sensor(hass, aioclient_mock): diff --git a/tests/components/amberelectric/test_binary_sensor.py b/tests/components/amberelectric/test_binary_sensor.py index b5d6504447e..32cec180dbc 100644 --- a/tests/components/amberelectric/test_binary_sensor.py +++ b/tests/components/amberelectric/test_binary_sensor.py @@ -19,12 +19,9 @@ from homeassistant.components.amberelectric.const import ( from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from .helpers import GENERAL_CHANNEL, GENERAL_ONLY_SITE_ID, generate_current_interval + from tests.common import MockConfigEntry -from tests.components.amberelectric.helpers import ( - GENERAL_CHANNEL, - GENERAL_ONLY_SITE_ID, - generate_current_interval, -) MOCK_API_TOKEN = "psk_0000000000000000" diff --git a/tests/components/amberelectric/test_coordinator.py b/tests/components/amberelectric/test_coordinator.py index 924cd5249c0..64fa39192a6 100644 --- a/tests/components/amberelectric/test_coordinator.py +++ b/tests/components/amberelectric/test_coordinator.py @@ -19,7 +19,7 @@ from homeassistant.components.amberelectric.coordinator import ( from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import UpdateFailed -from tests.components.amberelectric.helpers import ( +from .helpers import ( CONTROLLED_LOAD_CHANNEL, FEED_IN_CHANNEL, GENERAL_AND_CONTROLLED_SITE_ID, diff --git a/tests/components/amberelectric/test_sensor.py b/tests/components/amberelectric/test_sensor.py index 08103576b49..7a35b2c1c7e 100644 --- a/tests/components/amberelectric/test_sensor.py +++ b/tests/components/amberelectric/test_sensor.py @@ -15,8 +15,7 @@ from homeassistant.components.amberelectric.const import ( from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from tests.common import MockConfigEntry -from tests.components.amberelectric.helpers import ( +from .helpers import ( CONTROLLED_LOAD_CHANNEL, FEED_IN_CHANNEL, GENERAL_AND_CONTROLLED_SITE_ID, @@ -25,6 +24,8 @@ from tests.components.amberelectric.helpers import ( GENERAL_ONLY_SITE_ID, ) +from tests.common import MockConfigEntry + MOCK_API_TOKEN = "psk_0000000000000000" diff --git a/tests/components/atag/test_climate.py b/tests/components/atag/test_climate.py index 115eb4035df..2369859a8e6 100644 --- a/tests/components/atag/test_climate.py +++ b/tests/components/atag/test_climate.py @@ -23,7 +23,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component -from tests.components.atag import UID, init_integration +from . import UID, init_integration + from tests.test_util.aiohttp import AiohttpClientMocker CLIMATE_ID = f"{Platform.CLIMATE}.{DOMAIN}" diff --git a/tests/components/atag/test_sensors.py b/tests/components/atag/test_sensors.py index aeefcb2789b..58a687512e2 100644 --- a/tests/components/atag/test_sensors.py +++ b/tests/components/atag/test_sensors.py @@ -3,7 +3,8 @@ from homeassistant.components.atag.sensor import SENSORS from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from tests.components.atag import UID, init_integration +from . import UID, init_integration + from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/atag/test_water_heater.py b/tests/components/atag/test_water_heater.py index 3372e8c69fa..428ff890116 100644 --- a/tests/components/atag/test_water_heater.py +++ b/tests/components/atag/test_water_heater.py @@ -10,7 +10,8 @@ from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from tests.components.atag import UID, init_integration +from . import UID, init_integration + from tests.test_util.aiohttp import AiohttpClientMocker WATER_HEATER_ID = f"{Platform.WATER_HEATER}.{DOMAIN}" diff --git a/tests/components/august/test_binary_sensor.py b/tests/components/august/test_binary_sensor.py index e2ff4a6771a..d062d30ba3f 100644 --- a/tests/components/august/test_binary_sensor.py +++ b/tests/components/august/test_binary_sensor.py @@ -17,8 +17,7 @@ from homeassistant.const import ( from homeassistant.helpers import device_registry as dr import homeassistant.util.dt as dt_util -from tests.common import async_fire_time_changed -from tests.components.august.mocks import ( +from .mocks import ( _create_august_with_devices, _mock_activities_from_fixture, _mock_doorbell_from_fixture, @@ -26,6 +25,8 @@ from tests.components.august.mocks import ( _mock_lock_from_fixture, ) +from tests.common import async_fire_time_changed + def _timetoken(): return str(time.time_ns())[:-2] diff --git a/tests/components/august/test_button.py b/tests/components/august/test_button.py index 2d3e6caf884..485bf4a7972 100644 --- a/tests/components/august/test_button.py +++ b/tests/components/august/test_button.py @@ -4,10 +4,7 @@ from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN from homeassistant.components.button.const import SERVICE_PRESS from homeassistant.const import ATTR_ENTITY_ID -from tests.components.august.mocks import ( - _create_august_api_with_devices, - _mock_lock_from_fixture, -) +from .mocks import _create_august_api_with_devices, _mock_lock_from_fixture async def test_wake_lock(hass): diff --git a/tests/components/august/test_camera.py b/tests/components/august/test_camera.py index 3c5379aa59e..8b93e5aff60 100644 --- a/tests/components/august/test_camera.py +++ b/tests/components/august/test_camera.py @@ -5,10 +5,7 @@ from unittest.mock import patch from homeassistant.const import STATE_IDLE -from tests.components.august.mocks import ( - _create_august_with_devices, - _mock_doorbell_from_fixture, -) +from .mocks import _create_august_with_devices, _mock_doorbell_from_fixture async def test_create_doorbell(hass, hass_client_no_auth): diff --git a/tests/components/august/test_diagnostics.py b/tests/components/august/test_diagnostics.py index 520daa91f91..0b65b6eea3e 100644 --- a/tests/components/august/test_diagnostics.py +++ b/tests/components/august/test_diagnostics.py @@ -1,10 +1,11 @@ """Test august diagnostics.""" -from tests.components.august.mocks import ( +from .mocks import ( _create_august_api_with_devices, _mock_doorbell_from_fixture, _mock_lock_from_fixture, ) + from tests.components.diagnostics import get_diagnostics_for_config_entry diff --git a/tests/components/august/test_gateway.py b/tests/components/august/test_gateway.py index 54a5e9321f2..724fa4482c3 100644 --- a/tests/components/august/test_gateway.py +++ b/tests/components/august/test_gateway.py @@ -6,7 +6,7 @@ from yalexs.authenticator_common import AuthenticationState from homeassistant.components.august.const import DOMAIN from homeassistant.components.august.gateway import AugustGateway -from tests.components.august.mocks import _mock_august_authentication, _mock_get_config +from .mocks import _mock_august_authentication, _mock_get_config async def test_refresh_access_token(hass): diff --git a/tests/components/august/test_init.py b/tests/components/august/test_init.py index ab3269e9ac8..a4bc3d7b16f 100644 --- a/tests/components/august/test_init.py +++ b/tests/components/august/test_init.py @@ -21,8 +21,7 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity_registry import EntityRegistry from homeassistant.setup import async_setup_component -from tests.common import MockConfigEntry -from tests.components.august.mocks import ( +from .mocks import ( _create_august_with_devices, _mock_august_authentication, _mock_doorsense_enabled_august_lock_detail, @@ -33,6 +32,8 @@ from tests.components.august.mocks import ( _mock_operative_august_lock_detail, ) +from tests.common import MockConfigEntry + async def test_august_api_is_failing(hass): """Config entry state is SETUP_RETRY when august api is failing.""" diff --git a/tests/components/august/test_lock.py b/tests/components/august/test_lock.py index 56f55138e36..e1b2a2b1bed 100644 --- a/tests/components/august/test_lock.py +++ b/tests/components/august/test_lock.py @@ -24,14 +24,15 @@ from homeassistant.const import ( from homeassistant.helpers import device_registry as dr, entity_registry as er import homeassistant.util.dt as dt_util -from tests.common import async_fire_time_changed -from tests.components.august.mocks import ( +from .mocks import ( _create_august_with_devices, _mock_activities_from_fixture, _mock_doorsense_enabled_august_lock_detail, _mock_lock_from_fixture, ) +from tests.common import async_fire_time_changed + async def test_lock_device_registry(hass): """Test creation of a lock with doorsense and bridge ands up in the registry.""" diff --git a/tests/components/august/test_sensor.py b/tests/components/august/test_sensor.py index 254f88ef4e9..1ed237ecf1f 100644 --- a/tests/components/august/test_sensor.py +++ b/tests/components/august/test_sensor.py @@ -2,7 +2,7 @@ from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, STATE_UNKNOWN from homeassistant.helpers import entity_registry as er -from tests.components.august.mocks import ( +from .mocks import ( _create_august_with_devices, _mock_activities_from_fixture, _mock_doorbell_from_fixture, diff --git a/tests/components/brother/test_diagnostics.py b/tests/components/brother/test_diagnostics.py index 73b3b6dda7a..bebd536c809 100644 --- a/tests/components/brother/test_diagnostics.py +++ b/tests/components/brother/test_diagnostics.py @@ -9,8 +9,9 @@ from aiohttp import ClientSession from homeassistant.core import HomeAssistant from homeassistant.util.dt import UTC +from . import init_integration + from tests.common import load_fixture -from tests.components.brother import init_integration from tests.components.diagnostics import get_diagnostics_for_config_entry diff --git a/tests/components/brother/test_init.py b/tests/components/brother/test_init.py index b034259974a..cd439a3a41f 100644 --- a/tests/components/brother/test_init.py +++ b/tests/components/brother/test_init.py @@ -9,8 +9,9 @@ from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_HOST, CONF_TYPE, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant +from . import init_integration + from tests.common import MockConfigEntry -from tests.components.brother import init_integration async def test_async_setup_entry(hass: HomeAssistant) -> None: diff --git a/tests/components/brother/test_sensor.py b/tests/components/brother/test_sensor.py index f50104c0b3a..9212e12e5b3 100644 --- a/tests/components/brother/test_sensor.py +++ b/tests/components/brother/test_sensor.py @@ -24,8 +24,9 @@ from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component from homeassistant.util.dt import UTC, utcnow +from . import init_integration + from tests.common import async_fire_time_changed, load_fixture -from tests.components.brother import init_integration ATTR_REMAINING_PAGES = "remaining_pages" ATTR_COUNTER = "counter" diff --git a/tests/components/bsblan/test_init.py b/tests/components/bsblan/test_init.py index c7937daf786..147ba46cb5b 100644 --- a/tests/components/bsblan/test_init.py +++ b/tests/components/bsblan/test_init.py @@ -5,7 +5,8 @@ from homeassistant.components.bsblan.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant -from tests.components.bsblan import init_integration, init_integration_without_auth +from . import init_integration, init_integration_without_auth + from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/coinbase/test_diagnostics.py b/tests/components/coinbase/test_diagnostics.py index b19f5c94d1b..f997b02f853 100644 --- a/tests/components/coinbase/test_diagnostics.py +++ b/tests/components/coinbase/test_diagnostics.py @@ -10,11 +10,8 @@ from .common import ( mock_get_exchange_rates, mocked_get_accounts, ) +from .const import MOCK_ACCOUNTS_RESPONSE_REDACTED, MOCK_ENTRY_REDACTED -from tests.components.coinbase.const import ( - MOCK_ACCOUNTS_RESPONSE_REDACTED, - MOCK_ENTRY_REDACTED, -) from tests.components.diagnostics import get_diagnostics_for_config_entry diff --git a/tests/components/counter/test_init.py b/tests/components/counter/test_init.py index 90885be770d..eb0907d87d4 100644 --- a/tests/components/counter/test_init.py +++ b/tests/components/counter/test_init.py @@ -24,12 +24,9 @@ from homeassistant.core import Context, CoreState, State from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component +from .common import async_decrement, async_increment, async_reset + from tests.common import mock_restore_cache -from tests.components.counter.common import ( - async_decrement, - async_increment, - async_reset, -) _LOGGER = logging.getLogger(__name__) From e9f55f4e54f07b09dce223fed77b45cb17373a50 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 19 Sep 2022 09:51:31 +0200 Subject: [PATCH 537/955] Apply hass-relative-import to tests (s-z) (#78733) --- tests/components/signal_messenger/test_notify.py | 2 +- tests/components/sleepiq/test_binary_sensor.py | 2 +- tests/components/sleepiq/test_button.py | 7 +------ tests/components/sleepiq/test_config_flow.py | 2 +- tests/components/sleepiq/test_init.py | 5 +++-- tests/components/sleepiq/test_light.py | 8 ++------ tests/components/sleepiq/test_number.py | 2 +- tests/components/sleepiq/test_select.py | 2 +- tests/components/sleepiq/test_sensor.py | 2 +- tests/components/sleepiq/test_switch.py | 8 ++------ tests/components/sonarr/test_config_flow.py | 3 ++- tests/components/speedtestdotnet/conftest.py | 2 +- tests/components/srp_energy/test_init.py | 2 +- tests/components/stream/test_hls.py | 5 +++-- tests/components/stream/test_ll_hls.py | 7 +------ tests/components/stream/test_worker.py | 5 +++-- tests/components/subaru/test_sensor.py | 7 +------ tests/components/switch/test_init.py | 2 +- tests/components/switch/test_light.py | 3 ++- tests/components/tcp/test_binary_sensor.py | 3 ++- tests/components/twinkly/test_init.py | 8 ++------ tests/components/twinkly/test_light.py | 3 ++- tests/components/velbus/conftest.py | 3 ++- tests/components/wallbox/test_config_flow.py | 2 +- tests/components/wallbox/test_init.py | 5 ++--- tests/components/wallbox/test_lock.py | 4 ++-- tests/components/wallbox/test_number.py | 4 ++-- tests/components/wallbox/test_sensor.py | 4 ++-- tests/components/wallbox/test_switch.py | 4 ++-- tests/components/whirlpool/test_init.py | 2 +- tests/components/wilight/test_config_flow.py | 5 +++-- tests/components/wilight/test_init.py | 2 +- tests/components/wilight/test_light.py | 2 +- tests/components/withings/test_common.py | 7 ++----- tests/components/xiaomi_miio/test_select.py | 3 ++- tests/components/zha/conftest.py | 3 ++- tests/components/zha/test_fan.py | 3 +-- tests/components/zha/test_light.py | 2 +- tests/components/zha/test_switch.py | 2 +- 39 files changed, 63 insertions(+), 84 deletions(-) diff --git a/tests/components/signal_messenger/test_notify.py b/tests/components/signal_messenger/test_notify.py index 6ed57813f46..cc233e88c03 100644 --- a/tests/components/signal_messenger/test_notify.py +++ b/tests/components/signal_messenger/test_notify.py @@ -14,7 +14,7 @@ import voluptuous as vol from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from tests.components.signal_messenger.conftest import ( +from .conftest import ( CONTENT, MESSAGE, NUMBER_FROM, diff --git a/tests/components/sleepiq/test_binary_sensor.py b/tests/components/sleepiq/test_binary_sensor.py index bce30ad2393..7f12b9ea44a 100644 --- a/tests/components/sleepiq/test_binary_sensor.py +++ b/tests/components/sleepiq/test_binary_sensor.py @@ -9,7 +9,7 @@ from homeassistant.const import ( ) from homeassistant.helpers import entity_registry as er -from tests.components.sleepiq.conftest import ( +from .conftest import ( BED_NAME, BED_NAME_LOWER, SLEEPER_L_ID, diff --git a/tests/components/sleepiq/test_button.py b/tests/components/sleepiq/test_button.py index cab3f36d73f..1c148dc8850 100644 --- a/tests/components/sleepiq/test_button.py +++ b/tests/components/sleepiq/test_button.py @@ -3,12 +3,7 @@ from homeassistant.components.button import DOMAIN from homeassistant.const import ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME from homeassistant.helpers import entity_registry as er -from tests.components.sleepiq.conftest import ( - BED_ID, - BED_NAME, - BED_NAME_LOWER, - setup_platform, -) +from .conftest import BED_ID, BED_NAME, BED_NAME_LOWER, setup_platform async def test_button_calibrate(hass, mock_asyncsleepiq): diff --git a/tests/components/sleepiq/test_config_flow.py b/tests/components/sleepiq/test_config_flow.py index 75a2524e2bc..3e944ec69f8 100644 --- a/tests/components/sleepiq/test_config_flow.py +++ b/tests/components/sleepiq/test_config_flow.py @@ -9,7 +9,7 @@ from homeassistant.components.sleepiq.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from tests.components.sleepiq.conftest import SLEEPIQ_CONFIG, setup_platform +from .conftest import SLEEPIQ_CONFIG, setup_platform async def test_import(hass: HomeAssistant) -> None: diff --git a/tests/components/sleepiq/test_init.py b/tests/components/sleepiq/test_init.py index e468734e063..6a02f795805 100644 --- a/tests/components/sleepiq/test_init.py +++ b/tests/components/sleepiq/test_init.py @@ -19,8 +19,7 @@ from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow -from tests.common import MockConfigEntry, async_fire_time_changed, mock_registry -from tests.components.sleepiq.conftest import ( +from .conftest import ( BED_ID, SLEEPER_L_ID, SLEEPER_L_NAME, @@ -29,6 +28,8 @@ from tests.components.sleepiq.conftest import ( setup_platform, ) +from tests.common import MockConfigEntry, async_fire_time_changed, mock_registry + ENTITY_IS_IN_BED = f"sensor.sleepnumber_{BED_ID}_{SLEEPER_L_NAME_LOWER}_{IS_IN_BED}" ENTITY_PRESSURE = f"sensor.sleepnumber_{BED_ID}_{SLEEPER_L_NAME_LOWER}_{PRESSURE}" ENTITY_SLEEP_NUMBER = ( diff --git a/tests/components/sleepiq/test_light.py b/tests/components/sleepiq/test_light.py index d7386cceb7b..6003dd82e30 100644 --- a/tests/components/sleepiq/test_light.py +++ b/tests/components/sleepiq/test_light.py @@ -5,13 +5,9 @@ from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON from homeassistant.helpers import entity_registry as er from homeassistant.util.dt import utcnow +from .conftest import BED_ID, BED_NAME, BED_NAME_LOWER, setup_platform + from tests.common import async_fire_time_changed -from tests.components.sleepiq.conftest import ( - BED_ID, - BED_NAME, - BED_NAME_LOWER, - setup_platform, -) async def test_setup(hass, mock_asyncsleepiq): diff --git a/tests/components/sleepiq/test_number.py b/tests/components/sleepiq/test_number.py index bf554b69499..903bf054ac7 100644 --- a/tests/components/sleepiq/test_number.py +++ b/tests/components/sleepiq/test_number.py @@ -10,7 +10,7 @@ from homeassistant.components.number.const import ( from homeassistant.const import ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, ATTR_ICON from homeassistant.helpers import entity_registry as er -from tests.components.sleepiq.conftest import ( +from .conftest import ( BED_ID, BED_NAME, BED_NAME_LOWER, diff --git a/tests/components/sleepiq/test_select.py b/tests/components/sleepiq/test_select.py index 855ca518f2d..d0e2a0e828d 100644 --- a/tests/components/sleepiq/test_select.py +++ b/tests/components/sleepiq/test_select.py @@ -11,7 +11,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from tests.components.sleepiq.conftest import ( +from .conftest import ( BED_ID, BED_NAME, BED_NAME_LOWER, diff --git a/tests/components/sleepiq/test_sensor.py b/tests/components/sleepiq/test_sensor.py index 68ee5319db6..b74f78dbc88 100644 --- a/tests/components/sleepiq/test_sensor.py +++ b/tests/components/sleepiq/test_sensor.py @@ -3,7 +3,7 @@ from homeassistant.components.sensor import DOMAIN from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_ICON from homeassistant.helpers import entity_registry as er -from tests.components.sleepiq.conftest import ( +from .conftest import ( BED_NAME, BED_NAME_LOWER, SLEEPER_L_ID, diff --git a/tests/components/sleepiq/test_switch.py b/tests/components/sleepiq/test_switch.py index 38fc747c39d..7e48b51fbea 100644 --- a/tests/components/sleepiq/test_switch.py +++ b/tests/components/sleepiq/test_switch.py @@ -5,13 +5,9 @@ from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON from homeassistant.helpers import entity_registry as er from homeassistant.util.dt import utcnow +from .conftest import BED_ID, BED_NAME, BED_NAME_LOWER, setup_platform + from tests.common import async_fire_time_changed -from tests.components.sleepiq.conftest import ( - BED_ID, - BED_NAME, - BED_NAME_LOWER, - setup_platform, -) async def test_setup(hass, mock_asyncsleepiq): diff --git a/tests/components/sonarr/test_config_flow.py b/tests/components/sonarr/test_config_flow.py index 5eea0974dee..bf2694d7a24 100644 --- a/tests/components/sonarr/test_config_flow.py +++ b/tests/components/sonarr/test_config_flow.py @@ -15,8 +15,9 @@ from homeassistant.const import CONF_API_KEY, CONF_SOURCE, CONF_URL, CONF_VERIFY from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from . import MOCK_REAUTH_INPUT, MOCK_USER_INPUT + from tests.common import MockConfigEntry -from tests.components.sonarr import MOCK_REAUTH_INPUT, MOCK_USER_INPUT async def test_show_user_form(hass: HomeAssistant) -> None: diff --git a/tests/components/speedtestdotnet/conftest.py b/tests/components/speedtestdotnet/conftest.py index 78a864cb934..3324b92d8bd 100644 --- a/tests/components/speedtestdotnet/conftest.py +++ b/tests/components/speedtestdotnet/conftest.py @@ -3,7 +3,7 @@ from unittest.mock import patch import pytest -from tests.components.speedtestdotnet import MOCK_RESULTS, MOCK_SERVERS +from . import MOCK_RESULTS, MOCK_SERVERS @pytest.fixture(autouse=True) diff --git a/tests/components/srp_energy/test_init.py b/tests/components/srp_energy/test_init.py index 8c8d87674fe..1806fc7d5c4 100644 --- a/tests/components/srp_energy/test_init.py +++ b/tests/components/srp_energy/test_init.py @@ -2,7 +2,7 @@ from homeassistant import config_entries from homeassistant.components import srp_energy -from tests.components.srp_energy import init_integration +from . import init_integration async def test_setup_entry(hass): diff --git a/tests/components/stream/test_hls.py b/tests/components/stream/test_hls.py index ad430cb6e49..204b460b026 100644 --- a/tests/components/stream/test_hls.py +++ b/tests/components/stream/test_hls.py @@ -20,13 +20,14 @@ from homeassistant.components.stream.core import Part from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.common import async_fire_time_changed -from tests.components.stream.common import ( +from .common import ( FAKE_TIME, DefaultSegment as Segment, assert_mp4_has_transform_matrix, ) +from tests.common import async_fire_time_changed + STREAM_SOURCE = "some-stream-source" INIT_BYTES = b"init" FAKE_PAYLOAD = b"fake-payload" diff --git a/tests/components/stream/test_ll_hls.py b/tests/components/stream/test_ll_hls.py index 447b9ff58e9..5755617f393 100644 --- a/tests/components/stream/test_ll_hls.py +++ b/tests/components/stream/test_ll_hls.py @@ -22,14 +22,9 @@ from homeassistant.components.stream.const import ( 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 .test_hls import STREAM_SOURCE, HlsClient, make_playlist -from tests.components.stream.common import ( - FAKE_TIME, - DefaultSegment as Segment, - generate_h264_video, -) - SEGMENT_DURATION = 6 TEST_PART_DURATION = 0.75 NUM_PART_SEGMENTS = int(-(-SEGMENT_DURATION // TEST_PART_DURATION)) diff --git a/tests/components/stream/test_worker.py b/tests/components/stream/test_worker.py index 70769840dd7..00d735df74d 100644 --- a/tests/components/stream/test_worker.py +++ b/tests/components/stream/test_worker.py @@ -48,9 +48,10 @@ from homeassistant.components.stream.worker import ( ) from homeassistant.setup import async_setup_component +from .common import 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 -from tests.components.stream.common import generate_h264_video, generate_h265_video -from tests.components.stream.test_ll_hls import TEST_PART_DURATION STREAM_SOURCE = "some-stream-source" # Formats here are arbitrary, not exercised by tests diff --git a/tests/components/subaru/test_sensor.py b/tests/components/subaru/test_sensor.py index f2a66e7e5e9..6ad5e729290 100644 --- a/tests/components/subaru/test_sensor.py +++ b/tests/components/subaru/test_sensor.py @@ -20,12 +20,7 @@ from .api_responses import ( VEHICLE_DATA, VEHICLE_STATUS_EV, ) - -from tests.components.subaru.conftest import ( - MOCK_API_FETCH, - MOCK_API_GET_DATA, - advance_time_to_next_fetch, -) +from .conftest import MOCK_API_FETCH, MOCK_API_GET_DATA, advance_time_to_next_fetch VEHICLE_NAME = VEHICLE_DATA[TEST_VIN_2_EV][VEHICLE_NAME] diff --git a/tests/components/switch/test_init.py b/tests/components/switch/test_init.py index ed3d3c59da9..29f77b0d470 100644 --- a/tests/components/switch/test_init.py +++ b/tests/components/switch/test_init.py @@ -6,7 +6,7 @@ from homeassistant.components import switch from homeassistant.const import CONF_PLATFORM from homeassistant.setup import async_setup_component -from tests.components.switch import common +from . import common @pytest.fixture(autouse=True) diff --git a/tests/components/switch/test_light.py b/tests/components/switch/test_light.py index f3d5cac9238..3d96ed8745c 100644 --- a/tests/components/switch/test_light.py +++ b/tests/components/switch/test_light.py @@ -6,8 +6,9 @@ from homeassistant.components.light import ( ) from homeassistant.setup import async_setup_component +from . import common as switch_common + from tests.components.light import common -from tests.components.switch import common as switch_common async def test_default_state(hass): diff --git a/tests/components/tcp/test_binary_sensor.py b/tests/components/tcp/test_binary_sensor.py index f8c13b41c30..9f1c25a3d49 100644 --- a/tests/components/tcp/test_binary_sensor.py +++ b/tests/components/tcp/test_binary_sensor.py @@ -8,8 +8,9 @@ from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow +from . import test_sensor as test_tcp + from tests.common import assert_setup_component, async_fire_time_changed -import tests.components.tcp.test_sensor as test_tcp BINARY_SENSOR_CONFIG = test_tcp.TEST_CONFIG["sensor"] TEST_CONFIG = {"binary_sensor": BINARY_SENSOR_CONFIG} diff --git a/tests/components/twinkly/test_init.py b/tests/components/twinkly/test_init.py index b6b86bb7b33..8cf026b4fb8 100644 --- a/tests/components/twinkly/test_init.py +++ b/tests/components/twinkly/test_init.py @@ -13,13 +13,9 @@ from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_MODEL from homeassistant.core import HomeAssistant +from . import TEST_HOST, TEST_MODEL, TEST_NAME_ORIGINAL, ClientMock + from tests.common import MockConfigEntry -from tests.components.twinkly import ( - TEST_HOST, - TEST_MODEL, - TEST_NAME_ORIGINAL, - ClientMock, -) async def test_load_unload_entry(hass: HomeAssistant): diff --git a/tests/components/twinkly/test_light.py b/tests/components/twinkly/test_light.py index 14bed7df007..40fea31a6ba 100644 --- a/tests/components/twinkly/test_light.py +++ b/tests/components/twinkly/test_light.py @@ -16,8 +16,9 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.device_registry import DeviceEntry from homeassistant.helpers.entity_registry import RegistryEntry +from . import TEST_MODEL, TEST_NAME_ORIGINAL, ClientMock + from tests.common import MockConfigEntry -from tests.components.twinkly import TEST_MODEL, TEST_NAME_ORIGINAL, ClientMock async def test_initial_state(hass: HomeAssistant): diff --git a/tests/components/velbus/conftest.py b/tests/components/velbus/conftest.py index c13ce3127fa..62aa0d0b505 100644 --- a/tests/components/velbus/conftest.py +++ b/tests/components/velbus/conftest.py @@ -8,8 +8,9 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, CONF_PORT from homeassistant.core import HomeAssistant +from .const import PORT_TCP + from tests.common import MockConfigEntry -from tests.components.velbus.const import PORT_TCP @pytest.fixture(name="controller") diff --git a/tests/components/wallbox/test_config_flow.py b/tests/components/wallbox/test_config_flow.py index c9254f77aac..bd9e51adda7 100644 --- a/tests/components/wallbox/test_config_flow.py +++ b/tests/components/wallbox/test_config_flow.py @@ -18,7 +18,7 @@ from homeassistant.components.wallbox.const import ( ) from homeassistant.core import HomeAssistant -from tests.components.wallbox import ( +from . import ( authorisation_response, authorisation_response_unauthorised, entry, diff --git a/tests/components/wallbox/test_init.py b/tests/components/wallbox/test_init.py index 5080bab87ea..a0db03b6c43 100644 --- a/tests/components/wallbox/test_init.py +++ b/tests/components/wallbox/test_init.py @@ -7,15 +7,14 @@ from homeassistant.components.wallbox import CHARGER_MAX_CHARGING_CURRENT_KEY from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant -from . import test_response - -from tests.components.wallbox import ( +from . import ( DOMAIN, authorisation_response, entry, setup_integration, setup_integration_connection_error, setup_integration_read_only, + test_response, ) diff --git a/tests/components/wallbox/test_lock.py b/tests/components/wallbox/test_lock.py index fbcb07b0e90..567d92757cd 100644 --- a/tests/components/wallbox/test_lock.py +++ b/tests/components/wallbox/test_lock.py @@ -9,13 +9,13 @@ from homeassistant.components.wallbox import CHARGER_LOCKED_UNLOCKED_KEY from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant -from tests.components.wallbox import ( +from . import ( authorisation_response, entry, setup_integration, setup_integration_read_only, ) -from tests.components.wallbox.const import MOCK_LOCK_ENTITY_ID +from .const import MOCK_LOCK_ENTITY_ID async def test_wallbox_lock_class(hass: HomeAssistant) -> None: diff --git a/tests/components/wallbox/test_number.py b/tests/components/wallbox/test_number.py index c8e8b29f28b..58e3450e6aa 100644 --- a/tests/components/wallbox/test_number.py +++ b/tests/components/wallbox/test_number.py @@ -9,8 +9,8 @@ from homeassistant.components.wallbox import CHARGER_MAX_CHARGING_CURRENT_KEY from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant -from tests.components.wallbox import authorisation_response, entry, setup_integration -from tests.components.wallbox.const import MOCK_NUMBER_ENTITY_ID +from . import authorisation_response, entry, setup_integration +from .const import MOCK_NUMBER_ENTITY_ID async def test_wallbox_number_class(hass: HomeAssistant) -> None: diff --git a/tests/components/wallbox/test_sensor.py b/tests/components/wallbox/test_sensor.py index 7663e518d81..a224085f65b 100644 --- a/tests/components/wallbox/test_sensor.py +++ b/tests/components/wallbox/test_sensor.py @@ -2,8 +2,8 @@ from homeassistant.const import CONF_ICON, CONF_UNIT_OF_MEASUREMENT, POWER_KILO_WATT from homeassistant.core import HomeAssistant -from tests.components.wallbox import entry, setup_integration -from tests.components.wallbox.const import ( +from . import entry, setup_integration +from .const import ( MOCK_SENSOR_CHARGING_POWER_ID, MOCK_SENSOR_CHARGING_SPEED_ID, MOCK_SENSOR_MAX_AVAILABLE_POWER, diff --git a/tests/components/wallbox/test_switch.py b/tests/components/wallbox/test_switch.py index c57fee0353f..588eea04513 100644 --- a/tests/components/wallbox/test_switch.py +++ b/tests/components/wallbox/test_switch.py @@ -10,8 +10,8 @@ from homeassistant.components.wallbox.const import CHARGER_STATUS_ID_KEY from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant -from tests.components.wallbox import authorisation_response, entry, setup_integration -from tests.components.wallbox.const import MOCK_SWITCH_ENTITY_ID +from . import authorisation_response, entry, setup_integration +from .const import MOCK_SWITCH_ENTITY_ID async def test_wallbox_switch_class(hass: HomeAssistant) -> None: diff --git a/tests/components/whirlpool/test_init.py b/tests/components/whirlpool/test_init.py index 626c127b61a..619c2c783b7 100644 --- a/tests/components/whirlpool/test_init.py +++ b/tests/components/whirlpool/test_init.py @@ -7,7 +7,7 @@ from homeassistant.components.whirlpool.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant -from tests.components.whirlpool import init_integration +from . import init_integration async def test_setup(hass: HomeAssistant): diff --git a/tests/components/wilight/test_config_flow.py b/tests/components/wilight/test_config_flow.py index a209d55ba99..a46af6e7d82 100644 --- a/tests/components/wilight/test_config_flow.py +++ b/tests/components/wilight/test_config_flow.py @@ -14,8 +14,7 @@ from homeassistant.const import CONF_HOST, CONF_NAME, CONF_SOURCE from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from tests.common import MockConfigEntry -from tests.components.wilight import ( +from . import ( CONF_COMPONENTS, HOST, MOCK_SSDP_DISCOVERY_INFO_MISSING_MANUFACTURER, @@ -26,6 +25,8 @@ from tests.components.wilight import ( WILIGHT_ID, ) +from tests.common import MockConfigEntry + @pytest.fixture(name="dummy_get_components_from_model_clear") def mock_dummy_get_components_from_model_clear(): diff --git a/tests/components/wilight/test_init.py b/tests/components/wilight/test_init.py index 5aadce3caea..860fd8ea54d 100644 --- a/tests/components/wilight/test_init.py +++ b/tests/components/wilight/test_init.py @@ -9,7 +9,7 @@ import requests from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant -from tests.components.wilight import ( +from . import ( HOST, UPNP_MAC_ADDRESS, UPNP_MODEL_NAME_P_B, diff --git a/tests/components/wilight/test_light.py b/tests/components/wilight/test_light.py index 2255840d01c..f82d493e70c 100644 --- a/tests/components/wilight/test_light.py +++ b/tests/components/wilight/test_light.py @@ -19,7 +19,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from tests.components.wilight import ( +from . import ( HOST, UPNP_MAC_ADDRESS, UPNP_MODEL_NAME_COLOR, diff --git a/tests/components/withings/test_common.py b/tests/components/withings/test_common.py index d65b80f256a..3917c894b60 100644 --- a/tests/components/withings/test_common.py +++ b/tests/components/withings/test_common.py @@ -19,12 +19,9 @@ from homeassistant.components.withings.common import ( from homeassistant.core import HomeAssistant from homeassistant.helpers.config_entry_oauth2_flow import AbstractOAuth2Implementation +from .common import ComponentFactory, get_data_manager_by_user_id, new_profile_config + from tests.common import MockConfigEntry -from tests.components.withings.common import ( - ComponentFactory, - get_data_manager_by_user_id, - new_profile_config, -) from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/xiaomi_miio/test_select.py b/tests/components/xiaomi_miio/test_select.py index 3fa8a3de291..014aa6fa2cd 100644 --- a/tests/components/xiaomi_miio/test_select.py +++ b/tests/components/xiaomi_miio/test_select.py @@ -32,8 +32,9 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant +from . import TEST_MAC + from tests.common import MockConfigEntry, async_fire_time_changed -from tests.components.xiaomi_miio import TEST_MAC @pytest.fixture(autouse=True) diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py index 27155e16cc7..a4d29224b74 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -20,9 +20,10 @@ import homeassistant.components.zha.core.const as zha_const import homeassistant.components.zha.core.device as zha_core_device from homeassistant.setup import async_setup_component +from . import common + from tests.common import MockConfigEntry from tests.components.light.conftest import mock_light_profiles # noqa: F401 -from tests.components.zha import common FIXTURE_GRP_ID = 0x1001 FIXTURE_GRP_NAME = "fixture group" diff --git a/tests/components/zha/test_fan.py b/tests/components/zha/test_fan.py index 9ebc5ae1c79..d635072fbbe 100644 --- a/tests/components/zha/test_fan.py +++ b/tests/components/zha/test_fan.py @@ -39,14 +39,13 @@ from .common import ( async_enable_traffic, async_find_group_entity_id, async_test_rejoin, + async_wait_for_updates, find_entity_id, get_zha_gateway, send_attributes_report, ) from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE -from tests.components.zha.common import async_wait_for_updates - IEEE_GROUPABLE_DEVICE = "01:2d:6f:00:0a:90:69:e8" IEEE_GROUPABLE_DEVICE2 = "02:2d:6f:00:0a:90:69:e8" diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index f3779c4841e..a9b8c7a14ee 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -28,6 +28,7 @@ from .common import ( async_find_group_entity_id, async_shift_time, async_test_rejoin, + async_wait_for_updates, find_entity_id, get_zha_gateway, patch_zha_config, @@ -36,7 +37,6 @@ from .common import ( from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE from tests.common import async_fire_time_changed -from tests.components.zha.common import async_wait_for_updates IEEE_GROUPABLE_DEVICE = "01:2d:6f:00:0a:90:69:e8" IEEE_GROUPABLE_DEVICE2 = "02:2d:6f:00:0a:90:69:e9" diff --git a/tests/components/zha/test_switch.py b/tests/components/zha/test_switch.py index 0b8fe658c28..11beec83b9f 100644 --- a/tests/components/zha/test_switch.py +++ b/tests/components/zha/test_switch.py @@ -26,6 +26,7 @@ from .common import ( async_enable_traffic, async_find_group_entity_id, async_test_rejoin, + async_wait_for_updates, find_entity_id, get_zha_gateway, send_attributes_report, @@ -33,7 +34,6 @@ from .common import ( from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_TYPE from tests.common import mock_coro -from tests.components.zha.common import async_wait_for_updates ON = 1 OFF = 0 From 38548b098691cdb0384992c24a2814c39378c6cd Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 19 Sep 2022 09:53:56 +0200 Subject: [PATCH 538/955] Adjust homekit-controller test imports (#78731) --- .../specific_devices/test_anker_eufycam.py | 2 +- .../specific_devices/test_aqara_gateway.py | 2 +- .../specific_devices/test_aqara_switch.py | 2 +- .../homekit_controller/specific_devices/test_arlo_baby.py | 2 +- .../specific_devices/test_connectsense.py | 2 +- .../homekit_controller/specific_devices/test_ecobee3.py | 2 +- .../specific_devices/test_ecobee_501.py | 2 +- .../specific_devices/test_ecobee_occupancy.py | 2 +- .../specific_devices/test_eve_degree.py | 2 +- .../specific_devices/test_eve_energy.py | 2 +- .../homekit_controller/specific_devices/test_haa_fan.py | 2 +- .../specific_devices/test_homeassistant_bridge.py | 2 +- .../specific_devices/test_hue_bridge.py | 2 +- .../specific_devices/test_koogeek_ls1.py | 5 +++-- .../specific_devices/test_koogeek_p1eu.py | 2 +- .../specific_devices/test_koogeek_sw2.py | 2 +- .../specific_devices/test_lennox_e30.py | 2 +- .../homekit_controller/specific_devices/test_lg_tv.py | 2 +- .../specific_devices/test_lutron_caseta_bridge.py | 2 +- .../homekit_controller/specific_devices/test_mss425f.py | 2 +- .../homekit_controller/specific_devices/test_mss565.py | 2 +- .../specific_devices/test_mysa_living.py | 2 +- .../specific_devices/test_nanoleaf_strip_nl55.py | 2 +- .../specific_devices/test_netamo_doorbell.py | 2 +- .../specific_devices/test_rainmachine_pro_8.py | 2 +- .../specific_devices/test_ryse_smart_bridge.py | 2 +- .../specific_devices/test_schlage_sense.py | 2 +- .../specific_devices/test_simpleconnect_fan.py | 2 +- .../specific_devices/test_velux_gateway.py | 2 +- .../specific_devices/test_vocolinc_flowerbud.py | 2 +- .../specific_devices/test_vocolinc_vp3.py | 2 +- .../homekit_controller/test_alarm_control_panel.py | 2 +- tests/components/homekit_controller/test_binary_sensor.py | 2 +- tests/components/homekit_controller/test_button.py | 2 +- tests/components/homekit_controller/test_camera.py | 2 +- tests/components/homekit_controller/test_climate.py | 2 +- tests/components/homekit_controller/test_connection.py | 7 ++----- tests/components/homekit_controller/test_cover.py | 2 +- .../components/homekit_controller/test_device_trigger.py | 3 ++- tests/components/homekit_controller/test_diagnostics.py | 6 ++---- tests/components/homekit_controller/test_fan.py | 2 +- tests/components/homekit_controller/test_humidifier.py | 2 +- tests/components/homekit_controller/test_init.py | 8 ++++++-- tests/components/homekit_controller/test_light.py | 2 +- tests/components/homekit_controller/test_lock.py | 2 +- tests/components/homekit_controller/test_media_player.py | 2 +- tests/components/homekit_controller/test_number.py | 2 +- tests/components/homekit_controller/test_select.py | 2 +- tests/components/homekit_controller/test_sensor.py | 2 +- tests/components/homekit_controller/test_storage.py | 6 ++---- tests/components/homekit_controller/test_switch.py | 2 +- 51 files changed, 62 insertions(+), 63 deletions(-) diff --git a/tests/components/homekit_controller/specific_devices/test_anker_eufycam.py b/tests/components/homekit_controller/specific_devices/test_anker_eufycam.py index 82348054df9..644abb8a3a6 100644 --- a/tests/components/homekit_controller/specific_devices/test_anker_eufycam.py +++ b/tests/components/homekit_controller/specific_devices/test_anker_eufycam.py @@ -1,6 +1,6 @@ """Test against characteristics captured from a eufycam.""" -from tests.components.homekit_controller.common import ( +from ..common import ( HUB_TEST_ACCESSORY_ID, DeviceTestInfo, EntityTestInfo, diff --git a/tests/components/homekit_controller/specific_devices/test_aqara_gateway.py b/tests/components/homekit_controller/specific_devices/test_aqara_gateway.py index 6950f4cb61e..150b39ac2ab 100644 --- a/tests/components/homekit_controller/specific_devices/test_aqara_gateway.py +++ b/tests/components/homekit_controller/specific_devices/test_aqara_gateway.py @@ -11,7 +11,7 @@ from homeassistant.components.alarm_control_panel import ( from homeassistant.components.number import NumberMode from homeassistant.helpers.entity import EntityCategory -from tests.components.homekit_controller.common import ( +from ..common import ( HUB_TEST_ACCESSORY_ID, DeviceTestInfo, EntityTestInfo, diff --git a/tests/components/homekit_controller/specific_devices/test_aqara_switch.py b/tests/components/homekit_controller/specific_devices/test_aqara_switch.py index daa6d593988..f3039e754d8 100644 --- a/tests/components/homekit_controller/specific_devices/test_aqara_switch.py +++ b/tests/components/homekit_controller/specific_devices/test_aqara_switch.py @@ -10,7 +10,7 @@ https://github.com/home-assistant/core/pull/39090 from homeassistant.components.sensor import SensorStateClass from homeassistant.const import PERCENTAGE -from tests.components.homekit_controller.common import ( +from ..common import ( HUB_TEST_ACCESSORY_ID, DeviceTestInfo, DeviceTriggerInfo, diff --git a/tests/components/homekit_controller/specific_devices/test_arlo_baby.py b/tests/components/homekit_controller/specific_devices/test_arlo_baby.py index fe3d1ea5efc..9a88e992a85 100644 --- a/tests/components/homekit_controller/specific_devices/test_arlo_baby.py +++ b/tests/components/homekit_controller/specific_devices/test_arlo_baby.py @@ -3,7 +3,7 @@ from homeassistant.components.sensor import SensorStateClass from homeassistant.const import PERCENTAGE, TEMP_CELSIUS -from tests.components.homekit_controller.common import ( +from ..common import ( HUB_TEST_ACCESSORY_ID, DeviceTestInfo, EntityTestInfo, diff --git a/tests/components/homekit_controller/specific_devices/test_connectsense.py b/tests/components/homekit_controller/specific_devices/test_connectsense.py index fbb95fc3d89..9e233ebdc10 100644 --- a/tests/components/homekit_controller/specific_devices/test_connectsense.py +++ b/tests/components/homekit_controller/specific_devices/test_connectsense.py @@ -7,7 +7,7 @@ from homeassistant.const import ( POWER_WATT, ) -from tests.components.homekit_controller.common import ( +from ..common import ( HUB_TEST_ACCESSORY_ID, DeviceTestInfo, EntityTestInfo, diff --git a/tests/components/homekit_controller/specific_devices/test_ecobee3.py b/tests/components/homekit_controller/specific_devices/test_ecobee3.py index 4da2f572626..27392afc9b0 100644 --- a/tests/components/homekit_controller/specific_devices/test_ecobee3.py +++ b/tests/components/homekit_controller/specific_devices/test_ecobee3.py @@ -19,7 +19,7 @@ from homeassistant.config_entries import ConfigEntryState from homeassistant.const import TEMP_CELSIUS from homeassistant.helpers import entity_registry as er -from tests.components.homekit_controller.common import ( +from ..common import ( HUB_TEST_ACCESSORY_ID, DeviceTestInfo, EntityTestInfo, diff --git a/tests/components/homekit_controller/specific_devices/test_ecobee_501.py b/tests/components/homekit_controller/specific_devices/test_ecobee_501.py index ca91607bd09..716ad6bc7c0 100644 --- a/tests/components/homekit_controller/specific_devices/test_ecobee_501.py +++ b/tests/components/homekit_controller/specific_devices/test_ecobee_501.py @@ -9,7 +9,7 @@ from homeassistant.components.climate.const import ( ) from homeassistant.const import STATE_ON -from tests.components.homekit_controller.common import ( +from ..common import ( HUB_TEST_ACCESSORY_ID, DeviceTestInfo, EntityTestInfo, diff --git a/tests/components/homekit_controller/specific_devices/test_ecobee_occupancy.py b/tests/components/homekit_controller/specific_devices/test_ecobee_occupancy.py index 6d98467a4cb..20dae666c69 100644 --- a/tests/components/homekit_controller/specific_devices/test_ecobee_occupancy.py +++ b/tests/components/homekit_controller/specific_devices/test_ecobee_occupancy.py @@ -4,7 +4,7 @@ Regression tests for Ecobee occupancy. https://github.com/home-assistant/core/issues/31827 """ -from tests.components.homekit_controller.common import ( +from ..common import ( HUB_TEST_ACCESSORY_ID, DeviceTestInfo, EntityTestInfo, diff --git a/tests/components/homekit_controller/specific_devices/test_eve_degree.py b/tests/components/homekit_controller/specific_devices/test_eve_degree.py index 55377801529..c3ca4b22f1a 100644 --- a/tests/components/homekit_controller/specific_devices/test_eve_degree.py +++ b/tests/components/homekit_controller/specific_devices/test_eve_degree.py @@ -5,7 +5,7 @@ from homeassistant.components.sensor import SensorStateClass from homeassistant.const import PERCENTAGE, PRESSURE_HPA, TEMP_CELSIUS from homeassistant.helpers.entity import EntityCategory -from tests.components.homekit_controller.common import ( +from ..common import ( HUB_TEST_ACCESSORY_ID, DeviceTestInfo, EntityTestInfo, 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 70ae4a1db23..292ab9c66ac 100644 --- a/tests/components/homekit_controller/specific_devices/test_eve_energy.py +++ b/tests/components/homekit_controller/specific_devices/test_eve_energy.py @@ -9,7 +9,7 @@ from homeassistant.const import ( ) from homeassistant.helpers.entity import EntityCategory -from tests.components.homekit_controller.common import ( +from ..common import ( HUB_TEST_ACCESSORY_ID, DeviceTestInfo, EntityTestInfo, diff --git a/tests/components/homekit_controller/specific_devices/test_haa_fan.py b/tests/components/homekit_controller/specific_devices/test_haa_fan.py index 39169ea5af9..307b7fa9408 100644 --- a/tests/components/homekit_controller/specific_devices/test_haa_fan.py +++ b/tests/components/homekit_controller/specific_devices/test_haa_fan.py @@ -3,7 +3,7 @@ from homeassistant.components.fan import ATTR_PERCENTAGE, SUPPORT_SET_SPEED from homeassistant.helpers.entity import EntityCategory -from tests.components.homekit_controller.common import ( +from ..common import ( HUB_TEST_ACCESSORY_ID, DeviceTestInfo, EntityTestInfo, diff --git a/tests/components/homekit_controller/specific_devices/test_homeassistant_bridge.py b/tests/components/homekit_controller/specific_devices/test_homeassistant_bridge.py index 76fab93b013..7c5e71f31b0 100644 --- a/tests/components/homekit_controller/specific_devices/test_homeassistant_bridge.py +++ b/tests/components/homekit_controller/specific_devices/test_homeassistant_bridge.py @@ -6,7 +6,7 @@ from homeassistant.components.fan import ( SUPPORT_SET_SPEED, ) -from tests.components.homekit_controller.common import ( +from ..common import ( HUB_TEST_ACCESSORY_ID, DeviceTestInfo, EntityTestInfo, diff --git a/tests/components/homekit_controller/specific_devices/test_hue_bridge.py b/tests/components/homekit_controller/specific_devices/test_hue_bridge.py index e64dc8378c5..ac6d8bdafa6 100644 --- a/tests/components/homekit_controller/specific_devices/test_hue_bridge.py +++ b/tests/components/homekit_controller/specific_devices/test_hue_bridge.py @@ -3,7 +3,7 @@ from homeassistant.components.sensor import SensorStateClass from homeassistant.const import PERCENTAGE -from tests.components.homekit_controller.common import ( +from ..common import ( HUB_TEST_ACCESSORY_ID, DeviceTestInfo, DeviceTriggerInfo, diff --git a/tests/components/homekit_controller/specific_devices/test_koogeek_ls1.py b/tests/components/homekit_controller/specific_devices/test_koogeek_ls1.py index 74525af1daf..99f34491e86 100644 --- a/tests/components/homekit_controller/specific_devices/test_koogeek_ls1.py +++ b/tests/components/homekit_controller/specific_devices/test_koogeek_ls1.py @@ -11,8 +11,7 @@ import pytest from homeassistant.helpers.entity import EntityCategory import homeassistant.util.dt as dt_util -from tests.common import async_fire_time_changed -from tests.components.homekit_controller.common import ( +from ..common import ( HUB_TEST_ACCESSORY_ID, DeviceTestInfo, EntityTestInfo, @@ -22,6 +21,8 @@ from tests.components.homekit_controller.common import ( setup_test_accessories, ) +from tests.common import async_fire_time_changed + LIGHT_ON = ("lightbulb", "on") diff --git a/tests/components/homekit_controller/specific_devices/test_koogeek_p1eu.py b/tests/components/homekit_controller/specific_devices/test_koogeek_p1eu.py index bf8c86b7a7d..7df1cee54d5 100644 --- a/tests/components/homekit_controller/specific_devices/test_koogeek_p1eu.py +++ b/tests/components/homekit_controller/specific_devices/test_koogeek_p1eu.py @@ -3,7 +3,7 @@ from homeassistant.components.sensor import SensorStateClass from homeassistant.const import POWER_WATT -from tests.components.homekit_controller.common import ( +from ..common import ( HUB_TEST_ACCESSORY_ID, DeviceTestInfo, EntityTestInfo, diff --git a/tests/components/homekit_controller/specific_devices/test_koogeek_sw2.py b/tests/components/homekit_controller/specific_devices/test_koogeek_sw2.py index 8307dc72f22..210fec0aafc 100644 --- a/tests/components/homekit_controller/specific_devices/test_koogeek_sw2.py +++ b/tests/components/homekit_controller/specific_devices/test_koogeek_sw2.py @@ -9,7 +9,7 @@ It should have 2 entities - the actual switch and a sensor for power usage. from homeassistant.components.sensor import SensorStateClass from homeassistant.const import POWER_WATT -from tests.components.homekit_controller.common import ( +from ..common import ( HUB_TEST_ACCESSORY_ID, DeviceTestInfo, EntityTestInfo, diff --git a/tests/components/homekit_controller/specific_devices/test_lennox_e30.py b/tests/components/homekit_controller/specific_devices/test_lennox_e30.py index a53916cc0a9..9bb31394cbd 100644 --- a/tests/components/homekit_controller/specific_devices/test_lennox_e30.py +++ b/tests/components/homekit_controller/specific_devices/test_lennox_e30.py @@ -9,7 +9,7 @@ from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE_RANGE, ) -from tests.components.homekit_controller.common import ( +from ..common import ( HUB_TEST_ACCESSORY_ID, DeviceTestInfo, EntityTestInfo, diff --git a/tests/components/homekit_controller/specific_devices/test_lg_tv.py b/tests/components/homekit_controller/specific_devices/test_lg_tv.py index ec26d3a7247..26909dabc59 100644 --- a/tests/components/homekit_controller/specific_devices/test_lg_tv.py +++ b/tests/components/homekit_controller/specific_devices/test_lg_tv.py @@ -6,7 +6,7 @@ from homeassistant.components.media_player.const import ( SUPPORT_SELECT_SOURCE, ) -from tests.components.homekit_controller.common import ( +from ..common import ( HUB_TEST_ACCESSORY_ID, DeviceTestInfo, EntityTestInfo, diff --git a/tests/components/homekit_controller/specific_devices/test_lutron_caseta_bridge.py b/tests/components/homekit_controller/specific_devices/test_lutron_caseta_bridge.py index 8961eb414fa..9df8cf4e5ae 100644 --- a/tests/components/homekit_controller/specific_devices/test_lutron_caseta_bridge.py +++ b/tests/components/homekit_controller/specific_devices/test_lutron_caseta_bridge.py @@ -2,7 +2,7 @@ from homeassistant.const import STATE_OFF -from tests.components.homekit_controller.common import ( +from ..common import ( HUB_TEST_ACCESSORY_ID, DeviceTestInfo, EntityTestInfo, diff --git a/tests/components/homekit_controller/specific_devices/test_mss425f.py b/tests/components/homekit_controller/specific_devices/test_mss425f.py index 3fe0ee739e6..6db4140bd75 100644 --- a/tests/components/homekit_controller/specific_devices/test_mss425f.py +++ b/tests/components/homekit_controller/specific_devices/test_mss425f.py @@ -4,7 +4,7 @@ from homeassistant.const import STATE_ON, STATE_UNKNOWN from homeassistant.helpers.entity import EntityCategory -from tests.components.homekit_controller.common import ( +from ..common import ( HUB_TEST_ACCESSORY_ID, DeviceTestInfo, EntityTestInfo, diff --git a/tests/components/homekit_controller/specific_devices/test_mss565.py b/tests/components/homekit_controller/specific_devices/test_mss565.py index 0045a5ec507..1a9c5bbbf6f 100644 --- a/tests/components/homekit_controller/specific_devices/test_mss565.py +++ b/tests/components/homekit_controller/specific_devices/test_mss565.py @@ -3,7 +3,7 @@ from homeassistant.const import STATE_ON -from tests.components.homekit_controller.common import ( +from ..common import ( HUB_TEST_ACCESSORY_ID, DeviceTestInfo, EntityTestInfo, diff --git a/tests/components/homekit_controller/specific_devices/test_mysa_living.py b/tests/components/homekit_controller/specific_devices/test_mysa_living.py index 1a3bcdb2271..2161de08b7b 100644 --- a/tests/components/homekit_controller/specific_devices/test_mysa_living.py +++ b/tests/components/homekit_controller/specific_devices/test_mysa_living.py @@ -4,7 +4,7 @@ from homeassistant.components.climate import SUPPORT_TARGET_TEMPERATURE from homeassistant.components.sensor import SensorStateClass from homeassistant.const import PERCENTAGE, TEMP_CELSIUS -from tests.components.homekit_controller.common import ( +from ..common import ( HUB_TEST_ACCESSORY_ID, DeviceTestInfo, EntityTestInfo, diff --git a/tests/components/homekit_controller/specific_devices/test_nanoleaf_strip_nl55.py b/tests/components/homekit_controller/specific_devices/test_nanoleaf_strip_nl55.py index 550c3a328d0..c66ea0d76a9 100644 --- a/tests/components/homekit_controller/specific_devices/test_nanoleaf_strip_nl55.py +++ b/tests/components/homekit_controller/specific_devices/test_nanoleaf_strip_nl55.py @@ -2,7 +2,7 @@ from homeassistant.helpers.entity import EntityCategory -from tests.components.homekit_controller.common import ( +from ..common import ( HUB_TEST_ACCESSORY_ID, DeviceTestInfo, EntityTestInfo, diff --git a/tests/components/homekit_controller/specific_devices/test_netamo_doorbell.py b/tests/components/homekit_controller/specific_devices/test_netamo_doorbell.py index 5d2b7fcbded..188bbaffedd 100644 --- a/tests/components/homekit_controller/specific_devices/test_netamo_doorbell.py +++ b/tests/components/homekit_controller/specific_devices/test_netamo_doorbell.py @@ -4,7 +4,7 @@ Regression tests for Netamo Doorbell. https://github.com/home-assistant/core/issues/44596 """ -from tests.components.homekit_controller.common import ( +from ..common import ( HUB_TEST_ACCESSORY_ID, DeviceTestInfo, DeviceTriggerInfo, diff --git a/tests/components/homekit_controller/specific_devices/test_rainmachine_pro_8.py b/tests/components/homekit_controller/specific_devices/test_rainmachine_pro_8.py index 6da8f13cbdd..ecea2cdafbb 100644 --- a/tests/components/homekit_controller/specific_devices/test_rainmachine_pro_8.py +++ b/tests/components/homekit_controller/specific_devices/test_rainmachine_pro_8.py @@ -4,7 +4,7 @@ Make sure that existing RainMachine support isn't broken. https://github.com/home-assistant/core/issues/31745 """ -from tests.components.homekit_controller.common import ( +from ..common import ( HUB_TEST_ACCESSORY_ID, DeviceTestInfo, EntityTestInfo, diff --git a/tests/components/homekit_controller/specific_devices/test_ryse_smart_bridge.py b/tests/components/homekit_controller/specific_devices/test_ryse_smart_bridge.py index 155eb1c5c27..a56ea9bdaf0 100644 --- a/tests/components/homekit_controller/specific_devices/test_ryse_smart_bridge.py +++ b/tests/components/homekit_controller/specific_devices/test_ryse_smart_bridge.py @@ -8,7 +8,7 @@ from homeassistant.components.cover import ( from homeassistant.components.sensor import SensorStateClass from homeassistant.const import PERCENTAGE -from tests.components.homekit_controller.common import ( +from ..common import ( HUB_TEST_ACCESSORY_ID, DeviceTestInfo, EntityTestInfo, diff --git a/tests/components/homekit_controller/specific_devices/test_schlage_sense.py b/tests/components/homekit_controller/specific_devices/test_schlage_sense.py index d572989e345..0a59ec6f70a 100644 --- a/tests/components/homekit_controller/specific_devices/test_schlage_sense.py +++ b/tests/components/homekit_controller/specific_devices/test_schlage_sense.py @@ -1,7 +1,7 @@ """Make sure that Schlage Sense is enumerated properly.""" -from tests.components.homekit_controller.common import ( +from ..common import ( HUB_TEST_ACCESSORY_ID, DeviceTestInfo, EntityTestInfo, diff --git a/tests/components/homekit_controller/specific_devices/test_simpleconnect_fan.py b/tests/components/homekit_controller/specific_devices/test_simpleconnect_fan.py index f160169a43c..036dde1af12 100644 --- a/tests/components/homekit_controller/specific_devices/test_simpleconnect_fan.py +++ b/tests/components/homekit_controller/specific_devices/test_simpleconnect_fan.py @@ -6,7 +6,7 @@ https://github.com/home-assistant/core/issues/26180 from homeassistant.components.fan import SUPPORT_DIRECTION, SUPPORT_SET_SPEED -from tests.components.homekit_controller.common import ( +from ..common import ( HUB_TEST_ACCESSORY_ID, DeviceTestInfo, EntityTestInfo, diff --git a/tests/components/homekit_controller/specific_devices/test_velux_gateway.py b/tests/components/homekit_controller/specific_devices/test_velux_gateway.py index 07c35fb867d..7442d84c224 100644 --- a/tests/components/homekit_controller/specific_devices/test_velux_gateway.py +++ b/tests/components/homekit_controller/specific_devices/test_velux_gateway.py @@ -16,7 +16,7 @@ from homeassistant.const import ( TEMP_CELSIUS, ) -from tests.components.homekit_controller.common import ( +from ..common import ( HUB_TEST_ACCESSORY_ID, DeviceTestInfo, EntityTestInfo, diff --git a/tests/components/homekit_controller/specific_devices/test_vocolinc_flowerbud.py b/tests/components/homekit_controller/specific_devices/test_vocolinc_flowerbud.py index f788b016ba2..b0f9216731b 100644 --- a/tests/components/homekit_controller/specific_devices/test_vocolinc_flowerbud.py +++ b/tests/components/homekit_controller/specific_devices/test_vocolinc_flowerbud.py @@ -6,7 +6,7 @@ from homeassistant.components.sensor import SensorStateClass from homeassistant.const import PERCENTAGE from homeassistant.helpers.entity import EntityCategory -from tests.components.homekit_controller.common import ( +from ..common import ( HUB_TEST_ACCESSORY_ID, DeviceTestInfo, EntityTestInfo, diff --git a/tests/components/homekit_controller/specific_devices/test_vocolinc_vp3.py b/tests/components/homekit_controller/specific_devices/test_vocolinc_vp3.py index 3a3579b8781..4037a44898e 100644 --- a/tests/components/homekit_controller/specific_devices/test_vocolinc_vp3.py +++ b/tests/components/homekit_controller/specific_devices/test_vocolinc_vp3.py @@ -3,7 +3,7 @@ from homeassistant.components.sensor import SensorStateClass from homeassistant.const import POWER_WATT -from tests.components.homekit_controller.common import ( +from ..common import ( HUB_TEST_ACCESSORY_ID, DeviceTestInfo, EntityTestInfo, diff --git a/tests/components/homekit_controller/test_alarm_control_panel.py b/tests/components/homekit_controller/test_alarm_control_panel.py index 2804ffff824..46979bd41f3 100644 --- a/tests/components/homekit_controller/test_alarm_control_panel.py +++ b/tests/components/homekit_controller/test_alarm_control_panel.py @@ -2,7 +2,7 @@ from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes -from tests.components.homekit_controller.common import setup_test_component +from .common import setup_test_component def create_security_system_service(accessory): diff --git a/tests/components/homekit_controller/test_binary_sensor.py b/tests/components/homekit_controller/test_binary_sensor.py index ff9877b2e44..e9cd9284332 100644 --- a/tests/components/homekit_controller/test_binary_sensor.py +++ b/tests/components/homekit_controller/test_binary_sensor.py @@ -4,7 +4,7 @@ from aiohomekit.model.services import ServicesTypes from homeassistant.components.binary_sensor import BinarySensorDeviceClass -from tests.components.homekit_controller.common import setup_test_component +from .common import setup_test_component def create_motion_sensor_service(accessory): diff --git a/tests/components/homekit_controller/test_button.py b/tests/components/homekit_controller/test_button.py index 79dbc59a38a..58c1feb8900 100644 --- a/tests/components/homekit_controller/test_button.py +++ b/tests/components/homekit_controller/test_button.py @@ -2,7 +2,7 @@ from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes -from tests.components.homekit_controller.common import Helper, setup_test_component +from .common import Helper, setup_test_component def create_switch_with_setup_button(accessory): diff --git a/tests/components/homekit_controller/test_camera.py b/tests/components/homekit_controller/test_camera.py index ddb25fddffc..e0ba609b30e 100644 --- a/tests/components/homekit_controller/test_camera.py +++ b/tests/components/homekit_controller/test_camera.py @@ -6,7 +6,7 @@ from aiohomekit.testing import FAKE_CAMERA_IMAGE from homeassistant.components import camera -from tests.components.homekit_controller.common import setup_test_component +from .common import setup_test_component def create_camera(accessory): diff --git a/tests/components/homekit_controller/test_climate.py b/tests/components/homekit_controller/test_climate.py index c750c428437..3a939948595 100644 --- a/tests/components/homekit_controller/test_climate.py +++ b/tests/components/homekit_controller/test_climate.py @@ -18,7 +18,7 @@ from homeassistant.components.climate.const import ( HVACMode, ) -from tests.components.homekit_controller.common import setup_test_component +from .common import setup_test_component # Test thermostat devices diff --git a/tests/components/homekit_controller/test_connection.py b/tests/components/homekit_controller/test_connection.py index 0099900b585..9db07a45d16 100644 --- a/tests/components/homekit_controller/test_connection.py +++ b/tests/components/homekit_controller/test_connection.py @@ -14,12 +14,9 @@ from homeassistant.components.homekit_controller.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr +from .common import setup_accessories_from_file, setup_platform, setup_test_accessories + from tests.common import MockConfigEntry -from tests.components.homekit_controller.common import ( - setup_accessories_from_file, - setup_platform, - setup_test_accessories, -) @dataclasses.dataclass diff --git a/tests/components/homekit_controller/test_cover.py b/tests/components/homekit_controller/test_cover.py index 35e6933f2cd..15422f2f0bc 100644 --- a/tests/components/homekit_controller/test_cover.py +++ b/tests/components/homekit_controller/test_cover.py @@ -2,7 +2,7 @@ from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes -from tests.components.homekit_controller.common import setup_test_component +from .common import setup_test_component def create_window_covering_service(accessory): diff --git a/tests/components/homekit_controller/test_device_trigger.py b/tests/components/homekit_controller/test_device_trigger.py index 2c7f3d73e83..22404063663 100644 --- a/tests/components/homekit_controller/test_device_trigger.py +++ b/tests/components/homekit_controller/test_device_trigger.py @@ -9,13 +9,14 @@ from homeassistant.components.homekit_controller.const import DOMAIN from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component +from .common import setup_test_component + from tests.common import ( assert_lists_same, async_get_device_automations, async_mock_service, ) from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401 -from tests.components.homekit_controller.common import setup_test_component # pylint: disable=redefined-outer-name diff --git a/tests/components/homekit_controller/test_diagnostics.py b/tests/components/homekit_controller/test_diagnostics.py index dd0e35b0d1e..eecb7ff51f8 100644 --- a/tests/components/homekit_controller/test_diagnostics.py +++ b/tests/components/homekit_controller/test_diagnostics.py @@ -5,14 +5,12 @@ from homeassistant.components.homekit_controller.const import KNOWN_DEVICES from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr +from .common import setup_accessories_from_file, setup_test_accessories + from tests.components.diagnostics import ( get_diagnostics_for_config_entry, get_diagnostics_for_device, ) -from tests.components.homekit_controller.common import ( - setup_accessories_from_file, - setup_test_accessories, -) async def test_config_entry(hass: HomeAssistant, hass_client: ClientSession, utcnow): diff --git a/tests/components/homekit_controller/test_fan.py b/tests/components/homekit_controller/test_fan.py index 9d166531562..de13772b5a1 100644 --- a/tests/components/homekit_controller/test_fan.py +++ b/tests/components/homekit_controller/test_fan.py @@ -2,7 +2,7 @@ from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes -from tests.components.homekit_controller.common import setup_test_component +from .common import setup_test_component def create_fan_service(accessory): diff --git a/tests/components/homekit_controller/test_humidifier.py b/tests/components/homekit_controller/test_humidifier.py index ea32b1931c2..4b631f19b7a 100644 --- a/tests/components/homekit_controller/test_humidifier.py +++ b/tests/components/homekit_controller/test_humidifier.py @@ -5,7 +5,7 @@ from aiohomekit.model.services import ServicesTypes from homeassistant.components.humidifier import DOMAIN from homeassistant.components.humidifier.const import MODE_AUTO, MODE_NORMAL -from tests.components.homekit_controller.common import setup_test_component +from .common import setup_test_component def create_humidifier_service(accessory): diff --git a/tests/components/homekit_controller/test_init.py b/tests/components/homekit_controller/test_init.py index a91700f699c..456de9c4208 100644 --- a/tests/components/homekit_controller/test_init.py +++ b/tests/components/homekit_controller/test_init.py @@ -18,10 +18,14 @@ from homeassistant.helpers.entity_registry import EntityRegistry from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow -from .common import Helper, remove_device, setup_test_accessories_with_controller +from .common import ( + Helper, + remove_device, + setup_test_accessories_with_controller, + setup_test_component, +) from tests.common import async_fire_time_changed -from tests.components.homekit_controller.common import setup_test_component ALIVE_DEVICE_NAME = "testdevice" ALIVE_DEVICE_ENTITY_ID = "light.testdevice" diff --git a/tests/components/homekit_controller/test_light.py b/tests/components/homekit_controller/test_light.py index 83bddf3d26a..726be15a32c 100644 --- a/tests/components/homekit_controller/test_light.py +++ b/tests/components/homekit_controller/test_light.py @@ -10,7 +10,7 @@ from homeassistant.components.light import ( ) from homeassistant.const import ATTR_SUPPORTED_FEATURES, STATE_UNAVAILABLE -from tests.components.homekit_controller.common import setup_test_component +from .common import setup_test_component LIGHT_BULB_NAME = "TestDevice" LIGHT_BULB_ENTITY_ID = "light.testdevice" diff --git a/tests/components/homekit_controller/test_lock.py b/tests/components/homekit_controller/test_lock.py index 8f996cea8e3..af21f26a012 100644 --- a/tests/components/homekit_controller/test_lock.py +++ b/tests/components/homekit_controller/test_lock.py @@ -2,7 +2,7 @@ from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes -from tests.components.homekit_controller.common import setup_test_component +from .common import setup_test_component def create_lock_service(accessory): diff --git a/tests/components/homekit_controller/test_media_player.py b/tests/components/homekit_controller/test_media_player.py index d45e785f315..7fb8c4edb2a 100644 --- a/tests/components/homekit_controller/test_media_player.py +++ b/tests/components/homekit_controller/test_media_player.py @@ -6,7 +6,7 @@ from aiohomekit.model.characteristics import ( from aiohomekit.model.services import ServicesTypes import pytest -from tests.components.homekit_controller.common import setup_test_component +from .common import setup_test_component def create_tv_service(accessory): diff --git a/tests/components/homekit_controller/test_number.py b/tests/components/homekit_controller/test_number.py index 6b375b60d9b..6d060416861 100644 --- a/tests/components/homekit_controller/test_number.py +++ b/tests/components/homekit_controller/test_number.py @@ -2,7 +2,7 @@ from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes -from tests.components.homekit_controller.common import Helper, setup_test_component +from .common import Helper, setup_test_component def create_switch_with_spray_level(accessory): diff --git a/tests/components/homekit_controller/test_select.py b/tests/components/homekit_controller/test_select.py index 55d5b168abe..22cd53d7a31 100644 --- a/tests/components/homekit_controller/test_select.py +++ b/tests/components/homekit_controller/test_select.py @@ -3,7 +3,7 @@ from aiohomekit.model import Accessory from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes -from tests.components.homekit_controller.common import Helper, setup_test_component +from .common import Helper, setup_test_component def create_service_with_ecobee_mode(accessory: Accessory): diff --git a/tests/components/homekit_controller/test_sensor.py b/tests/components/homekit_controller/test_sensor.py index 21112937939..0adfec470c4 100644 --- a/tests/components/homekit_controller/test_sensor.py +++ b/tests/components/homekit_controller/test_sensor.py @@ -10,7 +10,7 @@ from homeassistant.components.homekit_controller.sensor import ( ) from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass -from tests.components.homekit_controller.common import Helper, setup_test_component +from .common import Helper, setup_test_component def create_temperature_sensor_service(accessory): diff --git a/tests/components/homekit_controller/test_storage.py b/tests/components/homekit_controller/test_storage.py index 13d613e3916..f523856b1ea 100644 --- a/tests/components/homekit_controller/test_storage.py +++ b/tests/components/homekit_controller/test_storage.py @@ -5,11 +5,9 @@ from aiohomekit.model.services import ServicesTypes from homeassistant.components.homekit_controller.const import ENTITY_MAP from homeassistant.components.homekit_controller.storage import EntityMapStorage +from .common import setup_platform, setup_test_component + from tests.common import flush_store -from tests.components.homekit_controller.common import ( - setup_platform, - setup_test_component, -) async def test_load_from_storage(hass, hass_storage): diff --git a/tests/components/homekit_controller/test_switch.py b/tests/components/homekit_controller/test_switch.py index 9fafa4afada..a034624bd60 100644 --- a/tests/components/homekit_controller/test_switch.py +++ b/tests/components/homekit_controller/test_switch.py @@ -7,7 +7,7 @@ from aiohomekit.model.characteristics import ( ) from aiohomekit.model.services import ServicesTypes -from tests.components.homekit_controller.common import setup_test_component +from .common import setup_test_component def create_switch_service(accessory): From b9d34ce169a17f51769a668634c52d250e6e7952 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 19 Sep 2022 09:59:12 +0200 Subject: [PATCH 539/955] Improve sonos typing (#78661) --- homeassistant/components/sonos/favorites.py | 2 +- .../components/sonos/media_browser.py | 21 ++++++++++++------- homeassistant/components/sonos/speaker.py | 6 +++--- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/sonos/favorites.py b/homeassistant/components/sonos/favorites.py index 5284a5f6745..eeeb210b9ec 100644 --- a/homeassistant/components/sonos/favorites.py +++ b/homeassistant/components/sonos/favorites.py @@ -32,7 +32,7 @@ class SonosFavorites(SonosHouseholdCoordinator): self._favorites: list[DidlFavorite] = [] self.last_polled_ids: dict[str, int] = {} - def __iter__(self) -> Iterator: + def __iter__(self) -> Iterator[DidlFavorite]: """Return an iterator for the known favorites.""" favorites = self._favorites.copy() return iter(favorites) diff --git a/homeassistant/components/sonos/media_browser.py b/homeassistant/components/sonos/media_browser.py index 187c0dbef55..713d48fea55 100644 --- a/homeassistant/components/sonos/media_browser.py +++ b/homeassistant/components/sonos/media_browser.py @@ -7,13 +7,17 @@ from functools import partial import logging from typing import cast -from soco.data_structures import DidlFavorite, DidlObject +from soco.data_structures import DidlObject from soco.ms_data_structures import MusicServiceItem from soco.music_library import MusicLibrary from homeassistant.components import media_source, plex, spotify -from homeassistant.components.media_player import BrowseMedia, MediaClass, MediaType -from homeassistant.components.media_player.errors import BrowseError +from homeassistant.components.media_player import ( + BrowseError, + BrowseMedia, + MediaClass, + MediaType, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.network import is_internal_request @@ -32,6 +36,7 @@ from .const import ( SONOS_TYPES_MAPPING, ) from .exception import UnknownMediaType +from .favorites import SonosFavorites from .speaker import SonosMedia, SonosSpeaker _LOGGER = logging.getLogger(__name__) @@ -363,15 +368,15 @@ def library_payload(media_library: MusicLibrary, get_thumbnail_url=None) -> Brow ) -def favorites_payload(favorites: list[DidlFavorite]) -> BrowseMedia: +def favorites_payload(favorites: SonosFavorites) -> BrowseMedia: """ Create response payload to describe contents of a specific library. Used by async_browse_media. """ - children = [] + children: list[BrowseMedia] = [] - group_types = {fav.reference.item_class for fav in favorites} + group_types: set[str] = {fav.reference.item_class for fav in favorites} for group_type in sorted(group_types): try: media_content_type = SONOS_TYPES_MAPPING[group_type] @@ -402,13 +407,13 @@ def favorites_payload(favorites: list[DidlFavorite]) -> BrowseMedia: def favorites_folder_payload( - favorites: list[DidlFavorite], media_content_id: str + favorites: SonosFavorites, media_content_id: str ) -> BrowseMedia: """Create response payload to describe all items of a type of favorite. Used by async_browse_media. """ - children = [] + children: list[BrowseMedia] = [] content_type = SONOS_TYPES_MAPPING[media_content_id] for favorite in favorites: diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 516a431295a..98984eedc03 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -2,7 +2,7 @@ from __future__ import annotations import asyncio -from collections.abc import Callable, Coroutine +from collections.abc import Callable, Collection, Coroutine import contextlib import datetime from functools import partial @@ -946,7 +946,7 @@ class SonosSpeaker: ) -> None: """Snapshot all the speakers and optionally their groups.""" - def _snapshot_all(speakers: list[SonosSpeaker]) -> None: + def _snapshot_all(speakers: Collection[SonosSpeaker]) -> None: """Sync helper.""" for speaker in speakers: speaker.snapshot(with_group) @@ -1032,7 +1032,7 @@ class SonosSpeaker: return groups - def _restore_players(speakers: list[SonosSpeaker]) -> None: + def _restore_players(speakers: Collection[SonosSpeaker]) -> None: """Restore state of all players.""" for speaker in (s for s in speakers if not s.is_coordinator): speaker.restore() From 72bf1ca6dd2f9857306a4ad78035421737cd63f5 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 19 Sep 2022 10:21:48 +0200 Subject: [PATCH 540/955] Use attributes in kef media player (#77650) --- homeassistant/components/kef/media_player.py | 85 +++++--------------- 1 file changed, 19 insertions(+), 66 deletions(-) diff --git a/homeassistant/components/kef/media_player.py b/homeassistant/components/kef/media_player.py index a28819be406..078354c705e 100644 --- a/homeassistant/components/kef/media_player.py +++ b/homeassistant/components/kef/media_player.py @@ -184,6 +184,8 @@ async def async_setup_platform( class KefMediaPlayer(MediaPlayerEntity): """Kef Player Object.""" + _attr_icon = "mdi:speaker-wireless" + def __init__( self, name, @@ -200,8 +202,8 @@ class KefMediaPlayer(MediaPlayerEntity): unique_id, ): """Initialize the media player.""" - self._name = name - self._sources = sources + self._attr_name = name + self._attr_source_list = sources self._speaker = AsyncKefSpeaker( host, port, @@ -211,15 +213,11 @@ class KefMediaPlayer(MediaPlayerEntity): inverse_speaker_mode, loop=loop, ) - self._unique_id = unique_id + self._attr_unique_id = unique_id self._supports_on = supports_on self._speaker_type = speaker_type - self._state = None - self._muted = None - self._source = None - self._volume = None - self._is_online = None + self._attr_available = False self._dsp = None self._update_dsp_task_remover = None @@ -237,77 +235,32 @@ class KefMediaPlayer(MediaPlayerEntity): if supports_on: self._attr_supported_features |= MediaPlayerEntityFeature.TURN_ON - @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def state(self): - """Return the state of the device.""" - return self._state - async def async_update(self) -> None: """Update latest state.""" _LOGGER.debug("Running async_update") try: - self._is_online = await self._speaker.is_online() - if self._is_online: + self._attr_available = await self._speaker.is_online() + if self.available: ( - self._volume, - self._muted, + self._attr_volume_level, + self._attr_is_volume_muted, ) = await self._speaker.get_volume_and_is_muted() state = await self._speaker.get_state() - self._source = state.source - self._state = ( + self._attr_source = state.source + self._attr_state = ( MediaPlayerState.ON if state.is_on else MediaPlayerState.OFF ) if self._dsp is None: # Only do this when necessary because it is a slow operation await self.update_dsp() else: - self._muted = None - self._source = None - self._volume = None - self._state = MediaPlayerState.OFF + self._attr_is_volume_muted = None + self._attr_source = None + self._attr_volume_level = None + self._attr_state = MediaPlayerState.OFF except (ConnectionError, TimeoutError) as err: _LOGGER.debug("Error in `update`: %s", err) - self._state = None - - @property - def volume_level(self): - """Volume level of the media player (0..1).""" - return self._volume - - @property - def is_volume_muted(self): - """Boolean if volume is currently muted.""" - return self._muted - - @property - def source(self): - """Name of the current input source.""" - return self._source - - @property - def source_list(self): - """List of available input sources.""" - return self._sources - - @property - def available(self): - """Return if the speaker is reachable online.""" - return self._is_online - - @property - def unique_id(self): - """Return the device unique id.""" - return self._unique_id - - @property - def icon(self): - """Return the device's icon.""" - return "mdi:speaker-wireless" + self._attr_state = None async def async_turn_off(self) -> None: """Turn the media player off.""" @@ -340,7 +293,7 @@ class KefMediaPlayer(MediaPlayerEntity): async def async_select_source(self, source: str) -> None: """Select input source.""" - if source in self.source_list: + if self.source_list is not None and source in self.source_list: await self._speaker.set_source(source) else: raise ValueError(f"Unknown input source: {source}.") @@ -363,7 +316,7 @@ class KefMediaPlayer(MediaPlayerEntity): async def update_dsp(self, _=None) -> None: """Update the DSP settings.""" - if self._speaker_type == "LS50" and self._state == MediaPlayerState.OFF: + if self._speaker_type == "LS50" and self.state == MediaPlayerState.OFF: # The LSX is able to respond when off the LS50 has to be on. return From b5c1f856e2d6291bd195e921cf9b9f8d4a5b2acf Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 19 Sep 2022 10:25:25 +0200 Subject: [PATCH 541/955] Fix litterrobot tests (#78741) --- tests/components/litterrobot/test_sensor.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/components/litterrobot/test_sensor.py b/tests/components/litterrobot/test_sensor.py index 77668a9e592..1e56b7f06b2 100644 --- a/tests/components/litterrobot/test_sensor.py +++ b/tests/components/litterrobot/test_sensor.py @@ -1,6 +1,8 @@ """Test the Litter-Robot sensor entity.""" from unittest.mock import MagicMock +import pytest + from homeassistant.components.sensor import DOMAIN as PLATFORM_DOMAIN, SensorDeviceClass from homeassistant.const import MASS_POUNDS, PERCENTAGE, STATE_UNKNOWN from homeassistant.core import HomeAssistant @@ -66,6 +68,7 @@ async def test_gauge_icon() -> None: assert icon_for_gauge_level(100, 10) == GAUGE_FULL +@pytest.mark.freeze_time("2022-09-18 23:00:44+00:00") async def test_litter_robot_sensor( hass: HomeAssistant, mock_account_with_litterrobot_4: MagicMock ) -> None: @@ -85,7 +88,7 @@ async def test_litter_robot_sensor( assert sensor.state == "dfs" assert sensor.attributes["device_class"] == "litterrobot__status_code" sensor = hass.states.get("sensor.test_litter_level") - assert sensor.state == "70.0" + assert sensor.state == "0.0" assert sensor.attributes["unit_of_measurement"] == PERCENTAGE sensor = hass.states.get("sensor.test_pet_weight") assert sensor.state == "12.0" From 36eda3801d8b897fed6f6c16476ba5e8de858ed6 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 19 Sep 2022 11:11:05 +0200 Subject: [PATCH 542/955] Add LaMetric integration init tests (#78679) --- .coveragerc | 2 - tests/components/lametric/conftest.py | 15 ++++++ tests/components/lametric/test_init.py | 72 ++++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 tests/components/lametric/test_init.py diff --git a/.coveragerc b/.coveragerc index 811528b2911..1f73828daa4 100644 --- a/.coveragerc +++ b/.coveragerc @@ -648,9 +648,7 @@ omit = homeassistant/components/kostal_plenticore/switch.py homeassistant/components/kwb/sensor.py homeassistant/components/lacrosse/sensor.py - homeassistant/components/lametric/__init__.py homeassistant/components/lametric/button.py - homeassistant/components/lametric/coordinator.py homeassistant/components/lametric/entity.py homeassistant/components/lametric/notify.py homeassistant/components/lametric/number.py diff --git a/tests/components/lametric/conftest.py b/tests/components/lametric/conftest.py index 3640742c8ff..f040038743b 100644 --- a/tests/components/lametric/conftest.py +++ b/tests/components/lametric/conftest.py @@ -79,3 +79,18 @@ def mock_lametric_cloud_config_flow() -> Generator[MagicMock, None, None]: list[CloudDevice], load_fixture("cloud_devices.json", DOMAIN) ) yield lametric + + +@pytest.fixture +def mock_lametric() -> Generator[MagicMock, None, None]: + """Return a mocked LaMetric client.""" + with patch( + "homeassistant.components.lametric.coordinator.LaMetricDevice", autospec=True + ) as lametric_mock: + lametric = lametric_mock.return_value + lametric.api_key = "mock-api-key" + lametric.host = "127.0.0.1" + lametric.device.return_value = Device.parse_raw( + load_fixture("device.json", DOMAIN) + ) + yield lametric diff --git a/tests/components/lametric/test_init.py b/tests/components/lametric/test_init.py new file mode 100644 index 00000000000..965264e8917 --- /dev/null +++ b/tests/components/lametric/test_init.py @@ -0,0 +1,72 @@ +"""Tests for the LaMetric integration.""" +from collections.abc import Awaitable, Callable +from unittest.mock import MagicMock + +from aiohttp import ClientWebSocketResponse +from demetriek import LaMetricConnectionError, LaMetricConnectionTimeoutError +import pytest + +from homeassistant.components.lametric.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry +from tests.components.repairs import get_repairs + + +async def test_load_unload_config_entry( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_lametric: MagicMock, +) -> None: + """Test the LaMetric configuration entry loading/unloading.""" + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.LOADED + assert len(mock_lametric.device.mock_calls) == 1 + + await hass.config_entries.async_unload(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.NOT_LOADED + + +@pytest.mark.parametrize( + "side_effect", [LaMetricConnectionTimeoutError, LaMetricConnectionError] +) +async def test_config_entry_not_ready( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_lametric: MagicMock, + side_effect: Exception, +) -> None: + """Test the LaMetric configuration entry not ready.""" + mock_lametric.device.side_effect = side_effect + + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert len(mock_lametric.device.mock_calls) == 1 + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_yaml_config_raises_repairs( + hass: HomeAssistant, + hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]], + caplog: pytest.LogCaptureFixture, +) -> None: + """Test that YAML configuration raises an repairs issue.""" + await async_setup_component( + hass, DOMAIN, {DOMAIN: {CONF_CLIENT_ID: "foo", CONF_CLIENT_SECRET: "bar"}} + ) + + assert "The 'lametric' option is deprecated" in caplog.text + + issues = await get_repairs(hass, hass_ws_client) + assert len(issues) == 1 + assert issues[0]["issue_id"] == "manual_migration" From 019d297ff06889a7e9d30580b57d59de21fa5c86 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 19 Sep 2022 12:35:03 +0200 Subject: [PATCH 543/955] Apply hass-relative-import to recorder tests (#78734) --- tests/components/recorder/common.py | 3 ++- tests/components/recorder/test_history.py | 6 ++---- tests/components/recorder/test_statistics_v23_migration.py | 3 ++- tests/components/recorder/test_util.py | 3 +-- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/tests/components/recorder/common.py b/tests/components/recorder/common.py index 083630c7ea8..8d1929c7362 100644 --- a/tests/components/recorder/common.py +++ b/tests/components/recorder/common.py @@ -19,8 +19,9 @@ from homeassistant.components.recorder.tasks import RecorderTask, StatisticsTask from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util +from . import db_schema_0 + from tests.common import async_fire_time_changed, fire_time_changed -from tests.components.recorder import db_schema_0 DEFAULT_PURGE_TASKS = 3 diff --git a/tests/components/recorder/test_history.py b/tests/components/recorder/test_history.py index cc1d8e7faa7..f18ba0768ca 100644 --- a/tests/components/recorder/test_history.py +++ b/tests/components/recorder/test_history.py @@ -25,11 +25,9 @@ from homeassistant.core import HomeAssistant, State from homeassistant.helpers.json import JSONEncoder import homeassistant.util.dt as dt_util +from .common import async_wait_recording_done, wait_recording_done + from tests.common import SetupRecorderInstanceT, mock_state_change_event -from tests.components.recorder.common import ( - async_wait_recording_done, - wait_recording_done, -) async def _async_get_states( diff --git a/tests/components/recorder/test_statistics_v23_migration.py b/tests/components/recorder/test_statistics_v23_migration.py index a7cc2b35e61..fe6c95f7318 100644 --- a/tests/components/recorder/test_statistics_v23_migration.py +++ b/tests/components/recorder/test_statistics_v23_migration.py @@ -20,8 +20,9 @@ from homeassistant.helpers import recorder as recorder_helper from homeassistant.setup import setup_component import homeassistant.util.dt as dt_util +from .common import wait_recording_done + from tests.common import get_test_home_assistant -from tests.components.recorder.common import wait_recording_done ORIG_TZ = dt_util.DEFAULT_TIME_ZONE diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index ac4eeada3d3..70f4eb0da70 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -25,10 +25,9 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util -from .common import corrupt_db_file, run_information_with_session +from .common import corrupt_db_file, run_information_with_session, wait_recording_done from tests.common import SetupRecorderInstanceT, async_test_home_assistant -from tests.components.recorder.common import wait_recording_done def test_session_scope_not_setup(hass_recorder): From 747e538172104ea53ade373c2f4af25f9bd5ef15 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 19 Sep 2022 12:35:23 +0200 Subject: [PATCH 544/955] Apply hass-relative-import to bluetooth tests (#78736) --- .../bluetooth/test_active_update_coordinator.py | 2 +- .../bluetooth/test_passive_update_coordinator.py | 3 +-- .../components/bluetooth/test_passive_update_processor.py | 8 ++++---- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/tests/components/bluetooth/test_active_update_coordinator.py b/tests/components/bluetooth/test_active_update_coordinator.py index 318934d77b7..000a415b19b 100644 --- a/tests/components/bluetooth/test_active_update_coordinator.py +++ b/tests/components/bluetooth/test_active_update_coordinator.py @@ -20,7 +20,7 @@ from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo from homeassistant.setup import async_setup_component -from tests.components.bluetooth import inject_bluetooth_service_info +from . import inject_bluetooth_service_info _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/bluetooth/test_passive_update_coordinator.py b/tests/components/bluetooth/test_passive_update_coordinator.py index 15845abc5ac..b8ade8c39f9 100644 --- a/tests/components/bluetooth/test_passive_update_coordinator.py +++ b/tests/components/bluetooth/test_passive_update_coordinator.py @@ -20,10 +20,9 @@ from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util -from . import patch_all_discovered_devices +from . import inject_bluetooth_service_info, patch_all_discovered_devices from tests.common import async_fire_time_changed -from tests.components.bluetooth import inject_bluetooth_service_info _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/bluetooth/test_passive_update_processor.py b/tests/components/bluetooth/test_passive_update_processor.py index 482f3b3a94f..0ca5f299a50 100644 --- a/tests/components/bluetooth/test_passive_update_processor.py +++ b/tests/components/bluetooth/test_passive_update_processor.py @@ -34,14 +34,14 @@ from homeassistant.helpers.entity import DeviceInfo from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util -from . import patch_all_discovered_devices - -from tests.common import MockEntityPlatform, async_fire_time_changed -from tests.components.bluetooth import ( +from . import ( inject_bluetooth_service_info, inject_bluetooth_service_info_bleak, + patch_all_discovered_devices, ) +from tests.common import MockEntityPlatform, async_fire_time_changed + _LOGGER = logging.getLogger(__name__) From f30a94daa187614dc94e0196e5b5b489d6b17136 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 19 Sep 2022 12:35:59 +0200 Subject: [PATCH 545/955] Improve type hints in keenetic_ndms2 (#77649) --- homeassistant/components/keenetic_ndms2/device_tracker.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/keenetic_ndms2/device_tracker.py b/homeassistant/components/keenetic_ndms2/device_tracker.py index ca51b9ba4aa..a2b01040a5a 100644 --- a/homeassistant/components/keenetic_ndms2/device_tracker.py +++ b/homeassistant/components/keenetic_ndms2/device_tracker.py @@ -97,10 +97,10 @@ class KeeneticTracker(ScannerEntity): ) @property - def is_connected(self): + def is_connected(self) -> bool: """Return true if the device is connected to the network.""" return ( - self._last_seen + self._last_seen is not None and (dt_util.utcnow() - self._last_seen) < self._router.consider_home_interval ) From cd6959d809c0a34bc8239df8d20251f9819f5bd3 Mon Sep 17 00:00:00 2001 From: Jorim Tielemans Date: Mon, 19 Sep 2022 12:56:08 +0200 Subject: [PATCH 546/955] Update psutil to 5.9.2 (#78745) --- 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 398614815a2..8e9f6d3e896 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.1"], + "requirements": ["psutil==5.9.2"], "codeowners": [], "iot_class": "local_push", "loggers": ["psutil"] diff --git a/requirements_all.txt b/requirements_all.txt index 8abdbd3a8f9..36c15c0f9db 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1325,7 +1325,7 @@ proxmoxer==1.3.1 psutil-home-assistant==0.0.1 # homeassistant.components.systemmonitor -psutil==5.9.1 +psutil==5.9.2 # homeassistant.components.pulseaudio_loopback pulsectl==20.2.4 From 0dcbc8568422ece1a4c4b85df12c7fbbceb8a69f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 19 Sep 2022 12:57:07 +0200 Subject: [PATCH 547/955] Adjust relative-import plugin for tests (#78742) --- pylint/plugins/hass_imports.py | 29 +++++++++++++++-------------- tests/pylint/test_imports.py | 13 +++++++++++++ 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/pylint/plugins/hass_imports.py b/pylint/plugins/hass_imports.py index d3e6412d301..4b57794a07f 100644 --- a/pylint/plugins/hass_imports.py +++ b/pylint/plugins/hass_imports.py @@ -345,8 +345,10 @@ class HassImportsFormatChecker(BaseChecker): # type: ignore[misc] self, current_package: str, node: nodes.ImportFrom ) -> None: """Called when a ImportFrom node is visited.""" - if node.level <= 1 or not current_package.startswith( - "homeassistant.components" + if ( + node.level <= 1 + or not current_package.startswith("homeassistant.components.") + and not current_package.startswith("tests.components.") ): return split_package = current_package.split(".") @@ -372,18 +374,17 @@ class HassImportsFormatChecker(BaseChecker): # type: ignore[misc] ): self.add_message("hass-relative-import", node=node) return - if self.current_package.startswith("homeassistant.components."): - current_component = self.current_package.split(".")[2] - if node.modname == "homeassistant.components": - for name in node.names: - if name[0] == current_component: - self.add_message("hass-relative-import", node=node) - return - if node.modname.startswith( - f"homeassistant.components.{current_component}." - ): - self.add_message("hass-relative-import", node=node) - return + for root in ("homeassistant", "tests"): + if self.current_package.startswith(f"{root}.components."): + current_component = self.current_package.split(".")[2] + if node.modname == f"{root}.components": + for name in node.names: + if name[0] == current_component: + self.add_message("hass-relative-import", node=node) + return + if node.modname.startswith(f"{root}.components.{current_component}."): + self.add_message("hass-relative-import", node=node) + return if node.modname.startswith("homeassistant.components.") and ( node.modname.endswith(".const") or "const" in {names[0] for names in node.names} diff --git a/tests/pylint/test_imports.py b/tests/pylint/test_imports.py index fadaaf159a3..130947ac68d 100644 --- a/tests/pylint/test_imports.py +++ b/tests/pylint/test_imports.py @@ -35,6 +35,7 @@ from . import assert_adds_messages, assert_no_messages ("homeassistant.components.pylint_test.api.hub", "..const", "CONSTANT"), ("homeassistant.components.pylint_test.api.hub", "..", "CONSTANT"), ("homeassistant.components.pylint_test.api.hub", "...", "pylint_test"), + ("tests.components.pylint_test.api.hub", "..const", "CONSTANT"), ], ) def test_good_import( @@ -101,6 +102,18 @@ def test_good_import( "CONSTANT", "hass-relative-import", ), + ( + "tests.components.pylint_test.api.hub", + "tests.components.pylint_test.const", + "CONSTANT", + "hass-relative-import", + ), + ( + "tests.components.pylint_test.api.hub", + "...const", + "CONSTANT", + "hass-absolute-import", + ), ], ) def test_bad_import( From 33bd840c0aea3f522e8844662e48b73d0b72eee8 Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Mon, 19 Sep 2022 12:57:52 +0200 Subject: [PATCH 548/955] Bump pyoverkiz to 1.5.3 in Overkiz integration (#78743) --- 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 f2d17bab3f9..0b3e041f302 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.0"], + "requirements": ["pyoverkiz==1.5.3"], "zeroconf": [ { "type": "_kizbox._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 36c15c0f9db..dc50d6d31d3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1775,7 +1775,7 @@ pyotgw==2.0.3 pyotp==2.7.0 # homeassistant.components.overkiz -pyoverkiz==1.5.0 +pyoverkiz==1.5.3 # homeassistant.components.openweathermap pyowm==3.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0fd9feb6b19..8c8517c9080 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1246,7 +1246,7 @@ pyotgw==2.0.3 pyotp==2.7.0 # homeassistant.components.overkiz -pyoverkiz==1.5.0 +pyoverkiz==1.5.3 # homeassistant.components.openweathermap pyowm==3.2.0 From 3dc6db94cef64391ca8c38dce83fe1e9d21efded Mon Sep 17 00:00:00 2001 From: Nico Date: Mon, 19 Sep 2022 12:59:09 +0200 Subject: [PATCH 549/955] Bump aioimaplib to 1.0.1 (#78738) --- homeassistant/components/imap/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/imap/manifest.json b/homeassistant/components/imap/manifest.json index f4bbadfa6ac..36004113351 100644 --- a/homeassistant/components/imap/manifest.json +++ b/homeassistant/components/imap/manifest.json @@ -2,7 +2,7 @@ "domain": "imap", "name": "IMAP", "documentation": "https://www.home-assistant.io/integrations/imap", - "requirements": ["aioimaplib==1.0.0"], + "requirements": ["aioimaplib==1.0.1"], "codeowners": [], "iot_class": "cloud_push", "loggers": ["aioimaplib"] diff --git a/requirements_all.txt b/requirements_all.txt index dc50d6d31d3..1dc62b18dcb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -181,7 +181,7 @@ aiohttp_cors==0.7.0 aiohue==4.5.0 # homeassistant.components.imap -aioimaplib==1.0.0 +aioimaplib==1.0.1 # homeassistant.components.apache_kafka aiokafka==0.7.2 From 3eab4a234b25f2e7540cb9587921dc0e4ba51d06 Mon Sep 17 00:00:00 2001 From: Justin Vanderhooft Date: Mon, 19 Sep 2022 07:56:34 -0400 Subject: [PATCH 550/955] Add support for controlling manual watering time on Melnor Bluetooth devices (#78653) Co-authored-by: J. Nick Koston --- homeassistant/components/melnor/__init__.py | 1 + homeassistant/components/melnor/models.py | 38 +++++++- homeassistant/components/melnor/number.py | 96 +++++++++++++++++++++ homeassistant/components/melnor/sensor.py | 85 ++++++++++++++++-- homeassistant/components/melnor/switch.py | 37 ++++---- tests/components/melnor/conftest.py | 16 ++++ tests/components/melnor/test_number.py | 46 ++++++++++ tests/components/melnor/test_sensor.py | 41 +++++++++ tests/components/melnor/test_switch.py | 18 ++-- 9 files changed, 344 insertions(+), 34 deletions(-) create mode 100644 homeassistant/components/melnor/number.py create mode 100644 tests/components/melnor/test_number.py diff --git a/homeassistant/components/melnor/__init__.py b/homeassistant/components/melnor/__init__.py index 433380a9ab9..93b8d11ab24 100644 --- a/homeassistant/components/melnor/__init__.py +++ b/homeassistant/components/melnor/__init__.py @@ -15,6 +15,7 @@ from .const import DOMAIN from .models import MelnorDataUpdateCoordinator PLATFORMS: list[Platform] = [ + Platform.NUMBER, Platform.SENSOR, Platform.SWITCH, ] diff --git a/homeassistant/components/melnor/models.py b/homeassistant/components/melnor/models.py index e783f829c11..8cbe5f80680 100644 --- a/homeassistant/components/melnor/models.py +++ b/homeassistant/components/melnor/models.py @@ -1,10 +1,13 @@ """Melnor integration models.""" +from collections.abc import Callable from datetime import timedelta import logging +from typing import TypeVar from melnor_bluetooth.device import Device, Valve +from homeassistant.components.number import EntityDescription from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( @@ -39,7 +42,7 @@ class MelnorDataUpdateCoordinator(DataUpdateCoordinator[Device]): return self._device -class MelnorBluetoothBaseEntity(CoordinatorEntity[MelnorDataUpdateCoordinator]): +class MelnorBluetoothEntity(CoordinatorEntity[MelnorDataUpdateCoordinator]): """Base class for melnor entities.""" _device: Device @@ -73,7 +76,7 @@ class MelnorBluetoothBaseEntity(CoordinatorEntity[MelnorDataUpdateCoordinator]): return self._device.is_connected -class MelnorZoneEntity(MelnorBluetoothBaseEntity): +class MelnorZoneEntity(MelnorBluetoothEntity): """Base class for valves that define themselves as child devices.""" _valve: Valve @@ -81,11 +84,17 @@ class MelnorZoneEntity(MelnorBluetoothBaseEntity): def __init__( self, coordinator: MelnorDataUpdateCoordinator, + entity_description: EntityDescription, valve: Valve, ) -> None: """Initialize a valve entity.""" super().__init__(coordinator) + self._attr_unique_id = ( + f"{self._device.mac}-zone{valve.id}-{entity_description.key}" + ) + self.entity_description = entity_description + self._valve = valve self._attr_device_info = DeviceInfo( @@ -94,3 +103,28 @@ class MelnorZoneEntity(MelnorBluetoothBaseEntity): name=f"Zone {valve.id + 1}", via_device=(DOMAIN, self._device.mac), ) + + +T = TypeVar("T", bound=EntityDescription) + + +def get_entities_for_valves( + coordinator: MelnorDataUpdateCoordinator, + descriptions: list[T], + function: Callable[ + [Valve, T], + CoordinatorEntity[MelnorDataUpdateCoordinator], + ], +) -> list[CoordinatorEntity[MelnorDataUpdateCoordinator]]: + """Get descriptions for valves.""" + entities = [] + + # This device may not have 4 valves total, but the library will only expose the right number of valves + for i in range(1, 5): + valve = coordinator.data[f"zone{i}"] + + if valve is not None: + for description in descriptions: + entities.append(function(valve, description)) + + return entities diff --git a/homeassistant/components/melnor/number.py b/homeassistant/components/melnor/number.py new file mode 100644 index 00000000000..3f29e7cf772 --- /dev/null +++ b/homeassistant/components/melnor/number.py @@ -0,0 +1,96 @@ +"""Number support for Melnor Bluetooth water timer.""" + +from __future__ import annotations + +from collections.abc import Callable, Coroutine +from dataclasses import dataclass +from typing import Any + +from melnor_bluetooth.device import Valve + +from homeassistant.components.number import NumberEntity, NumberEntityDescription +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 .const import DOMAIN +from .models import ( + MelnorDataUpdateCoordinator, + MelnorZoneEntity, + get_entities_for_valves, +) + + +@dataclass +class MelnorZoneNumberEntityDescriptionMixin: + """Mixin for required keys.""" + + set_num_fn: Callable[[Valve, int], Coroutine[Any, Any, None]] + state_fn: Callable[[Valve], Any] + + +@dataclass +class MelnorZoneNumberEntityDescription( + NumberEntityDescription, MelnorZoneNumberEntityDescriptionMixin +): + """Describes Melnor number entity.""" + + +ZONE_ENTITY_DESCRIPTIONS: list[MelnorZoneNumberEntityDescription] = [ + MelnorZoneNumberEntityDescription( + entity_category=EntityCategory.CONFIG, + native_max_value=360, + native_min_value=1, + icon="mdi:timer-cog-outline", + key="manual_minutes", + name="Manual Minutes", + set_num_fn=lambda valve, value: valve.set_manual_watering_minutes(value), + state_fn=lambda valve: valve.manual_watering_minutes, + ) +] + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the number platform.""" + + coordinator: MelnorDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + + async_add_entities( + get_entities_for_valves( + coordinator, + ZONE_ENTITY_DESCRIPTIONS, + lambda valve, description: MelnorZoneNumber( + coordinator, description, valve + ), + ) + ) + + +class MelnorZoneNumber(MelnorZoneEntity, NumberEntity): + """A number implementation for a melnor device.""" + + entity_description: MelnorZoneNumberEntityDescription + + def __init__( + self, + coordinator: MelnorDataUpdateCoordinator, + entity_description: MelnorZoneNumberEntityDescription, + valve: Valve, + ) -> None: + """Initialize a number for a melnor device.""" + super().__init__(coordinator, entity_description, valve) + + @property + def native_value(self) -> float | None: + """Return the current value.""" + return self._valve.manual_watering_minutes + + async def async_set_native_value(self, value: float) -> None: + """Update the current value.""" + await self.entity_description.set_num_fn(self._valve, int(value)) + self._async_write_ha_state() diff --git a/homeassistant/components/melnor/sensor.py b/homeassistant/components/melnor/sensor.py index e642df4f9c3..42eb9e60c73 100644 --- a/homeassistant/components/melnor/sensor.py +++ b/homeassistant/components/melnor/sensor.py @@ -3,9 +3,10 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass +from datetime import datetime from typing import Any -from melnor_bluetooth.device import Device +from melnor_bluetooth.device import Device, Valve from homeassistant.components.sensor import ( SensorDeviceClass, @@ -19,9 +20,26 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType +from homeassistant.util import dt as dt_util from .const import DOMAIN -from .models import MelnorBluetoothBaseEntity, MelnorDataUpdateCoordinator +from .models import ( + MelnorBluetoothEntity, + MelnorDataUpdateCoordinator, + MelnorZoneEntity, + get_entities_for_valves, +) + + +def watering_seconds_left(valve: Valve) -> datetime | None: + """Calculate the number of minutes left in the current watering cycle.""" + + if valve.is_watering is not True or dt_util.now() > dt_util.utc_from_timestamp( + valve.watering_end_time + ): + return None + + return dt_util.utc_from_timestamp(valve.watering_end_time) @dataclass @@ -31,6 +49,20 @@ class MelnorSensorEntityDescriptionMixin: state_fn: Callable[[Device], Any] +@dataclass +class MelnorZoneSensorEntityDescriptionMixin: + """Mixin for required keys.""" + + state_fn: Callable[[Valve], Any] + + +@dataclass +class MelnorZoneSensorEntityDescription( + SensorEntityDescription, MelnorZoneSensorEntityDescriptionMixin +): + """Describes Melnor sensor entity.""" + + @dataclass class MelnorSensorEntityDescription( SensorEntityDescription, MelnorSensorEntityDescriptionMixin @@ -38,7 +70,7 @@ class MelnorSensorEntityDescription( """Describes Melnor sensor entity.""" -sensors = [ +DEVICE_ENTITY_DESCRIPTIONS: list[MelnorSensorEntityDescription] = [ MelnorSensorEntityDescription( device_class=SensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, @@ -60,6 +92,15 @@ sensors = [ ), ] +ZONE_ENTITY_DESCRIPTIONS: list[MelnorZoneSensorEntityDescription] = [ + MelnorZoneSensorEntityDescription( + device_class=SensorDeviceClass.TIMESTAMP, + key="manual_cycle_end", + name="Manual Cycle End", + state_fn=watering_seconds_left, + ), +] + async def async_setup_entry( hass: HomeAssistant, @@ -70,16 +111,28 @@ async def async_setup_entry( coordinator: MelnorDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + # Device-level sensors async_add_entities( MelnorSensorEntity( coordinator, description, ) - for description in sensors + for description in DEVICE_ENTITY_DESCRIPTIONS + ) + + # Valve/Zone-level sensors + async_add_entities( + get_entities_for_valves( + coordinator, + ZONE_ENTITY_DESCRIPTIONS, + lambda valve, description: MelnorZoneSensorEntity( + coordinator, description, valve + ), + ) ) -class MelnorSensorEntity(MelnorBluetoothBaseEntity, SensorEntity): +class MelnorSensorEntity(MelnorBluetoothEntity, SensorEntity): """Representation of a Melnor sensor.""" entity_description: MelnorSensorEntityDescription @@ -98,5 +151,25 @@ class MelnorSensorEntity(MelnorBluetoothBaseEntity, SensorEntity): @property def native_value(self) -> StateType: - """Return the battery level.""" + """Return the sensor value.""" return self.entity_description.state_fn(self._device) + + +class MelnorZoneSensorEntity(MelnorZoneEntity, SensorEntity): + """Representation of a Melnor sensor.""" + + entity_description: MelnorZoneSensorEntityDescription + + def __init__( + self, + coordinator: MelnorDataUpdateCoordinator, + entity_description: MelnorZoneSensorEntityDescription, + valve: Valve, + ) -> None: + """Initialize a sensor for a Melnor device.""" + super().__init__(coordinator, entity_description, valve) + + @property + def native_value(self) -> StateType: + """Return the sensor value.""" + return self.entity_description.state_fn(self._valve) diff --git a/homeassistant/components/melnor/switch.py b/homeassistant/components/melnor/switch.py index 20d95ad99a6..eca6f1a98cf 100644 --- a/homeassistant/components/melnor/switch.py +++ b/homeassistant/components/melnor/switch.py @@ -18,7 +18,11 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .models import MelnorDataUpdateCoordinator, MelnorZoneEntity +from .models import ( + MelnorDataUpdateCoordinator, + MelnorZoneEntity, + get_entities_for_valves, +) @dataclass @@ -36,12 +40,11 @@ class MelnorSwitchEntityDescription( """Describes Melnor switch entity.""" -switches = [ +ZONE_ENTITY_DESCRIPTIONS = [ MelnorSwitchEntityDescription( device_class=SwitchDeviceClass.SWITCH, icon="mdi:sprinkler", key="manual", - name="Manual", on_off_fn=lambda valve, bool: valve.set_is_watering(bool), state_fn=lambda valve: valve.is_watering, ) @@ -51,22 +54,21 @@ switches = [ async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, - async_add_devices: AddEntitiesCallback, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up the switch platform.""" - entities: list[MelnorZoneSwitch] = [] coordinator: MelnorDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] - # This device may not have 4 valves total, but the library will only expose the right number of valves - for i in range(1, 5): - valve = coordinator.data[f"zone{i}"] - if valve is not None: - - for description in switches: - entities.append(MelnorZoneSwitch(coordinator, valve, description)) - - async_add_devices(entities) + async_add_entities( + get_entities_for_valves( + coordinator, + ZONE_ENTITY_DESCRIPTIONS, + lambda valve, description: MelnorZoneSwitch( + coordinator, description, valve + ), + ) + ) class MelnorZoneSwitch(MelnorZoneEntity, SwitchEntity): @@ -77,14 +79,11 @@ class MelnorZoneSwitch(MelnorZoneEntity, SwitchEntity): def __init__( self, coordinator: MelnorDataUpdateCoordinator, - valve: Valve, entity_description: MelnorSwitchEntityDescription, + valve: Valve, ) -> None: """Initialize a switch for a melnor device.""" - super().__init__(coordinator, valve) - - self._attr_unique_id = f"{self._device.mac}-zone{valve.id}-manual" - self.entity_description = entity_description + super().__init__(coordinator, entity_description, valve) @property def is_on(self) -> bool: diff --git a/tests/components/melnor/conftest.py b/tests/components/melnor/conftest.py index 554349109ef..1b5af1f8abf 100644 --- a/tests/components/melnor/conftest.py +++ b/tests/components/melnor/conftest.py @@ -58,9 +58,11 @@ class MockedValve: _id: int _is_watering: bool _manual_watering_minutes: int + _end_time: int def __init__(self, identifier: int) -> None: """Initialize a mocked valve.""" + self._end_time = 0 self._id = identifier self._is_watering = False self._manual_watering_minutes = 0 @@ -79,6 +81,20 @@ class MockedValve: """Set the valve to manual watering.""" self._is_watering = is_watering + @property + def manual_watering_minutes(self): + """Return the number of minutes the valve is set to manual watering.""" + return self._manual_watering_minutes + + async def set_manual_watering_minutes(self, minutes: int): + """Set the valve to manual watering.""" + self._manual_watering_minutes = minutes + + @property + def watering_end_time(self) -> int: + """Return the end time of the current watering cycle.""" + return self._end_time + def mock_config_entry(hass: HomeAssistant): """Return a mock config entry.""" diff --git a/tests/components/melnor/test_number.py b/tests/components/melnor/test_number.py new file mode 100644 index 00000000000..77466ba50ed --- /dev/null +++ b/tests/components/melnor/test_number.py @@ -0,0 +1,46 @@ +"""Test the Melnor sensors.""" + +from __future__ import annotations + +from .conftest import ( + mock_config_entry, + patch_async_ble_device_from_address, + patch_async_register_callback, + patch_melnor_device, +) + + +async def test_manual_watering_minutes(hass): + """Test the manual watering switch.""" + + entry = mock_config_entry(hass) + + with patch_async_ble_device_from_address(), patch_melnor_device() as device_patch, patch_async_register_callback(): + + device = device_patch.return_value + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + number = hass.states.get("number.zone_1_manual_minutes") + + print(number) + assert number.state == "0" + assert number.attributes["max"] == 360 + assert number.attributes["min"] == 1 + assert number.attributes["step"] == 1.0 + assert number.attributes["icon"] == "mdi:timer-cog-outline" + + assert device.zone1.manual_watering_minutes == 0 + + await hass.services.async_call( + "number", + "set_value", + {"entity_id": "number.zone_1_manual_minutes", "value": 10}, + blocking=True, + ) + + number = hass.states.get("number.zone_1_manual_minutes") + + assert number.state == "10" + assert device.zone1.manual_watering_minutes == 10 diff --git a/tests/components/melnor/test_sensor.py b/tests/components/melnor/test_sensor.py index bef2bba35e0..778acbc96d9 100644 --- a/tests/components/melnor/test_sensor.py +++ b/tests/components/melnor/test_sensor.py @@ -2,9 +2,12 @@ from __future__ import annotations +from freezegun import freeze_time + from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass from homeassistant.const import PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT from homeassistant.helpers import entity_registry +import homeassistant.util.dt as dt_util from .conftest import ( mock_config_entry, @@ -14,6 +17,8 @@ from .conftest import ( patch_melnor_device, ) +from tests.common import async_fire_time_changed + async def test_battery_sensor(hass): """Test the battery sensor.""" @@ -31,6 +36,42 @@ async def test_battery_sensor(hass): assert battery_sensor.attributes["state_class"] == SensorStateClass.MEASUREMENT +async def test_minutes_remaining_sensor(hass): + """Test the minutes remaining sensor.""" + + now = dt_util.utcnow() + + entry = mock_config_entry(hass) + device = mock_melnor_device() + + end_time = now + dt_util.dt.timedelta(minutes=10) + + # we control this mock + # pylint: disable=protected-access + device.zone1._end_time = (end_time).timestamp() + + with freeze_time(now), patch_async_ble_device_from_address(), patch_melnor_device( + device + ), patch_async_register_callback(): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + # Valve is off, report 0 + minutes_sensor = hass.states.get("sensor.zone_1_manual_cycle_end") + assert minutes_sensor.state == "unknown" + assert minutes_sensor.attributes["device_class"] == SensorDeviceClass.TIMESTAMP + + # Turn valve on + device.zone1._is_watering = True + + async_fire_time_changed(hass, now + dt_util.dt.timedelta(seconds=10)) + await hass.async_block_till_done() + + # Valve is on, report 10 + minutes_remaining_sensor = hass.states.get("sensor.zone_1_manual_cycle_end") + assert minutes_remaining_sensor.state == end_time.isoformat(timespec="seconds") + + async def test_rssi_sensor(hass): """Test the rssi sensor.""" diff --git a/tests/components/melnor/test_switch.py b/tests/components/melnor/test_switch.py index ffe043f53a8..9539b00d9c6 100644 --- a/tests/components/melnor/test_switch.py +++ b/tests/components/melnor/test_switch.py @@ -22,7 +22,7 @@ async def test_manual_watering_switch_metadata(hass): assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - switch = hass.states.get("switch.zone_1_manual") + switch = hass.states.get("switch.zone_1") assert switch.attributes["device_class"] == SwitchDeviceClass.SWITCH assert switch.attributes["icon"] == "mdi:sprinkler" @@ -32,30 +32,34 @@ async def test_manual_watering_switch_on_off(hass): entry = mock_config_entry(hass) - with patch_async_ble_device_from_address(), patch_melnor_device(), patch_async_register_callback(): + with patch_async_ble_device_from_address(), patch_melnor_device() as device_patch, patch_async_register_callback(): + + device = device_patch.return_value assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - switch = hass.states.get("switch.zone_1_manual") + switch = hass.states.get("switch.zone_1") assert switch.state is STATE_OFF await hass.services.async_call( "switch", "turn_on", - {"entity_id": "switch.zone_1_manual"}, + {"entity_id": "switch.zone_1"}, blocking=True, ) - switch = hass.states.get("switch.zone_1_manual") + switch = hass.states.get("switch.zone_1") assert switch.state is STATE_ON + assert device.zone1.is_watering is True await hass.services.async_call( "switch", "turn_off", - {"entity_id": "switch.zone_1_manual"}, + {"entity_id": "switch.zone_1"}, blocking=True, ) - switch = hass.states.get("switch.zone_1_manual") + switch = hass.states.get("switch.zone_1") assert switch.state is STATE_OFF + assert device.zone1.is_watering is False From 4b813f2460f7620f1ec457d1f1b7575f7c14fc94 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 19 Sep 2022 14:27:21 +0200 Subject: [PATCH 551/955] Adjust pylint plugin for tests directory (#78727) * Add module_name to parametrize * Add tests for tests directory * Apply patch from mib1185 * Adjust plugin to allow imports from component being tested --- pylint/plugins/hass_imports.py | 17 ++++++++++ tests/pylint/test_imports.py | 59 ++++++++++++++++++++++++++++------ 2 files changed, 67 insertions(+), 9 deletions(-) diff --git a/pylint/plugins/hass_imports.py b/pylint/plugins/hass_imports.py index 4b57794a07f..718f9550312 100644 --- a/pylint/plugins/hass_imports.py +++ b/pylint/plugins/hass_imports.py @@ -333,12 +333,22 @@ class HassImportsFormatChecker(BaseChecker): # type: ignore[misc] def visit_import(self, node: nodes.Import) -> None: """Called when a Import node is visited.""" + if self.current_package is None: + return for module, _alias in node.names: if module.startswith(f"{self.current_package}."): self.add_message("hass-relative-import", node=node) + continue if module.startswith("homeassistant.components.") and module.endswith( "const" ): + if ( + self.current_package.startswith("tests.components.") + and self.current_package.split(".")[2] == module.split(".")[2] + ): + # Ignore check if the component being tested matches + # the component being imported from + continue self.add_message("hass-component-root-import", node=node) def _visit_importfrom_relative( @@ -389,6 +399,13 @@ class HassImportsFormatChecker(BaseChecker): # type: ignore[misc] node.modname.endswith(".const") or "const" in {names[0] for names in node.names} ): + if ( + self.current_package.startswith("tests.components.") + and self.current_package.split(".")[2] == node.modname.split(".")[2] + ): + # Ignore check if the component being tested matches + # the component being imported from + return self.add_message("hass-component-root-import", node=node) return if obsolete_imports := _OBSOLETE_IMPORT.get(node.modname): diff --git a/tests/pylint/test_imports.py b/tests/pylint/test_imports.py index 130947ac68d..5c8bad28902 100644 --- a/tests/pylint/test_imports.py +++ b/tests/pylint/test_imports.py @@ -148,22 +148,41 @@ def test_bad_import( @pytest.mark.parametrize( - "import_node", + "import_node,module_name", [ - "from homeassistant.components import climate", - "from homeassistant.components.climate import ClimateEntityFeature", + ( + "from homeassistant.components import climate", + "homeassistant.components.pylint_test.climate", + ), + ( + "from homeassistant.components.climate import ClimateEntityFeature", + "homeassistant.components.pylint_test.climate", + ), + ( + "from homeassistant.components.pylint_test import const", + "tests.components.pylint_test.climate", + ), + ( + "from homeassistant.components.pylint_test.const import CONSTANT", + "tests.components.pylint_test.climate", + ), + ( + "import homeassistant.components.pylint_test.const as climate", + "tests.components.pylint_test.climate", + ), ], ) def test_good_root_import( linter: UnittestLinter, imports_checker: BaseChecker, import_node: str, + module_name: str, ) -> None: """Ensure bad root imports are rejected.""" node = astroid.extract_node( f"{import_node} #@", - "homeassistant.components.pylint_test.climate", + module_name, ) imports_checker.visit_module(node.parent) @@ -175,23 +194,45 @@ def test_good_root_import( @pytest.mark.parametrize( - "import_node", + "import_node,module_name", [ - "import homeassistant.components.climate.const as climate", - "from homeassistant.components.climate import const", - "from homeassistant.components.climate.const import ClimateEntityFeature", + ( + "import homeassistant.components.climate.const as climate", + "homeassistant.components.pylint_test.climate", + ), + ( + "from homeassistant.components.climate import const", + "homeassistant.components.pylint_test.climate", + ), + ( + "from homeassistant.components.climate.const import ClimateEntityFeature", + "homeassistant.components.pylint_test.climate", + ), + ( + "from homeassistant.components.climate import const", + "tests.components.pylint_test.climate", + ), + ( + "from homeassistant.components.climate.const import CONSTANT", + "tests.components.pylint_test.climate", + ), + ( + "import homeassistant.components.climate.const as climate", + "tests.components.pylint_test.climate", + ), ], ) def test_bad_root_import( linter: UnittestLinter, imports_checker: BaseChecker, import_node: str, + module_name: str, ) -> None: """Ensure bad root imports are rejected.""" node = astroid.extract_node( f"{import_node} #@", - "homeassistant.components.pylint_test.climate", + module_name, ) imports_checker.visit_module(node.parent) From ecef7552172a72df00ec4e1195f54919618201fd Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 19 Sep 2022 14:56:51 +0200 Subject: [PATCH 552/955] Adjust root-import in tomorrowio tests (#78763) --- tests/components/tomorrowio/test_config_flow.py | 2 +- tests/components/tomorrowio/test_init.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/tomorrowio/test_config_flow.py b/tests/components/tomorrowio/test_config_flow.py index 77af05cdc7d..245dabffcc4 100644 --- a/tests/components/tomorrowio/test_config_flow.py +++ b/tests/components/tomorrowio/test_config_flow.py @@ -9,7 +9,7 @@ from pytomorrowio.exceptions import ( ) from homeassistant import data_entry_flow -from homeassistant.components.climacell.const import DOMAIN as CC_DOMAIN +from homeassistant.components.climacell import DOMAIN as CC_DOMAIN from homeassistant.components.tomorrowio.config_flow import ( _get_config_schema, _get_unique_id, diff --git a/tests/components/tomorrowio/test_init.py b/tests/components/tomorrowio/test_init.py index 2c0a882ff5a..411675427b1 100644 --- a/tests/components/tomorrowio/test_init.py +++ b/tests/components/tomorrowio/test_init.py @@ -2,7 +2,7 @@ from datetime import timedelta from unittest.mock import patch -from homeassistant.components.climacell.const import CONF_TIMESTEP, DOMAIN as CC_DOMAIN +from homeassistant.components.climacell import CONF_TIMESTEP, DOMAIN as CC_DOMAIN from homeassistant.components.tomorrowio.config_flow import ( _get_config_schema, _get_unique_id, From d83e9072e739ac4f384d7838cd9ee5296517af80 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 19 Sep 2022 15:05:29 +0200 Subject: [PATCH 553/955] Use attributes in zoneminder (#77895) --- .../components/zoneminder/binary_sensor.py | 26 ++----- homeassistant/components/zoneminder/camera.py | 24 +++---- homeassistant/components/zoneminder/sensor.py | 70 ++++++------------- homeassistant/components/zoneminder/switch.py | 16 ++--- 4 files changed, 45 insertions(+), 91 deletions(-) diff --git a/homeassistant/components/zoneminder/binary_sensor.py b/homeassistant/components/zoneminder/binary_sensor.py index a091c5fd308..268823c9470 100644 --- a/homeassistant/components/zoneminder/binary_sensor.py +++ b/homeassistant/components/zoneminder/binary_sensor.py @@ -1,6 +1,8 @@ """Support for ZoneMinder binary sensors.""" from __future__ import annotations +from zoneminder.zm import ZoneMinder + from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, @@ -28,27 +30,13 @@ async def async_setup_platform( class ZMAvailabilitySensor(BinarySensorEntity): """Representation of the availability of ZoneMinder as a binary sensor.""" - def __init__(self, host_name, client): + _attr_device_class = BinarySensorDeviceClass.CONNECTIVITY + + def __init__(self, host_name: str, client: ZoneMinder) -> None: """Initialize availability sensor.""" - self._state = None - self._name = host_name + self._attr_name = host_name self._client = client - @property - def name(self): - """Return the name of this binary sensor.""" - return self._name - - @property - def is_on(self): - """Return true if the binary sensor is on.""" - return self._state - - @property - def device_class(self): - """Return the class of this device, from component DEVICE_CLASSES.""" - return BinarySensorDeviceClass.CONNECTIVITY - def update(self) -> None: """Update the state of this sensor (availability of ZoneMinder).""" - self._state = self._client.is_available + self._attr_is_on = self._client.is_available diff --git a/homeassistant/components/zoneminder/camera.py b/homeassistant/components/zoneminder/camera.py index 7c0c8b9d453..e87e6f814cc 100644 --- a/homeassistant/components/zoneminder/camera.py +++ b/homeassistant/components/zoneminder/camera.py @@ -3,6 +3,9 @@ from __future__ import annotations import logging +from zoneminder.monitor import Monitor +from zoneminder.zm import ZoneMinder + from homeassistant.components.mjpeg import MjpegCamera, filter_urllib3_logging from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -22,6 +25,7 @@ def setup_platform( """Set up the ZoneMinder cameras.""" filter_urllib3_logging() cameras = [] + zm_client: ZoneMinder for zm_client in hass.data[ZONEMINDER_DOMAIN].values(): if not (monitors := zm_client.get_monitors()): _LOGGER.warning("Could not fetch monitors from ZoneMinder host: %s") @@ -38,7 +42,7 @@ class ZoneMinderCamera(MjpegCamera): _attr_should_poll = True # Cameras default to False - def __init__(self, monitor, verify_ssl): + def __init__(self, monitor: Monitor, verify_ssl: bool) -> None: """Initialize as a subclass of MjpegCamera.""" super().__init__( name=monitor.name, @@ -46,22 +50,12 @@ class ZoneMinderCamera(MjpegCamera): still_image_url=monitor.still_image_url, verify_ssl=verify_ssl, ) - self._is_recording = None - self._is_available = None + self._attr_is_recording = False + self._attr_available = False self._monitor = monitor def update(self) -> None: """Update our recording state from the ZM API.""" _LOGGER.debug("Updating camera state for monitor %i", self._monitor.id) - self._is_recording = self._monitor.is_recording - self._is_available = self._monitor.is_available - - @property - def is_recording(self): - """Return whether the monitor is in alarm mode.""" - return self._is_recording - - @property - def available(self): - """Return True if entity is available.""" - return self._is_available + self._attr_is_recording = self._monitor.is_recording + self._attr_available = self._monitor.is_available diff --git a/homeassistant/components/zoneminder/sensor.py b/homeassistant/components/zoneminder/sensor.py index a7534604514..c995e84343b 100644 --- a/homeassistant/components/zoneminder/sensor.py +++ b/homeassistant/components/zoneminder/sensor.py @@ -4,7 +4,8 @@ from __future__ import annotations import logging import voluptuous as vol -from zoneminder.monitor import TimePeriod +from zoneminder.monitor import Monitor, TimePeriod +from zoneminder.zm import ZoneMinder from homeassistant.components.sensor import ( PLATFORM_SCHEMA, @@ -73,6 +74,7 @@ def setup_platform( monitored_conditions = config[CONF_MONITORED_CONDITIONS] sensors: list[SensorEntity] = [] + zm_client: ZoneMinder for zm_client in hass.data[ZONEMINDER_DOMAIN].values(): if not (monitors := zm_client.get_monitors()): _LOGGER.warning("Could not fetch any monitors from ZoneMinder") @@ -95,34 +97,19 @@ def setup_platform( class ZMSensorMonitors(SensorEntity): """Get the status of each ZoneMinder monitor.""" - def __init__(self, monitor): + def __init__(self, monitor: Monitor) -> None: """Initialize monitor sensor.""" self._monitor = monitor - self._state = None - self._is_available = None - - @property - def name(self): - """Return the name of the sensor.""" - return f"{self._monitor.name} Status" - - @property - def native_value(self): - """Return the state of the sensor.""" - return self._state - - @property - def available(self): - """Return True if Monitor is available.""" - return self._is_available + self._attr_available = False + self._attr_name = f"{self._monitor.name} Status" def update(self) -> None: """Update the sensor.""" if not (state := self._monitor.function): - self._state = None + self._attr_native_value = None else: - self._state = state.value - self._is_available = self._monitor.is_available + self._attr_native_value = state.value + self._attr_available = self._monitor.is_available class ZMSensorEvents(SensorEntity): @@ -130,18 +117,19 @@ class ZMSensorEvents(SensorEntity): _attr_native_unit_of_measurement = "Events" - def __init__(self, monitor, include_archived, description: SensorEntityDescription): + def __init__( + self, + monitor: Monitor, + include_archived: bool, + description: SensorEntityDescription, + ) -> None: """Initialize event sensor.""" self.entity_description = description self._monitor = monitor self._include_archived = include_archived self.time_period = TimePeriod.get_time_period(description.key) - - @property - def name(self): - """Return the name of the sensor.""" - return f"{self._monitor.name} {self.time_period.title}" + self._attr_name = f"{monitor.name} {self.time_period.title}" def update(self) -> None: """Update the sensor.""" @@ -153,28 +141,14 @@ class ZMSensorEvents(SensorEntity): class ZMSensorRunState(SensorEntity): """Get the ZoneMinder run state.""" - def __init__(self, client): + _attr_name = "Run State" + + def __init__(self, client: ZoneMinder) -> None: """Initialize run state sensor.""" - self._state = None - self._is_available = None + self._attr_available = False self._client = client - @property - def name(self): - """Return the name of the sensor.""" - return "Run State" - - @property - def native_value(self): - """Return the state of the sensor.""" - return self._state - - @property - def available(self): - """Return True if ZoneMinder is available.""" - return self._is_available - def update(self) -> None: """Update the sensor.""" - self._state = self._client.get_active_state() - self._is_available = self._client.is_available + self._attr_native_value = self._client.get_active_state() + self._attr_available = self._client.is_available diff --git a/homeassistant/components/zoneminder/switch.py b/homeassistant/components/zoneminder/switch.py index fc153ca81d8..985866272a6 100644 --- a/homeassistant/components/zoneminder/switch.py +++ b/homeassistant/components/zoneminder/switch.py @@ -5,7 +5,8 @@ import logging from typing import Any import voluptuous as vol -from zoneminder.monitor import MonitorState +from zoneminder.monitor import Monitor, MonitorState +from zoneminder.zm import ZoneMinder from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity from homeassistant.const import CONF_COMMAND_OFF, CONF_COMMAND_ON @@ -38,6 +39,7 @@ def setup_platform( off_state = MonitorState(config.get(CONF_COMMAND_OFF)) switches = [] + zm_client: ZoneMinder for zm_client in hass.data[ZONEMINDER_DOMAIN].values(): if not (monitors := zm_client.get_monitors()): _LOGGER.warning("Could not fetch monitors from ZoneMinder") @@ -53,24 +55,20 @@ class ZMSwitchMonitors(SwitchEntity): icon = "mdi:record-rec" - def __init__(self, monitor, on_state, off_state): + def __init__(self, monitor: Monitor, on_state: str, off_state: str) -> None: """Initialize the switch.""" self._monitor = monitor self._on_state = on_state self._off_state = off_state - self._state = None - - @property - def name(self): - """Return the name of the switch.""" - return f"{self._monitor.name} State" + self._state: bool | None = None + self._attr_name = f"{monitor.name} State" def update(self) -> None: """Update the switch value.""" self._state = self._monitor.function == self._on_state @property - def is_on(self): + def is_on(self) -> bool | None: """Return True if entity is on.""" return self._state From 903edfd881d5c3d2ba70b50a3745c8258481c581 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 19 Sep 2022 15:07:42 +0200 Subject: [PATCH 554/955] Use correct constant in anthemav tests (#78759) --- tests/components/anthemav/test_media_player.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/components/anthemav/test_media_player.py b/tests/components/anthemav/test_media_player.py index a741c5e3b53..e6a4e60108a 100644 --- a/tests/components/anthemav/test_media_player.py +++ b/tests/components/anthemav/test_media_player.py @@ -4,14 +4,14 @@ from unittest.mock import AsyncMock import pytest -from homeassistant.components.media_player.const import ( +from homeassistant.components.media_player import ( ATTR_APP_NAME, ATTR_INPUT_SOURCE, ATTR_INPUT_SOURCE_LIST, ATTR_MEDIA_TITLE, + ATTR_MEDIA_VOLUME_LEVEL, ATTR_MEDIA_VOLUME_MUTED, ) -from homeassistant.components.siren.const import ATTR_VOLUME_LEVEL from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant @@ -63,7 +63,7 @@ async def test_update_states_zone1( states = hass.states.get("media_player.anthem_av") assert states assert states.state == STATE_ON - assert states.attributes[ATTR_VOLUME_LEVEL] == 42 + assert states.attributes[ATTR_MEDIA_VOLUME_LEVEL] == 42 assert states.attributes[ATTR_MEDIA_VOLUME_MUTED] is True assert states.attributes[ATTR_INPUT_SOURCE] == "TEST INPUT" assert states.attributes[ATTR_MEDIA_TITLE] == "TEST INPUT" From 75e52ef3894abbb86857b40853e5e6cd5c0e90c2 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 19 Sep 2022 15:08:40 +0200 Subject: [PATCH 555/955] Use DOMAIN constant in plex (#78764) --- homeassistant/components/plex/__init__.py | 36 +++++++++---------- homeassistant/components/plex/media_player.py | 14 ++++---- homeassistant/components/plex/sensor.py | 8 ++--- homeassistant/components/plex/view.py | 4 +-- tests/components/sonos/test_plex_playback.py | 2 +- 5 files changed, 32 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index 9ff8bcf7b54..20f14de56c9 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -34,7 +34,7 @@ from .const import ( CONF_SERVER, CONF_SERVER_IDENTIFIER, DISPATCHERS, - DOMAIN as PLEX_DOMAIN, + DOMAIN, GDM_DEBOUNCER, GDM_SCANNER, PLATFORMS, @@ -62,7 +62,7 @@ def is_plex_media_id(media_content_id): async def async_browse_media(hass, media_content_type, media_content_id, platform=None): """Browse Plex media.""" - plex_server = next(iter(hass.data[PLEX_DOMAIN][SERVERS].values()), None) + plex_server = next(iter(hass.data[DOMAIN][SERVERS].values()), None) if not plex_server: raise BrowseError("No Plex servers available") is_internal = is_internal_request(hass) @@ -81,7 +81,7 @@ async def async_browse_media(hass, media_content_type, media_content_id, platfor async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Plex component.""" hass.data.setdefault( - PLEX_DOMAIN, + DOMAIN, {SERVERS: {}, DISPATCHERS: {}, WEBSOCKETS: {}, PLATFORMS_COMPLETED: {}}, ) @@ -89,13 +89,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass.http.register_view(PlexImageView()) - gdm = hass.data[PLEX_DOMAIN][GDM_SCANNER] = GDM() + gdm = hass.data[DOMAIN][GDM_SCANNER] = GDM() def gdm_scan(): _LOGGER.debug("Scanning for GDM clients") gdm.scan(scan_for_clients=True) - hass.data[PLEX_DOMAIN][GDM_DEBOUNCER] = Debouncer[None]( + hass.data[DOMAIN][GDM_DEBOUNCER] = Debouncer[None]( hass, _LOGGER, cooldown=10, @@ -160,8 +160,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: "Connected to: %s (%s)", plex_server.friendly_name, plex_server.url_in_use ) server_id = plex_server.machine_identifier - hass.data[PLEX_DOMAIN][SERVERS][server_id] = plex_server - hass.data[PLEX_DOMAIN][PLATFORMS_COMPLETED][server_id] = set() + hass.data[DOMAIN][SERVERS][server_id] = plex_server + hass.data[DOMAIN][PLATFORMS_COMPLETED][server_id] = set() entry.add_update_listener(async_options_updated) @@ -170,8 +170,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id), plex_server.async_update_platforms, ) - hass.data[PLEX_DOMAIN][DISPATCHERS].setdefault(server_id, []) - hass.data[PLEX_DOMAIN][DISPATCHERS][server_id].append(unsub) + hass.data[DOMAIN][DISPATCHERS].setdefault(server_id, []) + hass.data[DOMAIN][DISPATCHERS][server_id].append(unsub) @callback def plex_websocket_callback(msgtype, data, error): @@ -213,11 +213,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: session=session, verify_ssl=verify_ssl, ) - hass.data[PLEX_DOMAIN][WEBSOCKETS][server_id] = websocket + hass.data[DOMAIN][WEBSOCKETS][server_id] = websocket def start_websocket_session(platform): - hass.data[PLEX_DOMAIN][PLATFORMS_COMPLETED][server_id].add(platform) - if hass.data[PLEX_DOMAIN][PLATFORMS_COMPLETED][server_id] == PLATFORMS: + hass.data[DOMAIN][PLATFORMS_COMPLETED][server_id].add(platform) + if hass.data[DOMAIN][PLATFORMS_COMPLETED][server_id] == PLATFORMS: hass.loop.create_task(websocket.listen()) def close_websocket_session(_): @@ -226,7 +226,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: unsub = hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STOP, close_websocket_session ) - hass.data[PLEX_DOMAIN][DISPATCHERS][server_id].append(unsub) + hass.data[DOMAIN][DISPATCHERS][server_id].append(unsub) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @@ -263,16 +263,16 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" server_id = entry.data[CONF_SERVER_IDENTIFIER] - websocket = hass.data[PLEX_DOMAIN][WEBSOCKETS].pop(server_id) + websocket = hass.data[DOMAIN][WEBSOCKETS].pop(server_id) websocket.close() - dispatchers = hass.data[PLEX_DOMAIN][DISPATCHERS].pop(server_id) + dispatchers = hass.data[DOMAIN][DISPATCHERS].pop(server_id) for unsub in dispatchers: unsub() unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - hass.data[PLEX_DOMAIN][SERVERS].pop(server_id) + hass.data[DOMAIN][SERVERS].pop(server_id) return unload_ok @@ -282,8 +282,8 @@ async def async_options_updated(hass: HomeAssistant, entry: ConfigEntry) -> None server_id = entry.data[CONF_SERVER_IDENTIFIER] # Guard incomplete setup during reauth flows - if server_id in hass.data[PLEX_DOMAIN][SERVERS]: - hass.data[PLEX_DOMAIN][SERVERS][server_id].options = entry.options + if server_id in hass.data[DOMAIN][SERVERS]: + hass.data[DOMAIN][SERVERS][server_id].options = entry.options @callback diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index ffb3cab251d..b92e513fafb 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -35,7 +35,7 @@ from .const import ( COMMON_PLAYERS, CONF_SERVER_IDENTIFIER, DISPATCHERS, - DOMAIN as PLEX_DOMAIN, + DOMAIN, NAME_FORMAT, PLEX_NEW_MP_SIGNAL, PLEX_UPDATE_MEDIA_PLAYER_SESSION_SIGNAL, @@ -86,7 +86,7 @@ async def async_setup_entry( unsub = async_dispatcher_connect( hass, PLEX_NEW_MP_SIGNAL.format(server_id), async_new_media_players ) - hass.data[PLEX_DOMAIN][DISPATCHERS][server_id].append(unsub) + hass.data[DOMAIN][DISPATCHERS][server_id].append(unsub) _LOGGER.debug("New entity listener created") @@ -95,14 +95,14 @@ def _async_add_entities(hass, registry, async_add_entities, server_id, new_entit """Set up Plex media_player entities.""" _LOGGER.debug("New entities: %s", new_entities) entities = [] - plexserver = hass.data[PLEX_DOMAIN][SERVERS][server_id] + plexserver = hass.data[DOMAIN][SERVERS][server_id] for entity_params in new_entities: plex_mp = PlexMediaPlayer(plexserver, **entity_params) entities.append(plex_mp) # Migration to per-server unique_ids old_entity_id = registry.async_get_entity_id( - MP_DOMAIN, PLEX_DOMAIN, plex_mp.machine_identifier + MP_DOMAIN, DOMAIN, plex_mp.machine_identifier ) if old_entity_id is not None: new_unique_id = f"{server_id}:{plex_mp.machine_identifier}" @@ -523,7 +523,7 @@ class PlexMediaPlayer(MediaPlayerEntity): if self.device_product in TRANSIENT_DEVICE_MODELS: return DeviceInfo( - identifiers={(PLEX_DOMAIN, "plex.tv-clients")}, + identifiers={(DOMAIN, "plex.tv-clients")}, name="Plex Client Service", manufacturer="Plex", model="Plex Clients", @@ -531,12 +531,12 @@ class PlexMediaPlayer(MediaPlayerEntity): ) return DeviceInfo( - identifiers={(PLEX_DOMAIN, self.machine_identifier)}, + identifiers={(DOMAIN, self.machine_identifier)}, manufacturer=self.device_platform or "Plex", model=self.device_product or self.device_make, name=self.name, sw_version=self.device_version, - via_device=(PLEX_DOMAIN, self.plex_server.machine_identifier), + via_device=(DOMAIN, self.plex_server.machine_identifier), ) async def async_browse_media( diff --git a/homeassistant/components/plex/sensor.py b/homeassistant/components/plex/sensor.py index 7bd57ae9711..f4a2ac6e03a 100644 --- a/homeassistant/components/plex/sensor.py +++ b/homeassistant/components/plex/sensor.py @@ -16,7 +16,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( CONF_SERVER_IDENTIFIER, - DOMAIN as PLEX_DOMAIN, + DOMAIN, NAME_FORMAT, PLEX_UPDATE_LIBRARY_SIGNAL, PLEX_UPDATE_SENSOR_SIGNAL, @@ -57,7 +57,7 @@ async def async_setup_entry( ) -> None: """Set up Plex sensor from a config entry.""" server_id = config_entry.data[CONF_SERVER_IDENTIFIER] - plexserver = hass.data[PLEX_DOMAIN][SERVERS][server_id] + plexserver = hass.data[DOMAIN][SERVERS][server_id] sensors = [PlexSensor(hass, plexserver)] def create_library_sensors(): @@ -118,7 +118,7 @@ class PlexSensor(SensorEntity): return None return DeviceInfo( - identifiers={(PLEX_DOMAIN, self._server.machine_identifier)}, + identifiers={(DOMAIN, self._server.machine_identifier)}, manufacturer="Plex", model="Plex Media Server", name=self._server.friendly_name, @@ -209,7 +209,7 @@ class PlexLibrarySectionSensor(SensorEntity): return None return DeviceInfo( - identifiers={(PLEX_DOMAIN, self.server_id)}, + identifiers={(DOMAIN, self.server_id)}, manufacturer="Plex", model="Plex Media Server", name=self.server_name, diff --git a/homeassistant/components/plex/view.py b/homeassistant/components/plex/view.py index 5780bd3c46b..a2c31f17eb1 100644 --- a/homeassistant/components/plex/view.py +++ b/homeassistant/components/plex/view.py @@ -11,7 +11,7 @@ from aiohttp.typedefs import LooseHeaders from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView from homeassistant.components.media_player import async_fetch_image -from .const import DOMAIN as PLEX_DOMAIN, SERVERS +from .const import DOMAIN, SERVERS _LOGGER = logging.getLogger(__name__) @@ -33,7 +33,7 @@ class PlexImageView(HomeAssistantView): return web.Response(status=HTTPStatus.UNAUTHORIZED) hass = request.app["hass"] - if (server := hass.data[PLEX_DOMAIN][SERVERS].get(server_id)) is None: + if (server := hass.data[DOMAIN][SERVERS].get(server_id)) is None: return web.Response(status=HTTPStatus.NOT_FOUND) if (image_url := server.thumbnail_cache.get(media_content_id)) is None: diff --git a/tests/components/sonos/test_plex_playback.py b/tests/components/sonos/test_plex_playback.py index 27a9f652c95..e9f3fd7e63a 100644 --- a/tests/components/sonos/test_plex_playback.py +++ b/tests/components/sonos/test_plex_playback.py @@ -11,7 +11,7 @@ from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, SERVICE_PLAY_MEDIA, ) -from homeassistant.components.plex.const import DOMAIN as PLEX_DOMAIN, PLEX_URI_SCHEME +from homeassistant.components.plex import DOMAIN as PLEX_DOMAIN, PLEX_URI_SCHEME from homeassistant.const import ATTR_ENTITY_ID from homeassistant.exceptions import HomeAssistantError From 7bc2712142c5d047b0da4c88c99244aa0a923c7b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 19 Sep 2022 15:22:23 +0200 Subject: [PATCH 556/955] Adjust root-import in tests (#78761) * Adjust root-import in tests * Adjust diagnostics * Adjust button * Adjust select * Adjust device_tracker * Adjust camera * Adjust humidifier * Adjust media_source * Adjust update * Adjust siren * Adjust number * Adjust alarm_control_panel * Adjust notify * Adjust sensor * Adjust switch * Revert anthemav * Don't adjust demo humidifier --- tests/components/advantage_air/test_select.py | 2 +- tests/components/airzone/test_diagnostics.py | 2 +- tests/components/asuswrt/test_config_flow.py | 2 +- tests/components/asuswrt/test_sensor.py | 2 +- tests/components/august/test_button.py | 3 +-- .../bluetooth_le_tracker/test_device_tracker.py | 2 +- tests/components/bond/test_button.py | 3 +-- tests/components/canary/test_init.py | 2 +- tests/components/coinbase/const.py | 2 +- tests/components/demo/test_button.py | 2 +- tests/components/demo/test_number.py | 4 ++-- tests/components/demo/test_select.py | 2 +- tests/components/demo/test_siren.py | 2 +- tests/components/demo/test_update.py | 6 ++++-- tests/components/device_sun_light_trigger/test_init.py | 2 +- tests/components/dhcp/test_init.py | 2 +- tests/components/ecobee/test_humidifier.py | 4 ++-- tests/components/freebox/test_button.py | 3 +-- tests/components/fritz/test_config_flow.py | 2 +- tests/components/fritz/test_init.py | 2 +- tests/components/fully_kiosk/test_diagnostics.py | 2 +- tests/components/fully_kiosk/test_media_player.py | 2 +- tests/components/generic_hygrostat/test_humidifier.py | 2 +- tests/components/homekit/test_type_humidifiers.py | 4 ++-- tests/components/homekit/test_type_security_systems.py | 4 ++-- tests/components/homekit/test_type_switches.py | 2 +- .../specific_devices/test_vocolinc_flowerbud.py | 2 +- tests/components/homekit_controller/test_humidifier.py | 3 +-- tests/components/kostal_plenticore/test_number.py | 3 ++- tests/components/mazda/test_device_tracker.py | 3 +-- tests/components/mqtt/test_siren.py | 2 +- tests/components/netatmo/test_select.py | 7 +++++-- tests/components/nmap_tracker/test_config_flow.py | 2 +- tests/components/octoprint/test_button.py | 3 +-- tests/components/plex/test_button.py | 2 +- tests/components/prometheus/test_init.py | 2 +- tests/components/pushover/test_init.py | 2 +- tests/components/qnap_qsw/test_button.py | 2 +- tests/components/qnap_qsw/test_diagnostics.py | 2 +- tests/components/renault/const.py | 2 +- tests/components/renault/test_button.py | 2 +- tests/components/renault/test_select.py | 2 +- tests/components/risco/test_alarm_control_panel.py | 4 ++-- tests/components/rituals_perfume_genie/test_number.py | 4 ++-- tests/components/rituals_perfume_genie/test_select.py | 7 +++++-- tests/components/roku/test_select.py | 7 +++++-- tests/components/scrape/test_sensor.py | 7 +++++-- tests/components/sensibo/test_button.py | 3 +-- tests/components/sensibo/test_number.py | 2 +- tests/components/sensibo/test_select.py | 2 +- tests/components/sensibo/test_switch.py | 2 +- tests/components/shelly/test_button.py | 3 +-- tests/components/sleepiq/test_number.py | 4 ++-- tests/components/template/test_button.py | 2 +- tests/components/template/test_number.py | 2 +- tests/components/template/test_select.py | 2 +- tests/components/traccar/test_device_tracker.py | 2 +- tests/components/unifiprotect/test_select.py | 2 +- tests/components/utility_meter/test_init.py | 2 +- tests/components/utility_meter/test_sensor.py | 2 +- tests/components/wiz/test_number.py | 7 +++++-- tests/components/wled/test_number.py | 6 ++++-- tests/components/wled/test_select.py | 7 +++++-- tests/components/wled/test_update.py | 10 ++++------ tests/components/xiaomi_miio/test_select.py | 4 ++-- tests/components/zha/test_button.py | 3 +-- tests/components/zha/test_diagnostics.py | 2 +- tests/components/zha/test_siren.py | 2 +- tests/components/zwave_js/test_button.py | 2 +- tests/components/zwave_js/test_humidifier.py | 4 ++-- tests/components/zwave_js/test_siren.py | 7 +++++-- 71 files changed, 118 insertions(+), 103 deletions(-) diff --git a/tests/components/advantage_air/test_select.py b/tests/components/advantage_air/test_select.py index b8493c8ec45..11d4068fed7 100644 --- a/tests/components/advantage_air/test_select.py +++ b/tests/components/advantage_air/test_select.py @@ -1,7 +1,7 @@ """Test the Advantage Air Select Platform.""" from json import loads -from homeassistant.components.select.const import ( +from homeassistant.components.select import ( ATTR_OPTION, DOMAIN as SELECT_DOMAIN, SERVICE_SELECT_OPTION, diff --git a/tests/components/airzone/test_diagnostics.py b/tests/components/airzone/test_diagnostics.py index 4f7e2f61a48..f03257fd612 100644 --- a/tests/components/airzone/test_diagnostics.py +++ b/tests/components/airzone/test_diagnostics.py @@ -20,7 +20,7 @@ from aioairzone.const import ( from aiohttp import ClientSession from homeassistant.components.airzone.const import DOMAIN -from homeassistant.components.diagnostics.const import REDACTED +from homeassistant.components.diagnostics import REDACTED from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant diff --git a/tests/components/asuswrt/test_config_flow.py b/tests/components/asuswrt/test_config_flow.py index 013a81e7184..22a780fc12e 100644 --- a/tests/components/asuswrt/test_config_flow.py +++ b/tests/components/asuswrt/test_config_flow.py @@ -13,7 +13,7 @@ from homeassistant.components.asuswrt.const import ( CONF_TRACK_UNKNOWN, DOMAIN, ) -from homeassistant.components.device_tracker.const import CONF_CONSIDER_HOME +from homeassistant.components.device_tracker import CONF_CONSIDER_HOME from homeassistant.config_entries import SOURCE_USER from homeassistant.const import ( CONF_HOST, diff --git a/tests/components/asuswrt/test_sensor.py b/tests/components/asuswrt/test_sensor.py index 483592302bf..f519b936129 100644 --- a/tests/components/asuswrt/test_sensor.py +++ b/tests/components/asuswrt/test_sensor.py @@ -8,7 +8,7 @@ import pytest from homeassistant.components import device_tracker, sensor from homeassistant.components.asuswrt.const import CONF_INTERFACE, DOMAIN from homeassistant.components.asuswrt.router import DEFAULT_NAME -from homeassistant.components.device_tracker.const import CONF_CONSIDER_HOME +from homeassistant.components.device_tracker import CONF_CONSIDER_HOME from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( CONF_HOST, diff --git a/tests/components/august/test_button.py b/tests/components/august/test_button.py index 485bf4a7972..2ee71d62f43 100644 --- a/tests/components/august/test_button.py +++ b/tests/components/august/test_button.py @@ -1,7 +1,6 @@ """The button tests for the august platform.""" -from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN -from homeassistant.components.button.const import SERVICE_PRESS +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS from homeassistant.const import ATTR_ENTITY_ID from .mocks import _create_august_api_with_devices, _mock_lock_from_fixture diff --git a/tests/components/bluetooth_le_tracker/test_device_tracker.py b/tests/components/bluetooth_le_tracker/test_device_tracker.py index 071e8e16d23..36ed6abdde5 100644 --- a/tests/components/bluetooth_le_tracker/test_device_tracker.py +++ b/tests/components/bluetooth_le_tracker/test_device_tracker.py @@ -13,7 +13,7 @@ from homeassistant.components.bluetooth_le_tracker.device_tracker import ( CONF_TRACK_BATTERY, CONF_TRACK_BATTERY_INTERVAL, ) -from homeassistant.components.device_tracker.const import ( +from homeassistant.components.device_tracker import ( CONF_SCAN_INTERVAL, CONF_TRACK_NEW, DOMAIN, diff --git a/tests/components/bond/test_button.py b/tests/components/bond/test_button.py index 24ea4730d6c..1b8e89b15cd 100644 --- a/tests/components/bond/test_button.py +++ b/tests/components/bond/test_button.py @@ -4,8 +4,7 @@ from bond_async import Action, DeviceType from homeassistant import core from homeassistant.components.bond.button import STEP_SIZE -from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN -from homeassistant.components.button.const import SERVICE_PRESS +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS from homeassistant.const import ATTR_ENTITY_ID from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_registry import EntityRegistry diff --git a/tests/components/canary/test_init.py b/tests/components/canary/test_init.py index 21b897509eb..cbb077102a0 100644 --- a/tests/components/canary/test_init.py +++ b/tests/components/canary/test_init.py @@ -3,7 +3,7 @@ from unittest.mock import patch from requests import ConnectTimeout -from homeassistant.components.camera.const import DOMAIN as CAMERA_DOMAIN +from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN from homeassistant.components.canary.const import CONF_FFMPEG_ARGUMENTS, DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME diff --git a/tests/components/coinbase/const.py b/tests/components/coinbase/const.py index 6f9fff94421..4db6abca37d 100644 --- a/tests/components/coinbase/const.py +++ b/tests/components/coinbase/const.py @@ -1,6 +1,6 @@ """Constants for testing the Coinbase integration.""" -from homeassistant.components.diagnostics.const import REDACTED +from homeassistant.components.diagnostics import REDACTED GOOD_CURRENCY = "BTC" GOOD_CURRENCY_2 = "USD" diff --git a/tests/components/demo/test_button.py b/tests/components/demo/test_button.py index d4f6f97cb7a..98728c0ffab 100644 --- a/tests/components/demo/test_button.py +++ b/tests/components/demo/test_button.py @@ -4,7 +4,7 @@ from unittest.mock import patch import pytest -from homeassistant.components.button.const import DOMAIN, SERVICE_PRESS +from homeassistant.components.button import DOMAIN, SERVICE_PRESS from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component diff --git a/tests/components/demo/test_number.py b/tests/components/demo/test_number.py index 64f690d9eac..e8740d067f9 100644 --- a/tests/components/demo/test_number.py +++ b/tests/components/demo/test_number.py @@ -3,14 +3,14 @@ import pytest import voluptuous as vol -from homeassistant.components.number import NumberMode -from homeassistant.components.number.const import ( +from homeassistant.components.number import ( ATTR_MAX, ATTR_MIN, ATTR_STEP, ATTR_VALUE, DOMAIN, SERVICE_SET_VALUE, + NumberMode, ) from homeassistant.const import ATTR_ENTITY_ID, ATTR_MODE from homeassistant.setup import async_setup_component diff --git a/tests/components/demo/test_select.py b/tests/components/demo/test_select.py index 628c173da7e..06b8beac5fe 100644 --- a/tests/components/demo/test_select.py +++ b/tests/components/demo/test_select.py @@ -2,7 +2,7 @@ import pytest -from homeassistant.components.select.const import ( +from homeassistant.components.select import ( ATTR_OPTION, ATTR_OPTIONS, DOMAIN, diff --git a/tests/components/demo/test_siren.py b/tests/components/demo/test_siren.py index 9af31b53c74..4418a70a546 100644 --- a/tests/components/demo/test_siren.py +++ b/tests/components/demo/test_siren.py @@ -3,7 +3,7 @@ from unittest.mock import call, patch import pytest -from homeassistant.components.siren.const import ( +from homeassistant.components.siren import ( ATTR_AVAILABLE_TONES, ATTR_TONE, ATTR_VOLUME_LEVEL, diff --git a/tests/components/demo/test_update.py b/tests/components/demo/test_update.py index 35780114f29..c06483b7bdc 100644 --- a/tests/components/demo/test_update.py +++ b/tests/components/demo/test_update.py @@ -3,14 +3,16 @@ from unittest.mock import patch import pytest -from homeassistant.components.update import DOMAIN, SERVICE_INSTALL, UpdateDeviceClass -from homeassistant.components.update.const import ( +from homeassistant.components.update import ( ATTR_IN_PROGRESS, ATTR_INSTALLED_VERSION, ATTR_LATEST_VERSION, ATTR_RELEASE_SUMMARY, ATTR_RELEASE_URL, ATTR_TITLE, + DOMAIN, + SERVICE_INSTALL, + UpdateDeviceClass, ) from homeassistant.const import ( ATTR_DEVICE_CLASS, diff --git a/tests/components/device_sun_light_trigger/test_init.py b/tests/components/device_sun_light_trigger/test_init.py index f7d835427a1..976a07cbfb3 100644 --- a/tests/components/device_sun_light_trigger/test_init.py +++ b/tests/components/device_sun_light_trigger/test_init.py @@ -11,7 +11,7 @@ from homeassistant.components import ( group, light, ) -from homeassistant.components.device_tracker.const import DOMAIN +from homeassistant.components.device_tracker import DOMAIN from homeassistant.const import ( ATTR_ENTITY_ID, CONF_PLATFORM, diff --git a/tests/components/dhcp/test_init.py b/tests/components/dhcp/test_init.py index c6d889eab72..78dbb4a95d9 100644 --- a/tests/components/dhcp/test_init.py +++ b/tests/components/dhcp/test_init.py @@ -10,7 +10,7 @@ from scapy.layers.l2 import Ether from homeassistant import config_entries from homeassistant.components import dhcp -from homeassistant.components.device_tracker.const import ( +from homeassistant.components.device_tracker import ( ATTR_HOST_NAME, ATTR_IP, ATTR_MAC, diff --git a/tests/components/ecobee/test_humidifier.py b/tests/components/ecobee/test_humidifier.py index 0af5fd150e3..e99d98996d2 100644 --- a/tests/components/ecobee/test_humidifier.py +++ b/tests/components/ecobee/test_humidifier.py @@ -4,8 +4,7 @@ from unittest.mock import patch import pytest from homeassistant.components.ecobee.humidifier import MODE_MANUAL, MODE_OFF -from homeassistant.components.humidifier import DOMAIN as HUMIDIFIER_DOMAIN -from homeassistant.components.humidifier.const import ( +from homeassistant.components.humidifier import ( ATTR_AVAILABLE_MODES, ATTR_HUMIDITY, ATTR_MAX_HUMIDITY, @@ -13,6 +12,7 @@ from homeassistant.components.humidifier.const import ( DEFAULT_MAX_HUMIDITY, DEFAULT_MIN_HUMIDITY, DEVICE_CLASS_HUMIDIFIER, + DOMAIN as HUMIDIFIER_DOMAIN, MODE_AUTO, SERVICE_SET_HUMIDITY, SERVICE_SET_MODE, diff --git a/tests/components/freebox/test_button.py b/tests/components/freebox/test_button.py index 0a6625e163a..f7ec19f81e3 100644 --- a/tests/components/freebox/test_button.py +++ b/tests/components/freebox/test_button.py @@ -1,8 +1,7 @@ """Tests for the Freebox config flow.""" from unittest.mock import Mock, patch -from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN -from homeassistant.components.button.const import SERVICE_PRESS +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS from homeassistant.components.freebox.const import DOMAIN from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant diff --git a/tests/components/fritz/test_config_flow.py b/tests/components/fritz/test_config_flow.py index 76f556d0743..1eeb779da1f 100644 --- a/tests/components/fritz/test_config_flow.py +++ b/tests/components/fritz/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import patch from fritzconnection.core.exceptions import FritzConnectionException, FritzSecurityError -from homeassistant.components.device_tracker.const import ( +from homeassistant.components.device_tracker import ( CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME, ) diff --git a/tests/components/fritz/test_init.py b/tests/components/fritz/test_init.py index 9f7a17de900..ae05a5b31d6 100644 --- a/tests/components/fritz/test_init.py +++ b/tests/components/fritz/test_init.py @@ -4,7 +4,7 @@ from unittest.mock import patch from fritzconnection.core.exceptions import FritzSecurityError import pytest -from homeassistant.components.device_tracker.const import ( +from homeassistant.components.device_tracker import ( CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME, ) diff --git a/tests/components/fully_kiosk/test_diagnostics.py b/tests/components/fully_kiosk/test_diagnostics.py index 8136147fb6a..feff37331af 100644 --- a/tests/components/fully_kiosk/test_diagnostics.py +++ b/tests/components/fully_kiosk/test_diagnostics.py @@ -3,7 +3,7 @@ from unittest.mock import MagicMock from aiohttp import ClientSession -from homeassistant.components.diagnostics.const import REDACTED +from homeassistant.components.diagnostics import REDACTED from homeassistant.components.fully_kiosk.const import DOMAIN from homeassistant.components.fully_kiosk.diagnostics import ( DEVICE_INFO_TO_REDACT, diff --git a/tests/components/fully_kiosk/test_media_player.py b/tests/components/fully_kiosk/test_media_player.py index d423d809fbe..b55eb5e106d 100644 --- a/tests/components/fully_kiosk/test_media_player.py +++ b/tests/components/fully_kiosk/test_media_player.py @@ -5,7 +5,7 @@ from aiohttp import ClientSession from homeassistant.components.fully_kiosk.const import DOMAIN, MEDIA_SUPPORT_FULLYKIOSK import homeassistant.components.media_player as media_player -from homeassistant.components.media_source.const import DOMAIN as MS_DOMAIN +from homeassistant.components.media_source import DOMAIN as MS_DOMAIN from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er diff --git a/tests/components/generic_hygrostat/test_humidifier.py b/tests/components/generic_hygrostat/test_humidifier.py index b27b6324f86..3d7c31002ba 100644 --- a/tests/components/generic_hygrostat/test_humidifier.py +++ b/tests/components/generic_hygrostat/test_humidifier.py @@ -6,7 +6,7 @@ import pytest import voluptuous as vol from homeassistant.components import input_boolean, switch -from homeassistant.components.humidifier.const import ( +from homeassistant.components.humidifier import ( ATTR_HUMIDITY, DOMAIN, MODE_AWAY, diff --git a/tests/components/homekit/test_type_humidifiers.py b/tests/components/homekit/test_type_humidifiers.py index e5cbf978c83..3c3985a961b 100644 --- a/tests/components/homekit/test_type_humidifiers.py +++ b/tests/components/homekit/test_type_humidifiers.py @@ -16,8 +16,7 @@ from homeassistant.components.homekit.const import ( PROP_VALID_VALUES, ) from homeassistant.components.homekit.type_humidifiers import HumidifierDehumidifier -from homeassistant.components.humidifier import HumidifierDeviceClass -from homeassistant.components.humidifier.const import ( +from homeassistant.components.humidifier import ( ATTR_HUMIDITY, ATTR_MAX_HUMIDITY, ATTR_MIN_HUMIDITY, @@ -27,6 +26,7 @@ from homeassistant.components.humidifier.const import ( DEVICE_CLASS_HUMIDIFIER, DOMAIN, SERVICE_SET_HUMIDITY, + HumidifierDeviceClass, ) from homeassistant.components.sensor import SensorDeviceClass from homeassistant.const import ( diff --git a/tests/components/homekit/test_type_security_systems.py b/tests/components/homekit/test_type_security_systems.py index d1ce830a0e2..fc507024fe9 100644 --- a/tests/components/homekit/test_type_security_systems.py +++ b/tests/components/homekit/test_type_security_systems.py @@ -2,8 +2,8 @@ from pyhap.loader import get_loader import pytest -from homeassistant.components.alarm_control_panel import DOMAIN -from homeassistant.components.alarm_control_panel.const import ( +from homeassistant.components.alarm_control_panel import ( + DOMAIN, SUPPORT_ALARM_ARM_AWAY, SUPPORT_ALARM_ARM_HOME, SUPPORT_ALARM_ARM_NIGHT, diff --git a/tests/components/homekit/test_type_switches.py b/tests/components/homekit/test_type_switches.py index c1340e1d34e..6d87edc5617 100644 --- a/tests/components/homekit/test_type_switches.py +++ b/tests/components/homekit/test_type_switches.py @@ -17,7 +17,7 @@ from homeassistant.components.homekit.type_switches import ( Vacuum, Valve, ) -from homeassistant.components.select.const import ATTR_OPTIONS +from homeassistant.components.select import ATTR_OPTIONS from homeassistant.components.vacuum import ( DOMAIN as VACUUM_DOMAIN, SERVICE_RETURN_TO_BASE, diff --git a/tests/components/homekit_controller/specific_devices/test_vocolinc_flowerbud.py b/tests/components/homekit_controller/specific_devices/test_vocolinc_flowerbud.py index b0f9216731b..aa929fffecc 100644 --- a/tests/components/homekit_controller/specific_devices/test_vocolinc_flowerbud.py +++ b/tests/components/homekit_controller/specific_devices/test_vocolinc_flowerbud.py @@ -1,6 +1,6 @@ """Make sure that Vocolinc Flowerbud is enumerated properly.""" -from homeassistant.components.humidifier.const import SUPPORT_MODES +from homeassistant.components.humidifier import SUPPORT_MODES from homeassistant.components.number import NumberMode from homeassistant.components.sensor import SensorStateClass from homeassistant.const import PERCENTAGE diff --git a/tests/components/homekit_controller/test_humidifier.py b/tests/components/homekit_controller/test_humidifier.py index 4b631f19b7a..da981e9eac0 100644 --- a/tests/components/homekit_controller/test_humidifier.py +++ b/tests/components/homekit_controller/test_humidifier.py @@ -2,8 +2,7 @@ from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes -from homeassistant.components.humidifier import DOMAIN -from homeassistant.components.humidifier.const import MODE_AUTO, MODE_NORMAL +from homeassistant.components.humidifier import DOMAIN, MODE_AUTO, MODE_NORMAL from .common import setup_test_component diff --git a/tests/components/kostal_plenticore/test_number.py b/tests/components/kostal_plenticore/test_number.py index f6978612cff..a27b880b5d7 100644 --- a/tests/components/kostal_plenticore/test_number.py +++ b/tests/components/kostal_plenticore/test_number.py @@ -8,11 +8,12 @@ from kostal.plenticore import PlenticoreApiClient, SettingsData import pytest from homeassistant.components.number import ( + ATTR_MAX, + ATTR_MIN, ATTR_VALUE, DOMAIN as NUMBER_DOMAIN, SERVICE_SET_VALUE, ) -from homeassistant.components.number.const import ATTR_MAX, ATTR_MIN from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_registry import async_get diff --git a/tests/components/mazda/test_device_tracker.py b/tests/components/mazda/test_device_tracker.py index 42a70fff1d4..f2a43299414 100644 --- a/tests/components/mazda/test_device_tracker.py +++ b/tests/components/mazda/test_device_tracker.py @@ -1,6 +1,5 @@ """The device tracker tests for the Mazda Connected Services integration.""" -from homeassistant.components.device_tracker import SOURCE_TYPE_GPS -from homeassistant.components.device_tracker.const import ATTR_SOURCE_TYPE +from homeassistant.components.device_tracker import ATTR_SOURCE_TYPE, SOURCE_TYPE_GPS from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_ICON, diff --git a/tests/components/mqtt/test_siren.py b/tests/components/mqtt/test_siren.py index 7c16f3f01a3..47e52cca643 100644 --- a/tests/components/mqtt/test_siren.py +++ b/tests/components/mqtt/test_siren.py @@ -5,7 +5,7 @@ from unittest.mock import patch import pytest from homeassistant.components import mqtt, siren -from homeassistant.components.siren.const import ATTR_VOLUME_LEVEL +from homeassistant.components.siren import ATTR_VOLUME_LEVEL from homeassistant.const import ( ATTR_ASSUMED_STATE, ATTR_ENTITY_ID, diff --git a/tests/components/netatmo/test_select.py b/tests/components/netatmo/test_select.py index de357ffda89..12168a03ad8 100644 --- a/tests/components/netatmo/test_select.py +++ b/tests/components/netatmo/test_select.py @@ -1,8 +1,11 @@ """The tests for the Netatmo climate platform.""" from unittest.mock import patch -from homeassistant.components.select import DOMAIN as SELECT_DOMAIN -from homeassistant.components.select.const import ATTR_OPTION, ATTR_OPTIONS +from homeassistant.components.select import ( + ATTR_OPTION, + ATTR_OPTIONS, + DOMAIN as SELECT_DOMAIN, +) from homeassistant.const import ATTR_ENTITY_ID, CONF_WEBHOOK_ID, SERVICE_SELECT_OPTION from .common import selected_platforms, simulate_webhook diff --git a/tests/components/nmap_tracker/test_config_flow.py b/tests/components/nmap_tracker/test_config_flow.py index 9a94efb6968..96393a5139d 100644 --- a/tests/components/nmap_tracker/test_config_flow.py +++ b/tests/components/nmap_tracker/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import patch import pytest from homeassistant import config_entries, data_entry_flow -from homeassistant.components.device_tracker.const import ( +from homeassistant.components.device_tracker import ( CONF_CONSIDER_HOME, CONF_SCAN_INTERVAL, ) diff --git a/tests/components/octoprint/test_button.py b/tests/components/octoprint/test_button.py index 603739159af..644c1e39437 100644 --- a/tests/components/octoprint/test_button.py +++ b/tests/components/octoprint/test_button.py @@ -4,8 +4,7 @@ from unittest.mock import patch from pyoctoprintapi import OctoprintPrinterInfo import pytest -from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN -from homeassistant.components.button.const import SERVICE_PRESS +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS from homeassistant.components.octoprint import OctoprintDataUpdateCoordinator from homeassistant.components.octoprint.button import InvalidPrinterState from homeassistant.components.octoprint.const import DOMAIN diff --git a/tests/components/plex/test_button.py b/tests/components/plex/test_button.py index b540ba2d031..723b5e40241 100644 --- a/tests/components/plex/test_button.py +++ b/tests/components/plex/test_button.py @@ -2,7 +2,7 @@ from datetime import timedelta from unittest.mock import patch -from homeassistant.components.button.const import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS from homeassistant.components.plex.const import DEBOUNCE_TIMEOUT from homeassistant.const import ATTR_ENTITY_ID from homeassistant.util import dt diff --git a/tests/components/prometheus/test_init.py b/tests/components/prometheus/test_init.py index 5864b551b3c..8fa38664935 100644 --- a/tests/components/prometheus/test_init.py +++ b/tests/components/prometheus/test_init.py @@ -31,7 +31,7 @@ from homeassistant.components.climate.const import ( CURRENT_HVAC_COOL, CURRENT_HVAC_HEAT, ) -from homeassistant.components.humidifier.const import ATTR_AVAILABLE_MODES +from homeassistant.components.humidifier import ATTR_AVAILABLE_MODES from homeassistant.components.sensor import SensorDeviceClass from homeassistant.const import ( ATTR_BATTERY_LEVEL, diff --git a/tests/components/pushover/test_init.py b/tests/components/pushover/test_init.py index 22b38be0b48..4d1ee3cae19 100644 --- a/tests/components/pushover/test_init.py +++ b/tests/components/pushover/test_init.py @@ -7,7 +7,7 @@ import aiohttp from pushover_complete import BadAPIRequestError import pytest -from homeassistant.components.notify.const import DOMAIN as NOTIFY_DOMAIN +from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN from homeassistant.components.pushover.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant diff --git a/tests/components/qnap_qsw/test_button.py b/tests/components/qnap_qsw/test_button.py index 5423c9686d4..43e0ee4ba38 100644 --- a/tests/components/qnap_qsw/test_button.py +++ b/tests/components/qnap_qsw/test_button.py @@ -2,7 +2,7 @@ from unittest.mock import patch -from homeassistant.components.button.const import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN from homeassistant.core import HomeAssistant diff --git a/tests/components/qnap_qsw/test_diagnostics.py b/tests/components/qnap_qsw/test_diagnostics.py index a4b5c1658e1..da634ff6a39 100644 --- a/tests/components/qnap_qsw/test_diagnostics.py +++ b/tests/components/qnap_qsw/test_diagnostics.py @@ -30,7 +30,7 @@ from aioqsw.const import ( QSD_VERSION, ) -from homeassistant.components.diagnostics.const import REDACTED +from homeassistant.components.diagnostics import REDACTED from homeassistant.components.qnap_qsw.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME from homeassistant.core import HomeAssistant diff --git a/tests/components/renault/const.py b/tests/components/renault/const.py index 41a72c6b7ab..354d6bd6af4 100644 --- a/tests/components/renault/const.py +++ b/tests/components/renault/const.py @@ -8,7 +8,7 @@ from homeassistant.components.renault.const import ( DEVICE_CLASS_PLUG_STATE, DOMAIN, ) -from homeassistant.components.select.const import ATTR_OPTIONS +from homeassistant.components.select import ATTR_OPTIONS from homeassistant.components.sensor import ( ATTR_STATE_CLASS, SensorDeviceClass, diff --git a/tests/components/renault/test_button.py b/tests/components/renault/test_button.py index 6ed50a833f1..a7fdcb356cc 100644 --- a/tests/components/renault/test_button.py +++ b/tests/components/renault/test_button.py @@ -4,7 +4,7 @@ from unittest.mock import patch import pytest from renault_api.kamereon import schemas -from homeassistant.components.button.const import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_UNKNOWN, Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/renault/test_select.py b/tests/components/renault/test_select.py index 18c2eed8a7c..8ab9f116dba 100644 --- a/tests/components/renault/test_select.py +++ b/tests/components/renault/test_select.py @@ -4,7 +4,7 @@ from unittest.mock import patch import pytest from renault_api.kamereon import schemas -from homeassistant.components.select.const import ( +from homeassistant.components.select import ( ATTR_OPTION, DOMAIN as SELECT_DOMAIN, SERVICE_SELECT_OPTION, diff --git a/tests/components/risco/test_alarm_control_panel.py b/tests/components/risco/test_alarm_control_panel.py index 1625e78ece6..b2d24bbea32 100644 --- a/tests/components/risco/test_alarm_control_panel.py +++ b/tests/components/risco/test_alarm_control_panel.py @@ -3,8 +3,8 @@ from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch import pytest -from homeassistant.components.alarm_control_panel import DOMAIN as ALARM_DOMAIN -from homeassistant.components.alarm_control_panel.const import ( +from homeassistant.components.alarm_control_panel import ( + DOMAIN as ALARM_DOMAIN, SUPPORT_ALARM_ARM_AWAY, SUPPORT_ALARM_ARM_CUSTOM_BYPASS, SUPPORT_ALARM_ARM_HOME, diff --git a/tests/components/rituals_perfume_genie/test_number.py b/tests/components/rituals_perfume_genie/test_number.py index fc3937897b9..c66cecf819d 100644 --- a/tests/components/rituals_perfume_genie/test_number.py +++ b/tests/components/rituals_perfume_genie/test_number.py @@ -4,11 +4,11 @@ from __future__ import annotations import pytest from homeassistant.components.homeassistant import SERVICE_UPDATE_ENTITY -from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN -from homeassistant.components.number.const import ( +from homeassistant.components.number import ( ATTR_MAX, ATTR_MIN, ATTR_VALUE, + DOMAIN as NUMBER_DOMAIN, SERVICE_SET_VALUE, ) from homeassistant.components.rituals_perfume_genie.number import ( diff --git a/tests/components/rituals_perfume_genie/test_select.py b/tests/components/rituals_perfume_genie/test_select.py index 883e00b8a59..5b42cfba11d 100644 --- a/tests/components/rituals_perfume_genie/test_select.py +++ b/tests/components/rituals_perfume_genie/test_select.py @@ -3,8 +3,11 @@ import pytest from homeassistant.components.homeassistant import SERVICE_UPDATE_ENTITY from homeassistant.components.rituals_perfume_genie.select import ROOM_SIZE_SUFFIX -from homeassistant.components.select import DOMAIN as SELECT_DOMAIN -from homeassistant.components.select.const import ATTR_OPTION, ATTR_OPTIONS +from homeassistant.components.select import ( + ATTR_OPTION, + ATTR_OPTIONS, + DOMAIN as SELECT_DOMAIN, +) from homeassistant.const import ( AREA_SQUARE_METERS, ATTR_ENTITY_ID, diff --git a/tests/components/roku/test_select.py b/tests/components/roku/test_select.py index 003487c0adf..faf1d9cd7db 100644 --- a/tests/components/roku/test_select.py +++ b/tests/components/roku/test_select.py @@ -12,8 +12,11 @@ from rokuecp import ( from homeassistant.components.roku.const import DOMAIN from homeassistant.components.roku.coordinator import SCAN_INTERVAL -from homeassistant.components.select import DOMAIN as SELECT_DOMAIN -from homeassistant.components.select.const import ATTR_OPTION, ATTR_OPTIONS +from homeassistant.components.select import ( + ATTR_OPTION, + ATTR_OPTIONS, + DOMAIN as SELECT_DOMAIN, +) from homeassistant.const import ATTR_ENTITY_ID, ATTR_ICON, SERVICE_SELECT_OPTION from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError diff --git a/tests/components/scrape/test_sensor.py b/tests/components/scrape/test_sensor.py index aaf156208ef..d8da22aada1 100644 --- a/tests/components/scrape/test_sensor.py +++ b/tests/components/scrape/test_sensor.py @@ -3,8 +3,11 @@ from __future__ import annotations from unittest.mock import patch -from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass -from homeassistant.components.sensor.const import CONF_STATE_CLASS +from homeassistant.components.sensor import ( + CONF_STATE_CLASS, + SensorDeviceClass, + SensorStateClass, +) from homeassistant.const import ( CONF_DEVICE_CLASS, CONF_UNIT_OF_MEASUREMENT, diff --git a/tests/components/sensibo/test_button.py b/tests/components/sensibo/test_button.py index c0602525931..fb1b6295d9c 100644 --- a/tests/components/sensibo/test_button.py +++ b/tests/components/sensibo/test_button.py @@ -8,8 +8,7 @@ from freezegun.api import FrozenDateTimeFactory from pysensibo.model import SensiboData from pytest import MonkeyPatch, raises -from homeassistant.components.button import SERVICE_PRESS -from homeassistant.components.button.const import DOMAIN as BUTTON_DOMAIN +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, STATE_UNKNOWN from homeassistant.core import HomeAssistant diff --git a/tests/components/sensibo/test_number.py b/tests/components/sensibo/test_number.py index ed60d6653e9..ae878212597 100644 --- a/tests/components/sensibo/test_number.py +++ b/tests/components/sensibo/test_number.py @@ -8,7 +8,7 @@ from pysensibo.model import SensiboData import pytest from pytest import MonkeyPatch -from homeassistant.components.number.const import ( +from homeassistant.components.number import ( ATTR_VALUE, DOMAIN as NUMBER_DOMAIN, SERVICE_SET_VALUE, diff --git a/tests/components/sensibo/test_select.py b/tests/components/sensibo/test_select.py index ce361e224c9..0fdf67fd5c2 100644 --- a/tests/components/sensibo/test_select.py +++ b/tests/components/sensibo/test_select.py @@ -8,7 +8,7 @@ from pysensibo.model import SensiboData import pytest from pytest import MonkeyPatch -from homeassistant.components.select.const import ( +from homeassistant.components.select import ( ATTR_OPTION, DOMAIN as SELECT_DOMAIN, SERVICE_SELECT_OPTION, diff --git a/tests/components/sensibo/test_switch.py b/tests/components/sensibo/test_switch.py index 7b7b35cdd4e..2b99fa2e227 100644 --- a/tests/components/sensibo/test_switch.py +++ b/tests/components/sensibo/test_switch.py @@ -8,7 +8,7 @@ from pysensibo.model import SensiboData import pytest from pytest import MonkeyPatch -from homeassistant.components.switch.const import DOMAIN as SWITCH_DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, diff --git a/tests/components/shelly/test_button.py b/tests/components/shelly/test_button.py index 442e6ef248f..86a929ad6a4 100644 --- a/tests/components/shelly/test_button.py +++ b/tests/components/shelly/test_button.py @@ -1,6 +1,5 @@ """Tests for Shelly button platform.""" -from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN -from homeassistant.components.button.const import SERVICE_PRESS +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS from homeassistant.components.shelly.const import DOMAIN from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN from homeassistant.core import HomeAssistant diff --git a/tests/components/sleepiq/test_number.py b/tests/components/sleepiq/test_number.py index 903bf054ac7..195d155b9cf 100644 --- a/tests/components/sleepiq/test_number.py +++ b/tests/components/sleepiq/test_number.py @@ -1,10 +1,10 @@ """The tests for SleepIQ number platform.""" -from homeassistant.components.number import DOMAIN -from homeassistant.components.number.const import ( +from homeassistant.components.number import ( ATTR_MAX, ATTR_MIN, ATTR_STEP, ATTR_VALUE, + DOMAIN, SERVICE_SET_VALUE, ) from homeassistant.const import ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, ATTR_ICON diff --git a/tests/components/template/test_button.py b/tests/components/template/test_button.py index 48bedd8e928..cc366c70907 100644 --- a/tests/components/template/test_button.py +++ b/tests/components/template/test_button.py @@ -3,7 +3,7 @@ import datetime as dt from unittest.mock import patch from homeassistant import setup -from homeassistant.components.button.const import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS from homeassistant.components.template.button import DEFAULT_NAME from homeassistant.const import ( CONF_DEVICE_CLASS, diff --git a/tests/components/template/test_number.py b/tests/components/template/test_number.py index ea29bb303df..1c6a10c3a5a 100644 --- a/tests/components/template/test_number.py +++ b/tests/components/template/test_number.py @@ -6,7 +6,7 @@ from homeassistant.components.input_number import ( DOMAIN as INPUT_NUMBER_DOMAIN, SERVICE_SET_VALUE as INPUT_NUMBER_SERVICE_SET_VALUE, ) -from homeassistant.components.number.const import ( +from homeassistant.components.number import ( ATTR_MAX, ATTR_MIN, ATTR_STEP, diff --git a/tests/components/template/test_select.py b/tests/components/template/test_select.py index a58de63f186..9010339b2be 100644 --- a/tests/components/template/test_select.py +++ b/tests/components/template/test_select.py @@ -7,7 +7,7 @@ from homeassistant.components.input_select import ( SERVICE_SELECT_OPTION as INPUT_SELECT_SERVICE_SELECT_OPTION, SERVICE_SET_OPTIONS, ) -from homeassistant.components.select.const import ( +from homeassistant.components.select import ( ATTR_OPTION as SELECT_ATTR_OPTION, ATTR_OPTIONS as SELECT_ATTR_OPTIONS, DOMAIN as SELECT_DOMAIN, diff --git a/tests/components/traccar/test_device_tracker.py b/tests/components/traccar/test_device_tracker.py index 61bbe371a75..3cb7d96e5e3 100644 --- a/tests/components/traccar/test_device_tracker.py +++ b/tests/components/traccar/test_device_tracker.py @@ -4,7 +4,7 @@ from unittest.mock import AsyncMock, patch from pytraccar import ReportsEventeModel -from homeassistant.components.device_tracker.const import DOMAIN +from homeassistant.components.device_tracker import DOMAIN from homeassistant.components.traccar.device_tracker import ( PLATFORM_SCHEMA as TRACCAR_PLATFORM_SCHEMA, ) diff --git a/tests/components/unifiprotect/test_select.py b/tests/components/unifiprotect/test_select.py index 336a6f5af74..cecf899aba9 100644 --- a/tests/components/unifiprotect/test_select.py +++ b/tests/components/unifiprotect/test_select.py @@ -21,7 +21,7 @@ from pyunifiprotect.data import ( ) from pyunifiprotect.data.nvr import DoorbellMessage -from homeassistant.components.select.const import ATTR_OPTIONS +from homeassistant.components.select import ATTR_OPTIONS from homeassistant.components.unifiprotect.const import ( ATTR_DURATION, ATTR_MESSAGE, diff --git a/tests/components/utility_meter/test_init.py b/tests/components/utility_meter/test_init.py index faafd559852..cae291aac5a 100644 --- a/tests/components/utility_meter/test_init.py +++ b/tests/components/utility_meter/test_init.py @@ -6,7 +6,7 @@ from unittest.mock import patch import pytest -from homeassistant.components.select.const import ( +from homeassistant.components.select import ( ATTR_OPTION, DOMAIN as SELECT_DOMAIN, SERVICE_SELECT_OPTION, diff --git a/tests/components/utility_meter/test_sensor.py b/tests/components/utility_meter/test_sensor.py index 2284d806c04..ec12120bebd 100644 --- a/tests/components/utility_meter/test_sensor.py +++ b/tests/components/utility_meter/test_sensor.py @@ -5,7 +5,7 @@ from unittest.mock import patch from freezegun import freeze_time import pytest -from homeassistant.components.select.const import ( +from homeassistant.components.select import ( DOMAIN as SELECT_DOMAIN, SERVICE_SELECT_OPTION, ) diff --git a/tests/components/wiz/test_number.py b/tests/components/wiz/test_number.py index 1d45be9b8cf..9cf10d31904 100644 --- a/tests/components/wiz/test_number.py +++ b/tests/components/wiz/test_number.py @@ -1,7 +1,10 @@ """Tests for the number platform.""" -from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN -from homeassistant.components.number.const import ATTR_VALUE, SERVICE_SET_VALUE +from homeassistant.components.number import ( + ATTR_VALUE, + DOMAIN as NUMBER_DOMAIN, + SERVICE_SET_VALUE, +) from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er diff --git a/tests/components/wled/test_number.py b/tests/components/wled/test_number.py index 5c9d562e0c0..aff7f4f8e88 100644 --- a/tests/components/wled/test_number.py +++ b/tests/components/wled/test_number.py @@ -5,10 +5,12 @@ from unittest.mock import MagicMock import pytest from wled import Device as WLEDDevice, WLEDConnectionError, WLEDError -from homeassistant.components.number import ATTR_MAX, ATTR_MIN, DOMAIN as NUMBER_DOMAIN -from homeassistant.components.number.const import ( +from homeassistant.components.number import ( + ATTR_MAX, + ATTR_MIN, ATTR_STEP, ATTR_VALUE, + DOMAIN as NUMBER_DOMAIN, SERVICE_SET_VALUE, ) from homeassistant.components.wled.const import SCAN_INTERVAL diff --git a/tests/components/wled/test_select.py b/tests/components/wled/test_select.py index ebe3c7ca018..9f02cd6dada 100644 --- a/tests/components/wled/test_select.py +++ b/tests/components/wled/test_select.py @@ -5,8 +5,11 @@ from unittest.mock import MagicMock import pytest from wled import Device as WLEDDevice, WLEDConnectionError, WLEDError -from homeassistant.components.select import DOMAIN as SELECT_DOMAIN -from homeassistant.components.select.const import ATTR_OPTION, ATTR_OPTIONS +from homeassistant.components.select import ( + ATTR_OPTION, + ATTR_OPTIONS, + DOMAIN as SELECT_DOMAIN, +) from homeassistant.components.wled.const import SCAN_INTERVAL from homeassistant.const import ( ATTR_ENTITY_ID, diff --git a/tests/components/wled/test_update.py b/tests/components/wled/test_update.py index 2ddba81ac8b..706ec13cdd0 100644 --- a/tests/components/wled/test_update.py +++ b/tests/components/wled/test_update.py @@ -5,17 +5,15 @@ import pytest from wled import WLEDError from homeassistant.components.update import ( - DOMAIN as UPDATE_DOMAIN, - SERVICE_INSTALL, - UpdateDeviceClass, - UpdateEntityFeature, -) -from homeassistant.components.update.const import ( ATTR_INSTALLED_VERSION, ATTR_LATEST_VERSION, ATTR_RELEASE_SUMMARY, ATTR_RELEASE_URL, ATTR_TITLE, + DOMAIN as UPDATE_DOMAIN, + SERVICE_INSTALL, + UpdateDeviceClass, + UpdateEntityFeature, ) from homeassistant.const import ( ATTR_DEVICE_CLASS, diff --git a/tests/components/xiaomi_miio/test_select.py b/tests/components/xiaomi_miio/test_select.py index 014aa6fa2cd..48b8216bffc 100644 --- a/tests/components/xiaomi_miio/test_select.py +++ b/tests/components/xiaomi_miio/test_select.py @@ -9,10 +9,10 @@ from miio.integrations.airpurifier.dmaker.airfresh_t2017 import ( ) import pytest -from homeassistant.components.select import DOMAIN -from homeassistant.components.select.const import ( +from homeassistant.components.select import ( ATTR_OPTION, ATTR_OPTIONS, + DOMAIN, SERVICE_SELECT_OPTION, ) from homeassistant.components.xiaomi_miio import UPDATE_INTERVAL diff --git a/tests/components/zha/test_button.py b/tests/components/zha/test_button.py index 2fdc263d732..5e1350bc673 100644 --- a/tests/components/zha/test_button.py +++ b/tests/components/zha/test_button.py @@ -20,8 +20,7 @@ from zigpy.zcl.clusters.manufacturer_specific import ManufacturerSpecificCluster import zigpy.zcl.clusters.security as security import zigpy.zcl.foundation as zcl_f -from homeassistant.components.button import DOMAIN, ButtonDeviceClass -from homeassistant.components.button.const import SERVICE_PRESS +from homeassistant.components.button import DOMAIN, SERVICE_PRESS, ButtonDeviceClass from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, diff --git a/tests/components/zha/test_diagnostics.py b/tests/components/zha/test_diagnostics.py index d88996c78f1..807cba507af 100644 --- a/tests/components/zha/test_diagnostics.py +++ b/tests/components/zha/test_diagnostics.py @@ -7,7 +7,7 @@ import pytest import zigpy.profiles.zha as zha import zigpy.zcl.clusters.security as security -from homeassistant.components.diagnostics.const import REDACTED +from homeassistant.components.diagnostics import REDACTED from homeassistant.components.zha.core.device import ZHADevice from homeassistant.components.zha.diagnostics import KEYS_TO_REDACT from homeassistant.const import Platform diff --git a/tests/components/zha/test_siren.py b/tests/components/zha/test_siren.py index 72a40a8323e..404cdd2ac02 100644 --- a/tests/components/zha/test_siren.py +++ b/tests/components/zha/test_siren.py @@ -9,7 +9,7 @@ import zigpy.zcl.clusters.general as general import zigpy.zcl.clusters.security as security import zigpy.zcl.foundation as zcl_f -from homeassistant.components.siren.const import ( +from homeassistant.components.siren import ( ATTR_DURATION, ATTR_TONE, ATTR_VOLUME_LEVEL, diff --git a/tests/components/zwave_js/test_button.py b/tests/components/zwave_js/test_button.py index 5ae5f8e7254..336d3688988 100644 --- a/tests/components/zwave_js/test_button.py +++ b/tests/components/zwave_js/test_button.py @@ -1,5 +1,5 @@ """Test the Z-Wave JS button entities.""" -from homeassistant.components.button.const import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS from homeassistant.components.zwave_js.const import DOMAIN, SERVICE_REFRESH_VALUE from homeassistant.components.zwave_js.helpers import get_valueless_base_unique_id from homeassistant.const import ATTR_ENTITY_ID diff --git a/tests/components/zwave_js/test_humidifier.py b/tests/components/zwave_js/test_humidifier.py index 6e76fe8d164..37280ff5ad4 100644 --- a/tests/components/zwave_js/test_humidifier.py +++ b/tests/components/zwave_js/test_humidifier.py @@ -3,8 +3,7 @@ from zwave_js_server.const import CommandClass from zwave_js_server.const.command_class.humidity_control import HumidityControlMode from zwave_js_server.event import Event -from homeassistant.components.humidifier import HumidifierDeviceClass -from homeassistant.components.humidifier.const import ( +from homeassistant.components.humidifier import ( ATTR_HUMIDITY, ATTR_MAX_HUMIDITY, ATTR_MIN_HUMIDITY, @@ -12,6 +11,7 @@ from homeassistant.components.humidifier.const import ( DEFAULT_MIN_HUMIDITY, DOMAIN as HUMIDIFIER_DOMAIN, SERVICE_SET_HUMIDITY, + HumidifierDeviceClass, ) from homeassistant.const import ( ATTR_DEVICE_CLASS, diff --git a/tests/components/zwave_js/test_siren.py b/tests/components/zwave_js/test_siren.py index b1f7724316a..3284526aa0f 100644 --- a/tests/components/zwave_js/test_siren.py +++ b/tests/components/zwave_js/test_siren.py @@ -1,8 +1,11 @@ """Test the Z-Wave JS siren platform.""" from zwave_js_server.event import Event -from homeassistant.components.siren import ATTR_TONE, ATTR_VOLUME_LEVEL -from homeassistant.components.siren.const import ATTR_AVAILABLE_TONES +from homeassistant.components.siren import ( + ATTR_AVAILABLE_TONES, + ATTR_TONE, + ATTR_VOLUME_LEVEL, +) from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNKNOWN SIREN_ENTITY = "siren.indoor_siren_6_2" From 3b653de3fdd7241661aa19eff5a3e4586048c8b3 Mon Sep 17 00:00:00 2001 From: "Kenneth J. Miller" Date: Mon, 19 Sep 2022 15:35:13 +0200 Subject: [PATCH 557/955] Fix Airly CO sensor unit (#78649) --- homeassistant/components/airly/sensor.py | 1 - tests/components/airly/test_sensor.py | 1 - 2 files changed, 2 deletions(-) diff --git a/homeassistant/components/airly/sensor.py b/homeassistant/components/airly/sensor.py index 9fb4e61dbdb..bbb501ae47b 100644 --- a/homeassistant/components/airly/sensor.py +++ b/homeassistant/components/airly/sensor.py @@ -118,7 +118,6 @@ SENSOR_TYPES: tuple[AirlySensorEntityDescription, ...] = ( ), AirlySensorEntityDescription( key=ATTR_API_CO, - device_class=SensorDeviceClass.CO, name=ATTR_API_CO, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=SensorStateClass.MEASUREMENT, diff --git a/tests/components/airly/test_sensor.py b/tests/components/airly/test_sensor.py index d6bf4970130..9ac10f20fc3 100644 --- a/tests/components/airly/test_sensor.py +++ b/tests/components/airly/test_sensor.py @@ -108,7 +108,6 @@ async def test_sensor(hass, aioclient_mock): state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ) - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.CO assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT entry = registry.async_get("sensor.home_co") From 52a377ca9d2ca552d68116667b987967793392c2 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 19 Sep 2022 15:38:33 +0200 Subject: [PATCH 558/955] Adjust root-import in alexa tests (#78766) --- tests/components/alexa/test_capabilities.py | 50 ++++++++++----------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/tests/components/alexa/test_capabilities.py b/tests/components/alexa/test_capabilities.py index 10ad5f7ebd2..608ff428d04 100644 --- a/tests/components/alexa/test_capabilities.py +++ b/tests/components/alexa/test_capabilities.py @@ -4,15 +4,9 @@ from unittest.mock import patch import pytest from homeassistant.components.alexa import smart_home -from homeassistant.components.climate import const as climate +from homeassistant.components.climate import ATTR_CURRENT_TEMPERATURE, HVACMode from homeassistant.components.lock import STATE_JAMMED, STATE_LOCKING, STATE_UNLOCKING -from homeassistant.components.media_player.const import ( - SUPPORT_PAUSE, - SUPPORT_PLAY, - SUPPORT_STOP, - SUPPORT_VOLUME_MUTE, - SUPPORT_VOLUME_SET, -) +from homeassistant.components.media_player import MediaPlayerEntityFeature from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, STATE_ALARM_ARMED_AWAY, @@ -571,14 +565,14 @@ async def test_report_cover_range_value(hass): async def test_report_climate_state(hass): """Test ThermostatController reports state correctly.""" - for auto_modes in (climate.HVAC_MODE_AUTO, climate.HVAC_MODE_HEAT_COOL): + for auto_modes in (HVACMode.AUTO, HVACMode.HEAT_COOL): hass.states.async_set( "climate.downstairs", auto_modes, { "friendly_name": "Climate Downstairs", "supported_features": 91, - climate.ATTR_CURRENT_TEMPERATURE: 34, + ATTR_CURRENT_TEMPERATURE: 34, ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, ) @@ -590,14 +584,14 @@ async def test_report_climate_state(hass): {"value": 34.0, "scale": "CELSIUS"}, ) - for off_modes in [climate.HVAC_MODE_OFF]: + for off_modes in [HVACMode.OFF]: hass.states.async_set( "climate.downstairs", off_modes, { "friendly_name": "Climate Downstairs", "supported_features": 91, - climate.ATTR_CURRENT_TEMPERATURE: 34, + ATTR_CURRENT_TEMPERATURE: 34, ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, ) @@ -616,7 +610,7 @@ async def test_report_climate_state(hass): { "friendly_name": "Climate Downstairs", "supported_features": 91, - climate.ATTR_CURRENT_TEMPERATURE: 34, + ATTR_CURRENT_TEMPERATURE: 34, ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, ) @@ -633,7 +627,7 @@ async def test_report_climate_state(hass): { "friendly_name": "Climate Downstairs", "supported_features": 91, - climate.ATTR_CURRENT_TEMPERATURE: 31, + ATTR_CURRENT_TEMPERATURE: 31, ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, ) @@ -649,7 +643,7 @@ async def test_report_climate_state(hass): { "friendly_name": "Climate Heat", "supported_features": 91, - climate.ATTR_CURRENT_TEMPERATURE: 34, + ATTR_CURRENT_TEMPERATURE: 34, ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, ) @@ -665,7 +659,7 @@ async def test_report_climate_state(hass): { "friendly_name": "Climate Cool", "supported_features": 91, - climate.ATTR_CURRENT_TEMPERATURE: 34, + ATTR_CURRENT_TEMPERATURE: 34, ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, ) @@ -692,7 +686,7 @@ async def test_report_climate_state(hass): { "friendly_name": "Climate Unsupported", "supported_features": 91, - climate.ATTR_CURRENT_TEMPERATURE: 34, + ATTR_CURRENT_TEMPERATURE: 34, ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, ) @@ -727,8 +721,8 @@ async def test_temperature_sensor_climate(hass): for bad_value in (STATE_UNKNOWN, STATE_UNAVAILABLE, "not-number"): hass.states.async_set( "climate.downstairs", - climate.HVAC_MODE_HEAT, - {climate.ATTR_CURRENT_TEMPERATURE: bad_value}, + HVACMode.HEAT, + {ATTR_CURRENT_TEMPERATURE: bad_value}, ) properties = await reported_properties(hass, "climate.downstairs") @@ -736,8 +730,8 @@ async def test_temperature_sensor_climate(hass): hass.states.async_set( "climate.downstairs", - climate.HVAC_MODE_HEAT, - {climate.ATTR_CURRENT_TEMPERATURE: 34}, + HVACMode.HEAT, + {ATTR_CURRENT_TEMPERATURE: 34}, ) properties = await reported_properties(hass, "climate.downstairs") properties.assert_equal( @@ -782,7 +776,9 @@ async def test_report_playback_state(hass): "off", { "friendly_name": "Test media player", - "supported_features": SUPPORT_PAUSE | SUPPORT_PLAY | SUPPORT_STOP, + "supported_features": MediaPlayerEntityFeature.PAUSE + | MediaPlayerEntityFeature.PLAY + | MediaPlayerEntityFeature.STOP, "volume_level": 0.75, }, ) @@ -801,7 +797,8 @@ async def test_report_speaker_volume(hass): "on", { "friendly_name": "Test media player speaker", - "supported_features": SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET, + "supported_features": MediaPlayerEntityFeature.VOLUME_MUTE + | MediaPlayerEntityFeature.VOLUME_SET, "volume_level": None, "device_class": "speaker", }, @@ -815,7 +812,8 @@ async def test_report_speaker_volume(hass): "on", { "friendly_name": "Test media player speaker", - "supported_features": SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET, + "supported_features": MediaPlayerEntityFeature.VOLUME_MUTE + | MediaPlayerEntityFeature.VOLUME_SET, "volume_level": good_value / 100, "device_class": "speaker", }, @@ -921,11 +919,11 @@ async def test_get_property_blowup(hass, caplog): """Test we handle a property blowing up.""" hass.states.async_set( "climate.downstairs", - climate.HVAC_MODE_AUTO, + HVACMode.AUTO, { "friendly_name": "Climate Downstairs", "supported_features": 91, - climate.ATTR_CURRENT_TEMPERATURE: 34, + ATTR_CURRENT_TEMPERATURE: 34, ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, ) From 7ffac12de7a954fa5ae4a5dcdbc6ff39ffb9be96 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 19 Sep 2022 15:40:56 +0200 Subject: [PATCH 559/955] Adjust root-import in google-assistant tests (#78768) --- .../google_assistant/test_google_assistant.py | 4 +-- .../components/google_assistant/test_trait.py | 36 +++++++++---------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/tests/components/google_assistant/test_google_assistant.py b/tests/components/google_assistant/test_google_assistant.py index e8a2603cae3..3fd35846d7e 100644 --- a/tests/components/google_assistant/test_google_assistant.py +++ b/tests/components/google_assistant/test_google_assistant.py @@ -10,16 +10,16 @@ import pytest from homeassistant import const, core, setup from homeassistant.components import ( alarm_control_panel, + climate, cover, fan, google_assistant as ga, + humidifier, light, lock, media_player, switch, ) -from homeassistant.components.climate import const as climate -from homeassistant.components.humidifier import const as humidifier from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity import EntityCategory diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index bd12fdab61a..6d391df7439 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -9,9 +9,11 @@ from homeassistant.components import ( binary_sensor, button, camera, + climate, cover, fan, group, + humidifier, input_boolean, input_button, input_select, @@ -25,10 +27,8 @@ from homeassistant.components import ( switch, vacuum, ) -from homeassistant.components.climate import const as climate from homeassistant.components.google_assistant import const, error, helpers, trait from homeassistant.components.google_assistant.error import SmartHomeError -from homeassistant.components.humidifier import const as humidifier from homeassistant.components.media_player.const import ( MEDIA_TYPE_CHANNEL, SERVICE_PLAY_MEDIA, @@ -842,14 +842,14 @@ async def test_temperature_setting_climate_onoff(hass): hass, State( "climate.bla", - climate.HVAC_MODE_AUTO, + climate.HVACMode.AUTO, { ATTR_SUPPORTED_FEATURES: climate.SUPPORT_TARGET_TEMPERATURE_RANGE, climate.ATTR_HVAC_MODES: [ - climate.HVAC_MODE_OFF, - climate.HVAC_MODE_COOL, - climate.HVAC_MODE_HEAT, - climate.HVAC_MODE_HEAT_COOL, + climate.HVACMode.OFF, + climate.HVACMode.COOL, + climate.HVACMode.HEAT, + climate.HVACMode.HEAT_COOL, ], climate.ATTR_MIN_TEMP: None, climate.ATTR_MAX_TEMP: None, @@ -887,7 +887,7 @@ async def test_temperature_setting_climate_no_modes(hass): hass, State( "climate.bla", - climate.HVAC_MODE_AUTO, + climate.HVACMode.AUTO, { climate.ATTR_HVAC_MODES: [], climate.ATTR_MIN_TEMP: None, @@ -913,16 +913,16 @@ async def test_temperature_setting_climate_range(hass): hass, State( "climate.bla", - climate.HVAC_MODE_AUTO, + climate.HVACMode.AUTO, { climate.ATTR_CURRENT_TEMPERATURE: 70, climate.ATTR_CURRENT_HUMIDITY: 25, ATTR_SUPPORTED_FEATURES: climate.SUPPORT_TARGET_TEMPERATURE_RANGE, climate.ATTR_HVAC_MODES: [ STATE_OFF, - climate.HVAC_MODE_COOL, - climate.HVAC_MODE_HEAT, - climate.HVAC_MODE_AUTO, + climate.HVACMode.COOL, + climate.HVACMode.HEAT, + climate.HVACMode.AUTO, ], climate.ATTR_TARGET_TEMP_HIGH: 75, climate.ATTR_TARGET_TEMP_LOW: 65, @@ -970,7 +970,7 @@ async def test_temperature_setting_climate_range(hass): assert len(calls) == 1 assert calls[0].data == { ATTR_ENTITY_ID: "climate.bla", - climate.ATTR_HVAC_MODE: climate.HVAC_MODE_COOL, + climate.ATTR_HVAC_MODE: climate.HVACMode.COOL, } with pytest.raises(helpers.SmartHomeError) as err: @@ -995,9 +995,9 @@ async def test_temperature_setting_climate_setpoint(hass): hass, State( "climate.bla", - climate.HVAC_MODE_COOL, + climate.HVACMode.COOL, { - climate.ATTR_HVAC_MODES: [STATE_OFF, climate.HVAC_MODE_COOL], + climate.ATTR_HVAC_MODES: [STATE_OFF, climate.HVACMode.COOL], climate.ATTR_MIN_TEMP: 10, climate.ATTR_MAX_TEMP: 30, ATTR_TEMPERATURE: 18, @@ -1050,11 +1050,11 @@ async def test_temperature_setting_climate_setpoint_auto(hass): hass, State( "climate.bla", - climate.HVAC_MODE_HEAT_COOL, + climate.HVACMode.HEAT_COOL, { climate.ATTR_HVAC_MODES: [ - climate.HVAC_MODE_OFF, - climate.HVAC_MODE_HEAT_COOL, + climate.HVACMode.OFF, + climate.HVACMode.HEAT_COOL, ], climate.ATTR_MIN_TEMP: 10, climate.ATTR_MAX_TEMP: 30, From 7e41afe66018fa123366dccd280778ecf984b49f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 19 Sep 2022 15:41:45 +0200 Subject: [PATCH 560/955] Adjust root-import in nest tests (#78769) --- tests/components/nest/test_media_source.py | 190 ++++++++------------- 1 file changed, 75 insertions(+), 115 deletions(-) diff --git a/tests/components/nest/test_media_source.py b/tests/components/nest/test_media_source.py index dff740c84f4..cf039952c22 100644 --- a/tests/components/nest/test_media_source.py +++ b/tests/components/nest/test_media_source.py @@ -17,10 +17,13 @@ from google_nest_sdm.event import EventMessage import numpy as np import pytest -from homeassistant.components import media_source from homeassistant.components.media_player.errors import BrowseError -from homeassistant.components.media_source import const -from homeassistant.components.media_source.error import Unresolvable +from homeassistant.components.media_source import ( + URI_SCHEME, + Unresolvable, + async_browse_media, + async_resolve_media, +) from homeassistant.config_entries import ConfigEntryState from homeassistant.helpers import device_registry as dr from homeassistant.helpers.template import DATE_STR_FORMAT @@ -235,7 +238,7 @@ def create_battery_event_data( async def test_no_eligible_devices(hass, setup_platform): """Test a media source with no eligible camera devices.""" await setup_platform() - browse = await media_source.async_browse_media(hass, f"{const.URI_SCHEME}{DOMAIN}") + browse = await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}") assert browse.domain == DOMAIN assert browse.identifier == "" assert browse.title == "Nest" @@ -256,7 +259,7 @@ async def test_supported_device(hass, setup_platform): assert device assert device.name == DEVICE_NAME - browse = await media_source.async_browse_media(hass, f"{const.URI_SCHEME}{DOMAIN}") + browse = await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}") assert browse.domain == DOMAIN assert browse.title == "Nest" assert browse.identifier == "" @@ -266,9 +269,7 @@ async def test_supported_device(hass, setup_platform): assert browse.children[0].identifier == device.id assert browse.children[0].title == "Front: Recent Events" - browse = await media_source.async_browse_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}" - ) + browse = await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}/{device.id}") assert browse.domain == DOMAIN assert browse.identifier == device.id assert browse.title == "Front: Recent Events" @@ -279,7 +280,7 @@ async def test_integration_unloaded(hass, auth, setup_platform): """Test the media player loads, but has no devices, when config unloaded.""" await setup_platform() - browse = await media_source.async_browse_media(hass, f"{const.URI_SCHEME}{DOMAIN}") + browse = await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}") assert browse.domain == DOMAIN assert browse.identifier == "" assert browse.title == "Nest" @@ -294,7 +295,7 @@ async def test_integration_unloaded(hass, auth, setup_platform): assert entry.state == ConfigEntryState.NOT_LOADED # No devices returned - browse = await media_source.async_browse_media(hass, f"{const.URI_SCHEME}{DOMAIN}") + browse = await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}") assert browse.domain == DOMAIN assert browse.identifier == "" assert browse.title == "Nest" @@ -340,7 +341,7 @@ async def test_camera_event(hass, hass_client, subscriber, auth, setup_platform) event_identifier = received_event.data["nest_event_id"] # Media root directory - browse = await media_source.async_browse_media(hass, f"{const.URI_SCHEME}{DOMAIN}") + browse = await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}") assert browse.title == "Nest" assert browse.identifier == "" assert browse.can_expand @@ -355,9 +356,7 @@ async def test_camera_event(hass, hass_client, subscriber, auth, setup_platform) assert len(browse.children[0].children) == 0 # Browse to the device - browse = await media_source.async_browse_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}" - ) + browse = await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}/{device.id}") assert browse.domain == DOMAIN assert browse.identifier == device.id assert browse.title == "Front: Recent Events" @@ -372,8 +371,8 @@ async def test_camera_event(hass, hass_client, subscriber, auth, setup_platform) assert len(browse.children[0].children) == 0 # Browse to the event - browse = await media_source.async_browse_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{event_identifier}" + browse = await async_browse_media( + hass, f"{URI_SCHEME}{DOMAIN}/{device.id}/{event_identifier}" ) assert browse.domain == DOMAIN assert browse.identifier == f"{device.id}/{event_identifier}" @@ -383,8 +382,8 @@ async def test_camera_event(hass, hass_client, subscriber, auth, setup_platform) assert not browse.can_play # Resolving the event links to the media - media = await media_source.async_resolve_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{event_identifier}", None + media = await async_resolve_media( + hass, f"{URI_SCHEME}{DOMAIN}/{device.id}/{event_identifier}", None ) assert media.url == f"/api/nest/event_media/{device.id}/{event_identifier}" assert media.mime_type == "image/jpeg" @@ -396,9 +395,7 @@ async def test_camera_event(hass, hass_client, subscriber, auth, setup_platform) assert contents == IMAGE_BYTES_FROM_EVENT # Resolving the device id points to the most recent event - media = await media_source.async_resolve_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}", None - ) + media = await async_resolve_media(hass, f"{URI_SCHEME}{DOMAIN}/{device.id}", None) assert media.url == f"/api/nest/event_media/{device.id}/{event_identifier}" assert media.mime_type == "image/jpeg" @@ -446,9 +443,7 @@ async def test_event_order(hass, auth, subscriber, setup_platform): assert device assert device.name == DEVICE_NAME - browse = await media_source.async_browse_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}" - ) + browse = await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}/{device.id}") assert browse.domain == DOMAIN assert browse.identifier == device.id assert browse.title == "Front: Recent Events" @@ -528,9 +523,7 @@ async def test_multiple_image_events_in_session( assert received_event.data["type"] == "camera_person" event_identifier2 = received_event.data["nest_event_id"] - browse = await media_source.async_browse_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}" - ) + browse = await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}/{device.id}") assert browse.domain == DOMAIN assert browse.identifier == device.id assert browse.title == "Front: Recent Events" @@ -556,8 +549,8 @@ async def test_multiple_image_events_in_session( assert not event.can_play # Resolve the most recent event - media = await media_source.async_resolve_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{event_identifier2}", None + media = await async_resolve_media( + hass, f"{URI_SCHEME}{DOMAIN}/{device.id}/{event_identifier2}", None ) assert media.url == f"/api/nest/event_media/{device.id}/{event_identifier2}" assert media.mime_type == "image/jpeg" @@ -569,8 +562,8 @@ async def test_multiple_image_events_in_session( assert contents == IMAGE_BYTES_FROM_EVENT + b"-2" # Resolving the event links to the media - media = await media_source.async_resolve_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{event_identifier1}", None + media = await async_resolve_media( + hass, f"{URI_SCHEME}{DOMAIN}/{device.id}/{event_identifier1}", None ) assert media.url == f"/api/nest/event_media/{device.id}/{event_identifier1}" assert media.mime_type == "image/jpeg" @@ -638,9 +631,7 @@ async def test_multiple_clip_preview_events_in_session( assert received_event.data["type"] == "camera_person" event_identifier2 = received_event.data["nest_event_id"] - browse = await media_source.async_browse_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}" - ) + browse = await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}/{device.id}") assert browse.domain == DOMAIN assert browse.identifier == device.id assert browse.title == "Front: Recent Events" @@ -659,8 +650,8 @@ async def test_multiple_clip_preview_events_in_session( # Resolve media for each event that was published and they will resolve # to the same clip preview media clip object. # Resolve media for the first event - media = await media_source.async_resolve_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{event_identifier1}", None + media = await async_resolve_media( + hass, f"{URI_SCHEME}{DOMAIN}/{device.id}/{event_identifier1}", None ) assert media.url == f"/api/nest/event_media/{device.id}/{event_identifier1}" assert media.mime_type == "video/mp4" @@ -672,8 +663,8 @@ async def test_multiple_clip_preview_events_in_session( assert contents == IMAGE_BYTES_FROM_EVENT # Resolve media for the second event - media = await media_source.async_resolve_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{event_identifier1}", None + media = await async_resolve_media( + hass, f"{URI_SCHEME}{DOMAIN}/{device.id}/{event_identifier1}", None ) assert media.url == f"/api/nest/event_media/{device.id}/{event_identifier1}" assert media.mime_type == "video/mp4" @@ -694,13 +685,12 @@ async def test_browse_invalid_device_id(hass, auth, setup_platform): assert device.name == DEVICE_NAME with pytest.raises(BrowseError): - await media_source.async_browse_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/invalid-device-id" - ) + await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}/invalid-device-id") with pytest.raises(BrowseError): - await media_source.async_browse_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/invalid-device-id/invalid-event-id" + await async_browse_media( + hass, + f"{URI_SCHEME}{DOMAIN}/invalid-device-id/invalid-event-id", ) @@ -713,17 +703,15 @@ async def test_browse_invalid_event_id(hass, auth, setup_platform): assert device assert device.name == DEVICE_NAME - browse = await media_source.async_browse_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}" - ) + browse = await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}/{device.id}") assert browse.domain == DOMAIN assert browse.identifier == device.id assert browse.title == "Front: Recent Events" with pytest.raises(BrowseError): - await media_source.async_browse_media( + await async_browse_media( hass, - f"{const.URI_SCHEME}{DOMAIN}/{device.id}/GXXWRWVeHNUlUU3V3MGV3bUOYW...", + f"{URI_SCHEME}{DOMAIN}/{device.id}/GXXWRWVeHNUlUU3V3MGV3bUOYW...", ) @@ -737,9 +725,9 @@ async def test_resolve_missing_event_id(hass, auth, setup_platform): assert device.name == DEVICE_NAME with pytest.raises(Unresolvable): - await media_source.async_resolve_media( + await async_resolve_media( hass, - f"{const.URI_SCHEME}{DOMAIN}/{device.id}", + f"{URI_SCHEME}{DOMAIN}/{device.id}", None, ) @@ -748,9 +736,9 @@ async def test_resolve_invalid_device_id(hass, auth, setup_platform): """Test resolving media for an invalid event id.""" await setup_platform() with pytest.raises(Unresolvable): - await media_source.async_resolve_media( + await async_resolve_media( hass, - f"{const.URI_SCHEME}{DOMAIN}/invalid-device-id/GXXWRWVeHNUlUU3V3MGV3bUOYW...", + f"{URI_SCHEME}{DOMAIN}/invalid-device-id/GXXWRWVeHNUlUU3V3MGV3bUOYW...", None, ) @@ -766,9 +754,9 @@ async def test_resolve_invalid_event_id(hass, auth, setup_platform): # Assume any event ID can be resolved to a media url. Fetching the actual media may fail # if the ID is not valid. Content type is inferred based on the capabilities of the device. - media = await media_source.async_resolve_media( + media = await async_resolve_media( hass, - f"{const.URI_SCHEME}{DOMAIN}/{device.id}/GXXWRWVeHNUlUU3V3MGV3bUOYW...", + f"{URI_SCHEME}{DOMAIN}/{device.id}/GXXWRWVeHNUlUU3V3MGV3bUOYW...", None, ) assert ( @@ -815,7 +803,7 @@ async def test_camera_event_clip_preview( event_identifier = received_event.data["nest_event_id"] # List devices - browse = await media_source.async_browse_media(hass, f"{const.URI_SCHEME}{DOMAIN}") + browse = await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}") assert browse.domain == DOMAIN assert len(browse.children) == 1 assert browse.children[0].domain == DOMAIN @@ -827,9 +815,7 @@ async def test_camera_event_clip_preview( ) assert browse.children[0].can_play # Browse to the device - browse = await media_source.async_browse_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}" - ) + browse = await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}/{device.id}") assert browse.domain == DOMAIN assert browse.identifier == device.id assert browse.title == "Front: Recent Events" @@ -853,8 +839,8 @@ async def test_camera_event_clip_preview( assert browse.children[0].identifier == f"{device.id}/{event_identifier}" # Browse to the event - browse = await media_source.async_browse_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{event_identifier}" + browse = await async_browse_media( + hass, f"{URI_SCHEME}{DOMAIN}/{device.id}/{event_identifier}" ) assert browse.domain == DOMAIN event_timestamp_string = event_timestamp.strftime(DATE_STR_FORMAT) @@ -864,8 +850,8 @@ async def test_camera_event_clip_preview( assert browse.can_play # Resolving the event links to the media - media = await media_source.async_resolve_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{event_identifier}", None + media = await async_resolve_media( + hass, f"{URI_SCHEME}{DOMAIN}/{device.id}/{event_identifier}", None ) assert media.url == f"/api/nest/event_media/{device.id}/{event_identifier}" assert media.mime_type == "video/mp4" @@ -950,8 +936,8 @@ async def test_event_media_failure(hass, auth, hass_client, subscriber, setup_pl event_identifier = received_event.data["nest_event_id"] # Resolving the event links to the media - media = await media_source.async_resolve_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{event_identifier}", None + media = await async_resolve_media( + hass, f"{URI_SCHEME}{DOMAIN}/{device.id}/{event_identifier}", None ) assert media.url == f"/api/nest/event_media/{device.id}/{event_identifier}" assert media.mime_type == "image/jpeg" @@ -1011,17 +997,13 @@ async def test_multiple_devices( assert device2 # Very no events have been received yet - browse = await media_source.async_browse_media(hass, f"{const.URI_SCHEME}{DOMAIN}") + browse = await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}") assert len(browse.children) == 2 assert not browse.children[0].can_play assert not browse.children[1].can_play - browse = await media_source.async_browse_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{device1.id}" - ) + browse = await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}/{device1.id}") assert len(browse.children) == 0 - browse = await media_source.async_browse_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{device2.id}" - ) + browse = await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}/{device2.id}") assert len(browse.children) == 0 # Send events for device #1 @@ -1040,17 +1022,13 @@ async def test_multiple_devices( ) await hass.async_block_till_done() - browse = await media_source.async_browse_media(hass, f"{const.URI_SCHEME}{DOMAIN}") + browse = await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}") assert len(browse.children) == 2 assert browse.children[0].can_play assert not browse.children[1].can_play - browse = await media_source.async_browse_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{device1.id}" - ) + browse = await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}/{device1.id}") assert len(browse.children) == 5 - browse = await media_source.async_browse_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{device2.id}" - ) + browse = await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}/{device2.id}") assert len(browse.children) == 0 # Send events for device #2 @@ -1066,17 +1044,13 @@ async def test_multiple_devices( ) await hass.async_block_till_done() - browse = await media_source.async_browse_media(hass, f"{const.URI_SCHEME}{DOMAIN}") + browse = await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}") assert len(browse.children) == 2 assert browse.children[0].can_play assert browse.children[1].can_play - browse = await media_source.async_browse_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{device1.id}" - ) + browse = await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}/{device1.id}") assert len(browse.children) == 5 - browse = await media_source.async_browse_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{device2.id}" - ) + browse = await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}/{device2.id}") assert len(browse.children) == 3 @@ -1120,9 +1094,7 @@ async def test_media_store_persistence( await hass.async_block_till_done() # Browse to event - browse = await media_source.async_browse_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}" - ) + browse = await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}/{device.id}") assert len(browse.children) == 1 assert browse.children[0].domain == DOMAIN event_timestamp_string = event_timestamp.strftime(DATE_STR_FORMAT) @@ -1131,8 +1103,8 @@ async def test_media_store_persistence( assert browse.children[0].can_play event_identifier = browse.children[0].identifier - media = await media_source.async_resolve_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{event_identifier}", None + media = await async_resolve_media( + hass, f"{URI_SCHEME}{DOMAIN}/{event_identifier}", None ) assert media.url == f"/api/nest/event_media/{event_identifier}" assert media.mime_type == "video/mp4" @@ -1164,9 +1136,7 @@ async def test_media_store_persistence( assert device.name == DEVICE_NAME # Verify event metadata exists - browse = await media_source.async_browse_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}" - ) + browse = await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}/{device.id}") assert len(browse.children) == 1 assert browse.children[0].domain == DOMAIN event_timestamp_string = event_timestamp.strftime(DATE_STR_FORMAT) @@ -1175,8 +1145,8 @@ async def test_media_store_persistence( assert browse.children[0].can_play event_identifier = browse.children[0].identifier - media = await media_source.async_resolve_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{event_identifier}", None + media = await async_resolve_media( + hass, f"{URI_SCHEME}{DOMAIN}/{event_identifier}", None ) assert media.url == f"/api/nest/event_media/{event_identifier}" assert media.mime_type == "video/mp4" @@ -1220,16 +1190,14 @@ async def test_media_store_save_filesystem_error( assert device assert device.name == DEVICE_NAME - browse = await media_source.async_browse_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}" - ) + browse = await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}/{device.id}") assert browse.domain == DOMAIN assert browse.identifier == device.id assert len(browse.children) == 1 event = browse.children[0] - media = await media_source.async_resolve_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{event.identifier}", None + media = await async_resolve_media( + hass, f"{URI_SCHEME}{DOMAIN}/{event.identifier}", None ) assert media.url == f"/api/nest/event_media/{event.identifier}" assert media.mime_type == "video/mp4" @@ -1304,9 +1272,7 @@ async def test_camera_event_media_eviction( assert device.name == DEVICE_NAME # Browse to the device - browse = await media_source.async_browse_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}" - ) + browse = await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}/{device.id}") assert browse.domain == DOMAIN assert browse.identifier == device.id assert browse.title == "Front: Recent Events" @@ -1330,9 +1296,7 @@ async def test_camera_event_media_eviction( await hass.async_block_till_done() # Cache is limited to 5 events removing media as the cache is filled - browse = await media_source.async_browse_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}" - ) + browse = await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}/{device.id}") assert len(browse.children) == 5 auth.responses = [ @@ -1354,9 +1318,7 @@ async def test_camera_event_media_eviction( await hass.async_block_till_done() assert mock_remove.called - browse = await media_source.async_browse_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}" - ) + browse = await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}/{device.id}") assert len(browse.children) == 5 child_events = iter(browse.children) @@ -1404,8 +1366,8 @@ async def test_camera_image_resize(hass, auth, hass_client, subscriber, setup_pl assert received_event.data["type"] == "camera_person" event_identifier = received_event.data["nest_event_id"] - browse = await media_source.async_browse_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{event_identifier}" + browse = await async_browse_media( + hass, f"{URI_SCHEME}{DOMAIN}/{device.id}/{event_identifier}" ) assert browse.domain == DOMAIN assert browse.identifier == f"{device.id}/{event_identifier}" @@ -1424,7 +1386,7 @@ async def test_camera_image_resize(hass, auth, hass_client, subscriber, setup_pl assert contents == IMAGE_BYTES_FROM_EVENT # The event thumbnail is used for the device thumbnail - browse = await media_source.async_browse_media(hass, f"{const.URI_SCHEME}{DOMAIN}") + browse = await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}") assert browse.domain == DOMAIN assert len(browse.children) == 1 assert browse.children[0].identifier == device.id @@ -1436,9 +1398,7 @@ async def test_camera_image_resize(hass, auth, hass_client, subscriber, setup_pl assert browse.children[0].can_play # Browse to device. No thumbnail is needed for the device on the device page - browse = await media_source.async_browse_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}" - ) + browse = await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}/{device.id}") assert browse.domain == DOMAIN assert browse.identifier == device.id assert browse.title == "Front: Recent Events" From beca4bb7a59dab753a1395d0a986dd3526d03b7f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 19 Sep 2022 15:42:19 +0200 Subject: [PATCH 561/955] Adjust root-import in motioneye tests (#78770) --- .../components/motioneye/test_media_source.py | 80 +++++++++---------- 1 file changed, 39 insertions(+), 41 deletions(-) diff --git a/tests/components/motioneye/test_media_source.py b/tests/components/motioneye/test_media_source.py index 2cf31c21da7..541db872b51 100644 --- a/tests/components/motioneye/test_media_source.py +++ b/tests/components/motioneye/test_media_source.py @@ -5,10 +5,14 @@ from unittest.mock import AsyncMock, Mock, call from motioneye_client.client import MotionEyeClientPathError import pytest -from homeassistant.components import media_source -from homeassistant.components.media_source import const -from homeassistant.components.media_source.error import MediaSourceError, Unresolvable -from homeassistant.components.media_source.models import PlayMedia +from homeassistant.components.media_source import ( + URI_SCHEME, + MediaSourceError, + PlayMedia, + Unresolvable, + async_browse_media, + async_resolve_media, +) from homeassistant.components.motioneye.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr @@ -86,9 +90,9 @@ async def test_async_browse_media_success(hass: HomeAssistant) -> None: identifiers={TEST_CAMERA_DEVICE_IDENTIFIER}, ) - media = await media_source.async_browse_media( + media = await async_browse_media( hass, - f"{const.URI_SCHEME}{DOMAIN}", + f"{URI_SCHEME}{DOMAIN}", ) assert media.as_dict() == { @@ -117,9 +121,7 @@ async def test_async_browse_media_success(hass: HomeAssistant) -> None: "not_shown": 0, } - media = await media_source.async_browse_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{config.entry_id}" - ) + media = await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}/{config.entry_id}") assert media.as_dict() == { "title": "http://test:8766", @@ -150,8 +152,8 @@ async def test_async_browse_media_success(hass: HomeAssistant) -> None: "not_shown": 0, } - media = await media_source.async_browse_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{config.entry_id}#{device.id}" + media = await async_browse_media( + hass, f"{URI_SCHEME}{DOMAIN}/{config.entry_id}#{device.id}" ) assert media.as_dict() == { "title": "http://test:8766 Test Camera", @@ -196,8 +198,8 @@ async def test_async_browse_media_success(hass: HomeAssistant) -> None: } client.async_get_movies = AsyncMock(return_value=TEST_MOVIES) - media = await media_source.async_browse_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{config.entry_id}#{device.id}#movies" + media = await async_browse_media( + hass, f"{URI_SCHEME}{DOMAIN}/{config.entry_id}#{device.id}#movies" ) assert media.as_dict() == { @@ -231,9 +233,9 @@ async def test_async_browse_media_success(hass: HomeAssistant) -> None: } client.get_movie_url = Mock(return_value="http://movie") - media = await media_source.async_browse_media( + media = await async_browse_media( hass, - f"{const.URI_SCHEME}{DOMAIN}/{config.entry_id}#{device.id}#movies#/2021-04-25", + f"{URI_SCHEME}{DOMAIN}/{config.entry_id}#{device.id}#movies#/2021-04-25", ) assert media.as_dict() == { "title": "http://test:8766 Test Camera Movies 2021-04-25", @@ -310,9 +312,9 @@ async def test_async_browse_media_images_success(hass: HomeAssistant) -> None: client.async_get_images = AsyncMock(return_value=TEST_IMAGES) client.get_image_url = Mock(return_value="http://image") - media = await media_source.async_browse_media( + media = await async_browse_media( hass, - f"{const.URI_SCHEME}{DOMAIN}/{config.entry_id}#{device.id}#images#/2021-04-12", + f"{URI_SCHEME}{DOMAIN}/{config.entry_id}#{device.id}#images#/2021-04-12", ) assert media.as_dict() == { "title": "http://test:8766 Test Camera Images 2021-04-12", @@ -361,10 +363,10 @@ async def test_async_resolve_media_success(hass: HomeAssistant) -> None: # Test successful resolve for a movie. client.get_movie_url = Mock(return_value="http://movie-url") - media = await media_source.async_resolve_media( + media = await async_resolve_media( hass, ( - f"{const.URI_SCHEME}{DOMAIN}" + f"{URI_SCHEME}{DOMAIN}" f"/{TEST_CONFIG_ENTRY_ID}#{device.id}#movies#/foo.mp4" ), None, @@ -374,10 +376,10 @@ async def test_async_resolve_media_success(hass: HomeAssistant) -> None: # Test successful resolve for an image. client.get_image_url = Mock(return_value="http://image-url") - media = await media_source.async_resolve_media( + media = await async_resolve_media( hass, ( - f"{const.URI_SCHEME}{DOMAIN}" + f"{URI_SCHEME}{DOMAIN}" f"/{TEST_CONFIG_ENTRY_ID}#{device.id}#images#/foo.jpg" ), None, @@ -411,28 +413,24 @@ async def test_async_resolve_media_failure(hass: HomeAssistant) -> None: # URI doesn't contain necessary components. with pytest.raises(Unresolvable): - await media_source.async_resolve_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/foo", None - ) + await async_resolve_media(hass, f"{URI_SCHEME}{DOMAIN}/foo", None) # Config entry doesn't exist. with pytest.raises(MediaSourceError): - await media_source.async_resolve_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/1#2#3#4", None - ) + await async_resolve_media(hass, f"{URI_SCHEME}{DOMAIN}/1#2#3#4", None) # Device doesn't exist. with pytest.raises(MediaSourceError): - await media_source.async_resolve_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{TEST_CONFIG_ENTRY_ID}#2#3#4", None + await async_resolve_media( + hass, f"{URI_SCHEME}{DOMAIN}/{TEST_CONFIG_ENTRY_ID}#2#3#4", None ) # Device identifiers are incorrect (no camera id) with pytest.raises(MediaSourceError): - await media_source.async_resolve_media( + await async_resolve_media( hass, ( - f"{const.URI_SCHEME}{DOMAIN}" + f"{URI_SCHEME}{DOMAIN}" f"/{TEST_CONFIG_ENTRY_ID}#{broken_device_1.id}#images#4" ), None, @@ -440,10 +438,10 @@ async def test_async_resolve_media_failure(hass: HomeAssistant) -> None: # Device identifiers are incorrect (non integer camera id) with pytest.raises(MediaSourceError): - await media_source.async_resolve_media( + await async_resolve_media( hass, ( - f"{const.URI_SCHEME}{DOMAIN}" + f"{URI_SCHEME}{DOMAIN}" f"/{TEST_CONFIG_ENTRY_ID}#{broken_device_2.id}#images#4" ), None, @@ -451,19 +449,19 @@ async def test_async_resolve_media_failure(hass: HomeAssistant) -> None: # Kind is incorrect. with pytest.raises(MediaSourceError): - await media_source.async_resolve_media( + await async_resolve_media( hass, - f"{const.URI_SCHEME}{DOMAIN}/{TEST_CONFIG_ENTRY_ID}#{device.id}#games#moo", + f"{URI_SCHEME}{DOMAIN}/{TEST_CONFIG_ENTRY_ID}#{device.id}#games#moo", None, ) # Playback URL raises exception. client.get_movie_url = Mock(side_effect=MotionEyeClientPathError) with pytest.raises(Unresolvable): - await media_source.async_resolve_media( + await async_resolve_media( hass, ( - f"{const.URI_SCHEME}{DOMAIN}" + f"{URI_SCHEME}{DOMAIN}" f"/{TEST_CONFIG_ENTRY_ID}#{device.id}#movies#/foo.mp4" ), None, @@ -472,10 +470,10 @@ async def test_async_resolve_media_failure(hass: HomeAssistant) -> None: # Media path does not start with '/' client.get_movie_url = Mock(side_effect=MotionEyeClientPathError) with pytest.raises(MediaSourceError): - await media_source.async_resolve_media( + await async_resolve_media( hass, ( - f"{const.URI_SCHEME}{DOMAIN}" + f"{URI_SCHEME}{DOMAIN}" f"/{TEST_CONFIG_ENTRY_ID}#{device.id}#movies#foo.mp4" ), None, @@ -484,9 +482,9 @@ async def test_async_resolve_media_failure(hass: HomeAssistant) -> None: # Media missing path. broken_movies = {"mediaList": [{}, {"path": "something", "mimeType": "NOT_A_MIME"}]} client.async_get_movies = AsyncMock(return_value=broken_movies) - media = await media_source.async_browse_media( + media = await async_browse_media( hass, - f"{const.URI_SCHEME}{DOMAIN}/{config.entry_id}#{device.id}#movies#/2021-04-25", + f"{URI_SCHEME}{DOMAIN}/{config.entry_id}#{device.id}#movies#/2021-04-25", ) assert media.as_dict() == { "title": "http://test:8766 Test Camera Movies 2021-04-25", From e6970cb62fb0ee3f803dbfb032e14b3dd01342c4 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 19 Sep 2022 15:42:59 +0200 Subject: [PATCH 562/955] Adjust root-import in netatmo tests (#78771) --- tests/components/netatmo/test_media_source.py | 55 +++++++++---------- 1 file changed, 26 insertions(+), 29 deletions(-) diff --git a/tests/components/netatmo/test_media_source.py b/tests/components/netatmo/test_media_source.py index 390da95496a..01422dfd118 100644 --- a/tests/components/netatmo/test_media_source.py +++ b/tests/components/netatmo/test_media_source.py @@ -3,9 +3,14 @@ import ast import pytest -from homeassistant.components import media_source -from homeassistant.components.media_source import const -from homeassistant.components.media_source.models import PlayMedia +from homeassistant.components.media_source import ( + DOMAIN as MS_DOMAIN, + URI_SCHEME, + BrowseError, + PlayMedia, + async_browse_media, + async_resolve_media, +) from homeassistant.components.netatmo import DATA_CAMERAS, DATA_EVENTS, DOMAIN from homeassistant.setup import async_setup_component @@ -27,59 +32,51 @@ async def test_async_browse_media(hass): "12:34:56:78:90:ac": "MyOutdoorCamera", } - assert await async_setup_component(hass, const.DOMAIN, {}) + assert await async_setup_component(hass, MS_DOMAIN, {}) await hass.async_block_till_done() # Test camera not exists - with pytest.raises(media_source.BrowseError) as excinfo: - await media_source.async_browse_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/events/98:76:54:32:10:ff" - ) + with pytest.raises(BrowseError) as excinfo: + await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}/events/98:76:54:32:10:ff") assert str(excinfo.value) == "Camera does not exist." # Test browse event - with pytest.raises(media_source.BrowseError) as excinfo: - await media_source.async_browse_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/events/12:34:56:78:90:ab/12345" + with pytest.raises(BrowseError) as excinfo: + await async_browse_media( + hass, f"{URI_SCHEME}{DOMAIN}/events/12:34:56:78:90:ab/12345" ) assert str(excinfo.value) == "Event does not exist." # Test invalid base - with pytest.raises(media_source.BrowseError) as excinfo: - await media_source.async_browse_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/invalid/base" - ) + with pytest.raises(BrowseError) as excinfo: + await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}/invalid/base") assert str(excinfo.value) == "Unknown source directory." # Test invalid base - with pytest.raises(media_source.BrowseError) as excinfo: - await media_source.async_browse_media(hass, f"{const.URI_SCHEME}{DOMAIN}/") + with pytest.raises(BrowseError) as excinfo: + await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}/") assert str(excinfo.value) == "Invalid media source URI" # Test successful listing - media = await media_source.async_browse_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/events" - ) + media = await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}/events") # Test successful listing - media = await media_source.async_browse_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/events/" - ) + media = await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}/events/") # Test successful events listing - media = await media_source.async_browse_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/events/12:34:56:78:90:ab" + media = await async_browse_media( + hass, f"{URI_SCHEME}{DOMAIN}/events/12:34:56:78:90:ab" ) # Test successful event listing - media = await media_source.async_browse_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/events/12:34:56:78:90:ab/1599152672" + media = await async_browse_media( + hass, f"{URI_SCHEME}{DOMAIN}/events/12:34:56:78:90:ab/1599152672" ) assert media # Test successful event resolve - media = await media_source.async_resolve_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/events/12:34:56:78:90:ab/1599152672", None + media = await async_resolve_media( + hass, f"{URI_SCHEME}{DOMAIN}/events/12:34:56:78:90:ab/1599152672", None ) assert media == PlayMedia( url="http:///files/high/index.m3u8", mime_type="application/x-mpegURL" From 7cfc28e915a31994faa9d6937ec15c32e75fa062 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 19 Sep 2022 15:46:47 +0200 Subject: [PATCH 563/955] Add unit constant for revolutions per minute (#78752) --- homeassistant/components/comfoconnect/sensor.py | 5 +++-- homeassistant/components/xiaomi_miio/number.py | 6 +++--- homeassistant/components/xiaomi_miio/sensor.py | 11 ++++++----- homeassistant/const.py | 3 +++ 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/comfoconnect/sensor.py b/homeassistant/components/comfoconnect/sensor.py index 2be98b9eeb5..efb6b11a6d9 100644 --- a/homeassistant/components/comfoconnect/sensor.py +++ b/homeassistant/components/comfoconnect/sensor.py @@ -41,6 +41,7 @@ from homeassistant.const import ( ENERGY_KILO_WATT_HOUR, PERCENTAGE, POWER_WATT, + REVOLUTIONS_PER_MINUTE, TEMP_CELSIUS, TIME_DAYS, VOLUME_FLOW_RATE_CUBIC_METERS_PER_HOUR, @@ -159,7 +160,7 @@ SENSOR_TYPES = ( key=ATTR_SUPPLY_FAN_SPEED, state_class=SensorStateClass.MEASUREMENT, name="Supply fan speed", - native_unit_of_measurement="rpm", + native_unit_of_measurement=REVOLUTIONS_PER_MINUTE, icon="mdi:fan-plus", sensor_id=SENSOR_FAN_SUPPLY_SPEED, ), @@ -175,7 +176,7 @@ SENSOR_TYPES = ( key=ATTR_EXHAUST_FAN_SPEED, state_class=SensorStateClass.MEASUREMENT, name="Exhaust fan speed", - native_unit_of_measurement="rpm", + native_unit_of_measurement=REVOLUTIONS_PER_MINUTE, icon="mdi:fan-minus", sensor_id=SENSOR_FAN_EXHAUST_SPEED, ), diff --git a/homeassistant/components/xiaomi_miio/number.py b/homeassistant/components/xiaomi_miio/number.py index b324ab318a0..15cb7175d91 100644 --- a/homeassistant/components/xiaomi_miio/number.py +++ b/homeassistant/components/xiaomi_miio/number.py @@ -12,7 +12,7 @@ from homeassistant.components.number import ( NumberEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_MODEL, DEGREE, TIME_MINUTES +from homeassistant.const import CONF_MODEL, DEGREE, REVOLUTIONS_PER_MINUTE, TIME_MINUTES from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity import EntityCategory @@ -135,7 +135,7 @@ NUMBER_TYPES = { key=ATTR_MOTOR_SPEED, name="Motor speed", icon="mdi:fast-forward-outline", - native_unit_of_measurement="rpm", + native_unit_of_measurement=REVOLUTIONS_PER_MINUTE, native_min_value=200, native_max_value=2000, native_step=10, @@ -219,7 +219,7 @@ NUMBER_TYPES = { key=ATTR_FAVORITE_RPM, name="Favorite motor speed", icon="mdi:star-cog", - native_unit_of_measurement="rpm", + native_unit_of_measurement=REVOLUTIONS_PER_MINUTE, native_min_value=300, native_max_value=2200, native_step=10, diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index 472339a4103..6a84874e2e6 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -35,6 +35,7 @@ from homeassistant.const import ( PERCENTAGE, POWER_WATT, PRESSURE_HPA, + REVOLUTIONS_PER_MINUTE, TEMP_CELSIUS, TIME_DAYS, TIME_HOURS, @@ -192,7 +193,7 @@ SENSOR_TYPES = { ATTR_ACTUAL_SPEED: XiaomiMiioSensorDescription( key=ATTR_ACTUAL_SPEED, name="Actual speed", - native_unit_of_measurement="rpm", + native_unit_of_measurement=REVOLUTIONS_PER_MINUTE, icon="mdi:fast-forward", state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -200,7 +201,7 @@ SENSOR_TYPES = { ATTR_CONTROL_SPEED: XiaomiMiioSensorDescription( key=ATTR_CONTROL_SPEED, name="Control speed", - native_unit_of_measurement="rpm", + native_unit_of_measurement=REVOLUTIONS_PER_MINUTE, icon="mdi:fast-forward", state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -208,7 +209,7 @@ SENSOR_TYPES = { ATTR_FAVORITE_SPEED: XiaomiMiioSensorDescription( key=ATTR_FAVORITE_SPEED, name="Favorite speed", - native_unit_of_measurement="rpm", + native_unit_of_measurement=REVOLUTIONS_PER_MINUTE, icon="mdi:fast-forward", state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -216,7 +217,7 @@ SENSOR_TYPES = { ATTR_MOTOR_SPEED: XiaomiMiioSensorDescription( key=ATTR_MOTOR_SPEED, name="Motor speed", - native_unit_of_measurement="rpm", + native_unit_of_measurement=REVOLUTIONS_PER_MINUTE, icon="mdi:fast-forward", state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -224,7 +225,7 @@ SENSOR_TYPES = { ATTR_MOTOR2_SPEED: XiaomiMiioSensorDescription( key=ATTR_MOTOR2_SPEED, name="Second motor speed", - native_unit_of_measurement="rpm", + native_unit_of_measurement=REVOLUTIONS_PER_MINUTE, icon="mdi:fast-forward", state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, diff --git a/homeassistant/const.py b/homeassistant/const.py index 7afb781b3d9..27d172265ce 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -588,6 +588,9 @@ UV_INDEX: Final = "UV index" # Percentage units PERCENTAGE: Final = "%" +# Rotational speed units +REVOLUTIONS_PER_MINUTE: Final = "rpm" + # Irradiation units IRRADIATION_WATTS_PER_SQUARE_METER: Final = "W/m²" IRRADIATION_BTUS_PER_HOUR_SQUARE_FOOT: Final = "BTU/(h×ft²)" From 8cc0c4dbbaaf6033f5dfbf84d469cf6f49f6a782 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 19 Sep 2022 15:48:01 +0200 Subject: [PATCH 564/955] Adjust root-import in demo humidifier tests (#78772) --- tests/components/demo/test_humidifier.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/components/demo/test_humidifier.py b/tests/components/demo/test_humidifier.py index cd400f98347..f4c90612f81 100644 --- a/tests/components/demo/test_humidifier.py +++ b/tests/components/demo/test_humidifier.py @@ -3,13 +3,12 @@ import pytest import voluptuous as vol -from homeassistant.components.humidifier.const import ( +from homeassistant.components.humidifier import ( ATTR_HUMIDITY, ATTR_MAX_HUMIDITY, ATTR_MIN_HUMIDITY, DOMAIN, MODE_AWAY, - MODE_ECO, SERVICE_SET_HUMIDITY, SERVICE_SET_MODE, ) @@ -106,13 +105,13 @@ async def test_set_hold_mode_eco(hass): await hass.services.async_call( DOMAIN, SERVICE_SET_MODE, - {ATTR_MODE: MODE_ECO, ATTR_ENTITY_ID: ENTITY_HYGROSTAT}, + {ATTR_MODE: "eco", ATTR_ENTITY_ID: ENTITY_HYGROSTAT}, blocking=True, ) await hass.async_block_till_done() state = hass.states.get(ENTITY_HYGROSTAT) - assert state.attributes.get(ATTR_MODE) == MODE_ECO + assert state.attributes.get(ATTR_MODE) == "eco" async def test_turn_on(hass): From acf8bfb299a14684abc1227cdeb9d4557f379755 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 19 Sep 2022 16:11:39 +0200 Subject: [PATCH 565/955] Migrate Trafikverket Train to new entity naming style (#75208) --- homeassistant/components/trafikverket_train/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/trafikverket_train/sensor.py b/homeassistant/components/trafikverket_train/sensor.py index d9674e5373a..8ecf0a99c31 100644 --- a/homeassistant/components/trafikverket_train/sensor.py +++ b/homeassistant/components/trafikverket_train/sensor.py @@ -93,6 +93,7 @@ class TrainSensor(SensorEntity): _attr_icon = ICON _attr_device_class = SensorDeviceClass.TIMESTAMP + _attr_has_entity_name = True def __init__( self, @@ -106,7 +107,6 @@ class TrainSensor(SensorEntity): ) -> None: """Initialize the sensor.""" self._train_api = train_api - self._attr_name = name self._from_station = from_station self._to_station = to_station self._weekday = weekday From 691df5a394aa4ebd4e67fbd2ce74f26ab5abcc3f Mon Sep 17 00:00:00 2001 From: y34hbuddy <47507530+y34hbuddy@users.noreply.github.com> Date: Mon, 19 Sep 2022 10:22:13 -0400 Subject: [PATCH 566/955] Add support for imperial units of measure in volvooncall (#77669) --- .../components/volvooncall/__init__.py | 25 ++++++++++++++- .../components/volvooncall/config_flow.py | 29 +++++++++++++---- homeassistant/components/volvooncall/const.py | 4 +++ .../components/volvooncall/manifest.json | 2 +- .../components/volvooncall/strings.json | 4 +-- .../volvooncall/translations/en.json | 32 +++++++++---------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../volvooncall/test_config_flow.py | 18 +++++------ 9 files changed, 81 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/volvooncall/__init__.py b/homeassistant/components/volvooncall/__init__.py index 65e3b4b0c4e..5a24712b7a3 100644 --- a/homeassistant/components/volvooncall/__init__.py +++ b/homeassistant/components/volvooncall/__init__.py @@ -15,6 +15,7 @@ from homeassistant.const import ( CONF_REGION, CONF_RESOURCES, CONF_SCAN_INTERVAL, + CONF_UNIT_SYSTEM, CONF_USERNAME, ) from homeassistant.core import HomeAssistant @@ -38,6 +39,9 @@ from .const import ( DOMAIN, PLATFORMS, RESOURCES, + UNIT_SYSTEM_IMPERIAL, + UNIT_SYSTEM_METRIC, + UNIT_SYSTEM_SCANDINAVIAN_MILES, VOLVO_DISCOVERY_NEW, ) from .errors import InvalidAuth @@ -109,6 +113,19 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up the Volvo On Call component from a ConfigEntry.""" + + # added CONF_UNIT_SYSTEM / deprecated CONF_SCANDINAVIAN_MILES in 2022.10 to support imperial units + if CONF_UNIT_SYSTEM not in entry.data: + new_conf = {**entry.data} + + scandinavian_miles: bool = entry.data[CONF_SCANDINAVIAN_MILES] + + new_conf[CONF_UNIT_SYSTEM] = ( + UNIT_SYSTEM_SCANDINAVIAN_MILES if scandinavian_miles else UNIT_SYSTEM_METRIC + ) + + hass.config_entries.async_update_entry(entry, data=new_conf) + session = async_get_clientsession(hass) connection = Connection( @@ -183,7 +200,13 @@ class VolvoData: dashboard = vehicle.dashboard( mutable=self.config_entry.data[CONF_MUTABLE], - scandinavian_miles=self.config_entry.data[CONF_SCANDINAVIAN_MILES], + scandinavian_miles=( + self.config_entry.data[CONF_UNIT_SYSTEM] + == UNIT_SYSTEM_SCANDINAVIAN_MILES + ), + usa_units=( + self.config_entry.data[CONF_UNIT_SYSTEM] == UNIT_SYSTEM_IMPERIAL + ), ) for instrument in ( diff --git a/homeassistant/components/volvooncall/config_flow.py b/homeassistant/components/volvooncall/config_flow.py index 2ed3404ae94..c04fe6b4c4c 100644 --- a/homeassistant/components/volvooncall/config_flow.py +++ b/homeassistant/components/volvooncall/config_flow.py @@ -9,12 +9,23 @@ import voluptuous as vol from volvooncall import Connection from homeassistant import config_entries -from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_USERNAME +from homeassistant.const import ( + CONF_PASSWORD, + CONF_REGION, + CONF_UNIT_SYSTEM, + CONF_USERNAME, +) from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from . import VolvoData -from .const import CONF_MUTABLE, CONF_SCANDINAVIAN_MILES, DOMAIN +from .const import ( + CONF_MUTABLE, + DOMAIN, + UNIT_SYSTEM_IMPERIAL, + UNIT_SYSTEM_METRIC, + UNIT_SYSTEM_SCANDINAVIAN_MILES, +) from .errors import InvalidAuth _LOGGER = logging.getLogger(__name__) @@ -36,7 +47,7 @@ class VolvoOnCallConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): CONF_PASSWORD: "", CONF_REGION: None, CONF_MUTABLE: True, - CONF_SCANDINAVIAN_MILES: False, + CONF_UNIT_SYSTEM: UNIT_SYSTEM_METRIC, } if user_input is not None: @@ -76,10 +87,16 @@ class VolvoOnCallConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): vol.Required(CONF_REGION, default=defaults[CONF_REGION]): vol.In( {"na": "North America", "cn": "China", None: "Rest of world"} ), - vol.Optional(CONF_MUTABLE, default=defaults[CONF_MUTABLE]): bool, vol.Optional( - CONF_SCANDINAVIAN_MILES, default=defaults[CONF_SCANDINAVIAN_MILES] - ): bool, + CONF_UNIT_SYSTEM, default=defaults[CONF_UNIT_SYSTEM] + ): vol.In( + { + UNIT_SYSTEM_METRIC: "Metric", + UNIT_SYSTEM_SCANDINAVIAN_MILES: "Metric with Scandinavian Miles", + UNIT_SYSTEM_IMPERIAL: "Imperial", + } + ), + vol.Optional(CONF_MUTABLE, default=defaults[CONF_MUTABLE]): bool, }, ) diff --git a/homeassistant/components/volvooncall/const.py b/homeassistant/components/volvooncall/const.py index bc72f9c5267..4c969669af6 100644 --- a/homeassistant/components/volvooncall/const.py +++ b/homeassistant/components/volvooncall/const.py @@ -10,6 +10,10 @@ CONF_SERVICE_URL = "service_url" CONF_SCANDINAVIAN_MILES = "scandinavian_miles" CONF_MUTABLE = "mutable" +UNIT_SYSTEM_SCANDINAVIAN_MILES = "scandinavian_miles" +UNIT_SYSTEM_METRIC = "metric" +UNIT_SYSTEM_IMPERIAL = "imperial" + PLATFORMS = { "sensor": "sensor", "binary_sensor": "binary_sensor", diff --git a/homeassistant/components/volvooncall/manifest.json b/homeassistant/components/volvooncall/manifest.json index 4865b95d56b..b9da3657aed 100644 --- a/homeassistant/components/volvooncall/manifest.json +++ b/homeassistant/components/volvooncall/manifest.json @@ -2,7 +2,7 @@ "domain": "volvooncall", "name": "Volvo On Call", "documentation": "https://www.home-assistant.io/integrations/volvooncall", - "requirements": ["volvooncall==0.10.0"], + "requirements": ["volvooncall==0.10.1"], "codeowners": ["@molobrakos"], "iot_class": "cloud_polling", "loggers": ["geopy", "hbmqtt", "volvooncall"], diff --git a/homeassistant/components/volvooncall/strings.json b/homeassistant/components/volvooncall/strings.json index a0504094b4d..9e8471b04b1 100644 --- a/homeassistant/components/volvooncall/strings.json +++ b/homeassistant/components/volvooncall/strings.json @@ -6,8 +6,8 @@ "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]", "region": "Region", - "mutable": "Allow Remote Start / Lock / etc.", - "scandinavian_miles": "Use Scandinavian Miles" + "unit_system": "Unit System", + "mutable": "Allow Remote Start / Lock / etc." } } }, diff --git a/homeassistant/components/volvooncall/translations/en.json b/homeassistant/components/volvooncall/translations/en.json index 2b311052741..a75ab687d97 100644 --- a/homeassistant/components/volvooncall/translations/en.json +++ b/homeassistant/components/volvooncall/translations/en.json @@ -1,29 +1,29 @@ { "config": { - "abort": { - "already_configured": "Account is already configured", - "reauth_successful": "Re-authentication was successful" + "step": { + "user": { + "data": { + "username": "Username", + "password": "Password", + "region": "Region", + "mutable": "Allow Remote Start / Lock / etc.", + "unit_system": "Unit System" + } + } }, "error": { "invalid_auth": "Invalid authentication", "unknown": "Unexpected error" }, - "step": { - "user": { - "data": { - "mutable": "Allow Remote Start / Lock / etc.", - "password": "Password", - "region": "Region", - "scandinavian_miles": "Use Scandinavian Miles", - "username": "Username" - } - } + "abort": { + "already_configured": "Account is already configured", + "reauth_successful": "Re-authentication was successful" } }, "issues": { "deprecated_yaml": { - "description": "Configuring the Volvo On Call platform using YAML is being removed in a future release of Home Assistant.\n\nYour existing configuration has been imported into the UI automatically. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", - "title": "The Volvo On Call YAML configuration is being removed" + "title": "The Volvo On Call YAML configuration is being removed", + "description": "Configuring the Volvo On Call platform using YAML is being removed in a future release of Home Assistant.\n\nYour existing configuration has been imported into the UI automatically. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." } } -} \ No newline at end of file +} diff --git a/requirements_all.txt b/requirements_all.txt index 1dc62b18dcb..4032d10e6a0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2473,7 +2473,7 @@ vilfo-api-client==0.3.2 volkszaehler==0.3.2 # homeassistant.components.volvooncall -volvooncall==0.10.0 +volvooncall==0.10.1 # homeassistant.components.verisure vsure==1.8.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8c8517c9080..3679c3b598a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1698,7 +1698,7 @@ venstarcolortouch==0.18 vilfo-api-client==0.3.2 # homeassistant.components.volvooncall -volvooncall==0.10.0 +volvooncall==0.10.1 # homeassistant.components.verisure vsure==1.8.1 diff --git a/tests/components/volvooncall/test_config_flow.py b/tests/components/volvooncall/test_config_flow.py index b558bb7843f..549dc9d4409 100644 --- a/tests/components/volvooncall/test_config_flow.py +++ b/tests/components/volvooncall/test_config_flow.py @@ -29,8 +29,8 @@ async def test_form(hass: HomeAssistant) -> None: "username": "test-username", "password": "test-password", "region": "na", + "unit_system": "metric", "mutable": True, - "scandinavian_miles": False, }, ) await hass.async_block_till_done() @@ -41,8 +41,8 @@ async def test_form(hass: HomeAssistant) -> None: "username": "test-username", "password": "test-password", "region": "na", + "unit_system": "metric", "mutable": True, - "scandinavian_miles": False, } assert len(mock_setup_entry.mock_calls) == 1 @@ -65,8 +65,8 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: "username": "test-username", "password": "test-password", "region": "na", + "unit_system": "metric", "mutable": True, - "scandinavian_miles": False, }, ) @@ -95,8 +95,8 @@ async def test_flow_already_configured(hass: HomeAssistant) -> None: "username": "test-username", "password": "test-password", "region": "na", + "unit_system": "metric", "mutable": True, - "scandinavian_miles": False, }, ) await hass.async_block_till_done() @@ -121,8 +121,8 @@ async def test_form_other_exception(hass: HomeAssistant) -> None: "username": "test-username", "password": "test-password", "region": "na", + "unit_system": "metric", "mutable": True, - "scandinavian_miles": False, }, ) @@ -151,8 +151,8 @@ async def test_import(hass: HomeAssistant) -> None: "username": "test-username", "password": "test-password", "region": "na", + "unit_system": "metric", "mutable": True, - "scandinavian_miles": False, }, ) await hass.async_block_till_done() @@ -163,8 +163,8 @@ async def test_import(hass: HomeAssistant) -> None: "username": "test-username", "password": "test-password", "region": "na", + "unit_system": "metric", "mutable": True, - "scandinavian_miles": False, } assert len(mock_setup_entry.mock_calls) == 1 @@ -179,8 +179,8 @@ async def test_reauth(hass: HomeAssistant) -> None: "username": "test-username", "password": "test-password", "region": "na", + "unit_system": "metric", "mutable": True, - "scandinavian_miles": False, }, ) first_entry.add_to_hass(hass) @@ -212,8 +212,8 @@ async def test_reauth(hass: HomeAssistant) -> None: "username": "test-username", "password": "test-new-password", "region": "na", + "unit_system": "metric", "mutable": True, - "scandinavian_miles": False, }, ) await hass.async_block_till_done() From d591787077017a16d45154ea2ad204acb10d893b Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 19 Sep 2022 13:15:32 -0600 Subject: [PATCH 567/955] Guard Guardian switches from redundant on/off calls (#78791) --- homeassistant/components/guardian/switch.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/guardian/switch.py b/homeassistant/components/guardian/switch.py index 5471d2471e8..6f5dd4d16b7 100644 --- a/homeassistant/components/guardian/switch.py +++ b/homeassistant/components/guardian/switch.py @@ -146,6 +146,9 @@ class ValveControllerSwitch(ValveControllerEntity, SwitchEntity): async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" + if not self._attr_is_on: + return + try: async with self._client: await self.entity_description.off_action(self._client) @@ -159,6 +162,9 @@ class ValveControllerSwitch(ValveControllerEntity, SwitchEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" + if self._attr_is_on: + return + try: async with self._client: await self.entity_description.on_action(self._client) From 08c8ab7302e39f9f795b284820bb16c54e900f75 Mon Sep 17 00:00:00 2001 From: mkmer Date: Mon, 19 Sep 2022 15:18:53 -0400 Subject: [PATCH 568/955] Bumped AIOAladdinConnect 0.1.46 (#78767) --- .../components/aladdin_connect/__init__.py | 6 ++++-- .../components/aladdin_connect/config_flow.py | 16 ++++++++++------ .../components/aladdin_connect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../aladdin_connect/test_config_flow.py | 3 +++ tests/components/aladdin_connect/test_init.py | 3 +++ 7 files changed, 23 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/aladdin_connect/__init__.py b/homeassistant/components/aladdin_connect/__init__.py index 40dd0dd981a..e66efc1b0ab 100644 --- a/homeassistant/components/aladdin_connect/__init__.py +++ b/homeassistant/components/aladdin_connect/__init__.py @@ -4,6 +4,7 @@ import logging from typing import Final from AIOAladdinConnect import AladdinConnectClient +from AIOAladdinConnect.session_manager import InvalidPasswordError from aiohttp import ClientConnectionError from homeassistant.config_entries import ConfigEntry @@ -27,10 +28,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: username, password, async_get_clientsession(hass), CLIENT_ID ) try: - if not await acc.login(): - raise ConfigEntryAuthFailed("Incorrect Password") + await acc.login() except (ClientConnectionError, asyncio.TimeoutError) as ex: raise ConfigEntryNotReady("Can not connect to host") from ex + except InvalidPasswordError as ex: + raise ConfigEntryAuthFailed("Incorrect Password") from ex hass.data.setdefault(DOMAIN, {})[entry.entry_id] = acc await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/aladdin_connect/config_flow.py b/homeassistant/components/aladdin_connect/config_flow.py index 4f03d7cdb3b..1bfa9757907 100644 --- a/homeassistant/components/aladdin_connect/config_flow.py +++ b/homeassistant/components/aladdin_connect/config_flow.py @@ -7,7 +7,7 @@ import logging from typing import Any from AIOAladdinConnect import AladdinConnectClient -from aiohttp import ClientError +from AIOAladdinConnect.session_manager import InvalidPasswordError from aiohttp.client_exceptions import ClientConnectionError import voluptuous as vol @@ -43,9 +43,13 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None: async_get_clientsession(hass), CLIENT_ID, ) - login = await acc.login() - if not login: - raise InvalidAuth + try: + await acc.login() + except (ClientConnectionError, asyncio.TimeoutError) as ex: + raise ex + + except InvalidPasswordError as ex: + raise InvalidAuth from ex class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @@ -80,7 +84,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): except InvalidAuth: errors["base"] = "invalid_auth" - except (ClientConnectionError, asyncio.TimeoutError, ClientError): + except (ClientConnectionError, asyncio.TimeoutError): errors["base"] = "cannot_connect" else: @@ -117,7 +121,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): except InvalidAuth: errors["base"] = "invalid_auth" - except (ClientConnectionError, asyncio.TimeoutError, ClientError): + except (ClientConnectionError, asyncio.TimeoutError): errors["base"] = "cannot_connect" else: diff --git a/homeassistant/components/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index 3a3efa0f4a2..50ab6af6f85 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.44"], + "requirements": ["AIOAladdinConnect==0.1.46"], "codeowners": ["@mkmer"], "iot_class": "cloud_polling", "loggers": ["aladdin_connect"], diff --git a/requirements_all.txt b/requirements_all.txt index 4032d10e6a0..304ec394577 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.44 +AIOAladdinConnect==0.1.46 # homeassistant.components.adax Adax-local==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3679c3b598a..104275aad87 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.44 +AIOAladdinConnect==0.1.46 # homeassistant.components.adax Adax-local==0.1.4 diff --git a/tests/components/aladdin_connect/test_config_flow.py b/tests/components/aladdin_connect/test_config_flow.py index 19017e79570..47b6645e800 100644 --- a/tests/components/aladdin_connect/test_config_flow.py +++ b/tests/components/aladdin_connect/test_config_flow.py @@ -1,6 +1,7 @@ """Test the Aladdin Connect config flow.""" from unittest.mock import MagicMock, patch +from AIOAladdinConnect.session_manager import InvalidPasswordError from aiohttp.client_exceptions import ClientConnectionError from homeassistant import config_entries @@ -54,6 +55,7 @@ async def test_form_failed_auth( DOMAIN, context={"source": config_entries.SOURCE_USER} ) mock_aladdinconnect_api.login.return_value = False + mock_aladdinconnect_api.login.side_effect = InvalidPasswordError with patch( "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient", return_value=mock_aladdinconnect_api, @@ -231,6 +233,7 @@ async def test_reauth_flow_auth_error( assert result["type"] == FlowResultType.FORM assert result["errors"] == {} mock_aladdinconnect_api.login.return_value = False + mock_aladdinconnect_api.login.side_effect = InvalidPasswordError with patch( "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient", return_value=mock_aladdinconnect_api, diff --git a/tests/components/aladdin_connect/test_init.py b/tests/components/aladdin_connect/test_init.py index 4cbb2333cc5..9084754d569 100644 --- a/tests/components/aladdin_connect/test_init.py +++ b/tests/components/aladdin_connect/test_init.py @@ -1,6 +1,7 @@ """Test for Aladdin Connect init logic.""" from unittest.mock import MagicMock, patch +from AIOAladdinConnect.session_manager import InvalidPasswordError from aiohttp import ClientConnectionError from homeassistant.components.aladdin_connect.const import DOMAIN @@ -43,6 +44,7 @@ async def test_setup_login_error( ) config_entry.add_to_hass(hass) mock_aladdinconnect_api.login.return_value = False + mock_aladdinconnect_api.login.side_effect = InvalidPasswordError with patch( "homeassistant.components.aladdin_connect.cover.AladdinConnectClient", return_value=mock_aladdinconnect_api, @@ -100,6 +102,7 @@ async def test_entry_password_fail( ) entry.add_to_hass(hass) mock_aladdinconnect_api.login = AsyncMock(return_value=False) + mock_aladdinconnect_api.login.side_effect = InvalidPasswordError with patch( "homeassistant.components.aladdin_connect.AladdinConnectClient", return_value=mock_aladdinconnect_api, From 10a12b1bc929f06f3397a954936c754971f0db9e Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Mon, 19 Sep 2022 13:29:29 -0600 Subject: [PATCH 569/955] Bump pylitterbot to 2022.9.5 (#78785) --- homeassistant/components/litterrobot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/litterrobot/test_sensor.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/litterrobot/manifest.json b/homeassistant/components/litterrobot/manifest.json index fb26f0ca685..44a7fd837ed 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.9.3"], + "requirements": ["pylitterbot==2022.9.5"], "codeowners": ["@natekspencer", "@tkdrob"], "dhcp": [{ "hostname": "litter-robot4" }], "iot_class": "cloud_push", diff --git a/requirements_all.txt b/requirements_all.txt index 304ec394577..4aa1c5d07e1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1674,7 +1674,7 @@ pylibrespot-java==0.1.0 pylitejet==0.3.0 # homeassistant.components.litterrobot -pylitterbot==2022.9.3 +pylitterbot==2022.9.5 # homeassistant.components.lutron_caseta pylutron-caseta==0.15.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 104275aad87..a3ea1dbd719 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1169,7 +1169,7 @@ pylibrespot-java==0.1.0 pylitejet==0.3.0 # homeassistant.components.litterrobot -pylitterbot==2022.9.3 +pylitterbot==2022.9.5 # homeassistant.components.lutron_caseta pylutron-caseta==0.15.1 diff --git a/tests/components/litterrobot/test_sensor.py b/tests/components/litterrobot/test_sensor.py index 1e56b7f06b2..536e6ddf188 100644 --- a/tests/components/litterrobot/test_sensor.py +++ b/tests/components/litterrobot/test_sensor.py @@ -88,7 +88,7 @@ async def test_litter_robot_sensor( assert sensor.state == "dfs" assert sensor.attributes["device_class"] == "litterrobot__status_code" sensor = hass.states.get("sensor.test_litter_level") - assert sensor.state == "0.0" + assert sensor.state == "70.0" assert sensor.attributes["unit_of_measurement"] == PERCENTAGE sensor = hass.states.get("sensor.test_pet_weight") assert sensor.state == "12.0" From f07204ba55442e13e71c28535714fb0c024aba52 Mon Sep 17 00:00:00 2001 From: Brad Downey Date: Mon, 19 Sep 2022 12:54:01 -0700 Subject: [PATCH 570/955] Add unique_id to ohmconnect (#78479) Co-authored-by: Franck Nijhof --- homeassistant/components/ohmconnect/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/ohmconnect/sensor.py b/homeassistant/components/ohmconnect/sensor.py index 3d9dbd78419..11606bfc6c2 100644 --- a/homeassistant/components/ohmconnect/sensor.py +++ b/homeassistant/components/ohmconnect/sensor.py @@ -51,6 +51,7 @@ class OhmconnectSensor(SensorEntity): self._name = name self._ohmid = ohmid self._data = {} + self._attr_unique_id = ohmid @property def name(self): From e66f28f3f7de275de610ebee194618488fec1458 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 19 Sep 2022 23:09:50 +0200 Subject: [PATCH 571/955] Teach sqlite3 about HAFakeDatetime (#78756) --- tests/conftest.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 2c95770974b..1e7ac735125 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,6 +7,7 @@ from contextlib import asynccontextmanager import functools from json import JSONDecoder, loads import logging +import sqlite3 import ssl import threading from typing import Any @@ -104,6 +105,11 @@ def pytest_runtest_setup(): freezegun.api.datetime_to_fakedatetime = ha_datetime_to_fakedatetime freezegun.api.FakeDatetime = HAFakeDatetime + def adapt_datetime(val): + return val.isoformat(" ") + + sqlite3.register_adapter(HAFakeDatetime, adapt_datetime) + def ha_datetime_to_fakedatetime(datetime): """Convert datetime to FakeDatetime. From 1021c90bb8723fc827efd49abd472a8817d1ec9a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 19 Sep 2022 23:37:22 +0200 Subject: [PATCH 572/955] Use black to format hassfest generated files (#78794) --- .../generated/application_credentials.py | 4 +- homeassistant/generated/bluetooth.py | 120 +++--- homeassistant/generated/config_flows.py | 8 +- homeassistant/generated/dhcp.py | 387 +++++++++--------- homeassistant/generated/mqtt.py | 6 +- homeassistant/generated/ssdp.py | 208 +++++----- homeassistant/generated/supported_brands.py | 8 +- homeassistant/generated/usb.py | 42 +- homeassistant/generated/zeroconf.py | 308 +++++++------- script/hassfest/application_credentials.py | 11 +- script/hassfest/bluetooth.py | 15 +- script/hassfest/config_flow.py | 11 +- script/hassfest/dhcp.py | 18 +- script/hassfest/mqtt.py | 12 +- script/hassfest/serializer.py | 37 ++ script/hassfest/ssdp.py | 16 +- script/hassfest/supported_brands.py | 13 +- script/hassfest/usb.py | 11 +- script/hassfest/zeroconf.py | 22 +- 19 files changed, 633 insertions(+), 624 deletions(-) create mode 100644 script/hassfest/serializer.py diff --git a/homeassistant/generated/application_credentials.py b/homeassistant/generated/application_credentials.py index fb2b04989f7..d2e16f2b914 100644 --- a/homeassistant/generated/application_credentials.py +++ b/homeassistant/generated/application_credentials.py @@ -3,8 +3,6 @@ To update, run python3 -m script.hassfest """ -# fmt: off - APPLICATION_CREDENTIALS = [ "geocaching", "google", @@ -19,5 +17,5 @@ APPLICATION_CREDENTIALS = [ "spotify", "withings", "xbox", - "yolink" + "yolink", ] diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index f883e507163..f3f789f9829 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -4,23 +4,21 @@ To update, run python3 -m script.hassfest """ from __future__ import annotations -# fmt: off - BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ { "domain": "bluemaestro", "manufacturer_id": 307, - "connectable": False + "connectable": False, }, { "domain": "bthome", "connectable": False, - "service_data_uuid": "0000181c-0000-1000-8000-00805f9b34fb" + "service_data_uuid": "0000181c-0000-1000-8000-00805f9b34fb", }, { "domain": "bthome", "connectable": False, - "service_data_uuid": "0000181e-0000-1000-8000-00805f9b34fb" + "service_data_uuid": "0000181e-0000-1000-8000-00805f9b34fb", }, { "domain": "fjaraskupan", @@ -32,174 +30,174 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ 70, 74, 65, - 82 - ] + 82, + ], }, { "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": 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": 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 + "connectable": False, }, { "domain": "homekit_controller", "manufacturer_id": 76, "manufacturer_data_start": [ - 6 - ] + 6, + ], }, { "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 + "connectable": False, }, { "domain": "led_ble", - "local_name": "LEDnet*" + "local_name": "LEDnet*", }, { "domain": "led_ble", - "local_name": "BLE-LED*" + "local_name": "BLE-LED*", }, { "domain": "led_ble", - "local_name": "LEDBLE*" + "local_name": "LEDBLE*", }, { "domain": "led_ble", - "local_name": "Triones*" + "local_name": "Triones*", }, { "domain": "led_ble", - "local_name": "LEDBlue*" + "local_name": "LEDBlue*", }, { "domain": "led_ble", - "local_name": "Dream~*" + "local_name": "Dream~*", }, { "domain": "led_ble", - "local_name": "QHM-*" + "local_name": "QHM-*", }, { "domain": "led_ble", - "local_name": "AP-*" + "local_name": "AP-*", }, { "domain": "melnor", "manufacturer_data_start": [ - 89 + 89, ], - "manufacturer_id": 13 + "manufacturer_id": 13, }, { "domain": "moat", "local_name": "Moat_S*", - "connectable": False + "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 + "connectable": False, }, { "domain": "sensorpro", @@ -208,9 +206,9 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ 1, 1, 164, - 193 + 193, ], - "connectable": False + "connectable": False, }, { "domain": "sensorpro", @@ -219,79 +217,79 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ 1, 5, 164, - 193 + 193, ], - "connectable": False + "connectable": False, }, { "domain": "sensorpush", "local_name": "SensorPush*", - "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 + 0, ], - "connectable": False + "connectable": False, }, { "domain": "thermobeacon", "service_uuid": "0000fff0-0000-1000-8000-00805f9b34fb", "manufacturer_id": 17, "manufacturer_data_start": [ - 0 + 0, ], - "connectable": False + "connectable": False, }, { "domain": "thermobeacon", "service_uuid": "0000fff0-0000-1000-8000-00805f9b34fb", "manufacturer_id": 21, "manufacturer_data_start": [ - 0 + 0, ], - "connectable": False + "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 + "connectable": False, }, { "domain": "tilt_ble", - "manufacturer_id": 76 + "manufacturer_id": 76, }, { "domain": "xiaomi_ble", "connectable": False, - "service_data_uuid": "0000fe95-0000-1000-8000-00805f9b34fb" + "service_data_uuid": "0000fe95-0000-1000-8000-00805f9b34fb", }, { "domain": "yalexs_ble", "manufacturer_id": 465, - "service_uuid": "0000fe24-0000-1000-8000-00805f9b34fb" - } + "service_uuid": "0000fe24-0000-1000-8000-00805f9b34fb", + }, ] diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 1aa49e279db..81f7f91afb6 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -3,8 +3,6 @@ To update, run python3 -m script.hassfest """ -# fmt: off - FLOWS = { "integration": [ "abode", @@ -456,7 +454,7 @@ FLOWS = { "zerproc", "zha", "zwave_js", - "zwave_me" + "zwave_me", ], "helper": [ "derivative", @@ -466,6 +464,6 @@ FLOWS = { "switch_as_x", "threshold", "tod", - "utility_meter" - ] + "utility_meter", + ], } diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 11e0bbb0405..ae1b7a76a88 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -4,194 +4,201 @@ To update, run python3 -m script.hassfest """ from __future__ import annotations -# fmt: off - 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', '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': '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_*', - 'macaddress': 'DC44271*'}, - {'domain': 'tesla_wall_connector', - 'hostname': 'teslawallconnector_*', - 'macaddress': '98ED5C*'}, - {'domain': 'tesla_wall_connector', - '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': '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": "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": "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": "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": "tesla_wall_connector", + "hostname": "teslawallconnector_*", + "macaddress": "DC44271*", + }, + { + "domain": "tesla_wall_connector", + "hostname": "teslawallconnector_*", + "macaddress": "98ED5C*", + }, + { + "domain": "tesla_wall_connector", + "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": "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 41aac3e3a08..6b22ca26221 100644 --- a/homeassistant/generated/mqtt.py +++ b/homeassistant/generated/mqtt.py @@ -3,10 +3,8 @@ To update, run python3 -m script.hassfest """ -# fmt: off - MQTT = { "tasmota": [ - "tasmota/discovery/#" - ] + "tasmota/discovery/#", + ], } diff --git a/homeassistant/generated/ssdp.py b/homeassistant/generated/ssdp.py index b55240d1dc6..029afcd64fe 100644 --- a/homeassistant/generated/ssdp.py +++ b/homeassistant/generated/ssdp.py @@ -3,324 +3,322 @@ To update, run python3 -m script.hassfest """ -# fmt: off - SSDP = { "arcam_fmj": [ { "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1", - "manufacturer": "ARCAM" - } + "manufacturer": "ARCAM", + }, ], "axis": [ { - "manufacturer": "AXIS" - } + "manufacturer": "AXIS", + }, ], "control4": [ { - "st": "c4:director" - } + "st": "c4:director", + }, ], "deconz": [ { "manufacturer": "Royal Philips Electronics", - "manufacturerURL": "http://www.dresden-elektronik.de" - } + "manufacturerURL": "http://www.dresden-elektronik.de", + }, ], "denonavr": [ { "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1", - "manufacturer": "Denon" + "manufacturer": "Denon", }, { "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1", - "manufacturer": "DENON" + "manufacturer": "DENON", }, { "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1", - "manufacturer": "DENON PROFESSIONAL" + "manufacturer": "DENON PROFESSIONAL", }, { "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1", - "manufacturer": "Marantz" + "manufacturer": "Marantz", }, { "deviceType": "urn:schemas-upnp-org:device:MediaServer:1", - "manufacturer": "Denon" + "manufacturer": "Denon", }, { "deviceType": "urn:schemas-upnp-org:device:MediaServer:1", - "manufacturer": "DENON" + "manufacturer": "DENON", }, { "deviceType": "urn:schemas-upnp-org:device:MediaServer:1", - "manufacturer": "DENON PROFESSIONAL" + "manufacturer": "DENON PROFESSIONAL", }, { "deviceType": "urn:schemas-upnp-org:device:MediaServer:1", - "manufacturer": "Marantz" + "manufacturer": "Marantz", }, { "deviceType": "urn:schemas-denon-com:device:AiosDevice:1", - "manufacturer": "Denon" + "manufacturer": "Denon", }, { "deviceType": "urn:schemas-denon-com:device:AiosDevice:1", - "manufacturer": "DENON" + "manufacturer": "DENON", }, { "deviceType": "urn:schemas-denon-com:device:AiosDevice:1", - "manufacturer": "DENON PROFESSIONAL" + "manufacturer": "DENON PROFESSIONAL", }, { "deviceType": "urn:schemas-denon-com:device:AiosDevice:1", - "manufacturer": "Marantz" - } + "manufacturer": "Marantz", + }, ], "directv": [ { "deviceType": "urn:schemas-upnp-org:device:MediaServer:1", - "manufacturer": "DIRECTV" - } + "manufacturer": "DIRECTV", + }, ], "dlna_dmr": [ { "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1", - "st": "urn:schemas-upnp-org:device:MediaRenderer:1" + "st": "urn:schemas-upnp-org:device:MediaRenderer:1", }, { "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:2", - "st": "urn:schemas-upnp-org:device:MediaRenderer:2" + "st": "urn:schemas-upnp-org:device:MediaRenderer:2", }, { "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:3", - "st": "urn:schemas-upnp-org:device:MediaRenderer:3" - } + "st": "urn:schemas-upnp-org:device:MediaRenderer:3", + }, ], "dlna_dms": [ { "deviceType": "urn:schemas-upnp-org:device:MediaServer:1", - "st": "urn:schemas-upnp-org:device:MediaServer:1" + "st": "urn:schemas-upnp-org:device:MediaServer:1", }, { "deviceType": "urn:schemas-upnp-org:device:MediaServer:2", - "st": "urn:schemas-upnp-org:device:MediaServer:2" + "st": "urn:schemas-upnp-org:device:MediaServer:2", }, { "deviceType": "urn:schemas-upnp-org:device:MediaServer:3", - "st": "urn:schemas-upnp-org:device:MediaServer:3" + "st": "urn:schemas-upnp-org:device:MediaServer:3", }, { "deviceType": "urn:schemas-upnp-org:device:MediaServer:4", - "st": "urn:schemas-upnp-org:device:MediaServer:4" - } + "st": "urn:schemas-upnp-org:device:MediaServer:4", + }, ], "fritz": [ { - "st": "urn:schemas-upnp-org:device:fritzbox:1" - } + "st": "urn:schemas-upnp-org:device:fritzbox:1", + }, ], "fritzbox": [ { - "st": "urn:schemas-upnp-org:device:fritzbox:1" - } + "st": "urn:schemas-upnp-org:device:fritzbox:1", + }, ], "harmony": [ { "deviceType": "urn:myharmony-com:device:harmony:1", - "manufacturer": "Logitech" - } + "manufacturer": "Logitech", + }, ], "heos": [ { - "st": "urn:schemas-denon-com:device:ACT-Denon:1" - } + "st": "urn:schemas-denon-com:device:ACT-Denon:1", + }, ], "huawei_lte": [ { "deviceType": "urn:schemas-upnp-org:device:InternetGatewayDevice:1", - "manufacturer": "Huawei" - } + "manufacturer": "Huawei", + }, ], "hue": [ { "manufacturer": "Royal Philips Electronics", - "modelName": "Philips hue bridge 2012" + "modelName": "Philips hue bridge 2012", }, { "manufacturer": "Royal Philips Electronics", - "modelName": "Philips hue bridge 2015" + "modelName": "Philips hue bridge 2015", }, { "manufacturer": "Signify", - "modelName": "Philips hue bridge 2015" - } + "modelName": "Philips hue bridge 2015", + }, ], "hyperion": [ { "manufacturer": "Hyperion Open Source Ambient Lighting", - "st": "urn:hyperion-project.org:device:basic:1" - } + "st": "urn:hyperion-project.org:device:basic:1", + }, ], "isy994": [ { "deviceType": "urn:udi-com:device:X_Insteon_Lighting_Device:1", - "manufacturer": "Universal Devices Inc." - } + "manufacturer": "Universal Devices Inc.", + }, ], "kaleidescape": [ { "deviceType": "schemas-upnp-org:device:Basic:1", - "manufacturer": "Kaleidescape, Inc." - } + "manufacturer": "Kaleidescape, Inc.", + }, ], "keenetic_ndms2": [ { "deviceType": "urn:schemas-upnp-org:device:InternetGatewayDevice:1", - "manufacturer": "Keenetic Ltd." + "manufacturer": "Keenetic Ltd.", }, { "deviceType": "urn:schemas-upnp-org:device:InternetGatewayDevice:1", - "manufacturer": "ZyXEL Communications Corp." - } + "manufacturer": "ZyXEL Communications Corp.", + }, ], "konnected": [ { - "manufacturer": "konnected.io" - } + "manufacturer": "konnected.io", + }, ], "lametric": [ { - "deviceType": "urn:schemas-upnp-org:device:LaMetric:1" - } + "deviceType": "urn:schemas-upnp-org:device:LaMetric:1", + }, ], "nanoleaf": [ { - "st": "Nanoleaf_aurora:light" + "st": "Nanoleaf_aurora:light", }, { - "st": "nanoleaf:nl29" + "st": "nanoleaf:nl29", }, { - "st": "nanoleaf:nl42" + "st": "nanoleaf:nl42", }, { - "st": "nanoleaf:nl52" - } + "st": "nanoleaf:nl52", + }, ], "netgear": [ { "deviceType": "urn:schemas-upnp-org:device:InternetGatewayDevice:1", - "manufacturer": "NETGEAR, Inc." - } + "manufacturer": "NETGEAR, Inc.", + }, ], "octoprint": [ { "deviceType": "urn:schemas-upnp-org:device:Basic:1", - "manufacturer": "The OctoPrint Project" - } + "manufacturer": "The OctoPrint Project", + }, ], "roku": [ { "deviceType": "urn:roku-com:device:player:1-0", "manufacturer": "Roku", - "st": "roku:ecp" - } + "st": "roku:ecp", + }, ], "samsungtv": [ { - "st": "urn:samsung.com:device:RemoteControlReceiver:1" + "st": "urn:samsung.com:device:RemoteControlReceiver:1", }, { - "st": "urn:samsung.com:service:MainTVAgent2:1" + "st": "urn:samsung.com:service:MainTVAgent2:1", }, { "manufacturer": "Samsung", - "st": "urn:schemas-upnp-org:service:RenderingControl:1" + "st": "urn:schemas-upnp-org:service:RenderingControl:1", }, { "manufacturer": "Samsung Electronics", - "st": "urn:schemas-upnp-org:service:RenderingControl:1" - } + "st": "urn:schemas-upnp-org:service:RenderingControl:1", + }, ], "songpal": [ { "manufacturer": "Sony Corporation", - "st": "urn:schemas-sony-com:service:ScalarWebAPI:1" - } + "st": "urn:schemas-sony-com:service:ScalarWebAPI:1", + }, ], "sonos": [ { - "st": "urn:schemas-upnp-org:device:ZonePlayer:1" - } + "st": "urn:schemas-upnp-org:device:ZonePlayer:1", + }, ], "syncthru": [ { "deviceType": "urn:schemas-upnp-org:device:Printer:1", - "manufacturer": "Samsung Electronics" - } + "manufacturer": "Samsung Electronics", + }, ], "synology_dsm": [ { "deviceType": "urn:schemas-upnp-org:device:Basic:1", - "manufacturer": "Synology" - } + "manufacturer": "Synology", + }, ], "unifi": [ { "manufacturer": "Ubiquiti Networks", - "modelDescription": "UniFi Dream Machine" + "modelDescription": "UniFi Dream Machine", }, { "manufacturer": "Ubiquiti Networks", - "modelDescription": "UniFi Dream Machine Pro" + "modelDescription": "UniFi Dream Machine Pro", }, { "manufacturer": "Ubiquiti Networks", - "modelDescription": "UniFi Dream Machine SE" - } + "modelDescription": "UniFi Dream Machine SE", + }, ], "unifiprotect": [ { "manufacturer": "Ubiquiti Networks", - "modelDescription": "UniFi Dream Machine" + "modelDescription": "UniFi Dream Machine", }, { "manufacturer": "Ubiquiti Networks", - "modelDescription": "UniFi Dream Machine Pro" + "modelDescription": "UniFi Dream Machine Pro", }, { "manufacturer": "Ubiquiti Networks", - "modelDescription": "UniFi Dream Machine SE" - } + "modelDescription": "UniFi Dream Machine SE", + }, ], "upnp": [ { - "st": "urn:schemas-upnp-org:device:InternetGatewayDevice:1" + "st": "urn:schemas-upnp-org:device:InternetGatewayDevice:1", }, { - "st": "urn:schemas-upnp-org:device:InternetGatewayDevice:2" - } + "st": "urn:schemas-upnp-org:device:InternetGatewayDevice:2", + }, ], "webostv": [ { - "st": "urn:lge-com:service:webos-second-screen:1" - } + "st": "urn:lge-com:service:webos-second-screen:1", + }, ], "wemo": [ { - "manufacturer": "Belkin International Inc." - } + "manufacturer": "Belkin International Inc.", + }, ], "wilight": [ { - "manufacturer": "All Automacao Ltda" - } + "manufacturer": "All Automacao Ltda", + }, ], "yamaha_musiccast": [ { - "manufacturer": "Yamaha Corporation" - } - ] + "manufacturer": "Yamaha Corporation", + }, + ], } diff --git a/homeassistant/generated/supported_brands.py b/homeassistant/generated/supported_brands.py index 92c7b827855..39f7c6bea05 100644 --- a/homeassistant/generated/supported_brands.py +++ b/homeassistant/generated/supported_brands.py @@ -3,9 +3,7 @@ To update, run python3 -m script.hassfest """ -# fmt: off - -HAS_SUPPORTED_BRANDS = ( +HAS_SUPPORTED_BRANDS = [ "denonavr", "hunterdouglas_powerview", "inkbird", @@ -15,5 +13,5 @@ HAS_SUPPORTED_BRANDS = ( "thermobeacon", "wemo", "yalexs_ble", - "zwave_js" -) + "zwave_js", +] diff --git a/homeassistant/generated/usb.py b/homeassistant/generated/usb.py index 2a87f33cf4f..901f9f72da5 100644 --- a/homeassistant/generated/usb.py +++ b/homeassistant/generated/usb.py @@ -3,113 +3,111 @@ To update, run python3 -m script.hassfest """ -# fmt: off - USB = [ { "domain": "insteon", - "vid": "10BF" + "vid": "10BF", }, { "domain": "modem_callerid", "vid": "0572", - "pid": "1340" + "pid": "1340", }, { "domain": "velbus", "vid": "10CF", - "pid": "0B1B" + "pid": "0B1B", }, { "domain": "velbus", "vid": "10CF", - "pid": "0516" + "pid": "0516", }, { "domain": "velbus", "vid": "10CF", - "pid": "0517" + "pid": "0517", }, { "domain": "velbus", "vid": "10CF", - "pid": "0518" + "pid": "0518", }, { "domain": "zha", "vid": "10C4", "pid": "EA60", - "description": "*2652*" + "description": "*2652*", }, { "domain": "zha", "vid": "1A86", "pid": "55D4", - "description": "*sonoff*plus*" + "description": "*sonoff*plus*", }, { "domain": "zha", "vid": "10C4", "pid": "EA60", - "description": "*sonoff*plus*" + "description": "*sonoff*plus*", }, { "domain": "zha", "vid": "10C4", "pid": "EA60", - "description": "*tubeszb*" + "description": "*tubeszb*", }, { "domain": "zha", "vid": "1A86", "pid": "7523", - "description": "*tubeszb*" + "description": "*tubeszb*", }, { "domain": "zha", "vid": "1A86", "pid": "7523", - "description": "*zigstar*" + "description": "*zigstar*", }, { "domain": "zha", "vid": "1CF1", "pid": "0030", - "description": "*conbee*" + "description": "*conbee*", }, { "domain": "zha", "vid": "10C4", "pid": "8A2A", - "description": "*zigbee*" + "description": "*zigbee*", }, { "domain": "zha", "vid": "0403", "pid": "6015", - "description": "*zigate*" + "description": "*zigate*", }, { "domain": "zha", "vid": "10C4", "pid": "EA60", - "description": "*zigate*" + "description": "*zigate*", }, { "domain": "zha", "vid": "10C4", "pid": "8B34", - "description": "*bv 2010/10*" + "description": "*bv 2010/10*", }, { "domain": "zwave_js", "vid": "0658", - "pid": "0200" + "pid": "0200", }, { "domain": "zwave_js", "vid": "10C4", "pid": "8A2A", - "description": "*z-wave*" - } + "description": "*z-wave*", + }, ] diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 61d6fdae63c..18ac7112beb 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -3,425 +3,423 @@ To update, run python3 -m script.hassfest """ -# fmt: off - ZEROCONF = { "_Volumio._tcp.local.": [ { - "domain": "volumio" - } + "domain": "volumio", + }, ], "_airplay._tcp.local.": [ { "domain": "apple_tv", "properties": { - "model": "appletv*" - } + "model": "appletv*", + }, }, { "domain": "apple_tv", "properties": { - "model": "audioaccessory*" - } + "model": "audioaccessory*", + }, }, { "domain": "apple_tv", "properties": { - "am": "airport*" - } + "am": "airport*", + }, }, { "domain": "samsungtv", "properties": { - "manufacturer": "samsung*" - } - } + "manufacturer": "samsung*", + }, + }, ], "_airport._tcp.local.": [ { - "domain": "apple_tv" - } + "domain": "apple_tv", + }, ], "_api._tcp.local.": [ { "domain": "baf", "properties": { - "model": "haiku*" - } + "model": "haiku*", + }, }, { "domain": "baf", "properties": { - "model": "i6*" - } - } + "model": "i6*", + }, + }, ], "_api._udp.local.": [ { - "domain": "guardian" - } + "domain": "guardian", + }, ], "_appletv-v2._tcp.local.": [ { - "domain": "apple_tv" - } + "domain": "apple_tv", + }, ], "_axis-video._tcp.local.": [ { "domain": "axis", "properties": { - "macaddress": "00408c*" - } + "macaddress": "00408c*", + }, }, { "domain": "axis", "properties": { - "macaddress": "accc8e*" - } + "macaddress": "accc8e*", + }, }, { "domain": "axis", "properties": { - "macaddress": "b8a44f*" - } + "macaddress": "b8a44f*", + }, }, { "domain": "doorbird", "properties": { - "macaddress": "1ccae3*" - } - } + "macaddress": "1ccae3*", + }, + }, ], "_bond._tcp.local.": [ { - "domain": "bond" - } + "domain": "bond", + }, ], "_companion-link._tcp.local.": [ { - "domain": "apple_tv" - } + "domain": "apple_tv", + }, ], "_daap._tcp.local.": [ { - "domain": "forked_daapd" - } + "domain": "forked_daapd", + }, ], "_dkapi._tcp.local.": [ { - "domain": "daikin" - } + "domain": "daikin", + }, ], "_dvl-deviceapi._tcp.local.": [ { - "domain": "devolo_home_control" + "domain": "devolo_home_control", }, { "domain": "devolo_home_network", "properties": { - "MT": "*" - } - } + "MT": "*", + }, + }, ], "_easylink._tcp.local.": [ { "domain": "modern_forms", - "name": "wac*" - } + "name": "wac*", + }, ], "_elg._tcp.local.": [ { - "domain": "elgato" - } + "domain": "elgato", + }, ], "_enphase-envoy._tcp.local.": [ { - "domain": "enphase_envoy" - } + "domain": "enphase_envoy", + }, ], "_esphomelib._tcp.local.": [ { - "domain": "esphome" + "domain": "esphome", }, { "domain": "zha", - "name": "tube*" - } + "name": "tube*", + }, ], "_fbx-api._tcp.local.": [ { - "domain": "freebox" - } + "domain": "freebox", + }, ], "_googlecast._tcp.local.": [ { - "domain": "cast" - } + "domain": "cast", + }, ], "_hap._tcp.local.": [ { - "domain": "homekit_controller" + "domain": "homekit_controller", }, { "domain": "zwave_me", - "name": "*z.wave-me*" - } + "name": "*z.wave-me*", + }, ], "_hap._udp.local.": [ { - "domain": "homekit_controller" - } + "domain": "homekit_controller", + }, ], "_homekit._tcp.local.": [ { - "domain": "homekit" - } + "domain": "homekit", + }, ], "_hscp._tcp.local.": [ { - "domain": "apple_tv" - } + "domain": "apple_tv", + }, ], "_http._tcp.local.": [ { "domain": "awair", - "name": "awair*" + "name": "awair*", }, { "domain": "bosch_shc", - "name": "bosch shc*" + "name": "bosch shc*", }, { "domain": "nam", - "name": "nam-*" + "name": "nam-*", }, { "domain": "nam", "properties": { - "manufacturer": "nettigo" - } + "manufacturer": "nettigo", + }, }, { "domain": "pure_energie", - "name": "smartbridge*" + "name": "smartbridge*", }, { "domain": "rachio", - "name": "rachio*" + "name": "rachio*", }, { "domain": "rainmachine", - "name": "rainmachine*" + "name": "rainmachine*", }, { "domain": "shelly", - "name": "shelly*" - } + "name": "shelly*", + }, ], "_hue._tcp.local.": [ { - "domain": "hue" - } + "domain": "hue", + }, ], "_hwenergy._tcp.local.": [ { - "domain": "homewizard" - } + "domain": "homewizard", + }, ], "_ipp._tcp.local.": [ { - "domain": "ipp" - } + "domain": "ipp", + }, ], "_ipps._tcp.local.": [ { - "domain": "ipp" - } + "domain": "ipp", + }, ], "_kizbox._tcp.local.": [ { "domain": "overkiz", - "name": "gateway*" - } + "name": "gateway*", + }, ], "_leap._tcp.local.": [ { - "domain": "lutron_caseta" - } + "domain": "lutron_caseta", + }, ], "_lookin._tcp.local.": [ { - "domain": "lookin" - } + "domain": "lookin", + }, ], "_mediaremotetv._tcp.local.": [ { - "domain": "apple_tv" - } + "domain": "apple_tv", + }, ], "_miio._udp.local.": [ { - "domain": "xiaomi_aqara" + "domain": "xiaomi_aqara", }, { - "domain": "xiaomi_miio" + "domain": "xiaomi_miio", }, { "domain": "yeelight", - "name": "yeelink-*" - } + "name": "yeelink-*", + }, ], "_nanoleafapi._tcp.local.": [ { - "domain": "nanoleaf" - } + "domain": "nanoleaf", + }, ], "_nanoleafms._tcp.local.": [ { - "domain": "nanoleaf" - } + "domain": "nanoleaf", + }, ], "_nut._tcp.local.": [ { - "domain": "nut" - } + "domain": "nut", + }, ], "_octoprint._tcp.local.": [ { - "domain": "octoprint" - } + "domain": "octoprint", + }, ], "_plexmediasvr._tcp.local.": [ { - "domain": "plex" - } + "domain": "plex", + }, ], "_plugwise._tcp.local.": [ { - "domain": "plugwise" - } + "domain": "plugwise", + }, ], "_powerview._tcp.local.": [ { - "domain": "hunterdouglas_powerview" - } + "domain": "hunterdouglas_powerview", + }, ], "_printer._tcp.local.": [ { "domain": "brother", - "name": "brother*" - } + "name": "brother*", + }, ], "_raop._tcp.local.": [ { "domain": "apple_tv", "properties": { - "am": "appletv*" - } + "am": "appletv*", + }, }, { "domain": "apple_tv", "properties": { - "am": "audioaccessory*" - } + "am": "audioaccessory*", + }, }, { "domain": "apple_tv", "properties": { - "am": "airport*" - } - } + "am": "airport*", + }, + }, ], "_sideplay._tcp.local.": [ { "domain": "ecobee", "properties": { - "mdl": "eb-*" - } + "mdl": "eb-*", + }, }, { "domain": "ecobee", "properties": { - "mdl": "ecobee*" - } - } + "mdl": "ecobee*", + }, + }, ], "_sleep-proxy._udp.local.": [ { - "domain": "apple_tv" - } + "domain": "apple_tv", + }, ], "_sonos._tcp.local.": [ { - "domain": "sonos" - } + "domain": "sonos", + }, ], "_soundtouch._tcp.local.": [ { - "domain": "soundtouch" - } + "domain": "soundtouch", + }, ], "_spotify-connect._tcp.local.": [ { - "domain": "spotify" - } + "domain": "spotify", + }, ], "_ssh._tcp.local.": [ { "domain": "smappee", - "name": "smappee1*" + "name": "smappee1*", }, { "domain": "smappee", - "name": "smappee2*" + "name": "smappee2*", }, { "domain": "smappee", - "name": "smappee50*" - } + "name": "smappee50*", + }, ], "_system-bridge._tcp.local.": [ { - "domain": "system_bridge" - } + "domain": "system_bridge", + }, ], "_touch-able._tcp.local.": [ { - "domain": "apple_tv" - } + "domain": "apple_tv", + }, ], "_viziocast._tcp.local.": [ { - "domain": "vizio" - } + "domain": "vizio", + }, ], "_wled._tcp.local.": [ { - "domain": "wled" - } + "domain": "wled", + }, ], "_xbmc-jsonrpc-h._tcp.local.": [ { - "domain": "kodi" - } + "domain": "kodi", + }, ], "_zigate-zigbee-gateway._tcp.local.": [ { "domain": "zha", - "name": "*zigate*" - } + "name": "*zigate*", + }, ], "_zigstar_gw._tcp.local.": [ { "domain": "zha", - "name": "*zigstar*" - } + "name": "*zigstar*", + }, ], "_zwave-js-server._tcp.local.": [ { - "domain": "zwave_js" - } - ] + "domain": "zwave_js", + }, + ], } HOMEKIT = { @@ -483,5 +481,5 @@ HOMEKIT = { "ecobee*": "ecobee", "iSmartGate": "gogogate2", "iZone": "izone", - "tado": "tado" + "tado": "tado", } diff --git a/script/hassfest/application_credentials.py b/script/hassfest/application_credentials.py index 48d812dba02..2fb693bf429 100644 --- a/script/hassfest/application_credentials.py +++ b/script/hassfest/application_credentials.py @@ -1,9 +1,10 @@ """Generate application_credentials data.""" from __future__ import annotations -import json +import black from .model import Config, Integration +from .serializer import to_string BASE = """ \"\"\"Automatically generated by hassfest. @@ -11,8 +12,6 @@ BASE = """ To update, run python3 -m script.hassfest \"\"\" -# fmt: off - APPLICATION_CREDENTIALS = {} """.strip() @@ -30,7 +29,7 @@ def generate_and_validate(integrations: dict[str, Integration], config: Config) match_list.append(domain) - return BASE.format(json.dumps(match_list, indent=4)) + return black.format_str(BASE.format(to_string(match_list)), mode=black.Mode()) def validate(integrations: dict[str, Integration], config: Config) -> None: @@ -45,7 +44,7 @@ def validate(integrations: dict[str, Integration], config: Config) -> None: if config.specific_integrations: return - if application_credentials_path.read_text(encoding="utf-8").strip() != content: + if application_credentials_path.read_text(encoding="utf-8") != content: config.add_error( "application_credentials", "File application_credentials.py is not up to date. Run python3 -m script.hassfest", @@ -59,5 +58,5 @@ def generate(integrations: dict[str, Integration], config: Config): config.root / "homeassistant/generated/application_credentials.py" ) application_credentials_path.write_text( - f"{config.cache['application_credentials']}\n", encoding="utf-8" + f"{config.cache['application_credentials']}", encoding="utf-8" ) diff --git a/script/hassfest/bluetooth.py b/script/hassfest/bluetooth.py index 22241653e1d..0b57b1084e8 100644 --- a/script/hassfest/bluetooth.py +++ b/script/hassfest/bluetooth.py @@ -1,9 +1,10 @@ """Generate bluetooth file.""" from __future__ import annotations -import json +import black from .model import Config, Integration +from .serializer import to_string BASE = """ \"\"\"Automatically generated by hassfest. @@ -12,8 +13,6 @@ To update, run python3 -m script.hassfest \"\"\" from __future__ import annotations -# fmt: off - BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = {} """.strip() @@ -36,11 +35,7 @@ def generate_and_validate(integrations: list[dict[str, str]]): for entry in match_types: match_list.append({"domain": domain, **entry}) - return BASE.format( - json.dumps(match_list, indent=4) - .replace('": true', '": True') - .replace('": false', '": False') - ) + return black.format_str(BASE.format(to_string(match_list)), mode=black.Mode()) def validate(integrations: dict[str, Integration], config: Config): @@ -52,7 +47,7 @@ def validate(integrations: dict[str, Integration], config: Config): return with open(str(bluetooth_path)) as fp: - current = fp.read().strip() + current = fp.read() if current != content: config.add_error( "bluetooth", @@ -66,4 +61,4 @@ def generate(integrations: dict[str, Integration], config: Config): """Generate bluetooth file.""" bluetooth_path = config.root / "homeassistant/generated/bluetooth.py" with open(str(bluetooth_path), "w") as fp: - fp.write(f"{config.cache['bluetooth']}\n") + fp.write(f"{config.cache['bluetooth']}") diff --git a/script/hassfest/config_flow.py b/script/hassfest/config_flow.py index ad4a1d79229..c3c5610a48f 100644 --- a/script/hassfest/config_flow.py +++ b/script/hassfest/config_flow.py @@ -1,9 +1,10 @@ """Generate config flow file.""" from __future__ import annotations -import json +import black from .model import Config, Integration +from .serializer import to_string BASE = """ \"\"\"Automatically generated by hassfest. @@ -11,8 +12,6 @@ BASE = """ To update, run python3 -m script.hassfest \"\"\" -# fmt: off - FLOWS = {} """.strip() @@ -85,7 +84,7 @@ def generate_and_validate(integrations: dict[str, Integration], config: Config): domains[integration.integration_type].append(domain) - return BASE.format(json.dumps(domains, indent=4)) + return black.format_str(BASE.format(to_string(domains)), mode=black.Mode()) def validate(integrations: dict[str, Integration], config: Config): @@ -97,7 +96,7 @@ def validate(integrations: dict[str, Integration], config: Config): return with open(str(config_flow_path)) as fp: - if fp.read().strip() != content: + if fp.read() != content: config.add_error( "config_flow", "File config_flows.py is not up to date. " @@ -111,4 +110,4 @@ def generate(integrations: dict[str, Integration], config: Config): """Generate config flow file.""" config_flow_path = config.root / "homeassistant/generated/config_flows.py" with open(str(config_flow_path), "w") as fp: - fp.write(f"{config.cache['config_flow']}\n") + fp.write(f"{config.cache['config_flow']}") diff --git a/script/hassfest/dhcp.py b/script/hassfest/dhcp.py index 1aca6a1f68d..c246acec5f0 100644 --- a/script/hassfest/dhcp.py +++ b/script/hassfest/dhcp.py @@ -1,8 +1,7 @@ """Generate dhcp file.""" from __future__ import annotations -import pprint -import re +import black from .model import Config, Integration @@ -13,8 +12,6 @@ To update, run python3 -m script.hassfest \"\"\" from __future__ import annotations -# fmt: off - DHCP: list[dict[str, str | bool]] = {} """.strip() @@ -37,14 +34,7 @@ def generate_and_validate(integrations: list[dict[str, str]]): for entry in match_types: match_list.append({"domain": domain, **entry}) - # JSON will format `True` as `true` - # re.sub for flake8 E128 - formatted = pprint.pformat(match_list) - formatted_aligned_continuation = re.sub(r"^\[\{", "[\n {", formatted) - formatted_align_indent = re.sub( - r"(?m)^ ", " ", formatted_aligned_continuation, flags=re.MULTILINE, count=0 - ) - return BASE.format(formatted_align_indent) + return black.format_str(BASE.format(str(match_list)), mode=black.Mode()) def validate(integrations: dict[str, Integration], config: Config): @@ -56,7 +46,7 @@ def validate(integrations: dict[str, Integration], config: Config): return with open(str(dhcp_path)) as fp: - current = fp.read().strip() + current = fp.read() if current != content: config.add_error( "dhcp", @@ -70,4 +60,4 @@ def generate(integrations: dict[str, Integration], config: Config): """Generate dhcp file.""" dhcp_path = config.root / "homeassistant/generated/dhcp.py" with open(str(dhcp_path), "w") as fp: - fp.write(f"{config.cache['dhcp']}\n") + fp.write(f"{config.cache['dhcp']}") diff --git a/script/hassfest/mqtt.py b/script/hassfest/mqtt.py index f325518d7b9..ab5f159026e 100644 --- a/script/hassfest/mqtt.py +++ b/script/hassfest/mqtt.py @@ -2,9 +2,11 @@ from __future__ import annotations from collections import defaultdict -import json + +import black from .model import Config, Integration +from .serializer import to_string BASE = """ \"\"\"Automatically generated by hassfest. @@ -12,8 +14,6 @@ BASE = """ To update, run python3 -m script.hassfest \"\"\" -# fmt: off - MQTT = {} """.strip() @@ -37,7 +37,7 @@ def generate_and_validate(integrations: dict[str, Integration]): for topic in mqtt: data[domain].append(topic) - return BASE.format(json.dumps(data, indent=4)) + return black.format_str(BASE.format(to_string(data)), mode=black.Mode()) def validate(integrations: dict[str, Integration], config: Config): @@ -49,7 +49,7 @@ def validate(integrations: dict[str, Integration], config: Config): return with open(str(mqtt_path)) as fp: - if fp.read().strip() != content: + if fp.read() != content: config.add_error( "mqtt", "File mqtt.py is not up to date. Run python3 -m script.hassfest", @@ -62,4 +62,4 @@ def generate(integrations: dict[str, Integration], config: Config): """Generate MQTT file.""" mqtt_path = config.root / "homeassistant/generated/mqtt.py" with open(str(mqtt_path), "w") as fp: - fp.write(f"{config.cache['mqtt']}\n") + fp.write(f"{config.cache['mqtt']}") diff --git a/script/hassfest/serializer.py b/script/hassfest/serializer.py new file mode 100644 index 00000000000..8a6f410c345 --- /dev/null +++ b/script/hassfest/serializer.py @@ -0,0 +1,37 @@ +"""Hassfest utils.""" +from __future__ import annotations + +from typing import Any + + +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 + + +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 to_string(data: Any) -> str: + """Return a string representation of the input.""" + if isinstance(data, dict): + return _dict_to_str(data) + if isinstance(data, list): + return _list_to_str(data) + if isinstance(data, str): + return "'" + data + "'" + return data diff --git a/script/hassfest/ssdp.py b/script/hassfest/ssdp.py index 0611f9a2225..599746e9874 100644 --- a/script/hassfest/ssdp.py +++ b/script/hassfest/ssdp.py @@ -1,10 +1,12 @@ """Generate ssdp file.""" from __future__ import annotations -from collections import OrderedDict, defaultdict -import json +from collections import defaultdict + +import black from .model import Config, Integration +from .serializer import to_string BASE = """ \"\"\"Automatically generated by hassfest. @@ -12,15 +14,13 @@ BASE = """ To update, run python3 -m script.hassfest \"\"\" -# fmt: off - SSDP = {} """.strip() def sort_dict(value): """Sort a dictionary.""" - return OrderedDict((key, value[key]) for key in sorted(value)) + return {key: value[key] for key in sorted(value)} def generate_and_validate(integrations: dict[str, Integration]): @@ -42,7 +42,7 @@ def generate_and_validate(integrations: dict[str, Integration]): for matcher in ssdp: data[domain].append(sort_dict(matcher)) - return BASE.format(json.dumps(data, indent=4)) + return black.format_str(BASE.format(to_string(data)), mode=black.Mode()) def validate(integrations: dict[str, Integration], config: Config): @@ -54,7 +54,7 @@ def validate(integrations: dict[str, Integration], config: Config): return with open(str(ssdp_path)) as fp: - if fp.read().strip() != content: + if fp.read() != content: config.add_error( "ssdp", "File ssdp.py is not up to date. Run python3 -m script.hassfest", @@ -67,4 +67,4 @@ def generate(integrations: dict[str, Integration], config: Config): """Generate ssdp file.""" ssdp_path = config.root / "homeassistant/generated/ssdp.py" with open(str(ssdp_path), "w") as fp: - fp.write(f"{config.cache['ssdp']}\n") + fp.write(f"{config.cache['ssdp']}") diff --git a/script/hassfest/supported_brands.py b/script/hassfest/supported_brands.py index 6740260a04c..4ac2feb4032 100644 --- a/script/hassfest/supported_brands.py +++ b/script/hassfest/supported_brands.py @@ -1,9 +1,10 @@ """Generate supported_brands data.""" from __future__ import annotations -import json +import black from .model import Config, Integration +from .serializer import to_string BASE = """ \"\"\"Automatically generated by hassfest. @@ -11,9 +12,7 @@ BASE = """ To update, run python3 -m script.hassfest \"\"\" -# fmt: off - -HAS_SUPPORTED_BRANDS = ({}) +HAS_SUPPORTED_BRANDS = {} """.strip() @@ -26,7 +25,7 @@ def generate_and_validate(integrations: dict[str, Integration], config: Config) if integration.supported_brands ] - return BASE.format(json.dumps(brands, indent=4)[1:-1]) + return black.format_str(BASE.format(to_string(brands)), mode=black.Mode()) def validate(integrations: dict[str, Integration], config: Config) -> None: @@ -39,7 +38,7 @@ def validate(integrations: dict[str, Integration], config: Config) -> None: if config.specific_integrations: return - if supported_brands_path.read_text(encoding="utf-8").strip() != content: + if supported_brands_path.read_text(encoding="utf-8") != content: config.add_error( "supported_brands", "File supported_brands.py is not up to date. Run python3 -m script.hassfest", @@ -51,5 +50,5 @@ def generate(integrations: dict[str, Integration], config: Config): """Generate supported_brands data.""" supported_brands_path = config.root / "homeassistant/generated/supported_brands.py" supported_brands_path.write_text( - f"{config.cache['supported_brands']}\n", encoding="utf-8" + f"{config.cache['supported_brands']}", encoding="utf-8" ) diff --git a/script/hassfest/usb.py b/script/hassfest/usb.py index 6377fdcb8af..e71966d548a 100644 --- a/script/hassfest/usb.py +++ b/script/hassfest/usb.py @@ -1,9 +1,10 @@ """Generate usb file.""" from __future__ import annotations -import json +import black from .model import Config, Integration +from .serializer import to_string BASE = """ \"\"\"Automatically generated by hassfest. @@ -11,8 +12,6 @@ BASE = """ To update, run python3 -m script.hassfest \"\"\" -# fmt: off - USB = {} """.strip() @@ -40,7 +39,7 @@ def generate_and_validate(integrations: list[dict[str, str]]) -> str: } ) - return BASE.format(json.dumps(match_list, indent=4)) + return black.format_str(BASE.format(to_string(match_list)), mode=black.Mode()) def validate(integrations: dict[str, Integration], config: Config) -> None: @@ -52,7 +51,7 @@ def validate(integrations: dict[str, Integration], config: Config) -> None: return with open(str(usb_path)) as fp: - current = fp.read().strip() + current = fp.read() if current != content: config.add_error( "usb", @@ -66,4 +65,4 @@ def generate(integrations: dict[str, Integration], config: Config) -> None: """Generate usb file.""" usb_path = config.root / "homeassistant/generated/usb.py" with open(str(usb_path), "w") as fp: - fp.write(f"{config.cache['usb']}\n") + fp.write(f"{config.cache['usb']}") diff --git a/script/hassfest/zeroconf.py b/script/hassfest/zeroconf.py index 446a6f32aeb..939da08319a 100644 --- a/script/hassfest/zeroconf.py +++ b/script/hassfest/zeroconf.py @@ -1,12 +1,14 @@ """Generate zeroconf file.""" from __future__ import annotations -from collections import OrderedDict, defaultdict -import json +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. @@ -14,8 +16,6 @@ BASE = """ To update, run python3 -m script.hassfest \"\"\" -# fmt: off - ZEROCONF = {} HOMEKIT = {} @@ -82,12 +82,12 @@ def generate_and_validate(integrations: dict[str, Integration]): warned.add(key_2) break - zeroconf = OrderedDict( - (key, service_type_dict[key]) for key in sorted(service_type_dict) - ) - homekit = OrderedDict((key, homekit_dict[key]) for key in sorted(homekit_dict)) + 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 BASE.format(json.dumps(zeroconf, indent=4), json.dumps(homekit, indent=4)) + return black.format_str( + BASE.format(to_string(zeroconf), to_string(homekit)), mode=black.Mode() + ) def validate(integrations: dict[str, Integration], config: Config): @@ -99,7 +99,7 @@ def validate(integrations: dict[str, Integration], config: Config): return with open(str(zeroconf_path)) as fp: - current = fp.read().strip() + current = fp.read() if current != content: config.add_error( "zeroconf", @@ -113,4 +113,4 @@ def generate(integrations: dict[str, Integration], config: Config): """Generate zeroconf file.""" zeroconf_path = config.root / "homeassistant/generated/zeroconf.py" with open(str(zeroconf_path), "w") as fp: - fp.write(f"{config.cache['zeroconf']}\n") + fp.write(f"{config.cache['zeroconf']}") From 4b2bea4972a23c122e25317edece7af44da37531 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 20 Sep 2022 00:30:13 +0000 Subject: [PATCH 573/955] [ci skip] Translation update --- .../components/awair/translations/bg.json | 19 ++++++++--- .../components/bluetooth/translations/bg.json | 4 ++- .../components/demo/translations/bg.json | 1 + .../deutsche_bahn/translations/bg.json | 7 ++++ .../components/ecowitt/translations/bg.json | 1 + .../components/guardian/translations/bg.json | 23 +++++++++++++ .../components/guardian/translations/ca.json | 17 ++++++++-- .../components/guardian/translations/de.json | 2 +- .../components/guardian/translations/el.json | 11 +++++++ .../components/guardian/translations/es.json | 13 +++++++- .../components/guardian/translations/fr.json | 2 +- .../components/guardian/translations/hu.json | 2 +- .../guardian/translations/pt-BR.json | 6 ++-- .../guardian/translations/zh-Hant.json | 6 ++-- .../components/hue/translations/bg.json | 5 +++ .../components/icloud/translations/bg.json | 1 + .../justnimbus/translations/bg.json | 7 ++++ .../components/lametric/translations/bg.json | 8 ++++- .../components/lametric/translations/es.json | 3 +- .../litterrobot/translations/sensor.ca.json | 3 ++ .../litterrobot/translations/sensor.el.json | 3 ++ .../litterrobot/translations/sensor.es.json | 3 ++ .../components/miflora/translations/bg.json | 7 ++++ .../components/nobo_hub/translations/bg.json | 10 +++++- .../components/openuv/translations/bg.json | 8 +++++ .../components/openuv/translations/ca.json | 10 ++++++ .../components/openuv/translations/el.json | 10 ++++++ .../components/openuv/translations/es.json | 10 ++++++ .../components/overkiz/translations/bg.json | 3 +- .../components/risco/translations/bg.json | 4 +++ .../components/schedule/translations/bg.json | 3 ++ .../components/shelly/translations/pl.json | 30 ++++++++--------- .../simplisafe/translations/bg.json | 5 +++ .../simplisafe/translations/el.json | 6 ++++ .../simplisafe/translations/es.json | 6 ++++ .../speedtestdotnet/translations/bg.json | 12 +++++++ .../components/tasmota/translations/ca.json | 10 ++++++ .../components/tasmota/translations/de.json | 10 ++++++ .../components/tasmota/translations/el.json | 10 ++++++ .../components/tasmota/translations/es.json | 10 ++++++ .../components/tasmota/translations/fr.json | 8 +++++ .../tasmota/translations/pt-BR.json | 10 ++++++ .../components/tasmota/translations/ru.json | 10 ++++++ .../tasmota/translations/zh-Hant.json | 10 ++++++ .../unifiprotect/translations/bg.json | 12 +++++++ .../volvooncall/translations/bg.json | 1 + .../volvooncall/translations/ca.json | 1 + .../volvooncall/translations/de.json | 1 + .../volvooncall/translations/el.json | 1 + .../volvooncall/translations/en.json | 33 ++++++++++--------- .../volvooncall/translations/es.json | 1 + .../volvooncall/translations/fr.json | 1 + .../volvooncall/translations/hu.json | 1 + .../volvooncall/translations/pt-BR.json | 1 + .../volvooncall/translations/zh-Hant.json | 1 + .../xiaomi_miio/translations/select.bg.json | 10 ++++++ .../yalexs_ble/translations/bg.json | 6 +++- .../components/zha/translations/bg.json | 2 ++ .../components/zwave_js/translations/bg.json | 6 ++++ .../components/zwave_js/translations/ca.json | 6 ++++ .../components/zwave_js/translations/de.json | 6 ++++ .../components/zwave_js/translations/el.json | 6 ++++ .../components/zwave_js/translations/es.json | 6 ++++ .../components/zwave_js/translations/fr.json | 5 +++ .../components/zwave_js/translations/hu.json | 6 ++++ .../zwave_js/translations/pt-BR.json | 6 ++++ .../components/zwave_js/translations/ru.json | 6 ++++ .../zwave_js/translations/zh-Hant.json | 6 ++++ 68 files changed, 426 insertions(+), 54 deletions(-) create mode 100644 homeassistant/components/deutsche_bahn/translations/bg.json create mode 100644 homeassistant/components/miflora/translations/bg.json create mode 100644 homeassistant/components/schedule/translations/bg.json diff --git a/homeassistant/components/awair/translations/bg.json b/homeassistant/components/awair/translations/bg.json index 2c2bf0cdfd0..0f9884bf058 100644 --- a/homeassistant/components/awair/translations/bg.json +++ b/homeassistant/components/awair/translations/bg.json @@ -4,6 +4,7 @@ "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", "already_configured_account": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", "already_configured_device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u043e", + "no_devices_found": "\u041d\u044f\u043c\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\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", "unreachable": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, @@ -15,7 +16,7 @@ "step": { "cloud": { "data": { - "email": "Email" + "email": "\u0418\u043c\u0435\u0439\u043b" } }, "discovery_confirm": { @@ -24,7 +25,8 @@ "local": { "data": { "host": "IP \u0430\u0434\u0440\u0435\u0441" - } + }, + "description": "\u0421\u043b\u0435\u0434\u0432\u0430\u0439\u0442\u0435 [\u0442\u0435\u0437\u0438 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u0438]({url}) \u0437\u0430 \u0442\u043e\u0432\u0430 \u043a\u0430\u043a \u0434\u0430 \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u0442\u0435 \u043b\u043e\u043a\u0430\u043b\u043d\u0438\u044f API \u043d\u0430 Awair.\n\n\u0429\u0440\u0430\u043a\u043d\u0435\u0442\u0435 \u0432\u044a\u0440\u0445\u0443 \"\u0418\u0417\u041f\u0420\u0410\u0429\u0410\u041d\u0415\", \u043a\u043e\u0433\u0430\u0442\u043e \u0441\u0442\u0435 \u0433\u043e\u0442\u043e\u0432\u0438." }, "local_pick": { "data": { @@ -34,12 +36,21 @@ }, "reauth": { "data": { - "email": "Email" + "email": "\u0418\u043c\u0435\u0439\u043b" + } + }, + "reauth_confirm": { + "data": { + "email": "\u0418\u043c\u0435\u0439\u043b" } }, "user": { "data": { - "email": "Email" + "email": "\u0418\u043c\u0435\u0439\u043b" + }, + "menu_options": { + "cloud": "\u0421\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0447\u0440\u0435\u0437 \u043e\u0431\u043b\u0430\u043a\u0430", + "local": "\u0421\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u043b\u043e\u043a\u0430\u043b\u043d\u043e (\u0437\u0430 \u043f\u0440\u0435\u0434\u043f\u043e\u0447\u0438\u0442\u0430\u043d\u0435)" } } } diff --git a/homeassistant/components/bluetooth/translations/bg.json b/homeassistant/components/bluetooth/translations/bg.json index a34ca0f83f1..1d6b9891e4a 100644 --- a/homeassistant/components/bluetooth/translations/bg.json +++ b/homeassistant/components/bluetooth/translations/bg.json @@ -1,7 +1,8 @@ { "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" + "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", + "no_adapters": "\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 Bluetooth \u0430\u0434\u0430\u043f\u0442\u0435\u0440\u0438" }, "flow_title": "{name}", "step": { @@ -32,6 +33,7 @@ "step": { "init": { "data": { + "adapter": "Bluetooth \u0430\u0434\u0430\u043f\u0442\u0435\u0440, \u043a\u043e\u0439\u0442\u043e \u0434\u0430 \u0441\u0435 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430 \u0437\u0430 \u0441\u043a\u0430\u043d\u0438\u0440\u0430\u043d\u0435", "passive": "\u041f\u0430\u0441\u0438\u0432\u043d\u043e \u0441\u043a\u0430\u043d\u0438\u0440\u0430\u043d\u0435" } } diff --git a/homeassistant/components/demo/translations/bg.json b/homeassistant/components/demo/translations/bg.json index 3256ee582f9..2ecf8f371eb 100644 --- a/homeassistant/components/demo/translations/bg.json +++ b/homeassistant/components/demo/translations/bg.json @@ -4,6 +4,7 @@ "fix_flow": { "step": { "confirm": { + "description": "\u041d\u0430\u0442\u0438\u0441\u043d\u0435\u0442\u0435 \u0418\u0417\u041f\u0420\u0410\u0429\u0410\u041d\u0415, \u0437\u0430 \u0434\u0430 \u043f\u043e\u0442\u0432\u044a\u0440\u0434\u0438\u0442\u0435, \u0447\u0435 \u0437\u0430\u0445\u0440\u0430\u043d\u0432\u0430\u0449\u0438\u044f\u0442 \u0431\u043b\u043e\u043a \u0435 \u0441\u043c\u0435\u043d\u0435\u043d", "title": "\u0417\u0430\u0445\u0440\u0430\u043d\u0432\u0430\u043d\u0435\u0442\u043e \u0442\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u0431\u044a\u0434\u0435 \u0441\u043c\u0435\u043d\u0435\u043d\u043e" } } diff --git a/homeassistant/components/deutsche_bahn/translations/bg.json b/homeassistant/components/deutsche_bahn/translations/bg.json new file mode 100644 index 00000000000..84d8d17f23a --- /dev/null +++ b/homeassistant/components/deutsche_bahn/translations/bg.json @@ -0,0 +1,7 @@ +{ + "issues": { + "pending_removal": { + "title": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 Deutsche Bahn \u0441\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u0432\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecowitt/translations/bg.json b/homeassistant/components/ecowitt/translations/bg.json index eb32d7e9e6e..92e4c1888a4 100644 --- a/homeassistant/components/ecowitt/translations/bg.json +++ b/homeassistant/components/ecowitt/translations/bg.json @@ -1,6 +1,7 @@ { "config": { "error": { + "invalid_port": "\u041f\u043e\u0440\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0441\u0435 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430.", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "step": { diff --git a/homeassistant/components/guardian/translations/bg.json b/homeassistant/components/guardian/translations/bg.json index de9699e4a21..d48caec927f 100644 --- a/homeassistant/components/guardian/translations/bg.json +++ b/homeassistant/components/guardian/translations/bg.json @@ -12,5 +12,28 @@ } } } + }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "title": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 {deprecated_service} \u0449\u0435 \u0431\u044a\u0434\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0430\u0442\u0430" + } + } + }, + "title": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 {deprecated_service} \u0449\u0435 \u0431\u044a\u0434\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0430\u0442\u0430" + }, + "replaced_old_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u0410\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u0439\u0442\u0435 \u0432\u0441\u0438\u0447\u043a\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0438 \u0438\u043b\u0438 \u0441\u043a\u0440\u0438\u043f\u0442\u043e\u0432\u0435, \u043a\u043e\u0438\u0442\u043e \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0442 \u0442\u043e\u0437\u0438 \u043e\u0431\u0435\u043a\u0442, \u0442\u0430\u043a\u0430 \u0447\u0435 \u0432\u043c\u0435\u0441\u0442\u043e \u043d\u0435\u0433\u043e \u0434\u0430 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0442 `{replacement_entity_id}`.", + "title": "\u041e\u0431\u0435\u043a\u0442\u044a\u0442 {old_entity_id} \u0449\u0435 \u0431\u044a\u0434\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0430\u0442" + } + } + }, + "title": "\u041e\u0431\u0435\u043a\u0442\u044a\u0442 {old_entity_id} \u0449\u0435 \u0431\u044a\u0434\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0430\u0442" + } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/ca.json b/homeassistant/components/guardian/translations/ca.json index cfe8675b70c..dee6614924d 100644 --- a/homeassistant/components/guardian/translations/ca.json +++ b/homeassistant/components/guardian/translations/ca.json @@ -23,12 +23,23 @@ "fix_flow": { "step": { "confirm": { - "description": "Actualitza totes les automatitzacions o 'scripts' que utilitzin aquest servei perqu\u00e8 passin a utilitzar el servei `{alternate_service}` amb un ID d'entitat objectiu o 'target' `{alternate_target}`. Despr\u00e9s, fes clic a ENVIA per marcar aquest problema com a resolt.", - "title": "El servei {deprecated_service} est\u00e0 sent eliminat" + "description": "Actualitza totes les automatitzacions o 'scripts' que utilitzin aquest servei perqu\u00e8 passin a utilitzar el servei `{alternate_service}` amb un ID d'entitat objectiu o 'target' `{alternate_target}`.", + "title": "El servei {deprecated_service} ser\u00e0 eliminat" } } }, - "title": "El servei {deprecated_service} est\u00e0 sent eliminat" + "title": "El servei {deprecated_service} ser\u00e0 eliminat" + }, + "replaced_old_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "Actualitza totes les automatitzacions o 'scripts' que utilitzin aquesta entitat perqu\u00e8 passin a utilitzar l'entitat `{replacement_entity_id}`.", + "title": "L'entitat {old_entity_id} s'eliminar\u00e0" + } + } + }, + "title": "L'entitat {old_entity_id} s'eliminar\u00e0" } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/de.json b/homeassistant/components/guardian/translations/de.json index 76c18651998..9d5c0c0265c 100644 --- a/homeassistant/components/guardian/translations/de.json +++ b/homeassistant/components/guardian/translations/de.json @@ -34,7 +34,7 @@ "fix_flow": { "step": { "confirm": { - "description": "Diese Entit\u00e4t wurde durch `{replacement_entity_id}` ersetzt.", + "description": "Aktualisiere alle Automatisierungen oder Skripte, die diese Entit\u00e4t verwenden, um stattdessen `{replacement_entity_id}` zu verwenden.", "title": "Die Entit\u00e4t {old_entity_id} wird entfernt" } } diff --git a/homeassistant/components/guardian/translations/el.json b/homeassistant/components/guardian/translations/el.json index 2a4963c8649..0857e0284a4 100644 --- a/homeassistant/components/guardian/translations/el.json +++ b/homeassistant/components/guardian/translations/el.json @@ -29,6 +29,17 @@ } }, "title": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 {deprecated_service} \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + }, + "replaced_old_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u0395\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03c3\u03c4\u03b5 \u03cc\u03bb\u03bf\u03c5\u03c2 \u03c4\u03bf\u03c5\u03c2 \u03b1\u03c5\u03c4\u03bf\u03bc\u03b1\u03c4\u03b9\u03c3\u03bc\u03bf\u03cd\u03c2 \u03ae \u03c4\u03b1 \u03c3\u03b5\u03bd\u03ac\u03c1\u03b9\u03b1 \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03bf\u03cd\u03bd \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7\u03bd \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 \u03ce\u03c3\u03c4\u03b5 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03bf\u03cd\u03bd \u03b1\u03bd\u03c4\u03af \u03b3\u03b9\u03b1 \u03b1\u03c5\u03c4\u03ae\u03bd \u03c4\u03bf `{replacement_entity_id}`.", + "title": "\u0397 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 {old_entity_id} \u03b8\u03b1 \u03b1\u03c6\u03b1\u03b9\u03c1\u03b5\u03b8\u03b5\u03af" + } + } + }, + "title": "\u0397 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 {old_entity_id} \u03b8\u03b1 \u03b1\u03c6\u03b1\u03b9\u03c1\u03b5\u03b8\u03b5\u03af" } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/es.json b/homeassistant/components/guardian/translations/es.json index 93bccfbd03d..16233859836 100644 --- a/homeassistant/components/guardian/translations/es.json +++ b/homeassistant/components/guardian/translations/es.json @@ -23,12 +23,23 @@ "fix_flow": { "step": { "confirm": { - "description": "Actualiza cualquier automatizaci\u00f3n o script que use este servicio para usar en su lugar el servicio `{alternate_service}` con un ID de entidad de destino de `{alternate_target}`. Luego, haz clic en ENVIAR a continuaci\u00f3n para marcar este problema como resuelto.", + "description": "Actualiza cualquier automatizaci\u00f3n o script que use este servicio para usar en su lugar el servicio `{alternate_service}` con una ID de entidad de destino de `{alternate_target}`.", "title": "Se va a eliminar el servicio {deprecated_service}" } } }, "title": "Se va a eliminar el servicio {deprecated_service}" + }, + "replaced_old_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "Actualiza cualquier automatizaci\u00f3n o script que use esta entidad para usar `{replacement_entity_id}`.", + "title": "Se eliminar\u00e1 la entidad {old_entity_id}" + } + } + }, + "title": "Se eliminar\u00e1 la entidad {old_entity_id}" } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/fr.json b/homeassistant/components/guardian/translations/fr.json index 1cb1436cae2..253d28ec770 100644 --- a/homeassistant/components/guardian/translations/fr.json +++ b/homeassistant/components/guardian/translations/fr.json @@ -34,7 +34,7 @@ "fix_flow": { "step": { "confirm": { - "description": "Cette entit\u00e9 a \u00e9t\u00e9 remplac\u00e9e par `{replacement_entity_id}`.", + "description": "Modifiez tout script ou automatisation utilisant cette entit\u00e9 afin qu'ils utilisent `{replacement_entity_id}` \u00e0 la place.", "title": "L'entit\u00e9 {old_entity_id} sera supprim\u00e9e" } } diff --git a/homeassistant/components/guardian/translations/hu.json b/homeassistant/components/guardian/translations/hu.json index 38873657dda..20b5d00e2f1 100644 --- a/homeassistant/components/guardian/translations/hu.json +++ b/homeassistant/components/guardian/translations/hu.json @@ -34,7 +34,7 @@ "fix_flow": { "step": { "confirm": { - "description": "Ezt az entit\u00e1st a `{replacement_entity_id}} v\u00e1ltotta fel.", + "description": "Friss\u00edtse az ezt az entit\u00e1st haszn\u00e1l\u00f3 automatiz\u00e1l\u00e1sokat vagy szkripteket, hogy helyette a k\u00f6vetkez\u0151t haszn\u00e1ja: `{replacement_entity_id}`", "title": "{old_entity_id} entit\u00e1s el lesz t\u00e1vol\u00edtva" } } diff --git a/homeassistant/components/guardian/translations/pt-BR.json b/homeassistant/components/guardian/translations/pt-BR.json index 1166d53009f..53e3c724b7c 100644 --- a/homeassistant/components/guardian/translations/pt-BR.json +++ b/homeassistant/components/guardian/translations/pt-BR.json @@ -24,17 +24,17 @@ "step": { "confirm": { "description": "Atualize quaisquer automa\u00e7\u00f5es ou scripts que usam este servi\u00e7o para usar o servi\u00e7o `{alternate_service}` com um ID de entidade de destino de `{alternate_target}`.", - "title": "O servi\u00e7o {deprecated_service} est\u00e1 sendo removido" + "title": "O servi\u00e7o {deprecated_service} ser\u00e1 removido" } } }, - "title": "O servi\u00e7o {deprecated_service} est\u00e1 sendo removido" + "title": "O servi\u00e7o {deprecated_service} ser\u00e1 removido" }, "replaced_old_entity": { "fix_flow": { "step": { "confirm": { - "description": "Esta entidade foi substitu\u00edda por `{replacement_entity_id}`.", + "description": "Atualize quaisquer automa\u00e7\u00f5es ou scripts que usam essa entidade para usar `{replacement_entity_id}`.", "title": "A entidade {old_entity_id} ser\u00e1 removida" } } diff --git a/homeassistant/components/guardian/translations/zh-Hant.json b/homeassistant/components/guardian/translations/zh-Hant.json index 9870758f067..bd30a848b35 100644 --- a/homeassistant/components/guardian/translations/zh-Hant.json +++ b/homeassistant/components/guardian/translations/zh-Hant.json @@ -24,17 +24,17 @@ "step": { "confirm": { "description": "\u4f7f\u7528\u6b64\u670d\u52d9\u4ee5\u66f4\u65b0\u4efb\u4f55\u81ea\u52d5\u5316\u6216\u8173\u672c\u3001\u4ee5\u53d6\u4ee3\u4f7f\u7528\u76ee\u6a19\u5be6\u9ad4 ID \u70ba `{alternate_target}` \u4e4b `{alternate_service}` \u670d\u52d9\u3002", - "title": "{deprecated_service} \u670d\u52d9\u5373\u5c07\u79fb\u9664" + "title": "{deprecated_service} \u670d\u52d9\u5c07\u79fb\u9664" } } }, - "title": "{deprecated_service} \u670d\u52d9\u5373\u5c07\u79fb\u9664" + "title": "{deprecated_service} \u670d\u52d9\u5c07\u79fb\u9664" }, "replaced_old_entity": { "fix_flow": { "step": { "confirm": { - "description": "\u5be6\u9ad4\u5c07\u7531 `{replacement_entity_id}` \u6240\u53d6\u4ee3\u3002", + "description": "\u4f7f\u7528\u6b64\u5be6\u9ad4\u4ee5\u66f4\u65b0\u4efb\u4f55\u81ea\u52d5\u5316\u6216\u8173\u672c\uff0c\u4ee5\u53d6\u4ee3 `{replacement_entity_id}`\u3002", "title": "{old_entity_id} \u5be6\u9ad4\u5c07\u9032\u884c\u79fb\u9664" } } diff --git a/homeassistant/components/hue/translations/bg.json b/homeassistant/components/hue/translations/bg.json index 93617d8c0e5..242c902fde5 100644 --- a/homeassistant/components/hue/translations/bg.json +++ b/homeassistant/components/hue/translations/bg.json @@ -40,8 +40,13 @@ "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_3": "\u0422\u0440\u0435\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": { + "start": "\"{subtype}\" \u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u043f\u044a\u0440\u0432\u043e\u043d\u0430\u0447\u0430\u043b\u043d\u043e" } }, "options": { diff --git a/homeassistant/components/icloud/translations/bg.json b/homeassistant/components/icloud/translations/bg.json index b3c75ad207e..3c54ef831d1 100644 --- a/homeassistant/components/icloud/translations/bg.json +++ b/homeassistant/components/icloud/translations/bg.json @@ -18,6 +18,7 @@ "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u0430" }, + "description": "\u041f\u043e-\u0440\u0430\u043d\u043e \u0432\u044a\u0432\u0435\u0434\u0435\u043d\u0430\u0442\u0430 \u043f\u0430\u0440\u043e\u043b\u0430 \u0437\u0430 {username} \u0432\u0435\u0447\u0435 \u043d\u0435 \u0440\u0430\u0431\u043e\u0442\u0438. \u0410\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u0439\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u0430\u0442\u0430 \u0441\u0438, \u0437\u0430 \u0434\u0430 \u043f\u0440\u043e\u0434\u044a\u043b\u0436\u0438\u0442\u0435 \u0434\u0430 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0442\u0435 \u0442\u0430\u0437\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f.", "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" }, "user": { diff --git a/homeassistant/components/justnimbus/translations/bg.json b/homeassistant/components/justnimbus/translations/bg.json index 9c8ae1484d5..809ac784bde 100644 --- a/homeassistant/components/justnimbus/translations/bg.json +++ b/homeassistant/components/justnimbus/translations/bg.json @@ -7,6 +7,13 @@ "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" + }, + "step": { + "user": { + "data": { + "client_id": "ID \u043d\u0430 \u043a\u043b\u0438\u0435\u043d\u0442\u0430" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/lametric/translations/bg.json b/homeassistant/components/lametric/translations/bg.json index 86a81cc31e9..28104788cbd 100644 --- a/homeassistant/components/lametric/translations/bg.json +++ b/homeassistant/components/lametric/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", + "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", @@ -22,6 +23,11 @@ }, "pick_implementation": { "title": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u043c\u0435\u0442\u043e\u0434 \u0437\u0430 \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" + }, + "user_cloud_select_device": { + "data": { + "device": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e LaMetric, \u043a\u043e\u0435\u0442\u043e \u0438\u0441\u043a\u0430\u0442\u0435 \u0434\u0430 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u0435" + } } } }, diff --git a/homeassistant/components/lametric/translations/es.json b/homeassistant/components/lametric/translations/es.json index e7cbe914a2e..7cc6fc36cf3 100644 --- a/homeassistant/components/lametric/translations/es.json +++ b/homeassistant/components/lametric/translations/es.json @@ -7,7 +7,8 @@ "link_local_address": "Las direcciones de enlace local no son compatibles", "missing_configuration": "La integraci\u00f3n de LaMetric no est\u00e1 configurada. Por favor, sigue la documentaci\u00f3n.", "no_devices": "El usuario autorizado no tiene dispositivos LaMetric", - "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [revisa la secci\u00f3n de ayuda]({docs_url})" + "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [revisa la secci\u00f3n de ayuda]({docs_url})", + "unknown": "Error inesperado" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/litterrobot/translations/sensor.ca.json b/homeassistant/components/litterrobot/translations/sensor.ca.json index dd8731b78c5..6749f6af0a0 100644 --- a/homeassistant/components/litterrobot/translations/sensor.ca.json +++ b/homeassistant/components/litterrobot/translations/sensor.ca.json @@ -4,6 +4,7 @@ "br": "Bossa extreta", "ccc": "Cicle de neteja completat", "ccp": "Cicle de neteja en curs", + "cd": "Gat detectat", "csf": "Error del sensor de gat", "csi": "Sensor de gat interromput", "cst": "Temps del sensor de gats", @@ -19,6 +20,8 @@ "otf": "Error per sobre-parell", "p": "Pausat/ada", "pd": "Detecci\u00f3 de pessigada", + "pwrd": "Apagant", + "pwru": "Engegant", "rdy": "A punt", "scf": "Error del sensor de gat a l'arrencada", "sdf": "Dip\u00f2sit ple a l'arrencada", diff --git a/homeassistant/components/litterrobot/translations/sensor.el.json b/homeassistant/components/litterrobot/translations/sensor.el.json index f34d6078495..d140630ea24 100644 --- a/homeassistant/components/litterrobot/translations/sensor.el.json +++ b/homeassistant/components/litterrobot/translations/sensor.el.json @@ -4,6 +4,7 @@ "br": "\u03a4\u03bf \u03ba\u03b1\u03c0\u03cc \u03b1\u03c6\u03b1\u03b9\u03c1\u03ad\u03b8\u03b7\u03ba\u03b5", "ccc": "\u039f\u03bb\u03bf\u03ba\u03bb\u03b7\u03c1\u03ce\u03b8\u03b7\u03ba\u03b5 \u03bf \u03ba\u03cd\u03ba\u03bb\u03bf\u03c2 \u03ba\u03b1\u03b8\u03b1\u03c1\u03b9\u03c3\u03bc\u03bf\u03cd", "ccp": "\u039a\u03cd\u03ba\u03bb\u03bf\u03c2 \u03ba\u03b1\u03b8\u03b1\u03c1\u03b9\u03c3\u03bc\u03bf\u03cd \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", + "cd": "\u0391\u03bd\u03b9\u03c7\u03bd\u03b5\u03cd\u03c4\u03b7\u03ba\u03b5 \u03b3\u03ac\u03c4\u03b1", "csf": "\u03a3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b3\u03ac\u03c4\u03b1\u03c2", "csi": "\u0394\u03b9\u03b1\u03ba\u03bf\u03c0\u03ae \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b3\u03ac\u03c4\u03b1\u03c2", "cst": "\u03a7\u03c1\u03bf\u03bd\u03b9\u03c3\u03bc\u03cc\u03c2 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b3\u03ac\u03c4\u03b1\u03c2", @@ -19,6 +20,8 @@ "otf": "\u0392\u03bb\u03ac\u03b2\u03b7 \u03c5\u03c0\u03b5\u03c1\u03b2\u03bf\u03bb\u03b9\u03ba\u03ae\u03c2 \u03c1\u03bf\u03c0\u03ae\u03c2", "p": "\u03a3\u03b5 \u03c0\u03b1\u03cd\u03c3\u03b7", "pd": "\u0391\u03bd\u03af\u03c7\u03bd\u03b5\u03c5\u03c3\u03b7 \u03c4\u03c3\u03b9\u03bc\u03c0\u03ae\u03bc\u03b1\u03c4\u03bf\u03c2", + "pwrd": "\u0391\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7", + "pwru": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7", "rdy": "\u0388\u03c4\u03bf\u03b9\u03bc\u03bf", "scf": "\u03a3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b3\u03ac\u03c4\u03b1\u03c2 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03b5\u03ba\u03ba\u03af\u03bd\u03b7\u03c3\u03b7", "sdf": "\u0393\u03b5\u03bc\u03ac\u03c4\u03bf \u03c3\u03c5\u03c1\u03c4\u03ac\u03c1\u03b9 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03b5\u03ba\u03ba\u03af\u03bd\u03b7\u03c3\u03b7", diff --git a/homeassistant/components/litterrobot/translations/sensor.es.json b/homeassistant/components/litterrobot/translations/sensor.es.json index a67a15c6820..b954fe89f54 100644 --- a/homeassistant/components/litterrobot/translations/sensor.es.json +++ b/homeassistant/components/litterrobot/translations/sensor.es.json @@ -4,6 +4,7 @@ "br": "Bolsa extra\u00edda", "ccc": "Ciclo de limpieza completado", "ccp": "Ciclo de limpieza en curso", + "cd": "Gato detectado", "csf": "Fallo del sensor de gatos", "csi": "Sensor de gatos interrumpido", "cst": "Sincronizaci\u00f3n del sensor de gatos", @@ -19,6 +20,8 @@ "otf": "Fallo de par excesivo", "p": "En pausa", "pd": "Detecci\u00f3n de pellizcos", + "pwrd": "Apagando", + "pwru": "Encendiendo", "rdy": "Listo", "scf": "Fallo del sensor de gatos al inicio", "sdf": "Caj\u00f3n lleno al inicio", diff --git a/homeassistant/components/miflora/translations/bg.json b/homeassistant/components/miflora/translations/bg.json new file mode 100644 index 00000000000..bc09e4fc74d --- /dev/null +++ b/homeassistant/components/miflora/translations/bg.json @@ -0,0 +1,7 @@ +{ + "issues": { + "replaced": { + "title": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 Mi Flora \u0435 \u0437\u0430\u043c\u0435\u043d\u0435\u043d\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nobo_hub/translations/bg.json b/homeassistant/components/nobo_hub/translations/bg.json index f7372a7a5a7..88f263dc963 100644 --- a/homeassistant/components/nobo_hub/translations/bg.json +++ b/homeassistant/components/nobo_hub/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 - \u043f\u0440\u043e\u0432\u0435\u0440\u0435\u0442\u0435 \u0441\u0435\u0440\u0438\u0439\u043d\u0438\u044f \u043d\u043e\u043c\u0435\u0440", "invalid_ip": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d IP \u0430\u0434\u0440\u0435\u0441", "invalid_serial": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d \u0441\u0435\u0440\u0438\u0435\u043d \u043d\u043e\u043c\u0435\u0440", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" @@ -18,7 +19,14 @@ "selected": { "data": { "serial_suffix": "\u0421\u0443\u0444\u0438\u043a\u0441 \u043d\u0430 \u0441\u0435\u0440\u0438\u0439\u043d\u0438\u044f \u043d\u043e\u043c\u0435\u0440 (3 \u0446\u0438\u0444\u0440\u0438)" - } + }, + "description": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 {hub}.\n\n\u0417\u0430 \u0434\u0430 \u0441\u0435 \u0441\u0432\u044a\u0440\u0436\u0435\u0442\u0435 \u0441 \u0445\u044a\u0431\u0430, \u0442\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u0432\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0438\u0442\u0435 3 \u0446\u0438\u0444\u0440\u0438 \u043e\u0442 \u0441\u0435\u0440\u0438\u0439\u043d\u0438\u044f \u043c\u0443 \u043d\u043e\u043c\u0435\u0440." + }, + "user": { + "data": { + "device": "\u041e\u0442\u043a\u0440\u0438\u0442\u0438 \u0445\u044a\u0431\u043e\u0432\u0435" + }, + "description": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 Nob\u00f8 Ecohub \u0437\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435." } } } diff --git a/homeassistant/components/openuv/translations/bg.json b/homeassistant/components/openuv/translations/bg.json index 1bfee97d1e4..6959a04bb7a 100644 --- a/homeassistant/components/openuv/translations/bg.json +++ b/homeassistant/components/openuv/translations/bg.json @@ -18,6 +18,14 @@ } } }, + "issues": { + "deprecated_service_multiple_alternate_targets": { + "title": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 {deprecated_service} \u0441\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u0432\u0430" + }, + "deprecated_service_single_alternate_target": { + "title": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 {deprecated_service} \u0441\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u0432\u0430" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/openuv/translations/ca.json b/homeassistant/components/openuv/translations/ca.json index df55a79d1a5..36043c3bde3 100644 --- a/homeassistant/components/openuv/translations/ca.json +++ b/homeassistant/components/openuv/translations/ca.json @@ -18,6 +18,16 @@ } } }, + "issues": { + "deprecated_service_multiple_alternate_targets": { + "description": "Actualitza totes les automatitzacions o 'scripts' que utilitzin aquest servei perqu\u00e8 passin a utilitzar el servei `{alternate_service}` amb alguna de les seg\u00fcents entitats com a objectiu o 'target': `{alternate_targets}`.", + "title": "El servei {deprecated_service} est\u00e0 sent eliminat" + }, + "deprecated_service_single_alternate_target": { + "description": "Actualitza totes les automatitzacions o 'scripts' que utilitzin aquest servei perqu\u00e8 passin a utilitzar el servei `{alternate_service}` amb `{alternate_targets}` com a objectius o 'targets'.", + "title": "El servei {deprecated_service} est\u00e0 sent eliminat" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/openuv/translations/el.json b/homeassistant/components/openuv/translations/el.json index c86a90e1f09..0b81ff25fd5 100644 --- a/homeassistant/components/openuv/translations/el.json +++ b/homeassistant/components/openuv/translations/el.json @@ -18,6 +18,16 @@ } } }, + "issues": { + "deprecated_service_multiple_alternate_targets": { + "description": "\u0395\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03c5\u03c7\u03cc\u03bd \u03b1\u03c5\u03c4\u03bf\u03bc\u03b1\u03c4\u03b9\u03c3\u03bc\u03bf\u03cd\u03c2 \u03ae \u03c3\u03b5\u03bd\u03ac\u03c1\u03b9\u03b1 \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03bf\u03cd\u03bd \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7\u03bd \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03ce\u03c3\u03c4\u03b5 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03bf\u03cd\u03bd \u03c4\u03b7\u03bd \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 `{alternate_service}` \u03bc\u03b5 \u03ad\u03bd\u03b1 \u03b1\u03c0\u03cc \u03b1\u03c5\u03c4\u03ac \u03c4\u03b1 \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03ac \u03bf\u03bd\u03c4\u03bf\u03c4\u03ae\u03c4\u03c9\u03bd \u03c9\u03c2 \u03c3\u03c4\u03cc\u03c7\u03bf: `{alternate_targets}`.", + "title": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 {deprecated_service} \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + }, + "deprecated_service_single_alternate_target": { + "description": "\u0395\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03c5\u03c7\u03cc\u03bd \u03b1\u03c5\u03c4\u03bf\u03bc\u03b1\u03c4\u03b9\u03c3\u03bc\u03bf\u03cd\u03c2 \u03ae \u03c3\u03b5\u03bd\u03ac\u03c1\u03b9\u03b1 \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03bf\u03cd\u03bd \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7\u03bd \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03ce\u03c3\u03c4\u03b5 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03bf\u03cd\u03bd \u03c4\u03b7\u03bd \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 `{alternate_service}` \u03bc\u03b5 \u03c3\u03c4\u03cc\u03c7\u03bf `{alternate_targets}`.", + "title": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 {deprecated_service} \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/openuv/translations/es.json b/homeassistant/components/openuv/translations/es.json index bb5f36499ba..66331b8e5a5 100644 --- a/homeassistant/components/openuv/translations/es.json +++ b/homeassistant/components/openuv/translations/es.json @@ -18,6 +18,16 @@ } } }, + "issues": { + "deprecated_service_multiple_alternate_targets": { + "description": "Actualiza cualquier automatizaci\u00f3n o script que use este servicio para usar en su lugar el servicio `{alternate_service}` con uno de estos ID de entidad como objetivo: `{alternate_targets}`.", + "title": "Se va a eliminar el servicio {deprecated_service}" + }, + "deprecated_service_single_alternate_target": { + "description": "Actualiza cualquier automatizaci\u00f3n o script que use este servicio para usar en su lugar el servicio `{alternate_service}` con `{alternate_targets}` como destino.", + "title": "Se va a eliminar el servicio {deprecated_service}" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/overkiz/translations/bg.json b/homeassistant/components/overkiz/translations/bg.json index afee50a6b00..ff6e8f03030 100644 --- a/homeassistant/components/overkiz/translations/bg.json +++ b/homeassistant/components/overkiz/translations/bg.json @@ -10,7 +10,8 @@ "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", "server_in_maintenance": "\u0421\u044a\u0440\u0432\u044a\u0440\u044a\u0442 \u0435 \u0441\u043f\u0440\u044f\u043d \u0437\u0430 \u043f\u043e\u0434\u0434\u0440\u044a\u0436\u043a\u0430", "too_many_requests": "\u0422\u0432\u044a\u0440\u0434\u0435 \u043c\u043d\u043e\u0433\u043e \u0437\u0430\u044f\u0432\u043a\u0438, \u043e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e \u043f\u043e-\u043a\u044a\u0441\u043d\u043e.", - "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430", + "unknown_user": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u0435\u043d \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b. \u0410\u043a\u0430\u0443\u043d\u0442\u0438\u0442\u0435 \u043d\u0430 Somfy Protect \u043d\u0435 \u0441\u0435 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430\u0442 \u043e\u0442 \u0442\u0430\u0437\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f." }, "flow_title": "\u0428\u043b\u044e\u0437: {gateway_id}", "step": { diff --git a/homeassistant/components/risco/translations/bg.json b/homeassistant/components/risco/translations/bg.json index 3cc6e1c2bbc..5e165a9dcfe 100644 --- a/homeassistant/components/risco/translations/bg.json +++ b/homeassistant/components/risco/translations/bg.json @@ -28,6 +28,10 @@ "password": "\u041f\u0430\u0440\u043e\u043b\u0430", "pin": "\u041f\u0418\u041d \u043a\u043e\u0434", "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + }, + "menu_options": { + "cloud": "Risco Cloud (\u043f\u0440\u0435\u043f\u043e\u0440\u044a\u0447\u0438\u0442\u0435\u043b\u043d\u043e)", + "local": "\u041b\u043e\u043a\u0430\u043b\u0435\u043d \u043f\u0430\u043d\u0435\u043b Risco (\u0437\u0430 \u043d\u0430\u043f\u0440\u0435\u0434\u043d\u0430\u043b\u0438)" } } } diff --git a/homeassistant/components/schedule/translations/bg.json b/homeassistant/components/schedule/translations/bg.json new file mode 100644 index 00000000000..292b4186ce8 --- /dev/null +++ b/homeassistant/components/schedule/translations/bg.json @@ -0,0 +1,3 @@ +{ + "title": "\u0413\u0440\u0430\u0444\u0438\u043a" +} \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/pl.json b/homeassistant/components/shelly/translations/pl.json index 71d7f047e5f..c9c7496d13e 100644 --- a/homeassistant/components/shelly/translations/pl.json +++ b/homeassistant/components/shelly/translations/pl.json @@ -32,23 +32,23 @@ "device_automation": { "trigger_subtype": { "button": "przycisk", - "button1": "pierwszy", - "button2": "drugi", - "button3": "trzeci", - "button4": "Czwarty przycisk" + "button1": "pierwszy przycisk", + "button2": "drugi przycisk", + "button3": "trzeci przycisk", + "button4": "czwarty przycisk" }, "trigger_type": { - "btn_down": "przycisk {subtype} zostanie wci\u015bni\u0119ty", - "btn_up": "przycisk {subtype} zostanie puszczony", - "double": "przycisk \"{subtype}\" zostanie dwukrotnie naci\u015bni\u0119ty", - "double_push": "przycisk {subtype} zostanie dwukrotnie naci\u015bni\u0119ty", - "long": "przycisk {subtype} zostanie d\u0142ugo naci\u015bni\u0119ty", - "long_push": "przycisk {subtype} zostanie d\u0142ugo naci\u015bni\u0119ty", - "long_single": "przycisk \"{subtype}\" zostanie d\u0142ugo naci\u015bni\u0119ty, a nast\u0119pnie pojedynczo naci\u015bni\u0119ty", - "single": "przycisk \"{subtype}\" zostanie pojedynczo naci\u015bni\u0119ty", - "single_long": "przycisk \"{subtype}\" pojedynczo naci\u015bni\u0119ty, a nast\u0119pnie d\u0142ugo naci\u015bni\u0119ty", - "single_push": "przycisk {subtype} zostanie pojedynczo naci\u015bni\u0119ty", - "triple": "przycisk \"{subtype}\" zostanie trzykrotnie naci\u015bni\u0119ty" + "btn_down": "{subtype} zostanie wci\u015bni\u0119ty", + "btn_up": "{subtype} zostanie puszczony", + "double": "{subtype} zostanie dwukrotnie naci\u015bni\u0119ty", + "double_push": "{subtype} zostanie dwukrotnie naci\u015bni\u0119ty", + "long": "{subtype} zostanie d\u0142ugo naci\u015bni\u0119ty", + "long_push": "{subtype} zostanie d\u0142ugo naci\u015bni\u0119ty", + "long_single": "{subtype} zostanie d\u0142ugo naci\u015bni\u0119ty, a nast\u0119pnie pojedynczo naci\u015bni\u0119ty", + "single": "{subtype} zostanie pojedynczo naci\u015bni\u0119ty", + "single_long": "{subtype} pojedynczo naci\u015bni\u0119ty, a nast\u0119pnie d\u0142ugo naci\u015bni\u0119ty", + "single_push": "{subtype} zostanie pojedynczo naci\u015bni\u0119ty", + "triple": "{subtype} zostanie trzykrotnie naci\u015bni\u0119ty" } } } \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/bg.json b/homeassistant/components/simplisafe/translations/bg.json index d1bb1c2f67f..fa71285d6c9 100644 --- a/homeassistant/components/simplisafe/translations/bg.json +++ b/homeassistant/components/simplisafe/translations/bg.json @@ -31,5 +31,10 @@ } } } + }, + "issues": { + "deprecated_service": { + "title": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 {deprecated_service} \u0441\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u0432\u0430" + } } } \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/el.json b/homeassistant/components/simplisafe/translations/el.json index d9c9123b391..d4cd15d93f0 100644 --- a/homeassistant/components/simplisafe/translations/el.json +++ b/homeassistant/components/simplisafe/translations/el.json @@ -39,6 +39,12 @@ } } }, + "issues": { + "deprecated_service": { + "description": "\u0395\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03c5\u03c7\u03cc\u03bd \u03b1\u03c5\u03c4\u03bf\u03bc\u03b1\u03c4\u03b9\u03c3\u03bc\u03bf\u03cd\u03c2 \u03ae \u03c3\u03b5\u03bd\u03ac\u03c1\u03b9\u03b1 \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03bf\u03cd\u03bd \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7\u03bd \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03ce\u03c3\u03c4\u03b5 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03bf\u03cd\u03bd \u03c4\u03b7\u03bd \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 `{alternate_service}` \u03bc\u03b5 \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2-\u03c3\u03c4\u03cc\u03c7\u03bf\u03c5 `{alternate_target}`. \u03a3\u03c4\u03b7 \u03c3\u03c5\u03bd\u03ad\u03c7\u03b5\u03b9\u03b1, \u03ba\u03ac\u03bd\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af \u03a5\u03a0\u039f\u0392\u039f\u039b\u0397 \u03c0\u03b1\u03c1\u03b1\u03ba\u03ac\u03c4\u03c9 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b5\u03c0\u03b9\u03c3\u03b7\u03bc\u03ac\u03bd\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03b6\u03ae\u03c4\u03b7\u03bc\u03b1 \u03c9\u03c2 \u03b5\u03c0\u03b9\u03bb\u03c5\u03bc\u03ad\u03bd\u03bf.", + "title": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 {deprecated_service} \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/simplisafe/translations/es.json b/homeassistant/components/simplisafe/translations/es.json index 89bdaab3eaf..38936c22ec2 100644 --- a/homeassistant/components/simplisafe/translations/es.json +++ b/homeassistant/components/simplisafe/translations/es.json @@ -39,6 +39,12 @@ } } }, + "issues": { + "deprecated_service": { + "description": "Actualiza cualquier automatizaci\u00f3n o script que use este servicio para usar en su lugar el servicio `{alternate_service}` con una ID de entidad de destino de `{alternate_target}`. Luego, haz clic en ENVIAR a continuaci\u00f3n para marcar este problema como resuelto.", + "title": "Se va a eliminar el servicio {deprecated_service}" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/speedtestdotnet/translations/bg.json b/homeassistant/components/speedtestdotnet/translations/bg.json index 0e175ee9902..31163643662 100644 --- a/homeassistant/components/speedtestdotnet/translations/bg.json +++ b/homeassistant/components/speedtestdotnet/translations/bg.json @@ -9,6 +9,18 @@ } } }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "title": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 Speedtest \u0441\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u0432\u0430" + } + } + }, + "title": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 Speedtest \u0441\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u0432\u0430" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/tasmota/translations/ca.json b/homeassistant/components/tasmota/translations/ca.json index f5adb5b0694..917dd00aeeb 100644 --- a/homeassistant/components/tasmota/translations/ca.json +++ b/homeassistant/components/tasmota/translations/ca.json @@ -16,5 +16,15 @@ "description": "Vols configurar Tasmota?" } } + }, + "issues": { + "topic_duplicated": { + "description": "Hi ha m\u00faltiples dispositius Tasmota amb el mateix 'topic' {topic}.\n\n Dispositius Tasmota amb aquest problema: {offenders}.", + "title": "M\u00faltiples dispositius Tasmota amb el mateix 'topic'" + }, + "topic_no_prefix": { + "description": "El dispositiu Tasmota {name} amb IP {ip} no inclou `%prefix%` al seu 'topic' complet. \n\n Les entitats d'aquests dispositius es desactiven fins que s'hagi corregit el problema.", + "title": "El dispositiu Tasmota {name} t\u00e9 un 'topic' MQTT inv\u00e0lid" + } } } \ No newline at end of file diff --git a/homeassistant/components/tasmota/translations/de.json b/homeassistant/components/tasmota/translations/de.json index 128b99a75b9..cd1da24e22c 100644 --- a/homeassistant/components/tasmota/translations/de.json +++ b/homeassistant/components/tasmota/translations/de.json @@ -16,5 +16,15 @@ "description": "M\u00f6chtest du Tasmota einrichten?" } } + }, + "issues": { + "topic_duplicated": { + "description": "Mehrere Tasmota-Ger\u00e4te teilen das Topic {topic} . \n\nTasmota-Ger\u00e4te mit diesem Problem: {offenders} .", + "title": "Mehrere Tasmota-Ger\u00e4te teilen das gleiche Topic" + }, + "topic_no_prefix": { + "description": "Tasmota-Ger\u00e4t {name} mit IP {ip} enth\u00e4lt `%prefix%` nicht in seinem vollst\u00e4ndigen Topic. \n\nEntit\u00e4ten f\u00fcr diese Ger\u00e4te sind deaktiviert, bis die Konfiguration korrigiert wurde.", + "title": "Tasmota-Ger\u00e4t {name} hat ein ung\u00fcltiges MQTT-Topic" + } } } \ No newline at end of file diff --git a/homeassistant/components/tasmota/translations/el.json b/homeassistant/components/tasmota/translations/el.json index 732724ab2ab..4dca509608f 100644 --- a/homeassistant/components/tasmota/translations/el.json +++ b/homeassistant/components/tasmota/translations/el.json @@ -16,5 +16,15 @@ "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03b5\u03b3\u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd Tasmota;" } } + }, + "issues": { + "topic_duplicated": { + "description": "\u03a0\u03bf\u03bb\u03bb\u03ad\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 Tasmota \u03bc\u03bf\u03b9\u03c1\u03ac\u03b6\u03bf\u03bd\u03c4\u03b1\u03b9 \u03c4\u03bf \u03b8\u03ad\u03bc\u03b1 {topic}. \n\n \u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 Tasmota \u03bc\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1: {offenders}.", + "title": "\u0391\u03c1\u03ba\u03b5\u03c4\u03ad\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 Tasmota \u03bc\u03bf\u03b9\u03c1\u03ac\u03b6\u03bf\u03bd\u03c4\u03b1\u03b9 \u03c4\u03bf \u03af\u03b4\u03b9\u03bf \u03b8\u03ad\u03bc\u03b1" + }, + "topic_no_prefix": { + "description": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae Tasmota {name} \u03bc\u03b5 IP {ip} \u03b4\u03b5\u03bd \u03c0\u03b5\u03c1\u03b9\u03bb\u03b1\u03bc\u03b2\u03ac\u03bd\u03b5\u03b9 \u03c4\u03bf \" %prefix% \" \u03c3\u03c4\u03bf \u03c0\u03bb\u03ae\u03c1\u03b5\u03c2 \u03b8\u03ad\u03bc\u03b1 \u03c4\u03b7\u03c2. \n\n \u039f\u03b9 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2 \u03b3\u03b9\u03b1 \u03b1\u03c5\u03c4\u03ad\u03c2 \u03c4\u03b9\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b5\u03c2 \u03bc\u03ad\u03c7\u03c1\u03b9 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03c9\u03b8\u03b5\u03af \u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7.", + "title": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae Tasmota {name} \u03ad\u03c7\u03b5\u03b9 \u03bc\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03b8\u03ad\u03bc\u03b1 MQTT" + } } } \ No newline at end of file diff --git a/homeassistant/components/tasmota/translations/es.json b/homeassistant/components/tasmota/translations/es.json index 0d3b7317330..b17c54e0c48 100644 --- a/homeassistant/components/tasmota/translations/es.json +++ b/homeassistant/components/tasmota/translations/es.json @@ -16,5 +16,15 @@ "description": "\u00bfQuieres configurar Tasmota?" } } + }, + "issues": { + "topic_duplicated": { + "description": "Varios dispositivos Tasmota comparten el tema {topic}. \n\nDispositivos Tasmota con este problema: {offenders}.", + "title": "Varios dispositivos Tasmota est\u00e1n compartiendo el mismo tema" + }, + "topic_no_prefix": { + "description": "El dispositivo Tasmota {name} con IP {ip} no incluye `%prefix%` en su tema completo. \n\nLas entidades para estos dispositivos est\u00e1n deshabilitadas hasta que se haya corregido la configuraci\u00f3n.", + "title": "El dispositivo Tasmota {name} tiene un tema MQTT no v\u00e1lido" + } } } \ No newline at end of file diff --git a/homeassistant/components/tasmota/translations/fr.json b/homeassistant/components/tasmota/translations/fr.json index 7521004ba2f..9ead135ce72 100644 --- a/homeassistant/components/tasmota/translations/fr.json +++ b/homeassistant/components/tasmota/translations/fr.json @@ -16,5 +16,13 @@ "description": "Voulez-vous configurer Tasmota ?" } } + }, + "issues": { + "topic_duplicated": { + "title": "Plusieurs appareils Tasmota partagent le m\u00eame sujet" + }, + "topic_no_prefix": { + "title": "Le sujet MQTT de l'appareil Tasmota {name} n'est pas valide" + } } } \ No newline at end of file diff --git a/homeassistant/components/tasmota/translations/pt-BR.json b/homeassistant/components/tasmota/translations/pt-BR.json index afd1c76c25f..da0864e38eb 100644 --- a/homeassistant/components/tasmota/translations/pt-BR.json +++ b/homeassistant/components/tasmota/translations/pt-BR.json @@ -16,5 +16,15 @@ "description": "Deseja configurar o Tasmota?" } } + }, + "issues": { + "topic_duplicated": { + "description": "V\u00e1rios dispositivos Tasmota est\u00e3o compartilhando o t\u00f3pico {topic}. \n\n Dispositivos Tasmota com este problema: {offenders}.", + "title": "V\u00e1rios dispositivos Tasmota est\u00e3o compartilhando o mesmo t\u00f3pico" + }, + "topic_no_prefix": { + "description": "O dispositivo Tasmota {name} com IP {ip} n\u00e3o inclui `%prefix%` em seu t\u00f3pico completo. \n\n As entidades para estes dispositivos s\u00e3o desabilitadas at\u00e9 que a configura\u00e7\u00e3o seja corrigida.", + "title": "O dispositivo Tasmota {name} tem um t\u00f3pico MQTT inv\u00e1lido" + } } } \ No newline at end of file diff --git a/homeassistant/components/tasmota/translations/ru.json b/homeassistant/components/tasmota/translations/ru.json index 14e336a9b58..761d6614f8c 100644 --- a/homeassistant/components/tasmota/translations/ru.json +++ b/homeassistant/components/tasmota/translations/ru.json @@ -16,5 +16,15 @@ "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Tasmota?" } } + }, + "issues": { + "topic_duplicated": { + "description": "\u041d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 Tasmota \u0441\u043e\u0432\u043c\u0435\u0441\u0442\u043d\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0442 \u0442\u043e\u043f\u0438\u043a {topic}.\n\n\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Tasmota \u0441 \u044d\u0442\u043e\u0439 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u043e\u0439: {offenders}.", + "title": "\u041d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 Tasmota \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0442 \u043e\u0434\u0438\u043d\u0430\u043a\u043e\u0432\u044b\u0435 \u0442\u043e\u043f\u0438\u043a\u0438" + }, + "topic_no_prefix": { + "description": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Tasmota {name} \u0441 IP-\u0430\u0434\u0440\u0435\u0441\u043e\u043c {ip} \u043d\u0435 \u0432\u043a\u043b\u044e\u0447\u0430\u0435\u0442 `%prefix%` \u0432 \u0441\u0432\u043e\u0439 \u0442\u043e\u043f\u0438\u043a.\n\n\u041e\u0431\u044a\u0435\u043a\u0442\u044b \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0434\u0435\u0430\u043a\u0442\u0438\u0432\u0438\u0440\u043e\u0432\u0430\u043d\u044b \u0434\u043e \u0442\u0435\u0445 \u043f\u043e\u0440, \u043f\u043e\u043a\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0435 \u0431\u0443\u0434\u0435\u0442 \u0438\u0441\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0430.", + "title": "\u0423 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Tasmota {name} \u043d\u0435\u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b\u0439 \u0442\u043e\u043f\u0438\u043a MQTT" + } } } \ No newline at end of file diff --git a/homeassistant/components/tasmota/translations/zh-Hant.json b/homeassistant/components/tasmota/translations/zh-Hant.json index 58833747f46..740f1aeb748 100644 --- a/homeassistant/components/tasmota/translations/zh-Hant.json +++ b/homeassistant/components/tasmota/translations/zh-Hant.json @@ -16,5 +16,15 @@ "description": "\u662f\u5426\u8981\u8a2d\u5b9a Tasmota\uff1f" } } + }, + "issues": { + "topic_duplicated": { + "description": "\u6709\u591a\u500b Tasmota \u88dd\u7f6e\u4f7f\u7528\u4e86\u76f8\u540c\u7684\u4e3b\u984c {topic}\u3002\n\n \u767c\u73fe\u554f\u984c\u7684 Tasmota \u88dd\u7f6e\uff1a{offenders}\u3002", + "title": "\u6709\u591a\u500b Tasmota \u88dd\u7f6e\u4f7f\u7528\u4e86\u76f8\u540c\u7684\u4e3b\u984c" + }, + "topic_no_prefix": { + "description": "IP \u70ba {ip} \u7684 Tasmota \u88dd\u7f6e {name} \u4e26\u672a\u65bc\u5b8c\u6574\u4e3b\u984c\u4e2d\u5305\u542b `%prefix%`\u3002\n\n\u6b64\u88dd\u7f6e\u5be6\u9ad4\u5c07\u9032\u884c\u505c\u7528\u3001\u76f4\u5230\u8a2d\u5b9a\u4fee\u6b63\u3002", + "title": "Tasmota \u88dd\u7f6e {name} \u4f7f\u7528\u4e86\u7121\u6548\u7684 MQTT \u4e3b\u984c" + } } } \ No newline at end of file diff --git a/homeassistant/components/unifiprotect/translations/bg.json b/homeassistant/components/unifiprotect/translations/bg.json index a3987858b93..c5b9cada9e0 100644 --- a/homeassistant/components/unifiprotect/translations/bg.json +++ b/homeassistant/components/unifiprotect/translations/bg.json @@ -33,5 +33,17 @@ "description": "\u0429\u0435 \u0432\u0438 \u0435 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c \u043b\u043e\u043a\u0430\u043b\u0435\u043d \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b, \u0441\u044a\u0437\u0434\u0430\u0434\u0435\u043d \u0432\u044a\u0432 \u0432\u0430\u0448\u0430\u0442\u0430 UniFi OS Console, \u0437\u0430 \u0434\u0430 \u0432\u043b\u0435\u0437\u0435\u0442\u0435 \u0441 \u043d\u0435\u0433\u043e. \u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u0438\u0442\u0435 \u0430\u043a\u0430\u0443\u043d\u0442\u0438 \u0441\u044a\u0437\u0434\u0430\u0434\u0435\u043d\u0438 \u0432 Ubiquiti Cloud \u043d\u044f\u043c\u0430 \u0434\u0430 \u0440\u0430\u0431\u043e\u0442\u044f\u0442. \u0417\u0430 \u043f\u043e\u0432\u0435\u0447\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f: {local_user_documentation_url}" } } + }, + "options": { + "error": { + "invalid_mac_list": "\u0422\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u0431\u044a\u0434\u0435 \u0441\u043f\u0438\u0441\u044a\u043a \u0441 MAC \u0430\u0434\u0440\u0435\u0441\u0438, \u0440\u0430\u0437\u0434\u0435\u043b\u0435\u043d\u0438 \u0441\u044a\u0441 \u0437\u0430\u043f\u0435\u0442\u0430\u0438" + }, + "step": { + "init": { + "data": { + "ignored_devices": "\u0421\u043f\u0438\u0441\u044a\u043a \u0441 MAC \u0430\u0434\u0440\u0435\u0441\u0438 \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430, \u043a\u043e\u0438\u0442\u043e \u0434\u0430 \u0441\u0435 \u0438\u0433\u043d\u043e\u0440\u0438\u0440\u0430\u0442, \u0440\u0430\u0437\u0434\u0435\u043b\u0435\u043d \u0441\u044a\u0441 \u0437\u0430\u043f\u0435\u0442\u0430\u0438" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/volvooncall/translations/bg.json b/homeassistant/components/volvooncall/translations/bg.json index aa879c9649c..598cf3c837f 100644 --- a/homeassistant/components/volvooncall/translations/bg.json +++ b/homeassistant/components/volvooncall/translations/bg.json @@ -11,6 +11,7 @@ "step": { "user": { "data": { + "mutable": "\u0420\u0430\u0437\u0440\u0435\u0448\u0430\u0432\u0430\u043d\u0435 \u043d\u0430 \u0434\u0438\u0441\u0442\u0430\u043d\u0446\u0438\u043e\u043d\u043d\u043e \u0441\u0442\u0430\u0440\u0442\u0438\u0440\u0430\u043d\u0435 / \u0437\u0430\u043a\u043b\u044e\u0447\u0432\u0430\u043d\u0435 \u0438 \u0434\u0440.", "password": "\u041f\u0430\u0440\u043e\u043b\u0430", "region": "\u0420\u0435\u0433\u0438\u043e\u043d", "scandinavian_miles": "\u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 \u0441\u043a\u0430\u043d\u0434\u0438\u043d\u0430\u0432\u0441\u043a\u0438 \u043c\u0438\u043b\u0438", diff --git a/homeassistant/components/volvooncall/translations/ca.json b/homeassistant/components/volvooncall/translations/ca.json index 26a287a5584..b261c0dc095 100644 --- a/homeassistant/components/volvooncall/translations/ca.json +++ b/homeassistant/components/volvooncall/translations/ca.json @@ -15,6 +15,7 @@ "password": "Contrasenya", "region": "Regi\u00f3", "scandinavian_miles": "Utilitza milles escandinaves", + "unit_system": "Sistema d'unitats", "username": "Nom d'usuari" } } diff --git a/homeassistant/components/volvooncall/translations/de.json b/homeassistant/components/volvooncall/translations/de.json index 561a19e635d..5d00fbb00a5 100644 --- a/homeassistant/components/volvooncall/translations/de.json +++ b/homeassistant/components/volvooncall/translations/de.json @@ -15,6 +15,7 @@ "password": "Passwort", "region": "Region", "scandinavian_miles": "Skandinavische Meilen verwenden", + "unit_system": "Einheitensystem", "username": "Benutzername" } } diff --git a/homeassistant/components/volvooncall/translations/el.json b/homeassistant/components/volvooncall/translations/el.json index 7739ac02417..f216755a0b8 100644 --- a/homeassistant/components/volvooncall/translations/el.json +++ b/homeassistant/components/volvooncall/translations/el.json @@ -15,6 +15,7 @@ "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", "region": "\u03a0\u03b5\u03c1\u03b9\u03bf\u03c7\u03ae", "scandinavian_miles": "\u03a7\u03c1\u03ae\u03c3\u03b7 \u03a3\u03ba\u03b1\u03bd\u03b4\u03b9\u03bd\u03b1\u03b2\u03b9\u03ba\u03ce\u03bd \u039c\u03b9\u03bb\u03af\u03c9\u03bd", + "unit_system": "\u039c\u03bf\u03bd\u03ac\u03b4\u03b1 \u03a3\u03c5\u03c3\u03c4\u03ae\u03bc\u03b1\u03c4\u03bf\u03c2", "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" } } diff --git a/homeassistant/components/volvooncall/translations/en.json b/homeassistant/components/volvooncall/translations/en.json index a75ab687d97..fca96e5e0ed 100644 --- a/homeassistant/components/volvooncall/translations/en.json +++ b/homeassistant/components/volvooncall/translations/en.json @@ -1,29 +1,30 @@ { "config": { - "step": { - "user": { - "data": { - "username": "Username", - "password": "Password", - "region": "Region", - "mutable": "Allow Remote Start / Lock / etc.", - "unit_system": "Unit System" - } - } + "abort": { + "already_configured": "Account is already configured", + "reauth_successful": "Re-authentication was successful" }, "error": { "invalid_auth": "Invalid authentication", "unknown": "Unexpected error" }, - "abort": { - "already_configured": "Account is already configured", - "reauth_successful": "Re-authentication was successful" + "step": { + "user": { + "data": { + "mutable": "Allow Remote Start / Lock / etc.", + "password": "Password", + "region": "Region", + "scandinavian_miles": "Use Scandinavian Miles", + "unit_system": "Unit System", + "username": "Username" + } + } } }, "issues": { "deprecated_yaml": { - "title": "The Volvo On Call YAML configuration is being removed", - "description": "Configuring the Volvo On Call platform using YAML is being removed in a future release of Home Assistant.\n\nYour existing configuration has been imported into the UI automatically. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + "description": "Configuring the Volvo On Call platform using YAML is being removed in a future release of Home Assistant.\n\nYour existing configuration has been imported into the UI automatically. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Volvo On Call YAML configuration is being removed" } } -} +} \ No newline at end of file diff --git a/homeassistant/components/volvooncall/translations/es.json b/homeassistant/components/volvooncall/translations/es.json index 57a21a3d905..bcca5a0da4d 100644 --- a/homeassistant/components/volvooncall/translations/es.json +++ b/homeassistant/components/volvooncall/translations/es.json @@ -15,6 +15,7 @@ "password": "Contrase\u00f1a", "region": "Regi\u00f3n", "scandinavian_miles": "Utilizar millas escandinavas", + "unit_system": "Sistema de unidades", "username": "Nombre de usuario" } } diff --git a/homeassistant/components/volvooncall/translations/fr.json b/homeassistant/components/volvooncall/translations/fr.json index bb18d166012..2449ab4bed4 100644 --- a/homeassistant/components/volvooncall/translations/fr.json +++ b/homeassistant/components/volvooncall/translations/fr.json @@ -15,6 +15,7 @@ "password": "Mot de passe", "region": "R\u00e9gion", "scandinavian_miles": "Utiliser les miles scandinaves", + "unit_system": "Syst\u00e8me d'unit\u00e9s", "username": "Nom d'utilisateur" } } diff --git a/homeassistant/components/volvooncall/translations/hu.json b/homeassistant/components/volvooncall/translations/hu.json index ae91d2d4b9b..5b382780bea 100644 --- a/homeassistant/components/volvooncall/translations/hu.json +++ b/homeassistant/components/volvooncall/translations/hu.json @@ -15,6 +15,7 @@ "password": "Jelsz\u00f3", "region": "R\u00e9gi\u00f3", "scandinavian_miles": "Skandin\u00e1v m\u00e9rf\u00f6ld haszn\u00e1lata", + "unit_system": "Egys\u00e9grendszer", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" } } diff --git a/homeassistant/components/volvooncall/translations/pt-BR.json b/homeassistant/components/volvooncall/translations/pt-BR.json index be90fa861e4..d8bb8a945d4 100644 --- a/homeassistant/components/volvooncall/translations/pt-BR.json +++ b/homeassistant/components/volvooncall/translations/pt-BR.json @@ -15,6 +15,7 @@ "password": "Senha", "region": "Regi\u00e3o", "scandinavian_miles": "Usar milhas escandinavas", + "unit_system": "Sistema de Unidades", "username": "Usu\u00e1rio" } } diff --git a/homeassistant/components/volvooncall/translations/zh-Hant.json b/homeassistant/components/volvooncall/translations/zh-Hant.json index 6eedaa512cf..65aeee8325f 100644 --- a/homeassistant/components/volvooncall/translations/zh-Hant.json +++ b/homeassistant/components/volvooncall/translations/zh-Hant.json @@ -15,6 +15,7 @@ "password": "\u5bc6\u78bc", "region": "\u5340\u57df", "scandinavian_miles": "\u4f7f\u7528\u7d0d\u7dad\u4e9e\u82f1\u91cc", + "unit_system": "\u55ae\u4f4d\u7cfb\u7d71", "username": "\u4f7f\u7528\u8005\u540d\u7a31" } } diff --git a/homeassistant/components/xiaomi_miio/translations/select.bg.json b/homeassistant/components/xiaomi_miio/translations/select.bg.json index fa3683359ec..de2968f7854 100644 --- a/homeassistant/components/xiaomi_miio/translations/select.bg.json +++ b/homeassistant/components/xiaomi_miio/translations/select.bg.json @@ -1,7 +1,17 @@ { "state": { + "xiaomi_miio__display_orientation": { + "forward": "\u041d\u0430\u043f\u0440\u0435\u0434", + "left": "\u041b\u044f\u0432\u043e", + "right": "\u0414\u044f\u0441\u043d\u043e" + }, "xiaomi_miio__led_brightness": { "off": "\u0418\u0437\u043a\u043b\u044e\u0447\u0435\u043d\u043e" + }, + "xiaomi_miio__ptc_level": { + "high": "\u0412\u0438\u0441\u043e\u043a\u043e", + "low": "\u041d\u0438\u0441\u043a\u043e", + "medium": "\u0421\u0440\u0435\u0434\u043d\u043e" } } } \ No newline at end of file diff --git a/homeassistant/components/yalexs_ble/translations/bg.json b/homeassistant/components/yalexs_ble/translations/bg.json index d849af39077..808c658d612 100644 --- a/homeassistant/components/yalexs_ble/translations/bg.json +++ b/homeassistant/components/yalexs_ble/translations/bg.json @@ -12,10 +12,14 @@ }, "flow_title": "{name}", "step": { + "integration_discovery_confirm": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 {name} \u043f\u0440\u0435\u0437 Bluetooth \u0441 \u0430\u0434\u0440\u0435\u0441 {address}?" + }, "user": { "data": { "address": "Bluetooth \u0430\u0434\u0440\u0435\u0441" - } + }, + "description": "\u041f\u0440\u043e\u0432\u0435\u0440\u0435\u0442\u0435 \u0432 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0442\u0430 \u043a\u0430\u043a \u0434\u0430 \u043d\u0430\u043c\u0435\u0440\u0438\u0442\u0435 \u043e\u0444\u043b\u0430\u0439\u043d \u043a\u043b\u044e\u0447\u0430." } } } diff --git a/homeassistant/components/zha/translations/bg.json b/homeassistant/components/zha/translations/bg.json index 18814ad0a33..3bd7629cd95 100644 --- a/homeassistant/components/zha/translations/bg.json +++ b/homeassistant/components/zha/translations/bg.json @@ -9,11 +9,13 @@ }, "step": { "choose_formation_strategy": { + "description": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u043c\u0440\u0435\u0436\u043e\u0432\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0437\u0430 \u0432\u0430\u0448\u0435\u0442\u043e \u0440\u0430\u0434\u0438\u043e.", "menu_options": { "reuse_settings": "\u0417\u0430\u043f\u0430\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438\u0442\u0435 \u043d\u0430 \u0440\u0430\u0434\u0438\u043e \u043c\u0440\u0435\u0436\u0430\u0442\u0430" } }, "choose_serial_port": { + "description": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0441\u0435\u0440\u0438\u0439\u043d\u0438\u044f \u043f\u043e\u0440\u0442 \u0437\u0430 \u0432\u0430\u0448\u0435\u0442\u043e Zigbee \u0440\u0430\u0434\u0438\u043e", "title": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0441\u0435\u0440\u0438\u0435\u043d \u043f\u043e\u0440\u0442" }, "confirm": { diff --git a/homeassistant/components/zwave_js/translations/bg.json b/homeassistant/components/zwave_js/translations/bg.json index dd6d70483d3..7bf8bfcc764 100644 --- a/homeassistant/components/zwave_js/translations/bg.json +++ b/homeassistant/components/zwave_js/translations/bg.json @@ -24,6 +24,12 @@ } } }, + "issues": { + "invalid_server_version": { + "description": "\u0412\u0435\u0440\u0441\u0438\u044f\u0442\u0430 \u043d\u0430 Z-Wave JS \u0441\u044a\u0440\u0432\u044a\u0440\u0430, \u043a\u043e\u044f\u0442\u043e \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0442\u0435 \u0432 \u043c\u043e\u043c\u0435\u043d\u0442\u0430, \u0435 \u0442\u0432\u044a\u0440\u0434\u0435 \u0441\u0442\u0430\u0440\u0430 \u0437\u0430 \u0442\u0430\u0437\u0438 \u0432\u0435\u0440\u0441\u0438\u044f \u043d\u0430 Home Assistant. \u041c\u043e\u043b\u044f, \u0430\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u0439\u0442\u0435 Z-Wave JS \u0441\u044a\u0440\u0432\u044a\u0440\u0430 \u0434\u043e \u043d\u0430\u0439-\u043d\u043e\u0432\u0430\u0442\u0430 \u0432\u0435\u0440\u0441\u0438\u044f, \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": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u0430 \u0435 \u043f\u043e-\u043d\u043e\u0432\u0430 \u0432\u0435\u0440\u0441\u0438\u044f \u043d\u0430 Z-Wave JS \u0441\u044a\u0440\u0432\u044a\u0440\u0430" + } + }, "options": { "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", diff --git a/homeassistant/components/zwave_js/translations/ca.json b/homeassistant/components/zwave_js/translations/ca.json index de21cc3f232..455fd8f9127 100644 --- a/homeassistant/components/zwave_js/translations/ca.json +++ b/homeassistant/components/zwave_js/translations/ca.json @@ -91,6 +91,12 @@ "zwave_js.value_updated.value": "Canvi del valor en un valor Z-Wave JS" } }, + "issues": { + "invalid_server_version": { + "description": "La versi\u00f3 del servidor Z-Wave JS que est\u00e0s executant actualment \u00e9s massa antiga per a aquesta versi\u00f3 de Home Assistant. Actualitza a l'\u00faltima versi\u00f3 per resoldre aquest problema.", + "title": "Es necessita una versi\u00f3 m\u00e9s nova del servidor Z-Wave JS" + } + }, "options": { "abort": { "addon_get_discovery_info_failed": "No s'ha pogut obtenir la informaci\u00f3 de descobriment del complement Z-Wave JS.", diff --git a/homeassistant/components/zwave_js/translations/de.json b/homeassistant/components/zwave_js/translations/de.json index ec0157f9a66..e200e086444 100644 --- a/homeassistant/components/zwave_js/translations/de.json +++ b/homeassistant/components/zwave_js/translations/de.json @@ -91,6 +91,12 @@ "zwave_js.value_updated.value": "Wert\u00e4nderung bei einem Z-Wave JS Wert" } }, + "issues": { + "invalid_server_version": { + "description": "Die Version des Z-Wave JS Servers, die du derzeit verwendest, ist zu alt f\u00fcr diese Version des Home Assistant. Bitte aktualisiere den Z-Wave JS Server auf die neueste Version, um dieses Problem zu beheben.", + "title": "Neuere Version von Z-Wave JS Server erforderlich" + } + }, "options": { "abort": { "addon_get_discovery_info_failed": "Die Discovery-Informationen des Z-Wave JS-Add-On konnten nicht abgerufen werden.", diff --git a/homeassistant/components/zwave_js/translations/el.json b/homeassistant/components/zwave_js/translations/el.json index 930c82aa891..9e671ee1aaa 100644 --- a/homeassistant/components/zwave_js/translations/el.json +++ b/homeassistant/components/zwave_js/translations/el.json @@ -91,6 +91,12 @@ "zwave_js.value_updated.value": "\u0391\u03bb\u03bb\u03b1\u03b3\u03ae \u03c4\u03b9\u03bc\u03ae\u03c2 \u03c3\u03b5 \u03c4\u03b9\u03bc\u03ae Z-Wave JS" } }, + "issues": { + "invalid_server_version": { + "description": "\u0397 \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7 \u03c4\u03bf\u03c5 Z-Wave JS Server \u03c0\u03bf\u03c5 \u03b5\u03ba\u03c4\u03b5\u03bb\u03b5\u03af\u03c4\u03b5 \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7 \u03c3\u03c4\u03b9\u03b3\u03bc\u03ae \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03bf\u03bb\u03cd \u03c0\u03b1\u03bb\u03b9\u03ac \u03b3\u03b9\u03b1 \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7\u03bd \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7 \u03c4\u03bf\u03c5 Home Assistant. \u0395\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd Z-Wave JS Server \u03c3\u03c4\u03b7\u03bd \u03c0\u03b9\u03bf \u03c0\u03c1\u03cc\u03c3\u03c6\u03b1\u03c4\u03b7 \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0391\u03c0\u03b1\u03b9\u03c4\u03b5\u03af\u03c4\u03b1\u03b9 \u03bd\u03b5\u03cc\u03c4\u03b5\u03c1\u03b7 \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae Z-Wave JS" + } + }, "options": { "abort": { "addon_get_discovery_info_failed": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03bb\u03ae\u03c8\u03b7\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03b9\u03ce\u03bd \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf\u03c5 Z-Wave JS.", diff --git a/homeassistant/components/zwave_js/translations/es.json b/homeassistant/components/zwave_js/translations/es.json index 62b43a0bab6..ff4d48f3f21 100644 --- a/homeassistant/components/zwave_js/translations/es.json +++ b/homeassistant/components/zwave_js/translations/es.json @@ -91,6 +91,12 @@ "zwave_js.value_updated.value": "Cambio de valor en un valor Z-Wave JS" } }, + "issues": { + "invalid_server_version": { + "description": "La versi\u00f3n de Z-Wave JS Server que est\u00e1 ejecutando actualmente es demasiado antigua para esta versi\u00f3n de Home Assistant. Actualiza Z-Wave JS Server a la \u00faltima versi\u00f3n para solucionar este problema.", + "title": "Se necesita una versi\u00f3n m\u00e1s reciente de Z-Wave JS Server" + } + }, "options": { "abort": { "addon_get_discovery_info_failed": "No se pudo obtener la informaci\u00f3n de descubrimiento del complemento Z-Wave JS.", diff --git a/homeassistant/components/zwave_js/translations/fr.json b/homeassistant/components/zwave_js/translations/fr.json index 47c3086489c..cf7552491c7 100644 --- a/homeassistant/components/zwave_js/translations/fr.json +++ b/homeassistant/components/zwave_js/translations/fr.json @@ -91,6 +91,11 @@ "zwave_js.value_updated.value": "Changement de valeur sur une valeur Z-Wave JS" } }, + "issues": { + "invalid_server_version": { + "title": "Une version plus r\u00e9cente du serveur Z-Wave\u00a0JS est n\u00e9cessaire" + } + }, "options": { "abort": { "addon_get_discovery_info_failed": "\u00c9chec de l'obtention des informations de d\u00e9couverte du module compl\u00e9mentaire Z-Wave JS.", diff --git a/homeassistant/components/zwave_js/translations/hu.json b/homeassistant/components/zwave_js/translations/hu.json index f93ff278c5f..5bf50719b49 100644 --- a/homeassistant/components/zwave_js/translations/hu.json +++ b/homeassistant/components/zwave_js/translations/hu.json @@ -91,6 +91,12 @@ "zwave_js.value_updated.value": "\u00c9rt\u00e9kv\u00e1ltoz\u00e1s egy Z-Wave JS \u00e9rt\u00e9ken" } }, + "issues": { + "invalid_server_version": { + "description": "A Z-Wave JS Server jelenleg fut\u00f3 verzi\u00f3ja t\u00fal r\u00e9gi a Home Assistant ezen verzi\u00f3j\u00e1hoz. A probl\u00e9ma megold\u00e1s\u00e1hoz friss\u00edtse a Z-Wave JS Servert a leg\u00fajabb verzi\u00f3ra.", + "title": "A Z-Wave JS Server \u00fajabb verzi\u00f3ja sz\u00fcks\u00e9ges" + } + }, "options": { "abort": { "addon_get_discovery_info_failed": "Nem siker\u00fclt leh\u00edvni a Z-Wave JS b\u0151v\u00edtm\u00e9ny felfedez\u00e9si inform\u00e1ci\u00f3kat.", diff --git a/homeassistant/components/zwave_js/translations/pt-BR.json b/homeassistant/components/zwave_js/translations/pt-BR.json index e8587aa6f94..83b6bed6365 100644 --- a/homeassistant/components/zwave_js/translations/pt-BR.json +++ b/homeassistant/components/zwave_js/translations/pt-BR.json @@ -91,6 +91,12 @@ "zwave_js.value_updated.value": "Altera\u00e7\u00e3o de valor em um valor Z-Wave JS" } }, + "issues": { + "invalid_server_version": { + "description": "A vers\u00e3o do Z-Wave JS Server que voc\u00ea est\u00e1 executando atualmente \u00e9 muito antiga para esta vers\u00e3o do Home Assistant. Atualize o Z-Wave JS Server para a vers\u00e3o mais recente para corrigir esse problema.", + "title": "\u00c9 necess\u00e1ria uma vers\u00e3o mais recente do Z-Wave JS Server" + } + }, "options": { "abort": { "addon_get_discovery_info_failed": "Falha em obter informa\u00e7\u00f5es sobre a descoberta do add-on Z-Wave JS.", diff --git a/homeassistant/components/zwave_js/translations/ru.json b/homeassistant/components/zwave_js/translations/ru.json index 44979a6cfe9..bbf816046df 100644 --- a/homeassistant/components/zwave_js/translations/ru.json +++ b/homeassistant/components/zwave_js/translations/ru.json @@ -91,6 +91,12 @@ "zwave_js.value_updated.value": "\u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f Z-Wave JS Value" } }, + "issues": { + "invalid_server_version": { + "description": "\u0412\u0435\u0440\u0441\u0438\u044f Z-Wave JS Server, \u043a\u043e\u0442\u043e\u0440\u0443\u044e \u0412\u044b \u0441\u0435\u0439\u0447\u0430\u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0435, \u0441\u043b\u0438\u0448\u043a\u043e\u043c \u0443\u0441\u0442\u0430\u0440\u0435\u043b\u0430 \u0434\u043b\u044f \u044d\u0442\u043e\u0439 \u0432\u0435\u0440\u0441\u0438\u0438 Home Assistant. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0431\u043d\u043e\u0432\u0438\u0442\u0435 Z-Wave JS Server \u0434\u043e \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0435\u0439 \u0432\u0435\u0440\u0441\u0438\u0438, \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": "\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0431\u043e\u043b\u0435\u0435 \u043d\u043e\u0432\u0430\u044f \u0432\u0435\u0440\u0441\u0438\u044f Z-Wave JS Server" + } + }, "options": { "abort": { "addon_get_discovery_info_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e\u0431 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0438 \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f Z-Wave JS.", diff --git a/homeassistant/components/zwave_js/translations/zh-Hant.json b/homeassistant/components/zwave_js/translations/zh-Hant.json index 246800d1048..3c5f898324a 100644 --- a/homeassistant/components/zwave_js/translations/zh-Hant.json +++ b/homeassistant/components/zwave_js/translations/zh-Hant.json @@ -91,6 +91,12 @@ "zwave_js.value_updated.value": "Z-Wave JS \u503c\u4e0a\u7684\u6578\u503c\u8b8a\u66f4" } }, + "issues": { + "invalid_server_version": { + "description": "\u76ee\u524d Home Assistant \u6240\u57f7\u884c\u7684 Z-Wave JS \u4f3a\u670d\u5668\u7248\u672c\u904e\u820a\u3001\u8acb\u66f4\u65b0\u81f3\u6700\u65b0\u7248\u672c Z-Wave JS \u4f3a\u670d\u5668\u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "\u9700\u8981\u5b89\u88dd\u65b0\u7248\u672c Z-Wave JS \u4f3a\u670d\u5668" + } + }, "options": { "abort": { "addon_get_discovery_info_failed": "\u53d6\u5f97 Z-Wave JS \u9644\u52a0\u5143\u4ef6\u641c\u7d22\u8cc7\u8a0a\u5931\u6557\u3002", From 5829ff5aea48171a5a5e16d8f2cd07e10eb98a6a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 19 Sep 2022 19:57:18 -0500 Subject: [PATCH 574/955] Prevent tilt_ble from matching generic ibeacons (#78722) --- homeassistant/components/tilt_ble/manifest.json | 3 ++- homeassistant/generated/bluetooth.py | 7 +++++++ tests/components/tilt_ble/test_sensor.py | 4 +++- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tilt_ble/manifest.json b/homeassistant/components/tilt_ble/manifest.json index 898807e1bec..b201628a7f5 100644 --- a/homeassistant/components/tilt_ble/manifest.json +++ b/homeassistant/components/tilt_ble/manifest.json @@ -5,7 +5,8 @@ "documentation": "https://www.home-assistant.io/integrations/tilt_ble", "bluetooth": [ { - "manufacturer_id": 76 + "manufacturer_id": 76, + "manufacturer_data_start": [2, 21, 164, 149, 187] } ], "requirements": ["tilt-ble==0.2.3"], diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index f3f789f9829..a97f50f2e5f 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -281,6 +281,13 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ { "domain": "tilt_ble", "manufacturer_id": 76, + "manufacturer_data_start": [ + 2, + 21, + 164, + 149, + 187, + ], }, { "domain": "xiaomi_ble", diff --git a/tests/components/tilt_ble/test_sensor.py b/tests/components/tilt_ble/test_sensor.py index 28454034864..19412a7692c 100644 --- a/tests/components/tilt_ble/test_sensor.py +++ b/tests/components/tilt_ble/test_sensor.py @@ -27,7 +27,9 @@ async def test_sensors(hass: HomeAssistant): assert len(hass.states.async_all()) == 0 inject_bluetooth_service_info(hass, TILT_GREEN_SERVICE_INFO) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 2 + assert ( + len(hass.states.async_all()) >= 2 + ) # may trigger ibeacon integration as well since tilt uses ibeacon temp_sensor = hass.states.get("sensor.tilt_green_temperature") assert temp_sensor is not None From 635c2f3738de1abebe0eed86ff18c8926136583b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 19 Sep 2022 19:58:18 -0500 Subject: [PATCH 575/955] Change bluetooth source to be the address of the adapter on Linux (#78795) --- homeassistant/components/bluetooth/scanner.py | 3 +- tests/components/bluetooth/conftest.py | 5 ++ .../components/bluetooth/test_diagnostics.py | 74 ++++++++++++++++++- 3 files changed, 78 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/scanner.py b/homeassistant/components/bluetooth/scanner.py index f9bfcf7bb79..3f089326eb2 100644 --- a/homeassistant/components/bluetooth/scanner.py +++ b/homeassistant/components/bluetooth/scanner.py @@ -25,6 +25,7 @@ from homeassistant.helpers.event import async_track_time_interval from homeassistant.util.package import is_docker_env from .const import ( + DEFAULT_ADDRESS, SCANNER_WATCHDOG_INTERVAL, SCANNER_WATCHDOG_TIMEOUT, SOURCE_LOCAL, @@ -132,7 +133,7 @@ class HaScanner(BaseHaScanner): self._start_time = 0.0 self._callbacks: list[Callable[[BluetoothServiceInfoBleak], None]] = [] self.name = adapter_human_name(adapter, address) - self.source = self.adapter or SOURCE_LOCAL + self.source = address if address != DEFAULT_ADDRESS else adapter or SOURCE_LOCAL @property def discovered_devices(self) -> list[BLEDevice]: diff --git a/tests/components/bluetooth/conftest.py b/tests/components/bluetooth/conftest.py index 857af1e7eaa..2c0e2e6eb9c 100644 --- a/tests/components/bluetooth/conftest.py +++ b/tests/components/bluetooth/conftest.py @@ -19,6 +19,11 @@ def bluez_dbus_mock(): def macos_adapter(): """Fixture that mocks the macos adapter.""" with patch( + "homeassistant.components.bluetooth.platform.system", return_value="Darwin" + ), patch( + "homeassistant.components.bluetooth.scanner.platform.system", + return_value="Darwin", + ), patch( "homeassistant.components.bluetooth.util.platform.system", return_value="Darwin" ): yield diff --git a/tests/components/bluetooth/test_diagnostics.py b/tests/components/bluetooth/test_diagnostics.py index 9059fcb9ab1..d641cae9c7c 100644 --- a/tests/components/bluetooth/test_diagnostics.py +++ b/tests/components/bluetooth/test_diagnostics.py @@ -6,6 +6,7 @@ from unittest.mock import ANY, patch from bleak.backends.scanner import BLEDevice from homeassistant.components import bluetooth +from homeassistant.components.bluetooth.const import DEFAULT_ADDRESS from tests.common import MockConfigEntry from tests.components.diagnostics import get_diagnostics_for_config_entry @@ -117,7 +118,7 @@ async def test_diagnostics( ], "last_detection": ANY, "name": "hci0 (00:00:00:00:00:01)", - "source": "hci0", + "source": "00:00:00:00:00:01", "start_time": ANY, "type": "HaScanner", }, @@ -128,7 +129,7 @@ async def test_diagnostics( ], "last_detection": ANY, "name": "hci0 (00:00:00:00:00:01)", - "source": "hci0", + "source": "00:00:00:00:00:01", "start_time": ANY, "type": "HaScanner", }, @@ -139,10 +140,77 @@ async def test_diagnostics( ], "last_detection": ANY, "name": "hci1 (00:00:00:00:00:02)", - "source": "hci1", + "source": "00:00:00:00:00:02", "start_time": ANY, "type": "HaScanner", }, ], }, } + + +async def test_diagnostics_macos( + hass, hass_client, mock_bleak_scanner_start, mock_bluetooth_adapters, macos_adapter +): + """Test we can setup and unsetup bluetooth with multiple adapters.""" + # Normally we do not want to patch our classes, but since bleak will import + # a different scanner based on the operating system, we need to patch here + # because we cannot import the scanner class directly without it throwing an + # error if the test is not running on linux since we won't have the correct + # deps installed when testing on MacOS. + + with patch( + "homeassistant.components.bluetooth.scanner.HaScanner.discovered_devices", + [BLEDevice(name="x", rssi=-60, address="44:44:33:11:23:45")], + ), patch( + "homeassistant.components.bluetooth.diagnostics.platform.system", + return_value="Darwin", + ), patch( + "homeassistant.components.bluetooth.diagnostics.get_dbus_managed_objects", + return_value={}, + ): + entry1 = MockConfigEntry( + domain=bluetooth.DOMAIN, + data={}, + title="Core Bluetooth", + unique_id=DEFAULT_ADDRESS, + ) + entry1.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry1.entry_id) + await hass.async_block_till_done() + + diag = await get_diagnostics_for_config_entry(hass, hass_client, entry1) + assert diag == { + "adapters": { + "Core Bluetooth": { + "address": "00:00:00:00:00:00", + "passive_scan": False, + "sw_version": ANY, + } + }, + "manager": { + "adapters": { + "Core Bluetooth": { + "address": "00:00:00:00:00:00", + "passive_scan": False, + "sw_version": ANY, + } + }, + "connectable_history": [], + "history": [], + "scanners": [ + { + "adapter": "Core Bluetooth", + "discovered_devices": [ + {"address": "44:44:33:11:23:45", "name": "x", "rssi": -60} + ], + "last_detection": ANY, + "name": "Core Bluetooth", + "source": "Core Bluetooth", + "start_time": ANY, + "type": "HaScanner", + } + ], + }, + } From a02eed9bd889735806780e257874b8e6a97b3ee6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 19 Sep 2022 19:59:07 -0500 Subject: [PATCH 576/955] Bump dbus-fast to 1.5.1 (#78802) --- 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 9c368ebf82a..1c4a8a2d2fc 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -9,7 +9,7 @@ "bleak-retry-connector==1.17.1", "bluetooth-adapters==0.5.1", "bluetooth-auto-recovery==0.3.3", - "dbus-fast==1.4.0" + "dbus-fast==1.5.1" ], "codeowners": ["@bdraco"], "config_flow": true, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 065bd8469c2..7aa9caf200b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ bluetooth-auto-recovery==0.3.3 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==37.0.4 -dbus-fast==1.4.0 +dbus-fast==1.5.1 fnvhash==0.1.0 hass-nabucasa==0.55.0 home-assistant-bluetooth==1.3.0 diff --git a/requirements_all.txt b/requirements_all.txt index 4aa1c5d07e1..67ef23cebdb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -535,7 +535,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.4.0 +dbus-fast==1.5.1 # homeassistant.components.debugpy debugpy==1.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a3ea1dbd719..1ab399f1112 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -412,7 +412,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.4.0 +dbus-fast==1.5.1 # homeassistant.components.debugpy debugpy==1.6.3 From caba202efab6816bc34324d674f7c2ec02d36286 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 19 Sep 2022 19:59:27 -0500 Subject: [PATCH 577/955] Fix failing bluetooth tests (#78757) --- tests/components/bluetooth/test_scanner.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/components/bluetooth/test_scanner.py b/tests/components/bluetooth/test_scanner.py index 2c1810d8338..e11d0c57837 100644 --- a/tests/components/bluetooth/test_scanner.py +++ b/tests/components/bluetooth/test_scanner.py @@ -1,4 +1,5 @@ """Tests for the Bluetooth integration scanners.""" +import time from unittest.mock import MagicMock, patch from bleak import BleakError @@ -203,7 +204,7 @@ async def test_recovery_from_dbus_restart(hass, one_adapter): assert called_start == 1 - start_time_monotonic = 1000 + start_time_monotonic = time.monotonic() scanner = _get_manager() mock_discovered = [MagicMock()] @@ -279,7 +280,7 @@ async def test_adapter_recovery(hass, one_adapter): _callback = callback scanner = MockBleakScanner() - start_time_monotonic = 1000 + start_time_monotonic = time.monotonic() with patch( "homeassistant.components.bluetooth.scanner.MONOTONIC_TIME", @@ -366,7 +367,7 @@ async def test_adapter_scanner_fails_to_start_first_time(hass, one_adapter): _callback = callback scanner = MockBleakScanner() - start_time_monotonic = 1000 + start_time_monotonic = time.monotonic() with patch( "homeassistant.components.bluetooth.scanner.MONOTONIC_TIME", @@ -471,7 +472,7 @@ async def test_adapter_fails_to_start_and_takes_a_bit_to_init( _callback = callback scanner = MockBleakScanner() - start_time_monotonic = 1000 + start_time_monotonic = time.monotonic() with patch( "homeassistant.components.bluetooth.scanner.ADAPTER_INIT_TIME", From 12856dea05ad48d0a8254079aa0ee10e6f7c3649 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 19 Sep 2022 15:02:13 -1000 Subject: [PATCH 578/955] Create an issue when Bluetooth is active on old HAOS (#78430) Co-authored-by: Paulus Schoutsen --- .../components/bluetooth/__init__.py | 60 ++++++++++++++++++- .../components/bluetooth/manifest.json | 1 + .../components/bluetooth/strings.json | 6 ++ .../components/bluetooth/translations/en.json | 13 ++-- tests/components/bluetooth/conftest.py | 55 +++++++++++++++++ tests/components/bluetooth/test_init.py | 49 +++++++++++++++ 6 files changed, 176 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 387615cdc29..24eab5c9a5c 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -8,14 +8,24 @@ import platform from typing import TYPE_CHECKING, cast import async_timeout +from awesomeversion import AwesomeVersion from homeassistant.components import usb -from homeassistant.config_entries import SOURCE_INTEGRATION_DISCOVERY, ConfigEntry -from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.config_entries import ( + SOURCE_IGNORE, + SOURCE_INTEGRATION_DISCOVERY, + ConfigEntry, +) +from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr, discovery_flow from homeassistant.helpers.debounce import Debouncer +from homeassistant.helpers.issue_registry import ( + IssueSeverity, + async_create_issue, + async_delete_issue, +) from homeassistant.loader import async_get_bluetooth from . import models @@ -71,6 +81,8 @@ __all__ = [ _LOGGER = logging.getLogger(__name__) +RECOMMENDED_MIN_HAOS_VERSION = AwesomeVersion("9.0.dev0") + def _get_manager(hass: HomeAssistant) -> BluetoothManager: """Get the bluetooth manager.""" @@ -223,6 +235,43 @@ async def async_get_adapter_from_address( return await _get_manager(hass).async_get_adapter_from_address(address) +@hass_callback +def _async_haos_is_new_enough(hass: HomeAssistant) -> bool: + """Check if the version of Home Assistant Operating System is new enough.""" + # Only warn if a USB adapter is plugged in + if not any( + entry + for entry in hass.config_entries.async_entries(DOMAIN) + if entry.source != SOURCE_IGNORE + ): + return True + if ( + not hass.components.hassio.is_hassio() + or not (os_info := hass.components.hassio.get_os_info()) + or not (haos_version := os_info.get("version")) + or AwesomeVersion(haos_version) >= RECOMMENDED_MIN_HAOS_VERSION + ): + return True + return False + + +@hass_callback +def _async_check_haos(hass: HomeAssistant) -> None: + """Create or delete an the haos_outdated issue.""" + if _async_haos_is_new_enough(hass): + async_delete_issue(hass, DOMAIN, "haos_outdated") + return + async_create_issue( + hass, + DOMAIN, + "haos_outdated", + is_fixable=False, + severity=IssueSeverity.WARNING, + learn_more_url="/config/updates", + translation_key="haos_outdated", + ) + + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the bluetooth integration.""" integration_matcher = IntegrationMatcher(await async_get_bluetooth(hass)) @@ -261,6 +310,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: EVENT_HOMEASSISTANT_STOP, hass_callback(lambda event: cancel()) ) + # Wait to check until after start to make sure + # that the system info is available. + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STARTED, + hass_callback(lambda event: _async_check_haos(hass)), + ) + return True diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 1c4a8a2d2fc..7a7cfbae007 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -3,6 +3,7 @@ "name": "Bluetooth", "documentation": "https://www.home-assistant.io/integrations/bluetooth", "dependencies": ["usb"], + "after_dependencies": ["hassio"], "quality_scale": "internal", "requirements": [ "bleak==0.17.0", diff --git a/homeassistant/components/bluetooth/strings.json b/homeassistant/components/bluetooth/strings.json index f838cd97798..cfde1b90cd8 100644 --- a/homeassistant/components/bluetooth/strings.json +++ b/homeassistant/components/bluetooth/strings.json @@ -1,4 +1,10 @@ { + "issues": { + "haos_outdated": { + "title": "Update to Home Assistant Operating System 9.0 or later", + "description": "To improve Bluetooth reliability and performance, we highly recommend you update to version 9.0 or later of the Home Assistant Operating System." + } + }, "config": { "flow_title": "{name}", "step": { diff --git a/homeassistant/components/bluetooth/translations/en.json b/homeassistant/components/bluetooth/translations/en.json index 7d76740602d..beefb842204 100644 --- a/homeassistant/components/bluetooth/translations/en.json +++ b/homeassistant/components/bluetooth/translations/en.json @@ -9,9 +9,6 @@ "bluetooth_confirm": { "description": "Do you want to setup {name}?" }, - "enable_bluetooth": { - "description": "Do you want to setup Bluetooth?" - }, "multiple_adapters": { "data": { "adapter": "Adapter" @@ -29,14 +26,18 @@ } } }, + "issues": { + "haos_outdated": { + "description": "To improve Bluetooth reliability and performance, we highly recommend you update to version 9.0 or later of the Home Assistant Operating System.", + "title": "Update to Home Assistant Operating System 9.0 or later" + } + }, "options": { "step": { "init": { "data": { - "adapter": "The Bluetooth Adapter to use for scanning", "passive": "Passive scanning" - }, - "description": "Passive listening requires BlueZ 5.63 or later with experimental features enabled." + } } } } diff --git a/tests/components/bluetooth/conftest.py b/tests/components/bluetooth/conftest.py index 2c0e2e6eb9c..4c78f063780 100644 --- a/tests/components/bluetooth/conftest.py +++ b/tests/components/bluetooth/conftest.py @@ -5,6 +5,44 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest +@pytest.fixture(name="operating_system_85") +def mock_operating_system_85(): + """Mock running Home Assistant Operating system 8.5.""" + with patch("homeassistant.components.hassio.is_hassio", return_value=True), patch( + "homeassistant.components.hassio.get_os_info", + return_value={ + "version": "8.5", + "version_latest": "10.0.dev20220912", + "update_available": False, + "board": "odroid-n2", + "boot": "B", + "data_disk": "/dev/mmcblk1p4", + }, + ), patch("homeassistant.components.hassio.get_info", return_value={}), patch( + "homeassistant.components.hassio.get_host_info", return_value={} + ): + yield + + +@pytest.fixture(name="operating_system_90") +def mock_operating_system_90(): + """Mock running Home Assistant Operating system 9.0.""" + with patch("homeassistant.components.hassio.is_hassio", return_value=True), patch( + "homeassistant.components.hassio.get_os_info", + return_value={ + "version": "9.0.dev20220912", + "version_latest": "10.0.dev20220912", + "update_available": False, + "board": "odroid-n2", + "boot": "B", + "data_disk": "/dev/mmcblk1p4", + }, + ), patch("homeassistant.components.hassio.get_info", return_value={}), patch( + "homeassistant.components.hassio.get_host_info", return_value={} + ): + yield + + @pytest.fixture(name="bluez_dbus_mock") def bluez_dbus_mock(): """Fixture that mocks out the bluez dbus calls.""" @@ -39,6 +77,23 @@ def windows_adapter(): yield +@pytest.fixture(name="no_adapters") +def no_adapter_fixture(bluez_dbus_mock): + """Fixture that mocks no adapters on Linux.""" + with patch( + "homeassistant.components.bluetooth.platform.system", return_value="Linux" + ), patch( + "homeassistant.components.bluetooth.scanner.platform.system", + return_value="Linux", + ), patch( + "homeassistant.components.bluetooth.util.platform.system", return_value="Linux" + ), patch( + "bluetooth_adapters.get_bluetooth_adapter_details", + return_value={}, + ): + yield + + @pytest.fixture(name="one_adapter") def one_adapter_fixture(bluez_dbus_mock): """Fixture that mocks one adapter on Linux.""" diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index a3045291286..7ee1a9840db 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -37,6 +37,7 @@ from homeassistant.components.bluetooth.match import ( from homeassistant.config_entries import ConfigEntryState from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.issue_registry import async_get as async_get_issue_registry from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util @@ -2680,3 +2681,51 @@ async def test_discover_new_usb_adapters(hass, mock_bleak_scanner_start, one_ada 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 +): + """Test we create an issue on outdated haos.""" + entry = MockConfigEntry( + domain=bluetooth.DOMAIN, data={}, unique_id="00:00:00:00:00:01" + ) + entry.add_to_hass(hass) + assert await async_setup_component(hass, bluetooth.DOMAIN, {}) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + registry = async_get_issue_registry(hass) + issue = registry.async_get_issue(DOMAIN, "haos_outdated") + assert issue is not None + + +async def test_issue_outdated_haos_no_adapters( + hass, mock_bleak_scanner_start, no_adapters, operating_system_85 +): + """Test we do not create an issue on outdated haos if there are no adapters.""" + assert await async_setup_component(hass, bluetooth.DOMAIN, {}) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + registry = async_get_issue_registry(hass) + issue = registry.async_get_issue(DOMAIN, "haos_outdated") + assert issue is None + + +async def test_haos_9_or_later( + hass, mock_bleak_scanner_start, one_adapter, operating_system_90 +): + """Test we do not create issues for haos 9.x or later.""" + entry = MockConfigEntry( + domain=bluetooth.DOMAIN, data={}, unique_id="00:00:00:00:00:01" + ) + entry.add_to_hass(hass) + assert await async_setup_component(hass, bluetooth.DOMAIN, {}) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + registry = async_get_issue_registry(hass) + issue = registry.async_get_issue(DOMAIN, "haos_outdated") + assert issue is None From bb78d52f349f9e7e81f8af166006cc5abebcc2d9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 19 Sep 2022 15:43:41 -1000 Subject: [PATCH 579/955] Add iBeacon Tracker integration (#78671) --- .strict-typing | 1 + CODEOWNERS | 2 + homeassistant/components/ibeacon/__init__.py | 24 ++ .../components/ibeacon/config_flow.py | 74 ++++ homeassistant/components/ibeacon/const.py | 33 ++ .../components/ibeacon/coordinator.py | 378 ++++++++++++++++++ .../components/ibeacon/device_tracker.py | 92 +++++ homeassistant/components/ibeacon/entity.py | 80 ++++ .../components/ibeacon/manifest.json | 12 + homeassistant/components/ibeacon/sensor.py | 134 +++++++ homeassistant/components/ibeacon/strings.json | 23 ++ .../components/ibeacon/translations/en.json | 23 ++ homeassistant/generated/bluetooth.py | 8 + homeassistant/generated/config_flows.py | 1 + mypy.ini | 10 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/ibeacon/__init__.py | 51 +++ tests/components/ibeacon/conftest.py | 1 + tests/components/ibeacon/test_config_flow.py | 67 ++++ tests/components/ibeacon/test_coordinator.py | 108 +++++ .../components/ibeacon/test_device_tracker.py | 132 ++++++ tests/components/ibeacon/test_sensor.py | 184 +++++++++ 23 files changed, 1444 insertions(+) create mode 100644 homeassistant/components/ibeacon/__init__.py create mode 100644 homeassistant/components/ibeacon/config_flow.py create mode 100644 homeassistant/components/ibeacon/const.py create mode 100644 homeassistant/components/ibeacon/coordinator.py create mode 100644 homeassistant/components/ibeacon/device_tracker.py create mode 100644 homeassistant/components/ibeacon/entity.py create mode 100644 homeassistant/components/ibeacon/manifest.json create mode 100644 homeassistant/components/ibeacon/sensor.py create mode 100644 homeassistant/components/ibeacon/strings.json create mode 100644 homeassistant/components/ibeacon/translations/en.json create mode 100644 tests/components/ibeacon/__init__.py create mode 100644 tests/components/ibeacon/conftest.py create mode 100644 tests/components/ibeacon/test_config_flow.py create mode 100644 tests/components/ibeacon/test_coordinator.py create mode 100644 tests/components/ibeacon/test_device_tracker.py create mode 100644 tests/components/ibeacon/test_sensor.py diff --git a/.strict-typing b/.strict-typing index 0905c17232b..c9dc14967cb 100644 --- a/.strict-typing +++ b/.strict-typing @@ -145,6 +145,7 @@ homeassistant.components.homewizard.* homeassistant.components.http.* homeassistant.components.huawei_lte.* homeassistant.components.hyperion.* +homeassistant.components.ibeacon.* homeassistant.components.image_processing.* homeassistant.components.input_button.* homeassistant.components.input_select.* diff --git a/CODEOWNERS b/CODEOWNERS index 314c43f418e..c087599baa4 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -508,6 +508,8 @@ build.json @home-assistant/supervisor /homeassistant/components/iammeter/ @lewei50 /homeassistant/components/iaqualink/ @flz /tests/components/iaqualink/ @flz +/homeassistant/components/ibeacon/ @bdraco +/tests/components/ibeacon/ @bdraco /homeassistant/components/icloud/ @Quentame @nzapponi /tests/components/icloud/ @Quentame @nzapponi /homeassistant/components/ign_sismologia/ @exxamalte diff --git a/homeassistant/components/ibeacon/__init__.py b/homeassistant/components/ibeacon/__init__.py new file mode 100644 index 00000000000..bf618c4ca12 --- /dev/null +++ b/homeassistant/components/ibeacon/__init__.py @@ -0,0 +1,24 @@ +"""The iBeacon tracker integration.""" +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import async_get + +from .const import DOMAIN, PLATFORMS +from .coordinator import IBeaconCoordinator + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Bluetooth LE Tracker from a config entry.""" + coordinator = hass.data[DOMAIN] = IBeaconCoordinator(hass, entry, async_get(hass)) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + coordinator.async_start() + 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.pop(DOMAIN) + return unload_ok diff --git a/homeassistant/components/ibeacon/config_flow.py b/homeassistant/components/ibeacon/config_flow.py new file mode 100644 index 00000000000..0cfe425f1f9 --- /dev/null +++ b/homeassistant/components/ibeacon/config_flow.py @@ -0,0 +1,74 @@ +"""Config flow for iBeacon Tracker integration.""" +from __future__ import annotations + +from typing import Any + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.components import bluetooth +from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.selector import ( + NumberSelector, + NumberSelectorConfig, + NumberSelectorMode, +) + +from .const import CONF_MIN_RSSI, DEFAULT_MIN_RSSI, DOMAIN + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for iBeacon Tracker.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + if not bluetooth.async_scanner_count(self.hass, connectable=False): + return self.async_abort(reason="bluetooth_not_available") + + if user_input is not None: + return self.async_create_entry(title="iBeacon Tracker", data={}) + + return self.async_show_form(step_id="user") + + @staticmethod + @callback + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> OptionsFlowHandler: + """Get the options flow for this handler.""" + return OptionsFlowHandler(config_entry) + + +class OptionsFlowHandler(config_entries.OptionsFlow): + """Handle a option flow for iBeacons.""" + + def __init__(self, entry: config_entries.ConfigEntry) -> None: + """Initialize options flow.""" + self.entry = entry + + async def async_step_init(self, user_input: dict | None = None) -> FlowResult: + """Handle options flow.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + data_schema = vol.Schema( + { + vol.Required( + CONF_MIN_RSSI, + default=self.entry.options.get(CONF_MIN_RSSI) or DEFAULT_MIN_RSSI, + ): NumberSelector( + NumberSelectorConfig( + min=-120, max=-30, step=1, mode=NumberSelectorMode.SLIDER + ) + ), + } + ) + return self.async_show_form(step_id="init", data_schema=data_schema) diff --git a/homeassistant/components/ibeacon/const.py b/homeassistant/components/ibeacon/const.py new file mode 100644 index 00000000000..2a8b760c8d3 --- /dev/null +++ b/homeassistant/components/ibeacon/const.py @@ -0,0 +1,33 @@ +"""Constants for the iBeacon Tracker integration.""" + +from datetime import timedelta + +from homeassistant.const import Platform + +DOMAIN = "ibeacon" + +PLATFORMS = [Platform.DEVICE_TRACKER, Platform.SENSOR] + +SIGNAL_IBEACON_DEVICE_NEW = "ibeacon_tracker_new_device" +SIGNAL_IBEACON_DEVICE_UNAVAILABLE = "ibeacon_tracker_unavailable_device" +SIGNAL_IBEACON_DEVICE_SEEN = "ibeacon_seen_device" + +ATTR_UUID = "uuid" +ATTR_MAJOR = "major" +ATTR_MINOR = "minor" +ATTR_SOURCE = "source" + +UNAVAILABLE_TIMEOUT = 180 # Number of seconds we wait for a beacon to be seen before marking it unavailable + +# How often to update RSSI if it has changed +# and look for unavailable groups that use a random MAC address +UPDATE_INTERVAL = timedelta(seconds=60) + +# If a device broadcasts this many unique ids from the same address +# we will add it to the ignore list since its garbage data. +MAX_IDS = 10 + +CONF_IGNORE_ADDRESSES = "ignore_addresses" + +CONF_MIN_RSSI = "min_rssi" +DEFAULT_MIN_RSSI = -85 diff --git a/homeassistant/components/ibeacon/coordinator.py b/homeassistant/components/ibeacon/coordinator.py new file mode 100644 index 00000000000..8c256e5a5f0 --- /dev/null +++ b/homeassistant/components/ibeacon/coordinator.py @@ -0,0 +1,378 @@ +"""Tracking for iBeacon devices.""" +from __future__ import annotations + +from datetime import datetime +import time + +from ibeacon_ble import ( + APPLE_MFR_ID, + IBEACON_FIRST_BYTE, + IBEACON_SECOND_BYTE, + iBeaconAdvertisement, + is_ibeacon_service_info, + parse, +) + +from homeassistant.components import bluetooth +from homeassistant.components.bluetooth.match import BluetoothCallbackMatcher +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.helpers.device_registry import DeviceRegistry +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.event import async_track_time_interval + +from .const import ( + CONF_IGNORE_ADDRESSES, + CONF_MIN_RSSI, + DEFAULT_MIN_RSSI, + DOMAIN, + MAX_IDS, + SIGNAL_IBEACON_DEVICE_NEW, + SIGNAL_IBEACON_DEVICE_SEEN, + SIGNAL_IBEACON_DEVICE_UNAVAILABLE, + UNAVAILABLE_TIMEOUT, + UPDATE_INTERVAL, +) + +MONOTONIC_TIME = time.monotonic + + +def signal_unavailable(unique_id: str) -> str: + """Signal for the unique_id going unavailable.""" + return f"{SIGNAL_IBEACON_DEVICE_UNAVAILABLE}_{unique_id}" + + +def signal_seen(unique_id: str) -> str: + """Signal for the unique_id being seen.""" + return f"{SIGNAL_IBEACON_DEVICE_SEEN}_{unique_id}" + + +def make_short_address(address: str) -> str: + """Convert a Bluetooth address to a short address.""" + results = address.replace("-", ":").split(":") + return f"{results[-2].upper()}{results[-1].upper()}"[-4:] + + +@callback +def async_name( + service_info: bluetooth.BluetoothServiceInfoBleak, + parsed: iBeaconAdvertisement, + unique_address: bool = False, +) -> str: + """Return a name for the device.""" + if service_info.address in ( + service_info.name, + service_info.name.replace("_", ":"), + ): + base_name = f"{parsed.uuid} {parsed.major}.{parsed.minor}" + else: + base_name = service_info.name + if unique_address: + short_address = make_short_address(service_info.address) + if not base_name.endswith(short_address): + return f"{base_name} {short_address}" + return base_name + + +@callback +def _async_dispatch_update( + hass: HomeAssistant, + device_id: str, + service_info: bluetooth.BluetoothServiceInfoBleak, + parsed: iBeaconAdvertisement, + new: bool, + unique_address: bool, +) -> None: + """Dispatch an update.""" + if new: + async_dispatcher_send( + hass, + SIGNAL_IBEACON_DEVICE_NEW, + device_id, + async_name(service_info, parsed, unique_address), + parsed, + ) + return + + async_dispatcher_send( + hass, + signal_seen(device_id), + parsed, + ) + + +class IBeaconCoordinator: + """Set up the iBeacon Coordinator.""" + + def __init__( + self, hass: HomeAssistant, entry: ConfigEntry, registry: DeviceRegistry + ) -> None: + """Initialize the Coordinator.""" + self.hass = hass + self._entry = entry + self._min_rssi = entry.options.get(CONF_MIN_RSSI) or DEFAULT_MIN_RSSI + self._dev_reg = registry + + # iBeacon devices that do not follow the spec + # and broadcast custom data in the major and minor fields + self._ignore_addresses: set[str] = set( + entry.data.get(CONF_IGNORE_ADDRESSES, []) + ) + + # iBeacons with fixed MAC addresses + self._last_rssi_by_unique_id: dict[str, int] = {} + self._group_ids_by_address: dict[str, set[str]] = {} + self._unique_ids_by_address: dict[str, set[str]] = {} + self._unique_ids_by_group_id: dict[str, set[str]] = {} + self._addresses_by_group_id: dict[str, set[str]] = {} + self._unavailable_trackers: dict[str, CALLBACK_TYPE] = {} + + # iBeacon with random MAC addresses + self._group_ids_random_macs: set[str] = set() + self._last_seen_by_group_id: dict[str, bluetooth.BluetoothServiceInfoBleak] = {} + self._unavailable_group_ids: set[str] = set() + + @callback + def _async_handle_unavailable( + self, service_info: bluetooth.BluetoothServiceInfoBleak + ) -> None: + """Handle unavailable devices.""" + address = service_info.address + self._async_cancel_unavailable_tracker(address) + for unique_id in self._unique_ids_by_address[address]: + async_dispatcher_send(self.hass, signal_unavailable(unique_id)) + + @callback + def _async_cancel_unavailable_tracker(self, address: str) -> None: + """Cancel unavailable tracking for an address.""" + self._unavailable_trackers.pop(address)() + + @callback + def _async_ignore_address(self, address: str) -> None: + """Ignore an address that does not follow the spec and any entities created by it.""" + self._ignore_addresses.add(address) + self._async_cancel_unavailable_tracker(address) + self.hass.config_entries.async_update_entry( + self._entry, + data=self._entry.data + | {CONF_IGNORE_ADDRESSES: sorted(self._ignore_addresses)}, + ) + self._async_purge_untrackable_entities(self._unique_ids_by_address[address]) + self._group_ids_by_address.pop(address) + self._unique_ids_by_address.pop(address) + + @callback + def _async_purge_untrackable_entities(self, unique_ids: set[str]) -> None: + """Remove entities that are no longer trackable.""" + for unique_id in unique_ids: + if device := self._dev_reg.async_get_device({(DOMAIN, unique_id)}): + self._dev_reg.async_remove_device(device.id) + self._last_rssi_by_unique_id.pop(unique_id, None) + + @callback + def _async_convert_random_mac_tracking( + self, + group_id: str, + service_info: bluetooth.BluetoothServiceInfoBleak, + parsed: iBeaconAdvertisement, + ) -> None: + """Switch to random mac tracking method when a group is using rotating mac addresses.""" + self._group_ids_random_macs.add(group_id) + self._async_purge_untrackable_entities(self._unique_ids_by_group_id[group_id]) + self._unique_ids_by_group_id.pop(group_id) + self._addresses_by_group_id.pop(group_id) + self._async_update_ibeacon_with_random_mac(group_id, service_info, parsed) + + def _async_track_ibeacon_with_unique_address( + self, address: str, group_id: str, unique_id: str + ) -> None: + """Track an iBeacon with a unique address.""" + self._unique_ids_by_address.setdefault(address, set()).add(unique_id) + self._group_ids_by_address.setdefault(address, set()).add(group_id) + + self._unique_ids_by_group_id.setdefault(group_id, set()).add(unique_id) + self._addresses_by_group_id.setdefault(group_id, set()).add(address) + + @callback + def _async_update_ibeacon( + self, + service_info: bluetooth.BluetoothServiceInfoBleak, + change: bluetooth.BluetoothChange, + ) -> None: + """Update from a bluetooth callback.""" + if service_info.address in self._ignore_addresses: + return + if service_info.rssi < self._min_rssi: + return + if not (parsed := parse(service_info)): + return + group_id = f"{parsed.uuid}_{parsed.major}_{parsed.minor}" + + if group_id in self._group_ids_random_macs: + self._async_update_ibeacon_with_random_mac(group_id, service_info, parsed) + return + + self._async_update_ibeacon_with_unique_address(group_id, service_info, parsed) + + @callback + def _async_update_ibeacon_with_random_mac( + self, + group_id: str, + service_info: bluetooth.BluetoothServiceInfoBleak, + parsed: iBeaconAdvertisement, + ) -> None: + """Update iBeacons with random mac addresses.""" + new = group_id not in self._last_seen_by_group_id + self._last_seen_by_group_id[group_id] = service_info + self._unavailable_group_ids.discard(group_id) + _async_dispatch_update(self.hass, group_id, service_info, parsed, new, False) + + @callback + def _async_update_ibeacon_with_unique_address( + self, + group_id: str, + service_info: bluetooth.BluetoothServiceInfoBleak, + parsed: iBeaconAdvertisement, + ) -> None: + # Handle iBeacon with a fixed mac address + # and or detect if the iBeacon is using a rotating mac address + # and switch to random mac tracking method + address = service_info.address + unique_id = f"{group_id}_{address}" + new = unique_id not in self._last_rssi_by_unique_id + self._last_rssi_by_unique_id[unique_id] = service_info.rssi + self._async_track_ibeacon_with_unique_address(address, group_id, unique_id) + if address not in self._unavailable_trackers: + self._unavailable_trackers[address] = bluetooth.async_track_unavailable( + self.hass, self._async_handle_unavailable, address + ) + # Some manufacturers violate the spec and flood us with random + # data (sometimes its temperature data). + # + # Once we see more than MAX_IDS from the same + # address we remove all the trackers for that address and add the + # address to the ignore list since we know its garbage data. + if len(self._group_ids_by_address[address]) >= MAX_IDS: + self._async_ignore_address(address) + return + + # Once we see more than MAX_IDS from the same + # group_id we remove all the trackers for that group_id + # as it means the addresses are being rotated. + if len(self._addresses_by_group_id[group_id]) >= MAX_IDS: + self._async_convert_random_mac_tracking(group_id, service_info, parsed) + return + + _async_dispatch_update(self.hass, unique_id, service_info, parsed, new, True) + + @callback + def _async_stop(self) -> None: + """Stop the Coordinator.""" + for cancel in self._unavailable_trackers.values(): + cancel() + self._unavailable_trackers.clear() + + async def _entry_updated(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + """Handle options update.""" + self._min_rssi = entry.options.get(CONF_MIN_RSSI) or DEFAULT_MIN_RSSI + + @callback + def _async_check_unavailable_groups_with_random_macs(self) -> None: + """Check for random mac groups that have not been seen in a while and mark them as unavailable.""" + now = MONOTONIC_TIME() + gone_unavailable = [ + group_id + for group_id in self._group_ids_random_macs + if group_id not in self._unavailable_group_ids + and (service_info := self._last_seen_by_group_id.get(group_id)) + and now - service_info.time > UNAVAILABLE_TIMEOUT + ] + for group_id in gone_unavailable: + self._unavailable_group_ids.add(group_id) + async_dispatcher_send(self.hass, signal_unavailable(group_id)) + + @callback + def _async_update_rssi(self) -> None: + """Check to see if the rssi has changed and update any devices. + + We don't callback on RSSI changes so we need to check them + here and send them over the dispatcher periodically to + ensure the distance calculation is update. + """ + for unique_id, rssi in self._last_rssi_by_unique_id.items(): + address = unique_id.split("_")[-1] + if ( + ( + service_info := bluetooth.async_last_service_info( + self.hass, address, connectable=False + ) + ) + and service_info.rssi != rssi + and (parsed := parse(service_info)) + ): + async_dispatcher_send( + self.hass, + signal_seen(unique_id), + parsed, + ) + + @callback + def _async_update(self, _now: datetime) -> None: + """Update the Coordinator.""" + self._async_check_unavailable_groups_with_random_macs() + self._async_update_rssi() + + @callback + def _async_restore_from_registry(self) -> None: + """Restore the state of the Coordinator from the device registry.""" + for device in self._dev_reg.devices.values(): + unique_id = None + for identifier in device.identifiers: + if identifier[0] == DOMAIN: + unique_id = identifier[1] + break + if not unique_id: + continue + # iBeacons with a fixed MAC address + if unique_id.count("_") == 3: + uuid, major, minor, address = unique_id.split("_") + group_id = f"{uuid}_{major}_{minor}" + self._async_track_ibeacon_with_unique_address( + address, group_id, unique_id + ) + # iBeacons with a random MAC address + elif unique_id.count("_") == 2: + uuid, major, minor = unique_id.split("_") + group_id = f"{uuid}_{major}_{minor}" + self._group_ids_random_macs.add(group_id) + + @callback + def async_start(self) -> None: + """Start the Coordinator.""" + self._async_restore_from_registry() + entry = self._entry + entry.async_on_unload(entry.add_update_listener(self._entry_updated)) + entry.async_on_unload( + bluetooth.async_register_callback( + self.hass, + self._async_update_ibeacon, + BluetoothCallbackMatcher( + connectable=False, + manufacturer_id=APPLE_MFR_ID, + manufacturer_data_start=[IBEACON_FIRST_BYTE, IBEACON_SECOND_BYTE], + ), # We will take data from any source + bluetooth.BluetoothScanningMode.PASSIVE, + ) + ) + entry.async_on_unload(self._async_stop) + # Replay any that are already there. + for service_info in bluetooth.async_discovered_service_info( + self.hass, connectable=False + ): + if is_ibeacon_service_info(service_info): + self._async_update_ibeacon( + service_info, bluetooth.BluetoothChange.ADVERTISEMENT + ) + entry.async_on_unload( + async_track_time_interval(self.hass, self._async_update, UPDATE_INTERVAL) + ) diff --git a/homeassistant/components/ibeacon/device_tracker.py b/homeassistant/components/ibeacon/device_tracker.py new file mode 100644 index 00000000000..e2db3bd291f --- /dev/null +++ b/homeassistant/components/ibeacon/device_tracker.py @@ -0,0 +1,92 @@ +"""Support for tracking iBeacon devices.""" +from __future__ import annotations + +from ibeacon_ble import iBeaconAdvertisement + +from homeassistant.components.device_tracker import SourceType +from homeassistant.components.device_tracker.config_entry import BaseTrackerEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import STATE_HOME, STATE_NOT_HOME +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN, SIGNAL_IBEACON_DEVICE_NEW +from .coordinator import IBeaconCoordinator +from .entity import IBeaconEntity + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up device tracker for iBeacon Tracker component.""" + coordinator: IBeaconCoordinator = hass.data[DOMAIN] + + @callback + def _async_device_new( + unique_id: str, + identifier: str, + parsed: iBeaconAdvertisement, + ) -> None: + """Signal a new device.""" + async_add_entities( + [ + IBeaconTrackerEntity( + coordinator, + identifier, + unique_id, + parsed, + ) + ] + ) + + entry.async_on_unload( + async_dispatcher_connect(hass, SIGNAL_IBEACON_DEVICE_NEW, _async_device_new) + ) + + +class IBeaconTrackerEntity(IBeaconEntity, BaseTrackerEntity): + """An iBeacon Tracker entity.""" + + def __init__( + self, + coordinator: IBeaconCoordinator, + identifier: str, + device_unique_id: str, + parsed: iBeaconAdvertisement, + ) -> None: + """Initialize an iBeacon tracker entity.""" + super().__init__(coordinator, identifier, device_unique_id, parsed) + self._attr_unique_id = device_unique_id + self._active = True + + @property + def state(self) -> str: + """Return the state of the device.""" + return STATE_HOME if self._active else STATE_NOT_HOME + + @property + def source_type(self) -> SourceType: + """Return tracker source type.""" + return SourceType.BLUETOOTH_LE + + @property + def icon(self) -> str: + """Return device icon.""" + return "mdi:bluetooth-connect" if self._active else "mdi:bluetooth-off" + + @callback + def _async_seen( + self, + parsed: iBeaconAdvertisement, + ) -> None: + """Update state.""" + self._active = True + self._parsed = parsed + self.async_write_ha_state() + + @callback + def _async_unavailable(self) -> None: + """Set unavailable.""" + self._active = False + self.async_write_ha_state() diff --git a/homeassistant/components/ibeacon/entity.py b/homeassistant/components/ibeacon/entity.py new file mode 100644 index 00000000000..3ce64fb8535 --- /dev/null +++ b/homeassistant/components/ibeacon/entity.py @@ -0,0 +1,80 @@ +"""Support for iBeacon device sensors.""" +from __future__ import annotations + +from abc import abstractmethod + +from ibeacon_ble import iBeaconAdvertisement + +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo, Entity + +from .const import ATTR_MAJOR, ATTR_MINOR, ATTR_SOURCE, ATTR_UUID, DOMAIN +from .coordinator import IBeaconCoordinator, signal_seen, signal_unavailable + + +class IBeaconEntity(Entity): + """An iBeacon entity.""" + + _attr_should_poll = False + _attr_has_entity_name = True + + def __init__( + self, + coordinator: IBeaconCoordinator, + identifier: str, + device_unique_id: str, + parsed: iBeaconAdvertisement, + ) -> None: + """Initialize an iBeacon sensor entity.""" + self._device_unique_id = device_unique_id + self._coordinator = coordinator + self._parsed = parsed + self._attr_device_info = DeviceInfo( + name=identifier, + identifiers={(DOMAIN, device_unique_id)}, + ) + + @property + def extra_state_attributes( + self, + ) -> dict[str, str | int]: + """Return the device state attributes.""" + parsed = self._parsed + return { + ATTR_UUID: str(parsed.uuid), + ATTR_MAJOR: parsed.major, + ATTR_MINOR: parsed.minor, + ATTR_SOURCE: parsed.source, + } + + @abstractmethod + @callback + def _async_seen( + self, + parsed: iBeaconAdvertisement, + ) -> None: + """Update state.""" + + @abstractmethod + @callback + def _async_unavailable(self) -> None: + """Set unavailable.""" + + async def async_added_to_hass(self) -> None: + """Register state update callbacks.""" + await super().async_added_to_hass() + self.async_on_remove( + async_dispatcher_connect( + self.hass, + signal_seen(self._device_unique_id), + self._async_seen, + ) + ) + self.async_on_remove( + async_dispatcher_connect( + self.hass, + signal_unavailable(self._device_unique_id), + self._async_unavailable, + ) + ) diff --git a/homeassistant/components/ibeacon/manifest.json b/homeassistant/components/ibeacon/manifest.json new file mode 100644 index 00000000000..531daed00f8 --- /dev/null +++ b/homeassistant/components/ibeacon/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "ibeacon", + "name": "iBeacon Tracker", + "documentation": "https://www.home-assistant.io/integrations/ibeacon", + "dependencies": ["bluetooth"], + "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [2, 21] }], + "requirements": ["ibeacon_ble==0.6.4"], + "codeowners": ["@bdraco"], + "iot_class": "local_push", + "loggers": ["bleak"], + "config_flow": true +} diff --git a/homeassistant/components/ibeacon/sensor.py b/homeassistant/components/ibeacon/sensor.py new file mode 100644 index 00000000000..d3468fbc3dc --- /dev/null +++ b/homeassistant/components/ibeacon/sensor.py @@ -0,0 +1,134 @@ +"""Support for iBeacon device sensors.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass + +from ibeacon_ble import iBeaconAdvertisement + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import LENGTH_METERS, SIGNAL_STRENGTH_DECIBELS_MILLIWATT +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN, SIGNAL_IBEACON_DEVICE_NEW +from .coordinator import IBeaconCoordinator +from .entity import IBeaconEntity + + +@dataclass +class IBeaconRequiredKeysMixin: + """Mixin for required keys.""" + + value_fn: Callable[[iBeaconAdvertisement], int | None] + + +@dataclass +class IBeaconSensorEntityDescription(SensorEntityDescription, IBeaconRequiredKeysMixin): + """Describes iBeacon sensor entity.""" + + +SENSOR_DESCRIPTIONS = ( + IBeaconSensorEntityDescription( + key="rssi", + name="Signal Strength", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + entity_registry_enabled_default=False, + value_fn=lambda parsed: parsed.rssi, + state_class=SensorStateClass.MEASUREMENT, + ), + IBeaconSensorEntityDescription( + key="power", + name="Power", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + entity_registry_enabled_default=False, + value_fn=lambda parsed: parsed.power, + state_class=SensorStateClass.MEASUREMENT, + ), + IBeaconSensorEntityDescription( + key="estimated_distance", + name="Estimated Distance", + icon="mdi:signal-distance-variant", + native_unit_of_measurement=LENGTH_METERS, + value_fn=lambda parsed: parsed.distance, + state_class=SensorStateClass.MEASUREMENT, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up sensors for iBeacon Tracker component.""" + coordinator: IBeaconCoordinator = hass.data[DOMAIN] + + @callback + def _async_device_new( + unique_id: str, + identifier: str, + parsed: iBeaconAdvertisement, + ) -> None: + """Signal a new device.""" + async_add_entities( + IBeaconSensorEntity( + coordinator, + description, + identifier, + unique_id, + parsed, + ) + for description in SENSOR_DESCRIPTIONS + ) + + entry.async_on_unload( + async_dispatcher_connect(hass, SIGNAL_IBEACON_DEVICE_NEW, _async_device_new) + ) + + +class IBeaconSensorEntity(IBeaconEntity, SensorEntity): + """An iBeacon sensor entity.""" + + entity_description: IBeaconSensorEntityDescription + + def __init__( + self, + coordinator: IBeaconCoordinator, + description: IBeaconSensorEntityDescription, + identifier: str, + device_unique_id: str, + parsed: iBeaconAdvertisement, + ) -> None: + """Initialize an iBeacon sensor entity.""" + super().__init__(coordinator, identifier, device_unique_id, parsed) + self._attr_unique_id = f"{device_unique_id}_{description.key}" + self.entity_description = description + + @callback + def _async_seen( + self, + parsed: iBeaconAdvertisement, + ) -> None: + """Update state.""" + self._attr_available = True + self._parsed = parsed + self.async_write_ha_state() + + @callback + def _async_unavailable(self) -> None: + """Update state.""" + self._attr_available = False + self.async_write_ha_state() + + @property + def native_value(self) -> int | None: + """Return the state of the sensor.""" + return self.entity_description.value_fn(self._parsed) diff --git a/homeassistant/components/ibeacon/strings.json b/homeassistant/components/ibeacon/strings.json new file mode 100644 index 00000000000..e2a1ab8393f --- /dev/null +++ b/homeassistant/components/ibeacon/strings.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "description": "Do you want to setup iBeacon Tracker?" + } + }, + "abort": { + "bluetooth_not_available": "At least one Bluetooth adapter or remote must be configured to use iBeacon Tracker.", + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" + } + }, + "options": { + "step": { + "init": { + "description": "iBeacons with an RSSI value lower than the Minimum RSSI will be ignored. If the integration is seeing neighboring iBeacons, increasing this value may help.", + "data": { + "min_rssi": "Minimum RSSI" + } + } + } + } +} diff --git a/homeassistant/components/ibeacon/translations/en.json b/homeassistant/components/ibeacon/translations/en.json new file mode 100644 index 00000000000..1125e778b19 --- /dev/null +++ b/homeassistant/components/ibeacon/translations/en.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "bluetooth_not_available": "At least one Bluetooth adapter or remote must be configured to use iBeacon Tracker.", + "single_instance_allowed": "Already configured. Only a single configuration possible." + }, + "step": { + "user": { + "description": "Do you want to setup iBeacon Tracker?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "min_rssi": "Minimum RSSI" + }, + "description": "iBeacons with an RSSI value lower than the Minimum RSSI will be ignored. If the integration is seeing neighboring iBeacons, increasing this value may help." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index a97f50f2e5f..ed98af896ff 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -115,6 +115,14 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ 6, ], }, + { + "domain": "ibeacon", + "manufacturer_id": 76, + "manufacturer_data_start": [ + 2, + 21, + ], + }, { "domain": "inkbird", "local_name": "sps", diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 81f7f91afb6..208a7e02efc 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -170,6 +170,7 @@ FLOWS = { "hyperion", "ialarm", "iaqualink", + "ibeacon", "icloud", "ifttt", "inkbird", diff --git a/mypy.ini b/mypy.ini index 9308ce22c52..70f45c24f62 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1202,6 +1202,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.ibeacon.*] +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 diff --git a/requirements_all.txt b/requirements_all.txt index 67ef23cebdb..6a12a38fdc5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -892,6 +892,9 @@ iammeter==0.1.7 # homeassistant.components.iaqualink iaqualink==0.4.1 +# homeassistant.components.ibeacon +ibeacon_ble==0.6.4 + # homeassistant.components.watson_tts ibm-watson==5.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1ab399f1112..d60c9146079 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -657,6 +657,9 @@ hyperion-py==0.7.5 # homeassistant.components.iaqualink iaqualink==0.4.1 +# homeassistant.components.ibeacon +ibeacon_ble==0.6.4 + # homeassistant.components.ping icmplib==3.0 diff --git a/tests/components/ibeacon/__init__.py b/tests/components/ibeacon/__init__.py new file mode 100644 index 00000000000..f9b2c1576ad --- /dev/null +++ b/tests/components/ibeacon/__init__.py @@ -0,0 +1,51 @@ +"""Tests for the ibeacon integration.""" +from bleak.backends.device import BLEDevice + +from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo + +BLUECHARM_BLE_DEVICE = BLEDevice( + address="61DE521B-F0BF-9F44-64D4-75BBE1738105", + name="BlueCharm_177999", +) +BLUECHARM_BEACON_SERVICE_INFO = BluetoothServiceInfo( + name="BlueCharm_177999", + address="61DE521B-F0BF-9F44-64D4-75BBE1738105", + rssi=-63, + service_data={}, + manufacturer_data={76: b"\x02\x15BlueCharmBeacons\x0e\xfe\x13U\xc5"}, + service_uuids=[], + source="local", +) +BLUECHARM_BEACON_SERVICE_INFO_2 = BluetoothServiceInfo( + name="BlueCharm_177999", + address="61DE521B-F0BF-9F44-64D4-75BBE1738105", + rssi=-53, + manufacturer_data={76: b"\x02\x15BlueCharmBeacons\x0e\xfe\x13U\xc5"}, + service_data={ + "00002080-0000-1000-8000-00805f9b34fb": b"j\x0c\x0e\xfe\x13U", + "0000feaa-0000-1000-8000-00805f9b34fb": b" \x00\x0c\x00\x1c\x00\x00\x00\x06h\x00\x008\x10", + }, + service_uuids=["0000feaa-0000-1000-8000-00805f9b34fb"], + source="local", +) +NO_NAME_BEACON_SERVICE_INFO = BluetoothServiceInfo( + name="61DE521B-F0BF-9F44-64D4-75BBE1738105", + address="61DE521B-F0BF-9F44-64D4-75BBE1738105", + rssi=-53, + manufacturer_data={76: b"\x02\x15NoNamearmBeacons\x0e\xfe\x13U\xc5"}, + service_data={ + "00002080-0000-1000-8000-00805f9b34fb": b"j\x0c\x0e\xfe\x13U", + "0000feaa-0000-1000-8000-00805f9b34fb": b" \x00\x0c\x00\x1c\x00\x00\x00\x06h\x00\x008\x10", + }, + service_uuids=["0000feaa-0000-1000-8000-00805f9b34fb"], + source="local", +) +BEACON_RANDOM_ADDRESS_SERVICE_INFO = BluetoothServiceInfo( + name="RandomAddress_1234", + address="AA:BB:CC:DD:EE:00", + rssi=-63, + service_data={}, + manufacturer_data={76: b"\x02\x15RandCharmBeacons\x0e\xfe\x13U\xc5"}, + service_uuids=[], + source="local", +) diff --git a/tests/components/ibeacon/conftest.py b/tests/components/ibeacon/conftest.py new file mode 100644 index 00000000000..655c74ec488 --- /dev/null +++ b/tests/components/ibeacon/conftest.py @@ -0,0 +1 @@ +"""ibeacon session fixtures.""" diff --git a/tests/components/ibeacon/test_config_flow.py b/tests/components/ibeacon/test_config_flow.py new file mode 100644 index 00000000000..bab465e5d75 --- /dev/null +++ b/tests/components/ibeacon/test_config_flow.py @@ -0,0 +1,67 @@ +"""Test the ibeacon config flow.""" + +from unittest.mock import patch + +from homeassistant import config_entries +from homeassistant.components.ibeacon.const import CONF_MIN_RSSI, DOMAIN +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry + + +async def test_setup_user_no_bluetooth(hass, mock_bluetooth_adapters): + """Test setting up via user interaction when bluetooth is not enabled.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "bluetooth_not_available" + + +async def test_setup_user(hass, enable_bluetooth): + """Test setting up via user interaction with bluetooth enabled.""" + 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.ibeacon.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"] == "iBeacon Tracker" + assert result2["data"] == {} + + +async def test_setup_user_already_setup(hass, enable_bluetooth): + """Test setting up via user when already setup .""" + MockConfigEntry(domain=DOMAIN).add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "single_instance_allowed" + + +async def test_options_flow(hass, enable_bluetooth): + """Test setting up via user when already setup .""" + entry = MockConfigEntry(domain=DOMAIN) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + + result = await hass.config_entries.options.async_init(entry.entry_id) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "init" + + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={CONF_MIN_RSSI: -70} + ) + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["data"] == {CONF_MIN_RSSI: -70} diff --git a/tests/components/ibeacon/test_coordinator.py b/tests/components/ibeacon/test_coordinator.py new file mode 100644 index 00000000000..d8981de8fa9 --- /dev/null +++ b/tests/components/ibeacon/test_coordinator.py @@ -0,0 +1,108 @@ +"""Test the ibeacon sensors.""" + + +from dataclasses import replace + +import pytest + +from homeassistant.components.ibeacon.const import CONF_MIN_RSSI, DOMAIN +from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo + +from . import BLUECHARM_BEACON_SERVICE_INFO + +from tests.common import MockConfigEntry +from tests.components.bluetooth import inject_bluetooth_service_info + + +@pytest.fixture(autouse=True) +def mock_bluetooth(enable_bluetooth): + """Auto mock bluetooth.""" + + +async def test_many_groups_same_address_ignored(hass): + """Test the different uuid, major, minor from many addresses removes all associated entities.""" + entry = MockConfigEntry( + domain=DOMAIN, + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + inject_bluetooth_service_info(hass, BLUECHARM_BEACON_SERVICE_INFO) + await hass.async_block_till_done() + + assert ( + hass.states.get("sensor.bluecharm_177999_8105_estimated_distance") is not None + ) + + for i in range(12): + service_info = BluetoothServiceInfo( + name="BlueCharm_177999", + address="61DE521B-F0BF-9F44-64D4-75BBE1738105", + rssi=-63, + service_data={}, + manufacturer_data={ + 76: b"\x02\x15BlueCharmBeacons" + bytearray([i]) + b"\xfe\x13U\xc5" + }, + service_uuids=[], + source="local", + ) + inject_bluetooth_service_info(hass, service_info) + + await hass.async_block_till_done() + assert hass.states.get("sensor.bluecharm_177999_8105_estimated_distance") is None + + +async def test_ignore_anything_less_than_min_rssi(hass): + """Test entities are not created when below the min rssi.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_MIN_RSSI: -60}, + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + inject_bluetooth_service_info( + hass, replace(BLUECHARM_BEACON_SERVICE_INFO, rssi=-100) + ) + await hass.async_block_till_done() + + assert hass.states.get("sensor.bluecharm_177999_8105_estimated_distance") is None + + inject_bluetooth_service_info( + hass, + replace( + BLUECHARM_BEACON_SERVICE_INFO, + rssi=-10, + service_uuids=["0000180f-0000-1000-8000-00805f9b34fb"], + ), + ) + await hass.async_block_till_done() + + assert ( + hass.states.get("sensor.bluecharm_177999_8105_estimated_distance") is not None + ) + + +async def test_ignore_not_ibeacons(hass): + """Test we ignore non-ibeacon data.""" + entry = MockConfigEntry( + domain=DOMAIN, + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + before_entity_count = len(hass.states.async_entity_ids()) + inject_bluetooth_service_info( + hass, + replace( + BLUECHARM_BEACON_SERVICE_INFO, manufacturer_data={76: b"\x02\x15invalid"} + ), + ) + await hass.async_block_till_done() + assert len(hass.states.async_entity_ids()) == before_entity_count diff --git a/tests/components/ibeacon/test_device_tracker.py b/tests/components/ibeacon/test_device_tracker.py new file mode 100644 index 00000000000..f3520f835ca --- /dev/null +++ b/tests/components/ibeacon/test_device_tracker.py @@ -0,0 +1,132 @@ +"""Test the ibeacon device trackers.""" + + +from dataclasses import replace +from datetime import timedelta +import time +from unittest.mock import patch + +import pytest + +from homeassistant.components.bluetooth.const import UNAVAILABLE_TRACK_SECONDS +from homeassistant.components.ibeacon.const import DOMAIN, UNAVAILABLE_TIMEOUT +from homeassistant.const import ( + ATTR_FRIENDLY_NAME, + STATE_HOME, + STATE_NOT_HOME, + STATE_UNAVAILABLE, +) +from homeassistant.util import dt as dt_util + +from . import ( + BEACON_RANDOM_ADDRESS_SERVICE_INFO, + BLUECHARM_BEACON_SERVICE_INFO, + BLUECHARM_BLE_DEVICE, +) + +from tests.common import MockConfigEntry, async_fire_time_changed +from tests.components.bluetooth import ( + inject_bluetooth_service_info, + patch_all_discovered_devices, +) + + +@pytest.fixture(autouse=True) +def mock_bluetooth(enable_bluetooth): + """Auto mock bluetooth.""" + + +async def test_device_tracker_fixed_address(hass): + """Test creating and updating device_tracker.""" + entry = MockConfigEntry( + domain=DOMAIN, + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + with patch_all_discovered_devices([BLUECHARM_BLE_DEVICE]): + inject_bluetooth_service_info(hass, BLUECHARM_BEACON_SERVICE_INFO) + await hass.async_block_till_done() + + tracker = hass.states.get("device_tracker.bluecharm_177999_8105") + tracker_attributes = tracker.attributes + assert tracker.state == STATE_HOME + assert tracker_attributes[ATTR_FRIENDLY_NAME] == "BlueCharm_177999 8105" + + with patch_all_discovered_devices([]): + await hass.async_block_till_done() + async_fire_time_changed( + hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS * 2) + ) + await hass.async_block_till_done() + + tracker = hass.states.get("device_tracker.bluecharm_177999_8105") + assert tracker.state == STATE_NOT_HOME + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + +async def test_device_tracker_random_address(hass): + """Test creating and updating device_tracker.""" + entry = MockConfigEntry( + domain=DOMAIN, + ) + entry.add_to_hass(hass) + start_time = time.monotonic() + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + for i in range(20): + inject_bluetooth_service_info( + hass, + replace( + BEACON_RANDOM_ADDRESS_SERVICE_INFO, address=f"AA:BB:CC:DD:EE:{i:02X}" + ), + ) + await hass.async_block_till_done() + + tracker = hass.states.get("device_tracker.randomaddress_1234") + tracker_attributes = tracker.attributes + assert tracker.state == STATE_HOME + assert tracker_attributes[ATTR_FRIENDLY_NAME] == "RandomAddress_1234" + + await hass.async_block_till_done() + with patch_all_discovered_devices([]), patch( + "homeassistant.components.ibeacon.coordinator.MONOTONIC_TIME", + return_value=start_time + UNAVAILABLE_TIMEOUT + 1, + ): + async_fire_time_changed( + hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TIMEOUT) + ) + await hass.async_block_till_done() + + tracker = hass.states.get("device_tracker.randomaddress_1234") + assert tracker.state == STATE_NOT_HOME + + inject_bluetooth_service_info( + hass, replace(BEACON_RANDOM_ADDRESS_SERVICE_INFO, address="AA:BB:CC:DD:EE:DD") + ) + await hass.async_block_till_done() + + tracker = hass.states.get("device_tracker.randomaddress_1234") + tracker_attributes = tracker.attributes + assert tracker.state == STATE_HOME + assert tracker_attributes[ATTR_FRIENDLY_NAME] == "RandomAddress_1234" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + tracker = hass.states.get("device_tracker.randomaddress_1234") + tracker_attributes = tracker.attributes + assert tracker.state == STATE_UNAVAILABLE + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + tracker = hass.states.get("device_tracker.randomaddress_1234") + tracker_attributes = tracker.attributes + assert tracker.state == STATE_HOME + assert tracker_attributes[ATTR_FRIENDLY_NAME] == "RandomAddress_1234" diff --git a/tests/components/ibeacon/test_sensor.py b/tests/components/ibeacon/test_sensor.py new file mode 100644 index 00000000000..38c03b0be5d --- /dev/null +++ b/tests/components/ibeacon/test_sensor.py @@ -0,0 +1,184 @@ +"""Test the ibeacon sensors.""" + + +from dataclasses import replace +from datetime import timedelta + +import pytest + +from homeassistant.components.bluetooth.const import UNAVAILABLE_TRACK_SECONDS +from homeassistant.components.ibeacon.const import DOMAIN, UPDATE_INTERVAL +from homeassistant.components.sensor import ATTR_STATE_CLASS +from homeassistant.const import ( + ATTR_FRIENDLY_NAME, + ATTR_UNIT_OF_MEASUREMENT, + STATE_UNAVAILABLE, +) +from homeassistant.util import dt as dt_util + +from . import ( + BLUECHARM_BEACON_SERVICE_INFO, + BLUECHARM_BEACON_SERVICE_INFO_2, + BLUECHARM_BLE_DEVICE, + NO_NAME_BEACON_SERVICE_INFO, +) + +from tests.common import MockConfigEntry, async_fire_time_changed +from tests.components.bluetooth import ( + inject_bluetooth_service_info, + patch_all_discovered_devices, +) + + +@pytest.fixture(autouse=True) +def mock_bluetooth(enable_bluetooth): + """Auto mock bluetooth.""" + + +async def test_sensors_updates_fixed_mac_address(hass): + """Test creating and updating sensors with a fixed mac address.""" + entry = MockConfigEntry( + domain=DOMAIN, + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + with patch_all_discovered_devices([BLUECHARM_BLE_DEVICE]): + inject_bluetooth_service_info(hass, BLUECHARM_BEACON_SERVICE_INFO) + await hass.async_block_till_done() + + distance_sensor = hass.states.get("sensor.bluecharm_177999_8105_estimated_distance") + distance_attributes = distance_sensor.attributes + assert distance_sensor.state == "2" + assert ( + distance_attributes[ATTR_FRIENDLY_NAME] + == "BlueCharm_177999 8105 Estimated Distance" + ) + assert distance_attributes[ATTR_UNIT_OF_MEASUREMENT] == "m" + assert distance_attributes[ATTR_STATE_CLASS] == "measurement" + + with patch_all_discovered_devices([BLUECHARM_BLE_DEVICE]): + inject_bluetooth_service_info(hass, BLUECHARM_BEACON_SERVICE_INFO_2) + await hass.async_block_till_done() + + distance_sensor = hass.states.get("sensor.bluecharm_177999_8105_estimated_distance") + distance_attributes = distance_sensor.attributes + assert distance_sensor.state == "0" + assert ( + distance_attributes[ATTR_FRIENDLY_NAME] + == "BlueCharm_177999 8105 Estimated Distance" + ) + assert distance_attributes[ATTR_UNIT_OF_MEASUREMENT] == "m" + assert distance_attributes[ATTR_STATE_CLASS] == "measurement" + + # Make sure RSSI updates are picked up by the periodic update + inject_bluetooth_service_info( + hass, replace(BLUECHARM_BEACON_SERVICE_INFO_2, rssi=-84) + ) + + # We should not see it right away since the update interval is 60 seconds + distance_sensor = hass.states.get("sensor.bluecharm_177999_8105_estimated_distance") + distance_attributes = distance_sensor.attributes + assert distance_sensor.state == "0" + + with patch_all_discovered_devices([BLUECHARM_BLE_DEVICE]): + async_fire_time_changed( + hass, + dt_util.utcnow() + timedelta(seconds=UPDATE_INTERVAL.total_seconds() * 2), + ) + await hass.async_block_till_done() + + distance_sensor = hass.states.get("sensor.bluecharm_177999_8105_estimated_distance") + distance_attributes = distance_sensor.attributes + assert distance_sensor.state == "14" + assert ( + distance_attributes[ATTR_FRIENDLY_NAME] + == "BlueCharm_177999 8105 Estimated Distance" + ) + assert distance_attributes[ATTR_UNIT_OF_MEASUREMENT] == "m" + assert distance_attributes[ATTR_STATE_CLASS] == "measurement" + + with patch_all_discovered_devices([]): + await hass.async_block_till_done() + async_fire_time_changed( + hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS * 2) + ) + await hass.async_block_till_done() + + distance_sensor = hass.states.get("sensor.bluecharm_177999_8105_estimated_distance") + assert distance_sensor.state == STATE_UNAVAILABLE + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + +async def test_sensor_with_no_local_name(hass): + """Test creating and updating sensors.""" + entry = MockConfigEntry( + domain=DOMAIN, + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + inject_bluetooth_service_info(hass, NO_NAME_BEACON_SERVICE_INFO) + await hass.async_block_till_done() + + assert ( + hass.states.get( + "sensor.4e6f4e61_6d65_6172_6d42_6561636f6e73_3838_4949_8105_estimated_distance" + ) + is not None + ) + + assert await hass.config_entries.async_unload(entry.entry_id) + + +async def test_sensor_sees_last_service_info(hass): + """Test sensors are created from recent history.""" + entry = MockConfigEntry( + domain=DOMAIN, + ) + entry.add_to_hass(hass) + inject_bluetooth_service_info(hass, BLUECHARM_BEACON_SERVICE_INFO) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert ( + hass.states.get("sensor.bluecharm_177999_8105_estimated_distance").state == "2" + ) + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + +async def test_can_unload_and_reload(hass): + """Test sensors get recreated on unload/setup.""" + entry = MockConfigEntry( + domain=DOMAIN, + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + inject_bluetooth_service_info(hass, BLUECHARM_BEACON_SERVICE_INFO) + await hass.async_block_till_done() + assert ( + hass.states.get("sensor.bluecharm_177999_8105_estimated_distance").state == "2" + ) + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + assert ( + hass.states.get("sensor.bluecharm_177999_8105_estimated_distance").state + == STATE_UNAVAILABLE + ) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert ( + hass.states.get("sensor.bluecharm_177999_8105_estimated_distance").state == "2" + ) From e05ca87cc7b05b79670cd505e98d4502e0c5a7e9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 20 Sep 2022 09:49:15 +0200 Subject: [PATCH 580/955] Bump codecov/codecov-action from 3.1.0 to 3.1.1 (#78812) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 8c507ef1c9e..3e40bdff4cb 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -838,9 +838,9 @@ jobs: uses: actions/download-artifact@v3 - name: Upload coverage to Codecov (full coverage) if: needs.info.outputs.test_full_suite == 'true' - uses: codecov/codecov-action@v3.1.0 + uses: codecov/codecov-action@v3.1.1 with: flags: full-suite - name: Upload coverage to Codecov (partial coverage) if: needs.info.outputs.test_full_suite == 'false' - uses: codecov/codecov-action@v3.1.0 + uses: codecov/codecov-action@v3.1.1 From bd0daf68e0377f5afe188c035ced801712dc12d8 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Tue, 20 Sep 2022 13:57:54 +0200 Subject: [PATCH 581/955] If brightness is not available, don't set a value (#78827) --- homeassistant/components/google_assistant/trait.py | 2 -- tests/components/google_assistant/test_smart_home.py | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 8c253523561..d7b6b45de87 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -260,8 +260,6 @@ class BrightnessTrait(_Trait): brightness = self.state.attributes.get(light.ATTR_BRIGHTNESS) if brightness is not None: response["brightness"] = round(100 * (brightness / 255)) - else: - response["brightness"] = 0 return response diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index eefa163bdd8..3306cbbaf5a 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -383,7 +383,7 @@ async def test_query_message(hass): "payload": { "devices": { "light.non_existing": {"online": False}, - "light.demo_light": {"on": False, "online": True, "brightness": 0}, + "light.demo_light": {"on": False, "online": True}, "light.another_light": { "on": True, "online": True, @@ -727,7 +727,6 @@ async def test_execute_times_out(hass, report_state, on, brightness, value): "states": { "on": on, "online": True, - "brightness": brightness, }, }, { From fe747601ffcbf0d3c556d017770b84bf36185f96 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 20 Sep 2022 16:49:11 +0200 Subject: [PATCH 582/955] Cleanup DeviceClass and StateClass in tests (#78811) --- .../alarmdecoder/test_config_flow.py | 12 +++-- tests/components/alexa/test_smart_home.py | 4 +- tests/components/ecobee/test_humidifier.py | 4 +- tests/components/fritz/test_sensor.py | 17 ++++--- .../homeassistant/triggers/test_time.py | 13 ++--- .../homekit/test_type_humidifiers.py | 6 +-- tests/components/homewizard/test_sensor.py | 47 +++++++++---------- tests/components/homewizard/test_switch.py | 8 ++-- tests/components/nest/test_sensor_sdm.py | 18 ++++--- 9 files changed, 68 insertions(+), 61 deletions(-) diff --git a/tests/components/alarmdecoder/test_config_flow.py b/tests/components/alarmdecoder/test_config_flow.py index e3fdb05ca00..60e019b1e05 100644 --- a/tests/components/alarmdecoder/test_config_flow.py +++ b/tests/components/alarmdecoder/test_config_flow.py @@ -27,7 +27,7 @@ from homeassistant.components.alarmdecoder.const import ( PROTOCOL_SERIAL, PROTOCOL_SOCKET, ) -from homeassistant.components.binary_sensor import DEVICE_CLASS_WINDOW +from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.const import CONF_HOST, CONF_PORT, CONF_PROTOCOL from homeassistant.core import HomeAssistant @@ -181,7 +181,10 @@ async def test_options_arm_flow(hass: HomeAssistant): async def test_options_zone_flow(hass: HomeAssistant): """Test options flow for adding/deleting zones.""" zone_number = "2" - zone_settings = {CONF_ZONE_NAME: "Front Entry", CONF_ZONE_TYPE: DEVICE_CLASS_WINDOW} + zone_settings = { + CONF_ZONE_NAME: "Front Entry", + CONF_ZONE_TYPE: BinarySensorDeviceClass.WINDOW, + } entry = MockConfigEntry(domain=DOMAIN) entry.add_to_hass(hass) @@ -257,7 +260,10 @@ async def test_options_zone_flow(hass: HomeAssistant): async def test_options_zone_flow_validation(hass: HomeAssistant): """Test input validation for zone options flow.""" zone_number = "2" - zone_settings = {CONF_ZONE_NAME: "Front Entry", CONF_ZONE_TYPE: DEVICE_CLASS_WINDOW} + zone_settings = { + CONF_ZONE_NAME: "Front Entry", + CONF_ZONE_TYPE: BinarySensorDeviceClass.WINDOW, + } entry = MockConfigEntry(domain=DOMAIN) entry.add_to_hass(hass) diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index d04b3719bf9..85926810290 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -7,7 +7,7 @@ import pytest from homeassistant.components.alexa import messages, smart_home import homeassistant.components.camera as camera -from homeassistant.components.cover import DEVICE_CLASS_GATE +from homeassistant.components.cover import CoverDeviceClass from homeassistant.components.media_player.const import ( SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, @@ -2765,7 +2765,7 @@ async def test_cover_gate(hass): { "friendly_name": "Test cover gate", "supported_features": 3, - "device_class": DEVICE_CLASS_GATE, + "device_class": CoverDeviceClass.GATE, }, ) appliance = await discovery_test(device, hass) diff --git a/tests/components/ecobee/test_humidifier.py b/tests/components/ecobee/test_humidifier.py index e99d98996d2..e8a370455f7 100644 --- a/tests/components/ecobee/test_humidifier.py +++ b/tests/components/ecobee/test_humidifier.py @@ -11,12 +11,12 @@ from homeassistant.components.humidifier import ( ATTR_MIN_HUMIDITY, DEFAULT_MAX_HUMIDITY, DEFAULT_MIN_HUMIDITY, - DEVICE_CLASS_HUMIDIFIER, DOMAIN as HUMIDIFIER_DOMAIN, MODE_AUTO, SERVICE_SET_HUMIDITY, SERVICE_SET_MODE, SUPPORT_MODES, + HumidifierDeviceClass, ) from homeassistant.const import ( ATTR_DEVICE_CLASS, @@ -50,7 +50,7 @@ async def test_attributes(hass): MODE_MANUAL, ] assert state.attributes.get(ATTR_FRIENDLY_NAME) == "ecobee" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_HUMIDIFIER + assert state.attributes.get(ATTR_DEVICE_CLASS) == HumidifierDeviceClass.HUMIDIFIER assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == SUPPORT_MODES diff --git a/tests/components/fritz/test_sensor.py b/tests/components/fritz/test_sensor.py index 73a7c1068ae..2a3435210e7 100644 --- a/tests/components/fritz/test_sensor.py +++ b/tests/components/fritz/test_sensor.py @@ -10,10 +10,9 @@ from homeassistant.components.fritz.const import DOMAIN from homeassistant.components.fritz.sensor import SENSOR_TYPES from homeassistant.components.sensor import ( ATTR_STATE_CLASS, - DEVICE_CLASS_TIMESTAMP, DOMAIN as SENSOR_DOMAIN, - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( @@ -38,21 +37,21 @@ SENSOR_STATES: dict[str, dict[str, Any]] = { }, "sensor.mock_title_device_uptime": { # ATTR_STATE: "2022-02-05T17:46:04+00:00", - ATTR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP, + ATTR_DEVICE_CLASS: SensorDeviceClass.TIMESTAMP, }, "sensor.mock_title_connection_uptime": { # ATTR_STATE: "2022-03-06T11:27:16+00:00", - ATTR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP, + ATTR_DEVICE_CLASS: SensorDeviceClass.TIMESTAMP, }, "sensor.mock_title_upload_throughput": { ATTR_STATE: "3.4", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIT_OF_MEASUREMENT: "kB/s", ATTR_ICON: "mdi:upload", }, "sensor.mock_title_download_throughput": { ATTR_STATE: "67.6", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIT_OF_MEASUREMENT: "kB/s", ATTR_ICON: "mdi:download", }, @@ -68,13 +67,13 @@ SENSOR_STATES: dict[str, dict[str, Any]] = { }, "sensor.mock_title_gb_sent": { ATTR_STATE: "1.7", - ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, + ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, ATTR_UNIT_OF_MEASUREMENT: "GB", ATTR_ICON: "mdi:upload", }, "sensor.mock_title_gb_received": { ATTR_STATE: "5.2", - ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, + ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, ATTR_UNIT_OF_MEASUREMENT: "GB", ATTR_ICON: "mdi:download", }, diff --git a/tests/components/homeassistant/triggers/test_time.py b/tests/components/homeassistant/triggers/test_time.py index 499fcf8611e..b13e974dc7b 100644 --- a/tests/components/homeassistant/triggers/test_time.py +++ b/tests/components/homeassistant/triggers/test_time.py @@ -5,8 +5,9 @@ from unittest.mock import Mock, patch import pytest import voluptuous as vol -from homeassistant.components import automation, sensor +from homeassistant.components import automation from homeassistant.components.homeassistant.triggers import time +from homeassistant.components.sensor import SensorDeviceClass from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, SERVICE_TURN_OFF from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -406,7 +407,7 @@ async def test_if_fires_using_at_sensor(hass, calls): hass.states.async_set( "sensor.next_alarm", trigger_dt.isoformat(), - {ATTR_DEVICE_CLASS: sensor.DEVICE_CLASS_TIMESTAMP}, + {ATTR_DEVICE_CLASS: SensorDeviceClass.TIMESTAMP}, ) time_that_will_not_match_right_away = trigger_dt - timedelta(minutes=1) @@ -445,7 +446,7 @@ async def test_if_fires_using_at_sensor(hass, calls): hass.states.async_set( "sensor.next_alarm", trigger_dt.isoformat(), - {ATTR_DEVICE_CLASS: sensor.DEVICE_CLASS_TIMESTAMP}, + {ATTR_DEVICE_CLASS: SensorDeviceClass.TIMESTAMP}, ) await hass.async_block_till_done() @@ -462,13 +463,13 @@ async def test_if_fires_using_at_sensor(hass, calls): hass.states.async_set( "sensor.next_alarm", trigger_dt.isoformat(), - {ATTR_DEVICE_CLASS: sensor.DEVICE_CLASS_TIMESTAMP}, + {ATTR_DEVICE_CLASS: SensorDeviceClass.TIMESTAMP}, ) await hass.async_block_till_done() hass.states.async_set( "sensor.next_alarm", broken, - {ATTR_DEVICE_CLASS: sensor.DEVICE_CLASS_TIMESTAMP}, + {ATTR_DEVICE_CLASS: SensorDeviceClass.TIMESTAMP}, ) await hass.async_block_till_done() @@ -482,7 +483,7 @@ async def test_if_fires_using_at_sensor(hass, calls): hass.states.async_set( "sensor.next_alarm", trigger_dt.isoformat(), - {ATTR_DEVICE_CLASS: sensor.DEVICE_CLASS_TIMESTAMP}, + {ATTR_DEVICE_CLASS: SensorDeviceClass.TIMESTAMP}, ) await hass.async_block_till_done() hass.states.async_set( diff --git a/tests/components/homekit/test_type_humidifiers.py b/tests/components/homekit/test_type_humidifiers.py index 3c3985a961b..2c424d24fa0 100644 --- a/tests/components/homekit/test_type_humidifiers.py +++ b/tests/components/homekit/test_type_humidifiers.py @@ -22,8 +22,6 @@ from homeassistant.components.humidifier import ( ATTR_MIN_HUMIDITY, DEFAULT_MAX_HUMIDITY, DEFAULT_MIN_HUMIDITY, - DEVICE_CLASS_DEHUMIDIFIER, - DEVICE_CLASS_HUMIDIFIER, DOMAIN, SERVICE_SET_HUMIDITY, HumidifierDeviceClass, @@ -88,7 +86,7 @@ async def test_humidifier(hass, hk_driver, events): hass.states.async_set( entity_id, STATE_OFF, - {ATTR_HUMIDITY: 42, ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDIFIER}, + {ATTR_HUMIDITY: 42, ATTR_DEVICE_CLASS: HumidifierDeviceClass.HUMIDIFIER}, ) await hass.async_block_till_done() assert acc.char_target_humidity.value == 42.0 @@ -128,7 +126,7 @@ async def test_dehumidifier(hass, hk_driver, events): entity_id = "humidifier.test" hass.states.async_set( - entity_id, STATE_OFF, {ATTR_DEVICE_CLASS: DEVICE_CLASS_DEHUMIDIFIER} + entity_id, STATE_OFF, {ATTR_DEVICE_CLASS: HumidifierDeviceClass.DEHUMIDIFIER} ) await hass.async_block_till_done() acc = HumidifierDehumidifier( diff --git a/tests/components/homewizard/test_sensor.py b/tests/components/homewizard/test_sensor.py index 85b6dc58235..145a2719b01 100644 --- a/tests/components/homewizard/test_sensor.py +++ b/tests/components/homewizard/test_sensor.py @@ -8,17 +8,14 @@ from homewizard_energy.models import Data from homeassistant.components.sensor import ( ATTR_STATE_CLASS, - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, + SensorStateClass, ) from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME, ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_GAS, - DEVICE_CLASS_POWER, ENERGY_KILO_WATT_HOUR, POWER_WATT, VOLUME_CUBIC_METERS, @@ -208,9 +205,9 @@ async def test_sensor_entity_total_power_import_t1_kwh( state.attributes.get(ATTR_FRIENDLY_NAME) == "Product Name (aabbccddeeff) Total power import T1" ) - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_ENERGY + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY assert ATTR_ICON not in state.attributes @@ -250,9 +247,9 @@ async def test_sensor_entity_total_power_import_t2_kwh( state.attributes.get(ATTR_FRIENDLY_NAME) == "Product Name (aabbccddeeff) Total power import T2" ) - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_ENERGY + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY assert ATTR_ICON not in state.attributes @@ -292,9 +289,9 @@ async def test_sensor_entity_total_power_export_t1_kwh( state.attributes.get(ATTR_FRIENDLY_NAME) == "Product Name (aabbccddeeff) Total power export T1" ) - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_ENERGY + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY assert ATTR_ICON not in state.attributes @@ -334,9 +331,9 @@ async def test_sensor_entity_total_power_export_t2_kwh( state.attributes.get(ATTR_FRIENDLY_NAME) == "Product Name (aabbccddeeff) Total power export T2" ) - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_ENERGY + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY assert ATTR_ICON not in state.attributes @@ -372,9 +369,9 @@ async def test_sensor_entity_active_power( state.attributes.get(ATTR_FRIENDLY_NAME) == "Product Name (aabbccddeeff) Active power" ) - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER assert ATTR_ICON not in state.attributes @@ -412,9 +409,9 @@ async def test_sensor_entity_active_power_l1( state.attributes.get(ATTR_FRIENDLY_NAME) == "Product Name (aabbccddeeff) Active power L1" ) - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER assert ATTR_ICON not in state.attributes @@ -452,9 +449,9 @@ async def test_sensor_entity_active_power_l2( state.attributes.get(ATTR_FRIENDLY_NAME) == "Product Name (aabbccddeeff) Active power L2" ) - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER assert ATTR_ICON not in state.attributes @@ -492,9 +489,9 @@ async def test_sensor_entity_active_power_l3( state.attributes.get(ATTR_FRIENDLY_NAME) == "Product Name (aabbccddeeff) Active power L3" ) - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER assert ATTR_ICON not in state.attributes @@ -528,9 +525,9 @@ async def test_sensor_entity_total_gas(hass, mock_config_entry_data, mock_config state.attributes.get(ATTR_FRIENDLY_NAME) == "Product Name (aabbccddeeff) Total gas" ) - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == VOLUME_CUBIC_METERS - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_GAS + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.GAS assert ATTR_ICON not in state.attributes @@ -569,7 +566,7 @@ async def test_sensor_entity_active_liters( == "Product Name (aabbccddeeff) Active water usage" ) - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "l/min" assert ATTR_DEVICE_CLASS not in state.attributes assert state.attributes.get(ATTR_ICON) == "mdi:water" @@ -610,7 +607,7 @@ async def test_sensor_entity_total_liters( == "Product Name (aabbccddeeff) Total water usage" ) - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == VOLUME_CUBIC_METERS assert ATTR_DEVICE_CLASS not in state.attributes assert state.attributes.get(ATTR_ICON) == "mdi:gauge" diff --git a/tests/components/homewizard/test_switch.py b/tests/components/homewizard/test_switch.py index 224d32e1b5c..64fb5a56909 100644 --- a/tests/components/homewizard/test_switch.py +++ b/tests/components/homewizard/test_switch.py @@ -5,7 +5,7 @@ from unittest.mock import AsyncMock, patch from homewizard_energy.models import State from homeassistant.components import switch -from homeassistant.components.switch import DEVICE_CLASS_OUTLET, DEVICE_CLASS_SWITCH +from homeassistant.components.switch import SwitchDeviceClass from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME, @@ -78,7 +78,7 @@ async def test_switch_loads_entities(hass, mock_config_entry_data, mock_config_e state_power_on.attributes.get(ATTR_FRIENDLY_NAME) == "Product Name (aabbccddeeff)" ) - assert state_power_on.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_OUTLET + assert state_power_on.attributes.get(ATTR_DEVICE_CLASS) == SwitchDeviceClass.OUTLET assert ATTR_ICON not in state_power_on.attributes state_switch_lock = hass.states.get("switch.product_name_aabbccddeeff_switch_lock") @@ -95,7 +95,9 @@ async def test_switch_loads_entities(hass, mock_config_entry_data, mock_config_e state_switch_lock.attributes.get(ATTR_FRIENDLY_NAME) == "Product Name (aabbccddeeff) Switch lock" ) - assert state_switch_lock.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_SWITCH + assert ( + state_switch_lock.attributes.get(ATTR_DEVICE_CLASS) == SwitchDeviceClass.SWITCH + ) assert ATTR_ICON not in state_switch_lock.attributes diff --git a/tests/components/nest/test_sensor_sdm.py b/tests/components/nest/test_sensor_sdm.py index 0f98d0c05b4..d1a89317959 100644 --- a/tests/components/nest/test_sensor_sdm.py +++ b/tests/components/nest/test_sensor_sdm.py @@ -10,13 +10,15 @@ from typing import Any from google_nest_sdm.event import EventMessage import pytest -from homeassistant.components.sensor import ATTR_STATE_CLASS, STATE_CLASS_MEASUREMENT +from homeassistant.components.sensor import ( + ATTR_STATE_CLASS, + SensorDeviceClass, + SensorStateClass, +) from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_TEMPERATURE, PERCENTAGE, TEMP_CELSIUS, ) @@ -58,16 +60,18 @@ async def test_thermostat_device( assert temperature is not None assert temperature.state == "25.1" assert temperature.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS - assert temperature.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE - assert temperature.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert ( + temperature.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE + ) + assert temperature.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert temperature.attributes.get(ATTR_FRIENDLY_NAME) == "My Sensor Temperature" humidity = hass.states.get("sensor.my_sensor_humidity") assert humidity is not None assert humidity.state == "35" assert humidity.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - assert humidity.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_HUMIDITY - assert humidity.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert humidity.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.HUMIDITY + assert humidity.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert humidity.attributes.get(ATTR_FRIENDLY_NAME) == "My Sensor Humidity" registry = er.async_get(hass) From 25b1dfb53a464c28a26cb30f06c15e2521098d06 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 20 Sep 2022 16:49:44 +0200 Subject: [PATCH 583/955] Cleanup EntityCategory in tests (#78808) --- tests/components/hue/test_sensor_v1.py | 4 ++-- tests/components/zha/test_button.py | 7 +++---- tests/components/zha/test_number.py | 5 +++-- tests/components/zha/test_select.py | 9 +++++---- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/tests/components/hue/test_sensor_v1.py b/tests/components/hue/test_sensor_v1.py index c35403eaac1..acefbdd07fe 100644 --- a/tests/components/hue/test_sensor_v1.py +++ b/tests/components/hue/test_sensor_v1.py @@ -7,7 +7,7 @@ import aiohue from homeassistant.components import hue from homeassistant.components.hue.const import ATTR_HUE_EVENT from homeassistant.components.hue.v1 import sensor_base -from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_registry import async_get from homeassistant.util import dt as dt_util @@ -351,7 +351,7 @@ async def test_sensors(hass, mock_bridge_v1): ent_reg = async_get(hass) assert ( ent_reg.async_get("sensor.hue_dimmer_switch_1_battery_level").entity_category - == ENTITY_CATEGORY_DIAGNOSTIC + == EntityCategory.DIAGNOSTIC ) diff --git a/tests/components/zha/test_button.py b/tests/components/zha/test_button.py index 5e1350bc673..17c8f0ccb28 100644 --- a/tests/components/zha/test_button.py +++ b/tests/components/zha/test_button.py @@ -24,12 +24,11 @@ from homeassistant.components.button import DOMAIN, SERVICE_PRESS, ButtonDeviceC from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, - ENTITY_CATEGORY_CONFIG, - ENTITY_CATEGORY_DIAGNOSTIC, STATE_UNKNOWN, Platform, ) from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity import EntityCategory from .common import find_entity_id from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_TYPE @@ -137,7 +136,7 @@ async def test_button(hass, contact_sensor): entry = entity_registry.async_get(entity_id) assert entry - assert entry.entity_category == ENTITY_CATEGORY_DIAGNOSTIC + assert entry.entity_category == EntityCategory.DIAGNOSTIC with patch( "zigpy.zcl.Cluster.request", @@ -177,7 +176,7 @@ async def test_frost_unlock(hass, tuya_water_valve): entry = entity_registry.async_get(entity_id) assert entry - assert entry.entity_category == ENTITY_CATEGORY_CONFIG + assert entry.entity_category == EntityCategory.CONFIG with patch( "zigpy.zcl.Cluster.request", diff --git a/tests/components/zha/test_number.py b/tests/components/zha/test_number.py index 808649b7f55..0bb620e98f4 100644 --- a/tests/components/zha/test_number.py +++ b/tests/components/zha/test_number.py @@ -8,8 +8,9 @@ import zigpy.zcl.clusters.general as general import zigpy.zcl.foundation as zcl_f from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN -from homeassistant.const import ENTITY_CATEGORY_CONFIG, STATE_UNAVAILABLE, Platform +from homeassistant.const import STATE_UNAVAILABLE, Platform from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity import EntityCategory from homeassistant.setup import async_setup_component from .common import ( @@ -259,7 +260,7 @@ async def test_level_control_number( entity_entry = entity_registry.async_get(entity_id) assert entity_entry - assert entity_entry.entity_category == ENTITY_CATEGORY_CONFIG + assert entity_entry.entity_category == EntityCategory.CONFIG # Test number set_value await hass.services.async_call( diff --git a/tests/components/zha/test_select.py b/tests/components/zha/test_select.py index 1c714def54b..b9c72975823 100644 --- a/tests/components/zha/test_select.py +++ b/tests/components/zha/test_select.py @@ -8,8 +8,9 @@ import zigpy.profiles.zha as zha import zigpy.zcl.clusters.general as general import zigpy.zcl.clusters.security as security -from homeassistant.const import ENTITY_CATEGORY_CONFIG, STATE_UNKNOWN, Platform +from homeassistant.const import STATE_UNKNOWN, Platform from homeassistant.helpers import entity_registry as er, restore_state +from homeassistant.helpers.entity import EntityCategory from homeassistant.util import dt as dt_util from .common import find_entity_id @@ -136,7 +137,7 @@ async def test_select(hass, siren): entity_entry = entity_registry.async_get(entity_id) assert entity_entry - assert entity_entry.entity_category == ENTITY_CATEGORY_CONFIG + assert entity_entry.entity_category == EntityCategory.CONFIG # Test select option with string value await hass.services.async_call( @@ -228,7 +229,7 @@ async def test_on_off_select_new_join(hass, light, zha_device_joined): entity_entry = entity_registry.async_get(entity_id) assert entity_entry - assert entity_entry.entity_category == ENTITY_CATEGORY_CONFIG + assert entity_entry.entity_category == EntityCategory.CONFIG # Test select option with string value await hass.services.async_call( @@ -300,7 +301,7 @@ async def test_on_off_select_restored(hass, light, zha_device_restored): entity_entry = entity_registry.async_get(entity_id) assert entity_entry - assert entity_entry.entity_category == ENTITY_CATEGORY_CONFIG + assert entity_entry.entity_category == EntityCategory.CONFIG async def test_on_off_select_unsupported(hass, light, zha_device_joined_restored): From 4f31f28e67a14ae87c708e3f7509ff832134ad9a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 20 Sep 2022 16:50:07 +0200 Subject: [PATCH 584/955] Cleanup SourceType in tests (#78809) --- .../device_tracker/test_entities.py | 4 +-- tests/components/device_tracker/test_init.py | 6 ++--- tests/components/dhcp/test_init.py | 12 ++++----- tests/components/mazda/test_device_tracker.py | 4 +-- tests/components/person/test_init.py | 26 +++++-------------- tests/components/zha/test_device_tracker.py | 4 +-- 6 files changed, 22 insertions(+), 34 deletions(-) diff --git a/tests/components/device_tracker/test_entities.py b/tests/components/device_tracker/test_entities.py index 12059cad601..0d4312d4688 100644 --- a/tests/components/device_tracker/test_entities.py +++ b/tests/components/device_tracker/test_entities.py @@ -11,7 +11,7 @@ from homeassistant.components.device_tracker.const import ( ATTR_MAC, ATTR_SOURCE_TYPE, DOMAIN, - SOURCE_TYPE_ROUTER, + SourceType, ) from homeassistant.const import ATTR_BATTERY_LEVEL, STATE_HOME, STATE_NOT_HOME from homeassistant.helpers import device_registry as dr @@ -37,7 +37,7 @@ async def test_scanner_entity_device_tracker(hass, enable_custom_integrations): entity_id = "device_tracker.test_ad_de_ef_be_ed_fe" entity_state = hass.states.get(entity_id) assert entity_state.attributes == { - ATTR_SOURCE_TYPE: SOURCE_TYPE_ROUTER, + ATTR_SOURCE_TYPE: SourceType.ROUTER, ATTR_BATTERY_LEVEL: 100, ATTR_IP: "0.0.0.0", ATTR_MAC: "ad:de:ef:be:ed:fe", diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py index 3bafa59fb96..9bb93a49637 100644 --- a/tests/components/device_tracker/test_init.py +++ b/tests/components/device_tracker/test_init.py @@ -9,7 +9,7 @@ import pytest from homeassistant.components import zone import homeassistant.components.device_tracker as device_tracker -from homeassistant.components.device_tracker import const, legacy +from homeassistant.components.device_tracker import SourceType, const, legacy from homeassistant.const import ( ATTR_ENTITY_PICTURE, ATTR_FRIENDLY_NAME, @@ -495,7 +495,7 @@ async def test_see_passive_zone_state( assert attrs.get("latitude") == 1 assert attrs.get("longitude") == 2 assert attrs.get("gps_accuracy") == 0 - assert attrs.get("source_type") == device_tracker.SOURCE_TYPE_ROUTER + assert attrs.get("source_type") == SourceType.ROUTER scanner.leave_home("dev1") @@ -515,7 +515,7 @@ async def test_see_passive_zone_state( assert attrs.get("latitude") is None assert attrs.get("longitude") is None assert attrs.get("gps_accuracy") is None - assert attrs.get("source_type") == device_tracker.SOURCE_TYPE_ROUTER + assert attrs.get("source_type") == SourceType.ROUTER @patch("homeassistant.components.device_tracker.const.LOGGER.warning") diff --git a/tests/components/dhcp/test_init.py b/tests/components/dhcp/test_init.py index 78dbb4a95d9..b84341f44ea 100644 --- a/tests/components/dhcp/test_init.py +++ b/tests/components/dhcp/test_init.py @@ -16,7 +16,7 @@ from homeassistant.components.device_tracker import ( ATTR_MAC, ATTR_SOURCE_TYPE, CONNECTED_DEVICE_REGISTERED, - SOURCE_TYPE_ROUTER, + SourceType, ) from homeassistant.components.dhcp.const import DOMAIN from homeassistant.const import ( @@ -603,7 +603,7 @@ async def test_device_tracker_hostname_and_macaddress_exists_before_start(hass): { ATTR_HOST_NAME: "Connect", ATTR_IP: "192.168.210.56", - ATTR_SOURCE_TYPE: SOURCE_TYPE_ROUTER, + ATTR_SOURCE_TYPE: SourceType.ROUTER, ATTR_MAC: "B8:B7:F1:6D:B5:33", }, ) @@ -701,7 +701,7 @@ async def test_device_tracker_hostname_and_macaddress_after_start(hass): { ATTR_HOST_NAME: "Connect", ATTR_IP: "192.168.210.56", - ATTR_SOURCE_TYPE: SOURCE_TYPE_ROUTER, + ATTR_SOURCE_TYPE: SourceType.ROUTER, ATTR_MAC: "B8:B7:F1:6D:B5:33", }, ) @@ -738,7 +738,7 @@ async def test_device_tracker_hostname_and_macaddress_after_start_not_home(hass) { ATTR_HOST_NAME: "connect", ATTR_IP: "192.168.210.56", - ATTR_SOURCE_TYPE: SOURCE_TYPE_ROUTER, + ATTR_SOURCE_TYPE: SourceType.ROUTER, ATTR_MAC: "B8:B7:F1:6D:B5:33", }, ) @@ -795,7 +795,7 @@ async def test_device_tracker_hostname_and_macaddress_after_start_hostname_missi STATE_HOME, { ATTR_IP: "192.168.210.56", - ATTR_SOURCE_TYPE: SOURCE_TYPE_ROUTER, + ATTR_SOURCE_TYPE: SourceType.ROUTER, ATTR_MAC: "B8:B7:F1:6D:B5:33", }, ) @@ -814,7 +814,7 @@ async def test_device_tracker_ignore_self_assigned_ips_before_start(hass): { ATTR_HOST_NAME: "connect", ATTR_IP: "169.254.210.56", - ATTR_SOURCE_TYPE: SOURCE_TYPE_ROUTER, + ATTR_SOURCE_TYPE: SourceType.ROUTER, ATTR_MAC: "B8:B7:F1:6D:B5:33", }, ) diff --git a/tests/components/mazda/test_device_tracker.py b/tests/components/mazda/test_device_tracker.py index f2a43299414..b674b3c3d3b 100644 --- a/tests/components/mazda/test_device_tracker.py +++ b/tests/components/mazda/test_device_tracker.py @@ -1,5 +1,5 @@ """The device tracker tests for the Mazda Connected Services integration.""" -from homeassistant.components.device_tracker import ATTR_SOURCE_TYPE, SOURCE_TYPE_GPS +from homeassistant.components.device_tracker import ATTR_SOURCE_TYPE, SourceType from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_ICON, @@ -23,7 +23,7 @@ async def test_device_tracker(hass): assert state.attributes.get(ATTR_ICON) == "mdi:car" assert state.attributes.get(ATTR_LATITUDE) == 1.234567 assert state.attributes.get(ATTR_LONGITUDE) == -2.345678 - assert state.attributes.get(ATTR_SOURCE_TYPE) == SOURCE_TYPE_GPS + assert state.attributes.get(ATTR_SOURCE_TYPE) == SourceType.GPS entry = entity_registry.async_get("device_tracker.my_mazda3_device_tracker") assert entry assert entry.unique_id == "JM000000000000000" diff --git a/tests/components/person/test_init.py b/tests/components/person/test_init.py index 981343ea3a5..02c43d59b7b 100644 --- a/tests/components/person/test_init.py +++ b/tests/components/person/test_init.py @@ -5,11 +5,7 @@ from unittest.mock import patch import pytest from homeassistant.components import person -from homeassistant.components.device_tracker import ( - ATTR_SOURCE_TYPE, - SOURCE_TYPE_GPS, - SOURCE_TYPE_ROUTER, -) +from homeassistant.components.device_tracker import ATTR_SOURCE_TYPE, SourceType from homeassistant.components.person import ATTR_SOURCE, ATTR_USER_ID, DOMAIN from homeassistant.const import ( ATTR_ENTITY_PICTURE, @@ -208,9 +204,7 @@ async def test_setup_two_trackers(hass, hass_admin_user): hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() - hass.states.async_set( - DEVICE_TRACKER, "home", {ATTR_SOURCE_TYPE: SOURCE_TYPE_ROUTER} - ) + hass.states.async_set(DEVICE_TRACKER, "home", {ATTR_SOURCE_TYPE: SourceType.ROUTER}) await hass.async_block_till_done() state = hass.states.get("person.tracked_person") @@ -229,12 +223,12 @@ async def test_setup_two_trackers(hass, hass_admin_user): ATTR_LATITUDE: 12.123456, ATTR_LONGITUDE: 13.123456, ATTR_GPS_ACCURACY: 12, - ATTR_SOURCE_TYPE: SOURCE_TYPE_GPS, + ATTR_SOURCE_TYPE: SourceType.GPS, }, ) await hass.async_block_till_done() hass.states.async_set( - DEVICE_TRACKER, "not_home", {ATTR_SOURCE_TYPE: SOURCE_TYPE_ROUTER} + DEVICE_TRACKER, "not_home", {ATTR_SOURCE_TYPE: SourceType.ROUTER} ) await hass.async_block_till_done() @@ -247,22 +241,16 @@ async def test_setup_two_trackers(hass, hass_admin_user): assert state.attributes.get(ATTR_SOURCE) == DEVICE_TRACKER_2 assert state.attributes.get(ATTR_USER_ID) == user_id - hass.states.async_set( - DEVICE_TRACKER_2, "zone1", {ATTR_SOURCE_TYPE: SOURCE_TYPE_GPS} - ) + hass.states.async_set(DEVICE_TRACKER_2, "zone1", {ATTR_SOURCE_TYPE: SourceType.GPS}) await hass.async_block_till_done() state = hass.states.get("person.tracked_person") assert state.state == "zone1" assert state.attributes.get(ATTR_SOURCE) == DEVICE_TRACKER_2 - hass.states.async_set( - DEVICE_TRACKER, "home", {ATTR_SOURCE_TYPE: SOURCE_TYPE_ROUTER} - ) + hass.states.async_set(DEVICE_TRACKER, "home", {ATTR_SOURCE_TYPE: SourceType.ROUTER}) await hass.async_block_till_done() - hass.states.async_set( - DEVICE_TRACKER_2, "zone2", {ATTR_SOURCE_TYPE: SOURCE_TYPE_GPS} - ) + hass.states.async_set(DEVICE_TRACKER_2, "zone2", {ATTR_SOURCE_TYPE: SourceType.GPS}) await hass.async_block_till_done() state = hass.states.get("person.tracked_person") diff --git a/tests/components/zha/test_device_tracker.py b/tests/components/zha/test_device_tracker.py index 59c36143a6e..55be7ca9ebd 100644 --- a/tests/components/zha/test_device_tracker.py +++ b/tests/components/zha/test_device_tracker.py @@ -7,7 +7,7 @@ import pytest import zigpy.profiles.zha import zigpy.zcl.clusters.general as general -from homeassistant.components.device_tracker import SOURCE_TYPE_ROUTER +from homeassistant.components.device_tracker import SourceType from homeassistant.components.zha.core.registries import ( SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE, ) @@ -101,7 +101,7 @@ async def test_device_tracker(hass, zha_device_joined_restored, zigpy_device_dt) entity = hass.data[Platform.DEVICE_TRACKER].get_entity(entity_id) assert entity.is_connected is True - assert entity.source_type == SOURCE_TYPE_ROUTER + assert entity.source_type == SourceType.ROUTER assert entity.battery_level == 100 # test adding device tracker to the network and HA From 924bffc7d090792a8c36b7aa901b1d3896c24008 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Tue, 20 Sep 2022 17:05:10 +0200 Subject: [PATCH 585/955] Add query data to google assistant diagnostic (#78828) --- homeassistant/components/google_assistant/diagnostics.py | 8 +++++++- homeassistant/components/google_assistant/smart_home.py | 7 ++++++- tests/components/google_assistant/test_diagnostics.py | 6 ++++++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/google_assistant/diagnostics.py b/homeassistant/components/google_assistant/diagnostics.py index 6cfaec99f6e..fd4347ddd2c 100644 --- a/homeassistant/components/google_assistant/diagnostics.py +++ b/homeassistant/components/google_assistant/diagnostics.py @@ -11,7 +11,11 @@ from homeassistant.helpers.typing import ConfigType from .const import CONF_SECURE_DEVICES_PIN, CONF_SERVICE_ACCOUNT, DATA_CONFIG, DOMAIN from .http import GoogleConfig -from .smart_home import async_devices_sync_response, create_sync_response +from .smart_home import ( + async_devices_query_response, + async_devices_sync_response, + create_sync_response, +) TO_REDACT = [ "uuid", @@ -32,9 +36,11 @@ async def async_get_config_entry_diagnostics( yaml_config: ConfigType = data[DATA_CONFIG] devices = await async_devices_sync_response(hass, config, REDACTED) sync = create_sync_response(REDACTED, devices) + query = await async_devices_query_response(hass, config, devices) return { "config_entry": async_redact_data(entry.as_dict(), TO_REDACT), "yaml_config": async_redact_data(yaml_config, TO_REDACT), "sync": async_redact_data(sync, TO_REDACT), + "query": async_redact_data(query, TO_REDACT), } diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index 75a3fd76b9b..c034e5fc6ca 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -130,6 +130,11 @@ async def async_devices_query(hass, data, payload): context=data.context, ) + return await async_devices_query_response(hass, data.config, payload_devices) + + +async def async_devices_query_response(hass, config, payload_devices): + """Generate the device serialization.""" devices = {} for device in payload_devices: devid = device["id"] @@ -139,7 +144,7 @@ async def async_devices_query(hass, data, payload): devices[devid] = {"online": False} continue - entity = GoogleEntity(hass, data.config, state) + entity = GoogleEntity(hass, config, state) try: devices[devid] = entity.query_serialize() except Exception: # pylint: disable=broad-except diff --git a/tests/components/google_assistant/test_diagnostics.py b/tests/components/google_assistant/test_diagnostics.py index 13721c17f88..99a091f0bdc 100644 --- a/tests/components/google_assistant/test_diagnostics.py +++ b/tests/components/google_assistant/test_diagnostics.py @@ -80,6 +80,12 @@ async def test_diagnostics(hass: core.HomeAssistant, hass_client: Any): }, ], }, + "query": { + "devices": { + "switch.ac": {"on": False, "online": True}, + "switch.decorative_lights": {"on": True, "online": True}, + } + }, "yaml_config": { "expose_by_default": True, "exposed_domains": [ From 3776fc3b9fa459781d901e86a348314ca197f1e1 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 20 Sep 2022 17:32:10 +0200 Subject: [PATCH 586/955] Add status codes 23 and 26 to Xiaomi Miio vacuum (#78289) * Add status codes 23 and 26 * change status 26 --- homeassistant/components/xiaomi_miio/vacuum.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/xiaomi_miio/vacuum.py b/homeassistant/components/xiaomi_miio/vacuum.py index 19c38ac0a82..3d6ada7481f 100644 --- a/homeassistant/components/xiaomi_miio/vacuum.py +++ b/homeassistant/components/xiaomi_miio/vacuum.py @@ -73,6 +73,8 @@ STATE_CODE_TO_STATE = { 17: STATE_CLEANING, # "Zoned cleaning" 18: STATE_CLEANING, # "Segment cleaning" 22: STATE_DOCKED, # "Emptying the bin" on s7+ + 23: STATE_DOCKED, # "Washing the mop" on s7maxV + 26: STATE_RETURNING, # "Going to wash the mop" on s7maxV 100: STATE_DOCKED, # "Charging complete" 101: STATE_ERROR, # "Device offline" } From 2a2cc79fc37f500c6b0936242f7d71a23ad320bc Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Tue, 20 Sep 2022 11:51:29 -0400 Subject: [PATCH 587/955] Add Lidarr integration (#66438) --- .coveragerc | 3 + CODEOWNERS | 2 + homeassistant/components/lidarr/__init__.py | 85 +++++++++ .../components/lidarr/config_flow.py | 111 ++++++++++++ homeassistant/components/lidarr/const.py | 38 ++++ .../components/lidarr/coordinator.py | 94 ++++++++++ homeassistant/components/lidarr/manifest.json | 10 ++ homeassistant/components/lidarr/sensor.py | 162 ++++++++++++++++++ homeassistant/components/lidarr/strings.json | 42 +++++ .../components/lidarr/translations/en.json | 42 +++++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 1 + requirements_test_all.txt | 1 + tests/components/lidarr/__init__.py | 53 ++++++ .../lidarr/fixtures/system-status.json | 29 ++++ tests/components/lidarr/test_config_flow.py | 142 +++++++++++++++ 16 files changed, 816 insertions(+) create mode 100644 homeassistant/components/lidarr/__init__.py create mode 100644 homeassistant/components/lidarr/config_flow.py create mode 100644 homeassistant/components/lidarr/const.py create mode 100644 homeassistant/components/lidarr/coordinator.py create mode 100644 homeassistant/components/lidarr/manifest.json create mode 100644 homeassistant/components/lidarr/sensor.py create mode 100644 homeassistant/components/lidarr/strings.json create mode 100644 homeassistant/components/lidarr/translations/en.json create mode 100644 tests/components/lidarr/__init__.py create mode 100644 tests/components/lidarr/fixtures/system-status.json create mode 100644 tests/components/lidarr/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 1f73828daa4..3daac135adb 100644 --- a/.coveragerc +++ b/.coveragerc @@ -667,6 +667,9 @@ omit = 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 + homeassistant/components/lidarr/coordinator.py + homeassistant/components/lidarr/sensor.py homeassistant/components/life360/__init__.py homeassistant/components/life360/const.py homeassistant/components/life360/coordinator.py diff --git a/CODEOWNERS b/CODEOWNERS index c087599baa4..8024c1dad17 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -609,6 +609,8 @@ build.json @home-assistant/supervisor /homeassistant/components/led_ble/ @bdraco /tests/components/led_ble/ @bdraco /homeassistant/components/lg_netcast/ @Drafteed +/homeassistant/components/lidarr/ @tkdrob +/tests/components/lidarr/ @tkdrob /homeassistant/components/life360/ @pnbruckner /tests/components/life360/ @pnbruckner /homeassistant/components/lifx/ @bdraco @Djelibeybi diff --git a/homeassistant/components/lidarr/__init__.py b/homeassistant/components/lidarr/__init__.py new file mode 100644 index 00000000000..6410e520b42 --- /dev/null +++ b/homeassistant/components/lidarr/__init__.py @@ -0,0 +1,85 @@ +"""The Lidarr component.""" +from __future__ import annotations + +from aiopyarr.lidarr_client import LidarrClient +from aiopyarr.models.host_configuration import PyArrHostConfiguration + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_KEY, CONF_URL, CONF_VERIFY_SSL, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo, EntityDescription +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DEFAULT_NAME, DOMAIN +from .coordinator import ( + DiskSpaceDataUpdateCoordinator, + LidarrDataUpdateCoordinator, + QueueDataUpdateCoordinator, + StatusDataUpdateCoordinator, + WantedDataUpdateCoordinator, +) + +PLATFORMS = [Platform.SENSOR] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Lidarr from a config entry.""" + host_configuration = PyArrHostConfiguration( + api_token=entry.data[CONF_API_KEY], + verify_ssl=entry.data[CONF_VERIFY_SSL], + url=entry.data[CONF_URL], + ) + lidarr = LidarrClient( + host_configuration=host_configuration, + session=async_get_clientsession(hass, host_configuration.verify_ssl), + request_timeout=60, + ) + coordinators: dict[str, LidarrDataUpdateCoordinator] = { + "disk_space": DiskSpaceDataUpdateCoordinator(hass, host_configuration, lidarr), + "queue": QueueDataUpdateCoordinator(hass, host_configuration, lidarr), + "status": StatusDataUpdateCoordinator(hass, host_configuration, lidarr), + "wanted": WantedDataUpdateCoordinator(hass, host_configuration, lidarr), + } + # Temporary, until we add diagnostic entities + _version = None + for coordinator in coordinators.values(): + await coordinator.async_config_entry_first_refresh() + if isinstance(coordinator, StatusDataUpdateCoordinator): + _version = coordinator.data + coordinator.system_version = _version + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinators + 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 + + +class LidarrEntity(CoordinatorEntity[LidarrDataUpdateCoordinator]): + """Defines a base Lidarr entity.""" + + _attr_has_entity_name = True + + def __init__( + self, coordinator: LidarrDataUpdateCoordinator, description: EntityDescription + ) -> None: + """Initialize the Lidarr entity.""" + super().__init__(coordinator) + self.entity_description = description + self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{description.key}" + self._attr_device_info = DeviceInfo( + configuration_url=coordinator.host_configuration.base_url, + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, coordinator.config_entry.entry_id)}, + manufacturer=DEFAULT_NAME, + name=DEFAULT_NAME, + sw_version=coordinator.system_version, + ) diff --git a/homeassistant/components/lidarr/config_flow.py b/homeassistant/components/lidarr/config_flow.py new file mode 100644 index 00000000000..1b7f2b23c11 --- /dev/null +++ b/homeassistant/components/lidarr/config_flow.py @@ -0,0 +1,111 @@ +"""Config flow for Lidarr.""" +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any + +from aiohttp import ClientConnectorError +from aiopyarr import SystemStatus, exceptions +from aiopyarr.lidarr_client import LidarrClient +import voluptuous as vol + +from homeassistant.config_entries import ConfigEntry, ConfigFlow +from homeassistant.const import CONF_API_KEY, CONF_URL, 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 DEFAULT_NAME, DOMAIN + + +class LidarrConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Lidarr.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize the flow.""" + self.entry: ConfigEntry | None = None + + async def async_step_reauth(self, user_input: Mapping[str, Any]) -> FlowResult: + """Handle configuration by re-auth.""" + self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: + """Confirm reauth dialog.""" + if user_input is not None: + return await self.async_step_user() + + self._set_confirm_only() + return self.async_show_form(step_id="reauth_confirm") + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initiated by the user.""" + errors = {} + + if user_input is None: + user_input = dict(self.entry.data) if self.entry else None + + else: + try: + result = await validate_input(self.hass, user_input) + if isinstance(result, tuple): + user_input[CONF_API_KEY] = result[1] + elif isinstance(result, str): + errors = {"base": result} + except exceptions.ArrAuthenticationException: + errors = {"base": "invalid_auth"} + except (ClientConnectorError, exceptions.ArrConnectionException): + errors = {"base": "cannot_connect"} + except exceptions.ArrException: + errors = {"base": "unknown"} + if not errors: + if self.entry: + self.hass.config_entries.async_update_entry( + self.entry, data=user_input + ) + await self.hass.config_entries.async_reload(self.entry.entry_id) + + return self.async_abort(reason="reauth_successful") + + return self.async_create_entry(title=DEFAULT_NAME, data=user_input) + + user_input = user_input or {} + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_URL, default=user_input.get(CONF_URL, "")): str, + vol.Optional(CONF_API_KEY): str, + vol.Optional( + CONF_VERIFY_SSL, + default=user_input.get(CONF_VERIFY_SSL, False), + ): bool, + } + ), + errors=errors, + ) + + +async def validate_input( + hass: HomeAssistant, data: dict[str, Any] +) -> tuple[str, str, str] | str | SystemStatus: + """Validate the user input allows us to connect. + + Data has the keys from DATA_SCHEMA with values provided by the user. + """ + lidarr = LidarrClient( + api_token=data.get(CONF_API_KEY, ""), + url=data[CONF_URL], + session=async_get_clientsession(hass), + verify_ssl=data[CONF_VERIFY_SSL], + ) + if CONF_API_KEY not in data: + return await lidarr.async_try_zeroconf() + return await lidarr.async_get_system_status() diff --git a/homeassistant/components/lidarr/const.py b/homeassistant/components/lidarr/const.py new file mode 100644 index 00000000000..08e284b9b31 --- /dev/null +++ b/homeassistant/components/lidarr/const.py @@ -0,0 +1,38 @@ +"""Constants for Lidarr.""" +import logging +from typing import Final + +from homeassistant.const import ( + DATA_BYTES, + DATA_EXABYTES, + DATA_GIGABYTES, + DATA_KILOBYTES, + DATA_MEGABYTES, + DATA_PETABYTES, + DATA_TERABYTES, + DATA_YOTTABYTES, + DATA_ZETTABYTES, +) + +BYTE_SIZES = [ + DATA_BYTES, + DATA_KILOBYTES, + DATA_MEGABYTES, + DATA_GIGABYTES, + DATA_TERABYTES, + DATA_PETABYTES, + DATA_EXABYTES, + DATA_ZETTABYTES, + DATA_YOTTABYTES, +] + +# Defaults +DEFAULT_DAYS = "1" +DEFAULT_HOST = "localhost" +DEFAULT_NAME = "Lidarr" +DEFAULT_UNIT = DATA_GIGABYTES +DEFAULT_MAX_RECORDS = 20 + +DOMAIN: Final = "lidarr" + +LOGGER = logging.getLogger(__package__) diff --git a/homeassistant/components/lidarr/coordinator.py b/homeassistant/components/lidarr/coordinator.py new file mode 100644 index 00000000000..be789c6a32a --- /dev/null +++ b/homeassistant/components/lidarr/coordinator.py @@ -0,0 +1,94 @@ +"""Data update coordinator for the Lidarr integration.""" +from __future__ import annotations + +from abc import abstractmethod +from datetime import timedelta +from typing import Generic, TypeVar, cast + +from aiopyarr import LidarrAlbum, LidarrQueue, LidarrRootFolder, exceptions +from aiopyarr.lidarr_client import LidarrClient +from aiopyarr.models.host_configuration import PyArrHostConfiguration + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DEFAULT_MAX_RECORDS, DOMAIN, LOGGER + +T = TypeVar("T", list[LidarrRootFolder], LidarrQueue, str, LidarrAlbum) + + +class LidarrDataUpdateCoordinator(DataUpdateCoordinator, Generic[T]): + """Data update coordinator for the Lidarr integration.""" + + config_entry: ConfigEntry + + def __init__( + self, + hass: HomeAssistant, + host_configuration: PyArrHostConfiguration, + api_client: LidarrClient, + ) -> None: + """Initialize the coordinator.""" + super().__init__( + hass=hass, + logger=LOGGER, + name=DOMAIN, + update_interval=timedelta(seconds=30), + ) + self.api_client = api_client + self.host_configuration = host_configuration + self.system_version: str | None = None + + async def _async_update_data(self) -> T: + """Get the latest data from Lidarr.""" + try: + return await self._fetch_data() + + except exceptions.ArrConnectionException as ex: + raise UpdateFailed(ex) from ex + except exceptions.ArrAuthenticationException as ex: + raise ConfigEntryAuthFailed( + "API Key is no longer valid. Please reauthenticate" + ) from ex + + @abstractmethod + async def _fetch_data(self) -> T: + """Fetch the actual data.""" + raise NotImplementedError + + +class DiskSpaceDataUpdateCoordinator(LidarrDataUpdateCoordinator): + """Disk space update coordinator for Lidarr.""" + + async def _fetch_data(self) -> list[LidarrRootFolder]: + """Fetch the data.""" + return cast(list, await self.api_client.async_get_root_folders()) + + +class QueueDataUpdateCoordinator(LidarrDataUpdateCoordinator): + """Queue update coordinator.""" + + async def _fetch_data(self) -> LidarrQueue: + """Fetch the album count in queue.""" + return await self.api_client.async_get_queue(page_size=DEFAULT_MAX_RECORDS) + + +class StatusDataUpdateCoordinator(LidarrDataUpdateCoordinator): + """Status update coordinator for Lidarr.""" + + async def _fetch_data(self) -> str: + """Fetch the data.""" + return (await self.api_client.async_get_system_status()).version + + +class WantedDataUpdateCoordinator(LidarrDataUpdateCoordinator): + """Wanted update coordinator.""" + + async def _fetch_data(self) -> LidarrAlbum: + """Fetch the wanted data.""" + return cast( + LidarrAlbum, + await self.api_client.async_get_wanted(page_size=DEFAULT_MAX_RECORDS), + ) diff --git a/homeassistant/components/lidarr/manifest.json b/homeassistant/components/lidarr/manifest.json new file mode 100644 index 00000000000..6f7ad875a46 --- /dev/null +++ b/homeassistant/components/lidarr/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "lidarr", + "name": "Lidarr", + "documentation": "https://www.home-assistant.io/integrations/lidarr", + "requirements": ["aiopyarr==22.7.0"], + "codeowners": ["@tkdrob"], + "config_flow": true, + "iot_class": "local_polling", + "loggers": ["aiopyarr"] +} diff --git a/homeassistant/components/lidarr/sensor.py b/homeassistant/components/lidarr/sensor.py new file mode 100644 index 00000000000..8529d9a6469 --- /dev/null +++ b/homeassistant/components/lidarr/sensor.py @@ -0,0 +1,162 @@ +"""Support for Lidarr.""" +from __future__ import annotations + +from collections.abc import Callable +from copy import deepcopy +from dataclasses import dataclass +from datetime import datetime +from typing import Generic + +from aiopyarr import LidarrQueueItem, LidarrRootFolder + +from homeassistant.components.sensor import ( + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import DATA_GIGABYTES +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType + +from . import LidarrEntity +from .const import BYTE_SIZES, DOMAIN +from .coordinator import LidarrDataUpdateCoordinator, T + + +def get_space(data: list[LidarrRootFolder], name: str) -> str: + """Get space.""" + space = [] + for mount in data: + if name in mount.path: + mount.freeSpace = mount.freeSpace if mount.accessible else 0 + space.append(mount.freeSpace / 1024 ** BYTE_SIZES.index(DATA_GIGABYTES)) + return f"{space[0]:.2f}" + + +def get_modified_description( + description: LidarrSensorEntityDescription, mount: LidarrRootFolder +) -> tuple[LidarrSensorEntityDescription, str]: + """Return modified description and folder name.""" + desc = deepcopy(description) + name = mount.path.rsplit("/")[-1].rsplit("\\")[-1] + desc.key = f"{description.key}_{name}" + desc.name = f"{description.name} {name}".capitalize() + return desc, name + + +@dataclass +class LidarrSensorEntityDescriptionMixIn(Generic[T]): + """Mixin for required keys.""" + + value_fn: Callable[[T, str], str] + + +@dataclass +class LidarrSensorEntityDescription( + SensorEntityDescription, LidarrSensorEntityDescriptionMixIn, Generic[T] +): + """Class to describe a Lidarr sensor.""" + + attributes_fn: Callable[ + [T], dict[str, StateType | datetime] | None + ] = lambda _: None + description_fn: Callable[ + [LidarrSensorEntityDescription, LidarrRootFolder], + tuple[LidarrSensorEntityDescription, str] | None, + ] = lambda _, __: None + + +SENSOR_TYPES: dict[str, LidarrSensorEntityDescription] = { + "disk_space": LidarrSensorEntityDescription( + key="disk_space", + name="Disk space", + native_unit_of_measurement=DATA_GIGABYTES, + icon="mdi:harddisk", + value_fn=get_space, + state_class=SensorStateClass.TOTAL, + description_fn=get_modified_description, + ), + "queue": LidarrSensorEntityDescription( + key="queue", + name="Queue", + native_unit_of_measurement="Albums", + icon="mdi:download", + value_fn=lambda data, _: data.totalRecords, + state_class=SensorStateClass.TOTAL, + attributes_fn=lambda data: {i.title: queue_str(i) for i in data.records}, + ), + "wanted": LidarrSensorEntityDescription( + key="wanted", + name="Wanted", + native_unit_of_measurement="Albums", + icon="mdi:music", + value_fn=lambda data, _: data.totalRecords, + state_class=SensorStateClass.TOTAL, + entity_registry_enabled_default=False, + attributes_fn=lambda data: { + album.title: album.artist.artistName for album in data.records + }, + ), +} + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Lidarr sensors based on a config entry.""" + coordinators: dict[str, LidarrDataUpdateCoordinator] = hass.data[DOMAIN][ + entry.entry_id + ] + entities = [] + for coordinator_type, description in SENSOR_TYPES.items(): + coordinator = coordinators[coordinator_type] + if coordinator_type != "disk_space": + entities.append(LidarrSensor(coordinator, description)) + else: + entities.extend( + LidarrSensor(coordinator, *get_modified_description(description, mount)) + for mount in coordinator.data + if description.description_fn + ) + async_add_entities(entities) + + +class LidarrSensor(LidarrEntity, SensorEntity): + """Implementation of the Lidarr sensor.""" + + entity_description: LidarrSensorEntityDescription + + def __init__( + self, + coordinator: LidarrDataUpdateCoordinator, + description: LidarrSensorEntityDescription, + folder_name: str = "", + ) -> None: + """Create Lidarr entity.""" + super().__init__(coordinator, description) + self.folder_name = folder_name + + @property + def extra_state_attributes(self) -> dict[str, StateType | datetime] | None: + """Return the state attributes of the sensor.""" + return self.entity_description.attributes_fn(self.coordinator.data) + + @property + def native_value(self) -> StateType: + """Return the state of the sensor.""" + return self.entity_description.value_fn(self.coordinator.data, self.folder_name) + + +def queue_str(item: LidarrQueueItem) -> str: + """Return string description of queue item.""" + if ( + item.sizeleft > 0 + and item.timeleft == "00:00:00" + or not hasattr(item, "trackedDownloadState") + ): + return "stopped" + return item.trackedDownloadState diff --git a/homeassistant/components/lidarr/strings.json b/homeassistant/components/lidarr/strings.json new file mode 100644 index 00000000000..662d930cbef --- /dev/null +++ b/homeassistant/components/lidarr/strings.json @@ -0,0 +1,42 @@ +{ + "config": { + "step": { + "user": { + "description": "API key can be retrieved automatically if login credentials were not set in application.\nYour API key can be found in Settings > General in the Lidarr Web UI.", + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]", + "url": "[%key:common::config_flow::data::url%]", + "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]" + } + }, + "reauth_confirm": { + "title": "[%key:common::config_flow::title::reauth%]", + "description": "The Lidarr integration needs to be manually re-authenticated with the Lidarr API", + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "zeroconf_failed": "API key not found. Please enter it manually", + "wrong_app": "Incorrect application reached. Please try again", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + } + }, + "options": { + "step": { + "init": { + "data": { + "upcoming_days": "Number of upcoming days to display on calendar", + "max_records": "Number of maximum records to display on wanted and queue" + } + } + } + } +} diff --git a/homeassistant/components/lidarr/translations/en.json b/homeassistant/components/lidarr/translations/en.json new file mode 100644 index 00000000000..03c7435eb36 --- /dev/null +++ b/homeassistant/components/lidarr/translations/en.json @@ -0,0 +1,42 @@ +{ + "config": { + "abort": { + "already_configured": "Service is already configured", + "reauth_successful": "Re-authentication was successful" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "zeroconf_failed": "API key not found. Please enter it manually", + "wrong_app": "Incorrect application reached. Please try again", + "unknown": "Unexpected error" + }, + "step": { + "reauth_confirm": { + "description": "The Lidarr integration needs to be manually re-authenticated with the Lidarr API", + "title": "Reauthenticate Integration", + "data": { + "api_key": "API Key" + } + }, + "user": { + "description": "API key can be retrieved automatically if login credentials were not set in application.\nYour API key can be found in Settings > General in the Lidarr Web UI.", + "data": { + "api_key": "API Key", + "url": "URL", + "verify_ssl": "Verify SSL certificate" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "upcoming_days": "Number of upcoming days to display on calendar", + "max_records": "Number of maximum records to display on wanted and queue" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 208a7e02efc..038596d3e23 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -204,6 +204,7 @@ FLOWS = { "laundrify", "led_ble", "lg_soundbar", + "lidarr", "life360", "lifx", "litejet", diff --git a/requirements_all.txt b/requirements_all.txt index 6a12a38fdc5..74f93f267c8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -231,6 +231,7 @@ aiopvapi==2.0.1 # homeassistant.components.pvpc_hourly_pricing aiopvpc==3.0.0 +# homeassistant.components.lidarr # homeassistant.components.sonarr aiopyarr==22.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d60c9146079..36822d4cbae 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -206,6 +206,7 @@ aiopvapi==2.0.1 # homeassistant.components.pvpc_hourly_pricing aiopvpc==3.0.0 +# homeassistant.components.lidarr # homeassistant.components.sonarr aiopyarr==22.7.0 diff --git a/tests/components/lidarr/__init__.py b/tests/components/lidarr/__init__.py new file mode 100644 index 00000000000..8c1220e4c6c --- /dev/null +++ b/tests/components/lidarr/__init__.py @@ -0,0 +1,53 @@ +"""Tests for the Lidarr component.""" +from aiopyarr.lidarr_client import LidarrClient + +from homeassistant.components.lidarr.const import DOMAIN +from homeassistant.const import ( + CONF_API_KEY, + CONF_URL, + CONF_VERIFY_SSL, + CONTENT_TYPE_JSON, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from tests.common import MockConfigEntry, load_fixture +from tests.test_util.aiohttp import AiohttpClientMocker + +BASE_PATH = "" +API_KEY = "1234567890abcdef1234567890abcdef" +URL = "http://127.0.0.1:8686" +client = LidarrClient(session=async_get_clientsession, api_token=API_KEY, url=URL) +API_URL = f"{URL}/api/{client._host.api_ver}" + +MOCK_REAUTH_INPUT = {CONF_API_KEY: "new_key"} + +MOCK_USER_INPUT = { + CONF_URL: URL, + CONF_VERIFY_SSL: False, +} + +CONF_DATA = MOCK_USER_INPUT | {CONF_API_KEY: API_KEY} + + +def mock_connection( + aioclient_mock: AiohttpClientMocker, + url: str = API_URL, +) -> None: + """Mock lidarr connection.""" + aioclient_mock.get( + f"{url}/system/status", + text=load_fixture("lidarr/system-status.json"), + headers={"Content-Type": CONTENT_TYPE_JSON}, + ) + + +def create_entry(hass: HomeAssistant) -> MockConfigEntry: + """Create Efergy entry in Home Assistant.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=CONF_DATA, + ) + + entry.add_to_hass(hass) + return entry diff --git a/tests/components/lidarr/fixtures/system-status.json b/tests/components/lidarr/fixtures/system-status.json new file mode 100644 index 00000000000..6baa9428ff6 --- /dev/null +++ b/tests/components/lidarr/fixtures/system-status.json @@ -0,0 +1,29 @@ +{ + "version": "10.0.0.34882", + "buildTime": "2020-09-01T23:23:23.9621974Z", + "isDebug": true, + "isProduction": false, + "isAdmin": false, + "isUserInteractive": true, + "startupPath": "C:\\ProgramData\\Radarr", + "appData": "C:\\ProgramData\\Radarr", + "osName": "Windows", + "osVersion": "10.0.18363.0", + "isNetCore": true, + "isMono": false, + "isMonoRuntime": false, + "isLinux": false, + "isOsx": false, + "isWindows": true, + "isDocker": false, + "mode": "console", + "branch": "nightly", + "authentication": "none", + "sqliteVersion": "3.32.1", + "migrationVersion": 180, + "urlBase": "", + "runtimeVersion": "3.1.10", + "runtimeName": "netCore", + "startTime": "2020-09-01T23:50:20.2415965Z", + "packageUpdateMechanism": "builtIn" +} diff --git a/tests/components/lidarr/test_config_flow.py b/tests/components/lidarr/test_config_flow.py new file mode 100644 index 00000000000..0ec48439012 --- /dev/null +++ b/tests/components/lidarr/test_config_flow.py @@ -0,0 +1,142 @@ +"""Test Lidarr config flow.""" +from unittest.mock import patch + +from aiopyarr import ArrAuthenticationException, ArrConnectionException, ArrException + +from homeassistant import data_entry_flow +from homeassistant.components.lidarr.const import DEFAULT_NAME, DOMAIN +from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER +from homeassistant.const import CONF_API_KEY, CONF_SOURCE +from homeassistant.core import HomeAssistant + +from . import API_KEY, CONF_DATA, MOCK_USER_INPUT, create_entry, mock_connection + +from tests.test_util.aiohttp import AiohttpClientMocker + + +def _patch_client(): + return patch( + "homeassistant.components.lidarr.config_flow.LidarrClient.async_get_system_status" + ) + + +async def test_flow_user_form( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test that the user set up form is served.""" + mock_connection(aioclient_mock) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_USER}, + ) + with patch( + "homeassistant.components.lidarr.config_flow.LidarrClient.async_try_zeroconf", + return_value=("/api/v3", API_KEY, ""), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=MOCK_USER_INPUT, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == DEFAULT_NAME + assert result["data"] == CONF_DATA + + +async def test_flow_user_invalid_auth(hass: HomeAssistant) -> None: + """Test invalid authentication.""" + with _patch_client() as client: + client.side_effect = ArrAuthenticationException + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_USER}, + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=CONF_DATA, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"]["base"] == "invalid_auth" + + +async def test_flow_user_cannot_connect(hass: HomeAssistant) -> None: + """Test connection error.""" + with _patch_client() as client: + client.side_effect = ArrConnectionException + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_USER}, + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=CONF_DATA, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"]["base"] == "cannot_connect" + + +async def test_flow_user_unknown_error(hass: HomeAssistant) -> None: + """Test unknown error.""" + with _patch_client() as client: + client.side_effect = ArrException + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_USER}, + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=CONF_DATA, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"]["base"] == "unknown" + + +async def test_flow_user_failed_zeroconf(hass: HomeAssistant) -> None: + """Test zero configuration failed.""" + with _patch_client() as client: + client.return_value = "zeroconf_failed" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_USER}, + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=CONF_DATA, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"]["base"] == "zeroconf_failed" + + +async def test_flow_reauth( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test reauth.""" + entry = create_entry(hass) + mock_connection(aioclient_mock) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + CONF_SOURCE: SOURCE_REAUTH, + "entry_id": entry.entry_id, + "unique_id": entry.unique_id, + }, + data=CONF_DATA, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "reauth_confirm" + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_API_KEY: "abc123"}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "reauth_successful" + assert entry.data[CONF_API_KEY] == "abc123" From 41d2ac3943ab7add357a9e01dd4a5b6c6e9b4b12 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 20 Sep 2022 17:55:13 +0200 Subject: [PATCH 588/955] Cleanup MediaClass and MediaType in tests (#78817) Cleanup MediaClass/MediaType in tests --- .../components/arcam_fmj/test_media_player.py | 8 +- tests/components/cast/test_media_player.py | 11 +- tests/components/directv/test_media_player.py | 14 +- .../forked_daapd/test_media_player.py | 49 ++++--- .../components/google_assistant/test_trait.py | 7 +- tests/components/heos/test_media_player.py | 18 ++- tests/components/media_player/test_init.py | 29 +++-- tests/components/media_source/test_init.py | 4 +- tests/components/media_source/test_models.py | 24 ++-- tests/components/plex/test_media_search.py | 44 +++---- tests/components/plex/test_playback.py | 24 ++-- tests/components/plex/test_services.py | 28 ++-- tests/components/ps4/test_init.py | 14 +- tests/components/ps4/test_media_player.py | 13 +- tests/components/roku/test_media_player.py | 121 ++++++++---------- .../components/samsungtv/test_media_player.py | 18 ++- tests/components/sonos/test_plex_playback.py | 16 +-- tests/components/tts/test_init.py | 18 +-- .../unifiprotect/test_media_source.py | 10 +- tests/components/webostv/test_media_player.py | 12 +- 20 files changed, 224 insertions(+), 258 deletions(-) diff --git a/tests/components/arcam_fmj/test_media_player.py b/tests/components/arcam_fmj/test_media_player.py index d3b221a05fd..19a0f456d7f 100644 --- a/tests/components/arcam_fmj/test_media_player.py +++ b/tests/components/arcam_fmj/test_media_player.py @@ -5,12 +5,12 @@ from unittest.mock import ANY, PropertyMock, patch from arcam.fmj import DecodeMode2CH, DecodeModeMCH, SourceCodes import pytest -from homeassistant.components.media_player.const import ( +from homeassistant.components.media_player import ( ATTR_INPUT_SOURCE, ATTR_SOUND_MODE, ATTR_SOUND_MODE_LIST, - MEDIA_TYPE_MUSIC, SERVICE_SELECT_SOURCE, + MediaType, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -226,8 +226,8 @@ async def test_set_volume_level(player, state, volume, call): @pytest.mark.parametrize( "source, media_content_type", [ - (SourceCodes.DAB, MEDIA_TYPE_MUSIC), - (SourceCodes.FM, MEDIA_TYPE_MUSIC), + (SourceCodes.DAB, MediaType.MUSIC), + (SourceCodes.FM, MediaType.MUSIC), (SourceCodes.PVR, None), (None, None), ], diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index 9d3e1a6d534..a983a51e99d 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -16,10 +16,7 @@ import yarl from homeassistant.components import media_player, tts from homeassistant.components.cast import media_player as cast from homeassistant.components.cast.media_player import ChromecastInfo -from homeassistant.components.media_player import BrowseMedia -from homeassistant.components.media_player.const import ( - MEDIA_CLASS_APP, - MEDIA_CLASS_PLAYLIST, +from homeassistant.components.media_player import ( SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, @@ -31,6 +28,8 @@ from homeassistant.components.media_player.const import ( SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, + BrowseMedia, + MediaClass, ) from homeassistant.config import async_process_ha_core_config from homeassistant.const import ( @@ -2110,7 +2109,7 @@ async def test_cast_platform_browse_media(hass: HomeAssistant, hass_ws_client): return_value=[ BrowseMedia( title="Spotify", - media_class=MEDIA_CLASS_APP, + media_class=MediaClass.APP, media_content_id="", media_content_type="spotify", thumbnail="https://brands.home-assistant.io/_/spotify/logo.png", @@ -2122,7 +2121,7 @@ async def test_cast_platform_browse_media(hass: HomeAssistant, hass_ws_client): async_browse_media=AsyncMock( return_value=BrowseMedia( title="Spotify Favourites", - media_class=MEDIA_CLASS_PLAYLIST, + media_class=MediaClass.PLAYLIST, media_content_id="", media_content_type="spotify", can_play=True, diff --git a/tests/components/directv/test_media_player.py b/tests/components/directv/test_media_player.py index 1c03758af72..84f72a5409e 100644 --- a/tests/components/directv/test_media_player.py +++ b/tests/components/directv/test_media_player.py @@ -12,8 +12,7 @@ from homeassistant.components.directv.media_player import ( ATTR_MEDIA_RECORDED, ATTR_MEDIA_START_TIME, ) -from homeassistant.components.media_player import MediaPlayerDeviceClass -from homeassistant.components.media_player.const import ( +from homeassistant.components.media_player import ( ATTR_INPUT_SOURCE, ATTR_MEDIA_ALBUM_NAME, ATTR_MEDIA_ARTIST, @@ -27,9 +26,6 @@ from homeassistant.components.media_player.const import ( ATTR_MEDIA_SERIES_TITLE, ATTR_MEDIA_TITLE, DOMAIN as MP_DOMAIN, - MEDIA_TYPE_MOVIE, - MEDIA_TYPE_MUSIC, - MEDIA_TYPE_TVSHOW, SERVICE_PLAY_MEDIA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, @@ -39,6 +35,8 @@ from homeassistant.components.media_player.const import ( SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, + MediaPlayerDeviceClass, + MediaType, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -218,7 +216,7 @@ async def test_check_attributes( assert state.state == STATE_PLAYING assert state.attributes.get(ATTR_MEDIA_CONTENT_ID) == "17016356" - assert state.attributes.get(ATTR_MEDIA_CONTENT_TYPE) == MEDIA_TYPE_MOVIE + assert state.attributes.get(ATTR_MEDIA_CONTENT_TYPE) == MediaType.MOVIE assert state.attributes.get(ATTR_MEDIA_DURATION) == 7200 assert state.attributes.get(ATTR_MEDIA_POSITION) == 4437 assert state.attributes.get(ATTR_MEDIA_POSITION_UPDATED_AT) @@ -237,7 +235,7 @@ async def test_check_attributes( assert state.state == STATE_PLAYING assert state.attributes.get(ATTR_MEDIA_CONTENT_ID) == "4405732" - assert state.attributes.get(ATTR_MEDIA_CONTENT_TYPE) == MEDIA_TYPE_TVSHOW + assert state.attributes.get(ATTR_MEDIA_CONTENT_TYPE) == MediaType.TVSHOW assert state.attributes.get(ATTR_MEDIA_DURATION) == 1791 assert state.attributes.get(ATTR_MEDIA_POSITION) == 263 assert state.attributes.get(ATTR_MEDIA_POSITION_UPDATED_AT) @@ -256,7 +254,7 @@ async def test_check_attributes( assert state.state == STATE_PLAYING assert state.attributes.get(ATTR_MEDIA_CONTENT_ID) == "76917562" - assert state.attributes.get(ATTR_MEDIA_CONTENT_TYPE) == MEDIA_TYPE_MUSIC + assert state.attributes.get(ATTR_MEDIA_CONTENT_TYPE) == MediaType.MUSIC assert state.attributes.get(ATTR_MEDIA_DURATION) == 86400 assert state.attributes.get(ATTR_MEDIA_POSITION) == 15050 assert state.attributes.get(ATTR_MEDIA_POSITION_UPDATED_AT) diff --git a/tests/components/forked_daapd/test_media_player.py b/tests/components/forked_daapd/test_media_player.py index c18b8df3f1c..8035ec99777 100644 --- a/tests/components/forked_daapd/test_media_player.py +++ b/tests/components/forked_daapd/test_media_player.py @@ -19,23 +19,6 @@ from homeassistant.components.forked_daapd.const import ( SUPPORTED_FEATURES_ZONE, ) from homeassistant.components.media_player import ( - SERVICE_CLEAR_PLAYLIST, - SERVICE_MEDIA_NEXT_TRACK, - SERVICE_MEDIA_PAUSE, - SERVICE_MEDIA_PLAY, - SERVICE_MEDIA_PREVIOUS_TRACK, - SERVICE_MEDIA_SEEK, - SERVICE_MEDIA_STOP, - SERVICE_PLAY_MEDIA, - SERVICE_SELECT_SOURCE, - SERVICE_SHUFFLE_SET, - SERVICE_TOGGLE, - SERVICE_TURN_OFF, - SERVICE_TURN_ON, - SERVICE_VOLUME_MUTE, - SERVICE_VOLUME_SET, -) -from homeassistant.components.media_player.const import ( ATTR_INPUT_SOURCE, ATTR_MEDIA_ALBUM_ARTIST, ATTR_MEDIA_ALBUM_NAME, @@ -51,8 +34,22 @@ from homeassistant.components.media_player.const import ( ATTR_MEDIA_VOLUME_LEVEL, ATTR_MEDIA_VOLUME_MUTED, DOMAIN as MP_DOMAIN, - MEDIA_TYPE_MUSIC, - MEDIA_TYPE_TVSHOW, + SERVICE_CLEAR_PLAYLIST, + SERVICE_MEDIA_NEXT_TRACK, + SERVICE_MEDIA_PAUSE, + SERVICE_MEDIA_PLAY, + SERVICE_MEDIA_PREVIOUS_TRACK, + SERVICE_MEDIA_SEEK, + SERVICE_MEDIA_STOP, + SERVICE_PLAY_MEDIA, + SERVICE_SELECT_SOURCE, + SERVICE_SHUFFLE_SET, + SERVICE_TOGGLE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + SERVICE_VOLUME_MUTE, + SERVICE_VOLUME_SET, + MediaType, ) from homeassistant.config_entries import SOURCE_USER from homeassistant.const import ( @@ -360,7 +357,7 @@ def test_master_state(hass, mock_api_object): assert not state.attributes[ATTR_MEDIA_VOLUME_MUTED] assert state.attributes[ATTR_MEDIA_VOLUME_LEVEL] == 0.2 assert state.attributes[ATTR_MEDIA_CONTENT_ID] == 12322 - assert state.attributes[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC + assert state.attributes[ATTR_MEDIA_CONTENT_TYPE] == MediaType.MUSIC assert state.attributes[ATTR_MEDIA_DURATION] == 0.05 assert state.attributes[ATTR_MEDIA_POSITION] == 0.005 assert state.attributes[ATTR_MEDIA_TITLE] == "No album" # reversed for url @@ -556,7 +553,7 @@ async def test_async_play_media_from_paused(hass, mock_api_object): TEST_MASTER_ENTITY_NAME, SERVICE_PLAY_MEDIA, { - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, + ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC, ATTR_MEDIA_CONTENT_ID: "http://example.com/somefile.mp3", }, ) @@ -580,7 +577,7 @@ async def test_async_play_media_from_stopped( TEST_MASTER_ENTITY_NAME, SERVICE_PLAY_MEDIA, { - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, + ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC, ATTR_MEDIA_CONTENT_ID: "http://example.com/somefile.mp3", }, ) @@ -597,7 +594,7 @@ async def test_async_play_media_unsupported(hass, mock_api_object): TEST_MASTER_ENTITY_NAME, SERVICE_PLAY_MEDIA, { - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_TVSHOW, + ATTR_MEDIA_CONTENT_TYPE: MediaType.TVSHOW, ATTR_MEDIA_CONTENT_ID: "wontwork.mp4", }, ) @@ -615,7 +612,7 @@ async def test_async_play_media_tts_timeout(hass, mock_api_object): TEST_MASTER_ENTITY_NAME, SERVICE_PLAY_MEDIA, { - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, + ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC, ATTR_MEDIA_CONTENT_ID: "http://example.com/somefile.mp3", }, ) @@ -724,7 +721,7 @@ async def test_librespot_java_play_media(hass, pipe_control_api_object): TEST_MASTER_ENTITY_NAME, SERVICE_PLAY_MEDIA, { - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, + ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC, ATTR_MEDIA_CONTENT_ID: "http://example.com/somefile.mp3", }, ) @@ -746,7 +743,7 @@ async def test_librespot_java_play_media_pause_timeout(hass, pipe_control_api_ob TEST_MASTER_ENTITY_NAME, SERVICE_PLAY_MEDIA, { - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, + ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC, ATTR_MEDIA_CONTENT_ID: "http://example.com/somefile.mp3", }, ) diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index 6d391df7439..9d80c7ff507 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -29,10 +29,7 @@ from homeassistant.components import ( ) from homeassistant.components.google_assistant import const, error, helpers, trait from homeassistant.components.google_assistant.error import SmartHomeError -from homeassistant.components.media_player.const import ( - MEDIA_TYPE_CHANNEL, - SERVICE_PLAY_MEDIA, -) +from homeassistant.components.media_player import SERVICE_PLAY_MEDIA, MediaType from homeassistant.config import async_process_ha_core_config from homeassistant.const import ( ATTR_ASSUMED_STATE, @@ -3169,7 +3166,7 @@ async def test_channel(hass): assert media_player_calls[0].data == { ATTR_ENTITY_ID: "media_player.demo", media_player.ATTR_MEDIA_CONTENT_ID: "1", - media_player.ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_CHANNEL, + media_player.ATTR_MEDIA_CONTENT_TYPE: MediaType.CHANNEL, } with pytest.raises(SmartHomeError, match="Channel is not available"): diff --git a/tests/components/heos/test_media_player.py b/tests/components/heos/test_media_player.py index ee75793df8e..5e43d6989a8 100644 --- a/tests/components/heos/test_media_player.py +++ b/tests/components/heos/test_media_player.py @@ -10,7 +10,7 @@ from homeassistant.components.heos.const import ( DOMAIN, SIGNAL_HEOS_UPDATED, ) -from homeassistant.components.media_player.const import ( +from homeassistant.components.media_player import ( ATTR_GROUP_MEMBERS, ATTR_INPUT_SOURCE, ATTR_INPUT_SOURCE_LIST, @@ -27,9 +27,6 @@ from homeassistant.components.media_player.const import ( ATTR_MEDIA_VOLUME_LEVEL, ATTR_MEDIA_VOLUME_MUTED, DOMAIN as MEDIA_PLAYER_DOMAIN, - MEDIA_TYPE_MUSIC, - MEDIA_TYPE_PLAYLIST, - MEDIA_TYPE_URL, SERVICE_CLEAR_PLAYLIST, SERVICE_JOIN, SERVICE_PLAY_MEDIA, @@ -40,6 +37,7 @@ from homeassistant.components.media_player.const import ( SUPPORT_PLAY, SUPPORT_PREVIOUS_TRACK, SUPPORT_STOP, + MediaType, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -77,7 +75,7 @@ async def test_state_attributes(hass, config_entry, config, controller): assert state.attributes[ATTR_MEDIA_VOLUME_LEVEL] == 0.25 assert not state.attributes[ATTR_MEDIA_VOLUME_MUTED] assert state.attributes[ATTR_MEDIA_CONTENT_ID] == "1" - assert state.attributes[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC + assert state.attributes[ATTR_MEDIA_CONTENT_TYPE] == MediaType.MUSIC assert ATTR_MEDIA_DURATION not in state.attributes assert ATTR_MEDIA_POSITION not in state.attributes assert state.attributes[ATTR_MEDIA_TITLE] == "Song" @@ -611,7 +609,7 @@ async def test_play_media_url(hass, config_entry, config, controller, caplog): SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: "media_player.test_player", - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_URL, + ATTR_MEDIA_CONTENT_TYPE: MediaType.URL, ATTR_MEDIA_CONTENT_ID: url, }, blocking=True, @@ -634,7 +632,7 @@ async def test_play_media_music(hass, config_entry, config, controller, caplog): SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: "media_player.test_player", - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, + ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC, ATTR_MEDIA_CONTENT_ID: url, }, blocking=True, @@ -708,7 +706,7 @@ async def test_play_media_playlist( SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: "media_player.test_player", - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_PLAYLIST, + ATTR_MEDIA_CONTENT_TYPE: MediaType.PLAYLIST, ATTR_MEDIA_CONTENT_ID: playlist.name, }, blocking=True, @@ -723,7 +721,7 @@ async def test_play_media_playlist( SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: "media_player.test_player", - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_PLAYLIST, + ATTR_MEDIA_CONTENT_TYPE: MediaType.PLAYLIST, ATTR_MEDIA_CONTENT_ID: playlist.name, ATTR_MEDIA_ENQUEUE: True, }, @@ -737,7 +735,7 @@ async def test_play_media_playlist( SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: "media_player.test_player", - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_PLAYLIST, + ATTR_MEDIA_CONTENT_TYPE: MediaType.PLAYLIST, ATTR_MEDIA_CONTENT_ID: "Invalid", }, blocking=True, diff --git a/tests/components/media_player/test_init.py b/tests/components/media_player/test_init.py index e7946d447e9..22206133fca 100644 --- a/tests/components/media_player/test_init.py +++ b/tests/components/media_player/test_init.py @@ -6,8 +6,12 @@ from unittest.mock import patch import pytest import voluptuous as vol -from homeassistant.components import media_player -from homeassistant.components.media_player.browse_media import BrowseMedia +from homeassistant.components.media_player import ( + BrowseMedia, + MediaClass, + MediaPlayerEnqueue, + MediaPlayerEntityFeature, +) from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF from homeassistant.setup import async_setup_component @@ -133,11 +137,11 @@ async def test_media_browse(hass, hass_ws_client): with patch( "homeassistant.components.demo.media_player.YOUTUBE_PLAYER_SUPPORT", - media_player.MediaPlayerEntityFeature.BROWSE_MEDIA, + MediaPlayerEntityFeature.BROWSE_MEDIA, ), patch( "homeassistant.components.media_player.MediaPlayerEntity.async_browse_media", return_value=BrowseMedia( - media_class=media_player.MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_id="mock-id", media_content_type="mock-type", title="Mock Title", @@ -176,7 +180,7 @@ async def test_media_browse(hass, hass_ws_client): with patch( "homeassistant.components.demo.media_player.YOUTUBE_PLAYER_SUPPORT", - media_player.MediaPlayerEntityFeature.BROWSE_MEDIA, + MediaPlayerEntityFeature.BROWSE_MEDIA, ), patch( "homeassistant.components.media_player.MediaPlayerEntity.async_browse_media", return_value={"bla": "yo"}, @@ -207,8 +211,7 @@ async def test_group_members_available_when_off(hass): # Fake group support for DemoYoutubePlayer with patch( "homeassistant.components.demo.media_player.YOUTUBE_PLAYER_SUPPORT", - media_player.MediaPlayerEntityFeature.GROUPING - | media_player.MediaPlayerEntityFeature.TURN_OFF, + MediaPlayerEntityFeature.GROUPING | MediaPlayerEntityFeature.TURN_OFF, ): await hass.services.async_call( "media_player", @@ -225,12 +228,12 @@ async def test_group_members_available_when_off(hass): @pytest.mark.parametrize( "input,expected", ( - (True, media_player.MediaPlayerEnqueue.ADD), - (False, media_player.MediaPlayerEnqueue.PLAY), - ("play", media_player.MediaPlayerEnqueue.PLAY), - ("next", media_player.MediaPlayerEnqueue.NEXT), - ("add", media_player.MediaPlayerEnqueue.ADD), - ("replace", media_player.MediaPlayerEnqueue.REPLACE), + (True, MediaPlayerEnqueue.ADD), + (False, MediaPlayerEnqueue.PLAY), + ("play", MediaPlayerEnqueue.PLAY), + ("next", MediaPlayerEnqueue.NEXT), + ("add", MediaPlayerEnqueue.ADD), + ("replace", MediaPlayerEnqueue.REPLACE), ), ) async def test_enqueue_rewrite(hass, input, expected): diff --git a/tests/components/media_source/test_init.py b/tests/components/media_source/test_init.py index 33dd263c46c..a87e1e529b9 100644 --- a/tests/components/media_source/test_init.py +++ b/tests/components/media_source/test_init.py @@ -5,7 +5,7 @@ import pytest import yarl from homeassistant.components import media_source -from homeassistant.components.media_player import MEDIA_CLASS_DIRECTORY, BrowseError +from homeassistant.components.media_player import BrowseError, MediaClass from homeassistant.components.media_source import const, models from homeassistant.setup import async_setup_component @@ -160,7 +160,7 @@ async def test_websocket_browse_media(hass, hass_ws_client): domain=media_source.DOMAIN, identifier="/media", title="Local Media", - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_type="listing", can_play=False, can_expand=True, diff --git a/tests/components/media_source/test_models.py b/tests/components/media_source/test_models.py index 8372382bb7a..edb2b219831 100644 --- a/tests/components/media_source/test_models.py +++ b/tests/components/media_source/test_models.py @@ -1,9 +1,5 @@ """Test Media Source model methods.""" -from homeassistant.components.media_player.const import ( - MEDIA_CLASS_DIRECTORY, - MEDIA_CLASS_MUSIC, - MEDIA_TYPE_MUSIC, -) +from homeassistant.components.media_player import MediaClass, MediaType from homeassistant.components.media_source import const, models @@ -12,19 +8,19 @@ async def test_browse_media_as_dict(): base = models.BrowseMediaSource( domain=const.DOMAIN, identifier="media", - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_type="folder", title="media/", can_play=False, can_expand=True, - children_media_class=MEDIA_CLASS_MUSIC, + children_media_class=MediaClass.MUSIC, ) base.children = [ models.BrowseMediaSource( domain=const.DOMAIN, identifier="media/test.mp3", - media_class=MEDIA_CLASS_MUSIC, - media_content_type=MEDIA_TYPE_MUSIC, + media_class=MediaClass.MUSIC, + media_content_type=MediaType.MUSIC, title="test.mp3", can_play=True, can_expand=False, @@ -33,15 +29,15 @@ async def test_browse_media_as_dict(): item = base.as_dict() assert item["title"] == "media/" - assert item["media_class"] == MEDIA_CLASS_DIRECTORY + assert item["media_class"] == MediaClass.DIRECTORY assert item["media_content_type"] == "folder" assert item["media_content_id"] == f"{const.URI_SCHEME}{const.DOMAIN}/media" assert not item["can_play"] assert item["can_expand"] - assert item["children_media_class"] == MEDIA_CLASS_MUSIC + assert item["children_media_class"] == MediaClass.MUSIC assert len(item["children"]) == 1 assert item["children"][0]["title"] == "test.mp3" - assert item["children"][0]["media_class"] == MEDIA_CLASS_MUSIC + assert item["children"][0]["media_class"] == MediaClass.MUSIC async def test_browse_media_parent_no_children(): @@ -49,7 +45,7 @@ async def test_browse_media_parent_no_children(): base = models.BrowseMediaSource( domain=const.DOMAIN, identifier="media", - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_type="folder", title="media/", can_play=False, @@ -58,7 +54,7 @@ async def test_browse_media_parent_no_children(): item = base.as_dict() assert item["title"] == "media/" - assert item["media_class"] == MEDIA_CLASS_DIRECTORY + assert item["media_class"] == MediaClass.DIRECTORY assert item["media_content_type"] == "folder" assert item["media_content_id"] == f"{const.URI_SCHEME}{const.DOMAIN}/media" assert not item["can_play"] diff --git a/tests/components/plex/test_media_search.py b/tests/components/plex/test_media_search.py index e5c19d31c4e..7a81224259c 100644 --- a/tests/components/plex/test_media_search.py +++ b/tests/components/plex/test_media_search.py @@ -4,16 +4,12 @@ from unittest.mock import patch from plexapi.exceptions import BadRequest, NotFound import pytest -from homeassistant.components.media_player.const import ( +from homeassistant.components.media_player import ( ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, DOMAIN as MEDIA_PLAYER_DOMAIN, - MEDIA_TYPE_EPISODE, - MEDIA_TYPE_MOVIE, - MEDIA_TYPE_MUSIC, - MEDIA_TYPE_PLAYLIST, - MEDIA_TYPE_VIDEO, SERVICE_PLAY_MEDIA, + MediaType, ) from homeassistant.components.plex.const import DOMAIN from homeassistant.components.plex.errors import MediaNotFound @@ -59,7 +55,7 @@ async def test_media_lookups(hass, mock_plex_server, requests_mock, playqueue_cr SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player_id, - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_EPISODE, + ATTR_MEDIA_CONTENT_TYPE: MediaType.EPISODE, ATTR_MEDIA_CONTENT_ID: payload, }, True, @@ -72,7 +68,7 @@ async def test_media_lookups(hass, mock_plex_server, requests_mock, playqueue_cr SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player_id, - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_EPISODE, + ATTR_MEDIA_CONTENT_TYPE: MediaType.EPISODE, ATTR_MEDIA_CONTENT_ID: '{"library_name": "TV Shows", "show_name": "TV Show"}', }, True, @@ -84,7 +80,7 @@ async def test_media_lookups(hass, mock_plex_server, requests_mock, playqueue_cr SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player_id, - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_EPISODE, + ATTR_MEDIA_CONTENT_TYPE: MediaType.EPISODE, ATTR_MEDIA_CONTENT_ID: '{"library_name": "TV Shows", "episode_name": "An Episode"}', }, True, @@ -98,7 +94,7 @@ async def test_media_lookups(hass, mock_plex_server, requests_mock, playqueue_cr SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player_id, - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_EPISODE, + ATTR_MEDIA_CONTENT_TYPE: MediaType.EPISODE, ATTR_MEDIA_CONTENT_ID: '{"library_name": "TV Shows", "show_name": "TV Show", "season_number": 1}', }, True, @@ -112,7 +108,7 @@ async def test_media_lookups(hass, mock_plex_server, requests_mock, playqueue_cr SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player_id, - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_EPISODE, + ATTR_MEDIA_CONTENT_TYPE: MediaType.EPISODE, ATTR_MEDIA_CONTENT_ID: '{"library_name": "TV Shows", "show_name": "TV Show", "season_number": 1, "episode_number": 3}', }, True, @@ -131,7 +127,7 @@ async def test_media_lookups(hass, mock_plex_server, requests_mock, playqueue_cr SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player_id, - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, + ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC, ATTR_MEDIA_CONTENT_ID: '{"library_name": "Music", "artist_name": "Artist"}', }, True, @@ -143,7 +139,7 @@ async def test_media_lookups(hass, mock_plex_server, requests_mock, playqueue_cr SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player_id, - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, + ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC, ATTR_MEDIA_CONTENT_ID: '{"library_name": "Music", "album_name": "Album"}', }, True, @@ -155,7 +151,7 @@ async def test_media_lookups(hass, mock_plex_server, requests_mock, playqueue_cr SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player_id, - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, + ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC, ATTR_MEDIA_CONTENT_ID: '{"library_name": "Music", "artist_name": "Artist", "track_name": "Track 3"}', }, True, @@ -169,7 +165,7 @@ async def test_media_lookups(hass, mock_plex_server, requests_mock, playqueue_cr SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player_id, - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, + ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC, ATTR_MEDIA_CONTENT_ID: '{"library_name": "Music", "artist_name": "Artist", "album_name": "Album"}', }, True, @@ -183,7 +179,7 @@ async def test_media_lookups(hass, mock_plex_server, requests_mock, playqueue_cr SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player_id, - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, + ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC, ATTR_MEDIA_CONTENT_ID: '{"library_name": "Music", "artist_name": "Artist", "album_name": "Album", "track_number": 3}', }, True, @@ -202,7 +198,7 @@ async def test_media_lookups(hass, mock_plex_server, requests_mock, playqueue_cr SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player_id, - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, + ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC, ATTR_MEDIA_CONTENT_ID: '{"library_name": "Music", "artist_name": "Artist", "album_name": "Album", "track_name": "Track 3"}', }, True, @@ -222,7 +218,7 @@ async def test_media_lookups(hass, mock_plex_server, requests_mock, playqueue_cr SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player_id, - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_VIDEO, + ATTR_MEDIA_CONTENT_TYPE: MediaType.VIDEO, ATTR_MEDIA_CONTENT_ID: '{"library_name": "Movies", "video_name": "Movie 1"}', }, True, @@ -234,7 +230,7 @@ async def test_media_lookups(hass, mock_plex_server, requests_mock, playqueue_cr SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player_id, - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MOVIE, + ATTR_MEDIA_CONTENT_TYPE: MediaType.MOVIE, ATTR_MEDIA_CONTENT_ID: '{"library_name": "Movies", "title": "Movie 1"}', }, True, @@ -248,7 +244,7 @@ async def test_media_lookups(hass, mock_plex_server, requests_mock, playqueue_cr SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player_id, - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_VIDEO, + ATTR_MEDIA_CONTENT_TYPE: MediaType.VIDEO, ATTR_MEDIA_CONTENT_ID: payload, }, True, @@ -263,7 +259,7 @@ async def test_media_lookups(hass, mock_plex_server, requests_mock, playqueue_cr SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player_id, - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_VIDEO, + ATTR_MEDIA_CONTENT_TYPE: MediaType.VIDEO, ATTR_MEDIA_CONTENT_ID: payload, }, True, @@ -276,7 +272,7 @@ async def test_media_lookups(hass, mock_plex_server, requests_mock, playqueue_cr SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player_id, - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_PLAYLIST, + ATTR_MEDIA_CONTENT_TYPE: MediaType.PLAYLIST, ATTR_MEDIA_CONTENT_ID: '{"playlist_name": "Playlist 1"}', }, True, @@ -289,7 +285,7 @@ async def test_media_lookups(hass, mock_plex_server, requests_mock, playqueue_cr SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player_id, - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_PLAYLIST, + ATTR_MEDIA_CONTENT_TYPE: MediaType.PLAYLIST, ATTR_MEDIA_CONTENT_ID: payload, }, True, @@ -303,7 +299,7 @@ async def test_media_lookups(hass, mock_plex_server, requests_mock, playqueue_cr SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player_id, - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_PLAYLIST, + ATTR_MEDIA_CONTENT_TYPE: MediaType.PLAYLIST, ATTR_MEDIA_CONTENT_ID: payload, }, True, diff --git a/tests/components/plex/test_playback.py b/tests/components/plex/test_playback.py index 10ab232f64f..6cd4e64f84b 100644 --- a/tests/components/plex/test_playback.py +++ b/tests/components/plex/test_playback.py @@ -4,12 +4,12 @@ from unittest.mock import Mock, patch import pytest -from homeassistant.components.media_player.const import ( +from homeassistant.components.media_player import ( ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, DOMAIN as MP_DOMAIN, - MEDIA_TYPE_MOVIE, SERVICE_PLAY_MEDIA, + MediaType, ) from homeassistant.components.plex.const import CONF_SERVER_IDENTIFIER, PLEX_URI_SCHEME from homeassistant.const import ATTR_ENTITY_ID @@ -69,13 +69,13 @@ async def test_media_player_playback( SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player, - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MOVIE, + ATTR_MEDIA_CONTENT_TYPE: MediaType.MOVIE, ATTR_MEDIA_CONTENT_ID: payload, }, True, ) assert not playmedia_mock.called - assert f"No {MEDIA_TYPE_MOVIE} results in 'Movies' for" in str(excinfo.value) + assert f"No {MediaType.MOVIE} results in 'Movies' for" in str(excinfo.value) movie1 = MockPlexMedia("Movie", "movie") movie2 = MockPlexMedia("Movie II", "movie") @@ -89,7 +89,7 @@ async def test_media_player_playback( SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player, - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MOVIE, + ATTR_MEDIA_CONTENT_TYPE: MediaType.MOVIE, ATTR_MEDIA_CONTENT_ID: '{"library_name": "Movies", "title": "Movie 1" }', }, True, @@ -104,7 +104,7 @@ async def test_media_player_playback( SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player, - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MOVIE, + ATTR_MEDIA_CONTENT_TYPE: MediaType.MOVIE, ATTR_MEDIA_CONTENT_ID: '{"library_name": "Movies", "title": "Movie 1", "resume": true}', }, True, @@ -119,7 +119,7 @@ async def test_media_player_playback( SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player, - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MOVIE, + ATTR_MEDIA_CONTENT_TYPE: MediaType.MOVIE, ATTR_MEDIA_CONTENT_ID: PLEX_URI_SCHEME + f"{DEFAULT_DATA[CONF_SERVER_IDENTIFIER]}/1", }, @@ -134,7 +134,7 @@ async def test_media_player_playback( SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player, - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MOVIE, + ATTR_MEDIA_CONTENT_TYPE: MediaType.MOVIE, ATTR_MEDIA_CONTENT_ID: PLEX_URI_SCHEME + f"{DEFAULT_DATA[CONF_SERVER_IDENTIFIER]}/1?resume=1", }, @@ -150,7 +150,7 @@ async def test_media_player_playback( SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player, - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MOVIE, + ATTR_MEDIA_CONTENT_TYPE: MediaType.MOVIE, ATTR_MEDIA_CONTENT_ID: PLEX_URI_SCHEME + "1", }, True, @@ -166,7 +166,7 @@ async def test_media_player_playback( SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player, - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MOVIE, + ATTR_MEDIA_CONTENT_TYPE: MediaType.MOVIE, ATTR_MEDIA_CONTENT_ID: '{"library_name": "Movies", "title": "Movie" }', }, True, @@ -184,7 +184,7 @@ async def test_media_player_playback( SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player, - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MOVIE, + ATTR_MEDIA_CONTENT_TYPE: MediaType.MOVIE, ATTR_MEDIA_CONTENT_ID: payload, }, True, @@ -202,7 +202,7 @@ async def test_media_player_playback( SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player, - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MOVIE, + ATTR_MEDIA_CONTENT_TYPE: MediaType.MOVIE, ATTR_MEDIA_CONTENT_ID: '{"library_name": "Movies", "title": "Movie", "allow_multiple": true }', }, True, diff --git a/tests/components/plex/test_services.py b/tests/components/plex/test_services.py index f0224230c3a..bb9888a8859 100644 --- a/tests/components/plex/test_services.py +++ b/tests/components/plex/test_services.py @@ -7,7 +7,7 @@ from plexapi.exceptions import NotFound import plexapi.playqueue import pytest -from homeassistant.components.media_player.const import MEDIA_TYPE_MUSIC +from homeassistant.components.media_player import MediaType from homeassistant.components.plex.const import ( CONF_SERVER, CONF_SERVER_IDENTIFIER, @@ -140,7 +140,7 @@ async def test_lookup_media_for_other_integrations( # Test with no Plex integration available with pytest.raises(HomeAssistantError) as excinfo: - process_plex_payload(hass, MEDIA_TYPE_MUSIC, CONTENT_ID) + process_plex_payload(hass, MediaType.MUSIC, CONTENT_ID) assert "Plex integration not configured" in str(excinfo.value) with patch( @@ -152,7 +152,7 @@ async def test_lookup_media_for_other_integrations( # Test with no Plex servers available with pytest.raises(HomeAssistantError) as excinfo: - process_plex_payload(hass, MEDIA_TYPE_MUSIC, CONTENT_ID) + process_plex_payload(hass, MediaType.MUSIC, CONTENT_ID) assert "No Plex servers available" in str(excinfo.value) # Complete setup of a Plex server @@ -161,28 +161,28 @@ async def test_lookup_media_for_other_integrations( # Test lookup success without playqueue result = process_plex_payload( - hass, MEDIA_TYPE_MUSIC, CONTENT_ID, supports_playqueues=False + hass, MediaType.MUSIC, CONTENT_ID, supports_playqueues=False ) assert isinstance(result.media, plexapi.audio.Artist) assert not result.shuffle # Test media key payload without playqueue result = process_plex_payload( - hass, MEDIA_TYPE_MUSIC, CONTENT_ID_KEY, supports_playqueues=False + hass, MediaType.MUSIC, CONTENT_ID_KEY, supports_playqueues=False ) assert isinstance(result.media, plexapi.audio.Track) assert not result.shuffle # Test with specified server without playqueue result = process_plex_payload( - hass, MEDIA_TYPE_MUSIC, CONTENT_ID_SERVER, supports_playqueues=False + hass, MediaType.MUSIC, CONTENT_ID_SERVER, supports_playqueues=False ) assert isinstance(result.media, plexapi.audio.Artist) assert not result.shuffle # Test shuffle without playqueue result = process_plex_payload( - hass, MEDIA_TYPE_MUSIC, CONTENT_ID_SHUFFLE, supports_playqueues=False + hass, MediaType.MUSIC, CONTENT_ID_SHUFFLE, supports_playqueues=False ) assert isinstance(result.media, plexapi.audio.Artist) assert result.shuffle @@ -190,12 +190,12 @@ async def test_lookup_media_for_other_integrations( # Test with media not found with patch("plexapi.library.LibrarySection.search", return_value=None): with pytest.raises(HomeAssistantError) as excinfo: - process_plex_payload(hass, MEDIA_TYPE_MUSIC, CONTENT_ID_BAD_MEDIA) - assert f"No {MEDIA_TYPE_MUSIC} results in 'Music' for" in str(excinfo.value) + process_plex_payload(hass, MediaType.MUSIC, CONTENT_ID_BAD_MEDIA) + assert f"No {MediaType.MUSIC} results in 'Music' for" in str(excinfo.value) # Test with playqueue requests_mock.get("https://1.2.3.4:32400/playQueues/1234", text=playqueue_1234) - result = process_plex_payload(hass, MEDIA_TYPE_MUSIC, CONTENT_ID_PLAYQUEUE) + result = process_plex_payload(hass, MediaType.MUSIC, CONTENT_ID_PLAYQUEUE) assert isinstance(result.media, plexapi.playqueue.PlayQueue) # Test with invalid playqueue @@ -203,12 +203,12 @@ async def test_lookup_media_for_other_integrations( "https://1.2.3.4:32400/playQueues/1235", status_code=HTTPStatus.NOT_FOUND ) with pytest.raises(HomeAssistantError) as excinfo: - process_plex_payload(hass, MEDIA_TYPE_MUSIC, CONTENT_ID_BAD_PLAYQUEUE) + process_plex_payload(hass, MediaType.MUSIC, CONTENT_ID_BAD_PLAYQUEUE) assert "PlayQueue '1235' could not be found" in str(excinfo.value) # Test playqueue is created with shuffle requests_mock.post("/playqueues", text=playqueue_created) - result = process_plex_payload(hass, MEDIA_TYPE_MUSIC, CONTENT_ID_SHUFFLE) + result = process_plex_payload(hass, MediaType.MUSIC, CONTENT_ID_SHUFFLE) assert isinstance(result.media, plexapi.playqueue.PlayQueue) @@ -218,7 +218,7 @@ async def test_lookup_media_with_urls(hass, mock_plex_server): # Test URL format result = process_plex_payload( - hass, MEDIA_TYPE_MUSIC, CONTENT_ID_URL, supports_playqueues=False + hass, MediaType.MUSIC, CONTENT_ID_URL, supports_playqueues=False ) assert isinstance(result.media, plexapi.audio.Track) assert result.shuffle is False @@ -226,7 +226,7 @@ async def test_lookup_media_with_urls(hass, mock_plex_server): # Test URL format with shuffle CONTENT_ID_URL_WITH_SHUFFLE = CONTENT_ID_URL + "?shuffle=1" result = process_plex_payload( - hass, MEDIA_TYPE_MUSIC, CONTENT_ID_URL_WITH_SHUFFLE, supports_playqueues=False + hass, MediaType.MUSIC, CONTENT_ID_URL_WITH_SHUFFLE, supports_playqueues=False ) assert isinstance(result.media, plexapi.audio.Track) assert result.shuffle is True diff --git a/tests/components/ps4/test_init.py b/tests/components/ps4/test_init.py index 7cda5b42fa2..bedfa7989f2 100644 --- a/tests/components/ps4/test_init.py +++ b/tests/components/ps4/test_init.py @@ -3,10 +3,10 @@ from unittest.mock import MagicMock, patch from homeassistant import config_entries, data_entry_flow from homeassistant.components import ps4 -from homeassistant.components.media_player.const import ( +from homeassistant.components.media_player import ( ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_TITLE, - MEDIA_TYPE_GAME, + MediaType, ) from homeassistant.components.ps4.const import ( ATTR_MEDIA_IMAGE_URL, @@ -86,20 +86,20 @@ MOCK_UNIQUE_ID = "someuniqueid" MOCK_ID = "CUSA00123" MOCK_URL = "http://someurl.jpeg" MOCK_TITLE = "Some Title" -MOCK_TYPE = MEDIA_TYPE_GAME +MOCK_TYPE = MediaType.GAME MOCK_GAMES_DATA_OLD_STR_FORMAT = {"mock_id": "mock_title", "mock_id2": "mock_title2"} MOCK_GAMES_DATA = { ATTR_LOCKED: False, - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_GAME, + ATTR_MEDIA_CONTENT_TYPE: MediaType.GAME, ATTR_MEDIA_IMAGE_URL: MOCK_URL, ATTR_MEDIA_TITLE: MOCK_TITLE, } MOCK_GAMES_DATA_LOCKED = { ATTR_LOCKED: True, - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_GAME, + ATTR_MEDIA_CONTENT_TYPE: MediaType.GAME, ATTR_MEDIA_IMAGE_URL: MOCK_URL, ATTR_MEDIA_TITLE: MOCK_TITLE, } @@ -215,7 +215,7 @@ def test_games_reformat_to_dict(hass): assert mock_data assert mock_data[ATTR_MEDIA_IMAGE_URL] is None assert mock_data[ATTR_LOCKED] is False - assert mock_data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_GAME + assert mock_data[ATTR_MEDIA_CONTENT_TYPE] == MediaType.GAME def test_load_games(hass): @@ -234,7 +234,7 @@ def test_load_games(hass): assert mock_data[ATTR_MEDIA_TITLE] == MOCK_TITLE assert mock_data[ATTR_MEDIA_IMAGE_URL] == MOCK_URL assert mock_data[ATTR_LOCKED] is False - assert mock_data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_GAME + assert mock_data[ATTR_MEDIA_CONTENT_TYPE] == MediaType.GAME def test_loading_games_returns_dict(hass): diff --git a/tests/components/ps4/test_media_player.py b/tests/components/ps4/test_media_player.py index 3f71f5f9d52..63188d2fa2c 100644 --- a/tests/components/ps4/test_media_player.py +++ b/tests/components/ps4/test_media_player.py @@ -6,14 +6,13 @@ from pyps4_2ndscreen.ddp import DEFAULT_UDP_PORT from pyps4_2ndscreen.media_art import TYPE_APP as PS_TYPE_APP from homeassistant.components import ps4 -from homeassistant.components.media_player.const import ( +from homeassistant.components.media_player import ( ATTR_INPUT_SOURCE, ATTR_INPUT_SOURCE_LIST, ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_TITLE, - MEDIA_TYPE_APP, - MEDIA_TYPE_GAME, + MediaType, ) from homeassistant.components.ps4.const import ( ATTR_MEDIA_IMAGE_URL, @@ -60,19 +59,19 @@ MOCK_RANDOM_PORT = "1234" MOCK_TITLE_ID = "CUSA00000" MOCK_TITLE_NAME = "Random Game" -MOCK_TITLE_TYPE = MEDIA_TYPE_GAME +MOCK_TITLE_TYPE = MediaType.GAME MOCK_TITLE_ART_URL = "https://somecoverurl" MOCK_GAMES_DATA = { ATTR_LOCKED: False, - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_GAME, + ATTR_MEDIA_CONTENT_TYPE: MediaType.GAME, ATTR_MEDIA_IMAGE_URL: MOCK_TITLE_ART_URL, ATTR_MEDIA_TITLE: MOCK_TITLE_NAME, } MOCK_GAMES_DATA_LOCKED = { ATTR_LOCKED: True, - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_GAME, + ATTR_MEDIA_CONTENT_TYPE: MediaType.GAME, ATTR_MEDIA_IMAGE_URL: MOCK_TITLE_ART_URL, ATTR_MEDIA_TITLE: MOCK_TITLE_NAME, } @@ -258,7 +257,7 @@ async def test_media_attributes_are_fetched(hass): mock_attrs = dict(mock_state.attributes) assert len(mock_fetch_app.mock_calls) == 1 - assert mock_attrs.get(ATTR_MEDIA_CONTENT_TYPE) == MEDIA_TYPE_APP + assert mock_attrs.get(ATTR_MEDIA_CONTENT_TYPE) == MediaType.APP async def test_media_attributes_are_loaded(hass, patch_load_json): diff --git a/tests/components/roku/test_media_player.py b/tests/components/roku/test_media_player.py index 8950bafe094..a19e88a2d70 100644 --- a/tests/components/roku/test_media_player.py +++ b/tests/components/roku/test_media_player.py @@ -5,8 +5,7 @@ from unittest.mock import MagicMock, patch import pytest from rokuecp import RokuConnectionError, RokuConnectionTimeoutError, RokuError -from homeassistant.components.media_player import MediaPlayerDeviceClass -from homeassistant.components.media_player.const import ( +from homeassistant.components.media_player import ( ATTR_APP_ID, ATTR_APP_NAME, ATTR_INPUT_SOURCE, @@ -19,17 +18,6 @@ from homeassistant.components.media_player.const import ( ATTR_MEDIA_TITLE, ATTR_MEDIA_VOLUME_MUTED, DOMAIN as MP_DOMAIN, - MEDIA_CLASS_APP, - MEDIA_CLASS_CHANNEL, - MEDIA_CLASS_DIRECTORY, - MEDIA_CLASS_VIDEO, - MEDIA_TYPE_APP, - MEDIA_TYPE_APPS, - MEDIA_TYPE_CHANNEL, - MEDIA_TYPE_CHANNELS, - MEDIA_TYPE_MUSIC, - MEDIA_TYPE_URL, - MEDIA_TYPE_VIDEO, SERVICE_PLAY_MEDIA, SERVICE_SELECT_SOURCE, SUPPORT_BROWSE_MEDIA, @@ -43,6 +31,9 @@ from homeassistant.components.media_player.const import ( SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, + MediaClass, + MediaPlayerDeviceClass, + MediaType, ) from homeassistant.components.roku.const import ( ATTR_CONTENT_ID, @@ -271,7 +262,7 @@ async def test_attributes_app( assert state assert state.state == STATE_ON - assert state.attributes.get(ATTR_MEDIA_CONTENT_TYPE) == MEDIA_TYPE_APP + assert state.attributes.get(ATTR_MEDIA_CONTENT_TYPE) == MediaType.APP assert state.attributes.get(ATTR_APP_ID) == "12" assert state.attributes.get(ATTR_APP_NAME) == "Netflix" assert state.attributes.get(ATTR_INPUT_SOURCE) == "Netflix" @@ -290,7 +281,7 @@ async def test_attributes_app_media_playing( assert state assert state.state == STATE_PLAYING - assert state.attributes.get(ATTR_MEDIA_CONTENT_TYPE) == MEDIA_TYPE_APP + assert state.attributes.get(ATTR_MEDIA_CONTENT_TYPE) == MediaType.APP assert state.attributes.get(ATTR_MEDIA_DURATION) == 6496 assert state.attributes.get(ATTR_MEDIA_POSITION) == 38 assert state.attributes.get(ATTR_APP_ID) == "74519" @@ -309,7 +300,7 @@ async def test_attributes_app_media_paused( assert state assert state.state == STATE_PAUSED - assert state.attributes.get(ATTR_MEDIA_CONTENT_TYPE) == MEDIA_TYPE_APP + assert state.attributes.get(ATTR_MEDIA_CONTENT_TYPE) == MediaType.APP assert state.attributes.get(ATTR_MEDIA_DURATION) == 6496 assert state.attributes.get(ATTR_MEDIA_POSITION) == 313 assert state.attributes.get(ATTR_APP_ID) == "74519" @@ -346,7 +337,7 @@ async def test_tv_attributes( assert state.attributes.get(ATTR_APP_ID) == "tvinput.dtv" assert state.attributes.get(ATTR_APP_NAME) == "Antenna TV" assert state.attributes.get(ATTR_INPUT_SOURCE) == "Antenna TV" - assert state.attributes.get(ATTR_MEDIA_CONTENT_TYPE) == MEDIA_TYPE_CHANNEL + assert state.attributes.get(ATTR_MEDIA_CONTENT_TYPE) == MediaType.CHANNEL assert state.attributes.get(ATTR_MEDIA_CHANNEL) == "getTV (14.3)" assert state.attributes.get(ATTR_MEDIA_TITLE) == "Airwolf" @@ -436,7 +427,7 @@ async def test_services( SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: MAIN_ENTITY_ID, - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_APP, + ATTR_MEDIA_CONTENT_TYPE: MediaType.APP, ATTR_MEDIA_CONTENT_ID: "11", }, blocking=True, @@ -450,7 +441,7 @@ async def test_services( SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: MAIN_ENTITY_ID, - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_APP, + ATTR_MEDIA_CONTENT_TYPE: MediaType.APP, ATTR_MEDIA_CONTENT_ID: "291097", ATTR_MEDIA_EXTRA: { ATTR_MEDIA_TYPE: "movie", @@ -517,7 +508,7 @@ async def test_services_play_media( SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: MAIN_ENTITY_ID, - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, + ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC, ATTR_MEDIA_CONTENT_ID: "https://localhost/media.m4a", ATTR_MEDIA_EXTRA: {ATTR_FORMAT: "blah"}, }, @@ -530,11 +521,11 @@ async def test_services_play_media( @pytest.mark.parametrize( "content_type, content_id, resolved_name, resolved_format", [ - (MEDIA_TYPE_URL, "http://localhost/media.m4a", "media.m4a", "m4a"), - (MEDIA_TYPE_MUSIC, "http://localhost/media.m4a", "media.m4a", "m4a"), - (MEDIA_TYPE_MUSIC, "http://localhost/media.mka", "media.mka", "mka"), + (MediaType.URL, "http://localhost/media.m4a", "media.m4a", "m4a"), + (MediaType.MUSIC, "http://localhost/media.m4a", "media.m4a", "m4a"), + (MediaType.MUSIC, "http://localhost/media.mka", "media.mka", "mka"), ( - MEDIA_TYPE_MUSIC, + MediaType.MUSIC, "http://localhost/api/tts_proxy/generated.mp3", "Text to Speech", "mp3", @@ -575,15 +566,15 @@ async def test_services_play_media_audio( @pytest.mark.parametrize( "content_type, content_id, resolved_name, resolved_format", [ - (MEDIA_TYPE_URL, "http://localhost/media.mp4", "media.mp4", "mp4"), - (MEDIA_TYPE_VIDEO, "http://localhost/media.m4v", "media.m4v", "mp4"), - (MEDIA_TYPE_VIDEO, "http://localhost/media.mov", "media.mov", "mp4"), - (MEDIA_TYPE_VIDEO, "http://localhost/media.mkv", "media.mkv", "mkv"), - (MEDIA_TYPE_VIDEO, "http://localhost/media.mks", "media.mks", "mks"), - (MEDIA_TYPE_VIDEO, "http://localhost/media.m3u8", "media.m3u8", "hls"), - (MEDIA_TYPE_VIDEO, "http://localhost/media.dash", "media.dash", "dash"), - (MEDIA_TYPE_VIDEO, "http://localhost/media.mpd", "media.mpd", "dash"), - (MEDIA_TYPE_VIDEO, "http://localhost/media.ism/manifest", "media.ism", "ism"), + (MediaType.URL, "http://localhost/media.mp4", "media.mp4", "mp4"), + (MediaType.VIDEO, "http://localhost/media.m4v", "media.m4v", "mp4"), + (MediaType.VIDEO, "http://localhost/media.mov", "media.mov", "mp4"), + (MediaType.VIDEO, "http://localhost/media.mkv", "media.mkv", "mkv"), + (MediaType.VIDEO, "http://localhost/media.mks", "media.mks", "mks"), + (MediaType.VIDEO, "http://localhost/media.m3u8", "media.m3u8", "hls"), + (MediaType.VIDEO, "http://localhost/media.dash", "media.dash", "dash"), + (MediaType.VIDEO, "http://localhost/media.mpd", "media.mpd", "dash"), + (MediaType.VIDEO, "http://localhost/media.ism/manifest", "media.ism", "ism"), ], ) async def test_services_play_media_video( @@ -717,7 +708,7 @@ async def test_tv_services( SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: TV_ENTITY_ID, - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_CHANNEL, + ATTR_MEDIA_CONTENT_TYPE: MediaType.CHANNEL, ATTR_MEDIA_CONTENT_ID: "55", }, blocking=True, @@ -752,16 +743,16 @@ async def test_media_browse( assert msg["result"] assert msg["result"]["title"] == "Apps" - assert msg["result"]["media_class"] == MEDIA_CLASS_DIRECTORY - assert msg["result"]["media_content_type"] == MEDIA_TYPE_APPS - assert msg["result"]["children_media_class"] == MEDIA_CLASS_APP + assert msg["result"]["media_class"] == MediaClass.DIRECTORY + assert msg["result"]["media_content_type"] == MediaType.APPS + assert msg["result"]["children_media_class"] == MediaClass.APP assert msg["result"]["can_expand"] assert not msg["result"]["can_play"] assert len(msg["result"]["children"]) == 8 - assert msg["result"]["children_media_class"] == MEDIA_CLASS_APP + assert msg["result"]["children_media_class"] == MediaClass.APP assert msg["result"]["children"][0]["title"] == "Roku Channel Store" - assert msg["result"]["children"][0]["media_content_type"] == MEDIA_TYPE_APP + assert msg["result"]["children"][0]["media_content_type"] == MediaType.APP assert msg["result"]["children"][0]["media_content_id"] == "11" assert ( msg["result"]["children"][0]["thumbnail"] @@ -811,7 +802,7 @@ async def test_media_browse_internal( "id": 1, "type": "media_player/browse_media", "entity_id": MAIN_ENTITY_ID, - "media_content_type": MEDIA_TYPE_APPS, + "media_content_type": MediaType.APPS, "media_content_id": "apps", } ) @@ -824,16 +815,16 @@ async def test_media_browse_internal( assert msg["result"] assert msg["result"]["title"] == "Apps" - assert msg["result"]["media_class"] == MEDIA_CLASS_DIRECTORY - assert msg["result"]["media_content_type"] == MEDIA_TYPE_APPS - assert msg["result"]["children_media_class"] == MEDIA_CLASS_APP + assert msg["result"]["media_class"] == MediaClass.DIRECTORY + assert msg["result"]["media_content_type"] == MediaType.APPS + assert msg["result"]["children_media_class"] == MediaClass.APP assert msg["result"]["can_expand"] assert not msg["result"]["can_play"] assert len(msg["result"]["children"]) == 8 - assert msg["result"]["children_media_class"] == MEDIA_CLASS_APP + assert msg["result"]["children_media_class"] == MediaClass.APP assert msg["result"]["children"][0]["title"] == "Roku Channel Store" - assert msg["result"]["children"][0]["media_content_type"] == MEDIA_TYPE_APP + assert msg["result"]["children"][0]["media_content_type"] == MediaType.APP assert msg["result"]["children"][0]["media_content_id"] == "11" assert "/query/icon/11" in msg["result"]["children"][0]["thumbnail"] assert msg["result"]["children"][0]["can_play"] @@ -873,17 +864,17 @@ async def test_media_browse_local_source( assert msg["result"] assert msg["result"]["title"] == "Roku" - assert msg["result"]["media_class"] == MEDIA_CLASS_DIRECTORY + assert msg["result"]["media_class"] == MediaClass.DIRECTORY assert msg["result"]["media_content_type"] == "root" assert msg["result"]["can_expand"] assert not msg["result"]["can_play"] assert len(msg["result"]["children"]) == 2 assert msg["result"]["children"][0]["title"] == "Apps" - assert msg["result"]["children"][0]["media_content_type"] == MEDIA_TYPE_APPS + assert msg["result"]["children"][0]["media_content_type"] == MediaType.APPS assert msg["result"]["children"][1]["title"] == "Local Media" - assert msg["result"]["children"][1]["media_class"] == MEDIA_CLASS_DIRECTORY + assert msg["result"]["children"][1]["media_class"] == MediaClass.DIRECTORY assert msg["result"]["children"][1]["media_content_type"] is None assert ( msg["result"]["children"][1]["media_content_id"] @@ -911,7 +902,7 @@ async def test_media_browse_local_source( assert msg["result"] assert msg["result"]["title"] == "Local Media" - assert msg["result"]["media_class"] == MEDIA_CLASS_DIRECTORY + assert msg["result"]["media_class"] == MediaClass.DIRECTORY assert msg["result"]["media_content_type"] is None assert len(msg["result"]["children"]) == 2 @@ -947,12 +938,12 @@ async def test_media_browse_local_source( assert msg["success"] assert msg["result"]["title"] == "media" - assert msg["result"]["media_class"] == MEDIA_CLASS_DIRECTORY + assert msg["result"]["media_class"] == MediaClass.DIRECTORY assert msg["result"]["media_content_type"] == "" assert len(msg["result"]["children"]) == 2 assert msg["result"]["children"][0]["title"] == "Epic Sax Guy 10 Hours.mp4" - assert msg["result"]["children"][0]["media_class"] == MEDIA_CLASS_VIDEO + assert msg["result"]["children"][0]["media_class"] == MediaClass.VIDEO assert msg["result"]["children"][0]["media_content_type"] == "video/mp4" assert ( msg["result"]["children"][0]["media_content_id"] @@ -986,7 +977,7 @@ async def test_tv_media_browse( assert msg["result"] assert msg["result"]["title"] == "Roku" - assert msg["result"]["media_class"] == MEDIA_CLASS_DIRECTORY + assert msg["result"]["media_class"] == MediaClass.DIRECTORY assert msg["result"]["media_content_type"] == "root" assert msg["result"]["can_expand"] assert not msg["result"]["can_play"] @@ -998,7 +989,7 @@ async def test_tv_media_browse( "id": 2, "type": "media_player/browse_media", "entity_id": TV_ENTITY_ID, - "media_content_type": MEDIA_TYPE_APPS, + "media_content_type": MediaType.APPS, "media_content_id": "apps", } ) @@ -1011,16 +1002,16 @@ async def test_tv_media_browse( assert msg["result"] assert msg["result"]["title"] == "Apps" - assert msg["result"]["media_class"] == MEDIA_CLASS_DIRECTORY - assert msg["result"]["media_content_type"] == MEDIA_TYPE_APPS - assert msg["result"]["children_media_class"] == MEDIA_CLASS_APP + assert msg["result"]["media_class"] == MediaClass.DIRECTORY + assert msg["result"]["media_content_type"] == MediaType.APPS + assert msg["result"]["children_media_class"] == MediaClass.APP assert msg["result"]["can_expand"] assert not msg["result"]["can_play"] assert len(msg["result"]["children"]) == 11 - assert msg["result"]["children_media_class"] == MEDIA_CLASS_APP + assert msg["result"]["children_media_class"] == MediaClass.APP assert msg["result"]["children"][0]["title"] == "Satellite TV" - assert msg["result"]["children"][0]["media_content_type"] == MEDIA_TYPE_APP + assert msg["result"]["children"][0]["media_content_type"] == MediaType.APP assert msg["result"]["children"][0]["media_content_id"] == "tvinput.hdmi2" assert ( msg["result"]["children"][0]["thumbnail"] @@ -1029,7 +1020,7 @@ async def test_tv_media_browse( assert msg["result"]["children"][0]["can_play"] assert msg["result"]["children"][3]["title"] == "Roku Channel Store" - assert msg["result"]["children"][3]["media_content_type"] == MEDIA_TYPE_APP + assert msg["result"]["children"][3]["media_content_type"] == MediaType.APP assert msg["result"]["children"][3]["media_content_id"] == "11" assert ( msg["result"]["children"][3]["thumbnail"] @@ -1043,7 +1034,7 @@ async def test_tv_media_browse( "id": 3, "type": "media_player/browse_media", "entity_id": TV_ENTITY_ID, - "media_content_type": MEDIA_TYPE_CHANNELS, + "media_content_type": MediaType.CHANNELS, "media_content_id": "channels", } ) @@ -1056,16 +1047,16 @@ async def test_tv_media_browse( assert msg["result"] assert msg["result"]["title"] == "TV Channels" - assert msg["result"]["media_class"] == MEDIA_CLASS_DIRECTORY - assert msg["result"]["media_content_type"] == MEDIA_TYPE_CHANNELS - assert msg["result"]["children_media_class"] == MEDIA_CLASS_CHANNEL + assert msg["result"]["media_class"] == MediaClass.DIRECTORY + assert msg["result"]["media_content_type"] == MediaType.CHANNELS + assert msg["result"]["children_media_class"] == MediaClass.CHANNEL assert msg["result"]["can_expand"] assert not msg["result"]["can_play"] assert len(msg["result"]["children"]) == 4 - assert msg["result"]["children_media_class"] == MEDIA_CLASS_CHANNEL + assert msg["result"]["children_media_class"] == MediaClass.CHANNEL assert msg["result"]["children"][0]["title"] == "WhatsOn (1.1)" - assert msg["result"]["children"][0]["media_content_type"] == MEDIA_TYPE_CHANNEL + assert msg["result"]["children"][0]["media_content_type"] == MediaType.CHANNEL assert msg["result"]["children"][0]["media_content_id"] == "1.1" assert msg["result"]["children"][0]["can_play"] diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index e548822f1d0..6d0edbfa8ae 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -23,20 +23,18 @@ from samsungtvws.exceptions import ConnectionFailure, HttpApiError, Unauthorized from samsungtvws.remote import ChannelEmitCommand, SendRemoteKey from websockets.exceptions import ConnectionClosedError, WebSocketException -from homeassistant.components.media_player import MediaPlayerDeviceClass -from homeassistant.components.media_player.const import ( +from homeassistant.components.media_player import ( ATTR_INPUT_SOURCE, ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_VOLUME_LEVEL, ATTR_MEDIA_VOLUME_MUTED, DOMAIN, - MEDIA_TYPE_APP, - MEDIA_TYPE_CHANNEL, - MEDIA_TYPE_URL, SERVICE_PLAY_MEDIA, SERVICE_SELECT_SOURCE, SUPPORT_TURN_ON, + MediaPlayerDeviceClass, + MediaType, ) from homeassistant.components.samsungtv.const import ( CONF_ON_ACTION, @@ -1120,7 +1118,7 @@ async def test_play_media(hass: HomeAssistant, remote: Mock) -> None: SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: ENTITY_ID, - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_CHANNEL, + ATTR_MEDIA_CONTENT_TYPE: MediaType.CHANNEL, ATTR_MEDIA_CONTENT_ID: "576", }, True, @@ -1149,7 +1147,7 @@ async def test_play_media_invalid_type(hass: HomeAssistant) -> None: SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: ENTITY_ID, - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_URL, + ATTR_MEDIA_CONTENT_TYPE: MediaType.URL, ATTR_MEDIA_CONTENT_ID: url, }, True, @@ -1171,7 +1169,7 @@ async def test_play_media_channel_as_string(hass: HomeAssistant) -> None: SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: ENTITY_ID, - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_CHANNEL, + ATTR_MEDIA_CONTENT_TYPE: MediaType.CHANNEL, ATTR_MEDIA_CONTENT_ID: url, }, True, @@ -1192,7 +1190,7 @@ async def test_play_media_channel_as_non_positive(hass: HomeAssistant) -> None: SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: ENTITY_ID, - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_CHANNEL, + ATTR_MEDIA_CONTENT_TYPE: MediaType.CHANNEL, ATTR_MEDIA_CONTENT_ID: "-4", }, True, @@ -1247,7 +1245,7 @@ async def test_play_media_app(hass: HomeAssistant, remotews: Mock) -> None: SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: ENTITY_ID, - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_APP, + ATTR_MEDIA_CONTENT_TYPE: MediaType.APP, ATTR_MEDIA_CONTENT_ID: "3201608010191", }, True, diff --git a/tests/components/sonos/test_plex_playback.py b/tests/components/sonos/test_plex_playback.py index e9f3fd7e63a..cbbddcf19d5 100644 --- a/tests/components/sonos/test_plex_playback.py +++ b/tests/components/sonos/test_plex_playback.py @@ -4,12 +4,12 @@ from unittest.mock import Mock, patch import pytest -from homeassistant.components.media_player.const import ( +from homeassistant.components.media_player import ( ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, DOMAIN as MP_DOMAIN, - MEDIA_TYPE_MUSIC, SERVICE_PLAY_MEDIA, + MediaType, ) from homeassistant.components.plex import DOMAIN as PLEX_DOMAIN, PLEX_URI_SCHEME from homeassistant.const import ATTR_ENTITY_ID @@ -38,7 +38,7 @@ async def test_plex_play_media(hass, async_autosetup_sonos): SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player, - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, + ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC, ATTR_MEDIA_CONTENT_ID: f"{PLEX_URI_SCHEME}{media_content_id}", }, blocking=True, @@ -47,7 +47,7 @@ async def test_plex_play_media(hass, async_autosetup_sonos): assert len(mock_lookup.mock_calls) == 1 assert len(mock_add_to_queue.mock_calls) == 1 assert not mock_shuffle.called - assert mock_lookup.mock_calls[0][1][0] == MEDIA_TYPE_MUSIC + assert mock_lookup.mock_calls[0][1][0] == MediaType.MUSIC assert mock_lookup.mock_calls[0][2] == json.loads(media_content_id) # Test handling shuffle in payload @@ -60,7 +60,7 @@ async def test_plex_play_media(hass, async_autosetup_sonos): SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player, - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, + ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC, ATTR_MEDIA_CONTENT_ID: f"{PLEX_URI_SCHEME}{shuffle_media_content_id}", }, blocking=True, @@ -69,7 +69,7 @@ async def test_plex_play_media(hass, async_autosetup_sonos): assert mock_shuffle.called assert len(mock_lookup.mock_calls) == 1 assert len(mock_add_to_queue.mock_calls) == 1 - assert mock_lookup.mock_calls[0][1][0] == MEDIA_TYPE_MUSIC + assert mock_lookup.mock_calls[0][1][0] == MediaType.MUSIC assert mock_lookup.mock_calls[0][2] == json.loads(media_content_id) # Test failed Plex service call @@ -83,7 +83,7 @@ async def test_plex_play_media(hass, async_autosetup_sonos): SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player, - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, + ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC, ATTR_MEDIA_CONTENT_ID: f"{PLEX_URI_SCHEME}{media_content_id}", }, blocking=True, @@ -108,7 +108,7 @@ async def test_plex_play_media(hass, async_autosetup_sonos): SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player, - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, + ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC, ATTR_MEDIA_CONTENT_ID: f"{PLEX_URI_SCHEME}{server_id}/{plex_item_key}?shuffle=1", }, blocking=True, diff --git a/tests/components/tts/test_init.py b/tests/components/tts/test_init.py index 7b348489059..61c5ab00180 100644 --- a/tests/components/tts/test_init.py +++ b/tests/components/tts/test_init.py @@ -7,13 +7,13 @@ import voluptuous as vol from homeassistant.components import media_source, tts from homeassistant.components.demo.tts import DemoProvider -from homeassistant.components.media_player.const import ( +from homeassistant.components.media_player import ( ATTR_MEDIA_ANNOUNCE, ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, DOMAIN as DOMAIN_MP, - MEDIA_TYPE_MUSIC, SERVICE_PLAY_MEDIA, + MediaType, ) from homeassistant.config import async_process_ha_core_config from homeassistant.exceptions import HomeAssistantError @@ -93,7 +93,7 @@ async def test_setup_component_and_test_service(hass, empty_cache_dir): assert len(calls) == 1 assert calls[0].data[ATTR_MEDIA_ANNOUNCE] is True - assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC + assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MediaType.MUSIC assert ( await get_media_source_url(hass, calls[0].data[ATTR_MEDIA_CONTENT_ID]) == "/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3" @@ -125,7 +125,7 @@ async def test_setup_component_and_test_service_with_config_language( blocking=True, ) assert len(calls) == 1 - assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC + assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MediaType.MUSIC assert ( await get_media_source_url(hass, calls[0].data[ATTR_MEDIA_CONTENT_ID]) == "/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_de_-_demo.mp3" @@ -160,7 +160,7 @@ async def test_setup_component_and_test_service_with_config_language_special( blocking=True, ) assert len(calls) == 1 - assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC + assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MediaType.MUSIC assert ( await get_media_source_url(hass, calls[0].data[ATTR_MEDIA_CONTENT_ID]) == "/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_en-us_-_demo.mp3" @@ -201,7 +201,7 @@ async def test_setup_component_and_test_service_with_service_language( blocking=True, ) assert len(calls) == 1 - assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC + assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MediaType.MUSIC assert ( await get_media_source_url(hass, calls[0].data[ATTR_MEDIA_CONTENT_ID]) == "/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_de_-_demo.mp3" @@ -265,7 +265,7 @@ async def test_setup_component_and_test_service_with_service_options( opt_hash = tts._hash_options({"voice": "alex", "age": 5}) assert len(calls) == 1 - assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC + assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MediaType.MUSIC assert ( await get_media_source_url(hass, calls[0].data[ATTR_MEDIA_CONTENT_ID]) == f"/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_de_{opt_hash}_demo.mp3" @@ -302,7 +302,7 @@ async def test_setup_component_and_test_with_service_options_def(hass, empty_cac opt_hash = tts._hash_options({"voice": "alex"}) assert len(calls) == 1 - assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC + assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MediaType.MUSIC assert ( await get_media_source_url(hass, calls[0].data[ATTR_MEDIA_CONTENT_ID]) == f"/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_de_{opt_hash}_demo.mp3" @@ -366,7 +366,7 @@ async def test_setup_component_and_test_service_with_base_url_set(hass): blocking=True, ) assert len(calls) == 1 - assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC + assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MediaType.MUSIC assert ( await get_media_source_url(hass, calls[0].data[ATTR_MEDIA_CONTENT_ID]) == "http://fnord" diff --git a/tests/components/unifiprotect/test_media_source.py b/tests/components/unifiprotect/test_media_source.py index 74a007e0ba0..4b1e47d6b0c 100644 --- a/tests/components/unifiprotect/test_media_source.py +++ b/tests/components/unifiprotect/test_media_source.py @@ -17,11 +17,7 @@ from pyunifiprotect.data import ( ) from pyunifiprotect.exceptions import NvrError -from homeassistant.components.media_player.const import ( - MEDIA_CLASS_IMAGE, - MEDIA_CLASS_VIDEO, -) -from homeassistant.components.media_player.errors import BrowseError +from homeassistant.components.media_player import BrowseError, MediaClass from homeassistant.components.media_source import MediaSourceItem from homeassistant.components.unifiprotect.const import DOMAIN from homeassistant.components.unifiprotect.media_source import ( @@ -679,7 +675,7 @@ async def test_browse_media_event( assert browse.identifier == "test_id:event:test_event_id" assert browse.children is None - assert browse.media_class == MEDIA_CLASS_VIDEO + assert browse.media_class == MediaClass.VIDEO async def test_browse_media_eventthumb( @@ -710,7 +706,7 @@ async def test_browse_media_eventthumb( assert browse.identifier == "test_id:eventthumb:test_event_id" assert browse.children is None - assert browse.media_class == MEDIA_CLASS_IMAGE + assert browse.media_class == MediaClass.IMAGE @freeze_time("2022-09-15 03:00:00-07:00") diff --git a/tests/components/webostv/test_media_player.py b/tests/components/webostv/test_media_player.py index f0ebdd70e97..174e4a73483 100644 --- a/tests/components/webostv/test_media_player.py +++ b/tests/components/webostv/test_media_player.py @@ -7,10 +7,6 @@ import pytest from homeassistant.components import automation from homeassistant.components.media_player import ( - DOMAIN as MP_DOMAIN, - MediaPlayerDeviceClass, -) -from homeassistant.components.media_player.const import ( ATTR_INPUT_SOURCE, ATTR_INPUT_SOURCE_LIST, ATTR_MEDIA_CONTENT_ID, @@ -18,11 +14,13 @@ from homeassistant.components.media_player.const import ( ATTR_MEDIA_TITLE, ATTR_MEDIA_VOLUME_LEVEL, ATTR_MEDIA_VOLUME_MUTED, - MEDIA_TYPE_CHANNEL, + DOMAIN as MP_DOMAIN, SERVICE_PLAY_MEDIA, SERVICE_SELECT_SOURCE, SUPPORT_TURN_ON, SUPPORT_VOLUME_SET, + MediaPlayerDeviceClass, + MediaType, ) from homeassistant.components.webostv.const import ( ATTR_BUTTON, @@ -304,7 +302,7 @@ async def test_entity_attributes(hass, client, monkeypatch): assert attrs[ATTR_MEDIA_VOLUME_LEVEL] == 0.37 assert attrs[ATTR_INPUT_SOURCE] == "Live TV" assert attrs[ATTR_INPUT_SOURCE_LIST] == ["Input01", "Input02", "Live TV"] - assert attrs[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_CHANNEL + assert attrs[ATTR_MEDIA_CONTENT_TYPE] == MediaType.CHANNEL assert attrs[ATTR_MEDIA_TITLE] == "Channel 1" assert attrs[ATTR_SOUND_OUTPUT] == "speaker" @@ -373,7 +371,7 @@ async def test_play_media(hass, client, media_id, ch_id): data = { ATTR_ENTITY_ID: ENTITY_ID, - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_CHANNEL, + ATTR_MEDIA_CONTENT_TYPE: MediaType.CHANNEL, ATTR_MEDIA_CONTENT_ID: media_id, } assert await hass.services.async_call(MP_DOMAIN, SERVICE_PLAY_MEDIA, data, True) From 6b3c91bd6ae7913bd1be390f7c6bd60a93e09300 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 20 Sep 2022 18:33:45 +0200 Subject: [PATCH 589/955] Cleanup ColorMode in tests (#78807) --- tests/components/abode/test_light.py | 9 ++-- tests/components/homekit/test_type_lights.py | 50 +++++++++----------- tests/components/hue/test_light_v1.py | 14 +++--- tests/components/hue/test_light_v2.py | 28 +++++------ 4 files changed, 48 insertions(+), 53 deletions(-) diff --git a/tests/components/abode/test_light.py b/tests/components/abode/test_light.py index 3a1adc069e4..3514376d5a0 100644 --- a/tests/components/abode/test_light.py +++ b/tests/components/abode/test_light.py @@ -8,9 +8,8 @@ from homeassistant.components.light import ( ATTR_COLOR_TEMP, ATTR_RGB_COLOR, ATTR_SUPPORTED_COLOR_MODES, - COLOR_MODE_COLOR_TEMP, - COLOR_MODE_HS, DOMAIN as LIGHT_DOMAIN, + ColorMode, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -52,10 +51,10 @@ async def test_attributes(hass: HomeAssistant) -> None: assert state.attributes.get("device_type") == "RGB Dimmer" assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Living Room Lamp" assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 0 - assert state.attributes.get(ATTR_COLOR_MODE) == COLOR_MODE_HS + assert state.attributes.get(ATTR_COLOR_MODE) == ColorMode.HS assert state.attributes.get(ATTR_SUPPORTED_COLOR_MODES) == [ - COLOR_MODE_COLOR_TEMP, - COLOR_MODE_HS, + ColorMode.COLOR_TEMP, + ColorMode.HS, ] diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py index 6835629be37..64e45aa937d 100644 --- a/tests/components/homekit/test_type_lights.py +++ b/tests/components/homekit/test_type_lights.py @@ -27,12 +27,8 @@ from homeassistant.components.light import ( ATTR_RGBWW_COLOR, ATTR_SUPPORTED_COLOR_MODES, ATTR_WHITE, - COLOR_MODE_COLOR_TEMP, - COLOR_MODE_RGB, - COLOR_MODE_RGBW, - COLOR_MODE_RGBWW, - COLOR_MODE_WHITE, DOMAIN, + ColorMode, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -583,24 +579,24 @@ async def test_light_restore(hass, hk_driver, events): "supported_color_modes, state_props, turn_on_props_with_brightness", [ [ - [COLOR_MODE_COLOR_TEMP, COLOR_MODE_RGBW], + [ColorMode.COLOR_TEMP, ColorMode.RGBW], { ATTR_RGBW_COLOR: (128, 50, 0, 255), ATTR_RGB_COLOR: (128, 50, 0), ATTR_HS_COLOR: (23.438, 100.0), ATTR_BRIGHTNESS: 255, - ATTR_COLOR_MODE: COLOR_MODE_RGBW, + ATTR_COLOR_MODE: ColorMode.RGBW, }, {ATTR_HS_COLOR: (145, 75), ATTR_BRIGHTNESS_PCT: 25}, ], [ - [COLOR_MODE_COLOR_TEMP, COLOR_MODE_RGBWW], + [ColorMode.COLOR_TEMP, ColorMode.RGBWW], { ATTR_RGBWW_COLOR: (128, 50, 0, 255, 255), ATTR_RGB_COLOR: (128, 50, 0), ATTR_HS_COLOR: (23.438, 100.0), ATTR_BRIGHTNESS: 255, - ATTR_COLOR_MODE: COLOR_MODE_RGBWW, + ATTR_COLOR_MODE: ColorMode.RGBWW, }, {ATTR_HS_COLOR: (145, 75), ATTR_BRIGHTNESS_PCT: 25}, ], @@ -703,24 +699,24 @@ async def test_light_rgb_with_color_temp( "supported_color_modes, state_props, turn_on_props_with_brightness", [ [ - [COLOR_MODE_RGBW], + [ColorMode.RGBW], { ATTR_RGBW_COLOR: (128, 50, 0, 255), ATTR_RGB_COLOR: (128, 50, 0), ATTR_HS_COLOR: (23.438, 100.0), ATTR_BRIGHTNESS: 255, - ATTR_COLOR_MODE: COLOR_MODE_RGBW, + ATTR_COLOR_MODE: ColorMode.RGBW, }, {ATTR_RGBW_COLOR: (0, 0, 0, 191)}, ], [ - [COLOR_MODE_RGBWW], + [ColorMode.RGBWW], { ATTR_RGBWW_COLOR: (128, 50, 0, 255, 255), ATTR_RGB_COLOR: (128, 50, 0), ATTR_HS_COLOR: (23.438, 100.0), ATTR_BRIGHTNESS: 255, - ATTR_COLOR_MODE: COLOR_MODE_RGBWW, + ATTR_COLOR_MODE: ColorMode.RGBWW, }, {ATTR_RGBWW_COLOR: (0, 0, 0, 165, 26)}, ], @@ -800,12 +796,12 @@ async def test_light_rgb_or_w_lights( entity_id, STATE_ON, { - ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_RGB, COLOR_MODE_WHITE], + ATTR_SUPPORTED_COLOR_MODES: [ColorMode.RGB, ColorMode.WHITE], ATTR_RGBW_COLOR: (128, 50, 0, 255), ATTR_RGB_COLOR: (128, 50, 0), ATTR_HS_COLOR: (23.438, 100.0), ATTR_BRIGHTNESS: 255, - ATTR_COLOR_MODE: COLOR_MODE_RGB, + ATTR_COLOR_MODE: ColorMode.RGB, }, ) await hass.async_block_till_done() @@ -884,9 +880,9 @@ async def test_light_rgb_or_w_lights( entity_id, STATE_ON, { - ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_RGB, COLOR_MODE_WHITE], + ATTR_SUPPORTED_COLOR_MODES: [ColorMode.RGB, ColorMode.WHITE], ATTR_BRIGHTNESS: 255, - ATTR_COLOR_MODE: COLOR_MODE_WHITE, + ATTR_COLOR_MODE: ColorMode.WHITE, }, ) await hass.async_block_till_done() @@ -900,23 +896,23 @@ async def test_light_rgb_or_w_lights( "supported_color_modes, state_props", [ [ - [COLOR_MODE_COLOR_TEMP, COLOR_MODE_RGBW], + [ColorMode.COLOR_TEMP, ColorMode.RGBW], { ATTR_RGBW_COLOR: (128, 50, 0, 255), ATTR_RGB_COLOR: (128, 50, 0), ATTR_HS_COLOR: (23.438, 100.0), ATTR_BRIGHTNESS: 255, - ATTR_COLOR_MODE: COLOR_MODE_RGBW, + ATTR_COLOR_MODE: ColorMode.RGBW, }, ], [ - [COLOR_MODE_COLOR_TEMP, COLOR_MODE_RGBWW], + [ColorMode.COLOR_TEMP, ColorMode.RGBWW], { ATTR_RGBWW_COLOR: (128, 50, 0, 255, 255), ATTR_RGB_COLOR: (128, 50, 0), ATTR_HS_COLOR: (23.438, 100.0), ATTR_BRIGHTNESS: 255, - ATTR_COLOR_MODE: COLOR_MODE_RGBWW, + ATTR_COLOR_MODE: ColorMode.RGBWW, }, ], ], @@ -1013,12 +1009,12 @@ async def test_light_rgbww_with_color_temp_conversion( entity_id, STATE_ON, { - ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_RGBWW], + ATTR_SUPPORTED_COLOR_MODES: [ColorMode.RGBWW], ATTR_RGBWW_COLOR: (128, 50, 0, 255, 255), ATTR_RGB_COLOR: (128, 50, 0), ATTR_HS_COLOR: (23.438, 100.0), ATTR_BRIGHTNESS: 255, - ATTR_COLOR_MODE: COLOR_MODE_RGBWW, + ATTR_COLOR_MODE: ColorMode.RGBWW, }, ) await hass.async_block_till_done() @@ -1091,12 +1087,12 @@ async def test_light_rgbww_with_color_temp_conversion( entity_id, STATE_ON, { - ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_RGBWW], + ATTR_SUPPORTED_COLOR_MODES: [ColorMode.RGBWW], ATTR_RGBWW_COLOR: (0, 0, 0, 128, 255), ATTR_RGB_COLOR: (255, 163, 79), ATTR_HS_COLOR: (28.636, 69.02), ATTR_BRIGHTNESS: 180, - ATTR_COLOR_MODE: COLOR_MODE_RGBWW, + ATTR_COLOR_MODE: ColorMode.RGBWW, }, ) await hass.async_block_till_done() @@ -1134,12 +1130,12 @@ async def test_light_rgbw_with_color_temp_conversion( entity_id, STATE_ON, { - ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_RGBW], + ATTR_SUPPORTED_COLOR_MODES: [ColorMode.RGBW], ATTR_RGBWW_COLOR: (128, 50, 0, 255, 255), ATTR_RGB_COLOR: (128, 50, 0), ATTR_HS_COLOR: (23.438, 100.0), ATTR_BRIGHTNESS: 255, - ATTR_COLOR_MODE: COLOR_MODE_RGBW, + ATTR_COLOR_MODE: ColorMode.RGBW, }, ) await hass.async_block_till_done() diff --git a/tests/components/hue/test_light_v1.py b/tests/components/hue/test_light_v1.py index 5423e6bd799..d0b4708769d 100644 --- a/tests/components/hue/test_light_v1.py +++ b/tests/components/hue/test_light_v1.py @@ -7,7 +7,7 @@ import aiohue from homeassistant.components import hue from homeassistant.components.hue.const import CONF_ALLOW_HUE_GROUPS from homeassistant.components.hue.v1 import light as hue_light -from homeassistant.components.light import COLOR_MODE_COLOR_TEMP, COLOR_MODE_HS +from homeassistant.components.light import ColorMode from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.util import color @@ -237,10 +237,10 @@ async def test_lights_color_mode(hass, mock_bridge_v1): assert lamp_1.attributes["brightness"] == 145 assert lamp_1.attributes["hs_color"] == (36.067, 69.804) assert "color_temp" not in lamp_1.attributes - assert lamp_1.attributes["color_mode"] == COLOR_MODE_HS + assert lamp_1.attributes["color_mode"] == ColorMode.HS assert lamp_1.attributes["supported_color_modes"] == [ - COLOR_MODE_COLOR_TEMP, - COLOR_MODE_HS, + ColorMode.COLOR_TEMP, + ColorMode.HS, ] new_light1_on = LIGHT_1_ON.copy() @@ -262,10 +262,10 @@ async def test_lights_color_mode(hass, mock_bridge_v1): assert lamp_1.attributes["brightness"] == 145 assert lamp_1.attributes["color_temp"] == 467 assert "hs_color" in lamp_1.attributes - assert lamp_1.attributes["color_mode"] == COLOR_MODE_COLOR_TEMP + assert lamp_1.attributes["color_mode"] == ColorMode.COLOR_TEMP assert lamp_1.attributes["supported_color_modes"] == [ - COLOR_MODE_COLOR_TEMP, - COLOR_MODE_HS, + ColorMode.COLOR_TEMP, + ColorMode.HS, ] diff --git a/tests/components/hue/test_light_v2.py b/tests/components/hue/test_light_v2.py index 203d2983fb1..bba28e03477 100644 --- a/tests/components/hue/test_light_v2.py +++ b/tests/components/hue/test_light_v2.py @@ -1,6 +1,6 @@ """Philips Hue lights platform tests for V2 bridge/api.""" -from homeassistant.components.light import COLOR_MODE_COLOR_TEMP, COLOR_MODE_XY +from homeassistant.components.light import ColorMode from homeassistant.helpers import entity_registry as er from .conftest import setup_platform @@ -27,10 +27,10 @@ async def test_lights(hass, mock_bridge_v2, v2_resources_test_data): assert light_1.state == "on" assert light_1.attributes["brightness"] == int(46.85 / 100 * 255) assert light_1.attributes["mode"] == "normal" - assert light_1.attributes["color_mode"] == COLOR_MODE_XY + assert light_1.attributes["color_mode"] == ColorMode.XY assert set(light_1.attributes["supported_color_modes"]) == { - COLOR_MODE_COLOR_TEMP, - COLOR_MODE_XY, + ColorMode.COLOR_TEMP, + ColorMode.XY, } assert light_1.attributes["xy_color"] == (0.5614, 0.4058) assert light_1.attributes["min_mireds"] == 153 @@ -47,7 +47,7 @@ async def test_lights(hass, mock_bridge_v2, v2_resources_test_data): ) assert light_2.state == "off" assert light_2.attributes["mode"] == "normal" - assert light_2.attributes["supported_color_modes"] == [COLOR_MODE_COLOR_TEMP] + assert light_2.attributes["supported_color_modes"] == [ColorMode.COLOR_TEMP] assert light_2.attributes["min_mireds"] == 153 assert light_2.attributes["max_mireds"] == 454 assert light_2.attributes["dynamics"] == "none" @@ -60,8 +60,8 @@ async def test_lights(hass, mock_bridge_v2, v2_resources_test_data): assert light_3.state == "on" assert light_3.attributes["brightness"] == 128 assert light_3.attributes["mode"] == "normal" - assert light_3.attributes["supported_color_modes"] == [COLOR_MODE_XY] - assert light_3.attributes["color_mode"] == COLOR_MODE_XY + assert light_3.attributes["supported_color_modes"] == [ColorMode.XY] + assert light_3.attributes["color_mode"] == ColorMode.XY assert light_3.attributes["dynamics"] == "dynamic_palette" # test light which supports on/off only @@ -113,8 +113,8 @@ async def test_light_turn_on_service(hass, mock_bridge_v2, v2_resources_test_dat assert test_light is not None assert test_light.state == "on" assert test_light.attributes["mode"] == "normal" - assert test_light.attributes["supported_color_modes"] == [COLOR_MODE_COLOR_TEMP] - assert test_light.attributes["color_mode"] == COLOR_MODE_COLOR_TEMP + assert test_light.attributes["supported_color_modes"] == [ColorMode.COLOR_TEMP] + assert test_light.attributes["color_mode"] == ColorMode.COLOR_TEMP assert test_light.attributes["brightness"] == 255 # test again with sending transition with 250ms which should round up to 200ms @@ -344,10 +344,10 @@ async def test_grouped_lights(hass, mock_bridge_v2, v2_resources_test_data): assert test_entity.attributes["friendly_name"] == "Test Zone" assert test_entity.state == "on" assert test_entity.attributes["brightness"] == 119 - assert test_entity.attributes["color_mode"] == COLOR_MODE_XY + assert test_entity.attributes["color_mode"] == ColorMode.XY assert set(test_entity.attributes["supported_color_modes"]) == { - COLOR_MODE_COLOR_TEMP, - COLOR_MODE_XY, + ColorMode.COLOR_TEMP, + ColorMode.XY, } assert test_entity.attributes["min_mireds"] == 153 assert test_entity.attributes["max_mireds"] == 500 @@ -365,7 +365,7 @@ async def test_grouped_lights(hass, mock_bridge_v2, v2_resources_test_data): assert test_entity is not None assert test_entity.attributes["friendly_name"] == "Test Room" assert test_entity.state == "off" - assert test_entity.attributes["supported_color_modes"] == [COLOR_MODE_COLOR_TEMP] + assert test_entity.attributes["supported_color_modes"] == [ColorMode.COLOR_TEMP] assert test_entity.attributes["min_mireds"] == 153 assert test_entity.attributes["max_mireds"] == 454 assert test_entity.attributes["is_hue_group"] is True @@ -417,7 +417,7 @@ async def test_grouped_lights(hass, mock_bridge_v2, v2_resources_test_data): test_light = hass.states.get(test_light_id) assert test_light is not None assert test_light.state == "on" - assert test_light.attributes["color_mode"] == COLOR_MODE_XY + assert test_light.attributes["color_mode"] == ColorMode.XY assert test_light.attributes["brightness"] == 255 assert test_light.attributes["xy_color"] == (0.123, 0.123) From e58531f118a0f49db747c2011341d8004ebae8d0 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 20 Sep 2022 19:40:06 +0200 Subject: [PATCH 590/955] Add MqttData helper to mqtt (#78825) * Add MqttData helper to mqtt * Adjust client for circular dependencies * Move MqttData to models.py * Move get_mqtt_data to util.py --- homeassistant/components/mqtt/__init__.py | 14 ++--- homeassistant/components/mqtt/client.py | 19 ++----- homeassistant/components/mqtt/config_flow.py | 10 ++-- .../components/mqtt/device_trigger.py | 11 ++-- homeassistant/components/mqtt/diagnostics.py | 6 ++- homeassistant/components/mqtt/discovery.py | 8 +-- homeassistant/components/mqtt/mixins.py | 53 ++++--------------- homeassistant/components/mqtt/models.py | 32 +++++++++-- homeassistant/components/mqtt/util.py | 9 ++++ 9 files changed, 74 insertions(+), 88 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index c14266e296f..03e4093bb01 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -76,7 +76,6 @@ from .const import ( # noqa: F401 PLATFORMS, RELOADABLE_PLATFORMS, ) -from .mixins import MqttData from .models import ( # noqa: F401 MqttCommandTemplate, MqttValueTemplate, @@ -86,6 +85,7 @@ from .models import ( # noqa: F401 ) from .util import ( _VALID_QOS_SCHEMA, + get_mqtt_data, mqtt_config_entry_enabled, valid_publish_topic, valid_subscribe_topic, @@ -164,7 +164,7 @@ async def _async_setup_discovery( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Start the MQTT protocol service.""" - mqtt_data: MqttData = hass.data.setdefault(DATA_MQTT, MqttData()) + mqtt_data = get_mqtt_data(hass, True) conf: ConfigType | None = config.get(DOMAIN) @@ -249,7 +249,7 @@ async def _async_config_entry_updated(hass: HomeAssistant, entry: ConfigEntry) - Causes for this is config entry options changing. """ - mqtt_data: MqttData = hass.data[DATA_MQTT] + mqtt_data = get_mqtt_data(hass) assert (client := mqtt_data.client) is not None if (conf := mqtt_data.config) is None: @@ -267,7 +267,7 @@ async def _async_config_entry_updated(hass: HomeAssistant, entry: ConfigEntry) - async def async_fetch_config(hass: HomeAssistant, entry: ConfigEntry) -> dict | None: """Fetch fresh MQTT yaml config from the hass config when (re)loading the entry.""" - mqtt_data: MqttData = hass.data[DATA_MQTT] + mqtt_data = get_mqtt_data(hass) if mqtt_data.reload_entry: hass_config = await conf_util.async_hass_config_yaml(hass) mqtt_data.config = CONFIG_SCHEMA_BASE(hass_config.get(DOMAIN, {})) @@ -307,7 +307,7 @@ async def async_fetch_config(hass: HomeAssistant, entry: ConfigEntry) -> dict | async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Load a config entry.""" - mqtt_data: MqttData = hass.data.setdefault(DATA_MQTT, MqttData()) + mqtt_data = get_mqtt_data(hass, True) # Merge basic configuration, and add missing defaults for basic options if (conf := await async_fetch_config(hass, entry)) is None: @@ -593,7 +593,7 @@ def async_subscribe_connection_status( def is_connected(hass: HomeAssistant) -> bool: """Return if MQTT client is connected.""" - mqtt_data: MqttData = hass.data[DATA_MQTT] + mqtt_data = get_mqtt_data(hass) assert mqtt_data.client is not None return mqtt_data.client.connected @@ -611,7 +611,7 @@ async def async_remove_config_entry_device( async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload MQTT dump and publish service when the config entry is unloaded.""" - mqtt_data: MqttData = hass.data[DATA_MQTT] + mqtt_data = get_mqtt_data(hass) assert mqtt_data.client is not None mqtt_client = mqtt_data.client diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index 28887818133..7ede1e50494 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -46,7 +46,6 @@ from .const import ( CONF_KEEPALIVE, CONF_TLS_INSECURE, CONF_WILL_MESSAGE, - DATA_MQTT, DEFAULT_ENCODING, DEFAULT_QOS, MQTT_CONNECTED, @@ -61,15 +60,13 @@ from .models import ( ReceiveMessage, ReceivePayloadType, ) -from .util import mqtt_config_entry_enabled +from .util import get_mqtt_data, mqtt_config_entry_enabled if TYPE_CHECKING: # Only import for paho-mqtt type checking here, imports are done locally # because integrations should be able to optionally rely on MQTT. import paho.mqtt.client as mqtt - from .mixins import MqttData - _LOGGER = logging.getLogger(__name__) @@ -100,11 +97,7 @@ async def async_publish( encoding: str | None = DEFAULT_ENCODING, ) -> None: """Publish message to a MQTT topic.""" - # Local import to avoid circular dependencies - # pylint: disable-next=import-outside-toplevel - from .mixins import MqttData - - mqtt_data: MqttData = hass.data.setdefault(DATA_MQTT, MqttData()) + mqtt_data = get_mqtt_data(hass, True) if mqtt_data.client is None or not mqtt_config_entry_enabled(hass): raise HomeAssistantError( f"Cannot publish to topic '{topic}', MQTT is not enabled" @@ -190,11 +183,7 @@ async def async_subscribe( Call the return value to unsubscribe. """ - # Local import to avoid circular dependencies - # pylint: disable-next=import-outside-toplevel - from .mixins import MqttData - - mqtt_data: MqttData = hass.data.setdefault(DATA_MQTT, MqttData()) + mqtt_data = get_mqtt_data(hass, True) if mqtt_data.client is None or not mqtt_config_entry_enabled(hass): raise HomeAssistantError( f"Cannot subscribe to topic '{topic}', MQTT is not enabled" @@ -332,7 +321,7 @@ class MQTT: # should be able to optionally rely on MQTT. import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel - self._mqtt_data: MqttData = hass.data[DATA_MQTT] + self._mqtt_data = get_mqtt_data(hass) self.hass = hass self.config_entry = config_entry diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index 12d97b41a74..afa2d98af2b 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -30,14 +30,12 @@ from .const import ( CONF_BIRTH_MESSAGE, CONF_BROKER, CONF_WILL_MESSAGE, - DATA_MQTT, DEFAULT_BIRTH, DEFAULT_DISCOVERY, DEFAULT_WILL, DOMAIN, ) -from .mixins import MqttData -from .util import MQTT_WILL_BIRTH_SCHEMA +from .util import MQTT_WILL_BIRTH_SCHEMA, get_mqtt_data MQTT_TIMEOUT = 5 @@ -165,7 +163,7 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Manage the MQTT broker configuration.""" - mqtt_data: MqttData = self.hass.data.setdefault(DATA_MQTT, MqttData()) + mqtt_data = get_mqtt_data(self.hass, True) errors = {} current_config = self.config_entry.data yaml_config = mqtt_data.config or {} @@ -216,7 +214,7 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Manage the MQTT options.""" - mqtt_data: MqttData = self.hass.data.setdefault(DATA_MQTT, MqttData()) + mqtt_data = get_mqtt_data(self.hass, True) errors = {} current_config = self.config_entry.data yaml_config = mqtt_data.config or {} @@ -351,7 +349,7 @@ def try_connection( import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel # Get the config from configuration.yaml - mqtt_data: MqttData = hass.data.setdefault(DATA_MQTT, MqttData()) + mqtt_data = get_mqtt_data(hass, True) yaml_config = mqtt_data.config or {} entry_config = { CONF_BROKER: broker, diff --git a/homeassistant/components/mqtt/device_trigger.py b/homeassistant/components/mqtt/device_trigger.py index 7e37ed72821..f51731284cc 100644 --- a/homeassistant/components/mqtt/device_trigger.py +++ b/homeassistant/components/mqtt/device_trigger.py @@ -33,17 +33,16 @@ from .const import ( CONF_PAYLOAD, CONF_QOS, CONF_TOPIC, - DATA_MQTT, DOMAIN, ) from .discovery import MQTT_DISCOVERY_DONE from .mixins import ( MQTT_ENTITY_DEVICE_INFO_SCHEMA, - MqttData, MqttDiscoveryDeviceUpdate, send_discovery_done, update_device, ) +from .util import get_mqtt_data _LOGGER = logging.getLogger(__name__) @@ -203,7 +202,7 @@ class MqttDeviceTrigger(MqttDiscoveryDeviceUpdate): self.device_id = device_id self.discovery_data = discovery_data self.hass = hass - self._mqtt_data: MqttData = hass.data[DATA_MQTT] + self._mqtt_data = get_mqtt_data(hass) MqttDiscoveryDeviceUpdate.__init__( self, @@ -281,7 +280,7 @@ async def async_setup_trigger( async def async_removed_from_device(hass: HomeAssistant, device_id: str) -> None: """Handle Mqtt removed from a device.""" - mqtt_data: MqttData = hass.data[DATA_MQTT] + mqtt_data = get_mqtt_data(hass) triggers = await async_get_triggers(hass, device_id) for trig in triggers: device_trigger: Trigger = mqtt_data.device_triggers.pop(trig[CONF_DISCOVERY_ID]) @@ -296,7 +295,7 @@ async def async_get_triggers( hass: HomeAssistant, device_id: str ) -> list[dict[str, str]]: """List device triggers for MQTT devices.""" - mqtt_data: MqttData = hass.data[DATA_MQTT] + mqtt_data = get_mqtt_data(hass) triggers: list[dict[str, str]] = [] if not mqtt_data.device_triggers: @@ -325,7 +324,7 @@ async def async_attach_trigger( trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" - mqtt_data: MqttData = hass.data[DATA_MQTT] + mqtt_data = get_mqtt_data(hass) device_id = config[CONF_DEVICE_ID] discovery_id = config[CONF_DISCOVERY_ID] diff --git a/homeassistant/components/mqtt/diagnostics.py b/homeassistant/components/mqtt/diagnostics.py index 2a6322cac63..173c583ca6a 100644 --- a/homeassistant/components/mqtt/diagnostics.py +++ b/homeassistant/components/mqtt/diagnostics.py @@ -16,7 +16,8 @@ from homeassistant.core import HomeAssistant, callback, split_entity_id from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.device_registry import DeviceEntry -from . import DATA_MQTT, MQTT, debug_info, is_connected +from . import debug_info, is_connected +from .util import get_mqtt_data REDACT_CONFIG = {CONF_PASSWORD, CONF_USERNAME} REDACT_STATE_DEVICE_TRACKER = {ATTR_LATITUDE, ATTR_LONGITUDE} @@ -43,7 +44,8 @@ def _async_get_diagnostics( device: DeviceEntry | None = None, ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - mqtt_instance: MQTT = hass.data[DATA_MQTT].client + mqtt_instance = get_mqtt_data(hass).client + assert mqtt_instance is not None redacted_config = async_redact_data(mqtt_instance.conf, REDACT_CONFIG) diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index 65051ce54fc..ee0d0a1ac9a 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -7,7 +7,6 @@ import functools import logging import re import time -from typing import TYPE_CHECKING from homeassistant.const import CONF_DEVICE, CONF_PLATFORM from homeassistant.core import HomeAssistant @@ -29,12 +28,9 @@ from .const import ( ATTR_DISCOVERY_TOPIC, CONF_AVAILABILITY, CONF_TOPIC, - DATA_MQTT, DOMAIN, ) - -if TYPE_CHECKING: - from .mixins import MqttData +from .util import get_mqtt_data _LOGGER = logging.getLogger(__name__) @@ -98,7 +94,7 @@ async def async_start( # noqa: C901 hass: HomeAssistant, discovery_topic, config_entry=None ) -> None: """Start MQTT Discovery.""" - mqtt_data: MqttData = hass.data[DATA_MQTT] + mqtt_data = get_mqtt_data(hass) mqtt_integrations = {} async def async_discovery_message_received(msg): diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 477be399e26..141d93666c5 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -4,10 +4,9 @@ from __future__ import annotations from abc import abstractmethod import asyncio from collections.abc import Callable, Coroutine -from dataclasses import dataclass, field from functools import partial import logging -from typing import TYPE_CHECKING, Any, Protocol, cast, final +from typing import Any, Protocol, cast, final import voluptuous as vol @@ -29,13 +28,7 @@ from homeassistant.const import ( CONF_UNIQUE_ID, CONF_VALUE_TEMPLATE, ) -from homeassistant.core import ( - CALLBACK_TYPE, - Event, - HomeAssistant, - async_get_hass, - callback, -) +from homeassistant.core import Event, HomeAssistant, async_get_hass, callback from homeassistant.helpers import ( config_validation as cv, device_registry as dr, @@ -60,7 +53,7 @@ from homeassistant.helpers.json import json_loads from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import debug_info, subscription -from .client import MQTT, Subscription, async_publish +from .client import async_publish from .const import ( ATTR_DISCOVERY_HASH, ATTR_DISCOVERY_PAYLOAD, @@ -69,7 +62,6 @@ from .const import ( CONF_ENCODING, CONF_QOS, CONF_TOPIC, - DATA_MQTT, DEFAULT_ENCODING, DEFAULT_PAYLOAD_AVAILABLE, DEFAULT_PAYLOAD_NOT_AVAILABLE, @@ -91,10 +83,7 @@ from .subscription import ( async_subscribe_topics, async_unsubscribe_topics, ) -from .util import mqtt_config_entry_enabled, valid_subscribe_topic - -if TYPE_CHECKING: - from .device_trigger import Trigger +from .util import get_mqtt_data, mqtt_config_entry_enabled, valid_subscribe_topic _LOGGER = logging.getLogger(__name__) @@ -272,27 +261,6 @@ def warn_for_legacy_schema(domain: str) -> Callable: return validator -@dataclass -class MqttData: - """Keep the MQTT entry data.""" - - client: MQTT | None = None - config: ConfigType | None = None - device_triggers: dict[str, Trigger] = field(default_factory=dict) - discovery_registry_hooks: dict[tuple[str, str], CALLBACK_TYPE] = field( - default_factory=dict - ) - last_discovery: float = 0.0 - reload_dispatchers: list[CALLBACK_TYPE] = field(default_factory=list) - reload_entry: bool = False - reload_handlers: dict[str, Callable[[], Coroutine[Any, Any, None]]] = field( - default_factory=dict - ) - reload_needed: bool = False - subscriptions_to_restore: list[Subscription] = field(default_factory=list) - updated_config: ConfigType = field(default_factory=dict) - - class SetupEntity(Protocol): """Protocol type for async_setup_entities.""" @@ -313,8 +281,7 @@ async def async_get_platform_config_from_yaml( config_yaml: ConfigType | None = None, ) -> list[ConfigType]: """Return a list of validated configurations for the domain.""" - - mqtt_data: MqttData = hass.data[DATA_MQTT] + mqtt_data = get_mqtt_data(hass) if config_yaml is None: config_yaml = mqtt_data.config if not config_yaml: @@ -331,7 +298,7 @@ async def async_setup_entry_helper( discovery_schema: vol.Schema, ) -> None: """Set up entity, automation or tag creation dynamically through MQTT discovery.""" - mqtt_data: MqttData = hass.data[DATA_MQTT] + mqtt_data = get_mqtt_data(hass) async def async_discover(discovery_payload): """Discover and add an MQTT entity, automation or tag.""" @@ -363,7 +330,7 @@ async def async_setup_entry_helper( async def _async_setup_entities() -> None: """Set up MQTT items from configuration.yaml.""" - mqtt_data: MqttData = hass.data[DATA_MQTT] + mqtt_data = get_mqtt_data(hass) if mqtt_data.updated_config: # The platform has been reloaded config_yaml = mqtt_data.updated_config @@ -395,7 +362,7 @@ async def async_setup_platform_helper( async_setup_entities: SetupEntity, ) -> None: """Help to set up the platform for manual configured MQTT entities.""" - mqtt_data: MqttData = hass.data[DATA_MQTT] + mqtt_data = get_mqtt_data(hass) if mqtt_data.reload_entry: _LOGGER.debug( "MQTT integration is %s, skipping setup of manually configured MQTT items while unloading the config entry", @@ -621,7 +588,7 @@ class MqttAvailability(Entity): @property def available(self) -> bool: """Return if the device is available.""" - mqtt_data: MqttData = self.hass.data[DATA_MQTT] + mqtt_data = get_mqtt_data(self.hass) assert mqtt_data.client is not None client = mqtt_data.client if not client.connected and not self.hass.is_stopping: @@ -844,7 +811,7 @@ class MqttDiscoveryUpdate(Entity): self._removed_from_hass = False if discovery_data is None: return - mqtt_data: MqttData = hass.data[DATA_MQTT] + mqtt_data = get_mqtt_data(hass) self._registry_hooks = mqtt_data.discovery_registry_hooks discovery_hash: tuple[str, str] = discovery_data[ATTR_DISCOVERY_HASH] if discovery_hash in self._registry_hooks: diff --git a/homeassistant/components/mqtt/models.py b/homeassistant/components/mqtt/models.py index d40b882d81b..2cff89f93a1 100644 --- a/homeassistant/components/mqtt/models.py +++ b/homeassistant/components/mqtt/models.py @@ -3,17 +3,22 @@ from __future__ import annotations from ast import literal_eval from collections.abc import Callable, Coroutine +from dataclasses import dataclass, field import datetime as dt -from typing import Any, Union +from typing import TYPE_CHECKING, Any, Union import attr from homeassistant.const import ATTR_ENTITY_ID, ATTR_NAME -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers import template from homeassistant.helpers.entity import Entity from homeassistant.helpers.service_info.mqtt import ReceivePayloadType -from homeassistant.helpers.typing import TemplateVarsType +from homeassistant.helpers.typing import ConfigType, TemplateVarsType + +if TYPE_CHECKING: + from .client import MQTT, Subscription + from .device_trigger import Trigger _SENTINEL = object() @@ -174,3 +179,24 @@ class MqttValueTemplate: return self._value_template.async_render_with_possible_json_value( payload, default, variables=values ) + + +@dataclass +class MqttData: + """Keep the MQTT entry data.""" + + client: MQTT | None = None + config: ConfigType | None = None + device_triggers: dict[str, Trigger] = field(default_factory=dict) + discovery_registry_hooks: dict[tuple[str, str], CALLBACK_TYPE] = field( + default_factory=dict + ) + last_discovery: float = 0.0 + reload_dispatchers: list[CALLBACK_TYPE] = field(default_factory=list) + reload_entry: bool = False + reload_handlers: dict[str, Callable[[], Coroutine[Any, Any, None]]] = field( + default_factory=dict + ) + reload_needed: bool = False + subscriptions_to_restore: list[Subscription] = field(default_factory=list) + updated_config: ConfigType = field(default_factory=dict) diff --git a/homeassistant/components/mqtt/util.py b/homeassistant/components/mqtt/util.py index 9ef30da7f3b..43734872e14 100644 --- a/homeassistant/components/mqtt/util.py +++ b/homeassistant/components/mqtt/util.py @@ -15,10 +15,12 @@ from .const import ( ATTR_QOS, ATTR_RETAIN, ATTR_TOPIC, + DATA_MQTT, DEFAULT_QOS, DEFAULT_RETAIN, DOMAIN, ) +from .models import MqttData def mqtt_config_entry_enabled(hass: HomeAssistant) -> bool | None: @@ -111,3 +113,10 @@ MQTT_WILL_BIRTH_SCHEMA = vol.Schema( }, required=True, ) + + +def get_mqtt_data(hass: HomeAssistant, ensure_exists: bool = False) -> MqttData: + """Return typed MqttData from hass.data[DATA_MQTT].""" + if ensure_exists: + return hass.data.setdefault(DATA_MQTT, MqttData()) + return hass.data[DATA_MQTT] From 774d5138cae66cf25963962f8ab72949746ec93e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 20 Sep 2022 20:17:49 +0200 Subject: [PATCH 591/955] Update PyJWT to 2.5.0 (#78776) --- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- tests/components/google_assistant/test_http.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7aa9caf200b..61dad6655de 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,4 +1,4 @@ -PyJWT==2.4.0 +PyJWT==2.5.0 PyNaCl==1.5.0 aiodiscover==1.4.13 aiohttp==3.8.1 diff --git a/pyproject.toml b/pyproject.toml index fda755febd3..a5b33379ed9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,7 @@ dependencies = [ "ifaddr==0.1.7", "jinja2==3.1.2", "lru-dict==1.1.8", - "PyJWT==2.4.0", + "PyJWT==2.5.0", # PyJWT has loose dependency. We want the latest one. "cryptography==37.0.4", "orjson==3.7.11", diff --git a/requirements.txt b/requirements.txt index 05693477c37..92a9be187b8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,7 +15,7 @@ home-assistant-bluetooth==1.3.0 ifaddr==0.1.7 jinja2==3.1.2 lru-dict==1.1.8 -PyJWT==2.4.0 +PyJWT==2.5.0 cryptography==37.0.4 orjson==3.7.11 pip>=21.0,<22.3 diff --git a/tests/components/google_assistant/test_http.py b/tests/components/google_assistant/test_http.py index 520b736d7bb..2cc62b47239 100644 --- a/tests/components/google_assistant/test_http.py +++ b/tests/components/google_assistant/test_http.py @@ -44,7 +44,7 @@ MOCK_HEADER = { async def test_get_jwt(hass): """Test signing of key.""" - jwt = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJkdW1teUBkdW1teS5pYW0uZ3NlcnZpY2VhY2NvdW50LmNvbSIsInNjb3BlIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vYXV0aC9ob21lZ3JhcGgiLCJhdWQiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20vby9vYXV0aDIvdG9rZW4iLCJpYXQiOjE1NzEwMTEyMDAsImV4cCI6MTU3MTAxNDgwMH0.gG06SmY-zSvFwSrdFfqIdC6AnC22rwz-d2F2UDeWbywjdmFL_1zceL-OOLBwjD8MJr6nR0kmN_Osu7ml9-EzzZjJqsRUxMjGn2G8nSYHbv16R4FYIp62Ibvt6Jj_wdFobEPoy_5OJ28P5Hdu0giGMlFBJMy0Tc6MgEDZA-cwOBw" + jwt = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkdW1teUBkdW1teS5pYW0uZ3NlcnZpY2VhY2NvdW50LmNvbSIsInNjb3BlIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vYXV0aC9ob21lZ3JhcGgiLCJhdWQiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20vby9vYXV0aDIvdG9rZW4iLCJpYXQiOjE1NzEwMTEyMDAsImV4cCI6MTU3MTAxNDgwMH0.akHbMhOflXdIDHVvUVwO0AoJONVOPUdCghN6hAdVz4gxjarrQeGYc_Qn2r84bEvCU7t6EvimKKr0fyupyzBAzfvKULs5mTHO3h2CwSgvOBMv8LnILboJmbO4JcgdnRV7d9G3ktQs7wWSCXJsI5i5jUr1Wfi9zWwxn2ebaAAgrp8" res = _get_homegraph_jwt( datetime(2019, 10, 14, tzinfo=timezone.utc), DUMMY_CONFIG["service_account"]["client_email"], From 6f782628b92cb312e34b63dbd402110a279878ec Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 20 Sep 2022 20:24:39 +0200 Subject: [PATCH 592/955] Pin Python patch versions [ci] (#78830) --- .github/workflows/ci.yaml | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 3e40bdff4cb..7209a0fbf6f 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -23,7 +23,10 @@ env: CACHE_VERSION: 1 PIP_CACHE_VERSION: 1 HA_SHORT_VERSION: 2022.10 - DEFAULT_PYTHON: 3.9 + # Pin latest Python patch versions to avoid issues + # with runners using different versions. + DEFAULT_PYTHON: 3.9.14 + ALL_PYTHON_VERSIONS: "['3.9.14', '3.10.7']" PRE_COMMIT_CACHE: ~/.cache/pre-commit PIP_CACHE: /tmp/pip-cache SQLALCHEMY_WARN_20: 1 @@ -46,6 +49,7 @@ jobs: pre-commit_cache_key: ${{ steps.generate_pre-commit_cache_key.outputs.key }} python_cache_key: ${{ steps.generate_python_cache_key.outputs.key }} requirements: ${{ steps.core.outputs.requirements }} + python_versions: ${{ steps.info.outputs.python_versions }} test_full_suite: ${{ steps.info.outputs.test_full_suite }} test_group_count: ${{ steps.info.outputs.test_group_count }} test_groups: ${{ steps.info.outputs.test_groups }} @@ -143,6 +147,8 @@ jobs: fi # Output & sent to GitHub Actions + echo "python_versions: ${ALL_PYTHON_VERSIONS}" + echo "::set-output name=python_versions::${ALL_PYTHON_VERSIONS}" echo "test_full_suite: ${test_full_suite}" echo "::set-output name=test_full_suite::${test_full_suite}" echo "integrations_glob: ${integrations_glob}" @@ -463,7 +469,7 @@ jobs: timeout-minutes: 60 strategy: matrix: - python-version: ["3.9", "3.10"] + python-version: ${{ fromJSON(needs.info.outputs.python_versions) }} steps: - name: Check out code from GitHub uses: actions/checkout@v3.0.2 @@ -483,7 +489,7 @@ jobs: with: path: venv key: >- - ${{ runner.os }}-${{ matrix.python-version }}-${{ + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ needs.info.outputs.python_cache_key }} - name: Restore pip wheel cache if: steps.cache-venv.outputs.cache-hit != 'true' @@ -491,10 +497,10 @@ jobs: with: path: ${{ env.PIP_CACHE }} key: >- - ${{ runner.os }}-${{ matrix.python-version }}-${{ + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ steps.generate-pip-key.outputs.key }} restore-keys: | - ${{ runner.os }}-${{ matrix.python-version }}-pip-${{ env.PIP_CACHE_VERSION }}-${{ env.HA_SHORT_VERSION }}- + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-pip-${{ env.PIP_CACHE_VERSION }}-${{ env.HA_SHORT_VERSION }}- - name: Install additional OS dependencies if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -541,7 +547,7 @@ jobs: with: path: venv key: >- - ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-${{ + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ needs.info.outputs.python_cache_key }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' @@ -573,7 +579,7 @@ jobs: with: path: venv key: >- - ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-${{ + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ needs.info.outputs.python_cache_key }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' @@ -606,7 +612,7 @@ jobs: with: path: venv key: >- - ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-${{ + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ needs.info.outputs.python_cache_key }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' @@ -650,7 +656,7 @@ jobs: with: path: venv key: >- - ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-${{ + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ needs.info.outputs.python_cache_key }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' @@ -682,7 +688,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.10"] + python-version: ${{ fromJson(needs.info.outputs.python_versions) }} name: Run pip check ${{ matrix.python-version }} steps: - name: Check out code from GitHub @@ -698,7 +704,7 @@ jobs: with: path: venv key: >- - ${{ runner.os }}-${{ matrix.python-version }}-${{ + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ needs.info.outputs.python_cache_key }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' @@ -729,7 +735,7 @@ jobs: fail-fast: false matrix: group: ${{ fromJson(needs.info.outputs.test_groups) }} - python-version: ["3.9", "3.10"] + python-version: ${{ fromJson(needs.info.outputs.python_versions) }} name: >- Run tests Python ${{ matrix.python-version }} (${{ matrix.group }}) steps: @@ -751,7 +757,7 @@ jobs: uses: actions/cache@v3.0.8 with: path: venv - key: ${{ runner.os }}-${{ matrix.python-version }}-${{ + key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ needs.info.outputs.python_cache_key }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' From 3cf26c4a5d6bc7442ed35e1679db68ef41da1c31 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 20 Sep 2022 20:28:27 +0200 Subject: [PATCH 593/955] Move constants in kostal_plenticore (#78837) --- .../components/kostal_plenticore/const.py | 853 ------------------ .../components/kostal_plenticore/select.py | 32 +- .../components/kostal_plenticore/sensor.py | 789 +++++++++++++++- .../components/kostal_plenticore/switch.py | 41 +- 4 files changed, 856 insertions(+), 859 deletions(-) diff --git a/homeassistant/components/kostal_plenticore/const.py b/homeassistant/components/kostal_plenticore/const.py index 7ae0b13f0e8..168c1ccb439 100644 --- a/homeassistant/components/kostal_plenticore/const.py +++ b/homeassistant/components/kostal_plenticore/const.py @@ -1,857 +1,4 @@ """Constants for the Kostal Plenticore Solar Inverter integration.""" -from typing import NamedTuple - -from homeassistant.components.sensor import ( - ATTR_STATE_CLASS, - SensorDeviceClass, - SensorStateClass, -) -from homeassistant.const import ( - ATTR_DEVICE_CLASS, - ATTR_ICON, - ATTR_UNIT_OF_MEASUREMENT, - ELECTRIC_CURRENT_AMPERE, - ELECTRIC_POTENTIAL_VOLT, - ENERGY_KILO_WATT_HOUR, - PERCENTAGE, - POWER_WATT, -) - DOMAIN = "kostal_plenticore" ATTR_ENABLED_DEFAULT = "entity_registry_enabled_default" - -# Defines all entities for process data. -# -# Each entry is defined with a tuple of these values: -# - module id (str) -# - process data id (str) -# - entity name suffix (str) -# - sensor properties (dict) -# - value formatter (str) -SENSOR_PROCESS_DATA = [ - ( - "devices:local", - "Inverter:State", - "Inverter State", - {ATTR_ICON: "mdi:state-machine"}, - "format_inverter_state", - ), - ( - "devices:local", - "Dc_P", - "Solar Power", - { - ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, - ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, - ATTR_ENABLED_DEFAULT: True, - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - }, - "format_round", - ), - ( - "devices:local", - "Grid_P", - "Grid Power", - { - ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, - ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, - ATTR_ENABLED_DEFAULT: True, - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - }, - "format_round", - ), - ( - "devices:local", - "HomeBat_P", - "Home Power from Battery", - { - ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, - ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, - }, - "format_round", - ), - ( - "devices:local", - "HomeGrid_P", - "Home Power from Grid", - { - ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, - ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - }, - "format_round", - ), - ( - "devices:local", - "HomeOwn_P", - "Home Power from Own", - { - ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, - ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - }, - "format_round", - ), - ( - "devices:local", - "HomePv_P", - "Home Power from PV", - { - ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, - ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - }, - "format_round", - ), - ( - "devices:local", - "Home_P", - "Home Power", - { - ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, - ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - }, - "format_round", - ), - ( - "devices:local:ac", - "P", - "AC Power", - { - ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, - ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, - ATTR_ENABLED_DEFAULT: True, - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - }, - "format_round", - ), - ( - "devices:local:pv1", - "P", - "DC1 Power", - { - ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, - ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - }, - "format_round", - ), - ( - "devices:local:pv1", - "U", - "DC1 Voltage", - { - ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_POTENTIAL_VOLT, - ATTR_DEVICE_CLASS: SensorDeviceClass.VOLTAGE, - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - }, - "format_round", - ), - ( - "devices:local:pv1", - "I", - "DC1 Current", - { - ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_CURRENT_AMPERE, - ATTR_DEVICE_CLASS: SensorDeviceClass.CURRENT, - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - }, - "format_float", - ), - ( - "devices:local:pv2", - "P", - "DC2 Power", - { - ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, - ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - }, - "format_round", - ), - ( - "devices:local:pv2", - "U", - "DC2 Voltage", - { - ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_POTENTIAL_VOLT, - ATTR_DEVICE_CLASS: SensorDeviceClass.VOLTAGE, - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - }, - "format_round", - ), - ( - "devices:local:pv2", - "I", - "DC2 Current", - { - ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_CURRENT_AMPERE, - ATTR_DEVICE_CLASS: SensorDeviceClass.CURRENT, - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - }, - "format_float", - ), - ( - "devices:local:pv3", - "P", - "DC3 Power", - { - ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, - ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - }, - "format_round", - ), - ( - "devices:local:pv3", - "U", - "DC3 Voltage", - { - ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_POTENTIAL_VOLT, - ATTR_DEVICE_CLASS: SensorDeviceClass.VOLTAGE, - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - }, - "format_round", - ), - ( - "devices:local:pv3", - "I", - "DC3 Current", - { - ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_CURRENT_AMPERE, - ATTR_DEVICE_CLASS: SensorDeviceClass.CURRENT, - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - }, - "format_float", - ), - ( - "devices:local", - "PV2Bat_P", - "PV to Battery Power", - { - ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, - ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - }, - "format_round", - ), - ( - "devices:local", - "EM_State", - "Energy Manager State", - {ATTR_ICON: "mdi:state-machine"}, - "format_em_manager_state", - ), - ( - "devices:local:battery", - "Cycles", - "Battery Cycles", - {ATTR_ICON: "mdi:recycle", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT}, - "format_round", - ), - ( - "devices:local:battery", - "P", - "Battery Power", - { - ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, - ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - }, - "format_round", - ), - ( - "devices:local:battery", - "SoC", - "Battery SoC", - { - ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, - ATTR_DEVICE_CLASS: SensorDeviceClass.BATTERY, - }, - "format_round", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:Autarky:Day", - "Autarky Day", - {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_ICON: "mdi:chart-donut"}, - "format_round", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:Autarky:Month", - "Autarky Month", - {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_ICON: "mdi:chart-donut"}, - "format_round", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:Autarky:Total", - "Autarky Total", - { - ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, - ATTR_ICON: "mdi:chart-donut", - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - }, - "format_round", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:Autarky:Year", - "Autarky Year", - {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_ICON: "mdi:chart-donut"}, - "format_round", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:OwnConsumptionRate:Day", - "Own Consumption Rate Day", - {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_ICON: "mdi:chart-donut"}, - "format_round", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:OwnConsumptionRate:Month", - "Own Consumption Rate Month", - {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_ICON: "mdi:chart-donut"}, - "format_round", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:OwnConsumptionRate:Total", - "Own Consumption Rate Total", - { - ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, - ATTR_ICON: "mdi:chart-donut", - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - }, - "format_round", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:OwnConsumptionRate:Year", - "Own Consumption Rate Year", - {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_ICON: "mdi:chart-donut"}, - "format_round", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyHome:Day", - "Home Consumption Day", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyHome:Month", - "Home Consumption Month", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyHome:Year", - "Home Consumption Year", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyHome:Total", - "Home Consumption Total", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyHomeBat:Day", - "Home Consumption from Battery Day", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyHomeBat:Month", - "Home Consumption from Battery Month", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyHomeBat:Year", - "Home Consumption from Battery Year", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyHomeBat:Total", - "Home Consumption from Battery Total", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyHomeGrid:Day", - "Home Consumption from Grid Day", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyHomeGrid:Month", - "Home Consumption from Grid Month", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyHomeGrid:Year", - "Home Consumption from Grid Year", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyHomeGrid:Total", - "Home Consumption from Grid Total", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyHomePv:Day", - "Home Consumption from PV Day", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyHomePv:Month", - "Home Consumption from PV Month", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyHomePv:Year", - "Home Consumption from PV Year", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyHomePv:Total", - "Home Consumption from PV Total", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyPv1:Day", - "Energy PV1 Day", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyPv1:Month", - "Energy PV1 Month", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyPv1:Year", - "Energy PV1 Year", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyPv1:Total", - "Energy PV1 Total", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyPv2:Day", - "Energy PV2 Day", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyPv2:Month", - "Energy PV2 Month", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyPv2:Year", - "Energy PV2 Year", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyPv2:Total", - "Energy PV2 Total", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyPv3:Day", - "Energy PV3 Day", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyPv3:Month", - "Energy PV3 Month", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyPv3:Year", - "Energy PV3 Year", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyPv3:Total", - "Energy PV3 Total", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:Yield:Day", - "Energy Yield Day", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - ATTR_ENABLED_DEFAULT: True, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:Yield:Month", - "Energy Yield Month", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:Yield:Year", - "Energy Yield Year", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:Yield:Total", - "Energy Yield Total", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyChargeGrid:Day", - "Battery Charge from Grid Day", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyChargeGrid:Month", - "Battery Charge from Grid Month", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyChargeGrid:Year", - "Battery Charge from Grid Year", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyChargeGrid:Total", - "Battery Charge from Grid Total", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyChargePv:Day", - "Battery Charge from PV Day", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyChargePv:Month", - "Battery Charge from PV Month", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyChargePv:Year", - "Battery Charge from PV Year", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyChargePv:Total", - "Battery Charge from PV Total", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyDischargeGrid:Day", - "Energy Discharge to Grid Day", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyDischargeGrid:Month", - "Energy Discharge to Grid Month", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyDischargeGrid:Year", - "Energy Discharge to Grid Year", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyDischargeGrid:Total", - "Energy Discharge to Grid Total", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, - }, - "format_energy", - ), -] - - -class SwitchData(NamedTuple): - """Representation of a SelectData tuple.""" - - module_id: str - data_id: str - name: str - is_on: str - on_value: str - on_label: str - off_value: str - off_label: str - - -# Defines all entities for switches. -# -# Each entry is defined with a tuple of these values: -# - module id (str) -# - process data id (str) -# - entity name suffix (str) -# - on Value (str) -# - on Label (str) -# - off Value (str) -# - off Label (str) -SWITCH_SETTINGS_DATA = [ - SwitchData( - "devices:local", - "Battery:Strategy", - "Battery Strategy", - "1", - "1", - "Automatic", - "2", - "Automatic economical", - ), -] - - -class SelectData(NamedTuple): - """Representation of a SelectData tuple.""" - - module_id: str - data_id: str - name: str - options: list - is_on: str - - -# Defines all entities for select widgets. -# -# Each entry is defined with a tuple of these values: -# - module id (str) -# - process data id (str) -# - entity name suffix (str) -# - options -# - entity is enabled by default (bool) -SELECT_SETTINGS_DATA = [ - SelectData( - "devices:local", - "battery_charge", - "Battery Charging / Usage mode", - ["None", "Battery:SmartBatteryControl:Enable", "Battery:TimeControl:Enable"], - "1", - ) -] diff --git a/homeassistant/components/kostal_plenticore/select.py b/homeassistant/components/kostal_plenticore/select.py index 7ac06f2ebef..61c4e8e47e8 100644 --- a/homeassistant/components/kostal_plenticore/select.py +++ b/homeassistant/components/kostal_plenticore/select.py @@ -4,6 +4,7 @@ from __future__ import annotations from abc import ABC from datetime import timedelta import logging +from typing import NamedTuple from homeassistant.components.select import SelectEntity from homeassistant.config_entries import ConfigEntry @@ -13,12 +14,41 @@ from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN, SELECT_SETTINGS_DATA +from .const import DOMAIN from .helper import Plenticore, SelectDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) +class SelectData(NamedTuple): + """Representation of a SelectData tuple.""" + + module_id: str + data_id: str + name: str + options: list + is_on: str + + +# Defines all entities for select widgets. +# +# Each entry is defined with a tuple of these values: +# - module id (str) +# - process data id (str) +# - entity name suffix (str) +# - options +# - entity is enabled by default (bool) +SELECT_SETTINGS_DATA = [ + SelectData( + "devices:local", + "battery_charge", + "Battery Charging / Usage mode", + ["None", "Battery:SmartBatteryControl:Enable", "Battery:TimeControl:Enable"], + "1", + ) +] + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: diff --git a/homeassistant/components/kostal_plenticore/sensor.py b/homeassistant/components/kostal_plenticore/sensor.py index f66264e1d7a..d9ab0d752e0 100644 --- a/homeassistant/components/kostal_plenticore/sensor.py +++ b/homeassistant/components/kostal_plenticore/sensor.py @@ -6,19 +6,802 @@ from datetime import timedelta import logging from typing import Any -from homeassistant.components.sensor import ATTR_STATE_CLASS, SensorEntity +from homeassistant.components.sensor import ( + ATTR_STATE_CLASS, + SensorDeviceClass, + SensorEntity, + SensorStateClass, +) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_ICON, + ATTR_UNIT_OF_MEASUREMENT, + ELECTRIC_CURRENT_AMPERE, + ELECTRIC_POTENTIAL_VOLT, + ENERGY_KILO_WATT_HOUR, + PERCENTAGE, + POWER_WATT, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import ATTR_ENABLED_DEFAULT, DOMAIN, SENSOR_PROCESS_DATA +from .const import ATTR_ENABLED_DEFAULT, DOMAIN from .helper import PlenticoreDataFormatter, ProcessDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) +# Defines all entities for process data. +# +# Each entry is defined with a tuple of these values: +# - module id (str) +# - process data id (str) +# - entity name suffix (str) +# - sensor properties (dict) +# - value formatter (str) +SENSOR_PROCESS_DATA = [ + ( + "devices:local", + "Inverter:State", + "Inverter State", + {ATTR_ICON: "mdi:state-machine"}, + "format_inverter_state", + ), + ( + "devices:local", + "Dc_P", + "Solar Power", + { + ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, + ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, + ATTR_ENABLED_DEFAULT: True, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + "format_round", + ), + ( + "devices:local", + "Grid_P", + "Grid Power", + { + ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, + ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, + ATTR_ENABLED_DEFAULT: True, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + "format_round", + ), + ( + "devices:local", + "HomeBat_P", + "Home Power from Battery", + { + ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, + ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, + }, + "format_round", + ), + ( + "devices:local", + "HomeGrid_P", + "Home Power from Grid", + { + ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, + ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + "format_round", + ), + ( + "devices:local", + "HomeOwn_P", + "Home Power from Own", + { + ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, + ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + "format_round", + ), + ( + "devices:local", + "HomePv_P", + "Home Power from PV", + { + ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, + ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + "format_round", + ), + ( + "devices:local", + "Home_P", + "Home Power", + { + ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, + ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + "format_round", + ), + ( + "devices:local:ac", + "P", + "AC Power", + { + ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, + ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, + ATTR_ENABLED_DEFAULT: True, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + "format_round", + ), + ( + "devices:local:pv1", + "P", + "DC1 Power", + { + ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, + ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + "format_round", + ), + ( + "devices:local:pv1", + "U", + "DC1 Voltage", + { + ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_POTENTIAL_VOLT, + ATTR_DEVICE_CLASS: SensorDeviceClass.VOLTAGE, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + "format_round", + ), + ( + "devices:local:pv1", + "I", + "DC1 Current", + { + ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_CURRENT_AMPERE, + ATTR_DEVICE_CLASS: SensorDeviceClass.CURRENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + "format_float", + ), + ( + "devices:local:pv2", + "P", + "DC2 Power", + { + ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, + ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + "format_round", + ), + ( + "devices:local:pv2", + "U", + "DC2 Voltage", + { + ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_POTENTIAL_VOLT, + ATTR_DEVICE_CLASS: SensorDeviceClass.VOLTAGE, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + "format_round", + ), + ( + "devices:local:pv2", + "I", + "DC2 Current", + { + ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_CURRENT_AMPERE, + ATTR_DEVICE_CLASS: SensorDeviceClass.CURRENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + "format_float", + ), + ( + "devices:local:pv3", + "P", + "DC3 Power", + { + ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, + ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + "format_round", + ), + ( + "devices:local:pv3", + "U", + "DC3 Voltage", + { + ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_POTENTIAL_VOLT, + ATTR_DEVICE_CLASS: SensorDeviceClass.VOLTAGE, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + "format_round", + ), + ( + "devices:local:pv3", + "I", + "DC3 Current", + { + ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_CURRENT_AMPERE, + ATTR_DEVICE_CLASS: SensorDeviceClass.CURRENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + "format_float", + ), + ( + "devices:local", + "PV2Bat_P", + "PV to Battery Power", + { + ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, + ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + "format_round", + ), + ( + "devices:local", + "EM_State", + "Energy Manager State", + {ATTR_ICON: "mdi:state-machine"}, + "format_em_manager_state", + ), + ( + "devices:local:battery", + "Cycles", + "Battery Cycles", + {ATTR_ICON: "mdi:recycle", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT}, + "format_round", + ), + ( + "devices:local:battery", + "P", + "Battery Power", + { + ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, + ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + "format_round", + ), + ( + "devices:local:battery", + "SoC", + "Battery SoC", + { + ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, + ATTR_DEVICE_CLASS: SensorDeviceClass.BATTERY, + }, + "format_round", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:Autarky:Day", + "Autarky Day", + {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_ICON: "mdi:chart-donut"}, + "format_round", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:Autarky:Month", + "Autarky Month", + {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_ICON: "mdi:chart-donut"}, + "format_round", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:Autarky:Total", + "Autarky Total", + { + ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, + ATTR_ICON: "mdi:chart-donut", + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + "format_round", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:Autarky:Year", + "Autarky Year", + {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_ICON: "mdi:chart-donut"}, + "format_round", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:OwnConsumptionRate:Day", + "Own Consumption Rate Day", + {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_ICON: "mdi:chart-donut"}, + "format_round", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:OwnConsumptionRate:Month", + "Own Consumption Rate Month", + {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_ICON: "mdi:chart-donut"}, + "format_round", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:OwnConsumptionRate:Total", + "Own Consumption Rate Total", + { + ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, + ATTR_ICON: "mdi:chart-donut", + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + "format_round", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:OwnConsumptionRate:Year", + "Own Consumption Rate Year", + {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_ICON: "mdi:chart-donut"}, + "format_round", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyHome:Day", + "Home Consumption Day", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyHome:Month", + "Home Consumption Month", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyHome:Year", + "Home Consumption Year", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyHome:Total", + "Home Consumption Total", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, + ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyHomeBat:Day", + "Home Consumption from Battery Day", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyHomeBat:Month", + "Home Consumption from Battery Month", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyHomeBat:Year", + "Home Consumption from Battery Year", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyHomeBat:Total", + "Home Consumption from Battery Total", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, + ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyHomeGrid:Day", + "Home Consumption from Grid Day", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyHomeGrid:Month", + "Home Consumption from Grid Month", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyHomeGrid:Year", + "Home Consumption from Grid Year", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyHomeGrid:Total", + "Home Consumption from Grid Total", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, + ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyHomePv:Day", + "Home Consumption from PV Day", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyHomePv:Month", + "Home Consumption from PV Month", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyHomePv:Year", + "Home Consumption from PV Year", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyHomePv:Total", + "Home Consumption from PV Total", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, + ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyPv1:Day", + "Energy PV1 Day", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyPv1:Month", + "Energy PV1 Month", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyPv1:Year", + "Energy PV1 Year", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyPv1:Total", + "Energy PV1 Total", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, + ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyPv2:Day", + "Energy PV2 Day", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyPv2:Month", + "Energy PV2 Month", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyPv2:Year", + "Energy PV2 Year", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyPv2:Total", + "Energy PV2 Total", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, + ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyPv3:Day", + "Energy PV3 Day", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyPv3:Month", + "Energy PV3 Month", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyPv3:Year", + "Energy PV3 Year", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyPv3:Total", + "Energy PV3 Total", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, + ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:Yield:Day", + "Energy Yield Day", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, + ATTR_ENABLED_DEFAULT: True, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:Yield:Month", + "Energy Yield Month", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:Yield:Year", + "Energy Yield Year", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:Yield:Total", + "Energy Yield Total", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, + ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyChargeGrid:Day", + "Battery Charge from Grid Day", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyChargeGrid:Month", + "Battery Charge from Grid Month", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyChargeGrid:Year", + "Battery Charge from Grid Year", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyChargeGrid:Total", + "Battery Charge from Grid Total", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, + ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyChargePv:Day", + "Battery Charge from PV Day", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyChargePv:Month", + "Battery Charge from PV Month", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyChargePv:Year", + "Battery Charge from PV Year", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyChargePv:Total", + "Battery Charge from PV Total", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, + ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyDischargeGrid:Day", + "Energy Discharge to Grid Day", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyDischargeGrid:Month", + "Energy Discharge to Grid Month", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyDischargeGrid:Year", + "Energy Discharge to Grid Year", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyDischargeGrid:Total", + "Energy Discharge to Grid Total", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, + ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, + }, + "format_energy", + ), +] + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback diff --git a/homeassistant/components/kostal_plenticore/switch.py b/homeassistant/components/kostal_plenticore/switch.py index 178b588e4c6..9136e9f7021 100644 --- a/homeassistant/components/kostal_plenticore/switch.py +++ b/homeassistant/components/kostal_plenticore/switch.py @@ -4,7 +4,7 @@ from __future__ import annotations from abc import ABC from datetime import timedelta import logging -from typing import Any +from typing import Any, NamedTuple from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry @@ -13,12 +13,49 @@ from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN, SWITCH_SETTINGS_DATA +from .const import DOMAIN from .helper import SettingDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) +class SwitchData(NamedTuple): + """Representation of a SelectData tuple.""" + + module_id: str + data_id: str + name: str + is_on: str + on_value: str + on_label: str + off_value: str + off_label: str + + +# Defines all entities for switches. +# +# Each entry is defined with a tuple of these values: +# - module id (str) +# - process data id (str) +# - entity name suffix (str) +# - on Value (str) +# - on Label (str) +# - off Value (str) +# - off Label (str) +SWITCH_SETTINGS_DATA = [ + SwitchData( + "devices:local", + "Battery:Strategy", + "Battery Strategy", + "1", + "1", + "Automatic", + "2", + "Automatic economical", + ), +] + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: From f453726b1862d1d247f6aefdd5f23455b87c11cf Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 20 Sep 2022 20:30:54 +0200 Subject: [PATCH 594/955] Cleanup HVACAction and HVACMode in tests (#78813) --- .../components/climate/test_device_action.py | 14 +- .../climate/test_device_condition.py | 14 +- .../components/climate/test_device_trigger.py | 34 +- tests/components/climate/test_init.py | 13 +- .../climate/test_reproduce_state.py | 16 +- tests/components/emulated_hue/test_hue_api.py | 2 +- .../google_assistant/test_smart_home.py | 8 +- .../homekit/test_type_thermostats.py | 560 +++++++++--------- .../maxcube/test_maxcube_climate.py | 61 +- tests/components/nest/test_climate_sdm.py | 269 ++++----- tests/components/prometheus/test_init.py | 21 +- 11 files changed, 497 insertions(+), 515 deletions(-) diff --git a/tests/components/climate/test_device_action.py b/tests/components/climate/test_device_action.py index 15e06835924..b0cd115362a 100644 --- a/tests/components/climate/test_device_action.py +++ b/tests/components/climate/test_device_action.py @@ -3,7 +3,7 @@ import pytest import voluptuous_serialize import homeassistant.components.automation as automation -from homeassistant.components.climate import DOMAIN, const, device_action +from homeassistant.components.climate import DOMAIN, HVACMode, const, device_action from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.helpers import config_validation as cv, device_registry from homeassistant.helpers.entity import EntityCategory @@ -152,9 +152,9 @@ async def test_action(hass): """Test for actions.""" hass.states.async_set( "climate.entity", - const.HVAC_MODE_COOL, + HVACMode.COOL, { - const.ATTR_HVAC_MODES: [const.HVAC_MODE_COOL, const.HVAC_MODE_OFF], + const.ATTR_HVAC_MODES: [HVACMode.COOL, HVACMode.OFF], const.ATTR_PRESET_MODES: [const.PRESET_HOME, const.PRESET_AWAY], }, ) @@ -174,7 +174,7 @@ async def test_action(hass): "device_id": "abcdefgh", "entity_id": "climate.entity", "type": "set_hvac_mode", - "hvac_mode": const.HVAC_MODE_OFF, + "hvac_mode": HVACMode.OFF, }, }, { @@ -213,7 +213,7 @@ async def test_action(hass): [ ( False, - {const.ATTR_HVAC_MODES: [const.HVAC_MODE_COOL, const.HVAC_MODE_OFF]}, + {const.ATTR_HVAC_MODES: [HVACMode.COOL, HVACMode.OFF]}, {}, "set_hvac_mode", [ @@ -242,7 +242,7 @@ async def test_action(hass): ( True, {}, - {const.ATTR_HVAC_MODES: [const.HVAC_MODE_COOL, const.HVAC_MODE_OFF]}, + {const.ATTR_HVAC_MODES: [HVACMode.COOL, HVACMode.OFF]}, "set_hvac_mode", [ { @@ -296,7 +296,7 @@ async def test_capabilities( if set_state: hass.states.async_set( f"{DOMAIN}.test_5678", - const.HVAC_MODE_COOL, + HVACMode.COOL, capabilities_state, ) diff --git a/tests/components/climate/test_device_condition.py b/tests/components/climate/test_device_condition.py index ce496812105..72298042ee8 100644 --- a/tests/components/climate/test_device_condition.py +++ b/tests/components/climate/test_device_condition.py @@ -3,7 +3,7 @@ import pytest import voluptuous_serialize import homeassistant.components.automation as automation -from homeassistant.components.climate import DOMAIN, const, device_condition +from homeassistant.components.climate import DOMAIN, HVACMode, const, device_condition from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.helpers import config_validation as cv, device_registry from homeassistant.helpers.entity import EntityCategory @@ -207,7 +207,7 @@ async def test_if_state(hass, calls): hass.states.async_set( "climate.entity", - const.HVAC_MODE_COOL, + HVACMode.COOL, { const.ATTR_PRESET_MODE: const.PRESET_AWAY, }, @@ -220,7 +220,7 @@ async def test_if_state(hass, calls): hass.states.async_set( "climate.entity", - const.HVAC_MODE_AUTO, + HVACMode.AUTO, { const.ATTR_PRESET_MODE: const.PRESET_AWAY, }, @@ -239,7 +239,7 @@ async def test_if_state(hass, calls): hass.states.async_set( "climate.entity", - const.HVAC_MODE_AUTO, + HVACMode.AUTO, { const.ATTR_PRESET_MODE: const.PRESET_HOME, }, @@ -256,7 +256,7 @@ async def test_if_state(hass, calls): [ ( False, - {const.ATTR_HVAC_MODES: [const.HVAC_MODE_COOL, const.HVAC_MODE_OFF]}, + {const.ATTR_HVAC_MODES: [HVACMode.COOL, HVACMode.OFF]}, {}, "is_hvac_mode", [ @@ -285,7 +285,7 @@ async def test_if_state(hass, calls): ( True, {}, - {const.ATTR_HVAC_MODES: [const.HVAC_MODE_COOL, const.HVAC_MODE_OFF]}, + {const.ATTR_HVAC_MODES: [HVACMode.COOL, HVACMode.OFF]}, "is_hvac_mode", [ { @@ -339,7 +339,7 @@ async def test_capabilities( if set_state: hass.states.async_set( f"{DOMAIN}.test_5678", - const.HVAC_MODE_COOL, + HVACMode.COOL, capabilities_state, ) diff --git a/tests/components/climate/test_device_trigger.py b/tests/components/climate/test_device_trigger.py index c66d5bcf468..25e1a9d920d 100644 --- a/tests/components/climate/test_device_trigger.py +++ b/tests/components/climate/test_device_trigger.py @@ -3,7 +3,13 @@ import pytest import voluptuous_serialize import homeassistant.components.automation as automation -from homeassistant.components.climate import DOMAIN, const, device_trigger +from homeassistant.components.climate import ( + DOMAIN, + HVACAction, + HVACMode, + const, + device_trigger, +) from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.const import TEMP_CELSIUS from homeassistant.helpers import config_validation as cv, device_registry @@ -52,9 +58,9 @@ async def test_get_triggers(hass, device_reg, entity_reg): entity_id = f"{DOMAIN}.test_5678" hass.states.async_set( entity_id, - const.HVAC_MODE_COOL, + HVACMode.COOL, { - const.ATTR_HVAC_ACTION: const.HVACAction.IDLE, + const.ATTR_HVAC_ACTION: HVACAction.IDLE, const.ATTR_CURRENT_HUMIDITY: 23, const.ATTR_CURRENT_TEMPERATURE: 18, }, @@ -114,9 +120,9 @@ async def test_get_triggers_hidden_auxiliary( entity_id = f"{DOMAIN}.test_5678" hass.states.async_set( entity_id, - const.HVAC_MODE_COOL, + HVACMode.COOL, { - const.ATTR_HVAC_ACTION: const.CURRENT_HVAC_IDLE, + const.ATTR_HVAC_ACTION: HVACAction.IDLE, const.ATTR_CURRENT_HUMIDITY: 23, const.ATTR_CURRENT_TEMPERATURE: 18, }, @@ -146,9 +152,9 @@ async def test_if_fires_on_state_change(hass, calls): """Test for turn_on and turn_off triggers firing.""" hass.states.async_set( "climate.entity", - const.HVAC_MODE_COOL, + HVACMode.COOL, { - const.ATTR_HVAC_ACTION: const.HVACAction.IDLE, + const.ATTR_HVAC_ACTION: HVACAction.IDLE, const.ATTR_CURRENT_HUMIDITY: 23, const.ATTR_CURRENT_TEMPERATURE: 18, }, @@ -166,7 +172,7 @@ async def test_if_fires_on_state_change(hass, calls): "device_id": "", "entity_id": "climate.entity", "type": "hvac_mode_changed", - "to": const.HVAC_MODE_AUTO, + "to": HVACMode.AUTO, }, "action": { "service": "test.automation", @@ -208,9 +214,9 @@ async def test_if_fires_on_state_change(hass, calls): # Fake that the HVAC mode is changing hass.states.async_set( "climate.entity", - const.HVAC_MODE_AUTO, + HVACMode.AUTO, { - const.ATTR_HVAC_ACTION: const.HVACAction.COOLING, + const.ATTR_HVAC_ACTION: HVACAction.COOLING, const.ATTR_CURRENT_HUMIDITY: 23, const.ATTR_CURRENT_TEMPERATURE: 18, }, @@ -222,9 +228,9 @@ async def test_if_fires_on_state_change(hass, calls): # Fake that the temperature is changing hass.states.async_set( "climate.entity", - const.HVAC_MODE_AUTO, + HVACMode.AUTO, { - const.ATTR_HVAC_ACTION: const.HVACAction.COOLING, + const.ATTR_HVAC_ACTION: HVACAction.COOLING, const.ATTR_CURRENT_HUMIDITY: 23, const.ATTR_CURRENT_TEMPERATURE: 23, }, @@ -236,9 +242,9 @@ async def test_if_fires_on_state_change(hass, calls): # Fake that the humidity is changing hass.states.async_set( "climate.entity", - const.HVAC_MODE_AUTO, + HVACMode.AUTO, { - const.ATTR_HVAC_ACTION: const.HVACAction.COOLING, + const.ATTR_HVAC_ACTION: HVACAction.COOLING, const.ATTR_CURRENT_HUMIDITY: 7, const.ATTR_CURRENT_TEMPERATURE: 23, }, diff --git a/tests/components/climate/test_init.py b/tests/components/climate/test_init.py index 1109704e1c4..7f780fb05c2 100644 --- a/tests/components/climate/test_init.py +++ b/tests/components/climate/test_init.py @@ -7,10 +7,9 @@ import pytest import voluptuous as vol from homeassistant.components.climate import ( - HVAC_MODE_HEAT, - HVAC_MODE_OFF, SET_TEMPERATURE_SCHEMA, ClimateEntity, + HVACMode, ) from tests.common import async_mock_service @@ -50,20 +49,20 @@ class MockClimateEntity(ClimateEntity): """Mock Climate device to use in tests.""" @property - def hvac_mode(self) -> str: + def hvac_mode(self) -> HVACMode: """Return hvac operation ie. heat, cool mode. - Need to be one of HVAC_MODE_*. + Need to be one of HVACMode.*. """ - return HVAC_MODE_HEAT + return HVACMode.HEAT @property - def hvac_modes(self) -> list[str]: + def hvac_modes(self) -> list[HVACMode]: """Return the list of available hvac operation modes. Need to be a subset of HVAC_MODES. """ - return [HVAC_MODE_OFF, HVAC_MODE_HEAT] + return [HVACMode.OFF, HVACMode.HEAT] def turn_on(self) -> None: """Turn on.""" diff --git a/tests/components/climate/test_reproduce_state.py b/tests/components/climate/test_reproduce_state.py index a6839043e62..c91340b43cb 100644 --- a/tests/components/climate/test_reproduce_state.py +++ b/tests/components/climate/test_reproduce_state.py @@ -11,9 +11,6 @@ from homeassistant.components.climate.const import ( ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, DOMAIN, - HVAC_MODE_AUTO, - HVAC_MODE_HEAT, - HVAC_MODE_OFF, SERVICE_SET_AUX_HEAT, SERVICE_SET_FAN_MODE, SERVICE_SET_HUMIDITY, @@ -21,6 +18,7 @@ from homeassistant.components.climate.const import ( SERVICE_SET_PRESET_MODE, SERVICE_SET_SWING_MODE, SERVICE_SET_TEMPERATURE, + HVACMode, ) from homeassistant.components.climate.reproduce_state import async_reproduce_states from homeassistant.const import ATTR_TEMPERATURE @@ -32,7 +30,7 @@ ENTITY_1 = "climate.test1" ENTITY_2 = "climate.test2" -@pytest.mark.parametrize("state", [HVAC_MODE_AUTO, HVAC_MODE_HEAT, HVAC_MODE_OFF]) +@pytest.mark.parametrize("state", [HVACMode.AUTO, HVACMode.HEAT, HVACMode.OFF]) async def test_with_hvac_mode(hass, state): """Test that state different hvac states.""" calls = async_mock_service(hass, DOMAIN, SERVICE_SET_HVAC_MODE) @@ -50,7 +48,7 @@ async def test_multiple_state(hass): calls_1 = async_mock_service(hass, DOMAIN, SERVICE_SET_HVAC_MODE) await async_reproduce_states( - hass, [State(ENTITY_1, HVAC_MODE_HEAT), State(ENTITY_2, HVAC_MODE_AUTO)] + hass, [State(ENTITY_1, HVACMode.HEAT), State(ENTITY_2, HVACMode.AUTO)] ) await hass.async_block_till_done() @@ -58,11 +56,11 @@ async def test_multiple_state(hass): assert len(calls_1) == 2 # order is not guaranteed assert any( - call.data == {"entity_id": ENTITY_1, "hvac_mode": HVAC_MODE_HEAT} + call.data == {"entity_id": ENTITY_1, "hvac_mode": HVACMode.HEAT} for call in calls_1 ) assert any( - call.data == {"entity_id": ENTITY_2, "hvac_mode": HVAC_MODE_AUTO} + call.data == {"entity_id": ENTITY_2, "hvac_mode": HVACMode.AUTO} for call in calls_1 ) @@ -85,13 +83,13 @@ async def test_state_with_context(hass): context = Context() await async_reproduce_states( - hass, [State(ENTITY_1, HVAC_MODE_HEAT)], context=context + hass, [State(ENTITY_1, HVACMode.HEAT)], context=context ) await hass.async_block_till_done() assert len(calls) == 1 - assert calls[0].data == {"entity_id": ENTITY_1, "hvac_mode": HVAC_MODE_HEAT} + assert calls[0].data == {"entity_id": ENTITY_1, "hvac_mode": HVACMode.HEAT} assert calls[0].context == context diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index e36903983fe..58303ce54a6 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -829,7 +829,7 @@ async def test_put_light_state_climate_set_temperature(hass_hue, hue_client): assert len(hvac_result_json) == 2 hvac = hass_hue.states.get("climate.hvac") - assert hvac.state == climate.const.HVAC_MODE_COOL + assert hvac.state == climate.HVACMode.COOL assert hvac.attributes[climate.ATTR_TEMPERATURE] == temperature # Make sure we can't change the ecobee temperature since it's not exposed diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 3306cbbaf5a..af61cfedf40 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -5,11 +5,7 @@ from unittest.mock import ANY, call, patch import pytest from homeassistant.components import camera -from homeassistant.components.climate.const import ( - ATTR_MAX_TEMP, - ATTR_MIN_TEMP, - HVAC_MODE_HEAT, -) +from homeassistant.components.climate import ATTR_MAX_TEMP, ATTR_MIN_TEMP, HVACMode from homeassistant.components.demo.binary_sensor import DemoBinarySensor from homeassistant.components.demo.cover import DemoCover from homeassistant.components.demo.light import LIGHT_EFFECT_LIST, DemoLight @@ -807,7 +803,7 @@ async def test_raising_error_trait(hass): """Test raising an error while executing a trait command.""" hass.states.async_set( "climate.bla", - HVAC_MODE_HEAT, + HVACMode.HEAT, {ATTR_MIN_TEMP: 15, ATTR_MAX_TEMP: 30, ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, ) diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index 5f002fbbf6c..f0c21acd54c 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -4,7 +4,7 @@ from unittest.mock import patch from pyhap.const import HAP_REPR_AID, HAP_REPR_CHARS, HAP_REPR_IID, HAP_REPR_VALUE import pytest -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_CURRENT_HUMIDITY, ATTR_CURRENT_TEMPERATURE, ATTR_FAN_MODE, @@ -22,11 +22,6 @@ from homeassistant.components.climate.const import ( ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_STEP, - CURRENT_HVAC_COOL, - CURRENT_HVAC_DRY, - CURRENT_HVAC_FAN, - CURRENT_HVAC_HEAT, - CURRENT_HVAC_IDLE, DEFAULT_MAX_TEMP, DEFAULT_MIN_HUMIDITY, DOMAIN as DOMAIN_CLIMATE, @@ -36,13 +31,6 @@ from homeassistant.components.climate.const import ( FAN_MEDIUM, FAN_OFF, FAN_ON, - HVAC_MODE_AUTO, - HVAC_MODE_COOL, - HVAC_MODE_DRY, - HVAC_MODE_FAN_ONLY, - HVAC_MODE_HEAT, - HVAC_MODE_HEAT_COOL, - HVAC_MODE_OFF, SERVICE_SET_FAN_MODE, SERVICE_SET_SWING_MODE, SUPPORT_FAN_MODE, @@ -52,6 +40,8 @@ from homeassistant.components.climate.const import ( SWING_BOTH, SWING_HORIZONTAL, SWING_OFF, + HVACAction, + HVACMode, ) from homeassistant.components.homekit.const import ( ATTR_VALUE, @@ -96,16 +86,16 @@ async def test_thermostat(hass, hk_driver, events): hass.states.async_set( entity_id, - HVAC_MODE_OFF, + HVACMode.OFF, { ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE, ATTR_HVAC_MODES: [ - HVAC_MODE_HEAT, - HVAC_MODE_HEAT_COOL, - HVAC_MODE_FAN_ONLY, - HVAC_MODE_COOL, - HVAC_MODE_OFF, - HVAC_MODE_AUTO, + HVACMode.HEAT, + HVACMode.HEAT_COOL, + HVACMode.FAN_ONLY, + HVACMode.COOL, + HVACMode.OFF, + HVACMode.AUTO, ], }, ) @@ -136,18 +126,18 @@ async def test_thermostat(hass, hk_driver, events): hass.states.async_set( entity_id, - HVAC_MODE_HEAT, + HVACMode.HEAT, { ATTR_TEMPERATURE: 22.2, ATTR_CURRENT_TEMPERATURE: 17.8, - ATTR_HVAC_ACTION: CURRENT_HVAC_HEAT, + ATTR_HVAC_ACTION: HVACAction.HEATING, ATTR_HVAC_MODES: [ - HVAC_MODE_HEAT, - HVAC_MODE_HEAT_COOL, - HVAC_MODE_FAN_ONLY, - HVAC_MODE_COOL, - HVAC_MODE_OFF, - HVAC_MODE_AUTO, + HVACMode.HEAT, + HVACMode.HEAT_COOL, + HVACMode.FAN_ONLY, + HVACMode.COOL, + HVACMode.OFF, + HVACMode.AUTO, ], }, ) @@ -160,18 +150,18 @@ async def test_thermostat(hass, hk_driver, events): hass.states.async_set( entity_id, - HVAC_MODE_HEAT, + HVACMode.HEAT, { ATTR_TEMPERATURE: 22.0, ATTR_CURRENT_TEMPERATURE: 23.0, - ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE, + ATTR_HVAC_ACTION: HVACAction.IDLE, ATTR_HVAC_MODES: [ - HVAC_MODE_HEAT, - HVAC_MODE_HEAT_COOL, - HVAC_MODE_FAN_ONLY, - HVAC_MODE_COOL, - HVAC_MODE_OFF, - HVAC_MODE_AUTO, + HVACMode.HEAT, + HVACMode.HEAT_COOL, + HVACMode.FAN_ONLY, + HVACMode.COOL, + HVACMode.OFF, + HVACMode.AUTO, ], }, ) @@ -184,18 +174,18 @@ async def test_thermostat(hass, hk_driver, events): hass.states.async_set( entity_id, - HVAC_MODE_FAN_ONLY, + HVACMode.FAN_ONLY, { ATTR_TEMPERATURE: 20.0, ATTR_CURRENT_TEMPERATURE: 25.0, - ATTR_HVAC_ACTION: CURRENT_HVAC_COOL, + ATTR_HVAC_ACTION: HVACAction.COOLING, ATTR_HVAC_MODES: [ - HVAC_MODE_HEAT, - HVAC_MODE_HEAT_COOL, - HVAC_MODE_FAN_ONLY, - HVAC_MODE_COOL, - HVAC_MODE_OFF, - HVAC_MODE_AUTO, + HVACMode.HEAT, + HVACMode.HEAT_COOL, + HVACMode.FAN_ONLY, + HVACMode.COOL, + HVACMode.OFF, + HVACMode.AUTO, ], }, ) @@ -208,11 +198,11 @@ async def test_thermostat(hass, hk_driver, events): hass.states.async_set( entity_id, - HVAC_MODE_COOL, + HVACMode.COOL, { ATTR_TEMPERATURE: 20.0, ATTR_CURRENT_TEMPERATURE: 19.0, - ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE, + ATTR_HVAC_ACTION: HVACAction.IDLE, }, ) await hass.async_block_till_done() @@ -224,7 +214,7 @@ async def test_thermostat(hass, hk_driver, events): hass.states.async_set( entity_id, - HVAC_MODE_OFF, + HVACMode.OFF, {ATTR_TEMPERATURE: 22.0, ATTR_CURRENT_TEMPERATURE: 18.0}, ) await hass.async_block_till_done() @@ -236,18 +226,18 @@ async def test_thermostat(hass, hk_driver, events): hass.states.async_set( entity_id, - HVAC_MODE_AUTO, + HVACMode.AUTO, { ATTR_TEMPERATURE: 22.0, ATTR_CURRENT_TEMPERATURE: 18.0, - ATTR_HVAC_ACTION: CURRENT_HVAC_HEAT, + ATTR_HVAC_ACTION: HVACAction.HEATING, ATTR_HVAC_MODES: [ - HVAC_MODE_HEAT, - HVAC_MODE_HEAT_COOL, - HVAC_MODE_FAN_ONLY, - HVAC_MODE_COOL, - HVAC_MODE_OFF, - HVAC_MODE_AUTO, + HVACMode.HEAT, + HVACMode.HEAT_COOL, + HVACMode.FAN_ONLY, + HVACMode.COOL, + HVACMode.OFF, + HVACMode.AUTO, ], }, ) @@ -260,18 +250,18 @@ async def test_thermostat(hass, hk_driver, events): hass.states.async_set( entity_id, - HVAC_MODE_HEAT_COOL, + HVACMode.HEAT_COOL, { ATTR_TEMPERATURE: 22.0, ATTR_CURRENT_TEMPERATURE: 25.0, - ATTR_HVAC_ACTION: CURRENT_HVAC_COOL, + ATTR_HVAC_ACTION: HVACAction.COOLING, ATTR_HVAC_MODES: [ - HVAC_MODE_HEAT, - HVAC_MODE_HEAT_COOL, - HVAC_MODE_FAN_ONLY, - HVAC_MODE_COOL, - HVAC_MODE_OFF, - HVAC_MODE_AUTO, + HVACMode.HEAT, + HVACMode.HEAT_COOL, + HVACMode.FAN_ONLY, + HVACMode.COOL, + HVACMode.OFF, + HVACMode.AUTO, ], }, ) @@ -284,18 +274,18 @@ async def test_thermostat(hass, hk_driver, events): hass.states.async_set( entity_id, - HVAC_MODE_AUTO, + HVACMode.AUTO, { ATTR_TEMPERATURE: 22.0, ATTR_CURRENT_TEMPERATURE: 22.0, - ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE, + ATTR_HVAC_ACTION: HVACAction.IDLE, ATTR_HVAC_MODES: [ - HVAC_MODE_HEAT, - HVAC_MODE_HEAT_COOL, - HVAC_MODE_FAN_ONLY, - HVAC_MODE_COOL, - HVAC_MODE_OFF, - HVAC_MODE_AUTO, + HVACMode.HEAT, + HVACMode.HEAT_COOL, + HVACMode.FAN_ONLY, + HVACMode.COOL, + HVACMode.OFF, + HVACMode.AUTO, ], }, ) @@ -308,18 +298,18 @@ async def test_thermostat(hass, hk_driver, events): hass.states.async_set( entity_id, - HVAC_MODE_FAN_ONLY, + HVACMode.FAN_ONLY, { ATTR_TEMPERATURE: 22.0, ATTR_CURRENT_TEMPERATURE: 22.0, - ATTR_HVAC_ACTION: CURRENT_HVAC_FAN, + ATTR_HVAC_ACTION: HVACAction.FAN, ATTR_HVAC_MODES: [ - HVAC_MODE_HEAT, - HVAC_MODE_HEAT_COOL, - HVAC_MODE_FAN_ONLY, - HVAC_MODE_COOL, - HVAC_MODE_OFF, - HVAC_MODE_AUTO, + HVACMode.HEAT, + HVACMode.HEAT_COOL, + HVACMode.FAN_ONLY, + HVACMode.COOL, + HVACMode.OFF, + HVACMode.AUTO, ], }, ) @@ -332,12 +322,12 @@ async def test_thermostat(hass, hk_driver, events): hass.states.async_set( entity_id, - HVAC_MODE_DRY, + HVACMode.DRY, { ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE, ATTR_TEMPERATURE: 22.0, ATTR_CURRENT_TEMPERATURE: 22.0, - ATTR_HVAC_ACTION: CURRENT_HVAC_DRY, + ATTR_HVAC_ACTION: HVACAction.DRYING, }, ) await hass.async_block_till_done() @@ -404,7 +394,7 @@ async def test_thermostat(hass, hk_driver, events): await hass.async_block_till_done() assert call_set_hvac_mode assert call_set_hvac_mode[0].data[ATTR_ENTITY_ID] == entity_id - assert call_set_hvac_mode[0].data[ATTR_HVAC_MODE] == HVAC_MODE_HEAT + assert call_set_hvac_mode[0].data[ATTR_HVAC_MODE] == HVACMode.HEAT assert acc.char_target_heat_cool.value == 1 assert len(events) == 2 assert events[-1].data[ATTR_VALUE] == "TargetHeatingCoolingState to 1" @@ -424,7 +414,7 @@ async def test_thermostat(hass, hk_driver, events): await hass.async_block_till_done() assert call_set_hvac_mode assert call_set_hvac_mode[1].data[ATTR_ENTITY_ID] == entity_id - assert call_set_hvac_mode[1].data[ATTR_HVAC_MODE] == HVAC_MODE_HEAT_COOL + assert call_set_hvac_mode[1].data[ATTR_HVAC_MODE] == HVACMode.HEAT_COOL assert acc.char_target_heat_cool.value == 3 assert len(events) == 3 assert events[-1].data[ATTR_VALUE] == "TargetHeatingCoolingState to 3" @@ -437,17 +427,17 @@ async def test_thermostat_auto(hass, hk_driver, events): # support_auto = True hass.states.async_set( entity_id, - HVAC_MODE_OFF, + HVACMode.OFF, { ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_TEMPERATURE_RANGE, ATTR_HVAC_MODES: [ - HVAC_MODE_HEAT, - HVAC_MODE_HEAT_COOL, - HVAC_MODE_FAN_ONLY, - HVAC_MODE_COOL, - HVAC_MODE_OFF, - HVAC_MODE_AUTO, + HVACMode.HEAT, + HVACMode.HEAT_COOL, + HVACMode.FAN_ONLY, + HVACMode.COOL, + HVACMode.OFF, + HVACMode.AUTO, ], }, ) @@ -470,19 +460,19 @@ async def test_thermostat_auto(hass, hk_driver, events): hass.states.async_set( entity_id, - HVAC_MODE_HEAT_COOL, + HVACMode.HEAT_COOL, { ATTR_TARGET_TEMP_HIGH: 22.0, ATTR_TARGET_TEMP_LOW: 20.0, ATTR_CURRENT_TEMPERATURE: 18.0, - ATTR_HVAC_ACTION: CURRENT_HVAC_HEAT, + ATTR_HVAC_ACTION: HVACAction.HEATING, ATTR_HVAC_MODES: [ - HVAC_MODE_HEAT, - HVAC_MODE_HEAT_COOL, - HVAC_MODE_FAN_ONLY, - HVAC_MODE_COOL, - HVAC_MODE_OFF, - HVAC_MODE_AUTO, + HVACMode.HEAT, + HVACMode.HEAT_COOL, + HVACMode.FAN_ONLY, + HVACMode.COOL, + HVACMode.OFF, + HVACMode.AUTO, ], }, ) @@ -496,19 +486,19 @@ async def test_thermostat_auto(hass, hk_driver, events): hass.states.async_set( entity_id, - HVAC_MODE_COOL, + HVACMode.COOL, { ATTR_TARGET_TEMP_HIGH: 23.0, ATTR_TARGET_TEMP_LOW: 19.0, ATTR_CURRENT_TEMPERATURE: 24.0, - ATTR_HVAC_ACTION: CURRENT_HVAC_COOL, + ATTR_HVAC_ACTION: HVACAction.COOLING, ATTR_HVAC_MODES: [ - HVAC_MODE_HEAT, - HVAC_MODE_HEAT_COOL, - HVAC_MODE_FAN_ONLY, - HVAC_MODE_COOL, - HVAC_MODE_OFF, - HVAC_MODE_AUTO, + HVACMode.HEAT, + HVACMode.HEAT_COOL, + HVACMode.FAN_ONLY, + HVACMode.COOL, + HVACMode.OFF, + HVACMode.AUTO, ], }, ) @@ -522,19 +512,19 @@ async def test_thermostat_auto(hass, hk_driver, events): hass.states.async_set( entity_id, - HVAC_MODE_AUTO, + HVACMode.AUTO, { ATTR_TARGET_TEMP_HIGH: 23.0, ATTR_TARGET_TEMP_LOW: 19.0, ATTR_CURRENT_TEMPERATURE: 21.0, - ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE, + ATTR_HVAC_ACTION: HVACAction.IDLE, ATTR_HVAC_MODES: [ - HVAC_MODE_HEAT, - HVAC_MODE_HEAT_COOL, - HVAC_MODE_FAN_ONLY, - HVAC_MODE_COOL, - HVAC_MODE_OFF, - HVAC_MODE_AUTO, + HVACMode.HEAT, + HVACMode.HEAT_COOL, + HVACMode.FAN_ONLY, + HVACMode.COOL, + HVACMode.OFF, + HVACMode.AUTO, ], }, ) @@ -591,17 +581,17 @@ async def test_thermostat_mode_and_temp_change(hass, hk_driver, events): # support_auto = True hass.states.async_set( entity_id, - HVAC_MODE_OFF, + HVACMode.OFF, { ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_TEMPERATURE_RANGE, ATTR_HVAC_MODES: [ - HVAC_MODE_HEAT, - HVAC_MODE_HEAT_COOL, - HVAC_MODE_FAN_ONLY, - HVAC_MODE_COOL, - HVAC_MODE_OFF, - HVAC_MODE_AUTO, + HVACMode.HEAT, + HVACMode.HEAT_COOL, + HVACMode.FAN_ONLY, + HVACMode.COOL, + HVACMode.OFF, + HVACMode.AUTO, ], }, ) @@ -624,19 +614,19 @@ async def test_thermostat_mode_and_temp_change(hass, hk_driver, events): hass.states.async_set( entity_id, - HVAC_MODE_COOL, + HVACMode.COOL, { ATTR_TARGET_TEMP_HIGH: 23.0, ATTR_TARGET_TEMP_LOW: 19.0, ATTR_CURRENT_TEMPERATURE: 21.0, - ATTR_HVAC_ACTION: CURRENT_HVAC_COOL, + ATTR_HVAC_ACTION: HVACAction.COOLING, ATTR_HVAC_MODES: [ - HVAC_MODE_HEAT, - HVAC_MODE_HEAT_COOL, - HVAC_MODE_FAN_ONLY, - HVAC_MODE_COOL, - HVAC_MODE_OFF, - HVAC_MODE_AUTO, + HVACMode.HEAT, + HVACMode.HEAT_COOL, + HVACMode.FAN_ONLY, + HVACMode.COOL, + HVACMode.OFF, + HVACMode.AUTO, ], }, ) @@ -682,7 +672,7 @@ async def test_thermostat_mode_and_temp_change(hass, hk_driver, events): await hass.async_block_till_done() assert call_set_hvac_mode[0] assert call_set_hvac_mode[0].data[ATTR_ENTITY_ID] == entity_id - assert call_set_hvac_mode[0].data[ATTR_HVAC_MODE] == HVAC_MODE_HEAT_COOL + assert call_set_hvac_mode[0].data[ATTR_HVAC_MODE] == HVACMode.HEAT_COOL assert call_set_temperature[0] assert call_set_temperature[0].data[ATTR_ENTITY_ID] == entity_id assert call_set_temperature[0].data[ATTR_TARGET_TEMP_LOW] == 20.0 @@ -702,7 +692,7 @@ async def test_thermostat_humidity(hass, hk_driver, events): entity_id = "climate.test" # support_auto = True - hass.states.async_set(entity_id, HVAC_MODE_OFF, {ATTR_SUPPORTED_FEATURES: 4}) + hass.states.async_set(entity_id, HVACMode.OFF, {ATTR_SUPPORTED_FEATURES: 4}) await hass.async_block_till_done() acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) hk_driver.add_accessory(acc) @@ -716,14 +706,14 @@ async def test_thermostat_humidity(hass, hk_driver, events): assert acc.char_target_humidity.properties[PROP_MIN_VALUE] == DEFAULT_MIN_HUMIDITY hass.states.async_set( - entity_id, HVAC_MODE_HEAT_COOL, {ATTR_HUMIDITY: 65, ATTR_CURRENT_HUMIDITY: 40} + entity_id, HVACMode.HEAT_COOL, {ATTR_HUMIDITY: 65, ATTR_CURRENT_HUMIDITY: 40} ) await hass.async_block_till_done() assert acc.char_current_humidity.value == 40 assert acc.char_target_humidity.value == 65 hass.states.async_set( - entity_id, HVAC_MODE_COOL, {ATTR_HUMIDITY: 35, ATTR_CURRENT_HUMIDITY: 70} + entity_id, HVACMode.COOL, {ATTR_HUMIDITY: 35, ATTR_CURRENT_HUMIDITY: 70} ) await hass.async_block_till_done() assert acc.char_current_humidity.value == 70 @@ -763,18 +753,18 @@ async def test_thermostat_power_state(hass, hk_driver, events): # SUPPORT_ON_OFF = True hass.states.async_set( entity_id, - HVAC_MODE_HEAT, + HVACMode.HEAT, { ATTR_SUPPORTED_FEATURES: 4096, ATTR_TEMPERATURE: 23.0, ATTR_CURRENT_TEMPERATURE: 18.0, - ATTR_HVAC_ACTION: CURRENT_HVAC_HEAT, + ATTR_HVAC_ACTION: HVACAction.HEATING, ATTR_HVAC_MODES: [ - HVAC_MODE_HEAT_COOL, - HVAC_MODE_COOL, - HVAC_MODE_AUTO, - HVAC_MODE_HEAT, - HVAC_MODE_OFF, + HVACMode.HEAT_COOL, + HVACMode.COOL, + HVACMode.AUTO, + HVACMode.HEAT, + HVACMode.OFF, ], }, ) @@ -790,17 +780,17 @@ async def test_thermostat_power_state(hass, hk_driver, events): hass.states.async_set( entity_id, - HVAC_MODE_OFF, + HVACMode.OFF, { ATTR_TEMPERATURE: 23.0, ATTR_CURRENT_TEMPERATURE: 18.0, - ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE, + ATTR_HVAC_ACTION: HVACAction.IDLE, ATTR_HVAC_MODES: [ - HVAC_MODE_HEAT_COOL, - HVAC_MODE_COOL, - HVAC_MODE_AUTO, - HVAC_MODE_HEAT, - HVAC_MODE_OFF, + HVACMode.HEAT_COOL, + HVACMode.COOL, + HVACMode.AUTO, + HVACMode.HEAT, + HVACMode.OFF, ], }, ) @@ -810,17 +800,17 @@ async def test_thermostat_power_state(hass, hk_driver, events): hass.states.async_set( entity_id, - HVAC_MODE_OFF, + HVACMode.OFF, { ATTR_TEMPERATURE: 23.0, ATTR_CURRENT_TEMPERATURE: 18.0, - ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE, + ATTR_HVAC_ACTION: HVACAction.IDLE, ATTR_HVAC_MODES: [ - HVAC_MODE_HEAT_COOL, - HVAC_MODE_COOL, - HVAC_MODE_AUTO, - HVAC_MODE_HEAT, - HVAC_MODE_OFF, + HVACMode.HEAT_COOL, + HVACMode.COOL, + HVACMode.AUTO, + HVACMode.HEAT, + HVACMode.OFF, ], }, ) @@ -849,7 +839,7 @@ async def test_thermostat_power_state(hass, hk_driver, events): await hass.async_block_till_done() assert call_set_hvac_mode assert call_set_hvac_mode[0].data[ATTR_ENTITY_ID] == entity_id - assert call_set_hvac_mode[0].data[ATTR_HVAC_MODE] == HVAC_MODE_HEAT + assert call_set_hvac_mode[0].data[ATTR_HVAC_MODE] == HVACMode.HEAT assert acc.char_target_heat_cool.value == 1 assert len(events) == 1 assert events[-1].data[ATTR_VALUE] == "TargetHeatingCoolingState to 1" @@ -870,7 +860,7 @@ async def test_thermostat_power_state(hass, hk_driver, events): await hass.async_block_till_done() assert call_set_hvac_mode assert call_set_hvac_mode[1].data[ATTR_ENTITY_ID] == entity_id - assert call_set_hvac_mode[1].data[ATTR_HVAC_MODE] == HVAC_MODE_COOL + assert call_set_hvac_mode[1].data[ATTR_HVAC_MODE] == HVACMode.COOL assert len(events) == 2 assert events[-1].data[ATTR_VALUE] == "TargetHeatingCoolingState to 2" assert acc.char_target_heat_cool.value == 2 @@ -883,7 +873,7 @@ async def test_thermostat_fahrenheit(hass, hk_driver, events): # support_ = True hass.states.async_set( entity_id, - HVAC_MODE_OFF, + HVACMode.OFF, { ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_TEMPERATURE_RANGE @@ -898,7 +888,7 @@ async def test_thermostat_fahrenheit(hass, hk_driver, events): hass.states.async_set( entity_id, - HVAC_MODE_HEAT_COOL, + HVACMode.HEAT_COOL, { ATTR_TARGET_TEMP_HIGH: 75.2, ATTR_TARGET_TEMP_LOW: 68.1, @@ -989,19 +979,19 @@ async def test_thermostat_get_temperature_range(hass, hk_driver): """Test if temperature range is evaluated correctly.""" entity_id = "climate.test" - hass.states.async_set(entity_id, HVAC_MODE_OFF) + hass.states.async_set(entity_id, HVACMode.OFF) await hass.async_block_till_done() acc = Thermostat(hass, hk_driver, "Climate", entity_id, 2, None) hass.states.async_set( - entity_id, HVAC_MODE_OFF, {ATTR_MIN_TEMP: 20, ATTR_MAX_TEMP: 25} + entity_id, HVACMode.OFF, {ATTR_MIN_TEMP: 20, ATTR_MAX_TEMP: 25} ) await hass.async_block_till_done() assert acc.get_temperature_range() == (20, 25) acc._unit = TEMP_FAHRENHEIT hass.states.async_set( - entity_id, HVAC_MODE_OFF, {ATTR_MIN_TEMP: 60, ATTR_MAX_TEMP: 70} + entity_id, HVACMode.OFF, {ATTR_MIN_TEMP: 60, ATTR_MAX_TEMP: 70} ) await hass.async_block_till_done() assert acc.get_temperature_range() == (15.5, 21.0) @@ -1011,7 +1001,7 @@ async def test_thermostat_temperature_step_whole(hass, hk_driver): """Test climate device with single digit precision.""" entity_id = "climate.test" - hass.states.async_set(entity_id, HVAC_MODE_OFF, {ATTR_TARGET_TEMP_STEP: 1}) + hass.states.async_set(entity_id, HVACMode.OFF, {ATTR_TARGET_TEMP_STEP: 1}) await hass.async_block_till_done() acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) hk_driver.add_accessory(acc) @@ -1039,7 +1029,7 @@ async def test_thermostat_restore(hass, hk_driver, events): capabilities={ ATTR_MIN_TEMP: 60, ATTR_MAX_TEMP: 70, - ATTR_HVAC_MODES: [HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF], + ATTR_HVAC_MODES: [HVACMode.HEAT_COOL, HVACMode.OFF], }, supported_features=0, original_device_class="mock-device-class", @@ -1072,7 +1062,7 @@ async def test_thermostat_hvac_modes(hass, hk_driver): entity_id = "climate.test" hass.states.async_set( - entity_id, HVAC_MODE_OFF, {ATTR_HVAC_MODES: [HVAC_MODE_HEAT, HVAC_MODE_OFF]} + entity_id, HVACMode.OFF, {ATTR_HVAC_MODES: [HVACMode.HEAT, HVACMode.OFF]} ) await hass.async_block_till_done() @@ -1106,13 +1096,13 @@ async def test_thermostat_hvac_modes_with_auto_heat_cool(hass, hk_driver): hass.states.async_set( entity_id, - HVAC_MODE_OFF, + HVACMode.OFF, { ATTR_HVAC_MODES: [ - HVAC_MODE_HEAT_COOL, - HVAC_MODE_AUTO, - HVAC_MODE_HEAT, - HVAC_MODE_OFF, + HVACMode.HEAT_COOL, + HVACMode.AUTO, + HVACMode.HEAT, + HVACMode.OFF, ] }, ) @@ -1159,7 +1149,7 @@ async def test_thermostat_hvac_modes_with_auto_heat_cool(hass, hk_driver): await hass.async_block_till_done() assert call_set_hvac_mode assert call_set_hvac_mode[0].data[ATTR_ENTITY_ID] == entity_id - assert call_set_hvac_mode[0].data[ATTR_HVAC_MODE] == HVAC_MODE_HEAT_COOL + assert call_set_hvac_mode[0].data[ATTR_HVAC_MODE] == HVACMode.HEAT_COOL assert acc.char_target_heat_cool.value == 3 @@ -1169,8 +1159,8 @@ async def test_thermostat_hvac_modes_with_auto_no_heat_cool(hass, hk_driver): hass.states.async_set( entity_id, - HVAC_MODE_HEAT, - {ATTR_HVAC_MODES: [HVAC_MODE_AUTO, HVAC_MODE_HEAT, HVAC_MODE_OFF]}, + HVACMode.HEAT, + {ATTR_HVAC_MODES: [HVACMode.AUTO, HVACMode.HEAT, HVACMode.OFF]}, ) call_set_hvac_mode = async_mock_service(hass, DOMAIN_CLIMATE, "set_hvac_mode") await hass.async_block_till_done() @@ -1216,7 +1206,7 @@ async def test_thermostat_hvac_modes_with_auto_no_heat_cool(hass, hk_driver): await hass.async_block_till_done() assert call_set_hvac_mode assert call_set_hvac_mode[0].data[ATTR_ENTITY_ID] == entity_id - assert call_set_hvac_mode[0].data[ATTR_HVAC_MODE] == HVAC_MODE_AUTO + assert call_set_hvac_mode[0].data[ATTR_HVAC_MODE] == HVACMode.AUTO assert acc.char_target_heat_cool.value == 3 @@ -1225,7 +1215,7 @@ async def test_thermostat_hvac_modes_with_auto_only(hass, hk_driver): entity_id = "climate.test" hass.states.async_set( - entity_id, HVAC_MODE_AUTO, {ATTR_HVAC_MODES: [HVAC_MODE_AUTO, HVAC_MODE_OFF]} + entity_id, HVACMode.AUTO, {ATTR_HVAC_MODES: [HVACMode.AUTO, HVACMode.OFF]} ) await hass.async_block_till_done() @@ -1271,7 +1261,7 @@ async def test_thermostat_hvac_modes_with_auto_only(hass, hk_driver): await hass.async_block_till_done() assert call_set_hvac_mode assert call_set_hvac_mode[0].data[ATTR_ENTITY_ID] == entity_id - assert call_set_hvac_mode[0].data[ATTR_HVAC_MODE] == HVAC_MODE_AUTO + assert call_set_hvac_mode[0].data[ATTR_HVAC_MODE] == HVACMode.AUTO async def test_thermostat_hvac_modes_with_heat_only(hass, hk_driver): @@ -1279,7 +1269,7 @@ async def test_thermostat_hvac_modes_with_heat_only(hass, hk_driver): entity_id = "climate.test" hass.states.async_set( - entity_id, HVAC_MODE_HEAT, {ATTR_HVAC_MODES: [HVAC_MODE_HEAT, HVAC_MODE_OFF]} + entity_id, HVACMode.HEAT, {ATTR_HVAC_MODES: [HVACMode.HEAT, HVACMode.OFF]} ) await hass.async_block_till_done() @@ -1326,13 +1316,13 @@ async def test_thermostat_hvac_modes_with_heat_only(hass, hk_driver): await hass.async_block_till_done() assert call_set_hvac_mode assert call_set_hvac_mode[0].data[ATTR_ENTITY_ID] == entity_id - assert call_set_hvac_mode[0].data[ATTR_HVAC_MODE] == HVAC_MODE_HEAT + assert call_set_hvac_mode[0].data[ATTR_HVAC_MODE] == HVACMode.HEAT acc.char_target_heat_cool.client_update_value(HC_HEAT_COOL_OFF) await hass.async_block_till_done() assert acc.char_target_heat_cool.value == HC_HEAT_COOL_OFF hass.states.async_set( - entity_id, HVAC_MODE_OFF, {ATTR_HVAC_MODES: [HVAC_MODE_HEAT, HVAC_MODE_OFF]} + entity_id, HVACMode.OFF, {ATTR_HVAC_MODES: [HVACMode.HEAT, HVACMode.OFF]} ) await hass.async_block_till_done() @@ -1357,7 +1347,7 @@ async def test_thermostat_hvac_modes_with_cool_only(hass, hk_driver): entity_id = "climate.test" hass.states.async_set( - entity_id, HVAC_MODE_COOL, {ATTR_HVAC_MODES: [HVAC_MODE_COOL, HVAC_MODE_OFF]} + entity_id, HVACMode.COOL, {ATTR_HVAC_MODES: [HVACMode.COOL, HVACMode.OFF]} ) await hass.async_block_till_done() @@ -1402,7 +1392,7 @@ async def test_thermostat_hvac_modes_with_cool_only(hass, hk_driver): await hass.async_block_till_done() assert call_set_hvac_mode assert call_set_hvac_mode[0].data[ATTR_ENTITY_ID] == entity_id - assert call_set_hvac_mode[0].data[ATTR_HVAC_MODE] == HVAC_MODE_COOL + assert call_set_hvac_mode[0].data[ATTR_HVAC_MODE] == HVACMode.COOL async def test_thermostat_hvac_modes_with_heat_cool_only(hass, hk_driver): @@ -1411,10 +1401,10 @@ async def test_thermostat_hvac_modes_with_heat_cool_only(hass, hk_driver): hass.states.async_set( entity_id, - HVAC_MODE_COOL, + HVACMode.COOL, { ATTR_CURRENT_TEMPERATURE: 30, - ATTR_HVAC_MODES: [HVAC_MODE_HEAT, HVAC_MODE_COOL, HVAC_MODE_OFF], + ATTR_HVAC_MODES: [HVACMode.HEAT, HVACMode.COOL, HVACMode.OFF], }, ) @@ -1468,7 +1458,7 @@ async def test_thermostat_hvac_modes_with_heat_cool_only(hass, hk_driver): await hass.async_block_till_done() assert call_set_hvac_mode assert call_set_hvac_mode[0].data[ATTR_ENTITY_ID] == entity_id - assert call_set_hvac_mode[0].data[ATTR_HVAC_MODE] == HVAC_MODE_COOL + assert call_set_hvac_mode[0].data[ATTR_HVAC_MODE] == HVACMode.COOL hk_driver.set_characteristics( { HAP_REPR_CHARS: [ @@ -1490,7 +1480,7 @@ async def test_thermostat_hvac_modes_with_heat_cool_only(hass, hk_driver): await hass.async_block_till_done() assert call_set_hvac_mode assert call_set_hvac_mode[1].data[ATTR_ENTITY_ID] == entity_id - assert call_set_hvac_mode[1].data[ATTR_HVAC_MODE] == HVAC_MODE_HEAT + assert call_set_hvac_mode[1].data[ATTR_HVAC_MODE] == HVACMode.HEAT async def test_thermostat_hvac_modes_without_off(hass, hk_driver): @@ -1498,7 +1488,7 @@ async def test_thermostat_hvac_modes_without_off(hass, hk_driver): entity_id = "climate.test" hass.states.async_set( - entity_id, HVAC_MODE_AUTO, {ATTR_HVAC_MODES: [HVAC_MODE_AUTO, HVAC_MODE_HEAT]} + entity_id, HVACMode.AUTO, {ATTR_HVAC_MODES: [HVACMode.AUTO, HVACMode.HEAT]} ) await hass.async_block_till_done() @@ -1537,7 +1527,7 @@ async def test_thermostat_without_target_temp_only_range(hass, hk_driver, events # support_auto = True hass.states.async_set( entity_id, - HVAC_MODE_OFF, + HVACMode.OFF, {ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE_RANGE}, ) await hass.async_block_till_done() @@ -1559,20 +1549,20 @@ async def test_thermostat_without_target_temp_only_range(hass, hk_driver, events hass.states.async_set( entity_id, - HVAC_MODE_HEAT_COOL, + HVACMode.HEAT_COOL, { ATTR_TARGET_TEMP_HIGH: 22.0, ATTR_TARGET_TEMP_LOW: 20.0, ATTR_CURRENT_TEMPERATURE: 18.0, - ATTR_HVAC_ACTION: CURRENT_HVAC_HEAT, + ATTR_HVAC_ACTION: HVACAction.HEATING, ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE_RANGE, ATTR_HVAC_MODES: [ - HVAC_MODE_HEAT, - HVAC_MODE_HEAT_COOL, - HVAC_MODE_FAN_ONLY, - HVAC_MODE_COOL, - HVAC_MODE_OFF, - HVAC_MODE_AUTO, + HVACMode.HEAT, + HVACMode.HEAT_COOL, + HVACMode.FAN_ONLY, + HVACMode.COOL, + HVACMode.OFF, + HVACMode.AUTO, ], }, ) @@ -1586,20 +1576,20 @@ async def test_thermostat_without_target_temp_only_range(hass, hk_driver, events hass.states.async_set( entity_id, - HVAC_MODE_COOL, + HVACMode.COOL, { ATTR_TARGET_TEMP_HIGH: 23.0, ATTR_TARGET_TEMP_LOW: 19.0, ATTR_CURRENT_TEMPERATURE: 24.0, - ATTR_HVAC_ACTION: CURRENT_HVAC_COOL, + ATTR_HVAC_ACTION: HVACAction.COOLING, ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE_RANGE, ATTR_HVAC_MODES: [ - HVAC_MODE_HEAT, - HVAC_MODE_HEAT_COOL, - HVAC_MODE_FAN_ONLY, - HVAC_MODE_COOL, - HVAC_MODE_OFF, - HVAC_MODE_AUTO, + HVACMode.HEAT, + HVACMode.HEAT_COOL, + HVACMode.FAN_ONLY, + HVACMode.COOL, + HVACMode.OFF, + HVACMode.AUTO, ], }, ) @@ -1613,20 +1603,20 @@ async def test_thermostat_without_target_temp_only_range(hass, hk_driver, events hass.states.async_set( entity_id, - HVAC_MODE_COOL, + HVACMode.COOL, { ATTR_TARGET_TEMP_HIGH: 23.0, ATTR_TARGET_TEMP_LOW: 19.0, ATTR_CURRENT_TEMPERATURE: 21.0, - ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE, + ATTR_HVAC_ACTION: HVACAction.IDLE, ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE_RANGE, ATTR_HVAC_MODES: [ - HVAC_MODE_HEAT, - HVAC_MODE_HEAT_COOL, - HVAC_MODE_FAN_ONLY, - HVAC_MODE_COOL, - HVAC_MODE_OFF, - HVAC_MODE_AUTO, + HVACMode.HEAT, + HVACMode.HEAT_COOL, + HVACMode.FAN_ONLY, + HVACMode.COOL, + HVACMode.OFF, + HVACMode.AUTO, ], }, ) @@ -1667,12 +1657,12 @@ async def test_thermostat_without_target_temp_only_range(hass, hk_driver, events hass.states.async_set( entity_id, - HVAC_MODE_HEAT, + HVACMode.HEAT, { ATTR_TARGET_TEMP_HIGH: 23.0, ATTR_TARGET_TEMP_LOW: 19.0, ATTR_CURRENT_TEMPERATURE: 21.0, - ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE, + ATTR_HVAC_ACTION: HVACAction.IDLE, ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE_RANGE, }, ) @@ -1716,7 +1706,7 @@ async def test_water_heater(hass, hk_driver, events): """Test if accessory and HA are updated accordingly.""" entity_id = "water_heater.test" - hass.states.async_set(entity_id, HVAC_MODE_HEAT) + hass.states.async_set(entity_id, HVACMode.HEAT) await hass.async_block_till_done() acc = WaterHeater(hass, hk_driver, "WaterHeater", entity_id, 2, None) await acc.run() @@ -1741,9 +1731,9 @@ async def test_water_heater(hass, hk_driver, events): hass.states.async_set( entity_id, - HVAC_MODE_HEAT, + HVACMode.HEAT, { - ATTR_HVAC_MODE: HVAC_MODE_HEAT, + ATTR_HVAC_MODE: HVACMode.HEAT, ATTR_TEMPERATURE: 56.0, ATTR_CURRENT_TEMPERATURE: 35.0, }, @@ -1756,7 +1746,7 @@ async def test_water_heater(hass, hk_driver, events): assert acc.char_display_units.value == 0 hass.states.async_set( - entity_id, HVAC_MODE_HEAT_COOL, {ATTR_HVAC_MODE: HVAC_MODE_HEAT_COOL} + entity_id, HVACMode.HEAT_COOL, {ATTR_HVAC_MODE: HVACMode.HEAT_COOL} ) await hass.async_block_till_done() assert acc.char_target_heat_cool.value == 1 @@ -1790,14 +1780,14 @@ async def test_water_heater_fahrenheit(hass, hk_driver, events): """Test if accessory and HA are update accordingly.""" entity_id = "water_heater.test" - hass.states.async_set(entity_id, HVAC_MODE_HEAT) + hass.states.async_set(entity_id, HVACMode.HEAT) await hass.async_block_till_done() with patch.object(hass.config.units, CONF_TEMPERATURE_UNIT, new=TEMP_FAHRENHEIT): acc = WaterHeater(hass, hk_driver, "WaterHeater", entity_id, 2, None) await acc.run() await hass.async_block_till_done() - hass.states.async_set(entity_id, HVAC_MODE_HEAT, {ATTR_TEMPERATURE: 131}) + hass.states.async_set(entity_id, HVACMode.HEAT, {ATTR_TEMPERATURE: 131}) await hass.async_block_till_done() assert acc.char_target_temp.value == 55.0 assert acc.char_current_temp.value == 50 @@ -1822,19 +1812,19 @@ async def test_water_heater_get_temperature_range(hass, hk_driver): """Test if temperature range is evaluated correctly.""" entity_id = "water_heater.test" - hass.states.async_set(entity_id, HVAC_MODE_HEAT) + hass.states.async_set(entity_id, HVACMode.HEAT) await hass.async_block_till_done() acc = WaterHeater(hass, hk_driver, "WaterHeater", entity_id, 2, None) hass.states.async_set( - entity_id, HVAC_MODE_HEAT, {ATTR_MIN_TEMP: 20, ATTR_MAX_TEMP: 25} + entity_id, HVACMode.HEAT, {ATTR_MIN_TEMP: 20, ATTR_MAX_TEMP: 25} ) await hass.async_block_till_done() assert acc.get_temperature_range() == (20, 25) acc._unit = TEMP_FAHRENHEIT hass.states.async_set( - entity_id, HVAC_MODE_OFF, {ATTR_MIN_TEMP: 60, ATTR_MAX_TEMP: 70} + entity_id, HVACMode.OFF, {ATTR_MIN_TEMP: 60, ATTR_MAX_TEMP: 70} ) await hass.async_block_till_done() assert acc.get_temperature_range() == (15.5, 21.0) @@ -1890,7 +1880,7 @@ async def test_thermostat_with_no_modes_when_we_first_see(hass, hk_driver, event # support_auto = True hass.states.async_set( entity_id, - HVAC_MODE_OFF, + HVACMode.OFF, { ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_TEMPERATURE_RANGE, @@ -1918,13 +1908,13 @@ async def test_thermostat_with_no_modes_when_we_first_see(hass, hk_driver, event hass.states.async_set( entity_id, - HVAC_MODE_HEAT_COOL, + HVACMode.HEAT_COOL, { ATTR_TARGET_TEMP_HIGH: 22.0, ATTR_TARGET_TEMP_LOW: 20.0, ATTR_CURRENT_TEMPERATURE: 18.0, - ATTR_HVAC_ACTION: CURRENT_HVAC_HEAT, - ATTR_HVAC_MODES: [HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, HVAC_MODE_AUTO], + ATTR_HVAC_ACTION: HVACAction.HEATING, + ATTR_HVAC_MODES: [HVACMode.HEAT_COOL, HVACMode.OFF, HVACMode.AUTO], }, ) await hass.async_block_till_done() @@ -1943,7 +1933,7 @@ async def test_thermostat_with_no_off_after_recheck(hass, hk_driver, events): # support_auto = True hass.states.async_set( entity_id, - HVAC_MODE_COOL, + HVACMode.COOL, { ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_TEMPERATURE_RANGE, @@ -1971,13 +1961,13 @@ async def test_thermostat_with_no_off_after_recheck(hass, hk_driver, events): hass.states.async_set( entity_id, - HVAC_MODE_HEAT_COOL, + HVACMode.HEAT_COOL, { ATTR_TARGET_TEMP_HIGH: 22.0, ATTR_TARGET_TEMP_LOW: 20.0, ATTR_CURRENT_TEMPERATURE: 18.0, - ATTR_HVAC_ACTION: CURRENT_HVAC_HEAT, - ATTR_HVAC_MODES: [HVAC_MODE_HEAT_COOL, HVAC_MODE_AUTO], + ATTR_HVAC_ACTION: HVACAction.HEATING, + ATTR_HVAC_MODES: [HVACMode.HEAT_COOL, HVACMode.AUTO], }, ) await hass.async_block_till_done() @@ -1995,7 +1985,7 @@ async def test_thermostat_with_temp_clamps(hass, hk_driver, events): hass.states.async_set( entity_id, - HVAC_MODE_COOL, + HVACMode.COOL, { ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_TEMPERATURE_RANGE, @@ -2025,13 +2015,13 @@ async def test_thermostat_with_temp_clamps(hass, hk_driver, events): hass.states.async_set( entity_id, - HVAC_MODE_HEAT_COOL, + HVACMode.HEAT_COOL, { ATTR_TARGET_TEMP_HIGH: 822.0, ATTR_TARGET_TEMP_LOW: 20.0, ATTR_CURRENT_TEMPERATURE: 9918.0, - ATTR_HVAC_ACTION: CURRENT_HVAC_HEAT, - ATTR_HVAC_MODES: [HVAC_MODE_HEAT_COOL, HVAC_MODE_AUTO], + ATTR_HVAC_ACTION: HVACAction.HEATING, + ATTR_HVAC_MODES: [HVACMode.HEAT_COOL, HVACMode.AUTO], }, ) await hass.async_block_till_done() @@ -2048,7 +2038,7 @@ async def test_thermostat_with_fan_modes_with_auto(hass, hk_driver, events): entity_id = "climate.test" hass.states.async_set( entity_id, - HVAC_MODE_OFF, + HVACMode.OFF, { ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_TEMPERATURE_RANGE @@ -2056,16 +2046,16 @@ async def test_thermostat_with_fan_modes_with_auto(hass, hk_driver, events): | SUPPORT_SWING_MODE, ATTR_FAN_MODES: [FAN_AUTO, FAN_LOW, FAN_MEDIUM, FAN_HIGH], ATTR_SWING_MODES: [SWING_BOTH, SWING_OFF, SWING_HORIZONTAL], - ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE, + ATTR_HVAC_ACTION: HVACAction.IDLE, ATTR_FAN_MODE: FAN_AUTO, ATTR_SWING_MODE: SWING_BOTH, ATTR_HVAC_MODES: [ - HVAC_MODE_HEAT, - HVAC_MODE_HEAT_COOL, - HVAC_MODE_FAN_ONLY, - HVAC_MODE_COOL, - HVAC_MODE_OFF, - HVAC_MODE_AUTO, + HVACMode.HEAT, + HVACMode.HEAT_COOL, + HVACMode.FAN_ONLY, + HVACMode.COOL, + HVACMode.OFF, + HVACMode.AUTO, ], }, ) @@ -2087,7 +2077,7 @@ async def test_thermostat_with_fan_modes_with_auto(hass, hk_driver, events): hass.states.async_set( entity_id, - HVAC_MODE_OFF, + HVACMode.OFF, { ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_TEMPERATURE_RANGE @@ -2095,16 +2085,16 @@ async def test_thermostat_with_fan_modes_with_auto(hass, hk_driver, events): | SUPPORT_SWING_MODE, ATTR_FAN_MODES: [FAN_AUTO, FAN_LOW, FAN_MEDIUM, FAN_HIGH], ATTR_SWING_MODES: [SWING_BOTH, SWING_OFF, SWING_HORIZONTAL], - ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE, + ATTR_HVAC_ACTION: HVACAction.IDLE, ATTR_FAN_MODE: FAN_LOW, ATTR_SWING_MODE: SWING_BOTH, ATTR_HVAC_MODES: [ - HVAC_MODE_HEAT, - HVAC_MODE_HEAT_COOL, - HVAC_MODE_FAN_ONLY, - HVAC_MODE_COOL, - HVAC_MODE_OFF, - HVAC_MODE_AUTO, + HVACMode.HEAT, + HVACMode.HEAT_COOL, + HVACMode.FAN_ONLY, + HVACMode.COOL, + HVACMode.OFF, + HVACMode.AUTO, ], }, ) @@ -2252,7 +2242,7 @@ async def test_thermostat_with_fan_modes_with_off(hass, hk_driver, events): entity_id = "climate.test" hass.states.async_set( entity_id, - HVAC_MODE_COOL, + HVACMode.COOL, { ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_TEMPERATURE_RANGE @@ -2260,16 +2250,16 @@ async def test_thermostat_with_fan_modes_with_off(hass, hk_driver, events): | SUPPORT_SWING_MODE, ATTR_FAN_MODES: [FAN_ON, FAN_OFF], ATTR_SWING_MODES: [SWING_BOTH, SWING_OFF, SWING_HORIZONTAL], - ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE, + ATTR_HVAC_ACTION: HVACAction.IDLE, ATTR_FAN_MODE: FAN_ON, ATTR_SWING_MODE: SWING_BOTH, ATTR_HVAC_MODES: [ - HVAC_MODE_HEAT, - HVAC_MODE_HEAT_COOL, - HVAC_MODE_FAN_ONLY, - HVAC_MODE_COOL, - HVAC_MODE_OFF, - HVAC_MODE_AUTO, + HVACMode.HEAT, + HVACMode.HEAT_COOL, + HVACMode.FAN_ONLY, + HVACMode.COOL, + HVACMode.OFF, + HVACMode.AUTO, ], }, ) @@ -2291,7 +2281,7 @@ async def test_thermostat_with_fan_modes_with_off(hass, hk_driver, events): hass.states.async_set( entity_id, - HVAC_MODE_COOL, + HVACMode.COOL, { ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_TEMPERATURE_RANGE @@ -2299,16 +2289,16 @@ async def test_thermostat_with_fan_modes_with_off(hass, hk_driver, events): | SUPPORT_SWING_MODE, ATTR_FAN_MODES: [FAN_ON, FAN_OFF], ATTR_SWING_MODES: [SWING_BOTH, SWING_OFF, SWING_HORIZONTAL], - ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE, + ATTR_HVAC_ACTION: HVACAction.IDLE, ATTR_FAN_MODE: FAN_OFF, ATTR_SWING_MODE: SWING_BOTH, ATTR_HVAC_MODES: [ - HVAC_MODE_HEAT, - HVAC_MODE_HEAT_COOL, - HVAC_MODE_FAN_ONLY, - HVAC_MODE_COOL, - HVAC_MODE_OFF, - HVAC_MODE_AUTO, + HVACMode.HEAT, + HVACMode.HEAT_COOL, + HVACMode.FAN_ONLY, + HVACMode.COOL, + HVACMode.OFF, + HVACMode.AUTO, ], }, ) @@ -2359,7 +2349,7 @@ async def test_thermostat_with_fan_modes_set_to_none(hass, hk_driver, events): entity_id = "climate.test" hass.states.async_set( entity_id, - HVAC_MODE_OFF, + HVACMode.OFF, { ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_TEMPERATURE_RANGE @@ -2367,16 +2357,16 @@ async def test_thermostat_with_fan_modes_set_to_none(hass, hk_driver, events): | SUPPORT_SWING_MODE, ATTR_FAN_MODES: None, ATTR_SWING_MODES: [SWING_BOTH, SWING_OFF, SWING_HORIZONTAL], - ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE, + ATTR_HVAC_ACTION: HVACAction.IDLE, ATTR_FAN_MODE: FAN_AUTO, ATTR_SWING_MODE: SWING_BOTH, ATTR_HVAC_MODES: [ - HVAC_MODE_HEAT, - HVAC_MODE_HEAT_COOL, - HVAC_MODE_FAN_ONLY, - HVAC_MODE_COOL, - HVAC_MODE_OFF, - HVAC_MODE_AUTO, + HVACMode.HEAT, + HVACMode.HEAT_COOL, + HVACMode.FAN_ONLY, + HVACMode.COOL, + HVACMode.OFF, + HVACMode.AUTO, ], }, ) @@ -2403,23 +2393,23 @@ async def test_thermostat_with_fan_modes_set_to_none_not_supported( entity_id = "climate.test" hass.states.async_set( entity_id, - HVAC_MODE_OFF, + HVACMode.OFF, { ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_TEMPERATURE_RANGE | SUPPORT_SWING_MODE, ATTR_FAN_MODES: None, ATTR_SWING_MODES: [SWING_BOTH, SWING_OFF, SWING_HORIZONTAL], - ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE, + ATTR_HVAC_ACTION: HVACAction.IDLE, ATTR_FAN_MODE: FAN_AUTO, ATTR_SWING_MODE: SWING_BOTH, ATTR_HVAC_MODES: [ - HVAC_MODE_HEAT, - HVAC_MODE_HEAT_COOL, - HVAC_MODE_FAN_ONLY, - HVAC_MODE_COOL, - HVAC_MODE_OFF, - HVAC_MODE_AUTO, + HVACMode.HEAT, + HVACMode.HEAT_COOL, + HVACMode.FAN_ONLY, + HVACMode.COOL, + HVACMode.OFF, + HVACMode.AUTO, ], }, ) @@ -2446,7 +2436,7 @@ async def test_thermostat_with_supported_features_target_temp_but_fan_mode_set( entity_id = "climate.test" hass.states.async_set( entity_id, - HVAC_MODE_OFF, + HVACMode.OFF, { ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE, ATTR_MIN_TEMP: 44.6, @@ -2457,13 +2447,13 @@ async def test_thermostat_with_supported_features_target_temp_but_fan_mode_set( ATTR_TARGET_TEMP_LOW: None, ATTR_FAN_MODE: FAN_AUTO, ATTR_FAN_MODES: None, - ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE, + ATTR_HVAC_ACTION: HVACAction.IDLE, ATTR_FAN_MODE: FAN_AUTO, ATTR_PRESET_MODE: "home", ATTR_FRIENDLY_NAME: "Rec Room", ATTR_HVAC_MODES: [ - HVAC_MODE_OFF, - HVAC_MODE_HEAT, + HVACMode.OFF, + HVACMode.HEAT, ], }, ) diff --git a/tests/components/maxcube/test_maxcube_climate.py b/tests/components/maxcube/test_maxcube_climate.py index 1a3cd4945ce..f3c3eb17158 100644 --- a/tests/components/maxcube/test_maxcube_climate.py +++ b/tests/components/maxcube/test_maxcube_climate.py @@ -12,7 +12,7 @@ from maxcube.thermostat import MaxThermostat from maxcube.wallthermostat import MaxWallThermostat import pytest -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_CURRENT_TEMPERATURE, ATTR_HVAC_ACTION, ATTR_HVAC_MODE, @@ -23,14 +23,7 @@ from homeassistant.components.climate.const import ( ATTR_PRESET_MODES, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, - CURRENT_HVAC_HEAT, - CURRENT_HVAC_IDLE, - CURRENT_HVAC_OFF, DOMAIN as CLIMATE_DOMAIN, - HVAC_MODE_AUTO, - HVAC_MODE_DRY, - HVAC_MODE_HEAT, - HVAC_MODE_OFF, PRESET_AWAY, PRESET_BOOST, PRESET_COMFORT, @@ -40,6 +33,8 @@ from homeassistant.components.climate.const import ( SERVICE_SET_PRESET_MODE, SERVICE_SET_TEMPERATURE, ClimateEntityFeature, + HVACAction, + HVACMode, ) from homeassistant.components.maxcube.climate import ( MAX_TEMPERATURE, @@ -72,13 +67,13 @@ async def test_setup_thermostat(hass, cube: MaxCube): assert entity.unique_id == "AABBCCDD01" state = hass.states.get(ENTITY_ID) - assert state.state == HVAC_MODE_AUTO + assert state.state == HVACMode.AUTO assert state.attributes.get(ATTR_FRIENDLY_NAME) == "TestRoom TestThermostat" - assert state.attributes.get(ATTR_HVAC_ACTION) == CURRENT_HVAC_HEAT + assert state.attributes.get(ATTR_HVAC_ACTION) == HVACAction.HEATING assert state.attributes.get(ATTR_HVAC_MODES) == [ - HVAC_MODE_OFF, - HVAC_MODE_AUTO, - HVAC_MODE_HEAT, + HVACMode.OFF, + HVACMode.AUTO, + HVACMode.HEAT, ] assert state.attributes.get(ATTR_PRESET_MODES) == [ PRESET_NONE, @@ -108,9 +103,9 @@ async def test_setup_wallthermostat(hass, cube: MaxCube): assert entity.unique_id == "AABBCCDD02" state = hass.states.get(WALL_ENTITY_ID) - assert state.state == HVAC_MODE_OFF + assert state.state == HVACMode.OFF assert state.attributes.get(ATTR_FRIENDLY_NAME) == "TestRoom TestWallThermostat" - assert state.attributes.get(ATTR_HVAC_ACTION) == CURRENT_HVAC_HEAT + assert state.attributes.get(ATTR_HVAC_ACTION) == HVACAction.HEATING assert state.attributes.get(ATTR_PRESET_MODE) == PRESET_NONE assert state.attributes.get(ATTR_MAX_TEMP) == 29.0 assert state.attributes.get(ATTR_MIN_TEMP) == 5.0 @@ -125,7 +120,7 @@ async def test_thermostat_set_hvac_mode_off( await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_HVAC_MODE, - {ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVAC_MODE_OFF}, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVACMode.OFF}, blocking=True, ) cube.set_temperature_mode.assert_called_once_with( @@ -140,13 +135,13 @@ async def test_thermostat_set_hvac_mode_off( await hass.async_block_till_done() state = hass.states.get(ENTITY_ID) - assert state.state == HVAC_MODE_OFF + assert state.state == HVACMode.OFF assert state.attributes.get(ATTR_TEMPERATURE) is None - assert state.attributes.get(ATTR_HVAC_ACTION) == CURRENT_HVAC_OFF + assert state.attributes.get(ATTR_HVAC_ACTION) == HVACAction.OFF assert state.attributes.get(VALVE_POSITION) == 0 wall_state = hass.states.get(WALL_ENTITY_ID) - assert wall_state.attributes.get(ATTR_HVAC_ACTION) == CURRENT_HVAC_OFF + assert wall_state.attributes.get(ATTR_HVAC_ACTION) == HVACAction.OFF async def test_thermostat_set_hvac_mode_heat( @@ -156,7 +151,7 @@ async def test_thermostat_set_hvac_mode_heat( await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_HVAC_MODE, - {ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVAC_MODE_HEAT}, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVACMode.HEAT}, blocking=True, ) cube.set_temperature_mode.assert_called_once_with( @@ -169,7 +164,7 @@ async def test_thermostat_set_hvac_mode_heat( await hass.async_block_till_done() state = hass.states.get(ENTITY_ID) - assert state.state == HVAC_MODE_HEAT + assert state.state == HVACMode.HEAT async def test_thermostat_set_invalid_hvac_mode( @@ -180,7 +175,7 @@ async def test_thermostat_set_invalid_hvac_mode( await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_HVAC_MODE, - {ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVAC_MODE_DRY}, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVACMode.DRY}, blocking=True, ) cube.set_temperature_mode.assert_not_called() @@ -204,9 +199,9 @@ async def test_thermostat_set_temperature( await hass.async_block_till_done() state = hass.states.get(ENTITY_ID) - assert state.state == HVAC_MODE_AUTO + assert state.state == HVACMode.AUTO assert state.attributes.get(ATTR_TEMPERATURE) == 10.0 - assert state.attributes.get(ATTR_HVAC_ACTION) == CURRENT_HVAC_IDLE + assert state.attributes.get(ATTR_HVAC_ACTION) == HVACAction.IDLE async def test_thermostat_set_no_temperature( @@ -246,7 +241,7 @@ async def test_thermostat_set_preset_on(hass, cube: MaxCube, thermostat: MaxTher await hass.async_block_till_done() state = hass.states.get(ENTITY_ID) - assert state.state == HVAC_MODE_HEAT + assert state.state == HVACMode.HEAT assert state.attributes.get(ATTR_TEMPERATURE) is None assert state.attributes.get(ATTR_PRESET_MODE) == PRESET_ON @@ -271,7 +266,7 @@ async def test_thermostat_set_preset_comfort( await hass.async_block_till_done() state = hass.states.get(ENTITY_ID) - assert state.state == HVAC_MODE_HEAT + assert state.state == HVACMode.HEAT assert state.attributes.get(ATTR_TEMPERATURE) == thermostat.comfort_temperature assert state.attributes.get(ATTR_PRESET_MODE) == PRESET_COMFORT @@ -296,7 +291,7 @@ async def test_thermostat_set_preset_eco( await hass.async_block_till_done() state = hass.states.get(ENTITY_ID) - assert state.state == HVAC_MODE_HEAT + assert state.state == HVACMode.HEAT assert state.attributes.get(ATTR_TEMPERATURE) == thermostat.eco_temperature assert state.attributes.get(ATTR_PRESET_MODE) == PRESET_ECO @@ -321,7 +316,7 @@ async def test_thermostat_set_preset_away( await hass.async_block_till_done() state = hass.states.get(ENTITY_ID) - assert state.state == HVAC_MODE_HEAT + assert state.state == HVACMode.HEAT assert state.attributes.get(ATTR_TEMPERATURE) == thermostat.eco_temperature assert state.attributes.get(ATTR_PRESET_MODE) == PRESET_AWAY @@ -346,7 +341,7 @@ async def test_thermostat_set_preset_boost( await hass.async_block_till_done() state = hass.states.get(ENTITY_ID) - assert state.state == HVAC_MODE_AUTO + assert state.state == HVACMode.AUTO assert state.attributes.get(ATTR_TEMPERATURE) == thermostat.eco_temperature assert state.attributes.get(ATTR_PRESET_MODE) == PRESET_BOOST @@ -387,7 +382,7 @@ async def test_wallthermostat_set_hvac_mode_heat( await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_HVAC_MODE, - {ATTR_ENTITY_ID: WALL_ENTITY_ID, ATTR_HVAC_MODE: HVAC_MODE_HEAT}, + {ATTR_ENTITY_ID: WALL_ENTITY_ID, ATTR_HVAC_MODE: HVACMode.HEAT}, blocking=True, ) cube.set_temperature_mode.assert_called_once_with( @@ -399,7 +394,7 @@ async def test_wallthermostat_set_hvac_mode_heat( await hass.async_block_till_done() state = hass.states.get(WALL_ENTITY_ID) - assert state.state == HVAC_MODE_HEAT + assert state.state == HVACMode.HEAT assert state.attributes.get(ATTR_TEMPERATURE) == MIN_TEMPERATURE @@ -410,7 +405,7 @@ async def test_wallthermostat_set_hvac_mode_auto( await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_HVAC_MODE, - {ATTR_ENTITY_ID: WALL_ENTITY_ID, ATTR_HVAC_MODE: HVAC_MODE_AUTO}, + {ATTR_ENTITY_ID: WALL_ENTITY_ID, ATTR_HVAC_MODE: HVACMode.AUTO}, blocking=True, ) cube.set_temperature_mode.assert_called_once_with( @@ -423,5 +418,5 @@ async def test_wallthermostat_set_hvac_mode_auto( await hass.async_block_till_done() state = hass.states.get(WALL_ENTITY_ID) - assert state.state == HVAC_MODE_AUTO + assert state.state == HVACMode.AUTO assert state.attributes.get(ATTR_TEMPERATURE) == 23.0 diff --git a/tests/components/nest/test_climate_sdm.py b/tests/components/nest/test_climate_sdm.py index c271687a348..440855f6ab7 100644 --- a/tests/components/nest/test_climate_sdm.py +++ b/tests/components/nest/test_climate_sdm.py @@ -14,7 +14,7 @@ from google_nest_sdm.auth import AbstractAuth from google_nest_sdm.event import EventMessage import pytest -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_CURRENT_TEMPERATURE, ATTR_FAN_MODE, ATTR_FAN_MODES, @@ -24,22 +24,15 @@ from homeassistant.components.climate.const import ( ATTR_PRESET_MODES, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, - CURRENT_HVAC_COOL, - CURRENT_HVAC_HEAT, - CURRENT_HVAC_IDLE, - CURRENT_HVAC_OFF, FAN_LOW, FAN_OFF, FAN_ON, - HVAC_MODE_COOL, - HVAC_MODE_DRY, - HVAC_MODE_HEAT, - HVAC_MODE_HEAT_COOL, - HVAC_MODE_OFF, PRESET_ECO, PRESET_NONE, PRESET_SLEEP, ClimateEntityFeature, + HVACAction, + HVACMode, ) from homeassistant.const import ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE from homeassistant.core import HomeAssistant @@ -136,14 +129,14 @@ async def test_thermostat_off( assert len(hass.states.async_all()) == 1 thermostat = hass.states.get("climate.my_thermostat") assert thermostat is not None - assert thermostat.state == HVAC_MODE_OFF - assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF + assert thermostat.state == HVACMode.OFF + assert thermostat.attributes[ATTR_HVAC_ACTION] == HVACAction.OFF assert thermostat.attributes[ATTR_CURRENT_TEMPERATURE] == 16.2 assert set(thermostat.attributes[ATTR_HVAC_MODES]) == { - HVAC_MODE_HEAT, - HVAC_MODE_COOL, - HVAC_MODE_HEAT_COOL, - HVAC_MODE_OFF, + HVACMode.HEAT, + HVACMode.COOL, + HVACMode.HEAT_COOL, + HVACMode.OFF, } assert thermostat.attributes[ATTR_TEMPERATURE] is None assert thermostat.attributes[ATTR_TARGET_TEMP_LOW] is None @@ -180,14 +173,14 @@ async def test_thermostat_heat( assert len(hass.states.async_all()) == 1 thermostat = hass.states.get("climate.my_thermostat") assert thermostat is not None - assert thermostat.state == HVAC_MODE_HEAT - assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_HEAT + assert thermostat.state == HVACMode.HEAT + assert thermostat.attributes[ATTR_HVAC_ACTION] == HVACAction.HEATING assert thermostat.attributes[ATTR_CURRENT_TEMPERATURE] == 16.2 assert set(thermostat.attributes[ATTR_HVAC_MODES]) == { - HVAC_MODE_HEAT, - HVAC_MODE_COOL, - HVAC_MODE_HEAT_COOL, - HVAC_MODE_OFF, + HVACMode.HEAT, + HVACMode.COOL, + HVACMode.HEAT_COOL, + HVACMode.OFF, } assert thermostat.attributes[ATTR_TEMPERATURE] == 22.0 assert thermostat.attributes[ATTR_TARGET_TEMP_LOW] is None @@ -222,14 +215,14 @@ async def test_thermostat_cool( assert len(hass.states.async_all()) == 1 thermostat = hass.states.get("climate.my_thermostat") assert thermostat is not None - assert thermostat.state == HVAC_MODE_COOL - assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_COOL + assert thermostat.state == HVACMode.COOL + assert thermostat.attributes[ATTR_HVAC_ACTION] == HVACAction.COOLING assert thermostat.attributes[ATTR_CURRENT_TEMPERATURE] == 29.9 assert set(thermostat.attributes[ATTR_HVAC_MODES]) == { - HVAC_MODE_HEAT, - HVAC_MODE_COOL, - HVAC_MODE_HEAT_COOL, - HVAC_MODE_OFF, + HVACMode.HEAT, + HVACMode.COOL, + HVACMode.HEAT_COOL, + HVACMode.OFF, } assert thermostat.attributes[ATTR_TEMPERATURE] == 28.0 assert thermostat.attributes[ATTR_TARGET_TEMP_LOW] is None @@ -265,14 +258,14 @@ async def test_thermostat_heatcool( assert len(hass.states.async_all()) == 1 thermostat = hass.states.get("climate.my_thermostat") assert thermostat is not None - assert thermostat.state == HVAC_MODE_HEAT_COOL - assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_COOL + assert thermostat.state == HVACMode.HEAT_COOL + assert thermostat.attributes[ATTR_HVAC_ACTION] == HVACAction.COOLING assert thermostat.attributes[ATTR_CURRENT_TEMPERATURE] == 29.9 assert set(thermostat.attributes[ATTR_HVAC_MODES]) == { - HVAC_MODE_HEAT, - HVAC_MODE_COOL, - HVAC_MODE_HEAT_COOL, - HVAC_MODE_OFF, + HVACMode.HEAT, + HVACMode.COOL, + HVACMode.HEAT_COOL, + HVACMode.OFF, } assert thermostat.attributes[ATTR_TARGET_TEMP_LOW] == 22.0 assert thermostat.attributes[ATTR_TARGET_TEMP_HIGH] == 28.0 @@ -314,14 +307,14 @@ async def test_thermostat_eco_off( assert len(hass.states.async_all()) == 1 thermostat = hass.states.get("climate.my_thermostat") assert thermostat is not None - assert thermostat.state == HVAC_MODE_HEAT_COOL - assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_COOL + assert thermostat.state == HVACMode.HEAT_COOL + assert thermostat.attributes[ATTR_HVAC_ACTION] == HVACAction.COOLING assert thermostat.attributes[ATTR_CURRENT_TEMPERATURE] == 29.9 assert set(thermostat.attributes[ATTR_HVAC_MODES]) == { - HVAC_MODE_HEAT, - HVAC_MODE_COOL, - HVAC_MODE_HEAT_COOL, - HVAC_MODE_OFF, + HVACMode.HEAT, + HVACMode.COOL, + HVACMode.HEAT_COOL, + HVACMode.OFF, } assert thermostat.attributes[ATTR_TARGET_TEMP_LOW] == 22.0 assert thermostat.attributes[ATTR_TARGET_TEMP_HIGH] == 28.0 @@ -363,14 +356,14 @@ async def test_thermostat_eco_on( assert len(hass.states.async_all()) == 1 thermostat = hass.states.get("climate.my_thermostat") assert thermostat is not None - assert thermostat.state == HVAC_MODE_HEAT_COOL - assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_COOL + assert thermostat.state == HVACMode.HEAT_COOL + assert thermostat.attributes[ATTR_HVAC_ACTION] == HVACAction.COOLING assert thermostat.attributes[ATTR_CURRENT_TEMPERATURE] == 29.9 assert set(thermostat.attributes[ATTR_HVAC_MODES]) == { - HVAC_MODE_HEAT, - HVAC_MODE_COOL, - HVAC_MODE_HEAT_COOL, - HVAC_MODE_OFF, + HVACMode.HEAT, + HVACMode.COOL, + HVACMode.HEAT_COOL, + HVACMode.OFF, } assert thermostat.attributes[ATTR_TARGET_TEMP_LOW] == 21.0 assert thermostat.attributes[ATTR_TARGET_TEMP_HIGH] == 29.0 @@ -409,12 +402,12 @@ async def test_thermostat_eco_heat_only( assert len(hass.states.async_all()) == 1 thermostat = hass.states.get("climate.my_thermostat") assert thermostat is not None - assert thermostat.state == HVAC_MODE_HEAT - assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE + assert thermostat.state == HVACMode.HEAT + assert thermostat.attributes[ATTR_HVAC_ACTION] == HVACAction.IDLE assert thermostat.attributes[ATTR_CURRENT_TEMPERATURE] == 29.9 assert set(thermostat.attributes[ATTR_HVAC_MODES]) == { - HVAC_MODE_HEAT, - HVAC_MODE_OFF, + HVACMode.HEAT, + HVACMode.OFF, } assert thermostat.attributes[ATTR_TEMPERATURE] == 21.0 assert ATTR_TARGET_TEMP_LOW not in thermostat.attributes @@ -445,10 +438,10 @@ async def test_thermostat_set_hvac_mode( assert len(hass.states.async_all()) == 1 thermostat = hass.states.get("climate.my_thermostat") assert thermostat is not None - assert thermostat.state == HVAC_MODE_OFF - assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF + assert thermostat.state == HVACMode.OFF + assert thermostat.attributes[ATTR_HVAC_ACTION] == HVACAction.OFF - await common.async_set_hvac_mode(hass, HVAC_MODE_HEAT) + await common.async_set_hvac_mode(hass, HVACMode.HEAT) await hass.async_block_till_done() assert auth.method == "post" @@ -461,8 +454,8 @@ async def test_thermostat_set_hvac_mode( # Local state does not reflect the update thermostat = hass.states.get("climate.my_thermostat") assert thermostat is not None - assert thermostat.state == HVAC_MODE_OFF - assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF + assert thermostat.state == HVACMode.OFF + assert thermostat.attributes[ATTR_HVAC_ACTION] == HVACAction.OFF # Simulate pubsub message when mode changes await create_event( @@ -476,8 +469,8 @@ async def test_thermostat_set_hvac_mode( thermostat = hass.states.get("climate.my_thermostat") assert thermostat is not None - assert thermostat.state == HVAC_MODE_HEAT - assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE + assert thermostat.state == HVACMode.HEAT + assert thermostat.attributes[ATTR_HVAC_ACTION] == HVACAction.IDLE # Simulate pubsub message when the thermostat starts heating await create_event( @@ -490,8 +483,8 @@ async def test_thermostat_set_hvac_mode( thermostat = hass.states.get("climate.my_thermostat") assert thermostat is not None - assert thermostat.state == HVAC_MODE_HEAT - assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_HEAT + assert thermostat.state == HVACMode.HEAT + assert thermostat.attributes[ATTR_HVAC_ACTION] == HVACAction.HEATING async def test_thermostat_invalid_hvac_mode( @@ -515,14 +508,14 @@ async def test_thermostat_invalid_hvac_mode( assert len(hass.states.async_all()) == 1 thermostat = hass.states.get("climate.my_thermostat") assert thermostat is not None - assert thermostat.state == HVAC_MODE_OFF - assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF + assert thermostat.state == HVACMode.OFF + assert thermostat.attributes[ATTR_HVAC_ACTION] == HVACAction.OFF with pytest.raises(ValueError): - await common.async_set_hvac_mode(hass, HVAC_MODE_DRY) + await common.async_set_hvac_mode(hass, HVACMode.DRY) await hass.async_block_till_done() - assert thermostat.state == HVAC_MODE_OFF + assert thermostat.state == HVACMode.OFF assert auth.method is None # No communication with API @@ -554,8 +547,8 @@ async def test_thermostat_set_eco_preset( assert len(hass.states.async_all()) == 1 thermostat = hass.states.get("climate.my_thermostat") assert thermostat is not None - assert thermostat.state == HVAC_MODE_OFF - assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF + assert thermostat.state == HVACMode.OFF + assert thermostat.attributes[ATTR_HVAC_ACTION] == HVACAction.OFF assert thermostat.attributes[ATTR_PRESET_MODE] == PRESET_NONE # Turn on eco mode @@ -572,8 +565,8 @@ async def test_thermostat_set_eco_preset( # Local state does not reflect the update thermostat = hass.states.get("climate.my_thermostat") assert thermostat is not None - assert thermostat.state == HVAC_MODE_OFF - assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF + assert thermostat.state == HVACMode.OFF + assert thermostat.attributes[ATTR_HVAC_ACTION] == HVACAction.OFF assert thermostat.attributes[ATTR_PRESET_MODE] == PRESET_NONE # Simulate pubsub message when mode changes @@ -590,8 +583,8 @@ async def test_thermostat_set_eco_preset( thermostat = hass.states.get("climate.my_thermostat") assert thermostat is not None - assert thermostat.state == HVAC_MODE_OFF - assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF + assert thermostat.state == HVACMode.OFF + assert thermostat.attributes[ATTR_HVAC_ACTION] == HVACAction.OFF assert thermostat.attributes[ATTR_PRESET_MODE] == PRESET_ECO # Turn off eco mode @@ -630,7 +623,7 @@ async def test_thermostat_set_cool( assert len(hass.states.async_all()) == 1 thermostat = hass.states.get("climate.my_thermostat") assert thermostat is not None - assert thermostat.state == HVAC_MODE_COOL + assert thermostat.state == HVACMode.COOL await common.async_set_temperature(hass, temperature=24.0) await hass.async_block_till_done() @@ -667,7 +660,7 @@ async def test_thermostat_set_heat( assert len(hass.states.async_all()) == 1 thermostat = hass.states.get("climate.my_thermostat") assert thermostat is not None - assert thermostat.state == HVAC_MODE_HEAT + assert thermostat.state == HVACMode.HEAT await common.async_set_temperature(hass, temperature=20.0) await hass.async_block_till_done() @@ -704,9 +697,9 @@ async def test_thermostat_set_temperature_hvac_mode( assert len(hass.states.async_all()) == 1 thermostat = hass.states.get("climate.my_thermostat") assert thermostat is not None - assert thermostat.state == HVAC_MODE_OFF + assert thermostat.state == HVACMode.OFF - await common.async_set_temperature(hass, temperature=24.0, hvac_mode=HVAC_MODE_COOL) + await common.async_set_temperature(hass, temperature=24.0, hvac_mode=HVACMode.COOL) await hass.async_block_till_done() assert auth.method == "post" @@ -716,7 +709,7 @@ async def test_thermostat_set_temperature_hvac_mode( "params": {"coolCelsius": 24.0}, } - await common.async_set_temperature(hass, temperature=26.0, hvac_mode=HVAC_MODE_HEAT) + await common.async_set_temperature(hass, temperature=26.0, hvac_mode=HVACMode.HEAT) await hass.async_block_till_done() assert auth.method == "post" @@ -727,7 +720,7 @@ async def test_thermostat_set_temperature_hvac_mode( } await common.async_set_temperature( - hass, target_temp_low=20.0, target_temp_high=24.0, hvac_mode=HVAC_MODE_HEAT_COOL + hass, target_temp_low=20.0, target_temp_high=24.0, hvac_mode=HVACMode.HEAT_COOL ) await hass.async_block_till_done() @@ -764,7 +757,7 @@ async def test_thermostat_set_heat_cool( assert len(hass.states.async_all()) == 1 thermostat = hass.states.get("climate.my_thermostat") assert thermostat is not None - assert thermostat.state == HVAC_MODE_HEAT_COOL + assert thermostat.state == HVACMode.HEAT_COOL await common.async_set_temperature( hass, target_temp_low=20.0, target_temp_high=24.0 @@ -806,14 +799,14 @@ async def test_thermostat_fan_off( assert len(hass.states.async_all()) == 1 thermostat = hass.states.get("climate.my_thermostat") assert thermostat is not None - assert thermostat.state == HVAC_MODE_COOL - assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE + assert thermostat.state == HVACMode.COOL + assert thermostat.attributes[ATTR_HVAC_ACTION] == HVACAction.IDLE assert thermostat.attributes[ATTR_CURRENT_TEMPERATURE] == 16.2 assert set(thermostat.attributes[ATTR_HVAC_MODES]) == { - HVAC_MODE_HEAT, - HVAC_MODE_COOL, - HVAC_MODE_HEAT_COOL, - HVAC_MODE_OFF, + HVACMode.HEAT, + HVACMode.COOL, + HVACMode.HEAT_COOL, + HVACMode.OFF, } assert thermostat.attributes[ATTR_FAN_MODE] == FAN_OFF assert thermostat.attributes[ATTR_FAN_MODES] == [FAN_ON, FAN_OFF] @@ -853,14 +846,14 @@ async def test_thermostat_fan_on( assert len(hass.states.async_all()) == 1 thermostat = hass.states.get("climate.my_thermostat") assert thermostat is not None - assert thermostat.state == HVAC_MODE_COOL - assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE + assert thermostat.state == HVACMode.COOL + assert thermostat.attributes[ATTR_HVAC_ACTION] == HVACAction.IDLE assert thermostat.attributes[ATTR_CURRENT_TEMPERATURE] == 16.2 assert set(thermostat.attributes[ATTR_HVAC_MODES]) == { - HVAC_MODE_HEAT, - HVAC_MODE_COOL, - HVAC_MODE_HEAT_COOL, - HVAC_MODE_OFF, + HVACMode.HEAT, + HVACMode.COOL, + HVACMode.HEAT_COOL, + HVACMode.OFF, } assert thermostat.attributes[ATTR_FAN_MODE] == FAN_ON assert thermostat.attributes[ATTR_FAN_MODES] == [FAN_ON, FAN_OFF] @@ -897,13 +890,13 @@ async def test_thermostat_cool_with_fan( assert len(hass.states.async_all()) == 1 thermostat = hass.states.get("climate.my_thermostat") assert thermostat is not None - assert thermostat.state == HVAC_MODE_COOL - assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE + assert thermostat.state == HVACMode.COOL + assert thermostat.attributes[ATTR_HVAC_ACTION] == HVACAction.IDLE assert set(thermostat.attributes[ATTR_HVAC_MODES]) == { - HVAC_MODE_HEAT, - HVAC_MODE_COOL, - HVAC_MODE_HEAT_COOL, - HVAC_MODE_OFF, + HVACMode.HEAT, + HVACMode.COOL, + HVACMode.HEAT_COOL, + HVACMode.OFF, } assert thermostat.attributes[ATTR_FAN_MODE] == FAN_ON assert thermostat.attributes[ATTR_FAN_MODES] == [FAN_ON, FAN_OFF] @@ -941,7 +934,7 @@ async def test_thermostat_set_fan( assert len(hass.states.async_all()) == 1 thermostat = hass.states.get("climate.my_thermostat") assert thermostat is not None - assert thermostat.state == HVAC_MODE_HEAT + assert thermostat.state == HVACMode.HEAT assert thermostat.attributes[ATTR_FAN_MODE] == FAN_ON assert thermostat.attributes[ATTR_FAN_MODES] == [FAN_ON, FAN_OFF] assert thermostat.attributes[ATTR_SUPPORTED_FEATURES] == ( @@ -1003,7 +996,7 @@ async def test_thermostat_set_fan_when_off( assert len(hass.states.async_all()) == 1 thermostat = hass.states.get("climate.my_thermostat") assert thermostat is not None - assert thermostat.state == HVAC_MODE_OFF + assert thermostat.state == HVACMode.OFF assert thermostat.attributes[ATTR_FAN_MODE] == FAN_ON assert thermostat.attributes[ATTR_FAN_MODES] == [FAN_ON, FAN_OFF] assert thermostat.attributes[ATTR_SUPPORTED_FEATURES] == ( @@ -1041,14 +1034,14 @@ async def test_thermostat_fan_empty( assert len(hass.states.async_all()) == 1 thermostat = hass.states.get("climate.my_thermostat") assert thermostat is not None - assert thermostat.state == HVAC_MODE_OFF - assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF + assert thermostat.state == HVACMode.OFF + assert thermostat.attributes[ATTR_HVAC_ACTION] == HVACAction.OFF assert thermostat.attributes[ATTR_CURRENT_TEMPERATURE] == 16.2 assert set(thermostat.attributes[ATTR_HVAC_MODES]) == { - HVAC_MODE_HEAT, - HVAC_MODE_COOL, - HVAC_MODE_HEAT_COOL, - HVAC_MODE_OFF, + HVACMode.HEAT, + HVACMode.COOL, + HVACMode.HEAT_COOL, + HVACMode.OFF, } assert ATTR_FAN_MODE not in thermostat.attributes assert ATTR_FAN_MODES not in thermostat.attributes @@ -1092,14 +1085,14 @@ async def test_thermostat_invalid_fan_mode( assert len(hass.states.async_all()) == 1 thermostat = hass.states.get("climate.my_thermostat") assert thermostat is not None - assert thermostat.state == HVAC_MODE_COOL - assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE + assert thermostat.state == HVACMode.COOL + assert thermostat.attributes[ATTR_HVAC_ACTION] == HVACAction.IDLE assert thermostat.attributes[ATTR_CURRENT_TEMPERATURE] == 16.2 assert set(thermostat.attributes[ATTR_HVAC_MODES]) == { - HVAC_MODE_HEAT, - HVAC_MODE_COOL, - HVAC_MODE_HEAT_COOL, - HVAC_MODE_OFF, + HVACMode.HEAT, + HVACMode.COOL, + HVACMode.HEAT_COOL, + HVACMode.OFF, } assert thermostat.attributes[ATTR_FAN_MODE] == FAN_ON assert thermostat.attributes[ATTR_FAN_MODES] == [FAN_ON, FAN_OFF] @@ -1138,7 +1131,7 @@ async def test_thermostat_target_temp( assert len(hass.states.async_all()) == 1 thermostat = hass.states.get("climate.my_thermostat") assert thermostat is not None - assert thermostat.state == HVAC_MODE_HEAT + assert thermostat.state == HVACMode.HEAT assert thermostat.attributes[ATTR_TEMPERATURE] == 23.0 assert thermostat.attributes[ATTR_TARGET_TEMP_LOW] is None assert thermostat.attributes[ATTR_TARGET_TEMP_HIGH] is None @@ -1159,7 +1152,7 @@ async def test_thermostat_target_temp( thermostat = hass.states.get("climate.my_thermostat") assert thermostat is not None - assert thermostat.state == HVAC_MODE_HEAT_COOL + assert thermostat.state == HVACMode.HEAT_COOL assert thermostat.attributes[ATTR_TARGET_TEMP_LOW] == 22.0 assert thermostat.attributes[ATTR_TARGET_TEMP_HIGH] == 28.0 assert thermostat.attributes[ATTR_TEMPERATURE] is None @@ -1181,8 +1174,8 @@ async def test_thermostat_missing_mode_traits( assert len(hass.states.async_all()) == 1 thermostat = hass.states.get("climate.my_thermostat") assert thermostat is not None - assert thermostat.state == HVAC_MODE_OFF - assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF + assert thermostat.state == HVACMode.OFF + assert thermostat.attributes[ATTR_HVAC_ACTION] == HVACAction.OFF assert thermostat.attributes[ATTR_CURRENT_TEMPERATURE] is None assert set(thermostat.attributes[ATTR_HVAC_MODES]) == set() assert ATTR_TEMPERATURE not in thermostat.attributes @@ -1222,14 +1215,14 @@ async def test_thermostat_missing_temperature_trait( assert len(hass.states.async_all()) == 1 thermostat = hass.states.get("climate.my_thermostat") assert thermostat is not None - assert thermostat.state == HVAC_MODE_HEAT - assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE + assert thermostat.state == HVACMode.HEAT + assert thermostat.attributes[ATTR_HVAC_ACTION] == HVACAction.IDLE assert thermostat.attributes[ATTR_CURRENT_TEMPERATURE] is None assert set(thermostat.attributes[ATTR_HVAC_MODES]) == { - HVAC_MODE_HEAT, - HVAC_MODE_COOL, - HVAC_MODE_HEAT_COOL, - HVAC_MODE_OFF, + HVACMode.HEAT, + HVACMode.COOL, + HVACMode.HEAT_COOL, + HVACMode.OFF, } assert thermostat.attributes[ATTR_TEMPERATURE] is None assert thermostat.attributes[ATTR_TARGET_TEMP_LOW] is None @@ -1260,7 +1253,7 @@ async def test_thermostat_unexpected_hvac_status( assert len(hass.states.async_all()) == 1 thermostat = hass.states.get("climate.my_thermostat") assert thermostat is not None - assert thermostat.state == HVAC_MODE_OFF + assert thermostat.state == HVACMode.OFF assert ATTR_HVAC_ACTION not in thermostat.attributes assert thermostat.attributes[ATTR_CURRENT_TEMPERATURE] is None assert set(thermostat.attributes[ATTR_HVAC_MODES]) == set() @@ -1273,9 +1266,9 @@ async def test_thermostat_unexpected_hvac_status( assert ATTR_FAN_MODES not in thermostat.attributes with pytest.raises(ValueError): - await common.async_set_hvac_mode(hass, HVAC_MODE_DRY) + await common.async_set_hvac_mode(hass, HVACMode.DRY) await hass.async_block_till_done() - assert thermostat.state == HVAC_MODE_OFF + assert thermostat.state == HVACMode.OFF async def test_thermostat_missing_set_point( @@ -1298,14 +1291,14 @@ async def test_thermostat_missing_set_point( assert len(hass.states.async_all()) == 1 thermostat = hass.states.get("climate.my_thermostat") assert thermostat is not None - assert thermostat.state == HVAC_MODE_HEAT_COOL - assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE + assert thermostat.state == HVACMode.HEAT_COOL + assert thermostat.attributes[ATTR_HVAC_ACTION] == HVACAction.IDLE assert thermostat.attributes[ATTR_CURRENT_TEMPERATURE] is None assert set(thermostat.attributes[ATTR_HVAC_MODES]) == { - HVAC_MODE_HEAT, - HVAC_MODE_COOL, - HVAC_MODE_HEAT_COOL, - HVAC_MODE_OFF, + HVACMode.HEAT, + HVACMode.COOL, + HVACMode.HEAT_COOL, + HVACMode.OFF, } assert thermostat.attributes[ATTR_TEMPERATURE] is None assert thermostat.attributes[ATTR_TARGET_TEMP_LOW] is None @@ -1336,14 +1329,14 @@ async def test_thermostat_unexepected_hvac_mode( assert len(hass.states.async_all()) == 1 thermostat = hass.states.get("climate.my_thermostat") assert thermostat is not None - assert thermostat.state == HVAC_MODE_OFF - assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF + assert thermostat.state == HVACMode.OFF + assert thermostat.attributes[ATTR_HVAC_ACTION] == HVACAction.OFF assert thermostat.attributes[ATTR_CURRENT_TEMPERATURE] is None assert set(thermostat.attributes[ATTR_HVAC_MODES]) == { - HVAC_MODE_HEAT, - HVAC_MODE_COOL, - HVAC_MODE_HEAT_COOL, - HVAC_MODE_OFF, + HVACMode.HEAT, + HVACMode.COOL, + HVACMode.HEAT_COOL, + HVACMode.OFF, } assert thermostat.attributes[ATTR_TEMPERATURE] is None assert thermostat.attributes[ATTR_TARGET_TEMP_LOW] is None @@ -1377,7 +1370,7 @@ async def test_thermostat_invalid_set_preset_mode( assert len(hass.states.async_all()) == 1 thermostat = hass.states.get("climate.my_thermostat") assert thermostat is not None - assert thermostat.state == HVAC_MODE_OFF + assert thermostat.state == HVACMode.OFF assert thermostat.attributes[ATTR_PRESET_MODE] == PRESET_NONE assert thermostat.attributes[ATTR_PRESET_MODES] == [PRESET_ECO, PRESET_NONE] @@ -1425,18 +1418,18 @@ async def test_thermostat_hvac_mode_failure( assert len(hass.states.async_all()) == 1 thermostat = hass.states.get("climate.my_thermostat") assert thermostat is not None - assert thermostat.state == HVAC_MODE_COOL - assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE + assert thermostat.state == HVACMode.COOL + assert thermostat.attributes[ATTR_HVAC_ACTION] == HVACAction.IDLE auth.responses = [aiohttp.web.Response(status=HTTPStatus.BAD_REQUEST)] with pytest.raises(HomeAssistantError): - await common.async_set_hvac_mode(hass, HVAC_MODE_HEAT) + await common.async_set_hvac_mode(hass, HVACMode.HEAT) await hass.async_block_till_done() auth.responses = [aiohttp.web.Response(status=HTTPStatus.BAD_REQUEST)] with pytest.raises(HomeAssistantError): await common.async_set_temperature( - hass, hvac_mode=HVAC_MODE_HEAT, temperature=25.0 + hass, hvac_mode=HVACMode.HEAT, temperature=25.0 ) await hass.async_block_till_done() diff --git a/tests/components/prometheus/test_init.py b/tests/components/prometheus/test_init.py index 8fa38664935..1730e6c3f23 100644 --- a/tests/components/prometheus/test_init.py +++ b/tests/components/prometheus/test_init.py @@ -22,14 +22,12 @@ from homeassistant.components import ( sensor, switch, ) -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_CURRENT_TEMPERATURE, ATTR_HUMIDITY, ATTR_HVAC_ACTION, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, - CURRENT_HVAC_COOL, - CURRENT_HVAC_HEAT, ) from homeassistant.components.humidifier import ATTR_AVAILABLE_MODES from homeassistant.components.sensor import SensorDeviceClass @@ -520,7 +518,10 @@ async def test_renaming_entity_name( ATTR_FRIENDLY_NAME: "HeatPump Renamed", } set_state_with_entry( - hass, data["climate_1"], CURRENT_HVAC_HEAT, data["climate_1_attributes"] + hass, + data["climate_1"], + climate.HVACAction.HEATING, + data["climate_1_attributes"], ) await hass.async_block_till_done() @@ -971,9 +972,11 @@ async def climate_fixture(hass, registry): climate_1_attributes = { ATTR_TEMPERATURE: 20, ATTR_CURRENT_TEMPERATURE: 25, - ATTR_HVAC_ACTION: CURRENT_HVAC_HEAT, + ATTR_HVAC_ACTION: climate.HVACAction.HEATING, } - set_state_with_entry(hass, climate_1, CURRENT_HVAC_HEAT, climate_1_attributes) + set_state_with_entry( + hass, climate_1, climate.HVACAction.HEATING, climate_1_attributes + ) data["climate_1"] = climate_1 data["climate_1_attributes"] = climate_1_attributes @@ -990,9 +993,11 @@ async def climate_fixture(hass, registry): ATTR_CURRENT_TEMPERATURE: 22, ATTR_TARGET_TEMP_LOW: 21, ATTR_TARGET_TEMP_HIGH: 24, - ATTR_HVAC_ACTION: CURRENT_HVAC_COOL, + ATTR_HVAC_ACTION: climate.HVACAction.COOLING, } - set_state_with_entry(hass, climate_2, CURRENT_HVAC_HEAT, climate_2_attributes) + set_state_with_entry( + hass, climate_2, climate.HVACAction.HEATING, climate_2_attributes + ) data["climate_2"] = climate_2 data["climate_2_attributes"] = climate_2_attributes From 3f512e38db9ed3e85626190077134f9274282e2a Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Tue, 20 Sep 2022 22:03:10 +0200 Subject: [PATCH 595/955] Fix Sonos cover art when browsing albums (#75105) --- homeassistant/components/sonos/media_browser.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/sonos/media_browser.py b/homeassistant/components/sonos/media_browser.py index 713d48fea55..1f245d23018 100644 --- a/homeassistant/components/sonos/media_browser.py +++ b/homeassistant/components/sonos/media_browser.py @@ -500,11 +500,9 @@ def get_media( if not item_id.startswith("A:ALBUM") and search_type == SONOS_ALBUM: item_id = "A:ALBUMARTIST/" + "/".join(item_id.split("/")[2:]) - for item in media_library.browse_by_idstring( - search_type, - "/".join(item_id.split("/")[:-1]), - full_album_art_uri=True, - max_items=0, - ): - if item.item_id == item_id: - return item + search_term = item_id.split("/")[-1] + matches = media_library.get_music_library_information( + search_type, search_term=search_term, full_album_art_uri=True + ) + if len(matches) > 0: + return matches[0] From dae00c70dec2adabc4d309608ae2a34568967dc6 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 20 Sep 2022 23:43:57 +0200 Subject: [PATCH 596/955] Allow selecting display unit when fetching statistics (#78578) --- homeassistant/components/recorder/core.py | 10 +- .../components/recorder/statistics.py | 227 +++-- homeassistant/components/recorder/tasks.py | 7 +- .../components/recorder/websocket_api.py | 79 +- tests/components/demo/test_init.py | 2 + tests/components/recorder/test_statistics.py | 35 +- .../components/recorder/test_websocket_api.py | 827 +++++++++++++++++- tests/components/sensor/test_recorder.py | 285 +++--- 8 files changed, 1231 insertions(+), 241 deletions(-) diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 88b63e93733..8277828abbc 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -480,10 +480,16 @@ class Recorder(threading.Thread): @callback def async_adjust_statistics( - self, statistic_id: str, start_time: datetime, sum_adjustment: float + self, + statistic_id: str, + start_time: datetime, + sum_adjustment: float, + display_unit: str, ) -> None: """Adjust statistics.""" - self.queue_task(AdjustStatisticsTask(statistic_id, start_time, sum_adjustment)) + self.queue_task( + AdjustStatisticsTask(statistic_id, start_time, sum_adjustment, display_unit) + ) @callback def async_clear_statistics(self, statistic_ids: list[str]) -> None: diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 8585ca37fac..f720a2cd62e 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -6,6 +6,7 @@ from collections.abc import Callable, Iterable import contextlib import dataclasses from datetime import datetime, timedelta +from functools import partial from itertools import chain, groupby import json import logging @@ -25,11 +26,12 @@ import voluptuous as vol from homeassistant.const import ( ENERGY_KILO_WATT_HOUR, + ENERGY_MEGA_WATT_HOUR, + ENERGY_WATT_HOUR, POWER_KILO_WATT, POWER_WATT, PRESSURE_PA, TEMP_CELSIUS, - VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS, ) from homeassistant.core import Event, HomeAssistant, callback, valid_entity_id @@ -41,7 +43,6 @@ from homeassistant.helpers.typing import UNDEFINED, UndefinedType import homeassistant.util.dt as dt_util import homeassistant.util.pressure as pressure_util import homeassistant.util.temperature as temperature_util -from homeassistant.util.unit_system import UnitSystem import homeassistant.util.volume as volume_util from .const import DOMAIN, MAX_ROWS_TO_PURGE, SupportedDialect @@ -131,65 +132,138 @@ QUERY_STATISTIC_META_ID = [ ] -def _convert_power(value: float | None, state_unit: str, _: UnitSystem) -> float | None: - """Convert power in W to to_unit.""" +def _convert_energy_from_kwh(to_unit: str, value: float | None) -> float | None: + """Convert energy in kWh to to_unit.""" if value is None: return None - if state_unit == POWER_KILO_WATT: + if to_unit == ENERGY_MEGA_WATT_HOUR: + return value / 1000 + if to_unit == ENERGY_WATT_HOUR: + return value * 1000 + return value + + +def _convert_energy_to_kwh(from_unit: str, value: float) -> float: + """Convert energy in from_unit to kWh.""" + if from_unit == ENERGY_MEGA_WATT_HOUR: + return value * 1000 + if from_unit == ENERGY_WATT_HOUR: return value / 1000 return value -def _convert_pressure( - value: float | None, state_unit: str, _: UnitSystem -) -> float | None: +def _convert_power_from_w(to_unit: str, value: float | None) -> float | None: + """Convert power in W to to_unit.""" + if value is None: + return None + if to_unit == POWER_KILO_WATT: + return value / 1000 + return value + + +def _convert_pressure_from_pa(to_unit: str, value: float | None) -> float | None: """Convert pressure in Pa to to_unit.""" if value is None: return None - return pressure_util.convert(value, PRESSURE_PA, state_unit) + return pressure_util.convert(value, PRESSURE_PA, to_unit) -def _convert_temperature( - value: float | None, state_unit: str, _: UnitSystem -) -> float | None: +def _convert_temperature_from_c(to_unit: str, value: float | None) -> float | None: """Convert temperature in °C to to_unit.""" if value is None: return None - return temperature_util.convert(value, TEMP_CELSIUS, state_unit) + return temperature_util.convert(value, TEMP_CELSIUS, to_unit) -def _convert_volume(value: float | None, _: str, units: UnitSystem) -> float | None: - """Convert volume in m³ to ft³ or m³.""" +def _convert_volume_from_m3(to_unit: str, value: float | None) -> float | None: + """Convert volume in m³ to to_unit.""" if value is None: return None - return volume_util.convert(value, VOLUME_CUBIC_METERS, _volume_unit(units)) + return volume_util.convert(value, VOLUME_CUBIC_METERS, to_unit) -# Convert power, pressure, temperature and volume statistics from the normalized unit -# used for statistics to the unit configured by the user -STATISTIC_UNIT_TO_DISPLAY_UNIT_CONVERSIONS: dict[ - str, Callable[[float | None, str, UnitSystem], float | None] -] = { - POWER_WATT: _convert_power, - PRESSURE_PA: _convert_pressure, - TEMP_CELSIUS: _convert_temperature, - VOLUME_CUBIC_METERS: _convert_volume, +def _convert_volume_to_m3(from_unit: str, value: float) -> float: + """Convert volume in from_unit to m³.""" + return volume_util.convert(value, from_unit, VOLUME_CUBIC_METERS) + + +STATISTIC_UNIT_TO_UNIT_CLASS: dict[str | None, str] = { + ENERGY_KILO_WATT_HOUR: "energy", + POWER_WATT: "power", + PRESSURE_PA: "pressure", + TEMP_CELSIUS: "temperature", + VOLUME_CUBIC_METERS: "volume", } -# Convert volume statistics from the display unit configured by the user -# to the normalized unit used for statistics -# This is used to support adjusting statistics in the display unit -DISPLAY_UNIT_TO_STATISTIC_UNIT_CONVERSIONS: dict[ - str, Callable[[float, UnitSystem], float] + +# Convert energy power, pressure, temperature and volume statistics from the +# normalized unit used for statistics to the unit configured by the user +STATISTIC_UNIT_TO_DISPLAY_UNIT_FUNCTIONS: dict[ + str, Callable[[str, float | None], float | None] ] = { - VOLUME_CUBIC_FEET: lambda x, units: volume_util.convert( - x, _volume_unit(units), VOLUME_CUBIC_METERS - ), + ENERGY_KILO_WATT_HOUR: _convert_energy_from_kwh, + POWER_WATT: _convert_power_from_w, + PRESSURE_PA: _convert_pressure_from_pa, + TEMP_CELSIUS: _convert_temperature_from_c, + VOLUME_CUBIC_METERS: _convert_volume_from_m3, +} + +# Convert energy and volume statistics from the display unit configured by the user +# to the normalized unit used for statistics. +# This is used to support adjusting statistics in the display unit +DISPLAY_UNIT_TO_STATISTIC_UNIT_FUNCTIONS: dict[str, Callable[[str, float], float]] = { + ENERGY_KILO_WATT_HOUR: _convert_energy_to_kwh, + VOLUME_CUBIC_METERS: _convert_volume_to_m3, } _LOGGER = logging.getLogger(__name__) +def _get_statistic_to_display_unit_converter( + statistic_unit: str | None, + state_unit: str | None, + requested_units: dict[str, str] | None, +) -> Callable[[float | None], float | None]: + """Prepare a converter from the normalized statistics unit to display unit.""" + + def no_conversion(val: float | None) -> float | None: + """Return val.""" + return val + + if statistic_unit is None: + return no_conversion + + if ( + convert_fn := STATISTIC_UNIT_TO_DISPLAY_UNIT_FUNCTIONS.get(statistic_unit) + ) is None: + return no_conversion + + unit_class = STATISTIC_UNIT_TO_UNIT_CLASS[statistic_unit] + display_unit = requested_units.get(unit_class) if requested_units else state_unit + return partial(convert_fn, display_unit) + + +def _get_display_to_statistic_unit_converter( + display_unit: str | None, + statistic_unit: str | None, +) -> Callable[[float], float]: + """Prepare a converter from the display unit to the normalized statistics unit.""" + + def no_conversion(val: float) -> float: + """Return val.""" + return val + + if statistic_unit is None: + return no_conversion + + if ( + convert_fn := DISPLAY_UNIT_TO_STATISTIC_UNIT_FUNCTIONS.get(statistic_unit) + ) is None: + return no_conversion + + return partial(convert_fn, display_unit) + + @dataclasses.dataclass class PlatformCompiledStatistics: """Compiled Statistics from a platform.""" @@ -802,28 +876,6 @@ def get_metadata( ) -def _volume_unit(units: UnitSystem) -> str: - """Return the preferred volume unit according to unit system.""" - if units.is_metric: - return VOLUME_CUBIC_METERS - return VOLUME_CUBIC_FEET - - -def _configured_unit( - unit: str | None, state_unit: str | None, units: UnitSystem -) -> str | None: - """Return the pressure and temperature units configured by the user. - - Energy and volume is normalized for the energy dashboard. - For other units, display in the unit of the source. - """ - if unit == ENERGY_KILO_WATT_HOUR: - return ENERGY_KILO_WATT_HOUR - if unit == VOLUME_CUBIC_METERS: - return _volume_unit(units) - return state_unit - - def clear_statistics(instance: Recorder, statistic_ids: list[str]) -> None: """Clear statistics for a list of statistic_ids.""" with session_scope(session=instance.get_session()) as session: @@ -868,11 +920,6 @@ def list_statistic_ids( """ result = {} - def _display_unit( - hass: HomeAssistant, statistic_unit: str | None, state_unit: str | None - ) -> str | None: - return _configured_unit(statistic_unit, state_unit, hass.config.units) - # Query the database with session_scope(hass=hass) as session: metadata = get_metadata_with_session( @@ -881,12 +928,13 @@ def list_statistic_ids( result = { meta["statistic_id"]: { + "display_unit_of_measurement": meta["state_unit_of_measurement"], "has_mean": meta["has_mean"], "has_sum": meta["has_sum"], "name": meta["name"], "source": meta["source"], - "display_unit_of_measurement": _display_unit( - hass, meta["unit_of_measurement"], meta["state_unit_of_measurement"] + "unit_class": STATISTIC_UNIT_TO_UNIT_CLASS.get( + meta["unit_of_measurement"] ), "unit_of_measurement": meta["unit_of_measurement"], } @@ -909,8 +957,9 @@ def list_statistic_ids( "has_sum": meta["has_sum"], "name": meta["name"], "source": meta["source"], - "display_unit_of_measurement": _display_unit( - hass, meta["unit_of_measurement"], meta["state_unit_of_measurement"] + "display_unit_of_measurement": meta["state_unit_of_measurement"], + "unit_class": STATISTIC_UNIT_TO_UNIT_CLASS.get( + meta["unit_of_measurement"] ), "unit_of_measurement": meta["unit_of_measurement"], } @@ -925,6 +974,7 @@ def list_statistic_ids( "source": info["source"], "display_unit_of_measurement": info["display_unit_of_measurement"], "statistics_unit_of_measurement": info["unit_of_measurement"], + "unit_class": info["unit_class"], } for _id, info in result.items() ] @@ -1079,6 +1129,7 @@ def statistics_during_period( statistic_ids: list[str] | None = None, period: Literal["5minute", "day", "hour", "month"] = "hour", 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. @@ -1120,10 +1171,20 @@ def statistics_during_period( table, start_time, start_time_as_datetime, + units, ) result = _sorted_statistics_to_dict( - hass, session, stats, statistic_ids, metadata, True, table, start_time, True + hass, + session, + stats, + statistic_ids, + metadata, + True, + table, + start_time, + True, + units, ) if period == "day": @@ -1192,6 +1253,8 @@ def _get_last_statistics( convert_units, table, None, + False, + None, ) @@ -1276,6 +1339,8 @@ def get_latest_short_term_statistics( False, StatisticsShortTerm, None, + False, + None, ) @@ -1320,18 +1385,18 @@ def _sorted_statistics_to_dict( convert_units: bool, table: type[Statistics | StatisticsShortTerm], start_time: datetime | None, - start_time_as_datetime: bool = False, + start_time_as_datetime: bool, + units: dict[str, str] | None, ) -> dict[str, list[dict]]: """Convert SQL results into JSON friendly data structure.""" result: dict = defaultdict(list) - units = hass.config.units metadata = dict(_metadata.values()) need_stat_at_start_time: set[int] = set() stats_at_start_time = {} - def no_conversion(val: Any, _unit: str | None, _units: Any) -> float | None: - """Return x.""" - return val # type: ignore[no-any-return] + def no_conversion(val: float | None) -> float | None: + """Return val.""" + return val # Set all statistic IDs to empty lists in result set to maintain the order if statistic_ids is not None: @@ -1357,11 +1422,8 @@ def _sorted_statistics_to_dict( unit = metadata[meta_id]["unit_of_measurement"] state_unit = metadata[meta_id]["state_unit_of_measurement"] statistic_id = metadata[meta_id]["statistic_id"] - convert: Callable[[Any, Any, Any], float | None] if unit is not None and convert_units: - convert = STATISTIC_UNIT_TO_DISPLAY_UNIT_CONVERSIONS.get( - unit, no_conversion - ) + convert = _get_statistic_to_display_unit_converter(unit, state_unit, units) else: convert = no_conversion ent_results = result[meta_id] @@ -1373,14 +1435,14 @@ def _sorted_statistics_to_dict( "statistic_id": statistic_id, "start": start if start_time_as_datetime else start.isoformat(), "end": end.isoformat(), - "mean": convert(db_state.mean, state_unit, units), - "min": convert(db_state.min, state_unit, units), - "max": convert(db_state.max, state_unit, units), + "mean": convert(db_state.mean), + "min": convert(db_state.min), + "max": convert(db_state.max), "last_reset": process_timestamp_to_utc_isoformat( db_state.last_reset ), - "state": convert(db_state.state, state_unit, units), - "sum": convert(db_state.sum, state_unit, units), + "state": convert(db_state.state), + "sum": convert(db_state.sum), } ) @@ -1556,6 +1618,7 @@ def adjust_statistics( statistic_id: str, start_time: datetime, sum_adjustment: float, + display_unit: str, ) -> bool: """Process an add_statistics job.""" @@ -1566,11 +1629,9 @@ def adjust_statistics( if statistic_id not in metadata: return True - units = instance.hass.config.units statistic_unit = metadata[statistic_id][1]["unit_of_measurement"] - display_unit = _configured_unit(statistic_unit, None, units) - convert = DISPLAY_UNIT_TO_STATISTIC_UNIT_CONVERSIONS.get(display_unit, lambda x, units: x) # type: ignore[arg-type] - sum_adjustment = convert(sum_adjustment, units) + convert = _get_display_to_statistic_unit_converter(display_unit, statistic_unit) + sum_adjustment = convert(sum_adjustment) _adjust_sum_statistics( session, diff --git a/homeassistant/components/recorder/tasks.py b/homeassistant/components/recorder/tasks.py index cdb97d9d67c..1ab84a1ce5a 100644 --- a/homeassistant/components/recorder/tasks.py +++ b/homeassistant/components/recorder/tasks.py @@ -145,6 +145,7 @@ class AdjustStatisticsTask(RecorderTask): statistic_id: str start_time: datetime sum_adjustment: float + display_unit: str def run(self, instance: Recorder) -> None: """Run statistics task.""" @@ -153,12 +154,16 @@ class AdjustStatisticsTask(RecorderTask): self.statistic_id, self.start_time, self.sum_adjustment, + self.display_unit, ): return # Schedule a new adjust statistics task if this one didn't finish instance.queue_task( AdjustStatisticsTask( - self.statistic_id, self.start_time, self.sum_adjustment + self.statistic_id, + self.start_time, + self.sum_adjustment, + self.display_unit, ) ) diff --git a/homeassistant/components/recorder/websocket_api.py b/homeassistant/components/recorder/websocket_api.py index c625620f4c0..a61d1e8d673 100644 --- a/homeassistant/components/recorder/websocket_api.py +++ b/homeassistant/components/recorder/websocket_api.py @@ -9,10 +9,21 @@ import voluptuous as vol from homeassistant.components import websocket_api from homeassistant.components.websocket_api import messages +from homeassistant.const import ( + ENERGY_KILO_WATT_HOUR, + ENERGY_MEGA_WATT_HOUR, + ENERGY_WATT_HOUR, + POWER_KILO_WATT, + POWER_WATT, + VOLUME_CUBIC_FEET, + VOLUME_CUBIC_METERS, +) from homeassistant.core import HomeAssistant, callback, valid_entity_id from homeassistant.helpers import config_validation as cv from homeassistant.helpers.json import JSON_DUMP from homeassistant.util import dt as dt_util +import homeassistant.util.pressure as pressure_util +import homeassistant.util.temperature as temperature_util from .const import MAX_QUEUE_BACKLOG from .statistics import ( @@ -47,15 +58,18 @@ def _ws_get_statistics_during_period( hass: HomeAssistant, msg_id: int, start_time: dt, - end_time: dt | None = None, - statistic_ids: list[str] | None = None, - period: Literal["5minute", "day", "hour", "month"] = "hour", + end_time: dt | None, + statistic_ids: list[str] | None, + period: Literal["5minute", "day", "hour", "month"], + units: dict[str, str], ) -> str: """Fetch statistics and convert them to json in the executor.""" return JSON_DUMP( messages.result_message( msg_id, - statistics_during_period(hass, start_time, end_time, statistic_ids, period), + statistics_during_period( + hass, start_time, end_time, statistic_ids, period, units=units + ), ) ) @@ -91,6 +105,7 @@ async def ws_handle_get_statistics_during_period( end_time, msg.get("statistic_ids"), msg.get("period"), + msg.get("units"), ) ) @@ -102,6 +117,17 @@ async def ws_handle_get_statistics_during_period( vol.Optional("end_time"): str, vol.Optional("statistic_ids"): [str], vol.Required("period"): vol.Any("5minute", "hour", "day", "month"), + vol.Optional("units"): vol.Schema( + { + vol.Optional("energy"): vol.Any( + ENERGY_WATT_HOUR, ENERGY_KILO_WATT_HOUR, ENERGY_MEGA_WATT_HOUR + ), + vol.Optional("power"): vol.Any(POWER_WATT, POWER_KILO_WATT), + vol.Optional("pressure"): vol.In(pressure_util.VALID_UNITS), + vol.Optional("temperature"): vol.In(temperature_util.VALID_UNITS), + vol.Optional("volume"): vol.Any(VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS), + } + ), } ) @websocket_api.async_response @@ -236,13 +262,18 @@ def ws_update_statistics_metadata( vol.Required("statistic_id"): str, vol.Required("start_time"): str, vol.Required("adjustment"): vol.Any(float, int), + vol.Required("display_unit"): vol.Any(str, None), } ) -@callback -def ws_adjust_sum_statistics( +@websocket_api.async_response +async def ws_adjust_sum_statistics( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict ) -> None: - """Adjust sum statistics.""" + """Adjust sum statistics. + + If the statistics is stored as kWh, it's allowed to make an adjustment in Wh or MWh + If the statistics is stored as m³, it's allowed to make an adjustment in ft³ + """ start_time_str = msg["start_time"] if start_time := dt_util.parse_datetime(start_time_str): @@ -251,8 +282,38 @@ def ws_adjust_sum_statistics( connection.send_error(msg["id"], "invalid_start_time", "Invalid start time") return + instance = get_instance(hass) + metadatas = await instance.async_add_executor_job( + list_statistic_ids, hass, (msg["statistic_id"],) + ) + if not metadatas: + connection.send_error(msg["id"], "unknown_statistic_id", "Unknown statistic ID") + return + metadata = metadatas[0] + + def valid_units(statistics_unit: str | None, display_unit: str | None) -> bool: + if statistics_unit == display_unit: + return True + if statistics_unit == ENERGY_KILO_WATT_HOUR and display_unit in ( + ENERGY_MEGA_WATT_HOUR, + ENERGY_WATT_HOUR, + ): + return True + if statistics_unit == VOLUME_CUBIC_METERS and display_unit == VOLUME_CUBIC_FEET: + return True + return False + + stat_unit = metadata["statistics_unit_of_measurement"] + if not valid_units(stat_unit, msg["display_unit"]): + connection.send_error( + msg["id"], + "invalid_units", + f"Can't convert {stat_unit} to {msg['display_unit']}", + ) + return + get_instance(hass).async_adjust_statistics( - msg["statistic_id"], start_time, msg["adjustment"] + msg["statistic_id"], start_time, msg["adjustment"], msg["display_unit"] ) connection.send_result(msg["id"]) @@ -286,7 +347,7 @@ def ws_adjust_sum_statistics( def ws_import_statistics( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict ) -> None: - """Adjust sum statistics.""" + """Import statistics.""" metadata = msg["metadata"] stats = msg["stats"] metadata["state_unit_of_measurement"] = metadata["unit_of_measurement"] diff --git a/tests/components/demo/test_init.py b/tests/components/demo/test_init.py index f7fec9629ac..79da28d8abd 100644 --- a/tests/components/demo/test_init.py +++ b/tests/components/demo/test_init.py @@ -70,6 +70,7 @@ async def test_demo_statistics(hass, recorder_mock): "source": "demo", "statistic_id": "demo:temperature_outdoor", "statistics_unit_of_measurement": "°C", + "unit_class": "temperature", } in statistic_ids assert { "display_unit_of_measurement": "kWh", @@ -79,6 +80,7 @@ async def test_demo_statistics(hass, recorder_mock): "source": "demo", "statistic_id": "demo:energy_consumption_kwh", "statistics_unit_of_measurement": "kWh", + "unit_class": "energy", } in statistic_ids diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index beb7cef2fb9..f2de32a443c 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -532,6 +532,7 @@ async def test_import_statistics( "name": "Total imported energy", "source": source, "statistics_unit_of_measurement": "kWh", + "unit_class": "energy", } ] metadata = get_metadata(hass, statistic_ids=(statistic_id,)) @@ -603,7 +604,7 @@ async def test_import_statistics( ] } - # Update the previously inserted statistics + rename and change unit + # Update the previously inserted statistics + rename and change display unit external_statistics = { "start": period1, "max": 1, @@ -620,13 +621,14 @@ async def test_import_statistics( statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ { - "display_unit_of_measurement": "kWh", + "display_unit_of_measurement": "MWh", "has_mean": False, "has_sum": True, "statistic_id": statistic_id, "name": "Total imported energy renamed", "source": source, "statistics_unit_of_measurement": "kWh", + "unit_class": "energy", } ] metadata = get_metadata(hass, statistic_ids=(statistic_id,)) @@ -651,12 +653,12 @@ async def test_import_statistics( "statistic_id": statistic_id, "start": period1.isoformat(), "end": (period1 + timedelta(hours=1)).isoformat(), - "max": approx(1.0), - "mean": approx(2.0), - "min": approx(3.0), + "max": approx(1.0 / 1000), + "mean": approx(2.0 / 1000), + "min": approx(3.0 / 1000), "last_reset": last_reset_utc_str, - "state": approx(4.0), - "sum": approx(5.0), + "state": approx(4.0 / 1000), + "sum": approx(5.0 / 1000), }, { "statistic_id": statistic_id, @@ -666,8 +668,8 @@ async def test_import_statistics( "mean": None, "min": None, "last_reset": last_reset_utc_str, - "state": approx(1.0), - "sum": approx(3.0), + "state": approx(1.0 / 1000), + "sum": approx(3.0 / 1000), }, ] } @@ -680,6 +682,7 @@ async def test_import_statistics( "statistic_id": statistic_id, "start_time": period2.isoformat(), "adjustment": 1000.0, + "display_unit": "MWh", } ) response = await client.receive_json() @@ -693,12 +696,12 @@ async def test_import_statistics( "statistic_id": statistic_id, "start": period1.isoformat(), "end": (period1 + timedelta(hours=1)).isoformat(), - "max": approx(1.0), - "mean": approx(2.0), - "min": approx(3.0), + "max": approx(1.0 / 1000), + "mean": approx(2.0 / 1000), + "min": approx(3.0 / 1000), "last_reset": last_reset_utc_str, - "state": approx(4.0), - "sum": approx(5.0), + "state": approx(4.0 / 1000), + "sum": approx(5.0 / 1000), }, { "statistic_id": statistic_id, @@ -708,8 +711,8 @@ async def test_import_statistics( "mean": None, "min": None, "last_reset": last_reset_utc_str, - "state": approx(1.0), - "sum": approx(1003.0), + "state": approx(1.0 / 1000), + "sum": approx(1000 + 3.0 / 1000), }, ] } diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index 1e0633248bd..6f8b2be7d58 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -50,12 +50,12 @@ TEMPERATURE_SENSOR_F_ATTRIBUTES = { "state_class": "measurement", "unit_of_measurement": "°F", } -ENERGY_SENSOR_ATTRIBUTES = { +ENERGY_SENSOR_KWH_ATTRIBUTES = { "device_class": "energy", "state_class": "total", "unit_of_measurement": "kWh", } -GAS_SENSOR_ATTRIBUTES = { +GAS_SENSOR_M3_ATTRIBUTES = { "device_class": "gas", "state_class": "total", "unit_of_measurement": "m³", @@ -133,6 +133,241 @@ async def test_statistics_during_period( } +@pytest.mark.parametrize( + "attributes, state, value, custom_units, converted_value", + [ + (POWER_SENSOR_KW_ATTRIBUTES, 10, 10, {"power": "W"}, 10000), + (POWER_SENSOR_KW_ATTRIBUTES, 10, 10, {"power": "kW"}, 10), + (PRESSURE_SENSOR_HPA_ATTRIBUTES, 10, 10, {"pressure": "Pa"}, 1000), + (PRESSURE_SENSOR_HPA_ATTRIBUTES, 10, 10, {"pressure": "hPa"}, 10), + (PRESSURE_SENSOR_HPA_ATTRIBUTES, 10, 10, {"pressure": "psi"}, 1000 / 6894.757), + (TEMPERATURE_SENSOR_C_ATTRIBUTES, 10, 10, {"temperature": "°C"}, 10), + (TEMPERATURE_SENSOR_C_ATTRIBUTES, 10, 10, {"temperature": "°F"}, 50), + (TEMPERATURE_SENSOR_C_ATTRIBUTES, 10, 10, {"temperature": "K"}, 283.15), + ], +) +async def test_statistics_during_period_unit_conversion( + hass, + hass_ws_client, + recorder_mock, + attributes, + state, + value, + custom_units, + converted_value, +): + """Test statistics_during_period.""" + now = dt_util.utcnow() + + await async_setup_component(hass, "sensor", {}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.test", state, attributes=attributes) + await async_wait_recording_done(hass) + + do_adhoc_statistics(hass, start=now) + await async_wait_recording_done(hass) + + client = await hass_ws_client() + + # Query in state unit + await client.send_json( + { + "id": 1, + "type": "recorder/statistics_during_period", + "start_time": now.isoformat(), + "statistic_ids": ["sensor.test"], + "period": "5minute", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + "sensor.test": [ + { + "statistic_id": "sensor.test", + "start": now.isoformat(), + "end": (now + timedelta(minutes=5)).isoformat(), + "mean": approx(value), + "min": approx(value), + "max": approx(value), + "last_reset": None, + "state": None, + "sum": None, + } + ] + } + + # Query in custom unit + await client.send_json( + { + "id": 2, + "type": "recorder/statistics_during_period", + "start_time": now.isoformat(), + "statistic_ids": ["sensor.test"], + "period": "5minute", + "units": custom_units, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + "sensor.test": [ + { + "statistic_id": "sensor.test", + "start": now.isoformat(), + "end": (now + timedelta(minutes=5)).isoformat(), + "mean": approx(converted_value), + "min": approx(converted_value), + "max": approx(converted_value), + "last_reset": None, + "state": None, + "sum": None, + } + ] + } + + +@pytest.mark.parametrize( + "attributes, state, value, custom_units, converted_value", + [ + (ENERGY_SENSOR_KWH_ATTRIBUTES, 10, 10, {"energy": "kWh"}, 10), + (ENERGY_SENSOR_KWH_ATTRIBUTES, 10, 10, {"energy": "MWh"}, 0.010), + (ENERGY_SENSOR_KWH_ATTRIBUTES, 10, 10, {"energy": "Wh"}, 10000), + (GAS_SENSOR_M3_ATTRIBUTES, 10, 10, {"volume": "m³"}, 10), + (GAS_SENSOR_M3_ATTRIBUTES, 10, 10, {"volume": "ft³"}, 353.147), + ], +) +async def test_sum_statistics_during_period_unit_conversion( + hass, + hass_ws_client, + recorder_mock, + attributes, + state, + value, + custom_units, + converted_value, +): + """Test statistics_during_period.""" + now = dt_util.utcnow() + + await async_setup_component(hass, "sensor", {}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.test", 0, attributes=attributes) + hass.states.async_set("sensor.test", state, attributes=attributes) + await async_wait_recording_done(hass) + + do_adhoc_statistics(hass, start=now) + await async_wait_recording_done(hass) + + client = await hass_ws_client() + + # Query in state unit + await client.send_json( + { + "id": 1, + "type": "recorder/statistics_during_period", + "start_time": now.isoformat(), + "statistic_ids": ["sensor.test"], + "period": "5minute", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + "sensor.test": [ + { + "statistic_id": "sensor.test", + "start": now.isoformat(), + "end": (now + timedelta(minutes=5)).isoformat(), + "mean": None, + "min": None, + "max": None, + "last_reset": None, + "state": approx(value), + "sum": approx(value), + } + ] + } + + # Query in custom unit + await client.send_json( + { + "id": 2, + "type": "recorder/statistics_during_period", + "start_time": now.isoformat(), + "statistic_ids": ["sensor.test"], + "period": "5minute", + "units": custom_units, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + "sensor.test": [ + { + "statistic_id": "sensor.test", + "start": now.isoformat(), + "end": (now + timedelta(minutes=5)).isoformat(), + "mean": None, + "min": None, + "max": None, + "last_reset": None, + "state": approx(converted_value), + "sum": approx(converted_value), + } + ] + } + + +@pytest.mark.parametrize( + "custom_units", + [ + {"energy": "W"}, + {"power": "Pa"}, + {"pressure": "K"}, + {"temperature": "m³"}, + {"volume": "kWh"}, + ], +) +async def test_statistics_during_period_invalid_unit_conversion( + hass, hass_ws_client, recorder_mock, custom_units +): + """Test statistics_during_period.""" + now = dt_util.utcnow() + + await async_setup_component(hass, "sensor", {}) + await async_recorder_block_till_done(hass) + + client = await hass_ws_client() + + # Query in state unit + await client.send_json( + { + "id": 1, + "type": "recorder/statistics_during_period", + "start_time": now.isoformat(), + "period": "5minute", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == {} + + # Query in custom unit + await client.send_json( + { + "id": 2, + "type": "recorder/statistics_during_period", + "start_time": now.isoformat(), + "period": "5minute", + "units": custom_units, + } + ) + response = await client.receive_json() + assert not response["success"] + assert response["error"]["code"] == "invalid_format" + + @pytest.mark.parametrize( "units, attributes, state, value", [ @@ -307,16 +542,16 @@ async def test_statistics_during_period_bad_end_time( @pytest.mark.parametrize( - "units, attributes, display_unit, statistics_unit", + "units, attributes, display_unit, statistics_unit, unit_class", [ - (IMPERIAL_SYSTEM, POWER_SENSOR_KW_ATTRIBUTES, "kW", "W"), - (METRIC_SYSTEM, POWER_SENSOR_KW_ATTRIBUTES, "kW", "W"), - (IMPERIAL_SYSTEM, TEMPERATURE_SENSOR_C_ATTRIBUTES, "°C", "°C"), - (METRIC_SYSTEM, TEMPERATURE_SENSOR_C_ATTRIBUTES, "°C", "°C"), - (IMPERIAL_SYSTEM, TEMPERATURE_SENSOR_F_ATTRIBUTES, "°F", "°C"), - (METRIC_SYSTEM, TEMPERATURE_SENSOR_F_ATTRIBUTES, "°F", "°C"), - (IMPERIAL_SYSTEM, PRESSURE_SENSOR_HPA_ATTRIBUTES, "hPa", "Pa"), - (METRIC_SYSTEM, PRESSURE_SENSOR_HPA_ATTRIBUTES, "hPa", "Pa"), + (IMPERIAL_SYSTEM, POWER_SENSOR_KW_ATTRIBUTES, "kW", "W", "power"), + (METRIC_SYSTEM, POWER_SENSOR_KW_ATTRIBUTES, "kW", "W", "power"), + (IMPERIAL_SYSTEM, TEMPERATURE_SENSOR_C_ATTRIBUTES, "°C", "°C", "temperature"), + (METRIC_SYSTEM, TEMPERATURE_SENSOR_C_ATTRIBUTES, "°C", "°C", "temperature"), + (IMPERIAL_SYSTEM, TEMPERATURE_SENSOR_F_ATTRIBUTES, "°F", "°C", "temperature"), + (METRIC_SYSTEM, TEMPERATURE_SENSOR_F_ATTRIBUTES, "°F", "°C", "temperature"), + (IMPERIAL_SYSTEM, PRESSURE_SENSOR_HPA_ATTRIBUTES, "hPa", "Pa", "pressure"), + (METRIC_SYSTEM, PRESSURE_SENSOR_HPA_ATTRIBUTES, "hPa", "Pa", "pressure"), ], ) async def test_list_statistic_ids( @@ -327,6 +562,7 @@ async def test_list_statistic_ids( attributes, display_unit, statistics_unit, + unit_class, ): """Test list_statistic_ids.""" now = dt_util.utcnow() @@ -356,6 +592,7 @@ async def test_list_statistic_ids( "source": "recorder", "display_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, + "unit_class": unit_class, } ] @@ -377,6 +614,7 @@ async def test_list_statistic_ids( "source": "recorder", "display_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, + "unit_class": unit_class, } ] @@ -400,6 +638,7 @@ async def test_list_statistic_ids( "source": "recorder", "display_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, + "unit_class": unit_class, } ] @@ -590,6 +829,7 @@ async def test_update_statistics_metadata( "name": None, "source": "recorder", "statistics_unit_of_measurement": "W", + "unit_class": "power", } ] @@ -617,6 +857,7 @@ async def test_update_statistics_metadata( "name": None, "source": "recorder", "statistics_unit_of_measurement": new_unit, + "unit_class": None, } ] @@ -802,14 +1043,14 @@ async def test_backup_end_without_start( @pytest.mark.parametrize( - "units, attributes, unit", + "units, attributes, unit, unit_class", [ - (METRIC_SYSTEM, GAS_SENSOR_ATTRIBUTES, "m³"), - (METRIC_SYSTEM, ENERGY_SENSOR_ATTRIBUTES, "kWh"), + (METRIC_SYSTEM, GAS_SENSOR_M3_ATTRIBUTES, "m³", "volume"), + (METRIC_SYSTEM, ENERGY_SENSOR_KWH_ATTRIBUTES, "kWh", "energy"), ], ) async def test_get_statistics_metadata( - hass, hass_ws_client, recorder_mock, units, attributes, unit + hass, hass_ws_client, recorder_mock, units, attributes, unit, unit_class ): """Test get_statistics_metadata.""" now = dt_util.utcnow() @@ -891,6 +1132,7 @@ async def test_get_statistics_metadata( "name": None, "source": "recorder", "statistics_unit_of_measurement": unit, + "unit_class": unit_class, } ] @@ -918,6 +1160,7 @@ async def test_get_statistics_metadata( "name": None, "source": "recorder", "statistics_unit_of_measurement": unit, + "unit_class": unit_class, } ] @@ -1014,6 +1257,7 @@ async def test_import_statistics( "name": "Total imported energy", "source": source, "statistics_unit_of_measurement": "kWh", + "unit_class": "energy", } ] metadata = get_metadata(hass, statistic_ids=(statistic_id,)) @@ -1149,6 +1393,119 @@ async def test_import_statistics( ] } + +@pytest.mark.parametrize( + "source, statistic_id", + ( + ("test", "test:total_energy_import"), + ("recorder", "sensor.total_energy_import"), + ), +) +async def test_adjust_sum_statistics_energy( + hass, hass_ws_client, recorder_mock, caplog, source, statistic_id +): + """Test adjusting statistics.""" + client = await hass_ws_client() + + assert "Compiling statistics for" not in caplog.text + assert "Statistics already compiled" not in caplog.text + + zero = dt_util.utcnow() + 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 = { + "start": period1.isoformat(), + "last_reset": None, + "state": 0, + "sum": 2, + } + external_statistics2 = { + "start": period2.isoformat(), + "last_reset": None, + "state": 1, + "sum": 3, + } + + external_metadata = { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": source, + "statistic_id": statistic_id, + "unit_of_measurement": "kWh", + } + + await client.send_json( + { + "id": 1, + "type": "recorder/import_statistics", + "metadata": external_metadata, + "stats": [external_statistics1, external_statistics2], + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] is None + + await async_wait_recording_done(hass) + stats = statistics_during_period(hass, zero, period="hour") + assert stats == { + statistic_id: [ + { + "statistic_id": statistic_id, + "start": period1.isoformat(), + "end": (period1 + timedelta(hours=1)).isoformat(), + "max": None, + "mean": None, + "min": None, + "last_reset": None, + "state": approx(0.0), + "sum": approx(2.0), + }, + { + "statistic_id": statistic_id, + "start": period2.isoformat(), + "end": (period2 + timedelta(hours=1)).isoformat(), + "max": None, + "mean": None, + "min": None, + "last_reset": None, + "state": approx(1.0), + "sum": approx(3.0), + }, + ] + } + statistic_ids = list_statistic_ids(hass) # TODO + assert statistic_ids == [ + { + "display_unit_of_measurement": "kWh", + "has_mean": False, + "has_sum": True, + "statistic_id": statistic_id, + "name": "Total imported energy", + "source": source, + "statistics_unit_of_measurement": "kWh", + "unit_class": "energy", + } + ] + metadata = get_metadata(hass, statistic_ids=(statistic_id,)) + assert metadata == { + statistic_id: ( + 1, + { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": source, + "state_unit_of_measurement": "kWh", + "statistic_id": statistic_id, + "unit_of_measurement": "kWh", + }, + ) + } + + # Adjust previously inserted statistics in kWh await client.send_json( { "id": 4, @@ -1156,6 +1513,7 @@ async def test_import_statistics( "statistic_id": statistic_id, "start_time": period2.isoformat(), "adjustment": 1000.0, + "display_unit": "kWh", } ) response = await client.receive_json() @@ -1169,12 +1527,12 @@ async def test_import_statistics( "statistic_id": statistic_id, "start": period1.isoformat(), "end": (period1 + timedelta(hours=1)).isoformat(), - "max": approx(1.0), - "mean": approx(2.0), - "min": approx(3.0), + "max": approx(None), + "mean": approx(None), + "min": approx(None), "last_reset": None, - "state": approx(4.0), - "sum": approx(5.0), + "state": approx(0.0), + "sum": approx(2.0), }, { "statistic_id": statistic_id, @@ -1189,3 +1547,432 @@ async def test_import_statistics( }, ] } + + # Adjust previously inserted statistics in MWh + await client.send_json( + { + "id": 5, + "type": "recorder/adjust_sum_statistics", + "statistic_id": statistic_id, + "start_time": period2.isoformat(), + "adjustment": 2.0, + "display_unit": "MWh", + } + ) + response = await client.receive_json() + assert response["success"] + + await async_wait_recording_done(hass) + stats = statistics_during_period(hass, zero, period="hour") + assert stats == { + statistic_id: [ + { + "statistic_id": statistic_id, + "start": period1.isoformat(), + "end": (period1 + timedelta(hours=1)).isoformat(), + "max": approx(None), + "mean": approx(None), + "min": approx(None), + "last_reset": None, + "state": approx(0.0), + "sum": approx(2.0), + }, + { + "statistic_id": statistic_id, + "start": period2.isoformat(), + "end": (period2 + timedelta(hours=1)).isoformat(), + "max": None, + "mean": None, + "min": None, + "last_reset": None, + "state": approx(1.0), + "sum": approx(3003.0), + }, + ] + } + + +@pytest.mark.parametrize( + "source, statistic_id", + ( + ("test", "test:total_gas"), + ("recorder", "sensor.total_gas"), + ), +) +async def test_adjust_sum_statistics_gas( + hass, hass_ws_client, recorder_mock, caplog, source, statistic_id +): + """Test adjusting statistics.""" + client = await hass_ws_client() + + assert "Compiling statistics for" not in caplog.text + assert "Statistics already compiled" not in caplog.text + + zero = dt_util.utcnow() + 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 = { + "start": period1.isoformat(), + "last_reset": None, + "state": 0, + "sum": 2, + } + external_statistics2 = { + "start": period2.isoformat(), + "last_reset": None, + "state": 1, + "sum": 3, + } + + external_metadata = { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": source, + "statistic_id": statistic_id, + "unit_of_measurement": "m³", + } + + await client.send_json( + { + "id": 1, + "type": "recorder/import_statistics", + "metadata": external_metadata, + "stats": [external_statistics1, external_statistics2], + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] is None + + await async_wait_recording_done(hass) + stats = statistics_during_period(hass, zero, period="hour") + assert stats == { + statistic_id: [ + { + "statistic_id": statistic_id, + "start": period1.isoformat(), + "end": (period1 + timedelta(hours=1)).isoformat(), + "max": None, + "mean": None, + "min": None, + "last_reset": None, + "state": approx(0.0), + "sum": approx(2.0), + }, + { + "statistic_id": statistic_id, + "start": period2.isoformat(), + "end": (period2 + timedelta(hours=1)).isoformat(), + "max": None, + "mean": None, + "min": None, + "last_reset": None, + "state": approx(1.0), + "sum": approx(3.0), + }, + ] + } + statistic_ids = list_statistic_ids(hass) # TODO + assert statistic_ids == [ + { + "display_unit_of_measurement": "m³", + "has_mean": False, + "has_sum": True, + "statistic_id": statistic_id, + "name": "Total imported energy", + "source": source, + "statistics_unit_of_measurement": "m³", + "unit_class": "volume", + } + ] + metadata = get_metadata(hass, statistic_ids=(statistic_id,)) + assert metadata == { + statistic_id: ( + 1, + { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": source, + "state_unit_of_measurement": "m³", + "statistic_id": statistic_id, + "unit_of_measurement": "m³", + }, + ) + } + + # Adjust previously inserted statistics in m³ + await client.send_json( + { + "id": 4, + "type": "recorder/adjust_sum_statistics", + "statistic_id": statistic_id, + "start_time": period2.isoformat(), + "adjustment": 1000.0, + "display_unit": "m³", + } + ) + response = await client.receive_json() + assert response["success"] + + await async_wait_recording_done(hass) + stats = statistics_during_period(hass, zero, period="hour") + assert stats == { + statistic_id: [ + { + "statistic_id": statistic_id, + "start": period1.isoformat(), + "end": (period1 + timedelta(hours=1)).isoformat(), + "max": approx(None), + "mean": approx(None), + "min": approx(None), + "last_reset": None, + "state": approx(0.0), + "sum": approx(2.0), + }, + { + "statistic_id": statistic_id, + "start": period2.isoformat(), + "end": (period2 + timedelta(hours=1)).isoformat(), + "max": None, + "mean": None, + "min": None, + "last_reset": None, + "state": approx(1.0), + "sum": approx(1003.0), + }, + ] + } + + # Adjust previously inserted statistics in ft³ + await client.send_json( + { + "id": 5, + "type": "recorder/adjust_sum_statistics", + "statistic_id": statistic_id, + "start_time": period2.isoformat(), + "adjustment": 35.3147, # ~1 m³ + "display_unit": "ft³", + } + ) + response = await client.receive_json() + assert response["success"] + + await async_wait_recording_done(hass) + stats = statistics_during_period(hass, zero, period="hour") + assert stats == { + statistic_id: [ + { + "statistic_id": statistic_id, + "start": period1.isoformat(), + "end": (period1 + timedelta(hours=1)).isoformat(), + "max": approx(None), + "mean": approx(None), + "min": approx(None), + "last_reset": None, + "state": approx(0.0), + "sum": approx(2.0), + }, + { + "statistic_id": statistic_id, + "start": period2.isoformat(), + "end": (period2 + timedelta(hours=1)).isoformat(), + "max": None, + "mean": None, + "min": None, + "last_reset": None, + "state": approx(1.0), + "sum": approx(1004), + }, + ] + } + + +@pytest.mark.parametrize( + "state_unit, statistic_unit, unit_class, factor, valid_units, invalid_units", + ( + ("kWh", "kWh", "energy", 1, ("Wh", "kWh", "MWh"), ("ft³", "m³", "cats", None)), + ("MWh", "MWh", None, 1, ("MWh",), ("Wh", "kWh", "ft³", "m³", "cats", None)), + ("m³", "m³", "volume", 1, ("ft³", "m³"), ("Wh", "kWh", "MWh", "cats", None)), + ("ft³", "ft³", None, 1, ("ft³",), ("m³", "Wh", "kWh", "MWh", "cats", None)), + ("dogs", "dogs", None, 1, ("dogs",), ("cats", None)), + (None, None, None, 1, (None,), ("cats",)), + ), +) +async def test_adjust_sum_statistics_errors( + hass, + hass_ws_client, + recorder_mock, + caplog, + state_unit, + statistic_unit, + unit_class, + factor, + valid_units, + invalid_units, +): + """Test incorrectly adjusting statistics.""" + statistic_id = "sensor.total_energy_import" + source = "recorder" + client = await hass_ws_client() + + assert "Compiling statistics for" not in caplog.text + assert "Statistics already compiled" not in caplog.text + + zero = dt_util.utcnow() + 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 = { + "start": period1.isoformat(), + "last_reset": None, + "state": 0, + "sum": 2, + } + external_statistics2 = { + "start": period2.isoformat(), + "last_reset": None, + "state": 1, + "sum": 3, + } + + external_metadata = { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": source, + "statistic_id": statistic_id, + "unit_of_measurement": statistic_unit, + } + + await client.send_json( + { + "id": 1, + "type": "recorder/import_statistics", + "metadata": external_metadata, + "stats": [external_statistics1, external_statistics2], + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] is None + + await async_wait_recording_done(hass) + stats = statistics_during_period(hass, zero, period="hour") + assert stats == { + statistic_id: [ + { + "statistic_id": statistic_id, + "start": period1.isoformat(), + "end": (period1 + timedelta(hours=1)).isoformat(), + "max": None, + "mean": None, + "min": None, + "last_reset": None, + "state": approx(0.0 * factor), + "sum": approx(2.0 * factor), + }, + { + "statistic_id": statistic_id, + "start": period2.isoformat(), + "end": (period2 + timedelta(hours=1)).isoformat(), + "max": None, + "mean": None, + "min": None, + "last_reset": None, + "state": approx(1.0 * factor), + "sum": approx(3.0 * factor), + }, + ] + } + previous_stats = stats + statistic_ids = list_statistic_ids(hass) + assert statistic_ids == [ + { + "display_unit_of_measurement": state_unit, + "has_mean": False, + "has_sum": True, + "statistic_id": statistic_id, + "name": "Total imported energy", + "source": source, + "statistics_unit_of_measurement": statistic_unit, + "unit_class": unit_class, + } + ] + metadata = get_metadata(hass, statistic_ids=(statistic_id,)) + assert metadata == { + statistic_id: ( + 1, + { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": source, + "state_unit_of_measurement": state_unit, + "statistic_id": statistic_id, + "unit_of_measurement": statistic_unit, + }, + ) + } + + # Try to adjust statistics + msg_id = 2 + await client.send_json( + { + "id": msg_id, + "type": "recorder/adjust_sum_statistics", + "statistic_id": "sensor.does_not_exist", + "start_time": period2.isoformat(), + "adjustment": 1000.0, + "display_unit": statistic_unit, + } + ) + response = await client.receive_json() + assert not response["success"] + assert response["error"]["code"] == "unknown_statistic_id" + + await async_wait_recording_done(hass) + stats = statistics_during_period(hass, zero, period="hour") + assert stats == previous_stats + + for unit in invalid_units: + msg_id += 1 + await client.send_json( + { + "id": msg_id, + "type": "recorder/adjust_sum_statistics", + "statistic_id": statistic_id, + "start_time": period2.isoformat(), + "adjustment": 1000.0, + "display_unit": unit, + } + ) + response = await client.receive_json() + assert not response["success"] + assert response["error"]["code"] == "invalid_units" + + await async_wait_recording_done(hass) + stats = statistics_during_period(hass, zero, period="hour") + assert stats == previous_stats + + for unit in valid_units: + msg_id += 1 + await client.send_json( + { + "id": msg_id, + "type": "recorder/adjust_sum_statistics", + "statistic_id": statistic_id, + "start_time": period2.isoformat(), + "adjustment": 1000.0, + "display_unit": unit, + } + ) + response = await client.receive_json() + assert response["success"] + + await async_wait_recording_done(hass) + stats = statistics_during_period(hass, zero, period="hour") + assert stats != previous_stats + previous_stats = stats diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index b20b270ee69..f240c0f8af8 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -76,20 +76,20 @@ def set_time_zone(): @pytest.mark.parametrize( - "device_class,state_unit,display_unit,statistics_unit,mean,min,max", + "device_class, state_unit, display_unit, statistics_unit, unit_class, mean, min, max", [ - (None, "%", "%", "%", 13.050847, -10, 30), - ("battery", "%", "%", "%", 13.050847, -10, 30), - ("battery", None, None, None, 13.050847, -10, 30), - ("humidity", "%", "%", "%", 13.050847, -10, 30), - ("humidity", None, None, None, 13.050847, -10, 30), - ("pressure", "Pa", "Pa", "Pa", 13.050847, -10, 30), - ("pressure", "hPa", "hPa", "Pa", 13.050847, -10, 30), - ("pressure", "mbar", "mbar", "Pa", 13.050847, -10, 30), - ("pressure", "inHg", "inHg", "Pa", 13.050847, -10, 30), - ("pressure", "psi", "psi", "Pa", 13.050847, -10, 30), - ("temperature", "°C", "°C", "°C", 13.050847, -10, 30), - ("temperature", "°F", "°F", "°C", 13.050847, -10, 30), + (None, "%", "%", "%", None, 13.050847, -10, 30), + ("battery", "%", "%", "%", None, 13.050847, -10, 30), + ("battery", None, None, None, None, 13.050847, -10, 30), + ("humidity", "%", "%", "%", None, 13.050847, -10, 30), + ("humidity", None, None, None, None, 13.050847, -10, 30), + ("pressure", "Pa", "Pa", "Pa", "pressure", 13.050847, -10, 30), + ("pressure", "hPa", "hPa", "Pa", "pressure", 13.050847, -10, 30), + ("pressure", "mbar", "mbar", "Pa", "pressure", 13.050847, -10, 30), + ("pressure", "inHg", "inHg", "Pa", "pressure", 13.050847, -10, 30), + ("pressure", "psi", "psi", "Pa", "pressure", 13.050847, -10, 30), + ("temperature", "°C", "°C", "°C", "temperature", 13.050847, -10, 30), + ("temperature", "°F", "°F", "°C", "temperature", 13.050847, -10, 30), ], ) def test_compile_hourly_statistics( @@ -99,6 +99,7 @@ def test_compile_hourly_statistics( state_unit, display_unit, statistics_unit, + unit_class, mean, min, max, @@ -129,6 +130,7 @@ def test_compile_hourly_statistics( "name": None, "source": "recorder", "statistics_unit_of_measurement": statistics_unit, + "unit_class": unit_class, } ] stats = statistics_during_period(hass, zero, period="5minute") @@ -151,13 +153,19 @@ def test_compile_hourly_statistics( @pytest.mark.parametrize( - "device_class,state_unit,display_unit,statistics_unit", + "device_class, state_unit, display_unit, statistics_unit, unit_class", [ - (None, "%", "%", "%"), + (None, "%", "%", "%", None), ], ) def test_compile_hourly_statistics_purged_state_changes( - hass_recorder, caplog, device_class, state_unit, display_unit, statistics_unit + hass_recorder, + caplog, + device_class, + state_unit, + display_unit, + statistics_unit, + unit_class, ): """Test compiling hourly statistics.""" zero = dt_util.utcnow() @@ -197,6 +205,7 @@ def test_compile_hourly_statistics_purged_state_changes( "name": None, "source": "recorder", "statistics_unit_of_measurement": statistics_unit, + "unit_class": unit_class, } ] stats = statistics_during_period(hass, zero, period="5minute") @@ -266,6 +275,7 @@ def test_compile_hourly_statistics_unsupported(hass_recorder, caplog, attributes "name": None, "source": "recorder", "statistics_unit_of_measurement": "°C", + "unit_class": "temperature", }, { "statistic_id": "sensor.test6", @@ -275,6 +285,7 @@ def test_compile_hourly_statistics_unsupported(hass_recorder, caplog, attributes "name": None, "source": "recorder", "statistics_unit_of_measurement": "°C", + "unit_class": "temperature", }, { "statistic_id": "sensor.test7", @@ -284,6 +295,7 @@ def test_compile_hourly_statistics_unsupported(hass_recorder, caplog, attributes "name": None, "source": "recorder", "statistics_unit_of_measurement": "°C", + "unit_class": "temperature", }, ] stats = statistics_during_period(hass, zero, period="5minute") @@ -333,20 +345,20 @@ def test_compile_hourly_statistics_unsupported(hass_recorder, caplog, attributes @pytest.mark.parametrize("state_class", ["total"]) @pytest.mark.parametrize( - "units,device_class,state_unit,display_unit,statistics_unit,factor", + "units, device_class, state_unit, display_unit, statistics_unit, unit_class, factor", [ - (IMPERIAL_SYSTEM, "energy", "kWh", "kWh", "kWh", 1), - (IMPERIAL_SYSTEM, "energy", "Wh", "kWh", "kWh", 1 / 1000), - (IMPERIAL_SYSTEM, "monetary", "EUR", "EUR", "EUR", 1), - (IMPERIAL_SYSTEM, "monetary", "SEK", "SEK", "SEK", 1), - (IMPERIAL_SYSTEM, "gas", "m³", "ft³", "m³", 35.314666711), - (IMPERIAL_SYSTEM, "gas", "ft³", "ft³", "m³", 1), - (METRIC_SYSTEM, "energy", "kWh", "kWh", "kWh", 1), - (METRIC_SYSTEM, "energy", "Wh", "kWh", "kWh", 1 / 1000), - (METRIC_SYSTEM, "monetary", "EUR", "EUR", "EUR", 1), - (METRIC_SYSTEM, "monetary", "SEK", "SEK", "SEK", 1), - (METRIC_SYSTEM, "gas", "m³", "m³", "m³", 1), - (METRIC_SYSTEM, "gas", "ft³", "m³", "m³", 0.0283168466), + (IMPERIAL_SYSTEM, "energy", "kWh", "kWh", "kWh", "energy", 1), + (IMPERIAL_SYSTEM, "energy", "Wh", "Wh", "kWh", "energy", 1), + (IMPERIAL_SYSTEM, "monetary", "EUR", "EUR", "EUR", None, 1), + (IMPERIAL_SYSTEM, "monetary", "SEK", "SEK", "SEK", None, 1), + (IMPERIAL_SYSTEM, "gas", "m³", "m³", "m³", "volume", 1), + (IMPERIAL_SYSTEM, "gas", "ft³", "ft³", "m³", "volume", 1), + (METRIC_SYSTEM, "energy", "kWh", "kWh", "kWh", "energy", 1), + (METRIC_SYSTEM, "energy", "Wh", "Wh", "kWh", "energy", 1), + (METRIC_SYSTEM, "monetary", "EUR", "EUR", "EUR", None, 1), + (METRIC_SYSTEM, "monetary", "SEK", "SEK", "SEK", None, 1), + (METRIC_SYSTEM, "gas", "m³", "m³", "m³", "volume", 1), + (METRIC_SYSTEM, "gas", "ft³", "ft³", "m³", "volume", 1), ], ) async def test_compile_hourly_sum_statistics_amount( @@ -360,6 +372,7 @@ async def test_compile_hourly_sum_statistics_amount( state_unit, display_unit, statistics_unit, + unit_class, factor, ): """Test compiling hourly statistics.""" @@ -405,6 +418,7 @@ async def test_compile_hourly_sum_statistics_amount( "name": None, "source": "recorder", "statistics_unit_of_measurement": statistics_unit, + "unit_class": unit_class, } ] stats = statistics_during_period(hass, period0, period="5minute") @@ -478,6 +492,7 @@ async def test_compile_hourly_sum_statistics_amount( "statistic_id": "sensor.test1", "start_time": period1.isoformat(), "adjustment": 100.0, + "display_unit": display_unit, } ) response = await client.receive_json() @@ -497,6 +512,7 @@ async def test_compile_hourly_sum_statistics_amount( "statistic_id": "sensor.test1", "start_time": period2.isoformat(), "adjustment": -400.0, + "display_unit": display_unit, } ) response = await client.receive_json() @@ -511,14 +527,14 @@ async def test_compile_hourly_sum_statistics_amount( @pytest.mark.parametrize("state_class", ["total"]) @pytest.mark.parametrize( - "device_class,state_unit,display_unit,statistics_unit,factor", + "device_class, state_unit, display_unit, statistics_unit, unit_class, factor", [ - ("energy", "kWh", "kWh", "kWh", 1), - ("energy", "Wh", "kWh", "kWh", 1 / 1000), - ("monetary", "EUR", "EUR", "EUR", 1), - ("monetary", "SEK", "SEK", "SEK", 1), - ("gas", "m³", "m³", "m³", 1), - ("gas", "ft³", "m³", "m³", 0.0283168466), + ("energy", "kWh", "kWh", "kWh", "energy", 1), + ("energy", "Wh", "Wh", "kWh", "energy", 1), + ("monetary", "EUR", "EUR", "EUR", None, 1), + ("monetary", "SEK", "SEK", "SEK", None, 1), + ("gas", "m³", "m³", "m³", "volume", 1), + ("gas", "ft³", "ft³", "m³", "volume", 1), ], ) def test_compile_hourly_sum_statistics_amount_reset_every_state_change( @@ -529,6 +545,7 @@ def test_compile_hourly_sum_statistics_amount_reset_every_state_change( state_unit, display_unit, statistics_unit, + unit_class, factor, ): """Test compiling hourly statistics.""" @@ -594,6 +611,7 @@ def test_compile_hourly_sum_statistics_amount_reset_every_state_change( "name": None, "source": "recorder", "statistics_unit_of_measurement": statistics_unit, + "unit_class": unit_class, } ] stats = statistics_during_period(hass, zero, period="5minute") @@ -630,9 +648,9 @@ def test_compile_hourly_sum_statistics_amount_reset_every_state_change( @pytest.mark.parametrize("state_class", ["total"]) @pytest.mark.parametrize( - "device_class,state_unit,display_unit,statistics_unit,factor", + "device_class, state_unit, display_unit, statistics_unit, unit_class, factor", [ - ("energy", "kWh", "kWh", "kWh", 1), + ("energy", "kWh", "kWh", "kWh", "energy", 1), ], ) def test_compile_hourly_sum_statistics_amount_invalid_last_reset( @@ -643,6 +661,7 @@ def test_compile_hourly_sum_statistics_amount_invalid_last_reset( state_unit, display_unit, statistics_unit, + unit_class, factor, ): """Test compiling hourly statistics.""" @@ -693,6 +712,7 @@ def test_compile_hourly_sum_statistics_amount_invalid_last_reset( "name": None, "source": "recorder", "statistics_unit_of_measurement": statistics_unit, + "unit_class": unit_class, } ] stats = statistics_during_period(hass, zero, period="5minute") @@ -717,9 +737,9 @@ def test_compile_hourly_sum_statistics_amount_invalid_last_reset( @pytest.mark.parametrize("state_class", ["total"]) @pytest.mark.parametrize( - "device_class,state_unit,display_unit,statistics_unit,factor", + "device_class, state_unit, display_unit, statistics_unit, unit_class, factor", [ - ("energy", "kWh", "kWh", "kWh", 1), + ("energy", "kWh", "kWh", "kWh", "energy", 1), ], ) def test_compile_hourly_sum_statistics_nan_inf_state( @@ -730,6 +750,7 @@ def test_compile_hourly_sum_statistics_nan_inf_state( state_unit, display_unit, statistics_unit, + unit_class, factor, ): """Test compiling hourly statistics with nan and inf states.""" @@ -776,6 +797,7 @@ def test_compile_hourly_sum_statistics_nan_inf_state( "name": None, "source": "recorder", "statistics_unit_of_measurement": statistics_unit, + "unit_class": unit_class, } ] stats = statistics_during_period(hass, zero, period="5minute") @@ -819,9 +841,9 @@ def test_compile_hourly_sum_statistics_nan_inf_state( ) @pytest.mark.parametrize("state_class", ["total_increasing"]) @pytest.mark.parametrize( - "device_class,state_unit,display_unit,statistics_unit,factor", + "device_class, state_unit, display_unit, statistics_unit, unit_class, factor", [ - ("energy", "kWh", "kWh", "kWh", 1), + ("energy", "kWh", "kWh", "kWh", "energy", 1), ], ) def test_compile_hourly_sum_statistics_negative_state( @@ -835,6 +857,7 @@ def test_compile_hourly_sum_statistics_negative_state( state_unit, display_unit, statistics_unit, + unit_class, factor, ): """Test compiling hourly statistics with negative states.""" @@ -889,6 +912,7 @@ def test_compile_hourly_sum_statistics_negative_state( "source": "recorder", "statistic_id": entity_id, "statistics_unit_of_measurement": statistics_unit, + "unit_class": unit_class, } in statistic_ids stats = statistics_during_period(hass, zero, period="5minute") assert stats[entity_id] == [ @@ -916,14 +940,14 @@ def test_compile_hourly_sum_statistics_negative_state( @pytest.mark.parametrize( - "device_class,state_unit,display_unit,statistics_unit,factor", + "device_class, state_unit, display_unit, statistics_unit, unit_class, factor", [ - ("energy", "kWh", "kWh", "kWh", 1), - ("energy", "Wh", "kWh", "kWh", 1 / 1000), - ("monetary", "EUR", "EUR", "EUR", 1), - ("monetary", "SEK", "SEK", "SEK", 1), - ("gas", "m³", "m³", "m³", 1), - ("gas", "ft³", "m³", "m³", 0.0283168466), + ("energy", "kWh", "kWh", "kWh", "energy", 1), + ("energy", "Wh", "Wh", "kWh", "energy", 1), + ("monetary", "EUR", "EUR", "EUR", None, 1), + ("monetary", "SEK", "SEK", "SEK", None, 1), + ("gas", "m³", "m³", "m³", "volume", 1), + ("gas", "ft³", "ft³", "m³", "volume", 1), ], ) def test_compile_hourly_sum_statistics_total_no_reset( @@ -933,6 +957,7 @@ def test_compile_hourly_sum_statistics_total_no_reset( state_unit, display_unit, statistics_unit, + unit_class, factor, ): """Test compiling hourly statistics.""" @@ -975,6 +1000,7 @@ def test_compile_hourly_sum_statistics_total_no_reset( "name": None, "source": "recorder", "statistics_unit_of_measurement": statistics_unit, + "unit_class": unit_class, } ] stats = statistics_during_period(hass, period0, period="5minute") @@ -1019,12 +1045,12 @@ def test_compile_hourly_sum_statistics_total_no_reset( @pytest.mark.parametrize( - "device_class,state_unit,display_unit,statistics_unit,factor", + "device_class, state_unit, display_unit, statistics_unit, unit_class, factor", [ - ("energy", "kWh", "kWh", "kWh", 1), - ("energy", "Wh", "kWh", "kWh", 1 / 1000), - ("gas", "m³", "m³", "m³", 1), - ("gas", "ft³", "m³", "m³", 0.0283168466), + ("energy", "kWh", "kWh", "kWh", "energy", 1), + ("energy", "Wh", "Wh", "kWh", "energy", 1), + ("gas", "m³", "m³", "m³", "volume", 1), + ("gas", "ft³", "ft³", "m³", "volume", 1), ], ) def test_compile_hourly_sum_statistics_total_increasing( @@ -1034,6 +1060,7 @@ def test_compile_hourly_sum_statistics_total_increasing( state_unit, display_unit, statistics_unit, + unit_class, factor, ): """Test compiling hourly statistics.""" @@ -1076,6 +1103,7 @@ def test_compile_hourly_sum_statistics_total_increasing( "name": None, "source": "recorder", "statistics_unit_of_measurement": statistics_unit, + "unit_class": unit_class, } ] stats = statistics_during_period(hass, period0, period="5minute") @@ -1123,8 +1151,8 @@ def test_compile_hourly_sum_statistics_total_increasing( @pytest.mark.parametrize( - "device_class,state_unit,display_unit,statistics_unit,factor", - [("energy", "kWh", "kWh", "kWh", 1)], + "device_class, state_unit, display_unit, statistics_unit, unit_class, factor", + [("energy", "kWh", "kWh", "kWh", "energy", 1)], ) def test_compile_hourly_sum_statistics_total_increasing_small_dip( hass_recorder, @@ -1133,6 +1161,7 @@ def test_compile_hourly_sum_statistics_total_increasing_small_dip( state_unit, display_unit, statistics_unit, + unit_class, factor, ): """Test small dips in sensor readings do not trigger a reset.""" @@ -1188,6 +1217,7 @@ def test_compile_hourly_sum_statistics_total_increasing_small_dip( "name": None, "source": "recorder", "statistics_unit_of_measurement": statistics_unit, + "unit_class": unit_class, } ] stats = statistics_during_period(hass, period0, period="5minute") @@ -1282,6 +1312,7 @@ def test_compile_hourly_energy_statistics_unsupported(hass_recorder, caplog): "name": None, "source": "recorder", "statistics_unit_of_measurement": "kWh", + "unit_class": "energy", } ] stats = statistics_during_period(hass, period0, period="5minute") @@ -1374,6 +1405,7 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): "name": None, "source": "recorder", "statistics_unit_of_measurement": "kWh", + "unit_class": "energy", }, { "statistic_id": "sensor.test2", @@ -1383,15 +1415,17 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): "name": None, "source": "recorder", "statistics_unit_of_measurement": "kWh", + "unit_class": "energy", }, { "statistic_id": "sensor.test3", - "display_unit_of_measurement": "kWh", + "display_unit_of_measurement": "Wh", "has_mean": False, "has_sum": True, "name": None, "source": "recorder", "statistics_unit_of_measurement": "kWh", + "unit_class": "energy", }, ] stats = statistics_during_period(hass, period0, period="5minute") @@ -1475,8 +1509,8 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): "mean": None, "min": None, "last_reset": process_timestamp_to_utc_isoformat(period0), - "state": approx(5.0 / 1000), - "sum": approx(5.0 / 1000), + "state": approx(5.0), + "sum": approx(5.0), }, { "statistic_id": "sensor.test3", @@ -1486,8 +1520,8 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): "mean": None, "min": None, "last_reset": process_timestamp_to_utc_isoformat(four), - "state": approx(50.0 / 1000), - "sum": approx(60.0 / 1000), + "state": approx(50.0), + "sum": approx(60.0), }, { "statistic_id": "sensor.test3", @@ -1497,8 +1531,8 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): "mean": None, "min": None, "last_reset": process_timestamp_to_utc_isoformat(four), - "state": approx(90.0 / 1000), - "sum": approx(100.0 / 1000), + "state": approx(90.0), + "sum": approx(100.0), }, ], } @@ -1666,31 +1700,31 @@ def test_compile_hourly_statistics_fails(hass_recorder, caplog): @pytest.mark.parametrize( - "state_class,device_class,state_unit,display_unit,statistics_unit,statistic_type", + "state_class, device_class, state_unit, display_unit, statistics_unit, unit_class, statistic_type", [ - ("measurement", "battery", "%", "%", "%", "mean"), - ("measurement", "battery", None, None, None, "mean"), - ("total", "energy", "Wh", "kWh", "kWh", "sum"), - ("total", "energy", "kWh", "kWh", "kWh", "sum"), - ("measurement", "energy", "Wh", "kWh", "kWh", "mean"), - ("measurement", "energy", "kWh", "kWh", "kWh", "mean"), - ("measurement", "humidity", "%", "%", "%", "mean"), - ("measurement", "humidity", None, None, None, "mean"), - ("total", "monetary", "USD", "USD", "USD", "sum"), - ("total", "monetary", "None", "None", "None", "sum"), - ("total", "gas", "m³", "m³", "m³", "sum"), - ("total", "gas", "ft³", "m³", "m³", "sum"), - ("measurement", "monetary", "USD", "USD", "USD", "mean"), - ("measurement", "monetary", "None", "None", "None", "mean"), - ("measurement", "gas", "m³", "m³", "m³", "mean"), - ("measurement", "gas", "ft³", "m³", "m³", "mean"), - ("measurement", "pressure", "Pa", "Pa", "Pa", "mean"), - ("measurement", "pressure", "hPa", "hPa", "Pa", "mean"), - ("measurement", "pressure", "mbar", "mbar", "Pa", "mean"), - ("measurement", "pressure", "inHg", "inHg", "Pa", "mean"), - ("measurement", "pressure", "psi", "psi", "Pa", "mean"), - ("measurement", "temperature", "°C", "°C", "°C", "mean"), - ("measurement", "temperature", "°F", "°F", "°C", "mean"), + ("measurement", "battery", "%", "%", "%", None, "mean"), + ("measurement", "battery", None, None, None, None, "mean"), + ("total", "energy", "Wh", "Wh", "kWh", "energy", "sum"), + ("total", "energy", "kWh", "kWh", "kWh", "energy", "sum"), + ("measurement", "energy", "Wh", "Wh", "kWh", "energy", "mean"), + ("measurement", "energy", "kWh", "kWh", "kWh", "energy", "mean"), + ("measurement", "humidity", "%", "%", "%", None, "mean"), + ("measurement", "humidity", None, None, None, None, "mean"), + ("total", "monetary", "USD", "USD", "USD", None, "sum"), + ("total", "monetary", "None", "None", "None", None, "sum"), + ("total", "gas", "m³", "m³", "m³", "volume", "sum"), + ("total", "gas", "ft³", "ft³", "m³", "volume", "sum"), + ("measurement", "monetary", "USD", "USD", "USD", None, "mean"), + ("measurement", "monetary", "None", "None", "None", None, "mean"), + ("measurement", "gas", "m³", "m³", "m³", "volume", "mean"), + ("measurement", "gas", "ft³", "ft³", "m³", "volume", "mean"), + ("measurement", "pressure", "Pa", "Pa", "Pa", "pressure", "mean"), + ("measurement", "pressure", "hPa", "hPa", "Pa", "pressure", "mean"), + ("measurement", "pressure", "mbar", "mbar", "Pa", "pressure", "mean"), + ("measurement", "pressure", "inHg", "inHg", "Pa", "pressure", "mean"), + ("measurement", "pressure", "psi", "psi", "Pa", "pressure", "mean"), + ("measurement", "temperature", "°C", "°C", "°C", "temperature", "mean"), + ("measurement", "temperature", "°F", "°F", "°C", "temperature", "mean"), ], ) def test_list_statistic_ids( @@ -1701,6 +1735,7 @@ def test_list_statistic_ids( state_unit, display_unit, statistics_unit, + unit_class, statistic_type, ): """Test listing future statistic ids.""" @@ -1724,6 +1759,7 @@ def test_list_statistic_ids( "name": None, "source": "recorder", "statistics_unit_of_measurement": statistics_unit, + "unit_class": unit_class, }, ] for stat_type in ["mean", "sum", "dogs"]: @@ -1738,6 +1774,7 @@ def test_list_statistic_ids( "name": None, "source": "recorder", "statistics_unit_of_measurement": statistics_unit, + "unit_class": unit_class, }, ] else: @@ -1772,12 +1809,12 @@ def test_list_statistic_ids_unsupported(hass_recorder, caplog, _attributes): @pytest.mark.parametrize( - "device_class,state_unit,display_unit,statistics_unit,mean,min,max", + "device_class, state_unit, display_unit, statistics_unit, unit_class, mean, min, max", [ - (None, None, None, None, 13.050847, -10, 30), - (None, "%", "%", "%", 13.050847, -10, 30), - ("battery", "%", "%", "%", 13.050847, -10, 30), - ("battery", None, None, None, 13.050847, -10, 30), + (None, None, None, None, None, 13.050847, -10, 30), + (None, "%", "%", "%", None, 13.050847, -10, 30), + ("battery", "%", "%", "%", None, 13.050847, -10, 30), + ("battery", None, None, None, None, 13.050847, -10, 30), ], ) def test_compile_hourly_statistics_changing_units_1( @@ -1787,6 +1824,7 @@ def test_compile_hourly_statistics_changing_units_1( state_unit, display_unit, statistics_unit, + unit_class, mean, min, max, @@ -1827,6 +1865,7 @@ def test_compile_hourly_statistics_changing_units_1( "name": None, "source": "recorder", "statistics_unit_of_measurement": statistics_unit, + "unit_class": unit_class, }, ] stats = statistics_during_period(hass, zero, period="5minute") @@ -1862,6 +1901,7 @@ def test_compile_hourly_statistics_changing_units_1( "name": None, "source": "recorder", "statistics_unit_of_measurement": statistics_unit, + "unit_class": unit_class, }, ] stats = statistics_during_period(hass, zero, period="5minute") @@ -1884,12 +1924,12 @@ def test_compile_hourly_statistics_changing_units_1( @pytest.mark.parametrize( - "device_class,state_unit,display_unit,statistics_unit,mean,min,max", + "device_class, state_unit, display_unit, statistics_unit, unit_class, mean, min, max", [ - (None, None, None, None, 13.050847, -10, 30), - (None, "%", "%", "%", 13.050847, -10, 30), - ("battery", "%", "%", "%", 13.050847, -10, 30), - ("battery", None, None, None, 13.050847, -10, 30), + (None, None, None, None, None, 13.050847, -10, 30), + (None, "%", "%", "%", None, 13.050847, -10, 30), + ("battery", "%", "%", "%", None, 13.050847, -10, 30), + ("battery", None, None, None, None, 13.050847, -10, 30), ], ) def test_compile_hourly_statistics_changing_units_2( @@ -1899,6 +1939,7 @@ def test_compile_hourly_statistics_changing_units_2( state_unit, display_unit, statistics_unit, + unit_class, mean, min, max, @@ -1936,6 +1977,7 @@ def test_compile_hourly_statistics_changing_units_2( "name": None, "source": "recorder", "statistics_unit_of_measurement": "cats", + "unit_class": unit_class, }, ] stats = statistics_during_period(hass, zero, period="5minute") @@ -1945,12 +1987,12 @@ def test_compile_hourly_statistics_changing_units_2( @pytest.mark.parametrize( - "device_class,state_unit,display_unit,statistics_unit,mean,min,max", + "device_class, state_unit, display_unit, statistics_unit, unit_class, mean, min, max", [ - (None, None, None, None, 13.050847, -10, 30), - (None, "%", "%", "%", 13.050847, -10, 30), - ("battery", "%", "%", "%", 13.050847, -10, 30), - ("battery", None, None, None, 13.050847, -10, 30), + (None, None, None, None, None, 13.050847, -10, 30), + (None, "%", "%", "%", None, 13.050847, -10, 30), + ("battery", "%", "%", "%", None, 13.050847, -10, 30), + ("battery", None, None, None, None, 13.050847, -10, 30), ], ) def test_compile_hourly_statistics_changing_units_3( @@ -1960,6 +2002,7 @@ def test_compile_hourly_statistics_changing_units_3( state_unit, display_unit, statistics_unit, + unit_class, mean, min, max, @@ -2000,6 +2043,7 @@ def test_compile_hourly_statistics_changing_units_3( "name": None, "source": "recorder", "statistics_unit_of_measurement": statistics_unit, + "unit_class": unit_class, }, ] stats = statistics_during_period(hass, zero, period="5minute") @@ -2035,6 +2079,7 @@ def test_compile_hourly_statistics_changing_units_3( "name": None, "source": "recorder", "statistics_unit_of_measurement": statistics_unit, + "unit_class": unit_class, }, ] stats = statistics_during_period(hass, zero, period="5minute") @@ -2057,13 +2102,21 @@ def test_compile_hourly_statistics_changing_units_3( @pytest.mark.parametrize( - "device_class,state_unit,statistic_unit,mean,min,max", + "device_class, state_unit, statistic_unit, unit_class, mean, min, max", [ - ("power", "kW", "W", 13.050847, -10, 30), + ("power", "kW", "W", None, 13.050847, -10, 30), ], ) def test_compile_hourly_statistics_changing_device_class_1( - hass_recorder, caplog, device_class, state_unit, statistic_unit, mean, min, max + hass_recorder, + caplog, + device_class, + state_unit, + statistic_unit, + unit_class, + mean, + min, + max, ): """Test compiling hourly statistics where device class changes from one hour to the next.""" zero = dt_util.utcnow() @@ -2091,6 +2144,7 @@ def test_compile_hourly_statistics_changing_device_class_1( "name": None, "source": "recorder", "statistics_unit_of_measurement": state_unit, + "unit_class": unit_class, }, ] stats = statistics_during_period(hass, zero, period="5minute") @@ -2140,6 +2194,7 @@ def test_compile_hourly_statistics_changing_device_class_1( "name": None, "source": "recorder", "statistics_unit_of_measurement": state_unit, + "unit_class": unit_class, }, ] stats = statistics_during_period(hass, zero, period="5minute") @@ -2162,9 +2217,9 @@ def test_compile_hourly_statistics_changing_device_class_1( @pytest.mark.parametrize( - "device_class,state_unit,display_unit,statistic_unit,mean,min,max", + "device_class, state_unit, display_unit, statistic_unit, unit_class, mean, min, max", [ - ("power", "kW", "kW", "W", 13.050847, -10, 30), + ("power", "kW", "kW", "W", "power", 13.050847, -10, 30), ], ) def test_compile_hourly_statistics_changing_device_class_2( @@ -2174,6 +2229,7 @@ def test_compile_hourly_statistics_changing_device_class_2( state_unit, display_unit, statistic_unit, + unit_class, mean, min, max, @@ -2205,6 +2261,7 @@ def test_compile_hourly_statistics_changing_device_class_2( "name": None, "source": "recorder", "statistics_unit_of_measurement": statistic_unit, + "unit_class": unit_class, }, ] stats = statistics_during_period(hass, zero, period="5minute") @@ -2254,6 +2311,7 @@ def test_compile_hourly_statistics_changing_device_class_2( "name": None, "source": "recorder", "statistics_unit_of_measurement": statistic_unit, + "unit_class": unit_class, }, ] stats = statistics_during_period(hass, zero, period="5minute") @@ -2276,9 +2334,9 @@ def test_compile_hourly_statistics_changing_device_class_2( @pytest.mark.parametrize( - "device_class,state_unit,display_unit,statistics_unit,mean,min,max", + "device_class, state_unit, display_unit, statistics_unit, unit_class, mean, min, max", [ - (None, None, None, None, 13.050847, -10, 30), + (None, None, None, None, None, 13.050847, -10, 30), ], ) def test_compile_hourly_statistics_changing_statistics( @@ -2288,6 +2346,7 @@ def test_compile_hourly_statistics_changing_statistics( state_unit, display_unit, statistics_unit, + unit_class, mean, min, max, @@ -2322,6 +2381,7 @@ def test_compile_hourly_statistics_changing_statistics( "name": None, "source": "recorder", "statistics_unit_of_measurement": None, + "unit_class": None, }, ] metadata = get_metadata(hass, statistic_ids=("sensor.test1",)) @@ -2358,6 +2418,7 @@ def test_compile_hourly_statistics_changing_statistics( "name": None, "source": "recorder", "statistics_unit_of_measurement": None, + "unit_class": None, }, ] metadata = get_metadata(hass, statistic_ids=("sensor.test1",)) @@ -2552,6 +2613,7 @@ def test_compile_statistics_hourly_daily_monthly_summary(hass_recorder, caplog): "name": None, "source": "recorder", "statistics_unit_of_measurement": "%", + "unit_class": None, }, { "statistic_id": "sensor.test2", @@ -2561,6 +2623,7 @@ def test_compile_statistics_hourly_daily_monthly_summary(hass_recorder, caplog): "name": None, "source": "recorder", "statistics_unit_of_measurement": "%", + "unit_class": None, }, { "statistic_id": "sensor.test3", @@ -2570,6 +2633,7 @@ def test_compile_statistics_hourly_daily_monthly_summary(hass_recorder, caplog): "name": None, "source": "recorder", "statistics_unit_of_measurement": "%", + "unit_class": None, }, { "statistic_id": "sensor.test4", @@ -2579,6 +2643,7 @@ def test_compile_statistics_hourly_daily_monthly_summary(hass_recorder, caplog): "name": None, "source": "recorder", "statistics_unit_of_measurement": "EUR", + "unit_class": None, }, ] @@ -2588,7 +2653,7 @@ def test_compile_statistics_hourly_daily_monthly_summary(hass_recorder, caplog): for i in range(13, 24): expected_sums["sensor.test4"][i] += sum_adjustment instance.async_adjust_statistics( - "sensor.test4", sum_adjustement_start, sum_adjustment + "sensor.test4", sum_adjustement_start, sum_adjustment, "EUR" ) wait_recording_done(hass) From d7eb277bc80499f6e3d48e7c0b285033a9c335d7 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 21 Sep 2022 00:32:35 +0000 Subject: [PATCH 597/955] [ci skip] Translation update --- .../accuweather/translations/bg.json | 9 ++++ .../components/bluetooth/translations/bg.json | 5 +++ .../components/bluetooth/translations/ca.json | 6 +++ .../components/bluetooth/translations/de.json | 6 +++ .../components/bluetooth/translations/el.json | 6 +++ .../components/bluetooth/translations/en.json | 7 +++- .../components/bluetooth/translations/es.json | 6 +++ .../components/bluetooth/translations/hu.json | 6 +++ .../components/bluetooth/translations/id.json | 6 +++ .../components/bluetooth/translations/nl.json | 5 +++ .../bluetooth/translations/pt-BR.json | 6 +++ .../bluetooth/translations/zh-Hant.json | 6 +++ .../components/bond/translations/bg.json | 3 ++ .../components/guardian/translations/id.json | 15 ++++++- .../components/ibeacon/translations/bg.json | 12 ++++++ .../components/ibeacon/translations/ca.json | 23 ++++++++++ .../components/ibeacon/translations/de.json | 23 ++++++++++ .../components/ibeacon/translations/el.json | 23 ++++++++++ .../components/ibeacon/translations/es.json | 23 ++++++++++ .../components/ibeacon/translations/fr.json | 21 ++++++++++ .../components/ibeacon/translations/hu.json | 23 ++++++++++ .../components/ibeacon/translations/id.json | 22 ++++++++++ .../components/ibeacon/translations/nl.json | 7 ++++ .../ibeacon/translations/pt-BR.json | 23 ++++++++++ .../ibeacon/translations/zh-Hant.json | 23 ++++++++++ .../components/lidarr/translations/ca.json | 41 ++++++++++++++++++ .../components/lidarr/translations/de.json | 42 +++++++++++++++++++ .../components/lidarr/translations/en.json | 18 ++++---- .../components/lidarr/translations/es.json | 42 +++++++++++++++++++ .../components/lidarr/translations/fr.json | 41 ++++++++++++++++++ .../components/lidarr/translations/hu.json | 13 ++++++ .../components/lidarr/translations/nl.json | 28 +++++++++++++ .../components/lidarr/translations/pt-BR.json | 42 +++++++++++++++++++ .../litterrobot/translations/sensor.id.json | 3 ++ .../moon/translations/sensor.pl.json | 4 +- .../components/nobo_hub/translations/nl.json | 1 + .../components/openuv/translations/id.json | 8 ++++ .../ovo_energy/translations/bg.json | 1 + .../simplisafe/translations/id.json | 5 +++ .../components/smappee/translations/bg.json | 1 + .../components/sun/translations/pl.json | 2 +- .../components/switchbee/translations/nl.json | 21 ++++++++++ .../volvooncall/translations/id.json | 1 + .../xiaomi_aqara/translations/bg.json | 1 + .../components/zha/translations/el.json | 1 + .../components/zha/translations/nl.json | 7 +++- 46 files changed, 622 insertions(+), 16 deletions(-) create mode 100644 homeassistant/components/ibeacon/translations/bg.json create mode 100644 homeassistant/components/ibeacon/translations/ca.json create mode 100644 homeassistant/components/ibeacon/translations/de.json create mode 100644 homeassistant/components/ibeacon/translations/el.json create mode 100644 homeassistant/components/ibeacon/translations/es.json create mode 100644 homeassistant/components/ibeacon/translations/fr.json create mode 100644 homeassistant/components/ibeacon/translations/hu.json create mode 100644 homeassistant/components/ibeacon/translations/id.json create mode 100644 homeassistant/components/ibeacon/translations/nl.json create mode 100644 homeassistant/components/ibeacon/translations/pt-BR.json create mode 100644 homeassistant/components/ibeacon/translations/zh-Hant.json create mode 100644 homeassistant/components/lidarr/translations/ca.json create mode 100644 homeassistant/components/lidarr/translations/de.json create mode 100644 homeassistant/components/lidarr/translations/es.json create mode 100644 homeassistant/components/lidarr/translations/fr.json create mode 100644 homeassistant/components/lidarr/translations/hu.json create mode 100644 homeassistant/components/lidarr/translations/nl.json create mode 100644 homeassistant/components/lidarr/translations/pt-BR.json create mode 100644 homeassistant/components/switchbee/translations/nl.json diff --git a/homeassistant/components/accuweather/translations/bg.json b/homeassistant/components/accuweather/translations/bg.json index 26fdf8e85d5..6cd4cdde80e 100644 --- a/homeassistant/components/accuweather/translations/bg.json +++ b/homeassistant/components/accuweather/translations/bg.json @@ -17,5 +17,14 @@ } } } + }, + "options": { + "step": { + "user": { + "data": { + "forecast": "\u041f\u0440\u043e\u0433\u043d\u043e\u0437\u0430 \u0437\u0430 \u0432\u0440\u0435\u043c\u0435\u0442\u043e" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/bg.json b/homeassistant/components/bluetooth/translations/bg.json index 1d6b9891e4a..7da387cae4c 100644 --- a/homeassistant/components/bluetooth/translations/bg.json +++ b/homeassistant/components/bluetooth/translations/bg.json @@ -29,6 +29,11 @@ } } }, + "issues": { + "haos_outdated": { + "title": "\u0410\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u0439\u0442\u0435 \u0434\u043e \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u0430 \u0441\u0438\u0441\u0442\u0435\u043c\u0430 Home Assistant 9.0 \u0438\u043b\u0438 \u043f\u043e-\u043d\u043e\u0432\u0430" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/bluetooth/translations/ca.json b/homeassistant/components/bluetooth/translations/ca.json index 8adff8747b5..8c124446672 100644 --- a/homeassistant/components/bluetooth/translations/ca.json +++ b/homeassistant/components/bluetooth/translations/ca.json @@ -29,6 +29,12 @@ } } }, + "issues": { + "haos_outdated": { + "description": "Per millorar la fiabilitat i el rendiment de Bluetooth, et recomanem que actualitzis a la versi\u00f3 9.0 o posterior del sistema operatiu Home Assistant.", + "title": "Actualitza el sistema operatiu a Home Assistant 9.0 o posterior" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/bluetooth/translations/de.json b/homeassistant/components/bluetooth/translations/de.json index 1f8f48e05ee..63bbf51c59e 100644 --- a/homeassistant/components/bluetooth/translations/de.json +++ b/homeassistant/components/bluetooth/translations/de.json @@ -29,6 +29,12 @@ } } }, + "issues": { + "haos_outdated": { + "description": "Zur Verbesserung der Bluetooth-Zuverl\u00e4ssigkeit und -Leistung empfehlen wir dir dringend ein Update auf Version 9.0 oder h\u00f6her des Home Assistant-Betriebssystems.", + "title": "Aktualisiere auf das Home Assistant-Betriebssystem 9.0 oder h\u00f6her" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/bluetooth/translations/el.json b/homeassistant/components/bluetooth/translations/el.json index 1d6685d307e..f31e930b3fa 100644 --- a/homeassistant/components/bluetooth/translations/el.json +++ b/homeassistant/components/bluetooth/translations/el.json @@ -29,6 +29,12 @@ } } }, + "issues": { + "haos_outdated": { + "description": "\u0393\u03b9\u03b1 \u03bd\u03b1 \u03b2\u03b5\u03bb\u03c4\u03b9\u03ce\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b1\u03be\u03b9\u03bf\u03c0\u03b9\u03c3\u03c4\u03af\u03b1 \u03ba\u03b1\u03b9 \u03c4\u03b7\u03bd \u03b1\u03c0\u03cc\u03b4\u03bf\u03c3\u03b7 \u03c4\u03bf\u03c5 Bluetooth, \u03c3\u03b1\u03c2 \u03c3\u03c5\u03bd\u03b9\u03c3\u03c4\u03bf\u03cd\u03bc\u03b5 \u03b1\u03bd\u03b5\u03c0\u03b9\u03c6\u03cd\u03bb\u03b1\u03ba\u03c4\u03b1 \u03bd\u03b1 \u03b5\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7 9.0 \u03ae \u03bd\u03b5\u03cc\u03c4\u03b5\u03c1\u03b7 \u03c4\u03bf\u03c5 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03b9\u03ba\u03bf\u03cd \u03c3\u03c5\u03c3\u03c4\u03ae\u03bc\u03b1\u03c4\u03bf\u03c2 Home Assistant.", + "title": "\u0395\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7 \u03c3\u03b5 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03b9\u03ba\u03cc \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1 Home Assistant 9.0 \u03ae \u03bd\u03b5\u03cc\u03c4\u03b5\u03c1\u03b7 \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/bluetooth/translations/en.json b/homeassistant/components/bluetooth/translations/en.json index beefb842204..73ed74356fd 100644 --- a/homeassistant/components/bluetooth/translations/en.json +++ b/homeassistant/components/bluetooth/translations/en.json @@ -9,6 +9,9 @@ "bluetooth_confirm": { "description": "Do you want to setup {name}?" }, + "enable_bluetooth": { + "description": "Do you want to setup Bluetooth?" + }, "multiple_adapters": { "data": { "adapter": "Adapter" @@ -36,8 +39,10 @@ "step": { "init": { "data": { + "adapter": "The Bluetooth Adapter to use for scanning", "passive": "Passive scanning" - } + }, + "description": "Passive listening requires BlueZ 5.63 or later with experimental features enabled." } } } diff --git a/homeassistant/components/bluetooth/translations/es.json b/homeassistant/components/bluetooth/translations/es.json index b28fc6cc695..4cfa38df9c2 100644 --- a/homeassistant/components/bluetooth/translations/es.json +++ b/homeassistant/components/bluetooth/translations/es.json @@ -29,6 +29,12 @@ } } }, + "issues": { + "haos_outdated": { + "description": "Para mejorar la confiabilidad y el rendimiento de Bluetooth, te recomendamos que actualices a la versi\u00f3n 9.0 o posterior de Home Assistant Operating System.", + "title": "Actualiza a Home Assistant Operating System 9.0 o posterior" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/bluetooth/translations/hu.json b/homeassistant/components/bluetooth/translations/hu.json index 38eb6d5ada0..79dc3204031 100644 --- a/homeassistant/components/bluetooth/translations/hu.json +++ b/homeassistant/components/bluetooth/translations/hu.json @@ -29,6 +29,12 @@ } } }, + "issues": { + "haos_outdated": { + "description": "A Bluetooth megb\u00edzhat\u00f3s\u00e1g\u00e1nak \u00e9s teljes\u00edtm\u00e9ny\u00e9nek jav\u00edt\u00e1sa \u00e9rdek\u00e9ben er\u0151sen javasoljuk, hogy friss\u00edtse a Home Assistant oper\u00e1ci\u00f3s rendszer\u00e9t 9.0 vagy \u00fajabb verzi\u00f3ra.", + "title": "Home Assistant oper\u00e1ci\u00f3s rendszer\u00e9nek friss\u00edt\u00e9se 9.0 vagy \u00fajabb verzi\u00f3ra" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/bluetooth/translations/id.json b/homeassistant/components/bluetooth/translations/id.json index c74420cd281..282071fc4f1 100644 --- a/homeassistant/components/bluetooth/translations/id.json +++ b/homeassistant/components/bluetooth/translations/id.json @@ -29,6 +29,12 @@ } } }, + "issues": { + "haos_outdated": { + "description": "Untuk meningkatkan keandalan dan performa Bluetooth, kami sangat menyarankan Anda memperbarui ke versi 9.0 atau yang lebih baru dari Home Assistant Operating System.", + "title": "Perbarui ke Home Assistant Operating System 9.0 atau yang lebih baru" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/bluetooth/translations/nl.json b/homeassistant/components/bluetooth/translations/nl.json index 9a0e95df8bc..c9db452612e 100644 --- a/homeassistant/components/bluetooth/translations/nl.json +++ b/homeassistant/components/bluetooth/translations/nl.json @@ -20,6 +20,11 @@ } } }, + "issues": { + "haos_outdated": { + "title": "Update naar het Home Assistant-besturingssysteem versie 9.0 of hoger" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/bluetooth/translations/pt-BR.json b/homeassistant/components/bluetooth/translations/pt-BR.json index 11c802b5023..389205c20db 100644 --- a/homeassistant/components/bluetooth/translations/pt-BR.json +++ b/homeassistant/components/bluetooth/translations/pt-BR.json @@ -29,6 +29,12 @@ } } }, + "issues": { + "haos_outdated": { + "description": "Para melhorar a confiabilidade e o desempenho do Bluetooth, recomendamos que voc\u00ea atualize para a vers\u00e3o 9.0 ou posterior do sistema operacional Home Assistant.", + "title": "Atualiza\u00e7\u00e3o para o sistema operacional Home Assistant 9.0 ou posterior" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/bluetooth/translations/zh-Hant.json b/homeassistant/components/bluetooth/translations/zh-Hant.json index 08b19a67d34..a45ccc52d44 100644 --- a/homeassistant/components/bluetooth/translations/zh-Hant.json +++ b/homeassistant/components/bluetooth/translations/zh-Hant.json @@ -29,6 +29,12 @@ } } }, + "issues": { + "haos_outdated": { + "description": "\u6b32\u6539\u5584\u85cd\u82bd\u53ef\u9760\u6027\u8207\u6548\u80fd\uff0c\u5f37\u70c8\u5efa\u8b70\u60a8\u66f4\u65b0\u81f3 9.0 \u6216\u66f4\u65b0\u7248\u672c\u4e4b Home Assistant OS\u3002", + "title": "\u8acb\u66f4\u65b0\u81f3 Home Assistant OS 9.0 \u6216\u66f4\u65b0\u7248\u672c" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/bond/translations/bg.json b/homeassistant/components/bond/translations/bg.json index 7f67a133aa8..da903ee46b1 100644 --- a/homeassistant/components/bond/translations/bg.json +++ b/homeassistant/components/bond/translations/bg.json @@ -10,6 +10,9 @@ }, "flow_title": "{name} ({host})", "step": { + "confirm": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 {name}?" + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442" diff --git a/homeassistant/components/guardian/translations/id.json b/homeassistant/components/guardian/translations/id.json index 0d06eb61729..131debefb94 100644 --- a/homeassistant/components/guardian/translations/id.json +++ b/homeassistant/components/guardian/translations/id.json @@ -24,11 +24,22 @@ "step": { "confirm": { "description": "Perbarui semua otomasi atau skrip yang menggunakan layanan ini untuk menggunakan layanan `{alternate_service}` dengan ID entitas target `{alternate_target}`. Kemudian, klik KIRIM di bawah ini untuk menandai masalah ini sebagai terselesaikan.", - "title": "Layanan {deprecated_service} dalam proses penghapusan" + "title": "Layanan {deprecated_service} akan dihapus" } } }, - "title": "Layanan {deprecated_service} dalam proses penghapusan" + "title": "Layanan {deprecated_service} akan dihapus" + }, + "replaced_old_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "Perbarui setiap otomasi atau skrip yang menggunakan entitas ini untuk menggunakan `{replacement_entity_id}`.", + "title": "Entitas {old_entity_id} akan dihapus" + } + } + }, + "title": "Entitas {old_entity_id} akan dihapus" } } } \ No newline at end of file diff --git a/homeassistant/components/ibeacon/translations/bg.json b/homeassistant/components/ibeacon/translations/bg.json new file mode 100644 index 00000000000..6f8f950076d --- /dev/null +++ b/homeassistant/components/ibeacon/translations/bg.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "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." + }, + "step": { + "user": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 iBeacon Tracker?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ibeacon/translations/ca.json b/homeassistant/components/ibeacon/translations/ca.json new file mode 100644 index 00000000000..aaca36cd8dd --- /dev/null +++ b/homeassistant/components/ibeacon/translations/ca.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "bluetooth_not_available": "Almenys un adaptador Bluetooth o controlador remot ha d'estar configurat per utilitzar iBeacon Tracker.", + "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." + }, + "step": { + "user": { + "description": "Vols configurar iBeacon Tracker?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "min_rssi": "RSSI m\u00ednim" + }, + "description": "S'ignoraran els iBeacons amb un valor d'RSSI inferior al m\u00ednim. Si la integraci\u00f3 veu iBeacons propers, augmentar aquest valor pot ajudar." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ibeacon/translations/de.json b/homeassistant/components/ibeacon/translations/de.json new file mode 100644 index 00000000000..c91a821d4cb --- /dev/null +++ b/homeassistant/components/ibeacon/translations/de.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "bluetooth_not_available": "Mindestens ein Bluetooth-Adapter oder eine Fernbedienung muss f\u00fcr die Verwendung von iBeacon Tracker konfiguriert sein.", + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, + "step": { + "user": { + "description": "M\u00f6chtest du den iBeacon Tracker einrichten?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "min_rssi": "Mindest-RSSI" + }, + "description": "iBeacons mit einem RSSI-Wert, der unter dem Mindest-RSSI liegt, werden ignoriert. Wenn die Integration benachbarte iBeacons sieht, kann eine Erh\u00f6hung dieses Wertes helfen." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ibeacon/translations/el.json b/homeassistant/components/ibeacon/translations/el.json new file mode 100644 index 00000000000..ca9522da0b7 --- /dev/null +++ b/homeassistant/components/ibeacon/translations/el.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "bluetooth_not_available": "\u03a4\u03bf\u03c5\u03bb\u03ac\u03c7\u03b9\u03c3\u03c4\u03bf\u03bd \u03ad\u03bd\u03b1\u03c2 \u03c0\u03c1\u03bf\u03c3\u03b1\u03c1\u03bc\u03bf\u03b3\u03ad\u03b1\u03c2 Bluetooth \u03ae \u03ad\u03bd\u03b1 \u03c4\u03b7\u03bb\u03b5\u03c7\u03b5\u03b9\u03c1\u03b9\u03c3\u03c4\u03ae\u03c1\u03b9\u03bf \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c7\u03c1\u03ae\u03c3\u03b7 \u03c4\u03bf\u03c5 iBeacon Tracker.", + "single_instance_allowed": "\u0388\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae." + }, + "step": { + "user": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf iBeacon Tracker;" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "min_rssi": "\u0395\u03bb\u03ac\u03c7\u03b9\u03c3\u03c4\u03bf RSSI" + }, + "description": "\u03a4\u03b1 iBeacons \u03bc\u03b5 \u03c4\u03b9\u03bc\u03ae RSSI \u03c7\u03b1\u03bc\u03b7\u03bb\u03cc\u03c4\u03b5\u03c1\u03b7 \u03b1\u03c0\u03cc \u03c4\u03b7\u03bd \u03b5\u03bb\u03ac\u03c7\u03b9\u03c3\u03c4\u03b7 \u03c4\u03b9\u03bc\u03ae RSSI \u03b8\u03b1 \u03b1\u03b3\u03bd\u03bf\u03b7\u03b8\u03bf\u03cd\u03bd. \u0395\u03ac\u03bd \u03b7 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03b2\u03bb\u03ad\u03c0\u03b5\u03b9 \u03b3\u03b5\u03b9\u03c4\u03bf\u03bd\u03b9\u03ba\u03ac iBeacons, \u03b7 \u03b1\u03cd\u03be\u03b7\u03c3\u03b7 \u03b1\u03c5\u03c4\u03ae\u03c2 \u03c4\u03b7\u03c2 \u03c4\u03b9\u03bc\u03ae\u03c2 \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03b2\u03bf\u03b7\u03b8\u03ae\u03c3\u03b5\u03b9." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ibeacon/translations/es.json b/homeassistant/components/ibeacon/translations/es.json new file mode 100644 index 00000000000..f13c33161ed --- /dev/null +++ b/homeassistant/components/ibeacon/translations/es.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "bluetooth_not_available": "Se debe configurar al menos un adaptador Bluetooth o un control remoto para usar iBeacon Tracker.", + "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." + }, + "step": { + "user": { + "description": "\u00bfQuieres configurar iBeacon Tracker?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "min_rssi": "RSSI m\u00ednimo" + }, + "description": "Se ignorar\u00e1n los iBeacons con un valor de RSSI inferior al RSSI m\u00ednimo. Si la integraci\u00f3n est\u00e1 viendo iBeacons vecinos, aumentar este valor puede ayudar." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ibeacon/translations/fr.json b/homeassistant/components/ibeacon/translations/fr.json new file mode 100644 index 00000000000..c86904b872f --- /dev/null +++ b/homeassistant/components/ibeacon/translations/fr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." + }, + "step": { + "user": { + "description": "Voulez-vous configurer iBeacon Tracker\u00a0?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "min_rssi": "RSSI minimal" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ibeacon/translations/hu.json b/homeassistant/components/ibeacon/translations/hu.json new file mode 100644 index 00000000000..598076ba70f --- /dev/null +++ b/homeassistant/components/ibeacon/translations/hu.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "bluetooth_not_available": "Legal\u00e1bb egy Bluetooth-adaptert vagy \u00e1tad\u00f3t be kell \u00e1ll\u00edtani az iBeacon Tracker haszn\u00e1lat\u00e1hoz.", + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." + }, + "step": { + "user": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani az iBeacon Tracker-t?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "min_rssi": "Minimum RSSI" + }, + "description": "A minimum RSSI \u00e9rt\u00e9kn\u00e9l alacsonyabb RSSI \u00e9rt\u00e9kkel rendelkez\u0151 iBeacon-\u00f6ket a rendszer figyelmen k\u00edv\u00fcl hagyja. Ha az integr\u00e1ci\u00f3 szomsz\u00e9d iBeacon-okat is l\u00e1t, akkor ennek az \u00e9rt\u00e9knek a n\u00f6vel\u00e9se seg\u00edthet." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ibeacon/translations/id.json b/homeassistant/components/ibeacon/translations/id.json new file mode 100644 index 00000000000..2c7a1adbd5e --- /dev/null +++ b/homeassistant/components/ibeacon/translations/id.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "bluetooth_not_available": "Setidaknya satu adaptor Bluetooth atau remote harus dikonfigurasi untuk menggunakan iBeacon Tracker.", + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + }, + "step": { + "user": { + "description": "Ingin menyiapkan iBeacon Tracker?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "min_rssi": "RSSI minimum" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ibeacon/translations/nl.json b/homeassistant/components/ibeacon/translations/nl.json new file mode 100644 index 00000000000..703ac8614c4 --- /dev/null +++ b/homeassistant/components/ibeacon/translations/nl.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ibeacon/translations/pt-BR.json b/homeassistant/components/ibeacon/translations/pt-BR.json new file mode 100644 index 00000000000..0dfe8a4d8cd --- /dev/null +++ b/homeassistant/components/ibeacon/translations/pt-BR.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "bluetooth_not_available": "Pelo menos um adaptador ou controle remoto Bluetooth deve ser configurado para usar o iBeacon Tracker.", + "single_instance_allowed": "J\u00e1 est\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "step": { + "user": { + "description": "Deseja configurar o iBeacon Tracker?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "min_rssi": "RSSI m\u00ednimo" + }, + "description": "Os iBeacons com um valor RSSI inferior ao RSSI m\u00ednimo ser\u00e3o ignorados. Se a integra\u00e7\u00e3o estiver vendo iBeacons vizinhos, aumentar esse valor pode ajudar." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ibeacon/translations/zh-Hant.json b/homeassistant/components/ibeacon/translations/zh-Hant.json new file mode 100644 index 00000000000..66bb34df27e --- /dev/null +++ b/homeassistant/components/ibeacon/translations/zh-Hant.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "bluetooth_not_available": "\u5fc5\u9808\u81f3\u5c11\u8a2d\u5b9a\u4e00\u7d44\u85cd\u82bd\u50b3\u8f38\u5668\u6216\u9060\u7aef\u88dd\u7f6e\u65b9\u80fd\u4f7f\u7528 iBeacon Tracker\u3002", + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + }, + "step": { + "user": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a iBeacon Tracker\uff1f" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "min_rssi": "\u6700\u5c0f RSSI \u503c" + }, + "description": "\u4f4e\u65bc\u6700\u5c0f RSSI \u503c\u7684 iBeacons \u5c07\u6703\u906d\u5230\u5ffd\u7565\u3002\u5047\u5982\u6574\u5408\u5075\u6e2c\u5230\u9644\u8fd1\u7684 iBeacon\u3001\u589e\u52a0\u6b64\u6578\u503c\u53ef\u80fd\u6709\u6240\u5e6b\u52a9\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lidarr/translations/ca.json b/homeassistant/components/lidarr/translations/ca.json new file mode 100644 index 00000000000..1610465f760 --- /dev/null +++ b/homeassistant/components/lidarr/translations/ca.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "El servei ja est\u00e0 configurat", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat", + "wrong_app": "No s'ha trobat l'aplicaci\u00f3 correcta. Torna-ho a intentar", + "zeroconf_failed": "No s'ha trobat la clau API. Introdu\u00efu-la manualment" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Clau API" + }, + "description": "La integraci\u00f3 Lidarr ha de tornar a autenticar-se manualment amb l'API de Lidarr", + "title": "Reautenticaci\u00f3 de la integraci\u00f3" + }, + "user": { + "data": { + "api_key": "Clau API", + "url": "URL", + "verify_ssl": "Verifica el certificat SSL" + }, + "description": "La clau API es pot recuperar autom\u00e0ticament si les credencials d'inici de sessi\u00f3 no s'han establert a l'aplicaci\u00f3.\nLa teva clau API es pot trobar a Configuraci\u00f3 ('Settings') > General a la interf\u00edcie web de Lidarr." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "upcoming_days": "Nombre dies propers a mostrar al calendari" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lidarr/translations/de.json b/homeassistant/components/lidarr/translations/de.json new file mode 100644 index 00000000000..a51b9c24a2f --- /dev/null +++ b/homeassistant/components/lidarr/translations/de.json @@ -0,0 +1,42 @@ +{ + "config": { + "abort": { + "already_configured": "Der Dienst ist bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler", + "wrong_app": "Falsche Anwendung erreicht. Bitte versuche es erneut", + "zeroconf_failed": "API-Schl\u00fcssel nicht gefunden. Bitte gib ihn manuell ein" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API-Schl\u00fcssel" + }, + "description": "Die Lidarr-Integration muss manuell erneut mit der Lidarr-API authentifiziert werden", + "title": "Integration erneut authentifizieren" + }, + "user": { + "data": { + "api_key": "API-Schl\u00fcssel", + "url": "URL", + "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" + }, + "description": "Der API-Schl\u00fcssel kann automatisch abgerufen werden, wenn in der Anwendung keine Anmeldeinformationen festgelegt wurden.\nDeinen API-Schl\u00fcssel findest du unter Einstellungen > Allgemein in der Lidarr-Web-Benutzeroberfl\u00e4che." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "max_records": "Anzahl der maximal anzuzeigenden Datens\u00e4tze f\u00fcr Gesucht und Warteschlange", + "upcoming_days": "Anzahl der kommenden Tage, die im Kalender angezeigt werden sollen" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lidarr/translations/en.json b/homeassistant/components/lidarr/translations/en.json index 03c7435eb36..0e0475d25cd 100644 --- a/homeassistant/components/lidarr/translations/en.json +++ b/homeassistant/components/lidarr/translations/en.json @@ -7,25 +7,25 @@ "error": { "cannot_connect": "Failed to connect", "invalid_auth": "Invalid authentication", - "zeroconf_failed": "API key not found. Please enter it manually", + "unknown": "Unexpected error", "wrong_app": "Incorrect application reached. Please try again", - "unknown": "Unexpected error" + "zeroconf_failed": "API key not found. Please enter it manually" }, "step": { "reauth_confirm": { - "description": "The Lidarr integration needs to be manually re-authenticated with the Lidarr API", - "title": "Reauthenticate Integration", "data": { "api_key": "API Key" - } + }, + "description": "The Lidarr integration needs to be manually re-authenticated with the Lidarr API", + "title": "Reauthenticate Integration" }, "user": { - "description": "API key can be retrieved automatically if login credentials were not set in application.\nYour API key can be found in Settings > General in the Lidarr Web UI.", "data": { "api_key": "API Key", "url": "URL", "verify_ssl": "Verify SSL certificate" - } + }, + "description": "API key can be retrieved automatically if login credentials were not set in application.\nYour API key can be found in Settings > General in the Lidarr Web UI." } } }, @@ -33,8 +33,8 @@ "step": { "init": { "data": { - "upcoming_days": "Number of upcoming days to display on calendar", - "max_records": "Number of maximum records to display on wanted and queue" + "max_records": "Number of maximum records to display on wanted and queue", + "upcoming_days": "Number of upcoming days to display on calendar" } } } diff --git a/homeassistant/components/lidarr/translations/es.json b/homeassistant/components/lidarr/translations/es.json new file mode 100644 index 00000000000..071ee1312ec --- /dev/null +++ b/homeassistant/components/lidarr/translations/es.json @@ -0,0 +1,42 @@ +{ + "config": { + "abort": { + "already_configured": "El servicio ya est\u00e1 configurado", + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "unknown": "Error inesperado", + "wrong_app": "Se ha alcanzado una aplicaci\u00f3n incorrecta. Por favor, int\u00e9ntalo de nuevo", + "zeroconf_failed": "Clave API no encontrada. Por favor, introd\u00facela manualmente" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Clave API" + }, + "description": "La integraci\u00f3n Lidarr debe volver a autenticarse manualmente con la API de Lidarr", + "title": "Volver a autenticar la integraci\u00f3n" + }, + "user": { + "data": { + "api_key": "Clave API", + "url": "URL", + "verify_ssl": "Verificar el certificado SSL" + }, + "description": "La clave API se puede recuperar autom\u00e1ticamente si las credenciales de inicio de sesi\u00f3n no se configuraron en la aplicaci\u00f3n.\nTu clave API se puede encontrar en Configuraci\u00f3n > General en la IU web de Lidarr." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "max_records": "N\u00famero m\u00e1ximo de registros para mostrar en b\u00fasqueda y cola", + "upcoming_days": "N\u00famero de pr\u00f3ximos d\u00edas para mostrar en el calendario" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lidarr/translations/fr.json b/homeassistant/components/lidarr/translations/fr.json new file mode 100644 index 00000000000..9eb6bf92cd2 --- /dev/null +++ b/homeassistant/components/lidarr/translations/fr.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification non valide", + "unknown": "Erreur inattendue", + "wrong_app": "Une application incorrecte a \u00e9t\u00e9 atteinte. Veuillez r\u00e9essayer", + "zeroconf_failed": "La cl\u00e9 d'API n'a pas \u00e9t\u00e9 trouv\u00e9e. Veuillez la saisir manuellement" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Cl\u00e9 d'API" + }, + "description": "L'int\u00e9gration Lidarr doit \u00eatre r\u00e9-authentifi\u00e9e manuellement avec l'API Lidarr", + "title": "R\u00e9-authentifier l'int\u00e9gration" + }, + "user": { + "data": { + "api_key": "Cl\u00e9 d'API", + "url": "URL", + "verify_ssl": "V\u00e9rifier le certificat SSL" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "max_records": "Nombre maximal d'enregistrements \u00e0 afficher sur la recherche et la file d'attente", + "upcoming_days": "Nombre de jours \u00e0 venir \u00e0 afficher sur le calendrier" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lidarr/translations/hu.json b/homeassistant/components/lidarr/translations/hu.json new file mode 100644 index 00000000000..6a18f68959e --- /dev/null +++ b/homeassistant/components/lidarr/translations/hu.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lidarr/translations/nl.json b/homeassistant/components/lidarr/translations/nl.json new file mode 100644 index 00000000000..0ec6ffdb679 --- /dev/null +++ b/homeassistant/components/lidarr/translations/nl.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Dienst is al geconfigureerd", + "reauth_successful": "Herauthenticatie geslaagd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API-sleutel" + }, + "title": "Integratie herauthenticeren" + }, + "user": { + "data": { + "api_key": "API-sleutel", + "url": "URL", + "verify_ssl": "SSL-certificaat verifi\u00ebren" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lidarr/translations/pt-BR.json b/homeassistant/components/lidarr/translations/pt-BR.json new file mode 100644 index 00000000000..9390e86b497 --- /dev/null +++ b/homeassistant/components/lidarr/translations/pt-BR.json @@ -0,0 +1,42 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" + }, + "error": { + "cannot_connect": "Falhou ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado", + "wrong_app": "Aplica\u00e7\u00e3o incorreta alcan\u00e7ada. Por favor, tente novamente", + "zeroconf_failed": "Chave de API n\u00e3o encontrada. Por favor, insira manualmente" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Chave de API" + }, + "description": "A integra\u00e7\u00e3o do Lidarr precisa ser autenticada manualmente com a API do Lidarr", + "title": "Reautenticar Integra\u00e7\u00e3o" + }, + "user": { + "data": { + "api_key": "Chave de API", + "url": "URL", + "verify_ssl": "Verificar certificado SSL" + }, + "description": "A chave de API pode ser recuperada automaticamente se as credenciais de login n\u00e3o tiverem sido definidas no aplicativo.\n Sua chave de API pode ser encontrada em Configura\u00e7\u00f5es > Geral na IU da Web do Lidarr." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "max_records": "N\u00famero m\u00e1ximo de registros a serem exibidos em desejados e em fila", + "upcoming_days": "N\u00famero de pr\u00f3ximos dias a serem exibidos no calend\u00e1rio" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/sensor.id.json b/homeassistant/components/litterrobot/translations/sensor.id.json index 20f76ca4322..003bb338a02 100644 --- a/homeassistant/components/litterrobot/translations/sensor.id.json +++ b/homeassistant/components/litterrobot/translations/sensor.id.json @@ -4,6 +4,7 @@ "br": "Bonnet Dihapus", "ccc": "Siklus Bersih Selesai", "ccp": "Siklus Bersih Sedang Berlangsung", + "cd": "Kucing Terdeteksi", "csf": "Kesalahan Sensor Kucing", "csi": "Sensor Kucing Terganggu", "cst": "Waktu Sensor Kucing", @@ -16,6 +17,8 @@ "offline": "Luring", "otf": "Kesalahan Torsi Berlebih", "p": "Jeda", + "pwrd": "Mematikan Daya", + "pwru": "Menyalakan", "rdy": "Siap", "scf": "Kesalahan Sensor Kucing Saat Mulai", "sdf": "Laci Penuh Saat Memulai" diff --git a/homeassistant/components/moon/translations/sensor.pl.json b/homeassistant/components/moon/translations/sensor.pl.json index 616db5be621..f70cd8f38a0 100644 --- a/homeassistant/components/moon/translations/sensor.pl.json +++ b/homeassistant/components/moon/translations/sensor.pl.json @@ -5,9 +5,9 @@ "full_moon": "pe\u0142nia", "last_quarter": "ostatnia kwadra", "new_moon": "n\u00f3w", - "waning_crescent": "sierp ubywaj\u0105cy", + "waning_crescent": "Sierp ubywaj\u0105cy", "waning_gibbous": "ubywaj\u0105cy garbaty", - "waxing_crescent": "sierp przybywaj\u0105cy", + "waxing_crescent": "Sierp przybywaj\u0105cy", "waxing_gibbous": "przybywaj\u0105cy garbaty" } } diff --git a/homeassistant/components/nobo_hub/translations/nl.json b/homeassistant/components/nobo_hub/translations/nl.json index dde5dfc5ad1..8a25fd2ab6b 100644 --- a/homeassistant/components/nobo_hub/translations/nl.json +++ b/homeassistant/components/nobo_hub/translations/nl.json @@ -4,6 +4,7 @@ "already_configured": "Apparaat is al geconfigureerd" }, "error": { + "invalid_serial": "Ongeldig serienummer", "unknown": "Onverwachte fout" }, "step": { diff --git a/homeassistant/components/openuv/translations/id.json b/homeassistant/components/openuv/translations/id.json index 568e92d1999..2ed7f57d3b9 100644 --- a/homeassistant/components/openuv/translations/id.json +++ b/homeassistant/components/openuv/translations/id.json @@ -18,6 +18,14 @@ } } }, + "issues": { + "deprecated_service_multiple_alternate_targets": { + "title": "Layanan {deprecated_service} dalam proses penghapusan" + }, + "deprecated_service_single_alternate_target": { + "title": "Layanan {deprecated_service} dalam proses penghapusan" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/ovo_energy/translations/bg.json b/homeassistant/components/ovo_energy/translations/bg.json index b7636becf45..9b0d9f27ccb 100644 --- a/homeassistant/components/ovo_energy/translations/bg.json +++ b/homeassistant/components/ovo_energy/translations/bg.json @@ -1,6 +1,7 @@ { "config": { "error": { + "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \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" }, diff --git a/homeassistant/components/simplisafe/translations/id.json b/homeassistant/components/simplisafe/translations/id.json index 4bb637e3f08..a0509622f33 100644 --- a/homeassistant/components/simplisafe/translations/id.json +++ b/homeassistant/components/simplisafe/translations/id.json @@ -39,6 +39,11 @@ } } }, + "issues": { + "deprecated_service": { + "title": "Layanan {deprecated_service} dalam proses penghapusan" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/smappee/translations/bg.json b/homeassistant/components/smappee/translations/bg.json index 7b8f03499a6..419abecf78b 100644 --- a/homeassistant/components/smappee/translations/bg.json +++ b/homeassistant/components/smappee/translations/bg.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured_device": "\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", "missing_configuration": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u044a\u0442 \u043d\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d. \u041c\u043e\u043b\u044f, \u0441\u043b\u0435\u0434\u0432\u0430\u0439\u0442\u0435 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0442\u0430." }, diff --git a/homeassistant/components/sun/translations/pl.json b/homeassistant/components/sun/translations/pl.json index 7e95dd568c6..0bd977db506 100644 --- a/homeassistant/components/sun/translations/pl.json +++ b/homeassistant/components/sun/translations/pl.json @@ -12,7 +12,7 @@ "state": { "_": { "above_horizon": "nad horyzontem", - "below_horizon": "poni\u017cej horyzontu" + "below_horizon": "Poni\u017cej horyzontu" } }, "title": "S\u0142o\u0144ce" diff --git a/homeassistant/components/switchbee/translations/nl.json b/homeassistant/components/switchbee/translations/nl.json new file mode 100644 index 00000000000..8ad15260b0d --- /dev/null +++ b/homeassistant/components/switchbee/translations/nl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Wachtwoord", + "username": "Gebruikersnaam" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/volvooncall/translations/id.json b/homeassistant/components/volvooncall/translations/id.json index c8815d0000e..d4b60911401 100644 --- a/homeassistant/components/volvooncall/translations/id.json +++ b/homeassistant/components/volvooncall/translations/id.json @@ -15,6 +15,7 @@ "password": "Kata Sandi", "region": "Wilayah", "scandinavian_miles": "Gunakan Mil Skandinavia", + "unit_system": "Sistem Unit", "username": "Nama Pengguna" } } diff --git a/homeassistant/components/xiaomi_aqara/translations/bg.json b/homeassistant/components/xiaomi_aqara/translations/bg.json index 61cc221fa2c..de2cba26ce2 100644 --- a/homeassistant/components/xiaomi_aqara/translations/bg.json +++ b/homeassistant/components/xiaomi_aqara/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": { + "invalid_host": "\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, \u0432\u0438\u0436\u0442\u0435 https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem", "invalid_mac": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d Mac \u0430\u0434\u0440\u0435\u0441" }, "flow_title": "{name}", diff --git a/homeassistant/components/zha/translations/el.json b/homeassistant/components/zha/translations/el.json index 621334e5492..69c437a2e06 100644 --- a/homeassistant/components/zha/translations/el.json +++ b/homeassistant/components/zha/translations/el.json @@ -173,6 +173,7 @@ "options": { "abort": { "not_zha_device": "\u0391\u03c5\u03c4\u03ae \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae zha", + "single_instance_allowed": "\u0388\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae.", "usb_probe_failed": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03b1\u03bd\u03af\u03c7\u03bd\u03b5\u03c5\u03c3\u03b7\u03c2 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 usb" }, "error": { diff --git a/homeassistant/components/zha/translations/nl.json b/homeassistant/components/zha/translations/nl.json index 2955326add4..4b28c5f7b07 100644 --- a/homeassistant/components/zha/translations/nl.json +++ b/homeassistant/components/zha/translations/nl.json @@ -120,6 +120,11 @@ "error": { "cannot_connect": "Kan geen verbinding maken" }, - "flow_title": "{name}" + "flow_title": "{name}", + "step": { + "upload_manual_backup": { + "title": "Upload een handmatige back-up" + } + } } } \ No newline at end of file From 7a6897c7578dffd6b67f57747ebd81b67b153e01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20J=C3=A4ger?= Date: Wed, 21 Sep 2022 08:20:44 +0200 Subject: [PATCH 598/955] Add deconz current hvac operation to thermostate based on "state" (#59989) * deconz - add current hvac operation to thermostate based on "state" * deconz - extend current hvac operation to thermostate based on "state" and "mode" * Add tests for current hvac action * Add boost mode as special case * format using Black * sort imports * Add test for device with mode none and state none * Update homeassistant/components/deconz/climate.py Co-authored-by: Robert Svensson * Fix test_climate.py test_no_mode_no_state * Add test for boost mode Co-authored-by: Robert Svensson --- homeassistant/components/deconz/climate.py | 16 ++ tests/components/deconz/test_climate.py | 201 ++++++++++++++++++++- 2 files changed, 216 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/deconz/climate.py b/homeassistant/components/deconz/climate.py index 24cc6c8d5c8..0d13f2639da 100644 --- a/homeassistant/components/deconz/climate.py +++ b/homeassistant/components/deconz/climate.py @@ -24,6 +24,7 @@ from homeassistant.components.climate import ( PRESET_ECO, ClimateEntity, ClimateEntityFeature, + HVACAction, HVACMode, ) from homeassistant.config_entries import ConfigEntry @@ -172,6 +173,21 @@ class DeconzThermostat(DeconzDevice[Thermostat], ClimateEntity): mode=HVAC_MODE_TO_DECONZ[hvac_mode], ) + @property + def hvac_action(self) -> str | None: + """Return current hvac operation ie. heat, cool. + + Preset 'BOOST' is interpreted as 'state_on'. + """ + if self._device.mode == ThermostatMode.OFF: + return HVACAction.OFF + + if self._device.state_on or self._device.preset == ThermostatPreset.BOOST: + if self._device.mode == ThermostatMode.COOL: + return HVACAction.COOLING + return HVACAction.HEATING + return HVACAction.IDLE + # Preset control @property diff --git a/tests/components/deconz/test_climate.py b/tests/components/deconz/test_climate.py index d93dd2ebfa1..c621ce02823 100644 --- a/tests/components/deconz/test_climate.py +++ b/tests/components/deconz/test_climate.py @@ -25,6 +25,7 @@ from homeassistant.components.climate.const import ( PRESET_BOOST, PRESET_COMFORT, PRESET_ECO, + HVACAction, HVACMode, ) from homeassistant.components.deconz.climate import ( @@ -108,6 +109,7 @@ async def test_simple_climate_device(hass, aioclient_mock, mock_deconz_websocket assert climate_thermostat.attributes["temperature"] == 21.0 assert climate_thermostat.attributes["locked"] is True assert hass.states.get("sensor.thermostat_battery").state == "59" + assert climate_thermostat.attributes["hvac_action"] == HVACAction.HEATING # Event signals thermostat configured off @@ -122,6 +124,10 @@ async def test_simple_climate_device(hass, aioclient_mock, mock_deconz_websocket await hass.async_block_till_done() assert hass.states.get("climate.thermostat").state == STATE_OFF + assert ( + hass.states.get("climate.thermostat").attributes["hvac_action"] + == HVACAction.IDLE + ) # Event signals thermostat state on @@ -136,6 +142,10 @@ async def test_simple_climate_device(hass, aioclient_mock, mock_deconz_websocket await hass.async_block_till_done() assert hass.states.get("climate.thermostat").state == HVACMode.HEAT + assert ( + hass.states.get("climate.thermostat").attributes["hvac_action"] + == HVACAction.HEATING + ) # Verify service calls @@ -210,6 +220,10 @@ async def test_climate_device_without_cooling_support( assert hass.states.get("sensor.thermostat_battery").state == "100" assert hass.states.get("climate.presence_sensor") is None assert hass.states.get("climate.clip_thermostat") is None + assert ( + hass.states.get("climate.thermostat").attributes["hvac_action"] + == HVACAction.HEATING + ) # Event signals thermostat configured off @@ -224,6 +238,10 @@ async def test_climate_device_without_cooling_support( await hass.async_block_till_done() assert hass.states.get("climate.thermostat").state == STATE_OFF + assert ( + hass.states.get("climate.thermostat").attributes["hvac_action"] + == HVACAction.OFF + ) # Event signals thermostat state on @@ -239,6 +257,10 @@ async def test_climate_device_without_cooling_support( await hass.async_block_till_done() assert hass.states.get("climate.thermostat").state == HVACMode.HEAT + assert ( + hass.states.get("climate.thermostat").attributes["hvac_action"] + == HVACAction.HEATING + ) # Event signals thermostat state off @@ -253,6 +275,10 @@ async def test_climate_device_without_cooling_support( await hass.async_block_till_done() assert hass.states.get("climate.thermostat").state == STATE_OFF + assert ( + hass.states.get("climate.thermostat").attributes["hvac_action"] + == HVACAction.IDLE + ) # Verify service calls @@ -382,8 +408,11 @@ async def test_climate_device_with_cooling_support( assert climate_thermostat.attributes["current_temperature"] == 23.2 assert climate_thermostat.attributes["temperature"] == 22.2 assert hass.states.get("sensor.zen_01_battery").state == "25" + assert ( + hass.states.get("climate.zen_01").attributes["hvac_action"] == HVACAction.IDLE + ) - # Event signals thermostat state cool + # Event signals thermostat mode cool event_changed_sensor = { "t": "event", @@ -398,6 +427,27 @@ async def test_climate_device_with_cooling_support( assert hass.states.get("climate.zen_01").state == HVACMode.COOL assert hass.states.get("climate.zen_01").attributes["temperature"] == 11.1 + assert ( + hass.states.get("climate.zen_01").attributes["hvac_action"] == HVACAction.IDLE + ) + + # Event signals thermostat state on + + event_changed_sensor = { + "t": "event", + "e": "changed", + "r": "sensors", + "id": "0", + "state": {"on": True}, + } + await mock_deconz_websocket(data=event_changed_sensor) + await hass.async_block_till_done() + + assert hass.states.get("climate.zen_01").state == HVACMode.COOL + assert ( + hass.states.get("climate.zen_01").attributes["hvac_action"] + == HVACAction.COOLING + ) # Verify service calls @@ -463,6 +513,9 @@ async def test_climate_device_with_fan_support( FAN_ON, FAN_OFF, ] + assert ( + hass.states.get("climate.zen_01").attributes["hvac_action"] == HVACAction.IDLE + ) # Event signals fan mode defaults to off @@ -477,6 +530,9 @@ async def test_climate_device_with_fan_support( await hass.async_block_till_done() assert hass.states.get("climate.zen_01").attributes["fan_mode"] == FAN_OFF + assert ( + hass.states.get("climate.zen_01").attributes["hvac_action"] == HVACAction.IDLE + ) # Event signals unsupported fan mode @@ -492,6 +548,10 @@ async def test_climate_device_with_fan_support( await hass.async_block_till_done() assert hass.states.get("climate.zen_01").attributes["fan_mode"] == FAN_ON + assert ( + hass.states.get("climate.zen_01").attributes["hvac_action"] + == HVACAction.HEATING + ) # Event signals unsupported fan mode @@ -506,6 +566,10 @@ async def test_climate_device_with_fan_support( await hass.async_block_till_done() assert hass.states.get("climate.zen_01").attributes["fan_mode"] == FAN_ON + assert ( + hass.states.get("climate.zen_01").attributes["hvac_action"] + == HVACAction.HEATING + ) # Verify service calls @@ -593,6 +657,9 @@ async def test_climate_device_with_preset(hass, aioclient_mock, mock_deconz_webs DECONZ_PRESET_HOLIDAY, DECONZ_PRESET_MANUAL, ] + assert ( + hass.states.get("climate.zen_01").attributes["hvac_action"] == HVACAction.IDLE + ) # Event signals deCONZ preset @@ -693,6 +760,10 @@ async def test_clip_climate_device(hass, aioclient_mock): assert len(hass.states.async_all()) == 3 assert hass.states.get("climate.clip_thermostat").state == HVACMode.HEAT + assert ( + hass.states.get("climate.clip_thermostat").attributes["hvac_action"] + == HVACAction.HEATING + ) # Disallow clip sensors @@ -713,6 +784,10 @@ async def test_clip_climate_device(hass, aioclient_mock): assert len(hass.states.async_all()) == 3 assert hass.states.get("climate.clip_thermostat").state == HVACMode.HEAT + assert ( + hass.states.get("climate.clip_thermostat").attributes["hvac_action"] + == HVACAction.HEATING + ) async def test_verify_state_update(hass, aioclient_mock, mock_deconz_websocket): @@ -738,6 +813,10 @@ async def test_verify_state_update(hass, aioclient_mock, mock_deconz_websocket): await setup_deconz_integration(hass, aioclient_mock) assert hass.states.get("climate.thermostat").state == HVACMode.AUTO + assert ( + hass.states.get("climate.thermostat").attributes["hvac_action"] + == HVACAction.HEATING + ) event_changed_sensor = { "t": "event", @@ -750,6 +829,10 @@ async def test_verify_state_update(hass, aioclient_mock, mock_deconz_websocket): await hass.async_block_till_done() assert hass.states.get("climate.thermostat").state == HVACMode.AUTO + assert ( + hass.states.get("climate.thermostat").attributes["hvac_action"] + == HVACAction.IDLE + ) async def test_add_new_climate_device(hass, aioclient_mock, mock_deconz_websocket): @@ -784,6 +867,10 @@ async def test_add_new_climate_device(hass, aioclient_mock, mock_deconz_websocke assert len(hass.states.async_all()) == 2 assert hass.states.get("climate.thermostat").state == HVACMode.AUTO assert hass.states.get("sensor.thermostat_battery").state == "100" + assert ( + hass.states.get("climate.thermostat").attributes["hvac_action"] + == HVACAction.HEATING + ) async def test_not_allow_clip_thermostat(hass, aioclient_mock): @@ -806,3 +893,115 @@ async def test_not_allow_clip_thermostat(hass, aioclient_mock): ) assert len(hass.states.async_all()) == 0 + + +async def test_no_mode_no_state(hass, aioclient_mock, mock_deconz_websocket): + """Test that a climate device without mode and state works.""" + data = { + "sensors": { + "0": { + "config": { + "battery": 25, + "heatsetpoint": 2222, + "mode": None, + "preset": "auto", + "offset": 0, + "on": True, + "reachable": True, + }, + "ep": 1, + "etag": "074549903686a77a12ef0f06c499b1ef", + "lastseen": "2020-11-27T13:45Z", + "manufacturername": "Zen Within", + "modelid": "Zen-01", + "name": "Zen-01", + "state": {"lastupdated": "none", "on": None, "temperature": 2290}, + "type": "ZHAThermostat", + "uniqueid": "00:24:46:00:00:11:6f:56-01-0201", + } + } + } + with patch.dict(DECONZ_WEB_REQUEST, data): + config_entry = await setup_deconz_integration(hass, aioclient_mock) + + assert len(hass.states.async_all()) == 2 + + climate_thermostat = hass.states.get("climate.zen_01") + + assert climate_thermostat.state is STATE_OFF + assert climate_thermostat.attributes["preset_mode"] is DECONZ_PRESET_AUTO + assert climate_thermostat.attributes["hvac_action"] is HVACAction.IDLE + + # Verify service calls + mock_deconz_put_request(aioclient_mock, config_entry.data, "/sensors/0/config") + + +async def test_boost_mode(hass, aioclient_mock, mock_deconz_websocket): + """Test that a climate device with boost mode and different state works.""" + data = { + "sensors": { + "0": { + "config": { + "battery": 58, + "heatsetpoint": 2200, + "locked": False, + "mode": "heat", + "offset": -200, + "on": True, + "preset": "manual", + "reachable": True, + "schedule": {}, + "schedule_on": False, + "setvalve": False, + "windowopen_set": False, + }, + "ep": 1, + "etag": "404c15db68c318ebe7832ce5aa3d1e30", + "lastannounced": "2022-08-31T03:00:59Z", + "lastseen": "2022-09-19T11:58Z", + "manufacturername": "_TZE200_b6wax7g0", + "modelid": "TS0601", + "name": "Thermostat", + "state": { + "lastupdated": "2022-09-19T11:58:24.204", + "lowbattery": False, + "on": False, + "temperature": 2200, + "valve": 0, + }, + "type": "ZHAThermostat", + "uniqueid": "84:fd:27:ff:fe:8a:eb:89-01-0201", + } + } + } + with patch.dict(DECONZ_WEB_REQUEST, data): + config_entry = await setup_deconz_integration(hass, aioclient_mock) + + assert len(hass.states.async_all()) == 3 + + climate_thermostat = hass.states.get("climate.thermostat") + + assert climate_thermostat.state == HVACMode.HEAT + + assert climate_thermostat.attributes["preset_mode"] is DECONZ_PRESET_MANUAL + assert climate_thermostat.attributes["hvac_action"] is HVACAction.IDLE + + # Event signals thermostat preset boost and valve 100 (real data) + event_changed_sensor = { + "t": "event", + "e": "changed", + "r": "sensors", + "id": "0", + "config": {"preset": "boost"}, + "state": {"valve": 100}, + } + + await mock_deconz_websocket(data=event_changed_sensor) + await hass.async_block_till_done() + + climate_thermostat = hass.states.get("climate.thermostat") + assert climate_thermostat.attributes["preset_mode"] is PRESET_BOOST + assert climate_thermostat.attributes["hvac_action"] is HVACAction.HEATING + + # Verify service calls + mock_deconz_put_request(aioclient_mock, config_entry.data, "/sensors/0/config") From c88a874063a45d0256b82809d8782f20213de688 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 21 Sep 2022 09:17:20 +0200 Subject: [PATCH 599/955] Cleanup FlowResultType in tests (#78810) --- tests/components/escea/test_config_flow.py | 13 ++++---- .../landisgyr_heat_meter/test_config_flow.py | 30 +++++++++---------- tests/components/pushover/test_config_flow.py | 17 ++++++----- .../components/simplisafe/test_config_flow.py | 11 ++++--- 4 files changed, 36 insertions(+), 35 deletions(-) diff --git a/tests/components/escea/test_config_flow.py b/tests/components/escea/test_config_flow.py index c4a5b323d22..0bb128f717e 100644 --- a/tests/components/escea/test_config_flow.py +++ b/tests/components/escea/test_config_flow.py @@ -5,10 +5,11 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.escea.const import DOMAIN, ESCEA_FIREPLACE from homeassistant.components.escea.discovery import DiscoveryServiceListener from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -58,12 +59,12 @@ async def test_not_found( ) # Confirmation form - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "no_devices_found" assert discovery_service.return_value.close.call_count == 1 @@ -90,12 +91,12 @@ async def test_found( ) # Confirmation form - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert mock_setup.call_count == 1 @@ -112,6 +113,6 @@ async def test_single_instance_allowed(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" assert discovery_service.call_count == 0 diff --git a/tests/components/landisgyr_heat_meter/test_config_flow.py b/tests/components/landisgyr_heat_meter/test_config_flow.py index b51d4493879..9200a9b3d23 100644 --- a/tests/components/landisgyr_heat_meter/test_config_flow.py +++ b/tests/components/landisgyr_heat_meter/test_config_flow.py @@ -7,7 +7,7 @@ import serial.tools.list_ports from homeassistant import config_entries from homeassistant.components.landisgyr_heat_meter import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType def mock_serial_port(): @@ -38,7 +38,7 @@ async def test_manual_entry(mock_heat_meter, hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -46,7 +46,7 @@ async def test_manual_entry(mock_heat_meter, hass: HomeAssistant) -> None: result["flow_id"], {"device": "Enter Manually"} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "setup_serial_manual_path" assert result["errors"] == {} @@ -58,7 +58,7 @@ async def test_manual_entry(mock_heat_meter, hass: HomeAssistant) -> None: result["flow_id"], {"device": "/dev/ttyUSB0"} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "LUGCUH50" assert result["data"] == { "device": "/dev/ttyUSB0", @@ -78,14 +78,14 @@ async def test_list_entry(mock_port, mock_heat_meter, hass: HomeAssistant) -> No result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( result["flow_id"], {"device": port.device} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "LUGCUH50" assert result["data"] == { "device": port.device, @@ -103,7 +103,7 @@ async def test_manual_entry_fail(mock_heat_meter, hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -111,7 +111,7 @@ async def test_manual_entry_fail(mock_heat_meter, hass: HomeAssistant) -> None: result["flow_id"], {"device": "Enter Manually"} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "setup_serial_manual_path" assert result["errors"] == {} @@ -123,7 +123,7 @@ async def test_manual_entry_fail(mock_heat_meter, hass: HomeAssistant) -> None: result["flow_id"], {"device": "/dev/ttyUSB0"} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "setup_serial_manual_path" assert result["errors"] == {"base": "cannot_connect"} @@ -139,14 +139,14 @@ async def test_list_entry_fail(mock_port, mock_heat_meter, hass: HomeAssistant) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( result["flow_id"], {"device": port.device} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -164,7 +164,7 @@ async def test_get_serial_by_id_realpath( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -181,7 +181,7 @@ async def test_get_serial_by_id_realpath( result = await hass.config_entries.flow.async_configure( result["flow_id"], {"device": port.device} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "LUGCUH50" assert result["data"] == { "device": port.device, @@ -203,7 +203,7 @@ async def test_get_serial_by_id_dev_path( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -218,7 +218,7 @@ async def test_get_serial_by_id_dev_path( result = await hass.config_entries.flow.async_configure( result["flow_id"], {"device": port.device} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "LUGCUH50" assert result["data"] == { "device": port.device, diff --git a/tests/components/pushover/test_config_flow.py b/tests/components/pushover/test_config_flow.py index 68672961a56..21c0bc3ab1e 100644 --- a/tests/components/pushover/test_config_flow.py +++ b/tests/components/pushover/test_config_flow.py @@ -4,10 +4,11 @@ from unittest.mock import MagicMock, patch from pushover_complete import BadAPIRequestError import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.pushover.const import CONF_USER_KEY, DOMAIN from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import MOCK_CONFIG @@ -42,7 +43,7 @@ async def test_flow_user(hass: HomeAssistant) -> None: result["flow_id"], user_input=MOCK_CONFIG, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "Pushover" assert result["data"] == MOCK_CONFIG @@ -65,7 +66,7 @@ async def test_flow_user_key_already_configured(hass: HomeAssistant) -> None: result["flow_id"], user_input=MOCK_CONFIG, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -90,7 +91,7 @@ async def test_flow_name_already_configured(hass: HomeAssistant) -> None: result["flow_id"], user_input=new_config, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -105,7 +106,7 @@ async def test_flow_invalid_user_key( context={"source": config_entries.SOURCE_USER}, data=MOCK_CONFIG, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {CONF_USER_KEY: "invalid_user_key"} @@ -121,7 +122,7 @@ async def test_flow_invalid_api_key( context={"source": config_entries.SOURCE_USER}, data=MOCK_CONFIG, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {CONF_API_KEY: "invalid_api_key"} @@ -135,7 +136,7 @@ async def test_flow_conn_err(hass: HomeAssistant, mock_pushover: MagicMock) -> N context={"source": config_entries.SOURCE_USER}, data=MOCK_CONFIG, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -148,7 +149,7 @@ async def test_import(hass: HomeAssistant) -> None: data=MOCK_CONFIG, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "Pushover" assert result["data"] == MOCK_CONFIG diff --git a/tests/components/simplisafe/test_config_flow.py b/tests/components/simplisafe/test_config_flow.py index fe803ba187e..d971b841943 100644 --- a/tests/components/simplisafe/test_config_flow.py +++ b/tests/components/simplisafe/test_config_flow.py @@ -5,7 +5,6 @@ from unittest.mock import patch import pytest from simplipy.errors import InvalidCredentialsError, SimplipyError -from homeassistant import data_entry_flow from homeassistant.components.simplisafe import DOMAIN from homeassistant.components.simplisafe.config_flow import CONF_AUTH_CODE from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER @@ -29,7 +28,7 @@ async def test_duplicate_error(config_entry, hass, setup_simplisafe): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_AUTH_CODE: VALID_AUTH_CODE} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -83,7 +82,7 @@ async def test_options_flow(config_entry, hass): result["flow_id"], user_input={CONF_CODE: "4321"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert config_entry.options == {CONF_CODE: "4321"} @@ -102,7 +101,7 @@ async def test_step_reauth(config_entry, hass, setup_simplisafe): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_AUTH_CODE: VALID_AUTH_CODE} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries()) == 1 @@ -126,7 +125,7 @@ async def test_step_reauth_wrong_account(config_entry, hass, setup_simplisafe): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_AUTH_CODE: VALID_AUTH_CODE} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "wrong_account" @@ -158,7 +157,7 @@ async def test_step_user(auth_code, caplog, hass, log_statement, setup_simplisaf result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_AUTH_CODE: auth_code} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY if log_statement: assert any(m for m in caplog.messages if log_statement in m) From 0ac581a0b1fa438a53f048adfab9b787884a63f6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 21 Sep 2022 10:48:55 +0200 Subject: [PATCH 600/955] Cleanup EntityFeature in tests (#78859) --- tests/components/blebox/test_cover.py | 33 ++-- tests/components/cast/test_media_player.py | 152 +++++++++--------- tests/components/cover/test_device_action.py | 54 +++---- .../components/cover/test_device_condition.py | 46 ++++-- tests/components/cover/test_device_trigger.py | 21 +-- tests/components/directv/test_media_player.py | 37 ++--- tests/components/ecobee/test_humidifier.py | 6 +- tests/components/gogogate2/test_cover.py | 7 +- tests/components/group/test_fan.py | 14 +- tests/components/group/test_media_player.py | 35 ++-- tests/components/heos/test_media_player.py | 16 +- tests/components/homekit/test_type_covers.py | 64 +++++--- tests/components/homekit/test_type_fans.py | 46 +++--- tests/components/homekit/test_type_remote.py | 16 +- .../homekit/test_type_security_systems.py | 26 ++- .../components/homekit/test_type_switches.py | 20 ++- .../homekit/test_type_thermostats.py | 95 ++++++----- .../specific_devices/test_aqara_gateway.py | 18 +-- .../specific_devices/test_haa_fan.py | 4 +- .../test_homeassistant_bridge.py | 12 +- .../specific_devices/test_mysa_living.py | 4 +- .../test_ryse_smart_bridge.py | 10 +- .../test_simpleconnect_fan.py | 5 +- .../specific_devices/test_velux_gateway.py | 12 +- .../test_vocolinc_flowerbud.py | 4 +- tests/components/mobile_app/test_webhook.py | 10 +- .../risco/test_alarm_control_panel.py | 15 +- tests/components/roku/test_media_player.py | 56 +++---- .../components/samsungtv/test_media_player.py | 5 +- tests/components/sharkiq/test_vacuum.py | 28 ++-- tests/components/smartthings/test_fan.py | 4 +- tests/components/template/test_fan.py | 7 +- tests/components/template/test_light.py | 9 +- tests/components/unifiprotect/test_camera.py | 10 +- tests/components/uvc/test_camera.py | 4 +- tests/components/webostv/test_media_player.py | 35 ++-- tests/components/zwave_js/test_fan.py | 4 +- 37 files changed, 472 insertions(+), 472 deletions(-) diff --git a/tests/components/blebox/test_cover.py b/tests/components/blebox/test_cover.py index 3c77dae562a..86655cfbd0a 100644 --- a/tests/components/blebox/test_cover.py +++ b/tests/components/blebox/test_cover.py @@ -12,11 +12,8 @@ from homeassistant.components.cover import ( STATE_CLOSING, STATE_OPEN, STATE_OPENING, - SUPPORT_CLOSE, - SUPPORT_OPEN, - SUPPORT_SET_POSITION, - SUPPORT_STOP, CoverDeviceClass, + CoverEntityFeature, ) from homeassistant.const import ( ATTR_DEVICE_CLASS, @@ -107,11 +104,11 @@ async def test_init_gatecontroller(gatecontroller, hass, config): assert state.attributes[ATTR_DEVICE_CLASS] == CoverDeviceClass.GATE supported_features = state.attributes[ATTR_SUPPORTED_FEATURES] - assert supported_features & SUPPORT_OPEN - assert supported_features & SUPPORT_CLOSE - assert supported_features & SUPPORT_STOP + assert supported_features & CoverEntityFeature.OPEN + assert supported_features & CoverEntityFeature.CLOSE + assert supported_features & CoverEntityFeature.STOP - assert supported_features & SUPPORT_SET_POSITION + assert supported_features & CoverEntityFeature.SET_POSITION assert ATTR_CURRENT_POSITION not in state.attributes assert state.state == STATE_UNKNOWN @@ -137,11 +134,11 @@ async def test_init_shutterbox(shutterbox, hass, config): assert entry.original_device_class == CoverDeviceClass.SHUTTER supported_features = state.attributes[ATTR_SUPPORTED_FEATURES] - assert supported_features & SUPPORT_OPEN - assert supported_features & SUPPORT_CLOSE - assert supported_features & SUPPORT_STOP + assert supported_features & CoverEntityFeature.OPEN + assert supported_features & CoverEntityFeature.CLOSE + assert supported_features & CoverEntityFeature.STOP - assert supported_features & SUPPORT_SET_POSITION + assert supported_features & CoverEntityFeature.SET_POSITION assert ATTR_CURRENT_POSITION not in state.attributes assert state.state == STATE_UNKNOWN @@ -167,13 +164,13 @@ async def test_init_gatebox(gatebox, hass, config): assert state.attributes[ATTR_DEVICE_CLASS] == CoverDeviceClass.DOOR supported_features = state.attributes[ATTR_SUPPORTED_FEATURES] - assert supported_features & SUPPORT_OPEN - assert supported_features & SUPPORT_CLOSE + assert supported_features & CoverEntityFeature.OPEN + assert supported_features & CoverEntityFeature.CLOSE # Not available during init since requires fetching state to detect - assert not supported_features & SUPPORT_STOP + assert not supported_features & CoverEntityFeature.STOP - assert not supported_features & SUPPORT_SET_POSITION + assert not supported_features & CoverEntityFeature.SET_POSITION assert ATTR_CURRENT_POSITION not in state.attributes assert state.state == STATE_UNKNOWN @@ -350,7 +347,7 @@ async def test_with_stop(gatebox, hass, config): state = hass.states.get(entity_id) supported_features = state.attributes[ATTR_SUPPORTED_FEATURES] - assert supported_features & SUPPORT_STOP + assert supported_features & CoverEntityFeature.STOP async def test_with_no_stop(gatebox, hass, config): @@ -364,7 +361,7 @@ async def test_with_no_stop(gatebox, hass, config): state = hass.states.get(entity_id) supported_features = state.attributes[ATTR_SUPPORTED_FEATURES] - assert not supported_features & SUPPORT_STOP + assert not supported_features & CoverEntityFeature.STOP @pytest.mark.parametrize("feature", ALL_COVER_FIXTURES, indirect=["feature"]) diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index a983a51e99d..4df11e49ad5 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -17,19 +17,9 @@ from homeassistant.components import media_player, tts from homeassistant.components.cast import media_player as cast from homeassistant.components.cast.media_player import ChromecastInfo from homeassistant.components.media_player import ( - SUPPORT_NEXT_TRACK, - SUPPORT_PAUSE, - SUPPORT_PLAY, - SUPPORT_PLAY_MEDIA, - SUPPORT_PREVIOUS_TRACK, - SUPPORT_SEEK, - SUPPORT_STOP, - SUPPORT_TURN_OFF, - SUPPORT_TURN_ON, - SUPPORT_VOLUME_MUTE, - SUPPORT_VOLUME_SET, BrowseMedia, MediaClass, + MediaPlayerEntityFeature, ) from homeassistant.config import async_process_ha_core_config from homeassistant.const import ( @@ -840,11 +830,11 @@ async def test_entity_cast_status(hass: HomeAssistant): # No media status, pause, play, stop not supported assert state.attributes.get("supported_features") == ( - SUPPORT_PLAY_MEDIA - | SUPPORT_TURN_OFF - | SUPPORT_TURN_ON - | SUPPORT_VOLUME_MUTE - | SUPPORT_VOLUME_SET + MediaPlayerEntityFeature.PLAY_MEDIA + | MediaPlayerEntityFeature.TURN_OFF + | MediaPlayerEntityFeature.TURN_ON + | MediaPlayerEntityFeature.VOLUME_MUTE + | MediaPlayerEntityFeature.VOLUME_SET ) cast_status = MagicMock() @@ -883,7 +873,9 @@ async def test_entity_cast_status(hass: HomeAssistant): await hass.async_block_till_done() state = hass.states.get(entity_id) assert state.attributes.get("supported_features") == ( - SUPPORT_PLAY_MEDIA | SUPPORT_TURN_OFF | SUPPORT_TURN_ON + MediaPlayerEntityFeature.PLAY_MEDIA + | MediaPlayerEntityFeature.TURN_OFF + | MediaPlayerEntityFeature.TURN_ON ) @@ -892,51 +884,51 @@ async def test_entity_cast_status(hass: HomeAssistant): [ ( pychromecast.const.CAST_TYPE_AUDIO, - SUPPORT_PAUSE - | SUPPORT_PLAY - | SUPPORT_PLAY_MEDIA - | SUPPORT_STOP - | SUPPORT_TURN_OFF - | SUPPORT_TURN_ON - | SUPPORT_VOLUME_MUTE - | SUPPORT_VOLUME_SET, - SUPPORT_PLAY_MEDIA - | SUPPORT_TURN_OFF - | SUPPORT_TURN_ON - | SUPPORT_VOLUME_MUTE - | SUPPORT_VOLUME_SET, + MediaPlayerEntityFeature.PAUSE + | MediaPlayerEntityFeature.PLAY + | MediaPlayerEntityFeature.PLAY_MEDIA + | MediaPlayerEntityFeature.STOP + | MediaPlayerEntityFeature.TURN_OFF + | MediaPlayerEntityFeature.TURN_ON + | MediaPlayerEntityFeature.VOLUME_MUTE + | MediaPlayerEntityFeature.VOLUME_SET, + MediaPlayerEntityFeature.PLAY_MEDIA + | MediaPlayerEntityFeature.TURN_OFF + | MediaPlayerEntityFeature.TURN_ON + | MediaPlayerEntityFeature.VOLUME_MUTE + | MediaPlayerEntityFeature.VOLUME_SET, ), ( pychromecast.const.CAST_TYPE_CHROMECAST, - SUPPORT_PAUSE - | SUPPORT_PLAY - | SUPPORT_PLAY_MEDIA - | SUPPORT_STOP - | SUPPORT_TURN_OFF - | SUPPORT_TURN_ON - | SUPPORT_VOLUME_MUTE - | SUPPORT_VOLUME_SET, - SUPPORT_PLAY_MEDIA - | SUPPORT_TURN_OFF - | SUPPORT_TURN_ON - | SUPPORT_VOLUME_MUTE - | SUPPORT_VOLUME_SET, + MediaPlayerEntityFeature.PAUSE + | MediaPlayerEntityFeature.PLAY + | MediaPlayerEntityFeature.PLAY_MEDIA + | MediaPlayerEntityFeature.STOP + | MediaPlayerEntityFeature.TURN_OFF + | MediaPlayerEntityFeature.TURN_ON + | MediaPlayerEntityFeature.VOLUME_MUTE + | MediaPlayerEntityFeature.VOLUME_SET, + MediaPlayerEntityFeature.PLAY_MEDIA + | MediaPlayerEntityFeature.TURN_OFF + | MediaPlayerEntityFeature.TURN_ON + | MediaPlayerEntityFeature.VOLUME_MUTE + | MediaPlayerEntityFeature.VOLUME_SET, ), ( pychromecast.const.CAST_TYPE_GROUP, - SUPPORT_PAUSE - | SUPPORT_PLAY - | SUPPORT_PLAY_MEDIA - | SUPPORT_STOP - | SUPPORT_TURN_OFF - | SUPPORT_TURN_ON - | SUPPORT_VOLUME_MUTE - | SUPPORT_VOLUME_SET, - SUPPORT_PLAY_MEDIA - | SUPPORT_TURN_OFF - | SUPPORT_TURN_ON - | SUPPORT_VOLUME_MUTE - | SUPPORT_VOLUME_SET, + MediaPlayerEntityFeature.PAUSE + | MediaPlayerEntityFeature.PLAY + | MediaPlayerEntityFeature.PLAY_MEDIA + | MediaPlayerEntityFeature.STOP + | MediaPlayerEntityFeature.TURN_OFF + | MediaPlayerEntityFeature.TURN_ON + | MediaPlayerEntityFeature.VOLUME_MUTE + | MediaPlayerEntityFeature.VOLUME_SET, + MediaPlayerEntityFeature.PLAY_MEDIA + | MediaPlayerEntityFeature.TURN_OFF + | MediaPlayerEntityFeature.TURN_ON + | MediaPlayerEntityFeature.VOLUME_MUTE + | MediaPlayerEntityFeature.VOLUME_SET, ), ], ) @@ -1394,14 +1386,14 @@ async def test_entity_control(hass: HomeAssistant, quick_play_mock): assert entity_id == reg.async_get_entity_id("media_player", "cast", str(info.uuid)) assert state.attributes.get("supported_features") == ( - SUPPORT_PAUSE - | SUPPORT_PLAY - | SUPPORT_PLAY_MEDIA - | SUPPORT_STOP - | SUPPORT_TURN_OFF - | SUPPORT_TURN_ON - | SUPPORT_VOLUME_MUTE - | SUPPORT_VOLUME_SET + MediaPlayerEntityFeature.PAUSE + | MediaPlayerEntityFeature.PLAY + | MediaPlayerEntityFeature.PLAY_MEDIA + | MediaPlayerEntityFeature.STOP + | MediaPlayerEntityFeature.TURN_OFF + | MediaPlayerEntityFeature.TURN_ON + | MediaPlayerEntityFeature.VOLUME_MUTE + | MediaPlayerEntityFeature.VOLUME_SET ) # Turn on @@ -1460,17 +1452,17 @@ async def test_entity_control(hass: HomeAssistant, quick_play_mock): state = hass.states.get(entity_id) assert state.attributes.get("supported_features") == ( - SUPPORT_PAUSE - | SUPPORT_PLAY - | SUPPORT_PLAY_MEDIA - | SUPPORT_STOP - | SUPPORT_TURN_OFF - | SUPPORT_TURN_ON - | SUPPORT_PREVIOUS_TRACK - | SUPPORT_NEXT_TRACK - | SUPPORT_SEEK - | SUPPORT_VOLUME_MUTE - | SUPPORT_VOLUME_SET + MediaPlayerEntityFeature.PAUSE + | MediaPlayerEntityFeature.PLAY + | MediaPlayerEntityFeature.PLAY_MEDIA + | MediaPlayerEntityFeature.STOP + | MediaPlayerEntityFeature.TURN_OFF + | MediaPlayerEntityFeature.TURN_ON + | MediaPlayerEntityFeature.PREVIOUS_TRACK + | MediaPlayerEntityFeature.NEXT_TRACK + | MediaPlayerEntityFeature.SEEK + | MediaPlayerEntityFeature.VOLUME_MUTE + | MediaPlayerEntityFeature.VOLUME_SET ) # Media previous @@ -1598,11 +1590,11 @@ async def test_entity_media_states_lovelace_app(hass: HomeAssistant): state = hass.states.get(entity_id) assert state.state == "playing" assert state.attributes.get("supported_features") == ( - SUPPORT_PLAY_MEDIA - | SUPPORT_TURN_OFF - | SUPPORT_TURN_ON - | SUPPORT_VOLUME_MUTE - | SUPPORT_VOLUME_SET + MediaPlayerEntityFeature.PLAY_MEDIA + | MediaPlayerEntityFeature.TURN_OFF + | MediaPlayerEntityFeature.TURN_ON + | MediaPlayerEntityFeature.VOLUME_MUTE + | MediaPlayerEntityFeature.VOLUME_SET ) media_status = MagicMock(images=None) diff --git a/tests/components/cover/test_device_action.py b/tests/components/cover/test_device_action.py index 8bbe42e9537..bdf10f7f9a2 100644 --- a/tests/components/cover/test_device_action.py +++ b/tests/components/cover/test_device_action.py @@ -2,17 +2,7 @@ import pytest import homeassistant.components.automation as automation -from homeassistant.components.cover import ( - DOMAIN, - SUPPORT_CLOSE, - SUPPORT_CLOSE_TILT, - SUPPORT_OPEN, - SUPPORT_OPEN_TILT, - SUPPORT_SET_POSITION, - SUPPORT_SET_TILT_POSITION, - SUPPORT_STOP, - SUPPORT_STOP_TILT, -) +from homeassistant.components.cover import DOMAIN, CoverEntityFeature from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.const import CONF_PLATFORM from homeassistant.helpers import device_registry @@ -48,21 +38,21 @@ def entity_reg(hass): "set_state,features_reg,features_state,expected_action_types", [ (False, 0, 0, []), - (False, SUPPORT_CLOSE_TILT, 0, ["close_tilt"]), - (False, SUPPORT_CLOSE, 0, ["close"]), - (False, SUPPORT_OPEN_TILT, 0, ["open_tilt"]), - (False, SUPPORT_OPEN, 0, ["open"]), - (False, SUPPORT_SET_POSITION, 0, ["set_position"]), - (False, SUPPORT_SET_TILT_POSITION, 0, ["set_tilt_position"]), - (False, SUPPORT_STOP, 0, ["stop"]), + (False, CoverEntityFeature.CLOSE_TILT, 0, ["close_tilt"]), + (False, CoverEntityFeature.CLOSE, 0, ["close"]), + (False, CoverEntityFeature.OPEN_TILT, 0, ["open_tilt"]), + (False, CoverEntityFeature.OPEN, 0, ["open"]), + (False, CoverEntityFeature.SET_POSITION, 0, ["set_position"]), + (False, CoverEntityFeature.SET_TILT_POSITION, 0, ["set_tilt_position"]), + (False, CoverEntityFeature.STOP, 0, ["stop"]), (True, 0, 0, []), - (True, 0, SUPPORT_CLOSE_TILT, ["close_tilt"]), - (True, 0, SUPPORT_CLOSE, ["close"]), - (True, 0, SUPPORT_OPEN_TILT, ["open_tilt"]), - (True, 0, SUPPORT_OPEN, ["open"]), - (True, 0, SUPPORT_SET_POSITION, ["set_position"]), - (True, 0, SUPPORT_SET_TILT_POSITION, ["set_tilt_position"]), - (True, 0, SUPPORT_STOP, ["stop"]), + (True, 0, CoverEntityFeature.CLOSE_TILT, ["close_tilt"]), + (True, 0, CoverEntityFeature.CLOSE, ["close"]), + (True, 0, CoverEntityFeature.OPEN_TILT, ["open_tilt"]), + (True, 0, CoverEntityFeature.OPEN, ["open"]), + (True, 0, CoverEntityFeature.SET_POSITION, ["set_position"]), + (True, 0, CoverEntityFeature.SET_TILT_POSITION, ["set_tilt_position"]), + (True, 0, CoverEntityFeature.STOP, ["stop"]), ], ) async def test_get_actions( @@ -141,7 +131,7 @@ async def test_get_actions_hidden_auxiliary( device_id=device_entry.id, entity_category=entity_category, hidden_by=hidden_by, - supported_features=SUPPORT_CLOSE, + supported_features=CoverEntityFeature.CLOSE, ) expected_actions = [] expected_actions += [ @@ -172,12 +162,12 @@ async def test_get_action_capabilities( is_on=True, unique_id="unique_set_pos_cover", current_cover_position=50, - supported_features=SUPPORT_OPEN - | SUPPORT_CLOSE - | SUPPORT_STOP - | SUPPORT_OPEN_TILT - | SUPPORT_CLOSE_TILT - | SUPPORT_STOP_TILT, + supported_features=CoverEntityFeature.OPEN + | CoverEntityFeature.CLOSE + | CoverEntityFeature.STOP + | CoverEntityFeature.OPEN_TILT + | CoverEntityFeature.CLOSE_TILT + | CoverEntityFeature.STOP_TILT, ), ) ent = platform.ENTITIES[0] diff --git a/tests/components/cover/test_device_condition.py b/tests/components/cover/test_device_condition.py index d387a492a9d..53be60fccd4 100644 --- a/tests/components/cover/test_device_condition.py +++ b/tests/components/cover/test_device_condition.py @@ -2,13 +2,7 @@ import pytest import homeassistant.components.automation as automation -from homeassistant.components.cover import ( - DOMAIN, - SUPPORT_CLOSE, - SUPPORT_OPEN, - SUPPORT_SET_POSITION, - SUPPORT_SET_TILT_POSITION, -) +from homeassistant.components.cover import DOMAIN, CoverEntityFeature from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.const import ( CONF_PLATFORM, @@ -57,15 +51,35 @@ def calls(hass): "set_state,features_reg,features_state,expected_condition_types", [ (False, 0, 0, []), - (False, SUPPORT_CLOSE, 0, ["is_open", "is_closed", "is_opening", "is_closing"]), - (False, SUPPORT_OPEN, 0, ["is_open", "is_closed", "is_opening", "is_closing"]), - (False, SUPPORT_SET_POSITION, 0, ["is_position"]), - (False, SUPPORT_SET_TILT_POSITION, 0, ["is_tilt_position"]), + ( + False, + CoverEntityFeature.CLOSE, + 0, + ["is_open", "is_closed", "is_opening", "is_closing"], + ), + ( + False, + CoverEntityFeature.OPEN, + 0, + ["is_open", "is_closed", "is_opening", "is_closing"], + ), + (False, CoverEntityFeature.SET_POSITION, 0, ["is_position"]), + (False, CoverEntityFeature.SET_TILT_POSITION, 0, ["is_tilt_position"]), (True, 0, 0, []), - (True, 0, SUPPORT_CLOSE, ["is_open", "is_closed", "is_opening", "is_closing"]), - (True, 0, SUPPORT_OPEN, ["is_open", "is_closed", "is_opening", "is_closing"]), - (True, 0, SUPPORT_SET_POSITION, ["is_position"]), - (True, 0, SUPPORT_SET_TILT_POSITION, ["is_tilt_position"]), + ( + True, + 0, + CoverEntityFeature.CLOSE, + ["is_open", "is_closed", "is_opening", "is_closing"], + ), + ( + True, + 0, + CoverEntityFeature.OPEN, + ["is_open", "is_closed", "is_opening", "is_closing"], + ), + (True, 0, CoverEntityFeature.SET_POSITION, ["is_position"]), + (True, 0, CoverEntityFeature.SET_TILT_POSITION, ["is_tilt_position"]), ], ) async def test_get_conditions( @@ -145,7 +159,7 @@ async def test_get_conditions_hidden_auxiliary( device_id=device_entry.id, entity_category=entity_category, hidden_by=hidden_by, - supported_features=SUPPORT_CLOSE, + supported_features=CoverEntityFeature.CLOSE, ) expected_conditions = [ { diff --git a/tests/components/cover/test_device_trigger.py b/tests/components/cover/test_device_trigger.py index 1d75e996335..ebd48951853 100644 --- a/tests/components/cover/test_device_trigger.py +++ b/tests/components/cover/test_device_trigger.py @@ -4,12 +4,7 @@ from datetime import timedelta import pytest import homeassistant.components.automation as automation -from homeassistant.components.cover import ( - DOMAIN, - SUPPORT_OPEN, - SUPPORT_SET_POSITION, - SUPPORT_SET_TILT_POSITION, -) +from homeassistant.components.cover import DOMAIN, CoverEntityFeature from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.const import ( CONF_PLATFORM, @@ -58,30 +53,30 @@ def calls(hass): @pytest.mark.parametrize( "set_state,features_reg,features_state,expected_trigger_types", [ - (False, SUPPORT_OPEN, 0, ["opened", "closed", "opening", "closing"]), + (False, CoverEntityFeature.OPEN, 0, ["opened", "closed", "opening", "closing"]), ( False, - SUPPORT_OPEN | SUPPORT_SET_POSITION, + CoverEntityFeature.OPEN | CoverEntityFeature.SET_POSITION, 0, ["opened", "closed", "opening", "closing", "position"], ), ( False, - SUPPORT_OPEN | SUPPORT_SET_TILT_POSITION, + CoverEntityFeature.OPEN | CoverEntityFeature.SET_TILT_POSITION, 0, ["opened", "closed", "opening", "closing", "tilt_position"], ), - (True, 0, SUPPORT_OPEN, ["opened", "closed", "opening", "closing"]), + (True, 0, CoverEntityFeature.OPEN, ["opened", "closed", "opening", "closing"]), ( True, 0, - SUPPORT_OPEN | SUPPORT_SET_POSITION, + CoverEntityFeature.OPEN | CoverEntityFeature.SET_POSITION, ["opened", "closed", "opening", "closing", "position"], ), ( True, 0, - SUPPORT_OPEN | SUPPORT_SET_TILT_POSITION, + CoverEntityFeature.OPEN | CoverEntityFeature.SET_TILT_POSITION, ["opened", "closed", "opening", "closing", "tilt_position"], ), ], @@ -165,7 +160,7 @@ async def test_get_triggers_hidden_auxiliary( device_id=device_entry.id, entity_category=entity_category, hidden_by=hidden_by, - supported_features=SUPPORT_OPEN, + supported_features=CoverEntityFeature.OPEN, ) expected_triggers = [ { diff --git a/tests/components/directv/test_media_player.py b/tests/components/directv/test_media_player.py index 84f72a5409e..60ccc49457c 100644 --- a/tests/components/directv/test_media_player.py +++ b/tests/components/directv/test_media_player.py @@ -27,15 +27,8 @@ from homeassistant.components.media_player import ( ATTR_MEDIA_TITLE, DOMAIN as MP_DOMAIN, SERVICE_PLAY_MEDIA, - SUPPORT_NEXT_TRACK, - SUPPORT_PAUSE, - SUPPORT_PLAY, - SUPPORT_PLAY_MEDIA, - SUPPORT_PREVIOUS_TRACK, - SUPPORT_STOP, - SUPPORT_TURN_OFF, - SUPPORT_TURN_ON, MediaPlayerDeviceClass, + MediaPlayerEntityFeature, MediaType, ) from homeassistant.const import ( @@ -180,26 +173,26 @@ async def test_supported_features( # Features supported for main DVR state = hass.states.get(MAIN_ENTITY_ID) assert ( - SUPPORT_PAUSE - | SUPPORT_TURN_ON - | SUPPORT_TURN_OFF - | SUPPORT_PLAY_MEDIA - | SUPPORT_STOP - | SUPPORT_NEXT_TRACK - | SUPPORT_PREVIOUS_TRACK - | SUPPORT_PLAY + MediaPlayerEntityFeature.PAUSE + | MediaPlayerEntityFeature.TURN_ON + | MediaPlayerEntityFeature.TURN_OFF + | MediaPlayerEntityFeature.PLAY_MEDIA + | MediaPlayerEntityFeature.STOP + | MediaPlayerEntityFeature.NEXT_TRACK + | MediaPlayerEntityFeature.PREVIOUS_TRACK + | MediaPlayerEntityFeature.PLAY == state.attributes.get("supported_features") ) # Feature supported for clients. state = hass.states.get(CLIENT_ENTITY_ID) assert ( - SUPPORT_PAUSE - | SUPPORT_PLAY_MEDIA - | SUPPORT_STOP - | SUPPORT_NEXT_TRACK - | SUPPORT_PREVIOUS_TRACK - | SUPPORT_PLAY + MediaPlayerEntityFeature.PAUSE + | MediaPlayerEntityFeature.PLAY_MEDIA + | MediaPlayerEntityFeature.STOP + | MediaPlayerEntityFeature.NEXT_TRACK + | MediaPlayerEntityFeature.PREVIOUS_TRACK + | MediaPlayerEntityFeature.PLAY == state.attributes.get("supported_features") ) diff --git a/tests/components/ecobee/test_humidifier.py b/tests/components/ecobee/test_humidifier.py index e8a370455f7..3b69a6629fe 100644 --- a/tests/components/ecobee/test_humidifier.py +++ b/tests/components/ecobee/test_humidifier.py @@ -15,8 +15,8 @@ from homeassistant.components.humidifier import ( MODE_AUTO, SERVICE_SET_HUMIDITY, SERVICE_SET_MODE, - SUPPORT_MODES, HumidifierDeviceClass, + HumidifierEntityFeature, ) from homeassistant.const import ( ATTR_DEVICE_CLASS, @@ -51,7 +51,9 @@ async def test_attributes(hass): ] assert state.attributes.get(ATTR_FRIENDLY_NAME) == "ecobee" assert state.attributes.get(ATTR_DEVICE_CLASS) == HumidifierDeviceClass.HUMIDIFIER - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == SUPPORT_MODES + assert ( + state.attributes.get(ATTR_SUPPORTED_FEATURES) == HumidifierEntityFeature.MODES + ) async def test_turn_on(hass): diff --git a/tests/components/gogogate2/test_cover.py b/tests/components/gogogate2/test_cover.py index 9f36dbc9008..8a3d647c3b4 100644 --- a/tests/components/gogogate2/test_cover.py +++ b/tests/components/gogogate2/test_cover.py @@ -17,9 +17,8 @@ from ismartgate.common import ( from homeassistant.components.cover import ( DOMAIN as COVER_DOMAIN, - SUPPORT_CLOSE, - SUPPORT_OPEN, CoverDeviceClass, + CoverEntityFeature, ) from homeassistant.components.gogogate2.const import ( DEVICE_TYPE_GOGOGATE2, @@ -117,7 +116,7 @@ async def test_open_close_update(gogogate2api_mock, hass: HomeAssistant) -> None "device_class": "garage", "door_id": 1, "friendly_name": "Door1", - "supported_features": SUPPORT_CLOSE | SUPPORT_OPEN, + "supported_features": CoverEntityFeature.CLOSE | CoverEntityFeature.OPEN, } api = MagicMock(GogoGate2Api) @@ -256,7 +255,7 @@ async def test_availability(ismartgateapi_mock, hass: HomeAssistant) -> None: "device_class": "garage", "door_id": 1, "friendly_name": "Door1", - "supported_features": SUPPORT_CLOSE | SUPPORT_OPEN, + "supported_features": CoverEntityFeature.CLOSE | CoverEntityFeature.OPEN, } api = MagicMock(ISmartGateApi) diff --git a/tests/components/group/test_fan.py b/tests/components/group/test_fan.py index b8e47bfb289..1717da1d121 100644 --- a/tests/components/group/test_fan.py +++ b/tests/components/group/test_fan.py @@ -18,9 +18,7 @@ from homeassistant.components.fan import ( SERVICE_SET_PERCENTAGE, SERVICE_TURN_OFF, SERVICE_TURN_ON, - SUPPORT_DIRECTION, - SUPPORT_OSCILLATE, - SUPPORT_SET_SPEED, + FanEntityFeature, ) from homeassistant.components.group import SERVICE_RELOAD from homeassistant.components.group.fan import DEFAULT_NAME @@ -54,7 +52,9 @@ FULL_FAN_ENTITY_IDS = [LIVING_ROOM_FAN_ENTITY_ID, PERCENTAGE_FULL_FAN_ENTITY_ID] LIMITED_FAN_ENTITY_IDS = [CEILING_FAN_ENTITY_ID, PERCENTAGE_LIMITED_FAN_ENTITY_ID] -FULL_SUPPORT_FEATURES = SUPPORT_SET_SPEED | SUPPORT_DIRECTION | SUPPORT_OSCILLATE +FULL_SUPPORT_FEATURES = ( + FanEntityFeature.SET_SPEED | FanEntityFeature.DIRECTION | FanEntityFeature.OSCILLATE +) CONFIG_MISSING_FAN = { @@ -233,7 +233,7 @@ async def test_attributes(hass, setup_comp): CEILING_FAN_ENTITY_ID, STATE_ON, { - ATTR_SUPPORTED_FEATURES: SUPPORT_SET_SPEED, + ATTR_SUPPORTED_FEATURES: FanEntityFeature.SET_SPEED, ATTR_PERCENTAGE: 50, }, ) @@ -242,7 +242,7 @@ async def test_attributes(hass, setup_comp): state = hass.states.get(FAN_GROUP) assert state.state == STATE_ON assert ATTR_ASSUMED_STATE not in state.attributes - assert state.attributes[ATTR_SUPPORTED_FEATURES] == SUPPORT_SET_SPEED + assert state.attributes[ATTR_SUPPORTED_FEATURES] == FanEntityFeature.SET_SPEED assert ATTR_PERCENTAGE in state.attributes assert state.attributes[ATTR_PERCENTAGE] == 50 assert ATTR_ASSUMED_STATE not in state.attributes @@ -256,7 +256,7 @@ async def test_attributes(hass, setup_comp): PERCENTAGE_LIMITED_FAN_ENTITY_ID, STATE_ON, { - ATTR_SUPPORTED_FEATURES: SUPPORT_SET_SPEED, + ATTR_SUPPORTED_FEATURES: FanEntityFeature.SET_SPEED, ATTR_PERCENTAGE: 75, }, ) diff --git a/tests/components/group/test_media_player.py b/tests/components/group/test_media_player.py index 5d0d52ae3ac..42bc96dbc6e 100644 --- a/tests/components/group/test_media_player.py +++ b/tests/components/group/test_media_player.py @@ -6,11 +6,15 @@ import pytest from homeassistant.components.group import DOMAIN from homeassistant.components.media_player import ( + ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_SEEK_POSITION, ATTR_MEDIA_SHUFFLE, + ATTR_MEDIA_TRACK, ATTR_MEDIA_VOLUME_LEVEL, + ATTR_MEDIA_VOLUME_MUTED, DOMAIN as MEDIA_DOMAIN, + SERVICE_CLEAR_PLAYLIST, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_SEEK, SERVICE_PLAY_MEDIA, @@ -18,20 +22,7 @@ from homeassistant.components.media_player import ( SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_VOLUME_SET, - SUPPORT_PAUSE, - SUPPORT_PLAY, - SUPPORT_PLAY_MEDIA, - SUPPORT_SEEK, - SUPPORT_STOP, - SUPPORT_VOLUME_MUTE, - SUPPORT_VOLUME_SET, - SUPPORT_VOLUME_STEP, -) -from homeassistant.components.media_player.const import ( - ATTR_MEDIA_CONTENT_ID, - ATTR_MEDIA_TRACK, - ATTR_MEDIA_VOLUME_MUTED, - SERVICE_CLEAR_PLAYLIST, + MediaPlayerEntityFeature, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -188,9 +179,17 @@ async def test_state_reporting(hass): async def test_supported_features(hass): """Test supported features reporting.""" - pause_play_stop = SUPPORT_PAUSE | SUPPORT_PLAY | SUPPORT_STOP - play_media = SUPPORT_PLAY_MEDIA - volume = SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_STEP + pause_play_stop = ( + MediaPlayerEntityFeature.PAUSE + | MediaPlayerEntityFeature.PLAY + | MediaPlayerEntityFeature.STOP + ) + play_media = MediaPlayerEntityFeature.PLAY_MEDIA + volume = ( + MediaPlayerEntityFeature.VOLUME_MUTE + | MediaPlayerEntityFeature.VOLUME_SET + | MediaPlayerEntityFeature.VOLUME_STEP + ) await async_setup_component( hass, @@ -352,7 +351,7 @@ async def test_service_calls(hass, mock_media_seek): ) state = hass.states.get("media_player.media_group") - assert state.attributes[ATTR_SUPPORTED_FEATURES] & SUPPORT_SEEK + assert state.attributes[ATTR_SUPPORTED_FEATURES] & MediaPlayerEntityFeature.SEEK assert not mock_media_seek.called await hass.services.async_call( diff --git a/tests/components/heos/test_media_player.py b/tests/components/heos/test_media_player.py index 5e43d6989a8..805fb07b403 100644 --- a/tests/components/heos/test_media_player.py +++ b/tests/components/heos/test_media_player.py @@ -32,11 +32,7 @@ from homeassistant.components.media_player import ( SERVICE_PLAY_MEDIA, SERVICE_SELECT_SOURCE, SERVICE_UNJOIN, - SUPPORT_NEXT_TRACK, - SUPPORT_PAUSE, - SUPPORT_PLAY, - SUPPORT_PREVIOUS_TRACK, - SUPPORT_STOP, + MediaPlayerEntityFeature, MediaType, ) from homeassistant.const import ( @@ -90,11 +86,11 @@ async def test_state_attributes(hass, config_entry, config, controller): assert state.attributes[ATTR_FRIENDLY_NAME] == "Test Player" assert ( state.attributes[ATTR_SUPPORTED_FEATURES] - == SUPPORT_PLAY - | SUPPORT_PAUSE - | SUPPORT_STOP - | SUPPORT_NEXT_TRACK - | SUPPORT_PREVIOUS_TRACK + == MediaPlayerEntityFeature.PLAY + | MediaPlayerEntityFeature.PAUSE + | MediaPlayerEntityFeature.STOP + | MediaPlayerEntityFeature.NEXT_TRACK + | MediaPlayerEntityFeature.PREVIOUS_TRACK | media_player.BASE_SUPPORTED_FEATURES ) assert ATTR_INPUT_SOURCE not in state.attributes diff --git a/tests/components/homekit/test_type_covers.py b/tests/components/homekit/test_type_covers.py index 8f26967c160..bc512a4b162 100644 --- a/tests/components/homekit/test_type_covers.py +++ b/tests/components/homekit/test_type_covers.py @@ -6,9 +6,7 @@ from homeassistant.components.cover import ( ATTR_POSITION, ATTR_TILT_POSITION, DOMAIN, - SUPPORT_SET_POSITION, - SUPPORT_SET_TILT_POSITION, - SUPPORT_STOP, + CoverEntityFeature, ) from homeassistant.components.homekit.const import ( ATTR_OBSTRUCTION_DETECTED, @@ -136,7 +134,9 @@ async def test_windowcovering_set_cover_position(hass, hk_driver, events): entity_id = "cover.window" hass.states.async_set( - entity_id, STATE_UNKNOWN, {ATTR_SUPPORTED_FEATURES: SUPPORT_SET_POSITION} + entity_id, + STATE_UNKNOWN, + {ATTR_SUPPORTED_FEATURES: CoverEntityFeature.SET_POSITION}, ) await hass.async_block_till_done() acc = WindowCovering(hass, hk_driver, "Cover", entity_id, 2, None) @@ -152,7 +152,10 @@ async def test_windowcovering_set_cover_position(hass, hk_driver, events): hass.states.async_set( entity_id, STATE_UNKNOWN, - {ATTR_SUPPORTED_FEATURES: SUPPORT_SET_POSITION, ATTR_CURRENT_POSITION: None}, + { + ATTR_SUPPORTED_FEATURES: CoverEntityFeature.SET_POSITION, + ATTR_CURRENT_POSITION: None, + }, ) await hass.async_block_till_done() assert acc.char_current_position.value == 0 @@ -162,7 +165,10 @@ async def test_windowcovering_set_cover_position(hass, hk_driver, events): hass.states.async_set( entity_id, STATE_OPENING, - {ATTR_SUPPORTED_FEATURES: SUPPORT_SET_POSITION, ATTR_CURRENT_POSITION: 60}, + { + ATTR_SUPPORTED_FEATURES: CoverEntityFeature.SET_POSITION, + ATTR_CURRENT_POSITION: 60, + }, ) await hass.async_block_till_done() assert acc.char_current_position.value == 60 @@ -172,7 +178,10 @@ async def test_windowcovering_set_cover_position(hass, hk_driver, events): hass.states.async_set( entity_id, STATE_OPENING, - {ATTR_SUPPORTED_FEATURES: SUPPORT_SET_POSITION, ATTR_CURRENT_POSITION: 70.0}, + { + ATTR_SUPPORTED_FEATURES: CoverEntityFeature.SET_POSITION, + ATTR_CURRENT_POSITION: 70.0, + }, ) await hass.async_block_till_done() assert acc.char_current_position.value == 70 @@ -182,7 +191,10 @@ async def test_windowcovering_set_cover_position(hass, hk_driver, events): hass.states.async_set( entity_id, STATE_CLOSING, - {ATTR_SUPPORTED_FEATURES: SUPPORT_SET_POSITION, ATTR_CURRENT_POSITION: 50}, + { + ATTR_SUPPORTED_FEATURES: CoverEntityFeature.SET_POSITION, + ATTR_CURRENT_POSITION: 50, + }, ) await hass.async_block_till_done() assert acc.char_current_position.value == 50 @@ -192,7 +204,10 @@ async def test_windowcovering_set_cover_position(hass, hk_driver, events): hass.states.async_set( entity_id, STATE_OPEN, - {ATTR_SUPPORTED_FEATURES: SUPPORT_SET_POSITION, ATTR_CURRENT_POSITION: 50}, + { + ATTR_SUPPORTED_FEATURES: CoverEntityFeature.SET_POSITION, + ATTR_CURRENT_POSITION: 50, + }, ) await hass.async_block_till_done() assert acc.char_current_position.value == 50 @@ -230,7 +245,10 @@ async def test_window_instantiate_set_position(hass, hk_driver, events): hass.states.async_set( entity_id, STATE_OPEN, - {ATTR_SUPPORTED_FEATURES: SUPPORT_SET_POSITION, ATTR_CURRENT_POSITION: 0}, + { + ATTR_SUPPORTED_FEATURES: CoverEntityFeature.SET_POSITION, + ATTR_CURRENT_POSITION: 0, + }, ) await hass.async_block_till_done() acc = Window(hass, hk_driver, "Window", entity_id, 2, None) @@ -246,7 +264,10 @@ async def test_window_instantiate_set_position(hass, hk_driver, events): hass.states.async_set( entity_id, STATE_OPEN, - {ATTR_SUPPORTED_FEATURES: SUPPORT_SET_POSITION, ATTR_CURRENT_POSITION: 50}, + { + ATTR_SUPPORTED_FEATURES: CoverEntityFeature.SET_POSITION, + ATTR_CURRENT_POSITION: 50, + }, ) await hass.async_block_till_done() assert acc.char_current_position.value == 50 @@ -257,7 +278,7 @@ async def test_window_instantiate_set_position(hass, hk_driver, events): entity_id, STATE_OPEN, { - ATTR_SUPPORTED_FEATURES: SUPPORT_SET_POSITION, + ATTR_SUPPORTED_FEATURES: CoverEntityFeature.SET_POSITION, ATTR_CURRENT_POSITION: "GARBAGE", }, ) @@ -272,7 +293,9 @@ async def test_windowcovering_cover_set_tilt(hass, hk_driver, events): entity_id = "cover.window" hass.states.async_set( - entity_id, STATE_UNKNOWN, {ATTR_SUPPORTED_FEATURES: SUPPORT_SET_TILT_POSITION} + entity_id, + STATE_UNKNOWN, + {ATTR_SUPPORTED_FEATURES: CoverEntityFeature.SET_TILT_POSITION}, ) await hass.async_block_till_done() acc = WindowCovering(hass, hk_driver, "Cover", entity_id, 2, None) @@ -339,7 +362,9 @@ async def test_windowcovering_tilt_only(hass, hk_driver, events): entity_id = "cover.window" hass.states.async_set( - entity_id, STATE_UNKNOWN, {ATTR_SUPPORTED_FEATURES: SUPPORT_SET_TILT_POSITION} + entity_id, + STATE_UNKNOWN, + {ATTR_SUPPORTED_FEATURES: CoverEntityFeature.SET_TILT_POSITION}, ) await hass.async_block_till_done() acc = WindowCovering(hass, hk_driver, "Cover", entity_id, 2, None) @@ -441,7 +466,7 @@ async def test_windowcovering_open_close_stop(hass, hk_driver, events): entity_id = "cover.window" hass.states.async_set( - entity_id, STATE_UNKNOWN, {ATTR_SUPPORTED_FEATURES: SUPPORT_STOP} + entity_id, STATE_UNKNOWN, {ATTR_SUPPORTED_FEATURES: CoverEntityFeature.STOP} ) acc = WindowCoveringBasic(hass, hk_driver, "Cover", entity_id, 2, None) await acc.run() @@ -492,7 +517,10 @@ async def test_windowcovering_open_close_with_position_and_stop( hass.states.async_set( entity_id, STATE_UNKNOWN, - {ATTR_SUPPORTED_FEATURES: SUPPORT_STOP | SUPPORT_SET_POSITION}, + { + ATTR_SUPPORTED_FEATURES: CoverEntityFeature.STOP + | CoverEntityFeature.SET_POSITION + }, ) acc = WindowCovering(hass, hk_driver, "Cover", entity_id, 2, None) await acc.run() @@ -532,7 +560,7 @@ async def test_windowcovering_basic_restore(hass, hk_driver, events): "9012", suggested_object_id="all_info_set", capabilities={}, - supported_features=SUPPORT_STOP, + supported_features=CoverEntityFeature.STOP, original_device_class="mock-device-class", ) @@ -570,7 +598,7 @@ async def test_windowcovering_restore(hass, hk_driver, events): "9012", suggested_object_id="all_info_set", capabilities={}, - supported_features=SUPPORT_STOP, + supported_features=CoverEntityFeature.STOP, original_device_class="mock-device-class", ) diff --git a/tests/components/homekit/test_type_fans.py b/tests/components/homekit/test_type_fans.py index b520eb7f874..fbcf6a00421 100644 --- a/tests/components/homekit/test_type_fans.py +++ b/tests/components/homekit/test_type_fans.py @@ -12,10 +12,7 @@ from homeassistant.components.fan import ( DIRECTION_FORWARD, DIRECTION_REVERSE, DOMAIN, - SUPPORT_DIRECTION, - SUPPORT_OSCILLATE, - SUPPORT_PRESET_MODE, - SUPPORT_SET_SPEED, + FanEntityFeature, ) from homeassistant.components.homekit.const import ATTR_VALUE, PROP_MIN_STEP from homeassistant.components.homekit.type_fans import Fan @@ -118,7 +115,10 @@ async def test_fan_direction(hass, hk_driver, events): hass.states.async_set( entity_id, STATE_ON, - {ATTR_SUPPORTED_FEATURES: SUPPORT_DIRECTION, ATTR_DIRECTION: DIRECTION_FORWARD}, + { + ATTR_SUPPORTED_FEATURES: FanEntityFeature.DIRECTION, + ATTR_DIRECTION: DIRECTION_FORWARD, + }, ) await hass.async_block_till_done() acc = Fan(hass, hk_driver, "Fan", entity_id, 1, None) @@ -186,7 +186,7 @@ async def test_fan_oscillate(hass, hk_driver, events): hass.states.async_set( entity_id, STATE_ON, - {ATTR_SUPPORTED_FEATURES: SUPPORT_OSCILLATE, ATTR_OSCILLATING: False}, + {ATTR_SUPPORTED_FEATURES: FanEntityFeature.OSCILLATE, ATTR_OSCILLATING: False}, ) await hass.async_block_till_done() acc = Fan(hass, hk_driver, "Fan", entity_id, 1, None) @@ -256,7 +256,7 @@ async def test_fan_speed(hass, hk_driver, events): entity_id, STATE_ON, { - ATTR_SUPPORTED_FEATURES: SUPPORT_SET_SPEED, + ATTR_SUPPORTED_FEATURES: FanEntityFeature.SET_SPEED, ATTR_PERCENTAGE: 0, ATTR_PERCENTAGE_STEP: 25, }, @@ -342,9 +342,9 @@ async def test_fan_set_all_one_shot(hass, hk_driver, events): entity_id, STATE_ON, { - ATTR_SUPPORTED_FEATURES: SUPPORT_SET_SPEED - | SUPPORT_OSCILLATE - | SUPPORT_DIRECTION, + ATTR_SUPPORTED_FEATURES: FanEntityFeature.SET_SPEED + | FanEntityFeature.OSCILLATE + | FanEntityFeature.DIRECTION, ATTR_PERCENTAGE: 0, ATTR_OSCILLATING: False, ATTR_DIRECTION: DIRECTION_FORWARD, @@ -364,9 +364,9 @@ async def test_fan_set_all_one_shot(hass, hk_driver, events): entity_id, STATE_OFF, { - ATTR_SUPPORTED_FEATURES: SUPPORT_SET_SPEED - | SUPPORT_OSCILLATE - | SUPPORT_DIRECTION, + ATTR_SUPPORTED_FEATURES: FanEntityFeature.SET_SPEED + | FanEntityFeature.OSCILLATE + | FanEntityFeature.DIRECTION, ATTR_PERCENTAGE: 0, ATTR_OSCILLATING: False, ATTR_DIRECTION: DIRECTION_FORWARD, @@ -435,9 +435,9 @@ async def test_fan_set_all_one_shot(hass, hk_driver, events): entity_id, STATE_ON, { - ATTR_SUPPORTED_FEATURES: SUPPORT_SET_SPEED - | SUPPORT_OSCILLATE - | SUPPORT_DIRECTION, + ATTR_SUPPORTED_FEATURES: FanEntityFeature.SET_SPEED + | FanEntityFeature.OSCILLATE + | FanEntityFeature.DIRECTION, ATTR_PERCENTAGE: 0, ATTR_OSCILLATING: False, ATTR_DIRECTION: DIRECTION_FORWARD, @@ -545,7 +545,9 @@ async def test_fan_restore(hass, hk_driver, events): "9012", suggested_object_id="all_info_set", capabilities={"speed_list": ["off", "low", "medium", "high"]}, - supported_features=SUPPORT_SET_SPEED | SUPPORT_OSCILLATE | SUPPORT_DIRECTION, + supported_features=FanEntityFeature.SET_SPEED + | FanEntityFeature.OSCILLATE + | FanEntityFeature.DIRECTION, original_device_class="mock-device-class", ) @@ -575,7 +577,7 @@ async def test_fan_multiple_preset_modes(hass, hk_driver, events): entity_id, STATE_ON, { - ATTR_SUPPORTED_FEATURES: SUPPORT_PRESET_MODE, + ATTR_SUPPORTED_FEATURES: FanEntityFeature.PRESET_MODE, ATTR_PRESET_MODE: "auto", ATTR_PRESET_MODES: ["auto", "smart"], }, @@ -594,7 +596,7 @@ async def test_fan_multiple_preset_modes(hass, hk_driver, events): entity_id, STATE_ON, { - ATTR_SUPPORTED_FEATURES: SUPPORT_PRESET_MODE, + ATTR_SUPPORTED_FEATURES: FanEntityFeature.PRESET_MODE, ATTR_PRESET_MODE: "smart", ATTR_PRESET_MODES: ["auto", "smart"], }, @@ -655,7 +657,8 @@ async def test_fan_single_preset_mode(hass, hk_driver, events): entity_id, STATE_ON, { - ATTR_SUPPORTED_FEATURES: SUPPORT_PRESET_MODE | SUPPORT_SET_SPEED, + ATTR_SUPPORTED_FEATURES: FanEntityFeature.PRESET_MODE + | FanEntityFeature.SET_SPEED, ATTR_PERCENTAGE: 42, ATTR_PRESET_MODE: "smart", ATTR_PRESET_MODES: ["smart"], @@ -718,7 +721,8 @@ async def test_fan_single_preset_mode(hass, hk_driver, events): entity_id, STATE_ON, { - ATTR_SUPPORTED_FEATURES: SUPPORT_PRESET_MODE | SUPPORT_SET_SPEED, + ATTR_SUPPORTED_FEATURES: FanEntityFeature.PRESET_MODE + | FanEntityFeature.SET_SPEED, ATTR_PERCENTAGE: 42, ATTR_PRESET_MODE: None, ATTR_PRESET_MODES: ["smart"], diff --git a/tests/components/homekit/test_type_remote.py b/tests/components/homekit/test_type_remote.py index e77dd5de54c..b66601aeb32 100644 --- a/tests/components/homekit/test_type_remote.py +++ b/tests/components/homekit/test_type_remote.py @@ -16,7 +16,7 @@ from homeassistant.components.remote import ( ATTR_ACTIVITY_LIST, ATTR_CURRENT_ACTIVITY, DOMAIN, - SUPPORT_ACTIVITY, + RemoteEntityFeature, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -36,7 +36,7 @@ async def test_activity_remote(hass, hk_driver, events, caplog): entity_id, None, { - ATTR_SUPPORTED_FEATURES: SUPPORT_ACTIVITY, + ATTR_SUPPORTED_FEATURES: RemoteEntityFeature.ACTIVITY, ATTR_CURRENT_ACTIVITY: "Apple TV", ATTR_ACTIVITY_LIST: ["TV", "Apple TV"], }, @@ -57,7 +57,7 @@ async def test_activity_remote(hass, hk_driver, events, caplog): entity_id, STATE_ON, { - ATTR_SUPPORTED_FEATURES: SUPPORT_ACTIVITY, + ATTR_SUPPORTED_FEATURES: RemoteEntityFeature.ACTIVITY, ATTR_CURRENT_ACTIVITY: "Apple TV", ATTR_ACTIVITY_LIST: ["TV", "Apple TV"], }, @@ -81,7 +81,7 @@ async def test_activity_remote(hass, hk_driver, events, caplog): entity_id, STATE_ON, { - ATTR_SUPPORTED_FEATURES: SUPPORT_ACTIVITY, + ATTR_SUPPORTED_FEATURES: RemoteEntityFeature.ACTIVITY, ATTR_CURRENT_ACTIVITY: "TV", ATTR_ACTIVITY_LIST: ["TV", "Apple TV"], }, @@ -93,7 +93,7 @@ async def test_activity_remote(hass, hk_driver, events, caplog): entity_id, STATE_ON, { - ATTR_SUPPORTED_FEATURES: SUPPORT_ACTIVITY, + ATTR_SUPPORTED_FEATURES: RemoteEntityFeature.ACTIVITY, ATTR_CURRENT_ACTIVITY: "Apple TV", ATTR_ACTIVITY_LIST: ["TV", "Apple TV"], }, @@ -160,7 +160,7 @@ async def test_activity_remote(hass, hk_driver, events, caplog): entity_id, STATE_ON, { - ATTR_SUPPORTED_FEATURES: SUPPORT_ACTIVITY, + ATTR_SUPPORTED_FEATURES: RemoteEntityFeature.ACTIVITY, ATTR_CURRENT_ACTIVITY: "Amazon TV", ATTR_ACTIVITY_LIST: ["TV", "Apple TV", "Amazon TV"], }, @@ -176,7 +176,7 @@ async def test_activity_remote_bad_names(hass, hk_driver, events, caplog): entity_id, None, { - ATTR_SUPPORTED_FEATURES: SUPPORT_ACTIVITY, + ATTR_SUPPORTED_FEATURES: RemoteEntityFeature.ACTIVITY, ATTR_CURRENT_ACTIVITY: "Apple TV", ATTR_ACTIVITY_LIST: ["TV", "Apple TV", "[[[--Special--]]]", "Super"], }, @@ -197,7 +197,7 @@ async def test_activity_remote_bad_names(hass, hk_driver, events, caplog): entity_id, STATE_ON, { - ATTR_SUPPORTED_FEATURES: SUPPORT_ACTIVITY, + ATTR_SUPPORTED_FEATURES: RemoteEntityFeature.ACTIVITY, ATTR_CURRENT_ACTIVITY: "[[[--Special--]]]", ATTR_ACTIVITY_LIST: ["TV", "Apple TV", "[[[--Special--]]]", "Super"], }, diff --git a/tests/components/homekit/test_type_security_systems.py b/tests/components/homekit/test_type_security_systems.py index fc507024fe9..64f1d82d123 100644 --- a/tests/components/homekit/test_type_security_systems.py +++ b/tests/components/homekit/test_type_security_systems.py @@ -4,10 +4,7 @@ import pytest from homeassistant.components.alarm_control_panel import ( DOMAIN, - SUPPORT_ALARM_ARM_AWAY, - SUPPORT_ALARM_ARM_HOME, - SUPPORT_ALARM_ARM_NIGHT, - SUPPORT_ALARM_TRIGGER, + AlarmControlPanelEntityFeature, ) from homeassistant.components.homekit.const import ATTR_VALUE from homeassistant.components.homekit.type_security_systems import SecuritySystem @@ -208,7 +205,7 @@ async def test_supported_states(hass, hk_driver, events): # Set up a number of test configuration test_configs = [ { - "features": SUPPORT_ALARM_ARM_HOME, + "features": AlarmControlPanelEntityFeature.ARM_HOME, "current_values": [ default_current_states["Disarmed"], default_current_states["AlarmTriggered"], @@ -220,7 +217,7 @@ async def test_supported_states(hass, hk_driver, events): ], }, { - "features": SUPPORT_ALARM_ARM_AWAY, + "features": AlarmControlPanelEntityFeature.ARM_AWAY, "current_values": [ default_current_states["Disarmed"], default_current_states["AlarmTriggered"], @@ -232,7 +229,8 @@ async def test_supported_states(hass, hk_driver, events): ], }, { - "features": SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY, + "features": AlarmControlPanelEntityFeature.ARM_HOME + | AlarmControlPanelEntityFeature.ARM_AWAY, "current_values": [ default_current_states["Disarmed"], default_current_states["AlarmTriggered"], @@ -246,9 +244,9 @@ async def test_supported_states(hass, hk_driver, events): ], }, { - "features": SUPPORT_ALARM_ARM_HOME - | SUPPORT_ALARM_ARM_AWAY - | SUPPORT_ALARM_ARM_NIGHT, + "features": AlarmControlPanelEntityFeature.ARM_HOME + | AlarmControlPanelEntityFeature.ARM_AWAY + | AlarmControlPanelEntityFeature.ARM_NIGHT, "current_values": [ default_current_states["Disarmed"], default_current_states["AlarmTriggered"], @@ -264,10 +262,10 @@ async def test_supported_states(hass, hk_driver, events): ], }, { - "features": SUPPORT_ALARM_ARM_HOME - | SUPPORT_ALARM_ARM_AWAY - | SUPPORT_ALARM_ARM_NIGHT - | SUPPORT_ALARM_TRIGGER, + "features": AlarmControlPanelEntityFeature.ARM_HOME + | AlarmControlPanelEntityFeature.ARM_AWAY + | AlarmControlPanelEntityFeature.ARM_NIGHT + | AlarmControlPanelEntityFeature.TRIGGER, "current_values": [ default_current_states["Disarmed"], default_current_states["AlarmTriggered"], diff --git a/tests/components/homekit/test_type_switches.py b/tests/components/homekit/test_type_switches.py index 6d87edc5617..cc80201ae33 100644 --- a/tests/components/homekit/test_type_switches.py +++ b/tests/components/homekit/test_type_switches.py @@ -26,8 +26,7 @@ from homeassistant.components.vacuum import ( SERVICE_TURN_ON, STATE_CLEANING, STATE_DOCKED, - SUPPORT_RETURN_HOME, - SUPPORT_START, + VacuumEntityFeature, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -212,7 +211,12 @@ async def test_vacuum_set_state_with_returnhome_and_start_support( entity_id = "vacuum.roomba" hass.states.async_set( - entity_id, None, {ATTR_SUPPORTED_FEATURES: SUPPORT_RETURN_HOME | SUPPORT_START} + entity_id, + None, + { + ATTR_SUPPORTED_FEATURES: VacuumEntityFeature.RETURN_HOME + | VacuumEntityFeature.START + }, ) await hass.async_block_till_done() @@ -227,7 +231,10 @@ async def test_vacuum_set_state_with_returnhome_and_start_support( hass.states.async_set( entity_id, STATE_CLEANING, - {ATTR_SUPPORTED_FEATURES: SUPPORT_RETURN_HOME | SUPPORT_START}, + { + ATTR_SUPPORTED_FEATURES: VacuumEntityFeature.RETURN_HOME + | VacuumEntityFeature.START + }, ) await hass.async_block_till_done() assert acc.char_on.value == 1 @@ -235,7 +242,10 @@ async def test_vacuum_set_state_with_returnhome_and_start_support( hass.states.async_set( entity_id, STATE_DOCKED, - {ATTR_SUPPORTED_FEATURES: SUPPORT_RETURN_HOME | SUPPORT_START}, + { + ATTR_SUPPORTED_FEATURES: VacuumEntityFeature.RETURN_HOME + | VacuumEntityFeature.START + }, ) await hass.async_block_till_done() assert acc.char_on.value == 0 diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index f0c21acd54c..a964568cc60 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -33,13 +33,10 @@ from homeassistant.components.climate import ( FAN_ON, SERVICE_SET_FAN_MODE, SERVICE_SET_SWING_MODE, - SUPPORT_FAN_MODE, - SUPPORT_SWING_MODE, - SUPPORT_TARGET_TEMPERATURE, - SUPPORT_TARGET_TEMPERATURE_RANGE, SWING_BOTH, SWING_HORIZONTAL, SWING_OFF, + ClimateEntityFeature, HVACAction, HVACMode, ) @@ -88,7 +85,7 @@ async def test_thermostat(hass, hk_driver, events): entity_id, HVACMode.OFF, { - ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE, + ATTR_SUPPORTED_FEATURES: ClimateEntityFeature.TARGET_TEMPERATURE, ATTR_HVAC_MODES: [ HVACMode.HEAT, HVACMode.HEAT_COOL, @@ -324,7 +321,7 @@ async def test_thermostat(hass, hk_driver, events): entity_id, HVACMode.DRY, { - ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE, + ATTR_SUPPORTED_FEATURES: ClimateEntityFeature.TARGET_TEMPERATURE, ATTR_TEMPERATURE: 22.0, ATTR_CURRENT_TEMPERATURE: 22.0, ATTR_HVAC_ACTION: HVACAction.DRYING, @@ -429,8 +426,8 @@ async def test_thermostat_auto(hass, hk_driver, events): entity_id, HVACMode.OFF, { - ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE - | SUPPORT_TARGET_TEMPERATURE_RANGE, + ATTR_SUPPORTED_FEATURES: ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE, ATTR_HVAC_MODES: [ HVACMode.HEAT, HVACMode.HEAT_COOL, @@ -583,8 +580,8 @@ async def test_thermostat_mode_and_temp_change(hass, hk_driver, events): entity_id, HVACMode.OFF, { - ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE - | SUPPORT_TARGET_TEMPERATURE_RANGE, + ATTR_SUPPORTED_FEATURES: ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE, ATTR_HVAC_MODES: [ HVACMode.HEAT, HVACMode.HEAT_COOL, @@ -875,8 +872,8 @@ async def test_thermostat_fahrenheit(hass, hk_driver, events): entity_id, HVACMode.OFF, { - ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE - | SUPPORT_TARGET_TEMPERATURE_RANGE + ATTR_SUPPORTED_FEATURES: ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE }, ) await hass.async_block_till_done() @@ -894,8 +891,8 @@ async def test_thermostat_fahrenheit(hass, hk_driver, events): ATTR_TARGET_TEMP_LOW: 68.1, ATTR_TEMPERATURE: 71.6, ATTR_CURRENT_TEMPERATURE: 73.4, - ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE - | SUPPORT_TARGET_TEMPERATURE_RANGE, + ATTR_SUPPORTED_FEATURES: ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE, }, ) await hass.async_block_till_done() @@ -1528,7 +1525,7 @@ async def test_thermostat_without_target_temp_only_range(hass, hk_driver, events hass.states.async_set( entity_id, HVACMode.OFF, - {ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE_RANGE}, + {ATTR_SUPPORTED_FEATURES: ClimateEntityFeature.TARGET_TEMPERATURE_RANGE}, ) await hass.async_block_till_done() acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) @@ -1555,7 +1552,7 @@ async def test_thermostat_without_target_temp_only_range(hass, hk_driver, events ATTR_TARGET_TEMP_LOW: 20.0, ATTR_CURRENT_TEMPERATURE: 18.0, ATTR_HVAC_ACTION: HVACAction.HEATING, - ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE_RANGE, + ATTR_SUPPORTED_FEATURES: ClimateEntityFeature.TARGET_TEMPERATURE_RANGE, ATTR_HVAC_MODES: [ HVACMode.HEAT, HVACMode.HEAT_COOL, @@ -1582,7 +1579,7 @@ async def test_thermostat_without_target_temp_only_range(hass, hk_driver, events ATTR_TARGET_TEMP_LOW: 19.0, ATTR_CURRENT_TEMPERATURE: 24.0, ATTR_HVAC_ACTION: HVACAction.COOLING, - ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE_RANGE, + ATTR_SUPPORTED_FEATURES: ClimateEntityFeature.TARGET_TEMPERATURE_RANGE, ATTR_HVAC_MODES: [ HVACMode.HEAT, HVACMode.HEAT_COOL, @@ -1609,7 +1606,7 @@ async def test_thermostat_without_target_temp_only_range(hass, hk_driver, events ATTR_TARGET_TEMP_LOW: 19.0, ATTR_CURRENT_TEMPERATURE: 21.0, ATTR_HVAC_ACTION: HVACAction.IDLE, - ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE_RANGE, + ATTR_SUPPORTED_FEATURES: ClimateEntityFeature.TARGET_TEMPERATURE_RANGE, ATTR_HVAC_MODES: [ HVACMode.HEAT, HVACMode.HEAT_COOL, @@ -1663,7 +1660,7 @@ async def test_thermostat_without_target_temp_only_range(hass, hk_driver, events ATTR_TARGET_TEMP_LOW: 19.0, ATTR_CURRENT_TEMPERATURE: 21.0, ATTR_HVAC_ACTION: HVACAction.IDLE, - ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE_RANGE, + ATTR_SUPPORTED_FEATURES: ClimateEntityFeature.TARGET_TEMPERATURE_RANGE, }, ) await hass.async_block_till_done() @@ -1882,8 +1879,8 @@ async def test_thermostat_with_no_modes_when_we_first_see(hass, hk_driver, event entity_id, HVACMode.OFF, { - ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE - | SUPPORT_TARGET_TEMPERATURE_RANGE, + ATTR_SUPPORTED_FEATURES: ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE, ATTR_HVAC_MODES: [], }, ) @@ -1935,8 +1932,8 @@ async def test_thermostat_with_no_off_after_recheck(hass, hk_driver, events): entity_id, HVACMode.COOL, { - ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE - | SUPPORT_TARGET_TEMPERATURE_RANGE, + ATTR_SUPPORTED_FEATURES: ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE, ATTR_HVAC_MODES: [], }, ) @@ -1987,8 +1984,8 @@ async def test_thermostat_with_temp_clamps(hass, hk_driver, events): entity_id, HVACMode.COOL, { - ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE - | SUPPORT_TARGET_TEMPERATURE_RANGE, + ATTR_SUPPORTED_FEATURES: ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE, ATTR_HVAC_MODES: [], ATTR_MAX_TEMP: 50, ATTR_MIN_TEMP: 100, @@ -2040,10 +2037,10 @@ async def test_thermostat_with_fan_modes_with_auto(hass, hk_driver, events): entity_id, HVACMode.OFF, { - ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE - | SUPPORT_TARGET_TEMPERATURE_RANGE - | SUPPORT_FAN_MODE - | SUPPORT_SWING_MODE, + ATTR_SUPPORTED_FEATURES: ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE + | ClimateEntityFeature.FAN_MODE + | ClimateEntityFeature.SWING_MODE, ATTR_FAN_MODES: [FAN_AUTO, FAN_LOW, FAN_MEDIUM, FAN_HIGH], ATTR_SWING_MODES: [SWING_BOTH, SWING_OFF, SWING_HORIZONTAL], ATTR_HVAC_ACTION: HVACAction.IDLE, @@ -2079,10 +2076,10 @@ async def test_thermostat_with_fan_modes_with_auto(hass, hk_driver, events): entity_id, HVACMode.OFF, { - ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE - | SUPPORT_TARGET_TEMPERATURE_RANGE - | SUPPORT_FAN_MODE - | SUPPORT_SWING_MODE, + ATTR_SUPPORTED_FEATURES: ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE + | ClimateEntityFeature.FAN_MODE + | ClimateEntityFeature.SWING_MODE, ATTR_FAN_MODES: [FAN_AUTO, FAN_LOW, FAN_MEDIUM, FAN_HIGH], ATTR_SWING_MODES: [SWING_BOTH, SWING_OFF, SWING_HORIZONTAL], ATTR_HVAC_ACTION: HVACAction.IDLE, @@ -2244,10 +2241,10 @@ async def test_thermostat_with_fan_modes_with_off(hass, hk_driver, events): entity_id, HVACMode.COOL, { - ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE - | SUPPORT_TARGET_TEMPERATURE_RANGE - | SUPPORT_FAN_MODE - | SUPPORT_SWING_MODE, + ATTR_SUPPORTED_FEATURES: ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE + | ClimateEntityFeature.FAN_MODE + | ClimateEntityFeature.SWING_MODE, ATTR_FAN_MODES: [FAN_ON, FAN_OFF], ATTR_SWING_MODES: [SWING_BOTH, SWING_OFF, SWING_HORIZONTAL], ATTR_HVAC_ACTION: HVACAction.IDLE, @@ -2283,10 +2280,10 @@ async def test_thermostat_with_fan_modes_with_off(hass, hk_driver, events): entity_id, HVACMode.COOL, { - ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE - | SUPPORT_TARGET_TEMPERATURE_RANGE - | SUPPORT_FAN_MODE - | SUPPORT_SWING_MODE, + ATTR_SUPPORTED_FEATURES: ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE + | ClimateEntityFeature.FAN_MODE + | ClimateEntityFeature.SWING_MODE, ATTR_FAN_MODES: [FAN_ON, FAN_OFF], ATTR_SWING_MODES: [SWING_BOTH, SWING_OFF, SWING_HORIZONTAL], ATTR_HVAC_ACTION: HVACAction.IDLE, @@ -2351,10 +2348,10 @@ async def test_thermostat_with_fan_modes_set_to_none(hass, hk_driver, events): entity_id, HVACMode.OFF, { - ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE - | SUPPORT_TARGET_TEMPERATURE_RANGE - | SUPPORT_FAN_MODE - | SUPPORT_SWING_MODE, + ATTR_SUPPORTED_FEATURES: ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE + | ClimateEntityFeature.FAN_MODE + | ClimateEntityFeature.SWING_MODE, ATTR_FAN_MODES: None, ATTR_SWING_MODES: [SWING_BOTH, SWING_OFF, SWING_HORIZONTAL], ATTR_HVAC_ACTION: HVACAction.IDLE, @@ -2395,9 +2392,9 @@ async def test_thermostat_with_fan_modes_set_to_none_not_supported( entity_id, HVACMode.OFF, { - ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE - | SUPPORT_TARGET_TEMPERATURE_RANGE - | SUPPORT_SWING_MODE, + ATTR_SUPPORTED_FEATURES: ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE + | ClimateEntityFeature.SWING_MODE, ATTR_FAN_MODES: None, ATTR_SWING_MODES: [SWING_BOTH, SWING_OFF, SWING_HORIZONTAL], ATTR_HVAC_ACTION: HVACAction.IDLE, @@ -2438,7 +2435,7 @@ async def test_thermostat_with_supported_features_target_temp_but_fan_mode_set( entity_id, HVACMode.OFF, { - ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE, + ATTR_SUPPORTED_FEATURES: ClimateEntityFeature.TARGET_TEMPERATURE, ATTR_MIN_TEMP: 44.6, ATTR_MAX_TEMP: 95, ATTR_PRESET_MODES: ["home", "away"], diff --git a/tests/components/homekit_controller/specific_devices/test_aqara_gateway.py b/tests/components/homekit_controller/specific_devices/test_aqara_gateway.py index 150b39ac2ab..75423f3373e 100644 --- a/tests/components/homekit_controller/specific_devices/test_aqara_gateway.py +++ b/tests/components/homekit_controller/specific_devices/test_aqara_gateway.py @@ -3,11 +3,7 @@ Regression tests for Aqara Gateway V3. https://github.com/home-assistant/core/issues/20957 """ -from homeassistant.components.alarm_control_panel import ( - SUPPORT_ALARM_ARM_AWAY, - SUPPORT_ALARM_ARM_HOME, - SUPPORT_ALARM_ARM_NIGHT, -) +from homeassistant.components.alarm_control_panel import AlarmControlPanelEntityFeature from homeassistant.components.number import NumberMode from homeassistant.helpers.entity import EntityCategory @@ -42,9 +38,9 @@ async def test_aqara_gateway_setup(hass): "alarm_control_panel.aqara_hub_1563_security_system", friendly_name="Aqara Hub-1563 Security System", unique_id="homekit-0000000123456789-66304", - supported_features=SUPPORT_ALARM_ARM_NIGHT - | SUPPORT_ALARM_ARM_HOME - | SUPPORT_ALARM_ARM_AWAY, + supported_features=AlarmControlPanelEntityFeature.ARM_NIGHT + | AlarmControlPanelEntityFeature.ARM_HOME + | AlarmControlPanelEntityFeature.ARM_AWAY, state="disarmed", ), EntityTestInfo( @@ -101,9 +97,9 @@ async def test_aqara_gateway_e1_setup(hass): "alarm_control_panel.aqara_hub_e1_00a0_security_system", friendly_name="Aqara-Hub-E1-00A0 Security System", unique_id="homekit-00aa00000a0-16", - supported_features=SUPPORT_ALARM_ARM_NIGHT - | SUPPORT_ALARM_ARM_HOME - | SUPPORT_ALARM_ARM_AWAY, + supported_features=AlarmControlPanelEntityFeature.ARM_NIGHT + | AlarmControlPanelEntityFeature.ARM_HOME + | AlarmControlPanelEntityFeature.ARM_AWAY, state="disarmed", ), EntityTestInfo( diff --git a/tests/components/homekit_controller/specific_devices/test_haa_fan.py b/tests/components/homekit_controller/specific_devices/test_haa_fan.py index 307b7fa9408..2f01a2c404e 100644 --- a/tests/components/homekit_controller/specific_devices/test_haa_fan.py +++ b/tests/components/homekit_controller/specific_devices/test_haa_fan.py @@ -1,6 +1,6 @@ """Make sure that a H.A.A. fan can be setup.""" -from homeassistant.components.fan import ATTR_PERCENTAGE, SUPPORT_SET_SPEED +from homeassistant.components.fan import ATTR_PERCENTAGE, FanEntityFeature from homeassistant.helpers.entity import EntityCategory from ..common import ( @@ -58,7 +58,7 @@ async def test_haa_fan_setup(hass): friendly_name="HAA-C718B3", unique_id="homekit-C718B3-1-8", state="on", - supported_features=SUPPORT_SET_SPEED, + supported_features=FanEntityFeature.SET_SPEED, capabilities={ "preset_modes": None, }, diff --git a/tests/components/homekit_controller/specific_devices/test_homeassistant_bridge.py b/tests/components/homekit_controller/specific_devices/test_homeassistant_bridge.py index 7c5e71f31b0..175e534f639 100644 --- a/tests/components/homekit_controller/specific_devices/test_homeassistant_bridge.py +++ b/tests/components/homekit_controller/specific_devices/test_homeassistant_bridge.py @@ -1,10 +1,6 @@ """Test against characteristics captured from the Home Assistant HomeKit bridge running demo platforms.""" -from homeassistant.components.fan import ( - SUPPORT_DIRECTION, - SUPPORT_OSCILLATE, - SUPPORT_SET_SPEED, -) +from homeassistant.components.fan import FanEntityFeature from ..common import ( HUB_TEST_ACCESSORY_ID, @@ -49,9 +45,9 @@ async def test_homeassistant_bridge_fan_setup(hass): friendly_name="Living Room Fan", unique_id="homekit-fan.living_room_fan-8", supported_features=( - SUPPORT_DIRECTION - | SUPPORT_SET_SPEED - | SUPPORT_OSCILLATE + FanEntityFeature.DIRECTION + | FanEntityFeature.SET_SPEED + | FanEntityFeature.OSCILLATE ), capabilities={ "preset_modes": None, diff --git a/tests/components/homekit_controller/specific_devices/test_mysa_living.py b/tests/components/homekit_controller/specific_devices/test_mysa_living.py index 2161de08b7b..a5abe4ad2e7 100644 --- a/tests/components/homekit_controller/specific_devices/test_mysa_living.py +++ b/tests/components/homekit_controller/specific_devices/test_mysa_living.py @@ -1,6 +1,6 @@ """Make sure that Mysa Living is enumerated properly.""" -from homeassistant.components.climate import SUPPORT_TARGET_TEMPERATURE +from homeassistant.components.climate import ClimateEntityFeature from homeassistant.components.sensor import SensorStateClass from homeassistant.const import PERCENTAGE, TEMP_CELSIUS @@ -35,7 +35,7 @@ async def test_mysa_living_setup(hass): entity_id="climate.mysa_85dda9_thermostat", friendly_name="Mysa-85dda9 Thermostat", unique_id="homekit-AAAAAAA000-20", - supported_features=SUPPORT_TARGET_TEMPERATURE, + supported_features=ClimateEntityFeature.TARGET_TEMPERATURE, capabilities={ "hvac_modes": ["off", "heat", "cool", "heat_cool"], "max_temp": 35, diff --git a/tests/components/homekit_controller/specific_devices/test_ryse_smart_bridge.py b/tests/components/homekit_controller/specific_devices/test_ryse_smart_bridge.py index a56ea9bdaf0..aa9dee89be2 100644 --- a/tests/components/homekit_controller/specific_devices/test_ryse_smart_bridge.py +++ b/tests/components/homekit_controller/specific_devices/test_ryse_smart_bridge.py @@ -1,10 +1,6 @@ """Test against characteristics captured from a ryse smart bridge platforms.""" -from homeassistant.components.cover import ( - SUPPORT_CLOSE, - SUPPORT_OPEN, - SUPPORT_SET_POSITION, -) +from homeassistant.components.cover import CoverEntityFeature from homeassistant.components.sensor import SensorStateClass from homeassistant.const import PERCENTAGE @@ -17,7 +13,9 @@ from ..common import ( setup_test_accessories, ) -RYSE_SUPPORTED_FEATURES = SUPPORT_CLOSE | SUPPORT_SET_POSITION | SUPPORT_OPEN +RYSE_SUPPORTED_FEATURES = ( + CoverEntityFeature.CLOSE | CoverEntityFeature.SET_POSITION | CoverEntityFeature.OPEN +) async def test_ryse_smart_bridge_setup(hass): diff --git a/tests/components/homekit_controller/specific_devices/test_simpleconnect_fan.py b/tests/components/homekit_controller/specific_devices/test_simpleconnect_fan.py index 036dde1af12..ba24bdeef96 100644 --- a/tests/components/homekit_controller/specific_devices/test_simpleconnect_fan.py +++ b/tests/components/homekit_controller/specific_devices/test_simpleconnect_fan.py @@ -4,7 +4,7 @@ Test against characteristics captured from a SIMPLEconnect Fan. https://github.com/home-assistant/core/issues/26180 """ -from homeassistant.components.fan import SUPPORT_DIRECTION, SUPPORT_SET_SPEED +from homeassistant.components.fan import FanEntityFeature from ..common import ( HUB_TEST_ACCESSORY_ID, @@ -37,7 +37,8 @@ async def test_simpleconnect_fan_setup(hass): entity_id="fan.simpleconnect_fan_06f674_hunter_fan", friendly_name="SIMPLEconnect Fan-06F674 Hunter Fan", unique_id="homekit-1234567890abcd-8", - supported_features=SUPPORT_DIRECTION | SUPPORT_SET_SPEED, + supported_features=FanEntityFeature.DIRECTION + | FanEntityFeature.SET_SPEED, capabilities={ "preset_modes": None, }, diff --git a/tests/components/homekit_controller/specific_devices/test_velux_gateway.py b/tests/components/homekit_controller/specific_devices/test_velux_gateway.py index 7442d84c224..8a5102e0a87 100644 --- a/tests/components/homekit_controller/specific_devices/test_velux_gateway.py +++ b/tests/components/homekit_controller/specific_devices/test_velux_gateway.py @@ -4,11 +4,7 @@ Test against characteristics captured from a Velux Gateway. https://github.com/home-assistant/core/issues/44314 """ -from homeassistant.components.cover import ( - SUPPORT_CLOSE, - SUPPORT_OPEN, - SUPPORT_SET_POSITION, -) +from homeassistant.components.cover import CoverEntityFeature from homeassistant.components.sensor import SensorStateClass from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, @@ -56,9 +52,9 @@ async def test_velux_cover_setup(hass): entity_id="cover.velux_window_roof_window", friendly_name="VELUX Window Roof Window", unique_id="homekit-1111111a114a111a-8", - supported_features=SUPPORT_CLOSE - | SUPPORT_SET_POSITION - | SUPPORT_OPEN, + supported_features=CoverEntityFeature.CLOSE + | CoverEntityFeature.SET_POSITION + | CoverEntityFeature.OPEN, state="closed", ), ], diff --git a/tests/components/homekit_controller/specific_devices/test_vocolinc_flowerbud.py b/tests/components/homekit_controller/specific_devices/test_vocolinc_flowerbud.py index aa929fffecc..7c3262f3098 100644 --- a/tests/components/homekit_controller/specific_devices/test_vocolinc_flowerbud.py +++ b/tests/components/homekit_controller/specific_devices/test_vocolinc_flowerbud.py @@ -1,6 +1,6 @@ """Make sure that Vocolinc Flowerbud is enumerated properly.""" -from homeassistant.components.humidifier import SUPPORT_MODES +from homeassistant.components.humidifier import HumidifierEntityFeature from homeassistant.components.number import NumberMode from homeassistant.components.sensor import SensorStateClass from homeassistant.const import PERCENTAGE @@ -37,7 +37,7 @@ async def test_vocolinc_flowerbud_setup(hass): entity_id="humidifier.vocolinc_flowerbud_0d324b", friendly_name="VOCOlinc-Flowerbud-0d324b", unique_id="homekit-AM01121849000327-30", - supported_features=SUPPORT_MODES, + supported_features=HumidifierEntityFeature.MODES, capabilities={ "available_modes": ["normal", "auto"], "max_humidity": 100.0, diff --git a/tests/components/mobile_app/test_webhook.py b/tests/components/mobile_app/test_webhook.py index b7b95dff392..30036c0aba7 100644 --- a/tests/components/mobile_app/test_webhook.py +++ b/tests/components/mobile_app/test_webhook.py @@ -5,7 +5,7 @@ from unittest.mock import patch import pytest -from homeassistant.components.camera import SUPPORT_STREAM as CAMERA_SUPPORT_STREAM +from homeassistant.components.camera import CameraEntityFeature from homeassistant.components.mobile_app.const import CONF_SECRET from homeassistant.components.zone import DOMAIN as ZONE_DOMAIN from homeassistant.const import ( @@ -783,7 +783,9 @@ async def test_webhook_camera_stream_stream_available( ): """Test fetching camera stream URLs for an HLS/stream-supporting camera.""" hass.states.async_set( - "camera.stream_camera", "idle", {"supported_features": CAMERA_SUPPORT_STREAM} + "camera.stream_camera", + "idle", + {"supported_features": CameraEntityFeature.STREAM}, ) webhook_id = create_registrations[1]["webhook_id"] @@ -811,7 +813,9 @@ async def test_webhook_camera_stream_stream_available_but_errors( ): """Test fetching camera stream URLs for an HLS/stream-supporting camera but that streaming errors.""" hass.states.async_set( - "camera.stream_camera", "idle", {"supported_features": CAMERA_SUPPORT_STREAM} + "camera.stream_camera", + "idle", + {"supported_features": CameraEntityFeature.STREAM}, ) webhook_id = create_registrations[1]["webhook_id"] diff --git a/tests/components/risco/test_alarm_control_panel.py b/tests/components/risco/test_alarm_control_panel.py index b2d24bbea32..c690be6d799 100644 --- a/tests/components/risco/test_alarm_control_panel.py +++ b/tests/components/risco/test_alarm_control_panel.py @@ -5,10 +5,7 @@ import pytest from homeassistant.components.alarm_control_panel import ( DOMAIN as ALARM_DOMAIN, - SUPPORT_ALARM_ARM_AWAY, - SUPPORT_ALARM_ARM_CUSTOM_BYPASS, - SUPPORT_ALARM_ARM_HOME, - SUPPORT_ALARM_ARM_NIGHT, + AlarmControlPanelEntityFeature, ) from homeassistant.components.risco import CannotConnectError, UnauthorizedError from homeassistant.components.risco.const import DOMAIN @@ -72,7 +69,9 @@ FULL_CUSTOM_MAPPING = { } EXPECTED_FEATURES = ( - SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_NIGHT + AlarmControlPanelEntityFeature.ARM_AWAY + | AlarmControlPanelEntityFeature.ARM_HOME + | AlarmControlPanelEntityFeature.ARM_NIGHT ) @@ -294,7 +293,8 @@ async def test_cloud_sets_full_custom_mapping( registry = er.async_get(hass) entity = registry.async_get(FIRST_CLOUD_ENTITY_ID) assert ( - entity.supported_features == EXPECTED_FEATURES | SUPPORT_ALARM_ARM_CUSTOM_BYPASS + entity.supported_features + == EXPECTED_FEATURES | AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS ) await _test_cloud_service_call( @@ -669,7 +669,8 @@ async def test_local_sets_full_custom_mapping( registry = er.async_get(hass) entity = registry.async_get(FIRST_LOCAL_ENTITY_ID) assert ( - entity.supported_features == EXPECTED_FEATURES | SUPPORT_ALARM_ARM_CUSTOM_BYPASS + entity.supported_features + == EXPECTED_FEATURES | AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS ) await _test_local_service_call( diff --git a/tests/components/roku/test_media_player.py b/tests/components/roku/test_media_player.py index a19e88a2d70..7f59bfd308e 100644 --- a/tests/components/roku/test_media_player.py +++ b/tests/components/roku/test_media_player.py @@ -20,19 +20,9 @@ from homeassistant.components.media_player import ( DOMAIN as MP_DOMAIN, SERVICE_PLAY_MEDIA, SERVICE_SELECT_SOURCE, - SUPPORT_BROWSE_MEDIA, - SUPPORT_NEXT_TRACK, - SUPPORT_PAUSE, - SUPPORT_PLAY, - SUPPORT_PLAY_MEDIA, - SUPPORT_PREVIOUS_TRACK, - SUPPORT_SELECT_SOURCE, - SUPPORT_TURN_OFF, - SUPPORT_TURN_ON, - SUPPORT_VOLUME_MUTE, - SUPPORT_VOLUME_STEP, MediaClass, MediaPlayerDeviceClass, + MediaPlayerEntityFeature, MediaType, ) from homeassistant.components.roku.const import ( @@ -197,17 +187,17 @@ async def test_supported_features( # Features supported for Rokus state = hass.states.get(MAIN_ENTITY_ID) assert ( - SUPPORT_PREVIOUS_TRACK - | SUPPORT_NEXT_TRACK - | SUPPORT_VOLUME_STEP - | SUPPORT_VOLUME_MUTE - | SUPPORT_SELECT_SOURCE - | SUPPORT_PAUSE - | SUPPORT_PLAY - | SUPPORT_PLAY_MEDIA - | SUPPORT_TURN_ON - | SUPPORT_TURN_OFF - | SUPPORT_BROWSE_MEDIA + MediaPlayerEntityFeature.PREVIOUS_TRACK + | MediaPlayerEntityFeature.NEXT_TRACK + | MediaPlayerEntityFeature.VOLUME_STEP + | MediaPlayerEntityFeature.VOLUME_MUTE + | MediaPlayerEntityFeature.SELECT_SOURCE + | MediaPlayerEntityFeature.PAUSE + | MediaPlayerEntityFeature.PLAY + | MediaPlayerEntityFeature.PLAY_MEDIA + | MediaPlayerEntityFeature.TURN_ON + | MediaPlayerEntityFeature.TURN_OFF + | MediaPlayerEntityFeature.BROWSE_MEDIA == state.attributes.get("supported_features") ) @@ -222,17 +212,17 @@ async def test_tv_supported_features( state = hass.states.get(TV_ENTITY_ID) assert state assert ( - SUPPORT_PREVIOUS_TRACK - | SUPPORT_NEXT_TRACK - | SUPPORT_VOLUME_STEP - | SUPPORT_VOLUME_MUTE - | SUPPORT_SELECT_SOURCE - | SUPPORT_PAUSE - | SUPPORT_PLAY - | SUPPORT_PLAY_MEDIA - | SUPPORT_TURN_ON - | SUPPORT_TURN_OFF - | SUPPORT_BROWSE_MEDIA + MediaPlayerEntityFeature.PREVIOUS_TRACK + | MediaPlayerEntityFeature.NEXT_TRACK + | MediaPlayerEntityFeature.VOLUME_STEP + | MediaPlayerEntityFeature.VOLUME_MUTE + | MediaPlayerEntityFeature.SELECT_SOURCE + | MediaPlayerEntityFeature.PAUSE + | MediaPlayerEntityFeature.PLAY + | MediaPlayerEntityFeature.PLAY_MEDIA + | MediaPlayerEntityFeature.TURN_ON + | MediaPlayerEntityFeature.TURN_OFF + | MediaPlayerEntityFeature.BROWSE_MEDIA == state.attributes.get("supported_features") ) diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index 6d0edbfa8ae..aec072ab448 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -32,8 +32,8 @@ from homeassistant.components.media_player import ( DOMAIN, SERVICE_PLAY_MEDIA, SERVICE_SELECT_SOURCE, - SUPPORT_TURN_ON, MediaPlayerDeviceClass, + MediaPlayerEntityFeature, MediaType, ) from homeassistant.components.samsungtv.const import ( @@ -749,7 +749,8 @@ async def test_supported_features_with_turnon(hass: HomeAssistant) -> None: await setup_samsungtv(hass, MOCK_CONFIG) state = hass.states.get(ENTITY_ID) assert ( - state.attributes[ATTR_SUPPORTED_FEATURES] == SUPPORT_SAMSUNGTV | SUPPORT_TURN_ON + state.attributes[ATTR_SUPPORTED_FEATURES] + == SUPPORT_SAMSUNGTV | MediaPlayerEntityFeature.TURN_ON ) diff --git a/tests/components/sharkiq/test_vacuum.py b/tests/components/sharkiq/test_vacuum.py index ac6ab3274cb..36e9944e394 100644 --- a/tests/components/sharkiq/test_vacuum.py +++ b/tests/components/sharkiq/test_vacuum.py @@ -33,15 +33,7 @@ from homeassistant.components.vacuum import ( STATE_IDLE, STATE_PAUSED, STATE_RETURNING, - SUPPORT_BATTERY, - SUPPORT_FAN_SPEED, - SUPPORT_LOCATE, - SUPPORT_PAUSE, - SUPPORT_RETURN_HOME, - SUPPORT_START, - SUPPORT_STATE, - SUPPORT_STATUS, - SUPPORT_STOP, + VacuumEntityFeature, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -65,15 +57,15 @@ from tests.common import MockConfigEntry VAC_ENTITY_ID = f"vacuum.{SHARK_DEVICE_DICT['product_name'].lower()}" EXPECTED_FEATURES = ( - SUPPORT_BATTERY - | SUPPORT_FAN_SPEED - | SUPPORT_PAUSE - | SUPPORT_RETURN_HOME - | SUPPORT_START - | SUPPORT_STATE - | SUPPORT_STATUS - | SUPPORT_STOP - | SUPPORT_LOCATE + VacuumEntityFeature.BATTERY + | VacuumEntityFeature.FAN_SPEED + | VacuumEntityFeature.PAUSE + | VacuumEntityFeature.RETURN_HOME + | VacuumEntityFeature.START + | VacuumEntityFeature.STATE + | VacuumEntityFeature.STATUS + | VacuumEntityFeature.STOP + | VacuumEntityFeature.LOCATE ) diff --git a/tests/components/smartthings/test_fan.py b/tests/components/smartthings/test_fan.py index 1ee48c5c47d..9bf810b0712 100644 --- a/tests/components/smartthings/test_fan.py +++ b/tests/components/smartthings/test_fan.py @@ -9,7 +9,7 @@ from pysmartthings import Attribute, Capability from homeassistant.components.fan import ( ATTR_PERCENTAGE, DOMAIN as FAN_DOMAIN, - SUPPORT_SET_SPEED, + FanEntityFeature, ) from homeassistant.components.smartthings.const import DOMAIN, SIGNAL_SMARTTHINGS_UPDATE from homeassistant.config_entries import ConfigEntryState @@ -36,7 +36,7 @@ async def test_entity_state(hass, device_factory): # Dimmer 1 state = hass.states.get("fan.fan_1") assert state.state == "on" - assert state.attributes[ATTR_SUPPORTED_FEATURES] == SUPPORT_SET_SPEED + assert state.attributes[ATTR_SUPPORTED_FEATURES] == FanEntityFeature.SET_SPEED assert state.attributes[ATTR_PERCENTAGE] == 66 diff --git a/tests/components/template/test_fan.py b/tests/components/template/test_fan.py index f7805ae41d6..30bb9e00d59 100644 --- a/tests/components/template/test_fan.py +++ b/tests/components/template/test_fan.py @@ -11,8 +11,7 @@ from homeassistant.components.fan import ( DIRECTION_FORWARD, DIRECTION_REVERSE, DOMAIN, - SUPPORT_PRESET_MODE, - SUPPORT_SET_SPEED, + FanEntityFeature, ) from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE @@ -879,7 +878,7 @@ async def test_implemented_percentage(hass, speed_count, percentage_step): state = hass.states.get("fan.mechanical_ventilation") attributes = state.attributes assert attributes["percentage_step"] == percentage_step - assert attributes.get("supported_features") & SUPPORT_SET_SPEED + assert attributes.get("supported_features") & FanEntityFeature.SET_SPEED @pytest.mark.parametrize("count,domain", [(1, DOMAIN)]) @@ -947,4 +946,4 @@ async def test_implemented_preset_mode(hass, start_ha): state = hass.states.get("fan.mechanical_ventilation") attributes = state.attributes assert attributes.get("percentage") is None - assert attributes.get("supported_features") & SUPPORT_PRESET_MODE + assert attributes.get("supported_features") & FanEntityFeature.PRESET_MODE diff --git a/tests/components/template/test_light.py b/tests/components/template/test_light.py index 89fcc97e2f7..08b97514883 100644 --- a/tests/components/template/test_light.py +++ b/tests/components/template/test_light.py @@ -9,7 +9,6 @@ from homeassistant.components.light import ( ATTR_EFFECT, ATTR_HS_COLOR, ATTR_TRANSITION, - SUPPORT_TRANSITION, ColorMode, LightEntityFeature, ) @@ -342,7 +341,7 @@ async def test_on_action_with_transition(hass, setup_light, calls): assert state.state == STATE_OFF assert "color_mode" not in state.attributes assert state.attributes["supported_color_modes"] == [ColorMode.BRIGHTNESS] - assert state.attributes["supported_features"] == SUPPORT_TRANSITION + assert state.attributes["supported_features"] == LightEntityFeature.TRANSITION await hass.services.async_call( light.DOMAIN, @@ -357,7 +356,7 @@ async def test_on_action_with_transition(hass, setup_light, calls): assert state.state == STATE_OFF assert "color_mode" not in state.attributes assert state.attributes["supported_color_modes"] == [ColorMode.BRIGHTNESS] - assert state.attributes["supported_features"] == SUPPORT_TRANSITION + assert state.attributes["supported_features"] == LightEntityFeature.TRANSITION @pytest.mark.parametrize("count", [1]) @@ -498,7 +497,7 @@ async def test_off_action_with_transition(hass, setup_light, calls): assert state.state == STATE_ON assert state.attributes["color_mode"] == ColorMode.BRIGHTNESS assert state.attributes["supported_color_modes"] == [ColorMode.BRIGHTNESS] - assert state.attributes["supported_features"] == SUPPORT_TRANSITION + assert state.attributes["supported_features"] == LightEntityFeature.TRANSITION await hass.services.async_call( light.DOMAIN, @@ -512,7 +511,7 @@ async def test_off_action_with_transition(hass, setup_light, calls): assert state.state == STATE_ON assert state.attributes["color_mode"] == ColorMode.BRIGHTNESS assert state.attributes["supported_color_modes"] == [ColorMode.BRIGHTNESS] - assert state.attributes["supported_features"] == SUPPORT_TRANSITION + assert state.attributes["supported_features"] == LightEntityFeature.TRANSITION @pytest.mark.parametrize("count", [1]) diff --git a/tests/components/unifiprotect/test_camera.py b/tests/components/unifiprotect/test_camera.py index e7abbf7273a..2727d11285f 100644 --- a/tests/components/unifiprotect/test_camera.py +++ b/tests/components/unifiprotect/test_camera.py @@ -8,7 +8,7 @@ from pyunifiprotect.data import Camera as ProtectCamera, CameraChannel, StateTyp from pyunifiprotect.exceptions import NvrError from homeassistant.components.camera import ( - SUPPORT_STREAM, + CameraEntityFeature, async_get_image, async_get_stream_source, ) @@ -112,7 +112,7 @@ def validate_common_camera_state( hass: HomeAssistant, channel: CameraChannel, entity_id: str, - features: int = SUPPORT_STREAM, + features: int = CameraEntityFeature.STREAM, ): """Validate state that is common to all camera entity, regradless of type.""" entity_state = hass.states.get(entity_id) @@ -131,7 +131,7 @@ async def validate_rtsps_camera_state( camera_obj: ProtectCamera, channel_id: int, entity_id: str, - features: int = SUPPORT_STREAM, + features: int = CameraEntityFeature.STREAM, ): """Validate a camera's state.""" channel = camera_obj.channels[channel_id] @@ -145,7 +145,7 @@ async def validate_rtsp_camera_state( camera_obj: ProtectCamera, channel_id: int, entity_id: str, - features: int = SUPPORT_STREAM, + features: int = CameraEntityFeature.STREAM, ): """Validate a camera's state.""" channel = camera_obj.channels[channel_id] @@ -159,7 +159,7 @@ async def validate_no_stream_camera_state( camera_obj: ProtectCamera, channel_id: int, entity_id: str, - features: int = SUPPORT_STREAM, + features: int = CameraEntityFeature.STREAM, ): """Validate a camera's state.""" channel = camera_obj.channels[channel_id] diff --git a/tests/components/uvc/test_camera.py b/tests/components/uvc/test_camera.py index 75d0d40c9f3..030a56c6389 100644 --- a/tests/components/uvc/test_camera.py +++ b/tests/components/uvc/test_camera.py @@ -11,7 +11,7 @@ from homeassistant.components.camera import ( SERVICE_DISABLE_MOTION, SERVICE_ENABLE_MOTION, STATE_RECORDING, - SUPPORT_STREAM, + CameraEntityFeature, async_get_image, async_get_stream_source, ) @@ -331,7 +331,7 @@ async def test_properties(hass, mock_remote): assert state.state == STATE_RECORDING assert state.attributes["brand"] == "Ubiquiti" assert state.attributes["model_name"] == "UVC" - assert state.attributes["supported_features"] == SUPPORT_STREAM + assert state.attributes["supported_features"] == CameraEntityFeature.STREAM async def test_motion_recording_mode_properties(hass, mock_remote): diff --git a/tests/components/webostv/test_media_player.py b/tests/components/webostv/test_media_player.py index 174e4a73483..70549f5d4e8 100644 --- a/tests/components/webostv/test_media_player.py +++ b/tests/components/webostv/test_media_player.py @@ -17,9 +17,8 @@ from homeassistant.components.media_player import ( DOMAIN as MP_DOMAIN, SERVICE_PLAY_MEDIA, SERVICE_SELECT_SOURCE, - SUPPORT_TURN_ON, - SUPPORT_VOLUME_SET, MediaPlayerDeviceClass, + MediaPlayerEntityFeature, MediaType, ) from homeassistant.components.webostv.const import ( @@ -523,7 +522,7 @@ async def test_supported_features(hass, client, monkeypatch): # Support volume mute, step, set monkeypatch.setattr(client, "sound_output", "speaker") await client.mock_state_update() - supported = supported | SUPPORT_WEBOSTV_VOLUME | SUPPORT_VOLUME_SET + supported = supported | SUPPORT_WEBOSTV_VOLUME | MediaPlayerEntityFeature.VOLUME_SET attrs = hass.states.get(ENTITY_ID).attributes assert attrs[ATTR_SUPPORTED_FEATURES] == supported @@ -550,7 +549,7 @@ async def test_supported_features(hass, client, monkeypatch): ], }, ) - supported |= SUPPORT_TURN_ON + supported |= MediaPlayerEntityFeature.TURN_ON await client.mock_state_update() attrs = hass.states.get(ENTITY_ID).attributes @@ -561,7 +560,9 @@ async def test_cached_supported_features(hass, client, monkeypatch): """Test test supported features.""" monkeypatch.setattr(client, "is_on", False) monkeypatch.setattr(client, "sound_output", None) - supported = SUPPORT_WEBOSTV | SUPPORT_WEBOSTV_VOLUME | SUPPORT_TURN_ON + supported = ( + SUPPORT_WEBOSTV | SUPPORT_WEBOSTV_VOLUME | MediaPlayerEntityFeature.TURN_ON + ) mock_restore_cache( hass, [ @@ -581,7 +582,9 @@ async def test_cached_supported_features(hass, client, monkeypatch): # validate SUPPORT_TURN_ON is not cached attrs = hass.states.get(ENTITY_ID).attributes - assert attrs[ATTR_SUPPORTED_FEATURES] == supported & ~SUPPORT_TURN_ON + assert ( + attrs[ATTR_SUPPORTED_FEATURES] == supported & ~MediaPlayerEntityFeature.TURN_ON + ) # TV on, support volume mute, step monkeypatch.setattr(client, "is_on", True) @@ -608,7 +611,9 @@ async def test_cached_supported_features(hass, client, monkeypatch): monkeypatch.setattr(client, "sound_output", "speaker") await client.mock_state_update() - supported = SUPPORT_WEBOSTV | SUPPORT_WEBOSTV_VOLUME | SUPPORT_VOLUME_SET + supported = ( + SUPPORT_WEBOSTV | SUPPORT_WEBOSTV_VOLUME | MediaPlayerEntityFeature.VOLUME_SET + ) attrs = hass.states.get(ENTITY_ID).attributes assert attrs[ATTR_SUPPORTED_FEATURES] == supported @@ -618,7 +623,9 @@ async def test_cached_supported_features(hass, client, monkeypatch): monkeypatch.setattr(client, "sound_output", None) await client.mock_state_update() - supported = SUPPORT_WEBOSTV | SUPPORT_WEBOSTV_VOLUME | SUPPORT_VOLUME_SET + supported = ( + SUPPORT_WEBOSTV | SUPPORT_WEBOSTV_VOLUME | MediaPlayerEntityFeature.VOLUME_SET + ) attrs = hass.states.get(ENTITY_ID).attributes assert attrs[ATTR_SUPPORTED_FEATURES] == supported @@ -649,7 +656,9 @@ async def test_cached_supported_features(hass, client, monkeypatch): attrs = hass.states.get(ENTITY_ID).attributes - assert attrs[ATTR_SUPPORTED_FEATURES] == supported | SUPPORT_TURN_ON + assert ( + attrs[ATTR_SUPPORTED_FEATURES] == supported | MediaPlayerEntityFeature.TURN_ON + ) async def test_supported_features_no_cache(hass, client, monkeypatch): @@ -658,7 +667,9 @@ async def test_supported_features_no_cache(hass, client, monkeypatch): monkeypatch.setattr(client, "sound_output", None) await setup_webostv(hass) - supported = SUPPORT_WEBOSTV | SUPPORT_WEBOSTV_VOLUME | SUPPORT_VOLUME_SET + supported = ( + SUPPORT_WEBOSTV | SUPPORT_WEBOSTV_VOLUME | MediaPlayerEntityFeature.VOLUME_SET + ) attrs = hass.states.get(ENTITY_ID).attributes assert attrs[ATTR_SUPPORTED_FEATURES] == supported @@ -680,7 +691,9 @@ async def test_supported_features_ignore_cache(hass, client): ) await setup_webostv(hass) - supported = SUPPORT_WEBOSTV | SUPPORT_WEBOSTV_VOLUME | SUPPORT_VOLUME_SET + supported = ( + SUPPORT_WEBOSTV | SUPPORT_WEBOSTV_VOLUME | MediaPlayerEntityFeature.VOLUME_SET + ) attrs = hass.states.get(ENTITY_ID).attributes assert attrs[ATTR_SUPPORTED_FEATURES] == supported diff --git a/tests/components/zwave_js/test_fan.py b/tests/components/zwave_js/test_fan.py index 83863174d6f..97b4a0074bc 100644 --- a/tests/components/zwave_js/test_fan.py +++ b/tests/components/zwave_js/test_fan.py @@ -14,7 +14,7 @@ from homeassistant.components.fan import ( ATTR_PRESET_MODES, DOMAIN as FAN_DOMAIN, SERVICE_SET_PRESET_MODE, - SUPPORT_PRESET_MODE, + FanEntityFeature, NotValidPresetModeError, ) from homeassistant.components.zwave_js.fan import ATTR_FAN_STATE @@ -587,7 +587,7 @@ async def test_thermostat_fan(hass, client, climate_adc_t3000, integration): assert state.state == STATE_ON assert state.attributes.get(ATTR_FAN_STATE) == "Idle / off" assert state.attributes.get(ATTR_PRESET_MODE) == "Auto low" - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == SUPPORT_PRESET_MODE + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == FanEntityFeature.PRESET_MODE # Test setting preset mode await hass.services.async_call( From 00f1f3e3551e4f68aeeef4839d2a0606887d2c63 Mon Sep 17 00:00:00 2001 From: Midbin Date: Wed, 21 Sep 2022 11:12:50 +0200 Subject: [PATCH 601/955] Change minimal brightness value for hue.activate_scene service to 1 (#78154) * Adjust Implementation Hue Scene Brightness so it cannot be 0 --- homeassistant/components/hue/scene.py | 2 +- homeassistant/components/hue/services.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hue/scene.py b/homeassistant/components/hue/scene.py index 1504e52e33d..5b0c17ebbf4 100644 --- a/homeassistant/components/hue/scene.py +++ b/homeassistant/components/hue/scene.py @@ -71,7 +71,7 @@ async def async_setup_entry( vol.Coerce(float), vol.Range(min=0, max=600) ), vol.Optional(ATTR_BRIGHTNESS): vol.All( - vol.Coerce(int), vol.Range(min=0, max=255) + vol.Coerce(int), vol.Range(min=1, max=255) ), }, "_async_activate", diff --git a/homeassistant/components/hue/services.yaml b/homeassistant/components/hue/services.yaml index 96a66be199f..790100373f4 100644 --- a/homeassistant/components/hue/services.yaml +++ b/homeassistant/components/hue/services.yaml @@ -60,5 +60,5 @@ activate_scene: advanced: true selector: number: - min: 0 + min: 1 max: 255 From bc120e9ff2057c1e0ac46ba668fe59a66d0fdc93 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 21 Sep 2022 11:17:23 +0200 Subject: [PATCH 602/955] Use SensorEntityDescription in kostal plenticore (#78842) --- .../components/kostal_plenticore/sensor.py | 950 +++++++++--------- 1 file changed, 479 insertions(+), 471 deletions(-) diff --git a/homeassistant/components/kostal_plenticore/sensor.py b/homeassistant/components/kostal_plenticore/sensor.py index d9ab0d752e0..de3a2375a54 100644 --- a/homeassistant/components/kostal_plenticore/sensor.py +++ b/homeassistant/components/kostal_plenticore/sensor.py @@ -2,6 +2,7 @@ from __future__ import annotations from collections.abc import Callable +from dataclasses import dataclass from datetime import timedelta import logging from typing import Any @@ -10,6 +11,7 @@ from homeassistant.components.sensor import ( ATTR_STATE_CLASS, SensorDeviceClass, SensorEntity, + SensorEntityDescription, SensorStateClass, ) from homeassistant.config_entries import ConfigEntry @@ -33,772 +35,784 @@ from .helper import PlenticoreDataFormatter, ProcessDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) -# Defines all entities for process data. -# -# Each entry is defined with a tuple of these values: -# - module id (str) -# - process data id (str) -# - entity name suffix (str) -# - sensor properties (dict) -# - value formatter (str) + +@dataclass +class PlenticoreRequiredKeysMixin: + """A class that describes required properties for plenticore sensor entities.""" + + module_id: str + properties: dict[str, Any] + formatter: str + + +@dataclass +class PlenticoreSensorEntityDescription( + SensorEntityDescription, PlenticoreRequiredKeysMixin +): + """A class that describes plenticore sensor entities.""" + + SENSOR_PROCESS_DATA = [ - ( - "devices:local", - "Inverter:State", - "Inverter State", - {ATTR_ICON: "mdi:state-machine"}, - "format_inverter_state", + PlenticoreSensorEntityDescription( + module_id="devices:local", + key="Inverter:State", + name="Inverter State", + properties={ATTR_ICON: "mdi:state-machine"}, + formatter="format_inverter_state", ), - ( - "devices:local", - "Dc_P", - "Solar Power", - { + PlenticoreSensorEntityDescription( + module_id="devices:local", + key="Dc_P", + name="Solar Power", + properties={ ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, ATTR_ENABLED_DEFAULT: True, ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, }, - "format_round", + formatter="format_round", ), - ( - "devices:local", - "Grid_P", - "Grid Power", - { + PlenticoreSensorEntityDescription( + module_id="devices:local", + key="Grid_P", + name="Grid Power", + properties={ ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, ATTR_ENABLED_DEFAULT: True, ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, }, - "format_round", + formatter="format_round", ), - ( - "devices:local", - "HomeBat_P", - "Home Power from Battery", - { + PlenticoreSensorEntityDescription( + module_id="devices:local", + key="HomeBat_P", + name="Home Power from Battery", + properties={ ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, }, - "format_round", + formatter="format_round", ), - ( - "devices:local", - "HomeGrid_P", - "Home Power from Grid", - { + PlenticoreSensorEntityDescription( + module_id="devices:local", + key="HomeGrid_P", + name="Home Power from Grid", + properties={ ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, }, - "format_round", + formatter="format_round", ), - ( - "devices:local", - "HomeOwn_P", - "Home Power from Own", - { + PlenticoreSensorEntityDescription( + module_id="devices:local", + key="HomeOwn_P", + name="Home Power from Own", + properties={ ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, }, - "format_round", + formatter="format_round", ), - ( - "devices:local", - "HomePv_P", - "Home Power from PV", - { + PlenticoreSensorEntityDescription( + module_id="devices:local", + key="HomePv_P", + name="Home Power from PV", + properties={ ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, }, - "format_round", + formatter="format_round", ), - ( - "devices:local", - "Home_P", - "Home Power", - { + PlenticoreSensorEntityDescription( + module_id="devices:local", + key="Home_P", + name="Home Power", + properties={ ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, }, - "format_round", + formatter="format_round", ), - ( - "devices:local:ac", - "P", - "AC Power", - { + PlenticoreSensorEntityDescription( + module_id="devices:local:ac", + key="P", + name="AC Power", + properties={ ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, ATTR_ENABLED_DEFAULT: True, ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, }, - "format_round", + formatter="format_round", ), - ( - "devices:local:pv1", - "P", - "DC1 Power", - { + PlenticoreSensorEntityDescription( + module_id="devices:local:pv1", + key="P", + name="DC1 Power", + properties={ ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, }, - "format_round", + formatter="format_round", ), - ( - "devices:local:pv1", - "U", - "DC1 Voltage", - { + PlenticoreSensorEntityDescription( + module_id="devices:local:pv1", + key="U", + name="DC1 Voltage", + properties={ ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_POTENTIAL_VOLT, ATTR_DEVICE_CLASS: SensorDeviceClass.VOLTAGE, ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, }, - "format_round", + formatter="format_round", ), - ( - "devices:local:pv1", - "I", - "DC1 Current", - { + PlenticoreSensorEntityDescription( + module_id="devices:local:pv1", + key="I", + name="DC1 Current", + properties={ ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_CURRENT_AMPERE, ATTR_DEVICE_CLASS: SensorDeviceClass.CURRENT, ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, }, - "format_float", + formatter="format_float", ), - ( - "devices:local:pv2", - "P", - "DC2 Power", - { + PlenticoreSensorEntityDescription( + module_id="devices:local:pv2", + key="P", + name="DC2 Power", + properties={ ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, }, - "format_round", + formatter="format_round", ), - ( - "devices:local:pv2", - "U", - "DC2 Voltage", - { + PlenticoreSensorEntityDescription( + module_id="devices:local:pv2", + key="U", + name="DC2 Voltage", + properties={ ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_POTENTIAL_VOLT, ATTR_DEVICE_CLASS: SensorDeviceClass.VOLTAGE, ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, }, - "format_round", + formatter="format_round", ), - ( - "devices:local:pv2", - "I", - "DC2 Current", - { + PlenticoreSensorEntityDescription( + module_id="devices:local:pv2", + key="I", + name="DC2 Current", + properties={ ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_CURRENT_AMPERE, ATTR_DEVICE_CLASS: SensorDeviceClass.CURRENT, ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, }, - "format_float", + formatter="format_float", ), - ( - "devices:local:pv3", - "P", - "DC3 Power", - { + PlenticoreSensorEntityDescription( + module_id="devices:local:pv3", + key="P", + name="DC3 Power", + properties={ ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, }, - "format_round", + formatter="format_round", ), - ( - "devices:local:pv3", - "U", - "DC3 Voltage", - { + PlenticoreSensorEntityDescription( + module_id="devices:local:pv3", + key="U", + name="DC3 Voltage", + properties={ ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_POTENTIAL_VOLT, ATTR_DEVICE_CLASS: SensorDeviceClass.VOLTAGE, ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, }, - "format_round", + formatter="format_round", ), - ( - "devices:local:pv3", - "I", - "DC3 Current", - { + PlenticoreSensorEntityDescription( + module_id="devices:local:pv3", + key="I", + name="DC3 Current", + properties={ ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_CURRENT_AMPERE, ATTR_DEVICE_CLASS: SensorDeviceClass.CURRENT, ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, }, - "format_float", + formatter="format_float", ), - ( - "devices:local", - "PV2Bat_P", - "PV to Battery Power", - { + PlenticoreSensorEntityDescription( + module_id="devices:local", + key="PV2Bat_P", + name="PV to Battery Power", + properties={ ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, }, - "format_round", + formatter="format_round", ), - ( - "devices:local", - "EM_State", - "Energy Manager State", - {ATTR_ICON: "mdi:state-machine"}, - "format_em_manager_state", + PlenticoreSensorEntityDescription( + module_id="devices:local", + key="EM_State", + name="Energy Manager State", + properties={ATTR_ICON: "mdi:state-machine"}, + formatter="format_em_manager_state", ), - ( - "devices:local:battery", - "Cycles", - "Battery Cycles", - {ATTR_ICON: "mdi:recycle", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT}, - "format_round", + PlenticoreSensorEntityDescription( + module_id="devices:local:battery", + key="Cycles", + name="Battery Cycles", + properties={ + ATTR_ICON: "mdi:recycle", + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + formatter="format_round", ), - ( - "devices:local:battery", - "P", - "Battery Power", - { + PlenticoreSensorEntityDescription( + module_id="devices:local:battery", + key="P", + name="Battery Power", + properties={ ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, }, - "format_round", + formatter="format_round", ), - ( - "devices:local:battery", - "SoC", - "Battery SoC", - { + PlenticoreSensorEntityDescription( + module_id="devices:local:battery", + key="SoC", + name="Battery SoC", + properties={ ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_DEVICE_CLASS: SensorDeviceClass.BATTERY, }, - "format_round", + formatter="format_round", ), - ( - "scb:statistic:EnergyFlow", - "Statistic:Autarky:Day", - "Autarky Day", - {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_ICON: "mdi:chart-donut"}, - "format_round", + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:Autarky:Day", + name="Autarky Day", + properties={ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_ICON: "mdi:chart-donut"}, + formatter="format_round", ), - ( - "scb:statistic:EnergyFlow", - "Statistic:Autarky:Month", - "Autarky Month", - {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_ICON: "mdi:chart-donut"}, - "format_round", + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:Autarky:Month", + name="Autarky Month", + properties={ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_ICON: "mdi:chart-donut"}, + formatter="format_round", ), - ( - "scb:statistic:EnergyFlow", - "Statistic:Autarky:Total", - "Autarky Total", - { + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:Autarky:Total", + name="Autarky Total", + properties={ ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_ICON: "mdi:chart-donut", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, }, - "format_round", + formatter="format_round", ), - ( - "scb:statistic:EnergyFlow", - "Statistic:Autarky:Year", - "Autarky Year", - {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_ICON: "mdi:chart-donut"}, - "format_round", + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:Autarky:Year", + name="Autarky Year", + properties={ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_ICON: "mdi:chart-donut"}, + formatter="format_round", ), - ( - "scb:statistic:EnergyFlow", - "Statistic:OwnConsumptionRate:Day", - "Own Consumption Rate Day", - {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_ICON: "mdi:chart-donut"}, - "format_round", + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:OwnConsumptionRate:Day", + name="Own Consumption Rate Day", + properties={ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_ICON: "mdi:chart-donut"}, + formatter="format_round", ), - ( - "scb:statistic:EnergyFlow", - "Statistic:OwnConsumptionRate:Month", - "Own Consumption Rate Month", - {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_ICON: "mdi:chart-donut"}, - "format_round", + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:OwnConsumptionRate:Month", + name="Own Consumption Rate Month", + properties={ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_ICON: "mdi:chart-donut"}, + formatter="format_round", ), - ( - "scb:statistic:EnergyFlow", - "Statistic:OwnConsumptionRate:Total", - "Own Consumption Rate Total", - { + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:OwnConsumptionRate:Total", + name="Own Consumption Rate Total", + properties={ ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_ICON: "mdi:chart-donut", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, }, - "format_round", + formatter="format_round", ), - ( - "scb:statistic:EnergyFlow", - "Statistic:OwnConsumptionRate:Year", - "Own Consumption Rate Year", - {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_ICON: "mdi:chart-donut"}, - "format_round", + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:OwnConsumptionRate:Year", + name="Own Consumption Rate Year", + properties={ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_ICON: "mdi:chart-donut"}, + formatter="format_round", ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyHome:Day", - "Home Consumption Day", - { + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyHome:Day", + name="Home Consumption Day", + properties={ ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, }, - "format_energy", + formatter="format_energy", ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyHome:Month", - "Home Consumption Month", - { + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyHome:Month", + name="Home Consumption Month", + properties={ ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, }, - "format_energy", + formatter="format_energy", ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyHome:Year", - "Home Consumption Year", - { + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyHome:Year", + name="Home Consumption Year", + properties={ ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, }, - "format_energy", + formatter="format_energy", ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyHome:Total", - "Home Consumption Total", - { + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyHome:Total", + name="Home Consumption Total", + properties={ ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, }, - "format_energy", + formatter="format_energy", ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyHomeBat:Day", - "Home Consumption from Battery Day", - { + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyHomeBat:Day", + name="Home Consumption from Battery Day", + properties={ ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, }, - "format_energy", + formatter="format_energy", ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyHomeBat:Month", - "Home Consumption from Battery Month", - { + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyHomeBat:Month", + name="Home Consumption from Battery Month", + properties={ ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, }, - "format_energy", + formatter="format_energy", ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyHomeBat:Year", - "Home Consumption from Battery Year", - { + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyHomeBat:Year", + name="Home Consumption from Battery Year", + properties={ ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, }, - "format_energy", + formatter="format_energy", ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyHomeBat:Total", - "Home Consumption from Battery Total", - { + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyHomeBat:Total", + name="Home Consumption from Battery Total", + properties={ ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, }, - "format_energy", + formatter="format_energy", ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyHomeGrid:Day", - "Home Consumption from Grid Day", - { + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyHomeGrid:Day", + name="Home Consumption from Grid Day", + properties={ ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, }, - "format_energy", + formatter="format_energy", ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyHomeGrid:Month", - "Home Consumption from Grid Month", - { + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyHomeGrid:Month", + name="Home Consumption from Grid Month", + properties={ ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, }, - "format_energy", + formatter="format_energy", ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyHomeGrid:Year", - "Home Consumption from Grid Year", - { + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyHomeGrid:Year", + name="Home Consumption from Grid Year", + properties={ ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, }, - "format_energy", + formatter="format_energy", ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyHomeGrid:Total", - "Home Consumption from Grid Total", - { + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyHomeGrid:Total", + name="Home Consumption from Grid Total", + properties={ ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, }, - "format_energy", + formatter="format_energy", ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyHomePv:Day", - "Home Consumption from PV Day", - { + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyHomePv:Day", + name="Home Consumption from PV Day", + properties={ ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, }, - "format_energy", + formatter="format_energy", ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyHomePv:Month", - "Home Consumption from PV Month", - { + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyHomePv:Month", + name="Home Consumption from PV Month", + properties={ ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, }, - "format_energy", + formatter="format_energy", ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyHomePv:Year", - "Home Consumption from PV Year", - { + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyHomePv:Year", + name="Home Consumption from PV Year", + properties={ ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, }, - "format_energy", + formatter="format_energy", ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyHomePv:Total", - "Home Consumption from PV Total", - { + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyHomePv:Total", + name="Home Consumption from PV Total", + properties={ ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, }, - "format_energy", + formatter="format_energy", ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyPv1:Day", - "Energy PV1 Day", - { + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyPv1:Day", + name="Energy PV1 Day", + properties={ ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, }, - "format_energy", + formatter="format_energy", ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyPv1:Month", - "Energy PV1 Month", - { + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyPv1:Month", + name="Energy PV1 Month", + properties={ ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, }, - "format_energy", + formatter="format_energy", ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyPv1:Year", - "Energy PV1 Year", - { + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyPv1:Year", + name="Energy PV1 Year", + properties={ ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, }, - "format_energy", + formatter="format_energy", ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyPv1:Total", - "Energy PV1 Total", - { + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyPv1:Total", + name="Energy PV1 Total", + properties={ ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, }, - "format_energy", + formatter="format_energy", ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyPv2:Day", - "Energy PV2 Day", - { + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyPv2:Day", + name="Energy PV2 Day", + properties={ ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, }, - "format_energy", + formatter="format_energy", ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyPv2:Month", - "Energy PV2 Month", - { + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyPv2:Month", + name="Energy PV2 Month", + properties={ ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, }, - "format_energy", + formatter="format_energy", ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyPv2:Year", - "Energy PV2 Year", - { + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyPv2:Year", + name="Energy PV2 Year", + properties={ ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, }, - "format_energy", + formatter="format_energy", ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyPv2:Total", - "Energy PV2 Total", - { + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyPv2:Total", + name="Energy PV2 Total", + properties={ ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, }, - "format_energy", + formatter="format_energy", ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyPv3:Day", - "Energy PV3 Day", - { + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyPv3:Day", + name="Energy PV3 Day", + properties={ ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, }, - "format_energy", + formatter="format_energy", ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyPv3:Month", - "Energy PV3 Month", - { + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyPv3:Month", + name="Energy PV3 Month", + properties={ ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, }, - "format_energy", + formatter="format_energy", ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyPv3:Year", - "Energy PV3 Year", - { + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyPv3:Year", + name="Energy PV3 Year", + properties={ ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, }, - "format_energy", + formatter="format_energy", ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyPv3:Total", - "Energy PV3 Total", - { + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyPv3:Total", + name="Energy PV3 Total", + properties={ ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, }, - "format_energy", + formatter="format_energy", ), - ( - "scb:statistic:EnergyFlow", - "Statistic:Yield:Day", - "Energy Yield Day", - { + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:Yield:Day", + name="Energy Yield Day", + properties={ ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, ATTR_ENABLED_DEFAULT: True, }, - "format_energy", + formatter="format_energy", ), - ( - "scb:statistic:EnergyFlow", - "Statistic:Yield:Month", - "Energy Yield Month", - { + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:Yield:Month", + name="Energy Yield Month", + properties={ ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, }, - "format_energy", + formatter="format_energy", ), - ( - "scb:statistic:EnergyFlow", - "Statistic:Yield:Year", - "Energy Yield Year", - { + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:Yield:Year", + name="Energy Yield Year", + properties={ ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, }, - "format_energy", + formatter="format_energy", ), - ( - "scb:statistic:EnergyFlow", - "Statistic:Yield:Total", - "Energy Yield Total", - { + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:Yield:Total", + name="Energy Yield Total", + properties={ ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, }, - "format_energy", + formatter="format_energy", ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyChargeGrid:Day", - "Battery Charge from Grid Day", - { + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyChargeGrid:Day", + name="Battery Charge from Grid Day", + properties={ ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, }, - "format_energy", + formatter="format_energy", ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyChargeGrid:Month", - "Battery Charge from Grid Month", - { + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyChargeGrid:Month", + name="Battery Charge from Grid Month", + properties={ ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, }, - "format_energy", + formatter="format_energy", ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyChargeGrid:Year", - "Battery Charge from Grid Year", - { + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyChargeGrid:Year", + name="Battery Charge from Grid Year", + properties={ ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, }, - "format_energy", + formatter="format_energy", ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyChargeGrid:Total", - "Battery Charge from Grid Total", - { + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyChargeGrid:Total", + name="Battery Charge from Grid Total", + properties={ ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, }, - "format_energy", + formatter="format_energy", ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyChargePv:Day", - "Battery Charge from PV Day", - { + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyChargePv:Day", + name="Battery Charge from PV Day", + properties={ ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, }, - "format_energy", + formatter="format_energy", ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyChargePv:Month", - "Battery Charge from PV Month", - { + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyChargePv:Month", + name="Battery Charge from PV Month", + properties={ ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, }, - "format_energy", + formatter="format_energy", ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyChargePv:Year", - "Battery Charge from PV Year", - { + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyChargePv:Year", + name="Battery Charge from PV Year", + properties={ ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, }, - "format_energy", + formatter="format_energy", ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyChargePv:Total", - "Battery Charge from PV Total", - { + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyChargePv:Total", + name="Battery Charge from PV Total", + properties={ ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, }, - "format_energy", + formatter="format_energy", ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyDischargeGrid:Day", - "Energy Discharge to Grid Day", - { + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyDischargeGrid:Day", + name="Energy Discharge to Grid Day", + properties={ ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, }, - "format_energy", + formatter="format_energy", ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyDischargeGrid:Month", - "Energy Discharge to Grid Month", - { + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyDischargeGrid:Month", + name="Energy Discharge to Grid Month", + properties={ ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, }, - "format_energy", + formatter="format_energy", ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyDischargeGrid:Year", - "Energy Discharge to Grid Year", - { + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyDischargeGrid:Year", + name="Energy Discharge to Grid Year", + properties={ ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, }, - "format_energy", + formatter="format_energy", ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyDischargeGrid:Total", - "Energy Discharge to Grid Total", - { + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyDischargeGrid:Total", + name="Energy Discharge to Grid Total", + properties={ ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, }, - "format_energy", + formatter="format_energy", ), ] @@ -819,18 +833,12 @@ async def async_setup_entry( timedelta(seconds=10), plenticore, ) - module_id: str - data_id: str - name: str - sensor_data: dict[str, Any] - fmt: str - for ( # type: ignore[assignment] - module_id, - data_id, - name, - sensor_data, - fmt, - ) in SENSOR_PROCESS_DATA: + for description in SENSOR_PROCESS_DATA: + module_id = description.module_id + data_id = description.key + name = description.name + sensor_data = description.properties + fmt = description.formatter if ( module_id not in available_process_data or data_id not in available_process_data[module_id] @@ -868,7 +876,7 @@ class PlenticoreDataSensor(CoordinatorEntity, SensorEntity): platform_name: str, module_id: str, data_id: str, - sensor_name: str, + sensor_name: str | None, sensor_data: dict[str, Any], formatter: Callable[[str], Any], device_info: DeviceInfo, From a981d096aa1510b2579f87cfa877168e0bc05146 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 20 Sep 2022 23:18:38 -1000 Subject: [PATCH 603/955] Improve code readability in iBeacon integration (#78844) --- homeassistant/components/ibeacon/coordinator.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/ibeacon/coordinator.py b/homeassistant/components/ibeacon/coordinator.py index 8c256e5a5f0..58f973c0a7d 100644 --- a/homeassistant/components/ibeacon/coordinator.py +++ b/homeassistant/components/ibeacon/coordinator.py @@ -152,11 +152,9 @@ class IBeaconCoordinator: """Ignore an address that does not follow the spec and any entities created by it.""" self._ignore_addresses.add(address) self._async_cancel_unavailable_tracker(address) - self.hass.config_entries.async_update_entry( - self._entry, - data=self._entry.data - | {CONF_IGNORE_ADDRESSES: sorted(self._ignore_addresses)}, - ) + entry_data = self._entry.data + new_data = entry_data | {CONF_IGNORE_ADDRESSES: list(self._ignore_addresses)} + self.hass.config_entries.async_update_entry(self._entry, data=new_data) self._async_purge_untrackable_entities(self._unique_ids_by_address[address]) self._group_ids_by_address.pop(address) self._unique_ids_by_address.pop(address) From d7b58f6949e16d08879df0ee8216a777076815f7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 20 Sep 2022 23:21:14 -1000 Subject: [PATCH 604/955] Bump pySwitchbot to 0.19.11 (#78857) --- 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 bb670cc72d3..2c8172750c3 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.19.10"], + "requirements": ["PySwitchbot==0.19.11"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 74f93f267c8..f65d9c41f6c 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.19.10 +PySwitchbot==0.19.11 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 36822d4cbae..0c4c3a25639 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.19.10 +PySwitchbot==0.19.11 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From e265848b63af4fe1ad77c46a7193cb49eb0bfbee Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 21 Sep 2022 03:24:21 -0600 Subject: [PATCH 605/955] Remove deprecated Flu Near You integration (#78700) --- .coveragerc | 3 - .strict-typing | 1 - CODEOWNERS | 2 - .../components/flunearyou/__init__.py | 89 ------- .../components/flunearyou/config_flow.py | 60 ----- homeassistant/components/flunearyou/const.py | 8 - .../components/flunearyou/diagnostics.py | 32 --- .../components/flunearyou/manifest.json | 10 - .../components/flunearyou/repairs.py | 44 ---- homeassistant/components/flunearyou/sensor.py | 221 ------------------ .../components/flunearyou/strings.json | 33 --- .../flunearyou/translations/bg.json | 14 -- .../flunearyou/translations/ca.json | 33 --- .../flunearyou/translations/cs.json | 19 -- .../flunearyou/translations/de.json | 33 --- .../flunearyou/translations/el.json | 33 --- .../flunearyou/translations/en.json | 33 --- .../flunearyou/translations/es-419.json | 17 -- .../flunearyou/translations/es.json | 33 --- .../flunearyou/translations/et.json | 33 --- .../flunearyou/translations/fi.json | 7 - .../flunearyou/translations/fr.json | 25 -- .../flunearyou/translations/he.json | 18 -- .../flunearyou/translations/hu.json | 33 --- .../flunearyou/translations/id.json | 33 --- .../flunearyou/translations/it.json | 33 --- .../flunearyou/translations/ja.json | 33 --- .../flunearyou/translations/ko.json | 20 -- .../flunearyou/translations/lb.json | 20 -- .../flunearyou/translations/nl.json | 31 --- .../flunearyou/translations/no.json | 33 --- .../flunearyou/translations/pl.json | 33 --- .../flunearyou/translations/pt-BR.json | 33 --- .../flunearyou/translations/pt.json | 18 -- .../flunearyou/translations/ru.json | 33 --- .../flunearyou/translations/sk.json | 12 - .../flunearyou/translations/sl.json | 17 -- .../flunearyou/translations/sv.json | 33 --- .../flunearyou/translations/tr.json | 33 --- .../flunearyou/translations/uk.json | 20 -- .../flunearyou/translations/zh-Hans.json | 7 - .../flunearyou/translations/zh-Hant.json | 33 --- homeassistant/generated/config_flows.py | 1 - mypy.ini | 10 - requirements_all.txt | 3 - requirements_test_all.txt | 3 - tests/components/flunearyou/__init__.py | 1 - tests/components/flunearyou/conftest.py | 61 ----- .../flunearyou/fixtures/cdc_data.json | 10 - .../flunearyou/fixtures/user_data.json | 50 ---- .../components/flunearyou/test_config_flow.py | 52 ----- .../components/flunearyou/test_diagnostics.py | 67 ------ 52 files changed, 1567 deletions(-) delete mode 100644 homeassistant/components/flunearyou/__init__.py delete mode 100644 homeassistant/components/flunearyou/config_flow.py delete mode 100644 homeassistant/components/flunearyou/const.py delete mode 100644 homeassistant/components/flunearyou/diagnostics.py delete mode 100644 homeassistant/components/flunearyou/manifest.json delete mode 100644 homeassistant/components/flunearyou/repairs.py delete mode 100644 homeassistant/components/flunearyou/sensor.py delete mode 100644 homeassistant/components/flunearyou/strings.json delete mode 100644 homeassistant/components/flunearyou/translations/bg.json delete mode 100644 homeassistant/components/flunearyou/translations/ca.json delete mode 100644 homeassistant/components/flunearyou/translations/cs.json delete mode 100644 homeassistant/components/flunearyou/translations/de.json delete mode 100644 homeassistant/components/flunearyou/translations/el.json delete mode 100644 homeassistant/components/flunearyou/translations/en.json delete mode 100644 homeassistant/components/flunearyou/translations/es-419.json delete mode 100644 homeassistant/components/flunearyou/translations/es.json delete mode 100644 homeassistant/components/flunearyou/translations/et.json delete mode 100644 homeassistant/components/flunearyou/translations/fi.json delete mode 100644 homeassistant/components/flunearyou/translations/fr.json delete mode 100644 homeassistant/components/flunearyou/translations/he.json delete mode 100644 homeassistant/components/flunearyou/translations/hu.json delete mode 100644 homeassistant/components/flunearyou/translations/id.json delete mode 100644 homeassistant/components/flunearyou/translations/it.json delete mode 100644 homeassistant/components/flunearyou/translations/ja.json delete mode 100644 homeassistant/components/flunearyou/translations/ko.json delete mode 100644 homeassistant/components/flunearyou/translations/lb.json delete mode 100644 homeassistant/components/flunearyou/translations/nl.json delete mode 100644 homeassistant/components/flunearyou/translations/no.json delete mode 100644 homeassistant/components/flunearyou/translations/pl.json delete mode 100644 homeassistant/components/flunearyou/translations/pt-BR.json delete mode 100644 homeassistant/components/flunearyou/translations/pt.json delete mode 100644 homeassistant/components/flunearyou/translations/ru.json delete mode 100644 homeassistant/components/flunearyou/translations/sk.json delete mode 100644 homeassistant/components/flunearyou/translations/sl.json delete mode 100644 homeassistant/components/flunearyou/translations/sv.json delete mode 100644 homeassistant/components/flunearyou/translations/tr.json delete mode 100644 homeassistant/components/flunearyou/translations/uk.json delete mode 100644 homeassistant/components/flunearyou/translations/zh-Hans.json delete mode 100644 homeassistant/components/flunearyou/translations/zh-Hant.json delete mode 100644 tests/components/flunearyou/__init__.py delete mode 100644 tests/components/flunearyou/conftest.py delete mode 100644 tests/components/flunearyou/fixtures/cdc_data.json delete mode 100644 tests/components/flunearyou/fixtures/user_data.json delete mode 100644 tests/components/flunearyou/test_config_flow.py delete mode 100644 tests/components/flunearyou/test_diagnostics.py diff --git a/.coveragerc b/.coveragerc index 3daac135adb..c8d172ba30f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -402,9 +402,6 @@ omit = homeassistant/components/flume/coordinator.py homeassistant/components/flume/entity.py homeassistant/components/flume/sensor.py - homeassistant/components/flunearyou/__init__.py - homeassistant/components/flunearyou/repairs.py - homeassistant/components/flunearyou/sensor.py homeassistant/components/folder/sensor.py homeassistant/components/folder_watcher/* homeassistant/components/foobot/sensor.py diff --git a/.strict-typing b/.strict-typing index c9dc14967cb..8938e694a7e 100644 --- a/.strict-typing +++ b/.strict-typing @@ -103,7 +103,6 @@ homeassistant.components.feedreader.* homeassistant.components.file_upload.* homeassistant.components.filesize.* homeassistant.components.fitbit.* -homeassistant.components.flunearyou.* homeassistant.components.flux_led.* homeassistant.components.forecast_solar.* homeassistant.components.fritz.* diff --git a/CODEOWNERS b/CODEOWNERS index 8024c1dad17..87b7aafd1df 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -355,8 +355,6 @@ build.json @home-assistant/supervisor /tests/components/flo/ @dmulcahey /homeassistant/components/flume/ @ChrisMandich @bdraco @jeeftor /tests/components/flume/ @ChrisMandich @bdraco @jeeftor -/homeassistant/components/flunearyou/ @bachya -/tests/components/flunearyou/ @bachya /homeassistant/components/flux_led/ @icemanch @bdraco /tests/components/flux_led/ @icemanch @bdraco /homeassistant/components/forecast_solar/ @klaasnicolaas @frenck diff --git a/homeassistant/components/flunearyou/__init__.py b/homeassistant/components/flunearyou/__init__.py deleted file mode 100644 index ecdf05bbeb1..00000000000 --- a/homeassistant/components/flunearyou/__init__.py +++ /dev/null @@ -1,89 +0,0 @@ -"""The flunearyou component.""" -from __future__ import annotations - -import asyncio -from datetime import timedelta -from functools import partial -from typing import Any - -from pyflunearyou import Client -from pyflunearyou.errors import FluNearYouError - -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, Platform -from homeassistant.core import HomeAssistant -from homeassistant.helpers import aiohttp_client, config_validation as cv -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed - -from .const import CATEGORY_CDC_REPORT, CATEGORY_USER_REPORT, DOMAIN, LOGGER - -DEFAULT_UPDATE_INTERVAL = timedelta(minutes=30) - -CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) - -PLATFORMS = [Platform.SENSOR] - - -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Set up Flu Near You as config entry.""" - async_create_issue( - hass, - DOMAIN, - "integration_removal", - is_fixable=True, - severity=IssueSeverity.ERROR, - translation_key="integration_removal", - ) - - websession = aiohttp_client.async_get_clientsession(hass) - client = Client(session=websession) - - latitude = entry.data.get(CONF_LATITUDE, hass.config.latitude) - longitude = entry.data.get(CONF_LONGITUDE, hass.config.longitude) - - async def async_update(api_category: str) -> dict[str, Any]: - """Get updated date from the API based on category.""" - try: - if api_category == CATEGORY_CDC_REPORT: - data = await client.cdc_reports.status_by_coordinates( - latitude, longitude - ) - else: - data = await client.user_reports.status_by_coordinates( - latitude, longitude - ) - except FluNearYouError as err: - raise UpdateFailed(err) from err - - return data - - coordinators = {} - data_init_tasks = [] - - for api_category in (CATEGORY_CDC_REPORT, CATEGORY_USER_REPORT): - coordinator = coordinators[api_category] = DataUpdateCoordinator( - hass, - LOGGER, - name=f"{api_category} ({latitude}, {longitude})", - update_interval=DEFAULT_UPDATE_INTERVAL, - update_method=partial(async_update, api_category), - ) - data_init_tasks.append(coordinator.async_config_entry_first_refresh()) - - await asyncio.gather(*data_init_tasks) - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = coordinators - - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - - return True - - -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Unload an Flu Near You config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - if unload_ok: - hass.data[DOMAIN].pop(entry.entry_id) - - return unload_ok diff --git a/homeassistant/components/flunearyou/config_flow.py b/homeassistant/components/flunearyou/config_flow.py deleted file mode 100644 index 0005e0c257a..00000000000 --- a/homeassistant/components/flunearyou/config_flow.py +++ /dev/null @@ -1,60 +0,0 @@ -"""Define a config flow manager for flunearyou.""" -from __future__ import annotations - -from typing import Any - -from pyflunearyou import Client -from pyflunearyou.errors import FluNearYouError -import voluptuous as vol - -from homeassistant import config_entries -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE -from homeassistant.data_entry_flow import FlowResult -from homeassistant.helpers import aiohttp_client, config_validation as cv - -from .const import DOMAIN, LOGGER - - -class FluNearYouFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): - """Handle an FluNearYou config flow.""" - - VERSION = 1 - - @property - def data_schema(self) -> vol.Schema: - """Return the data schema for integration.""" - return vol.Schema( - { - vol.Required( - CONF_LATITUDE, default=self.hass.config.latitude - ): cv.latitude, - vol.Required( - CONF_LONGITUDE, default=self.hass.config.longitude - ): cv.longitude, - } - ) - - 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 self.async_show_form(step_id="user", data_schema=self.data_schema) - - unique_id = f"{user_input[CONF_LATITUDE]}, {user_input[CONF_LONGITUDE]}" - - await self.async_set_unique_id(unique_id) - self._abort_if_unique_id_configured() - - websession = aiohttp_client.async_get_clientsession(self.hass) - client = Client(session=websession) - - try: - await client.cdc_reports.status_by_coordinates( - user_input[CONF_LATITUDE], user_input[CONF_LONGITUDE] - ) - except FluNearYouError as err: - LOGGER.error("Error while configuring integration: %s", err) - return self.async_show_form(step_id="user", errors={"base": "unknown"}) - - return self.async_create_entry(title=unique_id, data=user_input) diff --git a/homeassistant/components/flunearyou/const.py b/homeassistant/components/flunearyou/const.py deleted file mode 100644 index dc9ac629d92..00000000000 --- a/homeassistant/components/flunearyou/const.py +++ /dev/null @@ -1,8 +0,0 @@ -"""Define flunearyou constants.""" -import logging - -DOMAIN = "flunearyou" -LOGGER = logging.getLogger(__package__) - -CATEGORY_CDC_REPORT = "cdc_report" -CATEGORY_USER_REPORT = "user_report" diff --git a/homeassistant/components/flunearyou/diagnostics.py b/homeassistant/components/flunearyou/diagnostics.py deleted file mode 100644 index 1f7812a7403..00000000000 --- a/homeassistant/components/flunearyou/diagnostics.py +++ /dev/null @@ -1,32 +0,0 @@ -"""Diagnostics support for Flu Near You.""" -from __future__ import annotations - -from typing import Any - -from homeassistant.components.diagnostics import async_redact_data -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE -from homeassistant.core import HomeAssistant -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator - -from .const import CATEGORY_CDC_REPORT, CATEGORY_USER_REPORT, DOMAIN - -TO_REDACT = { - CONF_LATITUDE, - CONF_LONGITUDE, -} - - -async def async_get_config_entry_diagnostics( - hass: HomeAssistant, entry: ConfigEntry -) -> dict[str, Any]: - """Return diagnostics for a config entry.""" - coordinators: dict[str, DataUpdateCoordinator] = hass.data[DOMAIN][entry.entry_id] - - return async_redact_data( - { - CATEGORY_CDC_REPORT: coordinators[CATEGORY_CDC_REPORT].data, - CATEGORY_USER_REPORT: coordinators[CATEGORY_USER_REPORT].data, - }, - TO_REDACT, - ) diff --git a/homeassistant/components/flunearyou/manifest.json b/homeassistant/components/flunearyou/manifest.json deleted file mode 100644 index ee69961d1b0..00000000000 --- a/homeassistant/components/flunearyou/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "domain": "flunearyou", - "name": "Flu Near You", - "config_flow": true, - "documentation": "https://www.home-assistant.io/integrations/flunearyou", - "requirements": ["pyflunearyou==2.0.2"], - "codeowners": ["@bachya"], - "iot_class": "cloud_polling", - "loggers": ["pyflunearyou"] -} diff --git a/homeassistant/components/flunearyou/repairs.py b/homeassistant/components/flunearyou/repairs.py deleted file mode 100644 index df81a1ae576..00000000000 --- a/homeassistant/components/flunearyou/repairs.py +++ /dev/null @@ -1,44 +0,0 @@ -"""Repairs platform for the Flu Near You integration.""" -from __future__ import annotations - -import asyncio - -import voluptuous as vol - -from homeassistant import data_entry_flow -from homeassistant.components.repairs import RepairsFlow -from homeassistant.core import HomeAssistant - -from .const import DOMAIN - - -class FluNearYouFixFlow(RepairsFlow): - """Handler for an issue fixing flow.""" - - async def async_step_init( - self, user_input: dict[str, str] | None = None - ) -> data_entry_flow.FlowResult: - """Handle the first step of a fix flow.""" - return await self.async_step_confirm() - - async def async_step_confirm( - self, user_input: dict[str, str] | None = None - ) -> data_entry_flow.FlowResult: - """Handle the confirm step of a fix flow.""" - if user_input is not None: - removal_tasks = [ - self.hass.config_entries.async_remove(entry.entry_id) - for entry in self.hass.config_entries.async_entries(DOMAIN) - ] - await asyncio.gather(*removal_tasks) - return self.async_create_entry(title="Fixed issue", data={}) - return self.async_show_form(step_id="confirm", data_schema=vol.Schema({})) - - -async def async_create_fix_flow( - hass: HomeAssistant, - issue_id: str, - data: dict[str, str | int | float | None] | None, -) -> RepairsFlow: - """Create flow.""" - return FluNearYouFixFlow() diff --git a/homeassistant/components/flunearyou/sensor.py b/homeassistant/components/flunearyou/sensor.py deleted file mode 100644 index f666e7412eb..00000000000 --- a/homeassistant/components/flunearyou/sensor.py +++ /dev/null @@ -1,221 +0,0 @@ -"""Support for user- and CDC-based flu info sensors from Flu Near You.""" -from __future__ import annotations - -from collections.abc import Mapping -from typing import Any, Union, cast - -from homeassistant.components.sensor import ( - SensorEntity, - SensorEntityDescription, - SensorStateClass, -) -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_STATE, CONF_LATITUDE, CONF_LONGITUDE -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import StateType -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, -) - -from .const import CATEGORY_CDC_REPORT, CATEGORY_USER_REPORT, DOMAIN - -ATTR_CITY = "city" -ATTR_REPORTED_DATE = "reported_date" -ATTR_REPORTED_LATITUDE = "reported_latitude" -ATTR_REPORTED_LONGITUDE = "reported_longitude" -ATTR_STATE_REPORTS_LAST_WEEK = "state_reports_last_week" -ATTR_STATE_REPORTS_THIS_WEEK = "state_reports_this_week" -ATTR_ZIP_CODE = "zip_code" - -SENSOR_TYPE_CDC_LEVEL = "level" -SENSOR_TYPE_CDC_LEVEL2 = "level2" -SENSOR_TYPE_USER_CHICK = "chick" -SENSOR_TYPE_USER_DENGUE = "dengue" -SENSOR_TYPE_USER_FLU = "flu" -SENSOR_TYPE_USER_LEPTO = "lepto" -SENSOR_TYPE_USER_NO_SYMPTOMS = "none" -SENSOR_TYPE_USER_SYMPTOMS = "symptoms" -SENSOR_TYPE_USER_TOTAL = "total" - -CDC_SENSOR_DESCRIPTIONS = ( - SensorEntityDescription( - key=SENSOR_TYPE_CDC_LEVEL, - name="CDC level", - icon="mdi:biohazard", - ), - SensorEntityDescription( - key=SENSOR_TYPE_CDC_LEVEL2, - name="CDC level 2", - icon="mdi:biohazard", - ), -) - -USER_SENSOR_DESCRIPTIONS = ( - SensorEntityDescription( - key=SENSOR_TYPE_USER_CHICK, - name="Avian flu symptoms", - icon="mdi:alert", - native_unit_of_measurement="reports", - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=SENSOR_TYPE_USER_DENGUE, - name="Dengue fever symptoms", - icon="mdi:alert", - native_unit_of_measurement="reports", - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=SENSOR_TYPE_USER_FLU, - name="Flu symptoms", - icon="mdi:alert", - native_unit_of_measurement="reports", - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=SENSOR_TYPE_USER_LEPTO, - name="Leptospirosis symptoms", - icon="mdi:alert", - native_unit_of_measurement="reports", - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=SENSOR_TYPE_USER_NO_SYMPTOMS, - name="No symptoms", - icon="mdi:alert", - native_unit_of_measurement="reports", - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=SENSOR_TYPE_USER_SYMPTOMS, - name="Flu-like symptoms", - icon="mdi:alert", - native_unit_of_measurement="reports", - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=SENSOR_TYPE_USER_TOTAL, - name="Total symptoms", - icon="mdi:alert", - native_unit_of_measurement="reports", - state_class=SensorStateClass.MEASUREMENT, - ), -) - -EXTENDED_SENSOR_TYPE_MAPPING = { - SENSOR_TYPE_USER_FLU: "ili", - SENSOR_TYPE_USER_NO_SYMPTOMS: "no_symptoms", - SENSOR_TYPE_USER_TOTAL: "total_surveys", -} - - -async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback -) -> None: - """Set up Flu Near You sensors based on a config entry.""" - coordinators = hass.data[DOMAIN][entry.entry_id] - - sensors: list[CdcSensor | UserSensor] = [ - CdcSensor(coordinators[CATEGORY_CDC_REPORT], entry, description) - for description in CDC_SENSOR_DESCRIPTIONS - ] - sensors.extend( - [ - UserSensor(coordinators[CATEGORY_USER_REPORT], entry, description) - for description in USER_SENSOR_DESCRIPTIONS - ] - ) - async_add_entities(sensors) - - -class FluNearYouSensor(CoordinatorEntity, SensorEntity): - """Define a base Flu Near You sensor.""" - - _attr_has_entity_name = True - - def __init__( - self, - coordinator: DataUpdateCoordinator, - entry: ConfigEntry, - description: SensorEntityDescription, - ) -> None: - """Initialize the sensor.""" - super().__init__(coordinator) - - self._attr_unique_id = ( - f"{entry.data[CONF_LATITUDE]}," - f"{entry.data[CONF_LONGITUDE]}_{description.key}" - ) - self._entry = entry - self.entity_description = description - - -class CdcSensor(FluNearYouSensor): - """Define a sensor for CDC reports.""" - - @property - def extra_state_attributes(self) -> Mapping[str, Any] | None: - """Return entity specific state attributes.""" - return { - ATTR_REPORTED_DATE: self.coordinator.data["week_date"], - ATTR_STATE: self.coordinator.data["name"], - } - - @property - def native_value(self) -> StateType: - """Return the value reported by the sensor.""" - return cast( - Union[str, None], self.coordinator.data[self.entity_description.key] - ) - - -class UserSensor(FluNearYouSensor): - """Define a sensor for user reports.""" - - @property - def extra_state_attributes(self) -> Mapping[str, Any] | None: - """Return entity specific state attributes.""" - attrs = { - ATTR_CITY: self.coordinator.data["local"]["city"].split("(")[0], - ATTR_REPORTED_LATITUDE: self.coordinator.data["local"]["latitude"], - ATTR_REPORTED_LONGITUDE: self.coordinator.data["local"]["longitude"], - ATTR_STATE: self.coordinator.data["state"]["name"], - ATTR_ZIP_CODE: self.coordinator.data["local"]["zip"], - } - - if self.entity_description.key in self.coordinator.data["state"]["data"]: - states_key = self.entity_description.key - elif self.entity_description.key in EXTENDED_SENSOR_TYPE_MAPPING: - states_key = EXTENDED_SENSOR_TYPE_MAPPING[self.entity_description.key] - - attrs[ATTR_STATE_REPORTS_THIS_WEEK] = self.coordinator.data["state"]["data"][ - states_key - ] - attrs[ATTR_STATE_REPORTS_LAST_WEEK] = self.coordinator.data["state"][ - "last_week_data" - ][states_key] - - return attrs - - @property - def native_value(self) -> StateType: - """Return the value reported by the sensor.""" - if self.entity_description.key == SENSOR_TYPE_USER_TOTAL: - value = sum( - v - for k, v in self.coordinator.data["local"].items() - if k - in ( - SENSOR_TYPE_USER_CHICK, - SENSOR_TYPE_USER_DENGUE, - SENSOR_TYPE_USER_FLU, - SENSOR_TYPE_USER_LEPTO, - SENSOR_TYPE_USER_SYMPTOMS, - ) - ) - else: - value = self.coordinator.data["local"][self.entity_description.key] - - return cast(int, value) diff --git a/homeassistant/components/flunearyou/strings.json b/homeassistant/components/flunearyou/strings.json deleted file mode 100644 index 59ec6125a34..00000000000 --- a/homeassistant/components/flunearyou/strings.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "config": { - "step": { - "user": { - "title": "Configure Flu Near You", - "description": "Monitor user-based and CDC reports for a pair of coordinates.", - "data": { - "latitude": "[%key:common::config_flow::data::latitude%]", - "longitude": "[%key:common::config_flow::data::longitude%]" - } - } - }, - "error": { - "unknown": "[%key:common::config_flow::error::unknown%]" - }, - "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_location%]" - } - }, - "issues": { - "integration_removal": { - "title": "Flu Near You is no longer available", - "fix_flow": { - "step": { - "confirm": { - "title": "Remove Flu Near You", - "description": "The external data source powering the Flu Near You integration is no longer available; thus, the integration no longer works.\n\nPress SUBMIT to remove Flu Near You from your Home Assistant instance." - } - } - } - } - } -} diff --git a/homeassistant/components/flunearyou/translations/bg.json b/homeassistant/components/flunearyou/translations/bg.json deleted file mode 100644 index 360abac2642..00000000000 --- a/homeassistant/components/flunearyou/translations/bg.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "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" - }, - "step": { - "user": { - "data": { - "latitude": "\u0413\u0435\u043e\u0433\u0440\u0430\u0444\u0441\u043a\u0430 \u0448\u0438\u0440\u0438\u043d\u0430" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/ca.json b/homeassistant/components/flunearyou/translations/ca.json deleted file mode 100644 index 9c4e55f8b54..00000000000 --- a/homeassistant/components/flunearyou/translations/ca.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "La ubicaci\u00f3 ja est\u00e0 configurada" - }, - "error": { - "unknown": "Error inesperat" - }, - "step": { - "user": { - "data": { - "latitude": "Latitud", - "longitude": "Longitud" - }, - "description": "Monitoritza informes basats en usuari i CDC per a parells de coordenades.", - "title": "Configuraci\u00f3 Flu Near You" - } - } - }, - "issues": { - "integration_removal": { - "fix_flow": { - "step": { - "confirm": { - "description": "La font de dades externa que alimenta la integraci\u00f3 Flu Near You ja no est\u00e0 disponible; per tant, la integraci\u00f3 ja no funciona. \n\nPrem ENVIAR per eliminar Flu Near You de Home Assistant.", - "title": "Elimina Flu Near You" - } - } - }, - "title": "Flu Near You ja no est\u00e0 disponible" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/cs.json b/homeassistant/components/flunearyou/translations/cs.json deleted file mode 100644 index 64cb764a52d..00000000000 --- a/homeassistant/components/flunearyou/translations/cs.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Um\u00edst\u011bn\u00ed je ji\u017e nastaveno" - }, - "error": { - "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" - }, - "step": { - "user": { - "data": { - "latitude": "Zem\u011bpisn\u00e1 \u0161\u00ed\u0159ka", - "longitude": "Zem\u011bpisn\u00e1 d\u00e9lka" - }, - "title": "Nastaven\u00ed Flu Near You" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/de.json b/homeassistant/components/flunearyou/translations/de.json deleted file mode 100644 index 4e2287ad9ca..00000000000 --- a/homeassistant/components/flunearyou/translations/de.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Standort ist bereits konfiguriert" - }, - "error": { - "unknown": "Unerwarteter Fehler" - }, - "step": { - "user": { - "data": { - "latitude": "Breitengrad", - "longitude": "L\u00e4ngengrad" - }, - "description": "\u00dcberwache benutzerbasierte und CDC-Berichte f\u00fcr ein Koordinatenpaar.", - "title": "Konfiguriere Grippe in deiner N\u00e4he" - } - } - }, - "issues": { - "integration_removal": { - "fix_flow": { - "step": { - "confirm": { - "description": "Die externe Datenquelle, aus der die Integration von Flu Near You gespeist wird, ist nicht mehr verf\u00fcgbar; daher funktioniert die Integration nicht mehr.\n\nDr\u00fccke SENDEN, um Flu Near You aus deiner Home Assistant-Instanz zu entfernen.", - "title": "Flu Near You entfernen" - } - } - }, - "title": "Flu Near You ist nicht mehr verf\u00fcgbar" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/el.json b/homeassistant/components/flunearyou/translations/el.json deleted file mode 100644 index ca6fae97190..00000000000 --- a/homeassistant/components/flunearyou/translations/el.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "\u0397 \u03c4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" - }, - "error": { - "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" - }, - "step": { - "user": { - "data": { - "latitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03c0\u03bb\u03ac\u03c4\u03bf\u03c2", - "longitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03bc\u03ae\u03ba\u03bf\u03c2" - }, - "description": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03cd\u03b8\u03b7\u03c3\u03b7 \u03b1\u03bd\u03b1\u03c6\u03bf\u03c1\u03ce\u03bd \u03b2\u03ac\u03c3\u03b5\u03b9 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03ba\u03b1\u03b9 CDC \u03b3\u03b9\u03b1 \u03ad\u03bd\u03b1 \u03b6\u03b5\u03cd\u03b3\u03bf\u03c2 \u03c3\u03c5\u03bd\u03c4\u03b5\u03c4\u03b1\u03b3\u03bc\u03ad\u03bd\u03c9\u03bd.", - "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Flu Near You" - } - } - }, - "issues": { - "integration_removal": { - "fix_flow": { - "step": { - "confirm": { - "description": "\u0397 \u03b5\u03be\u03c9\u03c4\u03b5\u03c1\u03b9\u03ba\u03ae \u03c0\u03b7\u03b3\u03ae \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03c9\u03bd \u03c0\u03bf\u03c5 \u03c4\u03c1\u03bf\u03c6\u03bf\u03b4\u03bf\u03c4\u03b5\u03af \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 Flu Near You \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b7. \u0395\u03c0\u03bf\u03bc\u03ad\u03bd\u03c9\u03c2, \u03b7 \u03b5\u03bd\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b4\u03b5\u03bd \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03b5\u03af \u03c0\u03bb\u03ad\u03bf\u03bd. \n\n \u03a0\u03b1\u03c4\u03ae\u03c3\u03c4\u03b5 SUBMIT \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b1\u03c6\u03b1\u03b9\u03c1\u03ad\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03b3\u03c1\u03af\u03c0\u03b7 \u03ba\u03bf\u03bd\u03c4\u03ac \u03c3\u03b1\u03c2 \u03b1\u03c0\u03cc \u03c4\u03b7\u03bd \u03c0\u03b1\u03c1\u03bf\u03c5\u03c3\u03af\u03b1 \u03c4\u03bf\u03c5 Home Assistant.", - "title": "\u0391\u03c6\u03b1\u03b9\u03c1\u03ad\u03c3\u03c4\u03b5 \u03c4\u03bf Flu Near You" - } - } - }, - "title": "\u03a4\u03bf Flu Near You \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03bf" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/en.json b/homeassistant/components/flunearyou/translations/en.json deleted file mode 100644 index 7e76b54b18a..00000000000 --- a/homeassistant/components/flunearyou/translations/en.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Location is already configured" - }, - "error": { - "unknown": "Unexpected error" - }, - "step": { - "user": { - "data": { - "latitude": "Latitude", - "longitude": "Longitude" - }, - "description": "Monitor user-based and CDC reports for a pair of coordinates.", - "title": "Configure Flu Near You" - } - } - }, - "issues": { - "integration_removal": { - "fix_flow": { - "step": { - "confirm": { - "description": "The external data source powering the Flu Near You integration is no longer available; thus, the integration no longer works.\n\nPress SUBMIT to remove Flu Near You from your Home Assistant instance.", - "title": "Remove Flu Near You" - } - } - }, - "title": "Flu Near You is no longer available" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/es-419.json b/homeassistant/components/flunearyou/translations/es-419.json deleted file mode 100644 index 726a898a8b6..00000000000 --- a/homeassistant/components/flunearyou/translations/es-419.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Estas coordenadas ya est\u00e1n registradas." - }, - "step": { - "user": { - "data": { - "latitude": "Latitud", - "longitude": "Longitud" - }, - "description": "Monitoree los repotes basados en el usuario y los CDC para un par de coordenadas.", - "title": "Configurar Flu Near You (Gripe cerca de usted)" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/es.json b/homeassistant/components/flunearyou/translations/es.json deleted file mode 100644 index a7d9cf89f6e..00000000000 --- a/homeassistant/components/flunearyou/translations/es.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "La ubicaci\u00f3n ya est\u00e1 configurada" - }, - "error": { - "unknown": "Error inesperado" - }, - "step": { - "user": { - "data": { - "latitude": "Latitud", - "longitude": "Longitud" - }, - "description": "Supervisa los informes del CDC y los basados en los usuarios para un par de coordenadas.", - "title": "Configurar Flu Near You" - } - } - }, - "issues": { - "integration_removal": { - "fix_flow": { - "step": { - "confirm": { - "description": "La fuente de datos externa que alimenta la integraci\u00f3n Flu Near You ya no est\u00e1 disponible; por lo tanto, la integraci\u00f3n ya no funciona. \n\nPulsa ENVIAR para eliminar Flu Near You de tu instancia Home Assistant.", - "title": "Eliminar Flu Near You" - } - } - }, - "title": "Flu Near You ya no est\u00e1 disponible" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/et.json b/homeassistant/components/flunearyou/translations/et.json deleted file mode 100644 index 447ad54c25a..00000000000 --- a/homeassistant/components/flunearyou/translations/et.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Asukoht on juba h\u00e4\u00e4lestatud" - }, - "error": { - "unknown": "Tundmatu viga" - }, - "step": { - "user": { - "data": { - "latitude": "Laiuskraad", - "longitude": "Pikkuskraad" - }, - "description": "Kasutajap\u00f5histe ja CDC-aruannete j\u00e4lgimine antud asukohas.", - "title": "Seadista Flu Near You" - } - } - }, - "issues": { - "integration_removal": { - "fix_flow": { - "step": { - "confirm": { - "description": "Flu Near You integratsiooni k\u00e4ivitav v\u00e4line andmeallikas ei ole enam saadaval; seega ei t\u00f6\u00f6ta integratsioon enam.\n\nVajutage SUBMIT, et eemaldada Flu Near You oma Home Assistant'i instantsist.", - "title": "Eemalda Flu Near You" - } - } - }, - "title": "Flu Near You pole enam saadaval" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/fi.json b/homeassistant/components/flunearyou/translations/fi.json deleted file mode 100644 index b751fda5e4c..00000000000 --- a/homeassistant/components/flunearyou/translations/fi.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "config": { - "error": { - "unknown": "Odottamaton virhe" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/fr.json b/homeassistant/components/flunearyou/translations/fr.json deleted file mode 100644 index ebc2ccc385a..00000000000 --- a/homeassistant/components/flunearyou/translations/fr.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "L'emplacement est d\u00e9j\u00e0 configur\u00e9" - }, - "error": { - "unknown": "Erreur inattendue" - }, - "step": { - "user": { - "data": { - "latitude": "Latitude", - "longitude": "Longitude" - }, - "description": "Surveillez les rapports des utilisateurs et du CDC pour des coordonn\u00e9es.", - "title": "Configurer Flu Near You" - } - } - }, - "issues": { - "integration_removal": { - "title": "Flu Near You n'est plus disponible" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/he.json b/homeassistant/components/flunearyou/translations/he.json deleted file mode 100644 index 02a79d5fbcc..00000000000 --- a/homeassistant/components/flunearyou/translations/he.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05de\u05d9\u05e7\u05d5\u05dd \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" - }, - "error": { - "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" - }, - "step": { - "user": { - "data": { - "latitude": "\u05e7\u05d5 \u05e8\u05d5\u05d7\u05d1", - "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/hu.json b/homeassistant/components/flunearyou/translations/hu.json deleted file mode 100644 index efda42723b4..00000000000 --- a/homeassistant/components/flunearyou/translations/hu.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "A hely m\u00e1r konfigur\u00e1lva van" - }, - "error": { - "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" - }, - "step": { - "user": { - "data": { - "latitude": "Sz\u00e9less\u00e9g", - "longitude": "Hossz\u00fas\u00e1g" - }, - "description": "Figyelje a felhaszn\u00e1l\u00f3alap\u00fa \u00e9s a CDC jelent\u00e9seket egy p\u00e1r koordin\u00e1t\u00e1ra.", - "title": "Flu Near You weboldal konfigur\u00e1l\u00e1sa" - } - } - }, - "issues": { - "integration_removal": { - "fix_flow": { - "step": { - "confirm": { - "description": "A Flu Near You integr\u00e1ci\u00f3t m\u0171k\u00f6dtet\u0151 k\u00fcls\u0151 adatforr\u00e1s m\u00e1r nem el\u00e9rhet\u0151, \u00edgy az integr\u00e1ci\u00f3 m\u00e1r nem m\u0171k\u00f6dik.\n\nNyomja meg a MEHET gombot a Flu Near You elt\u00e1vol\u00edt\u00e1s\u00e1hoz a Home Assistantb\u00f3l.", - "title": "Flu Near You elt\u00e1vol\u00edt\u00e1sa" - } - } - }, - "title": "Flu Near You m\u00e1r nem el\u00e9rhet\u0151" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/id.json b/homeassistant/components/flunearyou/translations/id.json deleted file mode 100644 index 72fcfefc78d..00000000000 --- a/homeassistant/components/flunearyou/translations/id.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Lokasi sudah dikonfigurasi" - }, - "error": { - "unknown": "Kesalahan yang tidak diharapkan" - }, - "step": { - "user": { - "data": { - "latitude": "Lintang", - "longitude": "Bujur" - }, - "description": "Pantau laporan berbasis pengguna dan CDC berdasarkan data koordinat.", - "title": "Konfigurasikan Flu Near You" - } - } - }, - "issues": { - "integration_removal": { - "fix_flow": { - "step": { - "confirm": { - "description": "Sumber data eksternal yang mendukung integrasi Flu Near You tidak lagi tersedia, sehingga integrasi tidak lagi berfungsi. \n\nTekan KIRIM untuk menghapus integrasi Flu Near You dari instans Home Assistant Anda.", - "title": "Hapus Integrasi Flu Near You" - } - } - }, - "title": "Integrasi Flu Year You tidak lagi tersedia" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/it.json b/homeassistant/components/flunearyou/translations/it.json deleted file mode 100644 index a8939f2d18c..00000000000 --- a/homeassistant/components/flunearyou/translations/it.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "La posizione \u00e8 gi\u00e0 configurata" - }, - "error": { - "unknown": "Errore imprevisto" - }, - "step": { - "user": { - "data": { - "latitude": "Latitudine", - "longitude": "Logitudine" - }, - "description": "Monitorare i rapporti basati su utenti e quelli della CDC per una coppia di coordinate.", - "title": "Configurare Flu Near You" - } - } - }, - "issues": { - "integration_removal": { - "fix_flow": { - "step": { - "confirm": { - "description": "L'origine dati esterna che alimenta l'integrazione Flu Near You non \u00e8 pi\u00f9 disponibile; quindi, l'integrazione non funziona pi\u00f9. \n\nPremi INVIA per rimuovere Flu Near You dall'istanza di Home Assistant.", - "title": "Rimuovi Flu Near You" - } - } - }, - "title": "Flu Near You non \u00e8 pi\u00f9 disponibile" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/ja.json b/homeassistant/components/flunearyou/translations/ja.json deleted file mode 100644 index 06cf83e27be..00000000000 --- a/homeassistant/components/flunearyou/translations/ja.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "\u30ed\u30b1\u30fc\u30b7\u30e7\u30f3\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" - }, - "error": { - "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" - }, - "step": { - "user": { - "data": { - "latitude": "\u7def\u5ea6", - "longitude": "\u7d4c\u5ea6" - }, - "description": "\u30e6\u30fc\u30b6\u30fc\u30d9\u30fc\u30b9\u306e\u30ec\u30dd\u30fc\u30c8\u3068CDC\u306e\u30ec\u30dd\u30fc\u30c8\u3092\u30da\u30a2\u306b\u3057\u3066\u5ea7\u6a19\u3067\u30e2\u30cb\u30bf\u30fc\u3057\u307e\u3059\u3002", - "title": "\u8fd1\u304f\u306eFlu\u3092\u8a2d\u5b9a" - } - } - }, - "issues": { - "integration_removal": { - "fix_flow": { - "step": { - "confirm": { - "description": "Flu Near You\u3068\u306e\u7d71\u5408\u306b\u5fc5\u8981\u306a\u5916\u90e8\u30c7\u30fc\u30bf\u30bd\u30fc\u30b9\u304c\u4f7f\u7528\u3067\u304d\u306a\u304f\u306a\u3063\u305f\u305f\u3081\u3001\u7d71\u5408\u306f\u6a5f\u80fd\u3057\u306a\u304f\u306a\u308a\u307e\u3057\u305f\u3002\n\nSUBMIT\u3092\u62bc\u3057\u3066\u3001Flu Near You\u3092Home Assistant\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u304b\u3089\u524a\u9664\u3057\u307e\u3059\u3002", - "title": "\u8fd1\u304f\u306eFlu Near You\u3092\u524a\u9664" - } - } - }, - "title": "Flu Near You\u306f\u3001\u5229\u7528\u3067\u304d\u306a\u304f\u306a\u308a\u307e\u3057\u305f" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/ko.json b/homeassistant/components/flunearyou/translations/ko.json deleted file mode 100644 index bfe1945fa67..00000000000 --- a/homeassistant/components/flunearyou/translations/ko.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "\uc704\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" - }, - "error": { - "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" - }, - "step": { - "user": { - "data": { - "latitude": "\uc704\ub3c4", - "longitude": "\uacbd\ub3c4" - }, - "description": "\uc0ac\uc6a9\uc790 \uae30\ubc18 \ub370\uc774\ud130 \ubc0f CDC \ubcf4\uace0\uc11c\uc5d0\uc11c \uc88c\ud45c\ub97c \ubaa8\ub2c8\ud130\ub9c1\ud569\ub2c8\ub2e4.", - "title": "Flu Near You \uad6c\uc131\ud558\uae30" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/lb.json b/homeassistant/components/flunearyou/translations/lb.json deleted file mode 100644 index 457f64f34a7..00000000000 --- a/homeassistant/components/flunearyou/translations/lb.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Standuert ass scho konfigur\u00e9iert" - }, - "error": { - "unknown": "Onerwaarte Feeler" - }, - "step": { - "user": { - "data": { - "latitude": "Breedegrad", - "longitude": "L\u00e4ngegrad" - }, - "description": "Iwwerwach Benotzer-bas\u00e9iert an CDC Berichter fir Koordinaten.", - "title": "Flu Near You konfigur\u00e9ieren" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/nl.json b/homeassistant/components/flunearyou/translations/nl.json deleted file mode 100644 index b38da0e9901..00000000000 --- a/homeassistant/components/flunearyou/translations/nl.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Locatie is al geconfigureerd" - }, - "error": { - "unknown": "Onverwachte fout" - }, - "step": { - "user": { - "data": { - "latitude": "Breedtegraad", - "longitude": "Lengtegraad" - }, - "description": "Bewaak gebruikersgebaseerde en CDC-rapporten voor een paar co\u00f6rdinaten.", - "title": "Configureer \nFlu Near You" - } - } - }, - "issues": { - "integration_removal": { - "fix_flow": { - "step": { - "confirm": { - "title": "Flu Near You verwijderen" - } - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/no.json b/homeassistant/components/flunearyou/translations/no.json deleted file mode 100644 index 898889bc348..00000000000 --- a/homeassistant/components/flunearyou/translations/no.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Plasseringen er allerede konfigurert" - }, - "error": { - "unknown": "Uventet feil" - }, - "step": { - "user": { - "data": { - "latitude": "Breddegrad", - "longitude": "Lengdegrad" - }, - "description": "Overv\u00e5k brukerbaserte rapporter og CDC-rapporter for et par koordinater.", - "title": "Konfigurere influensa i n\u00e6rheten av deg" - } - } - }, - "issues": { - "integration_removal": { - "fix_flow": { - "step": { - "confirm": { - "description": "Den eksterne datakilden som driver Flu Near You-integrasjonen er ikke lenger tilgjengelig; dermed fungerer ikke integreringen lenger. \n\n Trykk SUBMIT for \u00e5 fjerne Flu Near You fra Home Assistant-forekomsten.", - "title": "Fjern influensa n\u00e6r deg" - } - } - }, - "title": "Flu Near You er ikke lenger tilgjengelig" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/pl.json b/homeassistant/components/flunearyou/translations/pl.json deleted file mode 100644 index 71d390992e4..00000000000 --- a/homeassistant/components/flunearyou/translations/pl.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Lokalizacja jest ju\u017c skonfigurowana" - }, - "error": { - "unknown": "Nieoczekiwany b\u0142\u0105d" - }, - "step": { - "user": { - "data": { - "latitude": "Szeroko\u015b\u0107 geograficzna", - "longitude": "D\u0142ugo\u015b\u0107 geograficzna" - }, - "description": "Monitoruj raporty oparte na u\u017cytkownikach i CDC dla pary wsp\u00f3\u0142rz\u0119dnych.", - "title": "Konfiguracja Flu Near You" - } - } - }, - "issues": { - "integration_removal": { - "fix_flow": { - "step": { - "confirm": { - "description": "Zewn\u0119trzne \u017ar\u00f3d\u0142o danych dla integracji Flu Near You nie jest ju\u017c dost\u0119pne; tym sposobem integracja ju\u017c nie dzia\u0142a. \n\nNaci\u015bnij ZATWIERD\u0179, aby usun\u0105\u0107 Flu Near You z Home Assistanta.", - "title": "Usu\u0144 Flu Near You" - } - } - }, - "title": "Flu Near You nie jest ju\u017c dost\u0119pna" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/pt-BR.json b/homeassistant/components/flunearyou/translations/pt-BR.json deleted file mode 100644 index eeca693be89..00000000000 --- a/homeassistant/components/flunearyou/translations/pt-BR.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Localiza\u00e7\u00e3o j\u00e1 est\u00e1 configurada" - }, - "error": { - "unknown": "Erro inesperado" - }, - "step": { - "user": { - "data": { - "latitude": "Latitude", - "longitude": "Longitude" - }, - "description": "Monitore relat\u00f3rios baseados em usu\u00e1rio e CDC para um par de coordenadas.", - "title": "Configurar Flue Near You" - } - } - }, - "issues": { - "integration_removal": { - "fix_flow": { - "step": { - "confirm": { - "description": "A fonte de dados externa que alimenta a integra\u00e7\u00e3o do Flu Near You n\u00e3o est\u00e1 mais dispon\u00edvel; assim, a integra\u00e7\u00e3o n\u00e3o funciona mais. \n\n Pressione ENVIAR para remover Flu Near You da sua inst\u00e2ncia do Home Assistant.", - "title": "Remova Flu Near You" - } - } - }, - "title": "Flu Near You n\u00e3o est\u00e1 mais dispon\u00edvel" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/pt.json b/homeassistant/components/flunearyou/translations/pt.json deleted file mode 100644 index 219446a038d..00000000000 --- a/homeassistant/components/flunearyou/translations/pt.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "A localiza\u00e7\u00e3o j\u00e1 est\u00e1 configurada" - }, - "error": { - "unknown": "Erro inesperado" - }, - "step": { - "user": { - "data": { - "latitude": "Latitude", - "longitude": "Longitude" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/ru.json b/homeassistant/components/flunearyou/translations/ru.json deleted file mode 100644 index d9f184e05a6..00000000000 --- a/homeassistant/components/flunearyou/translations/ru.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." - }, - "error": { - "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." - }, - "step": { - "user": { - "data": { - "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", - "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430" - }, - "description": "\u041c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0445 \u0438 CDC \u043e\u0442\u0447\u0435\u0442\u043e\u0432 \u043f\u043e \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u043c \u043a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u0430\u043c.", - "title": "Flu Near You" - } - } - }, - "issues": { - "integration_removal": { - "fix_flow": { - "step": { - "confirm": { - "description": "\u0412\u043d\u0435\u0448\u043d\u0438\u0439 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a \u0434\u0430\u043d\u043d\u044b\u0445, \u043e\u0431\u0435\u0441\u043f\u0435\u0447\u0438\u0432\u0430\u044e\u0449\u0438\u0439 \u0440\u0430\u0431\u043e\u0442\u0443 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 Flu Near You, \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d.\n\n\u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c, \u0447\u0442\u043e\u0431\u044b \u0443\u0434\u0430\u043b\u0438\u0442\u044c Flu Near You \u0438\u0437 \u0412\u0430\u0448\u0435\u0433\u043e Home Assistant.", - "title": "\u0423\u0434\u0430\u043b\u0435\u043d\u0438\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 Flu Near You" - } - } - }, - "title": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f Flu Near You \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/sk.json b/homeassistant/components/flunearyou/translations/sk.json deleted file mode 100644 index e6945904d90..00000000000 --- a/homeassistant/components/flunearyou/translations/sk.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "config": { - "step": { - "user": { - "data": { - "latitude": "Zemepisn\u00e1 \u0161\u00edrka", - "longitude": "Zemepisn\u00e1 d\u013a\u017eka" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/sl.json b/homeassistant/components/flunearyou/translations/sl.json deleted file mode 100644 index 667f0e3c0ed..00000000000 --- a/homeassistant/components/flunearyou/translations/sl.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Te koordinate so \u017ee registrirane." - }, - "step": { - "user": { - "data": { - "latitude": "Zemljepisna \u0161irina", - "longitude": "Zemljepisna dol\u017eina" - }, - "description": "Spremljajte uporabni\u0161ke in CDC obvestila za dolo\u010dene koordinate.", - "title": "Konfigurirajte Flu Near You" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/sv.json b/homeassistant/components/flunearyou/translations/sv.json deleted file mode 100644 index 0ce55468d86..00000000000 --- a/homeassistant/components/flunearyou/translations/sv.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Dessa koordinater \u00e4r redan registrerade." - }, - "error": { - "unknown": "Ov\u00e4ntat fel" - }, - "step": { - "user": { - "data": { - "latitude": "Latitud", - "longitude": "Longitud" - }, - "description": "\u00d6vervaka anv\u00e4ndarbaserade och CDC-rapporter f\u00f6r ett par koordinater.", - "title": "Konfigurera influensa i n\u00e4rheten av dig" - } - } - }, - "issues": { - "integration_removal": { - "fix_flow": { - "step": { - "confirm": { - "description": "Den externa datak\u00e4llan som driver Flu Near You-integrationen \u00e4r inte l\u00e4ngre tillg\u00e4nglig; allts\u00e5 fungerar inte integrationen l\u00e4ngre. \n\n Tryck p\u00e5 SUBMIT f\u00f6r att ta bort Flu Near You fr\u00e5n din Home Assistant-instans.", - "title": "Ta bort influensa n\u00e4ra dig" - } - } - }, - "title": "Influensa n\u00e4ra dig \u00e4r inte l\u00e4ngre tillg\u00e4nglig" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/tr.json b/homeassistant/components/flunearyou/translations/tr.json deleted file mode 100644 index 1e762c75f7a..00000000000 --- a/homeassistant/components/flunearyou/translations/tr.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Konum zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" - }, - "error": { - "unknown": "Beklenmeyen hata" - }, - "step": { - "user": { - "data": { - "latitude": "Enlem", - "longitude": "Boylam" - }, - "description": "Bir \u00e7ift koordinat i\u00e7in kullan\u0131c\u0131 tabanl\u0131 raporlar\u0131 ve CDC raporlar\u0131n\u0131 izleyin.", - "title": "Flu Near You'yu Yap\u0131land\u0131r\u0131n" - } - } - }, - "issues": { - "integration_removal": { - "fix_flow": { - "step": { - "confirm": { - "description": "Flu Near You entegrasyonuna g\u00fc\u00e7 sa\u011flayan harici veri kayna\u011f\u0131 art\u0131k mevcut de\u011fil; bu nedenle, entegrasyon art\u0131k \u00e7al\u0131\u015fm\u0131yor. \n\n Home Assistant \u00f6rne\u011finden Flu Near You'yu kald\u0131rmak i\u00e7in G\u00d6NDER'e bas\u0131n.", - "title": "Yak\u0131n\u0131n\u0131zdaki Flu Near You'yu Kald\u0131r\u0131n" - } - } - }, - "title": "Flu Near You art\u0131k mevcut de\u011fil" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/uk.json b/homeassistant/components/flunearyou/translations/uk.json deleted file mode 100644 index 354a04d8e7a..00000000000 --- a/homeassistant/components/flunearyou/translations/uk.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u043b\u044f \u0446\u044c\u043e\u0433\u043e \u043c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0435." - }, - "error": { - "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" - }, - "step": { - "user": { - "data": { - "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", - "longitude": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430" - }, - "description": "\u041c\u043e\u043d\u0456\u0442\u043e\u0440\u0438\u043d\u0433 \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0446\u044c\u043a\u0438\u0445 \u0456 CDC \u0437\u0432\u0456\u0442\u0456\u0432 \u0437\u0430 \u0432\u043a\u0430\u0437\u0430\u043d\u0438\u043c\u0438 \u043a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u0430\u043c\u0438.", - "title": "Flu Near You" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/zh-Hans.json b/homeassistant/components/flunearyou/translations/zh-Hans.json deleted file mode 100644 index f55159dc235..00000000000 --- a/homeassistant/components/flunearyou/translations/zh-Hans.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "\u4f4d\u7f6e\u5b8c\u6210\u914d\u7f6e" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/zh-Hant.json b/homeassistant/components/flunearyou/translations/zh-Hant.json deleted file mode 100644 index 42b975c451b..00000000000 --- a/homeassistant/components/flunearyou/translations/zh-Hant.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "\u5ea7\u6a19\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" - }, - "error": { - "unknown": "\u672a\u9810\u671f\u932f\u8aa4" - }, - "step": { - "user": { - "data": { - "latitude": "\u7def\u5ea6", - "longitude": "\u7d93\u5ea6" - }, - "description": "\u76e3\u6e2c\u4f7f\u7528\u8005\u8207 CDC \u56de\u5831\u76f8\u5c0d\u61c9\u5ea7\u6a19\u3002", - "title": "\u8a2d\u5b9a Flu Near You" - } - } - }, - "issues": { - "integration_removal": { - "fix_flow": { - "step": { - "confirm": { - "description": "The external data source powering the Flu Near You \u6574\u5408\u6240\u4f7f\u7528\u7684\u5916\u90e8\u8cc7\u6599\u4f86\u6e90\u5df2\u7d93\u7121\u6cd5\u4f7f\u7528\uff0c\u6574\u5408\u7121\u6cd5\u4f7f\u7528\u3002\n\n\u6309\u4e0b\u300c\u50b3\u9001\u300d\u4ee5\u79fb\u9664\u7531 Home Assistant \u79fb\u9664 Flu Near You\u3002", - "title": "\u79fb\u9664 Flu Near You" - } - } - }, - "title": "Flu Near You \u5df2\u7d93\u7121\u6cd5\u4f7f\u7528" - } - } -} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 038596d3e23..08ec809b0f1 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -114,7 +114,6 @@ FLOWS = { "flipr", "flo", "flume", - "flunearyou", "flux_led", "forecast_solar", "forked_daapd", diff --git a/mypy.ini b/mypy.ini index 70f45c24f62..827d47d12ba 100644 --- a/mypy.ini +++ b/mypy.ini @@ -782,16 +782,6 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true -[mypy-homeassistant.components.flunearyou.*] -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.flux_led.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index f65d9c41f6c..4af838b99bb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1557,9 +1557,6 @@ pyflic==2.0.3 # homeassistant.components.flume pyflume==0.6.5 -# homeassistant.components.flunearyou -pyflunearyou==2.0.2 - # homeassistant.components.futurenow pyfnip==0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0c4c3a25639..2cd54e4f37f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1085,9 +1085,6 @@ pyflic==2.0.3 # homeassistant.components.flume pyflume==0.6.5 -# homeassistant.components.flunearyou -pyflunearyou==2.0.2 - # homeassistant.components.forked_daapd pyforked-daapd==0.1.11 diff --git a/tests/components/flunearyou/__init__.py b/tests/components/flunearyou/__init__.py deleted file mode 100644 index 21252facd75..00000000000 --- a/tests/components/flunearyou/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Define tests for the flunearyou component.""" diff --git a/tests/components/flunearyou/conftest.py b/tests/components/flunearyou/conftest.py deleted file mode 100644 index a3de61149aa..00000000000 --- a/tests/components/flunearyou/conftest.py +++ /dev/null @@ -1,61 +0,0 @@ -"""Define fixtures for Flu Near You tests.""" -import json -from unittest.mock import patch - -import pytest - -from homeassistant.components.flunearyou.const import DOMAIN -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE -from homeassistant.setup import async_setup_component - -from tests.common import MockConfigEntry, load_fixture - - -@pytest.fixture(name="config_entry") -def config_entry_fixture(hass, config, unique_id): - """Define a config entry fixture.""" - entry = MockConfigEntry(domain=DOMAIN, unique_id=unique_id, data=config) - entry.add_to_hass(hass) - return entry - - -@pytest.fixture(name="config") -def config_fixture(hass): - """Define a config entry data fixture.""" - return { - CONF_LATITUDE: 51.528308, - CONF_LONGITUDE: -0.3817765, - } - - -@pytest.fixture(name="data_cdc", scope="session") -def data_cdc_fixture(): - """Define CDC data.""" - return json.loads(load_fixture("cdc_data.json", "flunearyou")) - - -@pytest.fixture(name="data_user", scope="session") -def data_user_fixture(): - """Define user data.""" - return json.loads(load_fixture("user_data.json", "flunearyou")) - - -@pytest.fixture(name="setup_flunearyou") -async def setup_flunearyou_fixture(hass, data_cdc, data_user, config): - """Define a fixture to set up Flu Near You.""" - with patch( - "pyflunearyou.cdc.CdcReport.status_by_coordinates", return_value=data_cdc - ), patch( - "pyflunearyou.user.UserReport.status_by_coordinates", return_value=data_user - ), patch( - "homeassistant.components.flunearyou.PLATFORMS", [] - ): - assert await async_setup_component(hass, DOMAIN, config) - await hass.async_block_till_done() - yield - - -@pytest.fixture(name="unique_id") -def unique_id_fixture(hass): - """Define a config entry unique ID fixture.""" - return "51.528308, -0.3817765" diff --git a/tests/components/flunearyou/fixtures/cdc_data.json b/tests/components/flunearyou/fixtures/cdc_data.json deleted file mode 100644 index 0d0dd9dced0..00000000000 --- a/tests/components/flunearyou/fixtures/cdc_data.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "level": "Low", - "level2": "None", - "week_date": "2020-05-16", - "name": "Washington State", - "fill": { - "color": "#00B7B6", - "opacity": 0.7 - } -} diff --git a/tests/components/flunearyou/fixtures/user_data.json b/tests/components/flunearyou/fixtures/user_data.json deleted file mode 100644 index 79f86dfb5ab..00000000000 --- a/tests/components/flunearyou/fixtures/user_data.json +++ /dev/null @@ -1,50 +0,0 @@ -[ - { - "id": 1, - "city": "Chester(72934)", - "place_id": "49377", - "zip": "72934", - "contained_by": "610", - "latitude": "35.687603", - "longitude": "-94.253845", - "none": 1, - "symptoms": 0, - "flu": 0, - "lepto": 0, - "dengue": 0, - "chick": 0, - "icon": "1" - }, - { - "id": 2, - "city": "Los Angeles(90046)", - "place_id": "23818", - "zip": "90046", - "contained_by": "204", - "latitude": "34.114731", - "longitude": "-118.363724", - "none": 2, - "symptoms": 0, - "flu": 0, - "lepto": 0, - "dengue": 0, - "chick": 0, - "icon": "1" - }, - { - "id": 3, - "city": "Corvallis(97330)", - "place_id": "21462", - "zip": "97330", - "contained_by": "239", - "latitude": "44.638504", - "longitude": "-123.292938", - "none": 3, - "symptoms": 0, - "flu": 0, - "lepto": 0, - "dengue": 0, - "chick": 0, - "icon": "1" - } -] diff --git a/tests/components/flunearyou/test_config_flow.py b/tests/components/flunearyou/test_config_flow.py deleted file mode 100644 index c0fe58ea811..00000000000 --- a/tests/components/flunearyou/test_config_flow.py +++ /dev/null @@ -1,52 +0,0 @@ -"""Define tests for the flunearyou config flow.""" -from unittest.mock import patch - -from pyflunearyou.errors import FluNearYouError - -from homeassistant import data_entry_flow -from homeassistant.components.flunearyou import DOMAIN -from homeassistant.config_entries import SOURCE_USER -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE - - -async def test_duplicate_error(hass, config, config_entry, setup_flunearyou): - """Test that an error is shown when duplicates are added.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=config - ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT - assert result["reason"] == "already_configured" - - -async def test_errors(hass, config): - """Test that exceptions show the appropriate error.""" - with patch( - "pyflunearyou.cdc.CdcReport.status_by_coordinates", side_effect=FluNearYouError - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=config - ) - assert result["errors"] == {"base": "unknown"} - - -async def test_show_form(hass): - """Test that the form is served with no input.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) - - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "user" - - -async def test_step_user(hass, config, setup_flunearyou): - """Test that the user step works.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=config - ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert result["title"] == "51.528308, -0.3817765" - assert result["data"] == { - CONF_LATITUDE: 51.528308, - CONF_LONGITUDE: -0.3817765, - } diff --git a/tests/components/flunearyou/test_diagnostics.py b/tests/components/flunearyou/test_diagnostics.py deleted file mode 100644 index 1775f088a1f..00000000000 --- a/tests/components/flunearyou/test_diagnostics.py +++ /dev/null @@ -1,67 +0,0 @@ -"""Test Flu Near You diagnostics.""" -from homeassistant.components.diagnostics import REDACTED - -from tests.components.diagnostics import get_diagnostics_for_config_entry - - -async def test_entry_diagnostics(hass, config_entry, hass_client, setup_flunearyou): - """Test config entry diagnostics.""" - assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { - "cdc_report": { - "level": "Low", - "level2": "None", - "week_date": "2020-05-16", - "name": "Washington State", - "fill": {"color": "#00B7B6", "opacity": 0.7}, - }, - "user_report": [ - { - "id": 1, - "city": "Chester(72934)", - "place_id": "49377", - "zip": "72934", - "contained_by": "610", - "latitude": REDACTED, - "longitude": REDACTED, - "none": 1, - "symptoms": 0, - "flu": 0, - "lepto": 0, - "dengue": 0, - "chick": 0, - "icon": "1", - }, - { - "id": 2, - "city": "Los Angeles(90046)", - "place_id": "23818", - "zip": "90046", - "contained_by": "204", - "latitude": REDACTED, - "longitude": REDACTED, - "none": 2, - "symptoms": 0, - "flu": 0, - "lepto": 0, - "dengue": 0, - "chick": 0, - "icon": "1", - }, - { - "id": 3, - "city": "Corvallis(97330)", - "place_id": "21462", - "zip": "97330", - "contained_by": "239", - "latitude": REDACTED, - "longitude": REDACTED, - "none": 3, - "symptoms": 0, - "flu": 0, - "lepto": 0, - "dengue": 0, - "chick": 0, - "icon": "1", - }, - ], - } From 67f7c17d34dabc334d3b510ccbdb34fe659fbcb3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 21 Sep 2022 11:30:17 +0200 Subject: [PATCH 606/955] Use SwitchEntityDescription in kostal plenticore (#78841) --- .../components/kostal_plenticore/switch.py | 57 +++++++++---------- 1 file changed, 27 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/kostal_plenticore/switch.py b/homeassistant/components/kostal_plenticore/switch.py index 9136e9f7021..1e33d858b86 100644 --- a/homeassistant/components/kostal_plenticore/switch.py +++ b/homeassistant/components/kostal_plenticore/switch.py @@ -2,11 +2,12 @@ from __future__ import annotations from abc import ABC +from dataclasses import dataclass from datetime import timedelta import logging -from typing import Any, NamedTuple +from typing import Any -from homeassistant.components.switch import SwitchEntity +from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo, EntityCategory @@ -19,12 +20,11 @@ from .helper import SettingDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) -class SwitchData(NamedTuple): - """Representation of a SelectData tuple.""" +@dataclass +class PlenticoreRequiredKeysMixin: + """A class that describes required properties for plenticore switch entities.""" module_id: str - data_id: str - name: str is_on: str on_value: str on_label: str @@ -32,26 +32,23 @@ class SwitchData(NamedTuple): off_label: str -# Defines all entities for switches. -# -# Each entry is defined with a tuple of these values: -# - module id (str) -# - process data id (str) -# - entity name suffix (str) -# - on Value (str) -# - on Label (str) -# - off Value (str) -# - off Label (str) +@dataclass +class PlenticoreSwitchEntityDescription( + SwitchEntityDescription, PlenticoreRequiredKeysMixin +): + """A class that describes plenticore switch entities.""" + + SWITCH_SETTINGS_DATA = [ - SwitchData( - "devices:local", - "Battery:Strategy", - "Battery Strategy", - "1", - "1", - "Automatic", - "2", - "Automatic economical", + PlenticoreSwitchEntityDescription( + module_id="devices:local", + key="Battery:Strategy", + name="Battery Strategy", + is_on="1", + on_value="1", + on_label="Automatic", + off_value="2", + off_label="Automatic economical", ), ] @@ -73,13 +70,13 @@ async def async_setup_entry( plenticore, ) for switch in SWITCH_SETTINGS_DATA: - if switch.module_id not in available_settings_data or switch.data_id not in ( + if switch.module_id not in available_settings_data or switch.key not in ( setting.id for setting in available_settings_data[switch.module_id] ): _LOGGER.debug( "Skipping non existing setting data %s/%s", switch.module_id, - switch.data_id, + switch.key, ) continue @@ -89,7 +86,7 @@ async def async_setup_entry( entry.entry_id, entry.title, switch.module_id, - switch.data_id, + switch.key, switch.name, switch.is_on, switch.on_value, @@ -98,7 +95,7 @@ async def async_setup_entry( switch.off_label, plenticore.device_info, f"{entry.title} {switch.name}", - f"{entry.entry_id}_{switch.module_id}_{switch.data_id}", + f"{entry.entry_id}_{switch.module_id}_{switch.key}", ) ) @@ -117,7 +114,7 @@ class PlenticoreDataSwitch(CoordinatorEntity, SwitchEntity, ABC): platform_name: str, module_id: str, data_id: str, - name: str, + name: str | None, is_on: str, on_value: str, on_label: str, From daba474182a4cb9482ebef9cb8d981bd5b0c6434 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 21 Sep 2022 11:31:14 +0200 Subject: [PATCH 607/955] Use SelectEntityDescription in kostal plenticore (#78840) --- .../components/kostal_plenticore/select.py | 46 ++++++++++--------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/kostal_plenticore/select.py b/homeassistant/components/kostal_plenticore/select.py index 61c4e8e47e8..07d2d0bbb30 100644 --- a/homeassistant/components/kostal_plenticore/select.py +++ b/homeassistant/components/kostal_plenticore/select.py @@ -2,11 +2,11 @@ from __future__ import annotations from abc import ABC +from dataclasses import dataclass from datetime import timedelta import logging -from typing import NamedTuple -from homeassistant.components.select import SelectEntity +from homeassistant.components.select import SelectEntity, SelectEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.const import DEVICE_DEFAULT_NAME from homeassistant.core import HomeAssistant @@ -20,31 +20,33 @@ from .helper import Plenticore, SelectDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) -class SelectData(NamedTuple): - """Representation of a SelectData tuple.""" +@dataclass +class PlenticoreRequiredKeysMixin: + """A class that describes required properties for plenticore select entities.""" module_id: str - data_id: str - name: str options: list is_on: str -# Defines all entities for select widgets. -# -# Each entry is defined with a tuple of these values: -# - module id (str) -# - process data id (str) -# - entity name suffix (str) -# - options -# - entity is enabled by default (bool) +@dataclass +class PlenticoreSelectEntityDescription( + SelectEntityDescription, PlenticoreRequiredKeysMixin +): + """A class that describes plenticore select entities.""" + + SELECT_SETTINGS_DATA = [ - SelectData( - "devices:local", - "battery_charge", - "Battery Charging / Usage mode", - ["None", "Battery:SmartBatteryControl:Enable", "Battery:TimeControl:Enable"], - "1", + PlenticoreSelectEntityDescription( + module_id="devices:local", + key="battery_charge", + name="Battery Charging / Usage mode", + options=[ + "None", + "Battery:SmartBatteryControl:Enable", + "Battery:TimeControl:Enable", + ], + is_on="1", ) ] @@ -81,7 +83,7 @@ async def async_setup_entry( platform_name=entry.title, device_class="kostal_plenticore__battery", module_id=select.module_id, - data_id=select.data_id, + data_id=select.key, name=select.name, current_option="None", options=select.options, @@ -107,7 +109,7 @@ class PlenticoreDataSelect(CoordinatorEntity, SelectEntity, ABC): device_class: str | None, module_id: str, data_id: str, - name: str, + name: str | None, current_option: str | None, options: list[str], is_on: str, From 1800e98f0570bfd1029d9df881fb144fdd943e72 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 21 Sep 2022 11:49:22 +0200 Subject: [PATCH 608/955] Remove leftover debug print from Melnor (#78870) --- tests/components/melnor/test_number.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/components/melnor/test_number.py b/tests/components/melnor/test_number.py index 77466ba50ed..9cddbfceb77 100644 --- a/tests/components/melnor/test_number.py +++ b/tests/components/melnor/test_number.py @@ -24,7 +24,6 @@ async def test_manual_watering_minutes(hass): number = hass.states.get("number.zone_1_manual_minutes") - print(number) assert number.state == "0" assert number.attributes["max"] == 360 assert number.attributes["min"] == 1 From 9e31cf51cb2a8240891f7fc9bf132eec598d8843 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 21 Sep 2022 11:54:14 +0200 Subject: [PATCH 609/955] Adjust Plenticore switch initialisation (#78871) --- .../components/kostal_plenticore/switch.py | 62 ++++++++----------- 1 file changed, 25 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/kostal_plenticore/switch.py b/homeassistant/components/kostal_plenticore/switch.py index 1e33d858b86..c1498132925 100644 --- a/homeassistant/components/kostal_plenticore/switch.py +++ b/homeassistant/components/kostal_plenticore/switch.py @@ -69,33 +69,28 @@ async def async_setup_entry( timedelta(seconds=30), plenticore, ) - for switch in SWITCH_SETTINGS_DATA: - if switch.module_id not in available_settings_data or switch.key not in ( - setting.id for setting in available_settings_data[switch.module_id] + for description in SWITCH_SETTINGS_DATA: + if ( + description.module_id not in available_settings_data + or description.key + not in ( + setting.id for setting in available_settings_data[description.module_id] + ) ): _LOGGER.debug( "Skipping non existing setting data %s/%s", - switch.module_id, - switch.key, + description.module_id, + description.key, ) continue entities.append( PlenticoreDataSwitch( settings_data_update_coordinator, + description, entry.entry_id, entry.title, - switch.module_id, - switch.key, - switch.name, - switch.is_on, - switch.on_value, - switch.on_label, - switch.off_value, - switch.off_label, plenticore.device_info, - f"{entry.title} {switch.name}", - f"{entry.entry_id}_{switch.module_id}_{switch.key}", ) ) @@ -106,38 +101,31 @@ class PlenticoreDataSwitch(CoordinatorEntity, SwitchEntity, ABC): """Representation of a Plenticore Switch.""" _attr_entity_category = EntityCategory.CONFIG + entity_description: PlenticoreSwitchEntityDescription def __init__( self, - coordinator, + coordinator: SettingDataUpdateCoordinator, + description: PlenticoreSwitchEntityDescription, entry_id: str, platform_name: str, - module_id: str, - data_id: str, - name: str | None, - is_on: str, - on_value: str, - on_label: str, - off_value: str, - off_label: str, device_info: DeviceInfo, - attr_name: str, - attr_unique_id: str, - ): + ) -> None: """Create a new Switch Entity for Plenticore process data.""" super().__init__(coordinator) + self.entity_description = description self.entry_id = entry_id self.platform_name = platform_name - self.module_id = module_id - self.data_id = data_id - self._name = name - self._is_on = is_on - self._attr_name = attr_name - self.on_value = on_value - self.on_label = on_label - self.off_value = off_value - self.off_label = off_label - self._attr_unique_id = attr_unique_id + self.module_id = description.module_id + self.data_id = description.key + self._name = description.name + self._is_on = description.is_on + self._attr_name = f"{platform_name} {description.name}" + self.on_value = description.on_value + self.on_label = description.on_label + self.off_value = description.off_value + self.off_label = description.off_label + self._attr_unique_id = f"{entry_id}_{description.module_id}_{description.key}" self._device_info = device_info From 988e8bfe7b38545f87a5a3f91b9c786aaf3b6854 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 21 Sep 2022 11:58:59 +0200 Subject: [PATCH 610/955] Adjust Plenticore select initialisation (#78873) --- .../components/kostal_plenticore/select.py | 42 ++++++++----------- 1 file changed, 18 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/kostal_plenticore/select.py b/homeassistant/components/kostal_plenticore/select.py index 07d2d0bbb30..ee555ba26df 100644 --- a/homeassistant/components/kostal_plenticore/select.py +++ b/homeassistant/components/kostal_plenticore/select.py @@ -67,29 +67,26 @@ async def async_setup_entry( ) entities = [] - for select in SELECT_SETTINGS_DATA: - if select.module_id not in available_settings_data: + for description in SELECT_SETTINGS_DATA: + if description.module_id not in available_settings_data: continue - needed_data_ids = {data_id for data_id in select.options if data_id != "None"} + needed_data_ids = { + data_id for data_id in description.options if data_id != "None" + } available_data_ids = { - setting.id for setting in available_settings_data[select.module_id] + setting.id for setting in available_settings_data[description.module_id] } if not needed_data_ids <= available_data_ids: continue entities.append( PlenticoreDataSelect( select_data_update_coordinator, + description, entry_id=entry.entry_id, platform_name=entry.title, device_class="kostal_plenticore__battery", - module_id=select.module_id, - data_id=select.key, - name=select.name, current_option="None", - options=select.options, - is_on=select.is_on, device_info=plenticore.device_info, - unique_id=f"{entry.entry_id}_{select.module_id}", ) ) @@ -100,36 +97,33 @@ class PlenticoreDataSelect(CoordinatorEntity, SelectEntity, ABC): """Representation of a Plenticore Select.""" _attr_entity_category = EntityCategory.CONFIG + entity_description: PlenticoreSelectEntityDescription def __init__( self, - coordinator, + coordinator: SelectDataUpdateCoordinator, + description: PlenticoreSelectEntityDescription, entry_id: str, platform_name: str, device_class: str | None, - module_id: str, - data_id: str, - name: str | None, current_option: str | None, - options: list[str], - is_on: str, device_info: DeviceInfo, - unique_id: str, ) -> None: """Create a new Select Entity for Plenticore process data.""" super().__init__(coordinator) + self.entity_description = description self.entry_id = entry_id self.platform_name = platform_name self._attr_device_class = device_class - self.module_id = module_id - self.data_id = data_id - self._attr_options = options - self.all_options = options + self.module_id = description.module_id + self.data_id = description.key + self._attr_options = description.options + self.all_options = description.options self._attr_current_option = current_option - self._is_on = is_on + self._is_on = description.is_on self._device_info = device_info - self._attr_name = name or DEVICE_DEFAULT_NAME - self._attr_unique_id = unique_id + self._attr_name = description.name or DEVICE_DEFAULT_NAME + self._attr_unique_id = f"{entry_id}_{description.module_id}" @property def available(self) -> bool: From 063074acf531701ced617b72d19edd4a5870b28b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 21 Sep 2022 12:08:00 +0200 Subject: [PATCH 611/955] Adjust Plenticore sensor initialisation (#78869) --- .../components/kostal_plenticore/sensor.py | 40 +++++++------------ 1 file changed, 15 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/kostal_plenticore/sensor.py b/homeassistant/components/kostal_plenticore/sensor.py index de3a2375a54..05ece8d188b 100644 --- a/homeassistant/components/kostal_plenticore/sensor.py +++ b/homeassistant/components/kostal_plenticore/sensor.py @@ -26,7 +26,7 @@ from homeassistant.const import ( POWER_WATT, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo, EntityCategory +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -836,9 +836,6 @@ async def async_setup_entry( for description in SENSOR_PROCESS_DATA: module_id = description.module_id data_id = description.key - name = description.name - sensor_data = description.properties - fmt = description.formatter if ( module_id not in available_process_data or data_id not in available_process_data[module_id] @@ -851,15 +848,10 @@ async def async_setup_entry( entities.append( PlenticoreDataSensor( process_data_update_coordinator, + description, entry.entry_id, entry.title, - module_id, - data_id, - name, - sensor_data, - PlenticoreDataFormatter.get_method(fmt), plenticore.device_info, - None, ) ) @@ -869,34 +861,32 @@ async def async_setup_entry( class PlenticoreDataSensor(CoordinatorEntity, SensorEntity): """Representation of a Plenticore data Sensor.""" + entity_description: PlenticoreSensorEntityDescription + def __init__( self, - coordinator, + coordinator: ProcessDataUpdateCoordinator, + description: PlenticoreSensorEntityDescription, entry_id: str, platform_name: str, - module_id: str, - data_id: str, - sensor_name: str | None, - sensor_data: dict[str, Any], - formatter: Callable[[str], Any], device_info: DeviceInfo, - entity_category: EntityCategory | None, - ): + ) -> None: """Create a new Sensor Entity for Plenticore process data.""" super().__init__(coordinator) + self.entity_description = description self.entry_id = entry_id self.platform_name = platform_name - self.module_id = module_id - self.data_id = data_id + self.module_id = description.module_id + self.data_id = description.key - self._sensor_name = sensor_name - self._sensor_data = sensor_data - self._formatter = formatter + self._sensor_name = description.name + self._sensor_data = description.properties + self._formatter: Callable[[str], Any] = PlenticoreDataFormatter.get_method( + description.formatter + ) self._device_info = device_info - self._attr_entity_category = entity_category - @property def available(self) -> bool: """Return if entity is available.""" From 9e7c03af5612db381ae203c5447a79ad9b27b37e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 21 Sep 2022 12:37:31 +0200 Subject: [PATCH 612/955] Use m3 as intermediate unit for volume conversions (#78861) * Use m3 as SI volume standard unit * Adjust comment * Apply suggestion Co-authored-by: Erik Montnemery * Apply suggestion Co-authored-by: Erik Montnemery Co-authored-by: Erik Montnemery --- homeassistant/util/volume.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/homeassistant/util/volume.py b/homeassistant/util/volume.py index 73e89a064a2..498368e7e2b 100644 --- a/homeassistant/util/volume.py +++ b/homeassistant/util/volume.py @@ -25,20 +25,20 @@ VALID_UNITS: tuple[str, ...] = ( VOLUME_CUBIC_FEET, ) -ML_TO_L = 0.001 # 1 mL = 0.001 L -CUBIC_METER_TO_L = 1000 # 1 m3 = 1000 L -GALLON_TO_L = 231 * pow(IN_TO_M, 3) * CUBIC_METER_TO_L # US gallon is 231 cubic inches -FLUID_OUNCE_TO_L = GALLON_TO_L / 128 # 128 fluid ounces in a US gallon -CUBIC_FOOT_TO_L = CUBIC_METER_TO_L * pow(FOOT_TO_M, 3) +L_TO_CUBIC_METER = 0.001 # 1 L = 0.001 m³ +ML_TO_CUBIC_METER = 0.001 * L_TO_CUBIC_METER # 1 mL = 0.001 L +GALLON_TO_CUBIC_METER = 231 * pow(IN_TO_M, 3) # US gallon is 231 cubic inches +FLUID_OUNCE_TO_CUBIC_METER = GALLON_TO_CUBIC_METER / 128 # 128 fl. oz. in a US gallon +CUBIC_FOOT_TO_CUBIC_METER = pow(FOOT_TO_M, 3) -# Units in terms of L +# Units in terms of m³ UNIT_CONVERSION: dict[str, float] = { - VOLUME_LITERS: 1, - VOLUME_MILLILITERS: 1 / ML_TO_L, - VOLUME_GALLONS: 1 / GALLON_TO_L, - VOLUME_FLUID_OUNCE: 1 / FLUID_OUNCE_TO_L, - VOLUME_CUBIC_METERS: 1 / CUBIC_METER_TO_L, - VOLUME_CUBIC_FEET: 1 / CUBIC_FOOT_TO_L, + VOLUME_LITERS: 1 / L_TO_CUBIC_METER, + VOLUME_MILLILITERS: 1 / ML_TO_CUBIC_METER, + VOLUME_GALLONS: 1 / GALLON_TO_CUBIC_METER, + VOLUME_FLUID_OUNCE: 1 / FLUID_OUNCE_TO_CUBIC_METER, + VOLUME_CUBIC_METERS: 1, + VOLUME_CUBIC_FEET: 1 / CUBIC_FOOT_TO_CUBIC_METER, } @@ -63,7 +63,7 @@ def cubic_feet_to_cubic_meter(cubic_feet: float) -> float: def convert(volume: float, from_unit: str, to_unit: str) -> float: - """Convert a temperature from one unit to another.""" + """Convert a volume from one unit to another.""" if from_unit not in VALID_UNITS: raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(from_unit, VOLUME)) if to_unit not in VALID_UNITS: @@ -78,6 +78,6 @@ def convert(volume: float, from_unit: str, to_unit: str) -> float: def _convert(volume: float, from_unit: str, to_unit: str) -> float: - """Convert a temperature from one unit to another, bypassing checks.""" - liters = volume / UNIT_CONVERSION[from_unit] - return liters * UNIT_CONVERSION[to_unit] + """Convert a volume from one unit to another, bypassing checks.""" + cubic_meter = volume / UNIT_CONVERSION[from_unit] + return cubic_meter * UNIT_CONVERSION[to_unit] From a605b6bcf114c319550743e9766e63df8d6857fd Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 21 Sep 2022 13:27:11 +0200 Subject: [PATCH 613/955] Fix typo in tuya select (#78881) --- homeassistant/components/tuya/select.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tuya/select.py b/homeassistant/components/tuya/select.py index 6ec636b078d..36b08868e0f 100644 --- a/homeassistant/components/tuya/select.py +++ b/homeassistant/components/tuya/select.py @@ -397,7 +397,7 @@ class TuyaSelectEntity(TuyaEntity, SelectEntity): self.entity_description = description self._attr_unique_id = f"{super().unique_id}{description.key}" - self._attr_opions: list[str] = [] + self._attr_options: list[str] = [] if enum_type := self.find_dpcode( description.key, dptype=DPType.ENUM, prefer_function=True ): From cf3b5ff7f9804f4d8d129727fd243280c099d940 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 21 Sep 2022 13:37:53 +0200 Subject: [PATCH 614/955] Cleanup properties in Plenticore sensor (#78879) --- .../components/kostal_plenticore/const.py | 2 - .../components/kostal_plenticore/sensor.py | 527 ++++++------------ 2 files changed, 184 insertions(+), 345 deletions(-) diff --git a/homeassistant/components/kostal_plenticore/const.py b/homeassistant/components/kostal_plenticore/const.py index 168c1ccb439..c0e897e6131 100644 --- a/homeassistant/components/kostal_plenticore/const.py +++ b/homeassistant/components/kostal_plenticore/const.py @@ -1,4 +1,2 @@ """Constants for the Kostal Plenticore Solar Inverter integration.""" DOMAIN = "kostal_plenticore" - -ATTR_ENABLED_DEFAULT = "entity_registry_enabled_default" diff --git a/homeassistant/components/kostal_plenticore/sensor.py b/homeassistant/components/kostal_plenticore/sensor.py index 05ece8d188b..6555a35c8bc 100644 --- a/homeassistant/components/kostal_plenticore/sensor.py +++ b/homeassistant/components/kostal_plenticore/sensor.py @@ -8,7 +8,6 @@ import logging from typing import Any from homeassistant.components.sensor import ( - ATTR_STATE_CLASS, SensorDeviceClass, SensorEntity, SensorEntityDescription, @@ -16,9 +15,6 @@ from homeassistant.components.sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - ATTR_DEVICE_CLASS, - ATTR_ICON, - ATTR_UNIT_OF_MEASUREMENT, ELECTRIC_CURRENT_AMPERE, ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, @@ -30,7 +26,7 @@ from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import ATTR_ENABLED_DEFAULT, DOMAIN +from .const import DOMAIN from .helper import PlenticoreDataFormatter, ProcessDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -41,7 +37,6 @@ class PlenticoreRequiredKeysMixin: """A class that describes required properties for plenticore sensor entities.""" module_id: str - properties: dict[str, Any] formatter: str @@ -57,761 +52,633 @@ SENSOR_PROCESS_DATA = [ module_id="devices:local", key="Inverter:State", name="Inverter State", - properties={ATTR_ICON: "mdi:state-machine"}, + icon="mdi:state-machine", formatter="format_inverter_state", ), PlenticoreSensorEntityDescription( module_id="devices:local", key="Dc_P", name="Solar Power", - properties={ - ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, - ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, - ATTR_ENABLED_DEFAULT: True, - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - }, + native_unit_of_measurement=POWER_WATT, + device_class=SensorDeviceClass.POWER, + entity_registry_enabled_default=True, + state_class=SensorStateClass.MEASUREMENT, formatter="format_round", ), PlenticoreSensorEntityDescription( module_id="devices:local", key="Grid_P", name="Grid Power", - properties={ - ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, - ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, - ATTR_ENABLED_DEFAULT: True, - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - }, + native_unit_of_measurement=POWER_WATT, + device_class=SensorDeviceClass.POWER, + entity_registry_enabled_default=True, + state_class=SensorStateClass.MEASUREMENT, formatter="format_round", ), PlenticoreSensorEntityDescription( module_id="devices:local", key="HomeBat_P", name="Home Power from Battery", - properties={ - ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, - ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, - }, + native_unit_of_measurement=POWER_WATT, + device_class=SensorDeviceClass.POWER, formatter="format_round", ), PlenticoreSensorEntityDescription( module_id="devices:local", key="HomeGrid_P", name="Home Power from Grid", - properties={ - ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, - ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - }, + native_unit_of_measurement=POWER_WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, formatter="format_round", ), PlenticoreSensorEntityDescription( module_id="devices:local", key="HomeOwn_P", name="Home Power from Own", - properties={ - ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, - ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - }, + native_unit_of_measurement=POWER_WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, formatter="format_round", ), PlenticoreSensorEntityDescription( module_id="devices:local", key="HomePv_P", name="Home Power from PV", - properties={ - ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, - ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - }, + native_unit_of_measurement=POWER_WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, formatter="format_round", ), PlenticoreSensorEntityDescription( module_id="devices:local", key="Home_P", name="Home Power", - properties={ - ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, - ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - }, + native_unit_of_measurement=POWER_WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, formatter="format_round", ), PlenticoreSensorEntityDescription( module_id="devices:local:ac", key="P", name="AC Power", - properties={ - ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, - ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, - ATTR_ENABLED_DEFAULT: True, - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - }, + native_unit_of_measurement=POWER_WATT, + device_class=SensorDeviceClass.POWER, + entity_registry_enabled_default=True, + state_class=SensorStateClass.MEASUREMENT, formatter="format_round", ), PlenticoreSensorEntityDescription( module_id="devices:local:pv1", key="P", name="DC1 Power", - properties={ - ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, - ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - }, + native_unit_of_measurement=POWER_WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, formatter="format_round", ), PlenticoreSensorEntityDescription( module_id="devices:local:pv1", key="U", name="DC1 Voltage", - properties={ - ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_POTENTIAL_VOLT, - ATTR_DEVICE_CLASS: SensorDeviceClass.VOLTAGE, - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - }, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, formatter="format_round", ), PlenticoreSensorEntityDescription( module_id="devices:local:pv1", key="I", name="DC1 Current", - properties={ - ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_CURRENT_AMPERE, - ATTR_DEVICE_CLASS: SensorDeviceClass.CURRENT, - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - }, + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, formatter="format_float", ), PlenticoreSensorEntityDescription( module_id="devices:local:pv2", key="P", name="DC2 Power", - properties={ - ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, - ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - }, + native_unit_of_measurement=POWER_WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, formatter="format_round", ), PlenticoreSensorEntityDescription( module_id="devices:local:pv2", key="U", name="DC2 Voltage", - properties={ - ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_POTENTIAL_VOLT, - ATTR_DEVICE_CLASS: SensorDeviceClass.VOLTAGE, - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - }, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, formatter="format_round", ), PlenticoreSensorEntityDescription( module_id="devices:local:pv2", key="I", name="DC2 Current", - properties={ - ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_CURRENT_AMPERE, - ATTR_DEVICE_CLASS: SensorDeviceClass.CURRENT, - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - }, + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, formatter="format_float", ), PlenticoreSensorEntityDescription( module_id="devices:local:pv3", key="P", name="DC3 Power", - properties={ - ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, - ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - }, + native_unit_of_measurement=POWER_WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, formatter="format_round", ), PlenticoreSensorEntityDescription( module_id="devices:local:pv3", key="U", name="DC3 Voltage", - properties={ - ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_POTENTIAL_VOLT, - ATTR_DEVICE_CLASS: SensorDeviceClass.VOLTAGE, - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - }, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, formatter="format_round", ), PlenticoreSensorEntityDescription( module_id="devices:local:pv3", key="I", name="DC3 Current", - properties={ - ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_CURRENT_AMPERE, - ATTR_DEVICE_CLASS: SensorDeviceClass.CURRENT, - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - }, + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, formatter="format_float", ), PlenticoreSensorEntityDescription( module_id="devices:local", key="PV2Bat_P", name="PV to Battery Power", - properties={ - ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, - ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - }, + native_unit_of_measurement=POWER_WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, formatter="format_round", ), PlenticoreSensorEntityDescription( module_id="devices:local", key="EM_State", name="Energy Manager State", - properties={ATTR_ICON: "mdi:state-machine"}, + icon="mdi:state-machine", formatter="format_em_manager_state", ), PlenticoreSensorEntityDescription( module_id="devices:local:battery", key="Cycles", name="Battery Cycles", - properties={ - ATTR_ICON: "mdi:recycle", - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - }, + icon="mdi:recycle", + state_class=SensorStateClass.MEASUREMENT, formatter="format_round", ), PlenticoreSensorEntityDescription( module_id="devices:local:battery", key="P", name="Battery Power", - properties={ - ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, - ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - }, + native_unit_of_measurement=POWER_WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, formatter="format_round", ), PlenticoreSensorEntityDescription( module_id="devices:local:battery", key="SoC", name="Battery SoC", - properties={ - ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, - ATTR_DEVICE_CLASS: SensorDeviceClass.BATTERY, - }, + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.BATTERY, formatter="format_round", ), PlenticoreSensorEntityDescription( module_id="scb:statistic:EnergyFlow", key="Statistic:Autarky:Day", name="Autarky Day", - properties={ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_ICON: "mdi:chart-donut"}, + native_unit_of_measurement=PERCENTAGE, + icon="mdi:chart-donut", formatter="format_round", ), PlenticoreSensorEntityDescription( module_id="scb:statistic:EnergyFlow", key="Statistic:Autarky:Month", name="Autarky Month", - properties={ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_ICON: "mdi:chart-donut"}, + native_unit_of_measurement=PERCENTAGE, + icon="mdi:chart-donut", formatter="format_round", ), PlenticoreSensorEntityDescription( module_id="scb:statistic:EnergyFlow", key="Statistic:Autarky:Total", name="Autarky Total", - properties={ - ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, - ATTR_ICON: "mdi:chart-donut", - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - }, + native_unit_of_measurement=PERCENTAGE, + icon="mdi:chart-donut", + state_class=SensorStateClass.MEASUREMENT, formatter="format_round", ), PlenticoreSensorEntityDescription( module_id="scb:statistic:EnergyFlow", key="Statistic:Autarky:Year", name="Autarky Year", - properties={ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_ICON: "mdi:chart-donut"}, + native_unit_of_measurement=PERCENTAGE, + icon="mdi:chart-donut", formatter="format_round", ), PlenticoreSensorEntityDescription( module_id="scb:statistic:EnergyFlow", key="Statistic:OwnConsumptionRate:Day", name="Own Consumption Rate Day", - properties={ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_ICON: "mdi:chart-donut"}, + native_unit_of_measurement=PERCENTAGE, + icon="mdi:chart-donut", formatter="format_round", ), PlenticoreSensorEntityDescription( module_id="scb:statistic:EnergyFlow", key="Statistic:OwnConsumptionRate:Month", name="Own Consumption Rate Month", - properties={ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_ICON: "mdi:chart-donut"}, + native_unit_of_measurement=PERCENTAGE, + icon="mdi:chart-donut", formatter="format_round", ), PlenticoreSensorEntityDescription( module_id="scb:statistic:EnergyFlow", key="Statistic:OwnConsumptionRate:Total", name="Own Consumption Rate Total", - properties={ - ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, - ATTR_ICON: "mdi:chart-donut", - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - }, + native_unit_of_measurement=PERCENTAGE, + icon="mdi:chart-donut", + state_class=SensorStateClass.MEASUREMENT, formatter="format_round", ), PlenticoreSensorEntityDescription( module_id="scb:statistic:EnergyFlow", key="Statistic:OwnConsumptionRate:Year", name="Own Consumption Rate Year", - properties={ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_ICON: "mdi:chart-donut"}, + native_unit_of_measurement=PERCENTAGE, + icon="mdi:chart-donut", formatter="format_round", ), PlenticoreSensorEntityDescription( module_id="scb:statistic:EnergyFlow", key="Statistic:EnergyHome:Day", name="Home Consumption Day", - properties={ - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, formatter="format_energy", ), PlenticoreSensorEntityDescription( module_id="scb:statistic:EnergyFlow", key="Statistic:EnergyHome:Month", name="Home Consumption Month", - properties={ - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, formatter="format_energy", ), PlenticoreSensorEntityDescription( module_id="scb:statistic:EnergyFlow", key="Statistic:EnergyHome:Year", name="Home Consumption Year", - properties={ - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, formatter="format_energy", ), PlenticoreSensorEntityDescription( module_id="scb:statistic:EnergyFlow", key="Statistic:EnergyHome:Total", name="Home Consumption Total", - properties={ - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, - }, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, formatter="format_energy", ), PlenticoreSensorEntityDescription( module_id="scb:statistic:EnergyFlow", key="Statistic:EnergyHomeBat:Day", name="Home Consumption from Battery Day", - properties={ - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, formatter="format_energy", ), PlenticoreSensorEntityDescription( module_id="scb:statistic:EnergyFlow", key="Statistic:EnergyHomeBat:Month", name="Home Consumption from Battery Month", - properties={ - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, formatter="format_energy", ), PlenticoreSensorEntityDescription( module_id="scb:statistic:EnergyFlow", key="Statistic:EnergyHomeBat:Year", name="Home Consumption from Battery Year", - properties={ - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, formatter="format_energy", ), PlenticoreSensorEntityDescription( module_id="scb:statistic:EnergyFlow", key="Statistic:EnergyHomeBat:Total", name="Home Consumption from Battery Total", - properties={ - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, - }, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, formatter="format_energy", ), PlenticoreSensorEntityDescription( module_id="scb:statistic:EnergyFlow", key="Statistic:EnergyHomeGrid:Day", name="Home Consumption from Grid Day", - properties={ - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, formatter="format_energy", ), PlenticoreSensorEntityDescription( module_id="scb:statistic:EnergyFlow", key="Statistic:EnergyHomeGrid:Month", name="Home Consumption from Grid Month", - properties={ - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, formatter="format_energy", ), PlenticoreSensorEntityDescription( module_id="scb:statistic:EnergyFlow", key="Statistic:EnergyHomeGrid:Year", name="Home Consumption from Grid Year", - properties={ - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, formatter="format_energy", ), PlenticoreSensorEntityDescription( module_id="scb:statistic:EnergyFlow", key="Statistic:EnergyHomeGrid:Total", name="Home Consumption from Grid Total", - properties={ - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, - }, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, formatter="format_energy", ), PlenticoreSensorEntityDescription( module_id="scb:statistic:EnergyFlow", key="Statistic:EnergyHomePv:Day", name="Home Consumption from PV Day", - properties={ - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, formatter="format_energy", ), PlenticoreSensorEntityDescription( module_id="scb:statistic:EnergyFlow", key="Statistic:EnergyHomePv:Month", name="Home Consumption from PV Month", - properties={ - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, formatter="format_energy", ), PlenticoreSensorEntityDescription( module_id="scb:statistic:EnergyFlow", key="Statistic:EnergyHomePv:Year", name="Home Consumption from PV Year", - properties={ - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, formatter="format_energy", ), PlenticoreSensorEntityDescription( module_id="scb:statistic:EnergyFlow", key="Statistic:EnergyHomePv:Total", name="Home Consumption from PV Total", - properties={ - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, - }, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, formatter="format_energy", ), PlenticoreSensorEntityDescription( module_id="scb:statistic:EnergyFlow", key="Statistic:EnergyPv1:Day", name="Energy PV1 Day", - properties={ - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, formatter="format_energy", ), PlenticoreSensorEntityDescription( module_id="scb:statistic:EnergyFlow", key="Statistic:EnergyPv1:Month", name="Energy PV1 Month", - properties={ - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, formatter="format_energy", ), PlenticoreSensorEntityDescription( module_id="scb:statistic:EnergyFlow", key="Statistic:EnergyPv1:Year", name="Energy PV1 Year", - properties={ - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, formatter="format_energy", ), PlenticoreSensorEntityDescription( module_id="scb:statistic:EnergyFlow", key="Statistic:EnergyPv1:Total", name="Energy PV1 Total", - properties={ - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, - }, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, formatter="format_energy", ), PlenticoreSensorEntityDescription( module_id="scb:statistic:EnergyFlow", key="Statistic:EnergyPv2:Day", name="Energy PV2 Day", - properties={ - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, formatter="format_energy", ), PlenticoreSensorEntityDescription( module_id="scb:statistic:EnergyFlow", key="Statistic:EnergyPv2:Month", name="Energy PV2 Month", - properties={ - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, formatter="format_energy", ), PlenticoreSensorEntityDescription( module_id="scb:statistic:EnergyFlow", key="Statistic:EnergyPv2:Year", name="Energy PV2 Year", - properties={ - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, formatter="format_energy", ), PlenticoreSensorEntityDescription( module_id="scb:statistic:EnergyFlow", key="Statistic:EnergyPv2:Total", name="Energy PV2 Total", - properties={ - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, - }, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, formatter="format_energy", ), PlenticoreSensorEntityDescription( module_id="scb:statistic:EnergyFlow", key="Statistic:EnergyPv3:Day", name="Energy PV3 Day", - properties={ - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, formatter="format_energy", ), PlenticoreSensorEntityDescription( module_id="scb:statistic:EnergyFlow", key="Statistic:EnergyPv3:Month", name="Energy PV3 Month", - properties={ - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, formatter="format_energy", ), PlenticoreSensorEntityDescription( module_id="scb:statistic:EnergyFlow", key="Statistic:EnergyPv3:Year", name="Energy PV3 Year", - properties={ - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, formatter="format_energy", ), PlenticoreSensorEntityDescription( module_id="scb:statistic:EnergyFlow", key="Statistic:EnergyPv3:Total", name="Energy PV3 Total", - properties={ - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, - }, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, formatter="format_energy", ), PlenticoreSensorEntityDescription( module_id="scb:statistic:EnergyFlow", key="Statistic:Yield:Day", name="Energy Yield Day", - properties={ - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - ATTR_ENABLED_DEFAULT: True, - }, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + entity_registry_enabled_default=True, formatter="format_energy", ), PlenticoreSensorEntityDescription( module_id="scb:statistic:EnergyFlow", key="Statistic:Yield:Month", name="Energy Yield Month", - properties={ - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, formatter="format_energy", ), PlenticoreSensorEntityDescription( module_id="scb:statistic:EnergyFlow", key="Statistic:Yield:Year", name="Energy Yield Year", - properties={ - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, formatter="format_energy", ), PlenticoreSensorEntityDescription( module_id="scb:statistic:EnergyFlow", key="Statistic:Yield:Total", name="Energy Yield Total", - properties={ - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, - }, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, formatter="format_energy", ), PlenticoreSensorEntityDescription( module_id="scb:statistic:EnergyFlow", key="Statistic:EnergyChargeGrid:Day", name="Battery Charge from Grid Day", - properties={ - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, formatter="format_energy", ), PlenticoreSensorEntityDescription( module_id="scb:statistic:EnergyFlow", key="Statistic:EnergyChargeGrid:Month", name="Battery Charge from Grid Month", - properties={ - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, formatter="format_energy", ), PlenticoreSensorEntityDescription( module_id="scb:statistic:EnergyFlow", key="Statistic:EnergyChargeGrid:Year", name="Battery Charge from Grid Year", - properties={ - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, formatter="format_energy", ), PlenticoreSensorEntityDescription( module_id="scb:statistic:EnergyFlow", key="Statistic:EnergyChargeGrid:Total", name="Battery Charge from Grid Total", - properties={ - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, - }, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, formatter="format_energy", ), PlenticoreSensorEntityDescription( module_id="scb:statistic:EnergyFlow", key="Statistic:EnergyChargePv:Day", name="Battery Charge from PV Day", - properties={ - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, formatter="format_energy", ), PlenticoreSensorEntityDescription( module_id="scb:statistic:EnergyFlow", key="Statistic:EnergyChargePv:Month", name="Battery Charge from PV Month", - properties={ - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, formatter="format_energy", ), PlenticoreSensorEntityDescription( module_id="scb:statistic:EnergyFlow", key="Statistic:EnergyChargePv:Year", name="Battery Charge from PV Year", - properties={ - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, formatter="format_energy", ), PlenticoreSensorEntityDescription( module_id="scb:statistic:EnergyFlow", key="Statistic:EnergyChargePv:Total", name="Battery Charge from PV Total", - properties={ - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, - }, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, formatter="format_energy", ), PlenticoreSensorEntityDescription( module_id="scb:statistic:EnergyFlow", key="Statistic:EnergyDischargeGrid:Day", name="Energy Discharge to Grid Day", - properties={ - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, formatter="format_energy", ), PlenticoreSensorEntityDescription( module_id="scb:statistic:EnergyFlow", key="Statistic:EnergyDischargeGrid:Month", name="Energy Discharge to Grid Month", - properties={ - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, formatter="format_energy", ), PlenticoreSensorEntityDescription( module_id="scb:statistic:EnergyFlow", key="Statistic:EnergyDischargeGrid:Year", name="Energy Discharge to Grid Year", - properties={ - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, formatter="format_energy", ), PlenticoreSensorEntityDescription( module_id="scb:statistic:EnergyFlow", key="Statistic:EnergyDischargeGrid:Total", name="Energy Discharge to Grid Total", - properties={ - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, - }, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, formatter="format_energy", ), ] @@ -880,7 +747,6 @@ class PlenticoreDataSensor(CoordinatorEntity, SensorEntity): self.data_id = description.key self._sensor_name = description.name - self._sensor_data = description.properties self._formatter: Callable[[str], Any] = PlenticoreDataFormatter.get_method( description.formatter ) @@ -922,31 +788,6 @@ class PlenticoreDataSensor(CoordinatorEntity, SensorEntity): """Return the name of this Sensor Entity.""" return f"{self.platform_name} {self._sensor_name}" - @property - def native_unit_of_measurement(self) -> str | None: - """Return the unit of this Sensor Entity or None.""" - return self._sensor_data.get(ATTR_UNIT_OF_MEASUREMENT) - - @property - def icon(self) -> str | None: - """Return the icon name of this Sensor Entity or None.""" - return self._sensor_data.get(ATTR_ICON) - - @property - def device_class(self) -> str | None: - """Return the class of this device, from component DEVICE_CLASSES.""" - return self._sensor_data.get(ATTR_DEVICE_CLASS) - - @property - def state_class(self) -> str | None: - """Return the class of the state of this device, from component STATE_CLASSES.""" - return self._sensor_data.get(ATTR_STATE_CLASS) - - @property - def entity_registry_enabled_default(self) -> bool: - """Return if the entity should be enabled when first added to the entity registry.""" - return self._sensor_data.get(ATTR_ENABLED_DEFAULT, False) - @property def native_value(self) -> Any | None: """Return the state of the sensor.""" From e58dd6df95207f4a6ed9bb49eebd0c708a6e9094 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 21 Sep 2022 13:46:43 +0200 Subject: [PATCH 615/955] Cleanup Plenticore switch entity (#78878) --- homeassistant/components/kostal_plenticore/switch.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/kostal_plenticore/switch.py b/homeassistant/components/kostal_plenticore/switch.py index c1498132925..90ac26ef947 100644 --- a/homeassistant/components/kostal_plenticore/switch.py +++ b/homeassistant/components/kostal_plenticore/switch.py @@ -1,7 +1,6 @@ """Platform for Kostal Plenticore switches.""" from __future__ import annotations -from abc import ABC from dataclasses import dataclass from datetime import timedelta import logging @@ -97,7 +96,9 @@ async def async_setup_entry( async_add_entities(entities) -class PlenticoreDataSwitch(CoordinatorEntity, SwitchEntity, ABC): +class PlenticoreDataSwitch( + CoordinatorEntity[SettingDataUpdateCoordinator], SwitchEntity +): """Representation of a Plenticore Switch.""" _attr_entity_category = EntityCategory.CONFIG From 166160e2b5b60e8e59fe3ce03081aab9979e02cd Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 21 Sep 2022 13:57:40 +0200 Subject: [PATCH 616/955] Add LaMetric button tests (#78754) --- .coveragerc | 2 - tests/components/lametric/conftest.py | 13 +++ tests/components/lametric/test_button.py | 113 +++++++++++++++++++++++ 3 files changed, 126 insertions(+), 2 deletions(-) create mode 100644 tests/components/lametric/test_button.py diff --git a/.coveragerc b/.coveragerc index c8d172ba30f..352abd7bb7a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -645,8 +645,6 @@ omit = homeassistant/components/kostal_plenticore/switch.py homeassistant/components/kwb/sensor.py homeassistant/components/lacrosse/sensor.py - homeassistant/components/lametric/button.py - homeassistant/components/lametric/entity.py homeassistant/components/lametric/notify.py homeassistant/components/lametric/number.py homeassistant/components/lannouncer/notify.py diff --git a/tests/components/lametric/conftest.py b/tests/components/lametric/conftest.py index f040038743b..a645dc66051 100644 --- a/tests/components/lametric/conftest.py +++ b/tests/components/lametric/conftest.py @@ -94,3 +94,16 @@ def mock_lametric() -> Generator[MagicMock, None, None]: load_fixture("device.json", DOMAIN) ) yield lametric + + +@pytest.fixture +async def init_integration( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_lametric: MagicMock +) -> MockConfigEntry: + """Set up the LaMetric integration for testing.""" + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + return mock_config_entry diff --git a/tests/components/lametric/test_button.py b/tests/components/lametric/test_button.py new file mode 100644 index 00000000000..cd55c9914f5 --- /dev/null +++ b/tests/components/lametric/test_button.py @@ -0,0 +1,113 @@ +"""Tests for the LaMetric button platform.""" +from unittest.mock import MagicMock + +import pytest + +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS +from homeassistant.components.lametric.const import DOMAIN +from homeassistant.const import ATTR_ENTITY_ID, ATTR_ICON, STATE_UNKNOWN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.entity import EntityCategory + +from tests.common import MockConfigEntry + + +@pytest.mark.freeze_time("2022-09-19 12:07:30") +async def test_button_app_next( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_lametric: MagicMock, +) -> None: + """Test the LaMetric next app button.""" + device_registry = dr.async_get(hass) + entity_registry = er.async_get(hass) + + state = hass.states.get("button.frenck_s_lametric_next_app") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:arrow-right-bold" + assert state.state == STATE_UNKNOWN + + entry = entity_registry.async_get("button.frenck_s_lametric_next_app") + assert entry + assert entry.unique_id == "SA110405124500W00BS9-app_next" + assert entry.entity_category == EntityCategory.CONFIG + + assert entry.device_id + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.configuration_url is None + assert device_entry.connections == { + (dr.CONNECTION_NETWORK_MAC, "aa:bb:cc:dd:ee:ff") + } + assert device_entry.entry_type is None + assert device_entry.identifiers == {(DOMAIN, "SA110405124500W00BS9")} + assert device_entry.manufacturer == "LaMetric Inc." + assert device_entry.model == "LM 37X8" + assert device_entry.name == "Frenck's LaMetric" + assert device_entry.sw_version == "2.2.2" + assert device_entry.hw_version is None + + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: "button.frenck_s_lametric_next_app"}, + blocking=True, + ) + + assert len(mock_lametric.app_next.mock_calls) == 1 + mock_lametric.app_next.assert_called_with() + + state = hass.states.get("button.frenck_s_lametric_next_app") + assert state + assert state.state == "2022-09-19T12:07:30+00:00" + + +@pytest.mark.freeze_time("2022-09-19 12:07:30") +async def test_button_app_previous( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_lametric: MagicMock, +) -> None: + """Test the LaMetric previous app button.""" + device_registry = dr.async_get(hass) + entity_registry = er.async_get(hass) + + state = hass.states.get("button.frenck_s_lametric_previous_app") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:arrow-left-bold" + assert state.state == STATE_UNKNOWN + + entry = entity_registry.async_get("button.frenck_s_lametric_previous_app") + assert entry + assert entry.unique_id == "SA110405124500W00BS9-app_previous" + assert entry.entity_category == EntityCategory.CONFIG + + assert entry.device_id + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.configuration_url is None + assert device_entry.connections == { + (dr.CONNECTION_NETWORK_MAC, "aa:bb:cc:dd:ee:ff") + } + assert device_entry.entry_type is None + assert device_entry.identifiers == {(DOMAIN, "SA110405124500W00BS9")} + assert device_entry.manufacturer == "LaMetric Inc." + assert device_entry.model == "LM 37X8" + assert device_entry.name == "Frenck's LaMetric" + assert device_entry.sw_version == "2.2.2" + assert device_entry.hw_version is None + + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: "button.frenck_s_lametric_previous_app"}, + blocking=True, + ) + + assert len(mock_lametric.app_previous.mock_calls) == 1 + mock_lametric.app_previous.assert_called_with() + + state = hass.states.get("button.frenck_s_lametric_previous_app") + assert state + assert state.state == "2022-09-19T12:07:30+00:00" From a81bb10ff9f4e82c01865f662abc6163833ff1d2 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 21 Sep 2022 13:59:42 +0200 Subject: [PATCH 617/955] Update yarl to 1.8.1 (#78866) --- 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 61dad6655de..48eb026d30b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -41,7 +41,7 @@ sqlalchemy==1.4.41 typing-extensions>=3.10.0.2,<5.0 voluptuous-serialize==2.5.0 voluptuous==0.13.1 -yarl==1.7.2 +yarl==1.8.1 zeroconf==0.39.1 # Constrain pycryptodome to avoid vulnerability diff --git a/pyproject.toml b/pyproject.toml index a5b33379ed9..8381098badd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,7 +51,7 @@ dependencies = [ "typing-extensions>=3.10.0.2,<5.0", "voluptuous==0.13.1", "voluptuous-serialize==2.5.0", - "yarl==1.7.2", + "yarl==1.8.1", ] [project.urls] diff --git a/requirements.txt b/requirements.txt index 92a9be187b8..2f36b947bca 100644 --- a/requirements.txt +++ b/requirements.txt @@ -25,4 +25,4 @@ requests==2.28.1 typing-extensions>=3.10.0.2,<5.0 voluptuous==0.13.1 voluptuous-serialize==2.5.0 -yarl==1.7.2 +yarl==1.8.1 From 664a576113d085c1db45cdd650715fecdfd7f90c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 21 Sep 2022 14:32:25 +0200 Subject: [PATCH 618/955] Cleanup Plenticore select entity (#78877) --- .../components/kostal_plenticore/select.py | 27 +++++-------------- 1 file changed, 6 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/kostal_plenticore/select.py b/homeassistant/components/kostal_plenticore/select.py index ee555ba26df..3a2d3445a84 100644 --- a/homeassistant/components/kostal_plenticore/select.py +++ b/homeassistant/components/kostal_plenticore/select.py @@ -1,14 +1,12 @@ """Platform for Kostal Plenticore select widgets.""" from __future__ import annotations -from abc import ABC from dataclasses import dataclass from datetime import timedelta import logging from homeassistant.components.select import SelectEntity, SelectEntityDescription from homeassistant.config_entries import ConfigEntry -from homeassistant.const import DEVICE_DEFAULT_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -25,8 +23,7 @@ class PlenticoreRequiredKeysMixin: """A class that describes required properties for plenticore select entities.""" module_id: str - options: list - is_on: str + options: list[str] @dataclass @@ -46,7 +43,7 @@ SELECT_SETTINGS_DATA = [ "Battery:SmartBatteryControl:Enable", "Battery:TimeControl:Enable", ], - is_on="1", + device_class="kostal_plenticore__battery", ) ] @@ -84,8 +81,6 @@ async def async_setup_entry( description, entry_id=entry.entry_id, platform_name=entry.title, - device_class="kostal_plenticore__battery", - current_option="None", device_info=plenticore.device_info, ) ) @@ -93,7 +88,7 @@ async def async_setup_entry( async_add_entities(entities) -class PlenticoreDataSelect(CoordinatorEntity, SelectEntity, ABC): +class PlenticoreDataSelect(CoordinatorEntity, SelectEntity): """Representation of a Plenticore Select.""" _attr_entity_category = EntityCategory.CONFIG @@ -105,8 +100,6 @@ class PlenticoreDataSelect(CoordinatorEntity, SelectEntity, ABC): description: PlenticoreSelectEntityDescription, entry_id: str, platform_name: str, - device_class: str | None, - current_option: str | None, device_info: DeviceInfo, ) -> None: """Create a new Select Entity for Plenticore process data.""" @@ -114,15 +107,10 @@ class PlenticoreDataSelect(CoordinatorEntity, SelectEntity, ABC): self.entity_description = description self.entry_id = entry_id self.platform_name = platform_name - self._attr_device_class = device_class self.module_id = description.module_id self.data_id = description.key self._attr_options = description.options - self.all_options = description.options - self._attr_current_option = current_option - self._is_on = description.is_on self._device_info = device_info - self._attr_name = description.name or DEVICE_DEFAULT_NAME self._attr_unique_id = f"{entry_id}_{description.module_id}" @property @@ -138,19 +126,16 @@ class PlenticoreDataSelect(CoordinatorEntity, SelectEntity, ABC): async def async_added_to_hass(self) -> None: """Register this entity on the Update Coordinator.""" await super().async_added_to_hass() - self.coordinator.start_fetch_data( - self.module_id, self.data_id, self.all_options - ) + self.coordinator.start_fetch_data(self.module_id, self.data_id, self.options) async def async_will_remove_from_hass(self) -> None: """Unregister this entity from the Update Coordinator.""" - self.coordinator.stop_fetch_data(self.module_id, self.data_id, self.all_options) + self.coordinator.stop_fetch_data(self.module_id, self.data_id, self.options) await super().async_will_remove_from_hass() async def async_select_option(self, option: str) -> None: """Update the current selected option.""" - self._attr_current_option = option - for all_option in self._attr_options: + for all_option in self.options: if all_option != "None": await self.coordinator.async_write_data( self.module_id, {all_option: "0"} From d7382aadfe7160f330c2339cc79a8636cd755768 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 21 Sep 2022 14:48:38 +0200 Subject: [PATCH 619/955] Add new power utility (#78867) * Add power utility * Fix tests --- .../components/recorder/statistics.py | 16 ++++---- .../components/recorder/websocket_api.py | 13 +++--- homeassistant/components/sensor/recorder.py | 15 ++++--- homeassistant/util/power.py | 37 +++++++++++++++++ tests/util/test_power.py | 41 +++++++++++++++++++ 5 files changed, 102 insertions(+), 20 deletions(-) create mode 100644 homeassistant/util/power.py create mode 100644 tests/util/test_power.py diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index f720a2cd62e..1fbd8f192ef 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -28,7 +28,6 @@ from homeassistant.const import ( ENERGY_KILO_WATT_HOUR, ENERGY_MEGA_WATT_HOUR, ENERGY_WATT_HOUR, - POWER_KILO_WATT, POWER_WATT, PRESSURE_PA, TEMP_CELSIUS, @@ -40,10 +39,13 @@ from homeassistant.helpers import entity_registry from homeassistant.helpers.json import JSONEncoder from homeassistant.helpers.storage import STORAGE_DIR from homeassistant.helpers.typing import UNDEFINED, UndefinedType -import homeassistant.util.dt as dt_util -import homeassistant.util.pressure as pressure_util -import homeassistant.util.temperature as temperature_util -import homeassistant.util.volume as volume_util +from homeassistant.util import ( + dt as dt_util, + power as power_util, + pressure as pressure_util, + temperature as temperature_util, + volume as volume_util, +) from .const import DOMAIN, MAX_ROWS_TO_PURGE, SupportedDialect from .db_schema import Statistics, StatisticsMeta, StatisticsRuns, StatisticsShortTerm @@ -156,9 +158,7 @@ def _convert_power_from_w(to_unit: str, value: float | None) -> float | None: """Convert power in W to to_unit.""" if value is None: return None - if to_unit == POWER_KILO_WATT: - return value / 1000 - return value + return power_util.convert(value, POWER_WATT, to_unit) def _convert_pressure_from_pa(to_unit: str, value: float | None) -> float | None: diff --git a/homeassistant/components/recorder/websocket_api.py b/homeassistant/components/recorder/websocket_api.py index a61d1e8d673..8d371aaa001 100644 --- a/homeassistant/components/recorder/websocket_api.py +++ b/homeassistant/components/recorder/websocket_api.py @@ -13,17 +13,18 @@ from homeassistant.const import ( ENERGY_KILO_WATT_HOUR, ENERGY_MEGA_WATT_HOUR, ENERGY_WATT_HOUR, - POWER_KILO_WATT, - POWER_WATT, VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS, ) from homeassistant.core import HomeAssistant, callback, valid_entity_id from homeassistant.helpers import config_validation as cv from homeassistant.helpers.json import JSON_DUMP -from homeassistant.util import dt as dt_util -import homeassistant.util.pressure as pressure_util -import homeassistant.util.temperature as temperature_util +from homeassistant.util import ( + dt as dt_util, + power as power_util, + pressure as pressure_util, + temperature as temperature_util, +) from .const import MAX_QUEUE_BACKLOG from .statistics import ( @@ -122,7 +123,7 @@ async def ws_handle_get_statistics_during_period( vol.Optional("energy"): vol.Any( ENERGY_WATT_HOUR, ENERGY_KILO_WATT_HOUR, ENERGY_MEGA_WATT_HOUR ), - vol.Optional("power"): vol.Any(POWER_WATT, POWER_KILO_WATT), + vol.Optional("power"): vol.In(power_util.VALID_UNITS), vol.Optional("pressure"): vol.In(pressure_util.VALID_UNITS), vol.Optional("temperature"): vol.In(temperature_util.VALID_UNITS), vol.Optional("volume"): vol.Any(VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS), diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index dfbcc67f80d..e0a76cc3588 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -47,10 +47,13 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, State from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity import entity_sources -import homeassistant.util.dt as dt_util -import homeassistant.util.pressure as pressure_util -import homeassistant.util.temperature as temperature_util -import homeassistant.util.volume as volume_util +from homeassistant.util import ( + dt as dt_util, + power as power_util, + pressure as pressure_util, + temperature as temperature_util, + volume as volume_util, +) from . import ( ATTR_LAST_RESET, @@ -89,8 +92,8 @@ UNIT_CONVERSIONS: dict[str, dict[str, Callable]] = { }, # Convert power to W SensorDeviceClass.POWER: { - POWER_WATT: lambda x: x, - POWER_KILO_WATT: lambda x: x * 1000, + POWER_WATT: lambda x: x / power_util.UNIT_CONVERSION[POWER_WATT], + POWER_KILO_WATT: lambda x: x / power_util.UNIT_CONVERSION[POWER_KILO_WATT], }, # Convert pressure to Pa # Note: pressure_util.convert is bypassed to avoid redundant error checking diff --git a/homeassistant/util/power.py b/homeassistant/util/power.py new file mode 100644 index 00000000000..74be6d55377 --- /dev/null +++ b/homeassistant/util/power.py @@ -0,0 +1,37 @@ +"""Power util functions.""" +from __future__ import annotations + +from numbers import Number + +from homeassistant.const import ( + POWER_KILO_WATT, + POWER_WATT, + UNIT_NOT_RECOGNIZED_TEMPLATE, +) + +VALID_UNITS: tuple[str, ...] = ( + POWER_WATT, + POWER_KILO_WATT, +) + +UNIT_CONVERSION: dict[str, float] = { + POWER_WATT: 1, + POWER_KILO_WATT: 1 / 1000, +} + + +def convert(value: float, unit_1: str, unit_2: str) -> float: + """Convert one unit of measurement to another.""" + if unit_1 not in VALID_UNITS: + raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(unit_1, "power")) + if unit_2 not in VALID_UNITS: + raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(unit_2, "power")) + + if not isinstance(value, Number): + raise TypeError(f"{value} is not of numeric type") + + if unit_1 == unit_2: + return value + + watts = value / UNIT_CONVERSION[unit_1] + return watts * UNIT_CONVERSION[unit_2] diff --git a/tests/util/test_power.py b/tests/util/test_power.py new file mode 100644 index 00000000000..89a7f0abd47 --- /dev/null +++ b/tests/util/test_power.py @@ -0,0 +1,41 @@ +"""Test Home Assistant power utility functions.""" +import pytest + +from homeassistant.const import POWER_KILO_WATT, POWER_WATT +import homeassistant.util.power as power_util + +INVALID_SYMBOL = "bob" +VALID_SYMBOL = POWER_WATT + + +def test_convert_same_unit(): + """Test conversion from any unit to same unit.""" + assert power_util.convert(2, POWER_WATT, POWER_WATT) == 2 + assert power_util.convert(3, POWER_KILO_WATT, POWER_KILO_WATT) == 3 + + +def test_convert_invalid_unit(): + """Test exception is thrown for invalid units.""" + with pytest.raises(ValueError): + power_util.convert(5, INVALID_SYMBOL, VALID_SYMBOL) + + with pytest.raises(ValueError): + power_util.convert(5, VALID_SYMBOL, INVALID_SYMBOL) + + +def test_convert_nonnumeric_value(): + """Test exception is thrown for nonnumeric type.""" + with pytest.raises(TypeError): + power_util.convert("a", POWER_WATT, POWER_KILO_WATT) + + +def test_convert_from_kw(): + """Test conversion from kW to other units.""" + kilowatts = 10 + assert power_util.convert(kilowatts, POWER_KILO_WATT, POWER_WATT) == 10000 + + +def test_convert_from_w(): + """Test conversion from W to other units.""" + watts = 10 + assert power_util.convert(watts, POWER_WATT, POWER_KILO_WATT) == 0.01 From c4eafb98fa47562f4a8b9a9af362f861fb43fe48 Mon Sep 17 00:00:00 2001 From: Ernst Klamer Date: Wed, 21 Sep 2022 17:09:39 +0200 Subject: [PATCH 620/955] Add support for Kegtron Smart (Beer) Keg Monitor BLE devices (#78709) --- CODEOWNERS | 2 + homeassistant/components/kegtron/__init__.py | 49 ++++ .../components/kegtron/config_flow.py | 94 +++++++ homeassistant/components/kegtron/const.py | 3 + homeassistant/components/kegtron/device.py | 35 +++ .../components/kegtron/manifest.json | 16 ++ homeassistant/components/kegtron/sensor.py | 135 ++++++++++ homeassistant/components/kegtron/strings.json | 22 ++ .../components/kegtron/translations/en.json | 22 ++ homeassistant/generated/bluetooth.py | 5 + homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/kegtron/__init__.py | 49 ++++ tests/components/kegtron/conftest.py | 8 + tests/components/kegtron/test_config_flow.py | 196 ++++++++++++++ tests/components/kegtron/test_sensor.py | 248 ++++++++++++++++++ 17 files changed, 891 insertions(+) create mode 100644 homeassistant/components/kegtron/__init__.py create mode 100644 homeassistant/components/kegtron/config_flow.py create mode 100644 homeassistant/components/kegtron/const.py create mode 100644 homeassistant/components/kegtron/device.py create mode 100644 homeassistant/components/kegtron/manifest.json create mode 100644 homeassistant/components/kegtron/sensor.py create mode 100644 homeassistant/components/kegtron/strings.json create mode 100644 homeassistant/components/kegtron/translations/en.json create mode 100644 tests/components/kegtron/__init__.py create mode 100644 tests/components/kegtron/conftest.py create mode 100644 tests/components/kegtron/test_config_flow.py create mode 100644 tests/components/kegtron/test_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 87b7aafd1df..156a66b174f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -577,6 +577,8 @@ build.json @home-assistant/supervisor /homeassistant/components/keenetic_ndms2/ @foxel /tests/components/keenetic_ndms2/ @foxel /homeassistant/components/kef/ @basnijholt +/homeassistant/components/kegtron/ @Ernst79 +/tests/components/kegtron/ @Ernst79 /homeassistant/components/keyboard_remote/ @bendavid @lanrat /homeassistant/components/kmtronic/ @dgomes /tests/components/kmtronic/ @dgomes diff --git a/homeassistant/components/kegtron/__init__.py b/homeassistant/components/kegtron/__init__.py new file mode 100644 index 00000000000..7a1669bdcd4 --- /dev/null +++ b/homeassistant/components/kegtron/__init__.py @@ -0,0 +1,49 @@ +"""The Kegtron integration.""" +from __future__ import annotations + +import logging + +from kegtron_ble import KegtronBluetoothDeviceData + +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 Kegtron BLE device from a config entry.""" + address = entry.unique_id + assert address is not None + data = KegtronBluetoothDeviceData() + coordinator = hass.data.setdefault(DOMAIN, {})[ + entry.entry_id + ] = PassiveBluetoothProcessorCoordinator( + hass, + _LOGGER, + address=address, + mode=BluetoothScanningMode.PASSIVE, + 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/kegtron/config_flow.py b/homeassistant/components/kegtron/config_flow.py new file mode 100644 index 00000000000..cc0457af87b --- /dev/null +++ b/homeassistant/components/kegtron/config_flow.py @@ -0,0 +1,94 @@ +"""Config flow for Kegtron ble integration.""" +from __future__ import annotations + +from typing import Any + +from kegtron_ble import KegtronBluetoothDeviceData as DeviceData +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 KegtronConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for kegtron.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize the config flow.""" + self._discovery_info: BluetoothServiceInfoBleak | None = None + self._discovered_device: DeviceData | 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 = DeviceData() + 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 = DeviceData() + 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/kegtron/const.py b/homeassistant/components/kegtron/const.py new file mode 100644 index 00000000000..884ea8f635c --- /dev/null +++ b/homeassistant/components/kegtron/const.py @@ -0,0 +1,3 @@ +"""Constants for the Kegtron integration.""" + +DOMAIN = "kegtron" diff --git a/homeassistant/components/kegtron/device.py b/homeassistant/components/kegtron/device.py new file mode 100644 index 00000000000..b97aed76b7d --- /dev/null +++ b/homeassistant/components/kegtron/device.py @@ -0,0 +1,35 @@ +"""Support for Kegtron devices.""" +from __future__ import annotations + +import logging + +from kegtron_ble import DeviceKey, SensorDeviceInfo + +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothEntityKey, +) +from homeassistant.const import ATTR_MANUFACTURER, ATTR_MODEL, ATTR_NAME +from homeassistant.helpers.entity import DeviceInfo + +_LOGGER = logging.getLogger(__name__) + + +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[ATTR_NAME] = sensor_device_info.name + if sensor_device_info.manufacturer is not None: + hass_device_info[ATTR_MANUFACTURER] = sensor_device_info.manufacturer + if sensor_device_info.model is not None: + hass_device_info[ATTR_MODEL] = sensor_device_info.model + return hass_device_info diff --git a/homeassistant/components/kegtron/manifest.json b/homeassistant/components/kegtron/manifest.json new file mode 100644 index 00000000000..c3205736226 --- /dev/null +++ b/homeassistant/components/kegtron/manifest.json @@ -0,0 +1,16 @@ +{ + "domain": "kegtron", + "name": "Kegtron", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/kegtron", + "bluetooth": [ + { + "connectable": false, + "manufacturer_id": 65535 + } + ], + "requirements": ["kegtron-ble==0.4.0"], + "dependencies": ["bluetooth"], + "codeowners": ["@Ernst79"], + "iot_class": "local_push" +} diff --git a/homeassistant/components/kegtron/sensor.py b/homeassistant/components/kegtron/sensor.py new file mode 100644 index 00000000000..f52a7c79634 --- /dev/null +++ b/homeassistant/components/kegtron/sensor.py @@ -0,0 +1,135 @@ +"""Support for Kegtron sensors.""" +from __future__ import annotations + +from typing import Optional, Union + +from kegtron_ble import ( + SensorDeviceClass as KegtronSensorDeviceClass, + SensorUpdate, + Units, +) + +from homeassistant import config_entries +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothDataProcessor, + PassiveBluetoothDataUpdate, + PassiveBluetoothProcessorCoordinator, + PassiveBluetoothProcessorEntity, +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.const import SIGNAL_STRENGTH_DECIBELS_MILLIWATT, VOLUME_LITERS +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .device import device_key_to_bluetooth_entity_key, sensor_device_info_to_hass + +SENSOR_DESCRIPTIONS = { + KegtronSensorDeviceClass.PORT_COUNT: SensorEntityDescription( + key=KegtronSensorDeviceClass.PORT_COUNT, + icon="mdi:water-pump", + ), + KegtronSensorDeviceClass.KEG_SIZE: SensorEntityDescription( + key=KegtronSensorDeviceClass.KEG_SIZE, + icon="mdi:keg", + native_unit_of_measurement=VOLUME_LITERS, + state_class=SensorStateClass.MEASUREMENT, + ), + KegtronSensorDeviceClass.KEG_TYPE: SensorEntityDescription( + key=KegtronSensorDeviceClass.KEG_TYPE, + icon="mdi:keg", + ), + KegtronSensorDeviceClass.VOLUME_START: SensorEntityDescription( + key=KegtronSensorDeviceClass.VOLUME_START, + icon="mdi:keg", + native_unit_of_measurement=VOLUME_LITERS, + state_class=SensorStateClass.MEASUREMENT, + ), + KegtronSensorDeviceClass.VOLUME_DISPENSED: SensorEntityDescription( + key=KegtronSensorDeviceClass.VOLUME_DISPENSED, + icon="mdi:keg", + native_unit_of_measurement=VOLUME_LITERS, + state_class=SensorStateClass.TOTAL, + ), + KegtronSensorDeviceClass.PORT_STATE: SensorEntityDescription( + key=KegtronSensorDeviceClass.PORT_STATE, + icon="mdi:water-pump", + ), + KegtronSensorDeviceClass.PORT_NAME: SensorEntityDescription( + key=KegtronSensorDeviceClass.PORT_NAME, + icon="mdi:water-pump", + ), + KegtronSensorDeviceClass.SIGNAL_STRENGTH: SensorEntityDescription( + key=f"{KegtronSensorDeviceClass.SIGNAL_STRENGTH}_{Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT}", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), +} + + +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[ + description.device_class + ] + for device_key, description in sensor_update.entity_descriptions.items() + if description.device_class + }, + 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 Kegtron 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( + KegtronBluetoothSensorEntity, async_add_entities + ) + ) + entry.async_on_unload(coordinator.async_register_processor(processor)) + + +class KegtronBluetoothSensorEntity( + PassiveBluetoothProcessorEntity[ + PassiveBluetoothDataProcessor[Optional[Union[float, int]]] + ], + SensorEntity, +): + """Representation of a Kegtron 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/components/kegtron/strings.json b/homeassistant/components/kegtron/strings.json new file mode 100644 index 00000000000..a045d84771e --- /dev/null +++ b/homeassistant/components/kegtron/strings.json @@ -0,0 +1,22 @@ +{ + "config": { + "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "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%]" + } + }, + "abort": { + "not_supported": "Device not supported", + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/kegtron/translations/en.json b/homeassistant/components/kegtron/translations/en.json new file mode 100644 index 00000000000..ebd9760c161 --- /dev/null +++ b/homeassistant/components/kegtron/translations/en.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "already_in_progress": "Configuration flow is already in progress", + "no_devices_found": "No devices found on the network", + "not_supported": "Device not supported" + }, + "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 ed98af896ff..3ba5883a70c 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -148,6 +148,11 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ "local_name": "tps", "connectable": False, }, + { + "domain": "kegtron", + "connectable": False, + "manufacturer_id": 65535, + }, { "domain": "led_ble", "local_name": "LEDnet*", diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 08ec809b0f1..993503c678a 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -189,6 +189,7 @@ FLOWS = { "justnimbus", "kaleidescape", "keenetic_ndms2", + "kegtron", "kmtronic", "knx", "kodi", diff --git a/requirements_all.txt b/requirements_all.txt index 4af838b99bb..7433451ee2a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -956,6 +956,9 @@ kaiterra-async-client==1.0.0 # homeassistant.components.keba keba-kecontact==1.1.0 +# homeassistant.components.kegtron +kegtron-ble==0.4.0 + # homeassistant.components.kiwi kiwiki-client==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2cd54e4f37f..66ea321b882 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -697,6 +697,9 @@ jsonpath==0.82 # homeassistant.components.justnimbus justnimbus==0.6.0 +# homeassistant.components.kegtron +kegtron-ble==0.4.0 + # homeassistant.components.konnected konnected==1.2.0 diff --git a/tests/components/kegtron/__init__.py b/tests/components/kegtron/__init__.py new file mode 100644 index 00000000000..0b35dc8b959 --- /dev/null +++ b/tests/components/kegtron/__init__.py @@ -0,0 +1,49 @@ +"""Tests for the Kegtron integration.""" + +from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo + +NOT_KEGTRON_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", +) + +KEGTRON_KT100_SERVICE_INFO = BluetoothServiceInfo( + name="D0:CF:5E:5C:9B:75", + manufacturer_data={ + 65535: b"I\xef\x13\x88\x02\xe2\x01Single Port\x00\x00\x00\x00\x00\x00\x00\x00\x00" + }, + address="D0:CF:5E:5C:9B:75", + rssi=-82, + service_data={}, + service_uuids=[], + source="local", +) + +KEGTRON_KT200_PORT_1_SERVICE_INFO = BluetoothServiceInfo( + name="D0:CF:5E:5C:9B:75", + manufacturer_data={ + 65535: b"#P\xc3P2\xc8APort 1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + }, + address="D0:CF:5E:5C:9B:75", + rssi=-82, + service_uuids=[], + service_data={}, + source="local", +) + +KEGTRON_KT200_PORT_2_SERVICE_INFO = BluetoothServiceInfo( + name="D0:CF:5E:5C:9B:75", + manufacturer_data={ + 65535: b"\xe62:\x98\x02\xe2Q2nd Port\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + }, + address="D0:CF:5E:5C:9B:75", + rssi=-82, + service_uuids=[], + service_data={}, + source="local", +) diff --git a/tests/components/kegtron/conftest.py b/tests/components/kegtron/conftest.py new file mode 100644 index 00000000000..472cadddada --- /dev/null +++ b/tests/components/kegtron/conftest.py @@ -0,0 +1,8 @@ +"""Kegtron session fixtures.""" + +import pytest + + +@pytest.fixture(autouse=True) +def mock_bluetooth(enable_bluetooth): + """Auto mock bluetooth.""" diff --git a/tests/components/kegtron/test_config_flow.py b/tests/components/kegtron/test_config_flow.py new file mode 100644 index 00000000000..c7015ae6346 --- /dev/null +++ b/tests/components/kegtron/test_config_flow.py @@ -0,0 +1,196 @@ +"""Test the Kegtron config flow.""" + +from unittest.mock import patch + +from homeassistant import config_entries +from homeassistant.components.kegtron.const import DOMAIN +from homeassistant.data_entry_flow import FlowResultType + +from . import ( + KEGTRON_KT100_SERVICE_INFO, + KEGTRON_KT200_PORT_2_SERVICE_INFO, + NOT_KEGTRON_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=KEGTRON_KT100_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + with patch("homeassistant.components.kegtron.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"] == "Kegtron KT-100 9B75" + assert result2["data"] == {} + assert result2["result"].unique_id == "D0:CF:5E:5C:9B:75" + + +async def test_async_step_bluetooth_not_kegtron(hass): + """Test discovery via bluetooth not kegtron.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=NOT_KEGTRON_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.kegtron.config_flow.async_discovered_service_info", + return_value=[KEGTRON_KT200_PORT_2_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.kegtron.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "D0:CF:5E:5C:9B:75"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Kegtron KT-200 9B75" + assert result2["data"] == {} + assert result2["result"].unique_id == "D0:CF:5E:5C:9B:75" + + +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.kegtron.config_flow.async_discovered_service_info", + return_value=[KEGTRON_KT100_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="D0:CF:5E:5C:9B:75", + ) + entry.add_to_hass(hass) + + with patch("homeassistant.components.kegtron.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "D0:CF:5E:5C:9B:75"}, + ) + 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="D0:CF:5E:5C:9B:75", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.kegtron.config_flow.async_discovered_service_info", + return_value=[KEGTRON_KT100_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="D0:CF:5E:5C:9B:75", + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=KEGTRON_KT100_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=KEGTRON_KT100_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=KEGTRON_KT100_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=KEGTRON_KT100_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + with patch( + "homeassistant.components.kegtron.config_flow.async_discovered_service_info", + return_value=[KEGTRON_KT100_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.kegtron.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "D0:CF:5E:5C:9B:75"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Kegtron KT-100 9B75" + assert result2["data"] == {} + assert result2["result"].unique_id == "D0:CF:5E:5C:9B:75" + + # Verify the original one was aborted + assert not hass.config_entries.flow.async_progress(DOMAIN) diff --git a/tests/components/kegtron/test_sensor.py b/tests/components/kegtron/test_sensor.py new file mode 100644 index 00000000000..7c289300922 --- /dev/null +++ b/tests/components/kegtron/test_sensor.py @@ -0,0 +1,248 @@ +"""Test the Kegtron sensors.""" + +from homeassistant.components.kegtron.const import DOMAIN +from homeassistant.components.sensor import ATTR_STATE_CLASS +from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT + +from . import ( + KEGTRON_KT100_SERVICE_INFO, + KEGTRON_KT200_PORT_1_SERVICE_INFO, + KEGTRON_KT200_PORT_2_SERVICE_INFO, +) + +from tests.common import MockConfigEntry +from tests.components.bluetooth import inject_bluetooth_service_info + + +async def test_sensors_kt100(hass): + """Test setting up creates the sensors for Kegtron KT-100.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="D0:CF:5E:5C:9B:75", + ) + 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, + KEGTRON_KT100_SERVICE_INFO, + ) + await hass.async_block_till_done() + assert len(hass.states.async_all("sensor")) == 7 + + port_count_sensor = hass.states.get("sensor.kegtron_kt_100_9b75_port_count") + port_count_sensor_attrs = port_count_sensor.attributes + assert port_count_sensor.state == "Single port device" + assert ( + port_count_sensor_attrs[ATTR_FRIENDLY_NAME] == "Kegtron KT-100 9B75 Port Count" + ) + + keg_size_sensor = hass.states.get("sensor.kegtron_kt_100_9b75_keg_size") + keg_size_sensor_attrs = keg_size_sensor.attributes + assert keg_size_sensor.state == "18.927" + assert keg_size_sensor_attrs[ATTR_FRIENDLY_NAME] == "Kegtron KT-100 9B75 Keg Size" + assert keg_size_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == "L" + assert keg_size_sensor_attrs[ATTR_STATE_CLASS] == "measurement" + + keg_type_sensor = hass.states.get("sensor.kegtron_kt_100_9b75_keg_type") + keg_type_sensor_attrs = keg_type_sensor.attributes + assert keg_type_sensor.state == "Corny (5.0 gal)" + assert keg_type_sensor_attrs[ATTR_FRIENDLY_NAME] == "Kegtron KT-100 9B75 Keg Type" + + volume_start_sensor = hass.states.get("sensor.kegtron_kt_100_9b75_volume_start") + volume_start_sensor_attrs = volume_start_sensor.attributes + assert volume_start_sensor.state == "5.0" + assert ( + volume_start_sensor_attrs[ATTR_FRIENDLY_NAME] + == "Kegtron KT-100 9B75 Volume Start" + ) + assert volume_start_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == "L" + assert volume_start_sensor_attrs[ATTR_STATE_CLASS] == "measurement" + + volume_dispensed_sensor = hass.states.get( + "sensor.kegtron_kt_100_9b75_volume_dispensed" + ) + volume_dispensed_attrs = volume_dispensed_sensor.attributes + assert volume_dispensed_sensor.state == "0.738" + assert ( + volume_dispensed_attrs[ATTR_FRIENDLY_NAME] + == "Kegtron KT-100 9B75 Volume Dispensed" + ) + assert volume_dispensed_attrs[ATTR_UNIT_OF_MEASUREMENT] == "L" + assert volume_dispensed_attrs[ATTR_STATE_CLASS] == "total" + + port_state_sensor = hass.states.get("sensor.kegtron_kt_100_9b75_port_state") + port_state_sensor_attrs = port_state_sensor.attributes + assert port_state_sensor.state == "Configured" + assert ( + port_state_sensor_attrs[ATTR_FRIENDLY_NAME] == "Kegtron KT-100 9B75 Port State" + ) + + port_name_sensor = hass.states.get("sensor.kegtron_kt_100_9b75_port_name") + port_name_attrs = port_name_sensor.attributes + assert port_name_sensor.state == "Single Port" + assert port_name_attrs[ATTR_FRIENDLY_NAME] == "Kegtron KT-100 9B75 Port Name" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + +async def test_sensors_kt200(hass): + """Test setting up creates the sensors for Kegtron KT-200.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="D0:CF:5E:5C:9B:75", + ) + 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 + + # Kegtron KT-200 has two ports that are reported separately, start with port 2 + inject_bluetooth_service_info( + hass, + KEGTRON_KT200_PORT_2_SERVICE_INFO, + ) + await hass.async_block_till_done() + assert len(hass.states.async_all("sensor")) == 7 + + port_count_sensor = hass.states.get("sensor.kegtron_kt_200_9b75_port_count") + port_count_sensor_attrs = port_count_sensor.attributes + assert port_count_sensor.state == "Dual port device" + assert ( + port_count_sensor_attrs[ATTR_FRIENDLY_NAME] == "Kegtron KT-200 9B75 Port Count" + ) + + keg_size_sensor = hass.states.get("sensor.kegtron_kt_200_9b75_keg_size_port_2") + keg_size_sensor_attrs = keg_size_sensor.attributes + assert keg_size_sensor.state == "58.93" + assert ( + keg_size_sensor_attrs[ATTR_FRIENDLY_NAME] + == "Kegtron KT-200 9B75 Keg Size Port 2" + ) + assert keg_size_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == "L" + assert keg_size_sensor_attrs[ATTR_STATE_CLASS] == "measurement" + + keg_type_sensor = hass.states.get("sensor.kegtron_kt_200_9b75_keg_type_port_2") + keg_type_sensor_attrs = keg_type_sensor.attributes + assert keg_type_sensor.state == "Other (58.93 L)" + assert ( + keg_type_sensor_attrs[ATTR_FRIENDLY_NAME] + == "Kegtron KT-200 9B75 Keg Type Port 2" + ) + + volume_start_sensor = hass.states.get( + "sensor.kegtron_kt_200_9b75_volume_start_port_2" + ) + volume_start_sensor_attrs = volume_start_sensor.attributes + assert volume_start_sensor.state == "15.0" + assert ( + volume_start_sensor_attrs[ATTR_FRIENDLY_NAME] + == "Kegtron KT-200 9B75 Volume Start Port 2" + ) + assert volume_start_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == "L" + assert volume_start_sensor_attrs[ATTR_STATE_CLASS] == "measurement" + + volume_dispensed_sensor = hass.states.get( + "sensor.kegtron_kt_200_9b75_volume_dispensed_port_2" + ) + volume_dispensed_attrs = volume_dispensed_sensor.attributes + assert volume_dispensed_sensor.state == "0.738" + assert ( + volume_dispensed_attrs[ATTR_FRIENDLY_NAME] + == "Kegtron KT-200 9B75 Volume Dispensed Port 2" + ) + assert volume_dispensed_attrs[ATTR_UNIT_OF_MEASUREMENT] == "L" + assert volume_dispensed_attrs[ATTR_STATE_CLASS] == "total" + + port_state_sensor = hass.states.get("sensor.kegtron_kt_200_9b75_port_state_port_2") + port_state_sensor_attrs = port_state_sensor.attributes + assert port_state_sensor.state == "Configured" + assert ( + port_state_sensor_attrs[ATTR_FRIENDLY_NAME] + == "Kegtron KT-200 9B75 Port State Port 2" + ) + + port_name_sensor = hass.states.get("sensor.kegtron_kt_200_9b75_port_name_port_2") + port_name_attrs = port_name_sensor.attributes + assert port_name_sensor.state == "2nd Port" + assert port_name_attrs[ATTR_FRIENDLY_NAME] == "Kegtron KT-200 9B75 Port Name Port 2" + + # Followed by a BLE advertisement of port 1 + inject_bluetooth_service_info( + hass, + KEGTRON_KT200_PORT_1_SERVICE_INFO, + ) + await hass.async_block_till_done() + assert len(hass.states.async_all("sensor")) == 13 + + port_count_sensor = hass.states.get("sensor.kegtron_kt_200_9b75_port_count") + port_count_sensor_attrs = port_count_sensor.attributes + assert port_count_sensor.state == "Dual port device" + assert ( + port_count_sensor_attrs[ATTR_FRIENDLY_NAME] == "Kegtron KT-200 9B75 Port Count" + ) + + keg_size_sensor = hass.states.get("sensor.kegtron_kt_200_9b75_keg_size_port_1") + keg_size_sensor_attrs = keg_size_sensor.attributes + assert keg_size_sensor.state == "9.04" + assert ( + keg_size_sensor_attrs[ATTR_FRIENDLY_NAME] + == "Kegtron KT-200 9B75 Keg Size Port 1" + ) + assert keg_size_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == "L" + assert keg_size_sensor_attrs[ATTR_STATE_CLASS] == "measurement" + + keg_type_sensor = hass.states.get("sensor.kegtron_kt_200_9b75_keg_type_port_1") + keg_type_sensor_attrs = keg_type_sensor.attributes + assert keg_type_sensor.state == "Other (9.04 L)" + assert ( + keg_type_sensor_attrs[ATTR_FRIENDLY_NAME] + == "Kegtron KT-200 9B75 Keg Type Port 1" + ) + + volume_start_sensor = hass.states.get( + "sensor.kegtron_kt_200_9b75_volume_start_port_1" + ) + volume_start_sensor_attrs = volume_start_sensor.attributes + assert volume_start_sensor.state == "50.0" + assert ( + volume_start_sensor_attrs[ATTR_FRIENDLY_NAME] + == "Kegtron KT-200 9B75 Volume Start Port 1" + ) + assert volume_start_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == "L" + assert volume_start_sensor_attrs[ATTR_STATE_CLASS] == "measurement" + + volume_dispensed_sensor = hass.states.get( + "sensor.kegtron_kt_200_9b75_volume_dispensed_port_1" + ) + volume_dispensed_attrs = volume_dispensed_sensor.attributes + assert volume_dispensed_sensor.state == "13.0" + assert ( + volume_dispensed_attrs[ATTR_FRIENDLY_NAME] + == "Kegtron KT-200 9B75 Volume Dispensed Port 1" + ) + assert volume_dispensed_attrs[ATTR_UNIT_OF_MEASUREMENT] == "L" + assert volume_dispensed_attrs[ATTR_STATE_CLASS] == "total" + + port_state_sensor = hass.states.get("sensor.kegtron_kt_200_9b75_port_state_port_1") + port_state_sensor_attrs = port_state_sensor.attributes + assert port_state_sensor.state == "Configured" + assert ( + port_state_sensor_attrs[ATTR_FRIENDLY_NAME] + == "Kegtron KT-200 9B75 Port State Port 1" + ) + + port_name_sensor = hass.states.get("sensor.kegtron_kt_200_9b75_port_name_port_1") + port_name_attrs = port_name_sensor.attributes + assert port_name_sensor.state == "Port 1" + assert port_name_attrs[ATTR_FRIENDLY_NAME] == "Kegtron KT-200 9B75 Port Name Port 1" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() From 2c08dc509f93130aaf73274bb31aabdd02ec6067 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Wed, 21 Sep 2022 17:27:05 +0200 Subject: [PATCH 621/955] Check Surveillance Station permissions during setup of Synology DSM integration (#78884) --- homeassistant/components/synology_dsm/common.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/homeassistant/components/synology_dsm/common.py b/homeassistant/components/synology_dsm/common.py index 82f2c214804..019108c3230 100644 --- a/homeassistant/components/synology_dsm/common.py +++ b/homeassistant/components/synology_dsm/common.py @@ -91,6 +91,16 @@ class SynoApi: self._with_surveillance_station = bool( self.dsm.apis.get(SynoSurveillanceStation.CAMERA_API_KEY) ) + if self._with_surveillance_station: + try: + self.dsm.surveillance_station.update() + except SYNOLOGY_CONNECTION_EXCEPTIONS: + self._with_surveillance_station = False + self.dsm.reset(SynoSurveillanceStation.API_KEY) + LOGGER.info( + "Surveillance Station found, but disabled due to missing user permissions" + ) + LOGGER.debug( "State of Surveillance_station during setup of '%s': %s", self._entry.unique_id, From e7b594b5cf70ce454d1b3cfa007aa78fa3cfb1b8 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Wed, 21 Sep 2022 16:41:01 +0100 Subject: [PATCH 622/955] Fix parsing Eve Energy characteristic data (#78880) --- 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 d9e5bfb854b..7ecd54e0a79 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==1.5.9"], + "requirements": ["aiohomekit==1.5.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 7433451ee2a..a6f22aa535f 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==1.5.9 +aiohomekit==1.5.12 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 66ea321b882..31904cb485c 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==1.5.9 +aiohomekit==1.5.12 # homeassistant.components.emulated_hue # homeassistant.components.http From cd6697615f74aa7bf6d41e3ec4d7d9bad4ffa2e4 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 21 Sep 2022 18:08:53 +0200 Subject: [PATCH 623/955] Validate units when importing statistics (#78891) --- .../components/recorder/statistics.py | 40 ++++++++++++++++++- tests/components/demo/test_init.py | 1 + tests/components/energy/test_websocket_api.py | 9 +++++ tests/components/recorder/test_statistics.py | 23 +++++++++++ .../components/recorder/test_websocket_api.py | 28 ++++++++++++- 5 files changed, 98 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 1fbd8f192ef..5f780b5f0c7 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -195,6 +195,17 @@ STATISTIC_UNIT_TO_UNIT_CLASS: dict[str | None, str] = { VOLUME_CUBIC_METERS: "volume", } +STATISTIC_UNIT_TO_VALID_UNITS: dict[str | None, Iterable[str | None]] = { + ENERGY_KILO_WATT_HOUR: [ + ENERGY_KILO_WATT_HOUR, + ENERGY_MEGA_WATT_HOUR, + ENERGY_WATT_HOUR, + ], + POWER_WATT: power_util.VALID_UNITS, + PRESSURE_PA: pressure_util.VALID_UNITS, + TEMP_CELSIUS: temperature_util.VALID_UNITS, + VOLUME_CUBIC_METERS: volume_util.VALID_UNITS, +} # Convert energy power, pressure, temperature and volume statistics from the # normalized unit used for statistics to the unit configured by the user @@ -238,8 +249,17 @@ def _get_statistic_to_display_unit_converter( ) is None: return no_conversion + display_unit: str | None unit_class = STATISTIC_UNIT_TO_UNIT_CLASS[statistic_unit] - display_unit = requested_units.get(unit_class) if requested_units else state_unit + if requested_units and unit_class in requested_units: + display_unit = requested_units[unit_class] + else: + display_unit = state_unit + + if display_unit not in STATISTIC_UNIT_TO_VALID_UNITS[statistic_unit]: + # Guard against invalid state unit in the DB + return no_conversion + return partial(convert_fn, display_unit) @@ -1503,6 +1523,16 @@ def _async_import_statistics( get_instance(hass).async_import_statistics(metadata, statistics) +def _validate_units(statistics_unit: str | None, state_unit: str | None) -> None: + """Raise if the statistics unit and state unit are not compatible.""" + if statistics_unit == state_unit: + return + if (valid_units := STATISTIC_UNIT_TO_VALID_UNITS.get(statistics_unit)) is None: + raise HomeAssistantError(f"Invalid units {statistics_unit},{state_unit}") + if state_unit not in valid_units: + raise HomeAssistantError(f"Invalid units {statistics_unit},{state_unit}") + + @callback def async_import_statistics( hass: HomeAssistant, @@ -1520,6 +1550,10 @@ def async_import_statistics( if not metadata["source"] or metadata["source"] != DOMAIN: raise HomeAssistantError("Invalid source") + _validate_units( + metadata["unit_of_measurement"], metadata["state_unit_of_measurement"] + ) + _async_import_statistics(hass, metadata, statistics) @@ -1542,6 +1576,10 @@ def async_add_external_statistics( if not metadata["source"] or metadata["source"] != domain: raise HomeAssistantError("Invalid source") + _validate_units( + metadata["unit_of_measurement"], metadata["state_unit_of_measurement"] + ) + _async_import_statistics(hass, metadata, statistics) diff --git a/tests/components/demo/test_init.py b/tests/components/demo/test_init.py index 79da28d8abd..934321a0ed8 100644 --- a/tests/components/demo/test_init.py +++ b/tests/components/demo/test_init.py @@ -96,6 +96,7 @@ async def test_demo_statistics_growth(hass, recorder_mock): metadata = { "source": DOMAIN, "name": "Energy consumption 1", + "state_unit_of_measurement": "m³", "statistic_id": statistic_id, "unit_of_measurement": "m³", "has_mean": False, diff --git a/tests/components/energy/test_websocket_api.py b/tests/components/energy/test_websocket_api.py index 1e9f89ac726..8adc091305c 100644 --- a/tests/components/energy/test_websocket_api.py +++ b/tests/components/energy/test_websocket_api.py @@ -343,6 +343,7 @@ async def test_fossil_energy_consumption_no_co2(hass, hass_ws_client, recorder_m "has_sum": True, "name": "Total imported energy", "source": "test", + "state_unit_of_measurement": "kWh", "statistic_id": "test:total_energy_import_tariff_1", "unit_of_measurement": "kWh", } @@ -377,6 +378,7 @@ async def test_fossil_energy_consumption_no_co2(hass, hass_ws_client, recorder_m "has_sum": True, "name": "Total imported energy", "source": "test", + "state_unit_of_measurement": "kWh", "statistic_id": "test:total_energy_import_tariff_2", "unit_of_measurement": "kWh", } @@ -504,6 +506,7 @@ async def test_fossil_energy_consumption_hole(hass, hass_ws_client, recorder_moc "has_sum": True, "name": "Total imported energy", "source": "test", + "state_unit_of_measurement": "kWh", "statistic_id": "test:total_energy_import_tariff_1", "unit_of_measurement": "kWh", } @@ -538,6 +541,7 @@ async def test_fossil_energy_consumption_hole(hass, hass_ws_client, recorder_moc "has_sum": True, "name": "Total imported energy", "source": "test", + "state_unit_of_measurement": "kWh", "statistic_id": "test:total_energy_import_tariff_2", "unit_of_measurement": "kWh", } @@ -663,6 +667,7 @@ async def test_fossil_energy_consumption_no_data(hass, hass_ws_client, recorder_ "has_sum": True, "name": "Total imported energy", "source": "test", + "state_unit_of_measurement": "kWh", "statistic_id": "test:total_energy_import_tariff_1", "unit_of_measurement": "kWh", } @@ -697,6 +702,7 @@ async def test_fossil_energy_consumption_no_data(hass, hass_ws_client, recorder_ "has_sum": True, "name": "Total imported energy", "source": "test", + "state_unit_of_measurement": "kWh", "statistic_id": "test:total_energy_import_tariff_2", "unit_of_measurement": "kWh", } @@ -813,6 +819,7 @@ async def test_fossil_energy_consumption(hass, hass_ws_client, recorder_mock): "has_sum": True, "name": "Total imported energy", "source": "test", + "state_unit_of_measurement": "kWh", "statistic_id": "test:total_energy_import_tariff_1", "unit_of_measurement": "kWh", } @@ -847,6 +854,7 @@ async def test_fossil_energy_consumption(hass, hass_ws_client, recorder_mock): "has_sum": True, "name": "Total imported energy", "source": "test", + "state_unit_of_measurement": "kWh", "statistic_id": "test:total_energy_import_tariff_2", "unit_of_measurement": "kWh", } @@ -877,6 +885,7 @@ async def test_fossil_energy_consumption(hass, hass_ws_client, recorder_mock): "has_sum": False, "name": "Fossil percentage", "source": "test", + "state_unit_of_measurement": "%", "statistic_id": "test:fossil_percentage", "unit_of_measurement": "%", } diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index f2de32a443c..4fc98333bf4 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -741,6 +741,7 @@ def test_external_statistics_errors(hass_recorder, caplog): "has_sum": True, "name": "Total imported energy", "source": "test", + "state_unit_of_measurement": "kWh", "statistic_id": "test:total_energy_import", "unit_of_measurement": "kWh", } @@ -804,6 +805,16 @@ def test_external_statistics_errors(hass_recorder, caplog): assert list_statistic_ids(hass) == [] assert get_metadata(hass, statistic_ids=("test:total_energy_import",)) == {} + # Attempt to insert statistics with an invalid unit combination + external_metadata = {**_external_metadata, "state_unit_of_measurement": "cats"} + external_statistics = {**_external_statistics} + with pytest.raises(HomeAssistantError): + async_add_external_statistics(hass, external_metadata, (external_statistics,)) + wait_recording_done(hass) + assert statistics_during_period(hass, zero, period="hour") == {} + assert list_statistic_ids(hass) == [] + assert get_metadata(hass, statistic_ids=("test:total_energy_import",)) == {} + def test_import_statistics_errors(hass_recorder, caplog): """Test validation of imported statistics.""" @@ -828,6 +839,7 @@ def test_import_statistics_errors(hass_recorder, caplog): "has_sum": True, "name": "Total imported energy", "source": "recorder", + "state_unit_of_measurement": "kWh", "statistic_id": "sensor.total_energy_import", "unit_of_measurement": "kWh", } @@ -891,6 +903,16 @@ def test_import_statistics_errors(hass_recorder, caplog): assert list_statistic_ids(hass) == [] assert get_metadata(hass, statistic_ids=("sensor.total_energy_import",)) == {} + # Attempt to insert statistics with an invalid unit combination + external_metadata = {**_external_metadata, "state_unit_of_measurement": "cats"} + external_statistics = {**_external_statistics} + with pytest.raises(HomeAssistantError): + async_import_statistics(hass, external_metadata, (external_statistics,)) + wait_recording_done(hass) + assert statistics_during_period(hass, zero, period="hour") == {} + assert list_statistic_ids(hass) == [] + assert get_metadata(hass, statistic_ids=("sensor.total_energy_import",)) == {} + @pytest.mark.parametrize("timezone", ["America/Regina", "Europe/Vienna", "UTC"]) @pytest.mark.freeze_time("2021-08-01 00:00:00+00:00") @@ -940,6 +962,7 @@ def test_monthly_statistics(hass_recorder, caplog, timezone): "has_sum": True, "name": "Total imported energy", "source": "test", + "state_unit_of_measurement": "kWh", "statistic_id": "test:total_energy_import", "unit_of_measurement": "kWh", } diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index 6f8b2be7d58..e8214f0209e 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -1100,6 +1100,7 @@ async def test_get_statistics_metadata( "has_sum": True, "name": "Total imported energy", "source": "test", + "state_unit_of_measurement": unit, "statistic_id": "test:total_gas", "unit_of_measurement": unit, } @@ -1107,6 +1108,29 @@ async def test_get_statistics_metadata( async_add_external_statistics( hass, external_energy_metadata_1, external_energy_statistics_1 ) + await async_wait_recording_done(hass) + + await client.send_json( + { + "id": 2, + "type": "recorder/get_statistics_metadata", + "statistic_ids": ["test:total_gas"], + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == [ + { + "statistic_id": "test:total_gas", + "display_unit_of_measurement": unit, + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": "test", + "statistics_unit_of_measurement": unit, + "unit_class": unit_class, + } + ] hass.states.async_set("sensor.test", 10, attributes=attributes) await async_wait_recording_done(hass) @@ -1116,7 +1140,7 @@ async def test_get_statistics_metadata( await client.send_json( { - "id": 2, + "id": 3, "type": "recorder/get_statistics_metadata", "statistic_ids": ["sensor.test"], } @@ -1144,7 +1168,7 @@ async def test_get_statistics_metadata( await client.send_json( { - "id": 3, + "id": 4, "type": "recorder/get_statistics_metadata", "statistic_ids": ["sensor.test"], } From 0d696b84b27adf8b395cecdfed4a5522e55a2334 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 21 Sep 2022 18:57:41 +0200 Subject: [PATCH 624/955] Cleanup root component imports in tests (#78893) --- tests/components/advantage_air/test_climate.py | 2 +- tests/components/airzone/test_climate.py | 2 +- tests/components/alexa/test_smart_home.py | 2 +- tests/components/atag/test_climate.py | 4 +++- tests/components/balboa/test_climate.py | 2 +- tests/components/blackbird/test_media_player.py | 2 +- tests/components/blebox/test_climate.py | 2 +- tests/components/climate/common.py | 4 ++-- tests/components/climate/test_recorder.py | 2 +- tests/components/climate/test_reproduce_state.py | 2 +- tests/components/deconz/test_climate.py | 12 +++++------- tests/components/demo/test_climate.py | 2 +- tests/components/devolo_home_control/test_climate.py | 4 ++-- tests/components/freedompro/test_climate.py | 2 +- tests/components/fritzbox/__init__.py | 2 +- tests/components/fritzbox/test_climate.py | 2 +- tests/components/generic_thermostat/test_climate.py | 2 +- tests/components/google_translate/test_tts.py | 2 +- tests/components/gree/test_bridge.py | 2 +- tests/components/gree/test_climate.py | 4 ++-- tests/components/heos/test_init.py | 2 +- tests/components/homekit/test_type_media_players.py | 4 ++-- .../specific_devices/test_ecobee3.py | 2 +- .../specific_devices/test_ecobee_501.py | 2 +- .../specific_devices/test_lennox_e30.py | 2 +- .../specific_devices/test_lg_tv.py | 2 +- tests/components/homekit_controller/test_climate.py | 2 +- tests/components/homematicip_cloud/test_climate.py | 4 ++-- tests/components/kaleidescape/test_media_player.py | 2 +- tests/components/knx/test_climate.py | 2 +- tests/components/kodi/test_device_trigger.py | 2 +- tests/components/marytts/test_tts.py | 2 +- tests/components/media_player/common.py | 2 +- tests/components/media_player/test_recorder.py | 2 +- .../components/media_player/test_reproduce_state.py | 2 +- tests/components/melissa/test_climate.py | 2 +- tests/components/modbus/test_climate.py | 3 +-- tests/components/monoprice/test_media_player.py | 2 +- tests/components/mqtt/test_climate.py | 5 +++-- tests/components/netatmo/test_climate.py | 10 ++++------ tests/components/nexia/test_climate.py | 2 +- tests/components/plex/test_browse_media.py | 2 +- tests/components/plugwise/test_climate.py | 2 +- tests/components/samsungtv/test_init.py | 2 +- tests/components/sensibo/test_climate.py | 2 +- tests/components/sensibo/test_entity.py | 2 +- tests/components/smartthings/test_climate.py | 2 +- tests/components/smarttub/test_climate.py | 2 +- tests/components/sonos/test_services.py | 3 +-- tests/components/soundtouch/conftest.py | 2 +- tests/components/soundtouch/test_media_player.py | 2 +- tests/components/tts/test_notify.py | 2 +- tests/components/unifiprotect/test_media_player.py | 2 +- tests/components/universal/test_media_player.py | 2 +- tests/components/venstar/test_climate.py | 2 +- tests/components/venstar/util.py | 2 +- tests/components/vera/test_climate.py | 2 +- tests/components/vizio/test_media_player.py | 2 +- tests/components/voicerss/test_tts.py | 2 +- tests/components/whirlpool/test_climate.py | 2 +- tests/components/ws66i/test_media_player.py | 4 ++-- tests/components/yandextts/test_tts.py | 2 +- tests/components/zha/test_climate.py | 2 +- tests/components/zwave_js/test_api.py | 5 +---- tests/components/zwave_js/test_climate.py | 2 +- 65 files changed, 82 insertions(+), 88 deletions(-) diff --git a/tests/components/advantage_air/test_climate.py b/tests/components/advantage_air/test_climate.py index 39999ba4ed9..feb2a790e30 100644 --- a/tests/components/advantage_air/test_climate.py +++ b/tests/components/advantage_air/test_climate.py @@ -11,7 +11,7 @@ from homeassistant.components.advantage_air.const import ( ADVANTAGE_AIR_STATE_OFF, ADVANTAGE_AIR_STATE_ON, ) -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_FAN_MODE, ATTR_HVAC_MODE, DOMAIN as CLIMATE_DOMAIN, diff --git a/tests/components/airzone/test_climate.py b/tests/components/airzone/test_climate.py index dcb493351ba..a492abd8c61 100644 --- a/tests/components/airzone/test_climate.py +++ b/tests/components/airzone/test_climate.py @@ -14,7 +14,7 @@ from aioairzone.exceptions import AirzoneError import pytest from homeassistant.components.airzone.const import API_TEMPERATURE_STEP -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_CURRENT_HUMIDITY, ATTR_CURRENT_TEMPERATURE, ATTR_HVAC_ACTION, diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 85926810290..9d329e76c45 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -8,7 +8,7 @@ import pytest from homeassistant.components.alexa import messages, smart_home import homeassistant.components.camera as camera from homeassistant.components.cover import CoverDeviceClass -from homeassistant.components.media_player.const import ( +from homeassistant.components.media_player import ( SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, diff --git a/tests/components/atag/test_climate.py b/tests/components/atag/test_climate.py index 2369859a8e6..485ad0308bc 100644 --- a/tests/components/atag/test_climate.py +++ b/tests/components/atag/test_climate.py @@ -7,11 +7,13 @@ from homeassistant.components.climate import ( ATTR_HVAC_MODE, ATTR_PRESET_MODE, DOMAIN as CLIMATE_DOMAIN, + PRESET_AWAY, SERVICE_SET_HVAC_MODE, SERVICE_SET_PRESET_MODE, SERVICE_SET_TEMPERATURE, + HVACAction, + HVACMode, ) -from homeassistant.components.climate.const import PRESET_AWAY, HVACAction, HVACMode from homeassistant.components.homeassistant import DOMAIN as HA_DOMAIN from homeassistant.const import ( ATTR_ENTITY_ID, diff --git a/tests/components/balboa/test_climate.py b/tests/components/balboa/test_climate.py index ce3498c1d9f..571ec1f432e 100644 --- a/tests/components/balboa/test_climate.py +++ b/tests/components/balboa/test_climate.py @@ -5,7 +5,7 @@ from unittest.mock import MagicMock, patch import pytest -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_FAN_MODE, ATTR_HVAC_ACTION, ATTR_HVAC_MODES, diff --git a/tests/components/blackbird/test_media_player.py b/tests/components/blackbird/test_media_player.py index a2f5f9914da..1841af773bb 100644 --- a/tests/components/blackbird/test_media_player.py +++ b/tests/components/blackbird/test_media_player.py @@ -11,7 +11,7 @@ from homeassistant.components.blackbird.media_player import ( PLATFORM_SCHEMA, setup_platform, ) -from homeassistant.components.media_player.const import ( +from homeassistant.components.media_player import ( SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, diff --git a/tests/components/blebox/test_climate.py b/tests/components/blebox/test_climate.py index 8ae1c80d960..7bef83e632a 100644 --- a/tests/components/blebox/test_climate.py +++ b/tests/components/blebox/test_climate.py @@ -5,7 +5,7 @@ from unittest.mock import AsyncMock, PropertyMock import blebox_uniapi import pytest -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_CURRENT_TEMPERATURE, ATTR_HVAC_ACTION, ATTR_HVAC_MODE, diff --git a/tests/components/climate/common.py b/tests/components/climate/common.py index 5b75dc98e69..7d66b886810 100644 --- a/tests/components/climate/common.py +++ b/tests/components/climate/common.py @@ -3,8 +3,8 @@ All containing methods are legacy helpers that should not be used by new components. Instead call the service directly. """ -from homeassistant.components.climate import _LOGGER -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( + _LOGGER, ATTR_AUX_HEAT, ATTR_FAN_MODE, ATTR_HUMIDITY, diff --git a/tests/components/climate/test_recorder.py b/tests/components/climate/test_recorder.py index 7ed604495dc..bf254d2c02f 100644 --- a/tests/components/climate/test_recorder.py +++ b/tests/components/climate/test_recorder.py @@ -4,7 +4,7 @@ from __future__ import annotations from datetime import timedelta from homeassistant.components import climate -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_FAN_MODES, ATTR_HVAC_MODES, ATTR_MAX_HUMIDITY, diff --git a/tests/components/climate/test_reproduce_state.py b/tests/components/climate/test_reproduce_state.py index c91340b43cb..dc6bb97cde6 100644 --- a/tests/components/climate/test_reproduce_state.py +++ b/tests/components/climate/test_reproduce_state.py @@ -2,7 +2,7 @@ import pytest -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_AUX_HEAT, ATTR_FAN_MODE, ATTR_HUMIDITY, diff --git a/tests/components/deconz/test_climate.py b/tests/components/deconz/test_climate.py index c621ce02823..e85b8b0b6fd 100644 --- a/tests/components/deconz/test_climate.py +++ b/tests/components/deconz/test_climate.py @@ -4,18 +4,12 @@ from unittest.mock import patch import pytest from homeassistant.components.climate import ( - DOMAIN as CLIMATE_DOMAIN, - SERVICE_SET_FAN_MODE, - SERVICE_SET_HVAC_MODE, - SERVICE_SET_PRESET_MODE, - SERVICE_SET_TEMPERATURE, -) -from homeassistant.components.climate.const import ( ATTR_FAN_MODE, ATTR_HVAC_MODE, ATTR_PRESET_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, + DOMAIN as CLIMATE_DOMAIN, FAN_AUTO, FAN_HIGH, FAN_LOW, @@ -25,6 +19,10 @@ from homeassistant.components.climate.const import ( PRESET_BOOST, PRESET_COMFORT, PRESET_ECO, + SERVICE_SET_FAN_MODE, + SERVICE_SET_HVAC_MODE, + SERVICE_SET_PRESET_MODE, + SERVICE_SET_TEMPERATURE, HVACAction, HVACMode, ) diff --git a/tests/components/demo/test_climate.py b/tests/components/demo/test_climate.py index 332d2bf4f2e..67431ebb208 100644 --- a/tests/components/demo/test_climate.py +++ b/tests/components/demo/test_climate.py @@ -3,7 +3,7 @@ import pytest import voluptuous as vol -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_AUX_HEAT, ATTR_CURRENT_HUMIDITY, ATTR_CURRENT_TEMPERATURE, diff --git a/tests/components/devolo_home_control/test_climate.py b/tests/components/devolo_home_control/test_climate.py index 4fca1825459..98200b66476 100644 --- a/tests/components/devolo_home_control/test_climate.py +++ b/tests/components/devolo_home_control/test_climate.py @@ -1,9 +1,9 @@ """Tests for the devolo Home Control climate.""" from unittest.mock import patch -from homeassistant.components.climate import DOMAIN -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_HVAC_MODE, + DOMAIN, SERVICE_SET_TEMPERATURE, HVACMode, ) diff --git a/tests/components/freedompro/test_climate.py b/tests/components/freedompro/test_climate.py index da65abffeed..f12248da5cd 100644 --- a/tests/components/freedompro/test_climate.py +++ b/tests/components/freedompro/test_climate.py @@ -14,8 +14,8 @@ from homeassistant.components.climate import ( DOMAIN as CLIMATE_DOMAIN, SERVICE_SET_HVAC_MODE, SERVICE_SET_TEMPERATURE, + HVACMode, ) -from homeassistant.components.climate.const import HVACMode from homeassistant.const import ATTR_ENTITY_ID from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.util.dt import utcnow diff --git a/tests/components/fritzbox/__init__.py b/tests/components/fritzbox/__init__.py index 3ed4327d8e3..acafd19c924 100644 --- a/tests/components/fritzbox/__init__.py +++ b/tests/components/fritzbox/__init__.py @@ -4,7 +4,7 @@ from __future__ import annotations from typing import Any from unittest.mock import Mock -from homeassistant.components.climate.const import PRESET_COMFORT, PRESET_ECO +from homeassistant.components.climate import PRESET_COMFORT, PRESET_ECO from homeassistant.components.fritzbox.const import DOMAIN from homeassistant.core import HomeAssistant diff --git a/tests/components/fritzbox/test_climate.py b/tests/components/fritzbox/test_climate.py index 9cd7f1a74d7..26c3e5a3d34 100644 --- a/tests/components/fritzbox/test_climate.py +++ b/tests/components/fritzbox/test_climate.py @@ -4,7 +4,7 @@ from unittest.mock import Mock, call from requests.exceptions import HTTPError -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_CURRENT_TEMPERATURE, ATTR_HVAC_MODE, ATTR_HVAC_MODES, diff --git a/tests/components/generic_thermostat/test_climate.py b/tests/components/generic_thermostat/test_climate.py index c4d23b41579..f6a8795a29d 100644 --- a/tests/components/generic_thermostat/test_climate.py +++ b/tests/components/generic_thermostat/test_climate.py @@ -7,7 +7,7 @@ import voluptuous as vol from homeassistant import config as hass_config from homeassistant.components import input_boolean, switch -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_PRESET_MODE, DOMAIN, PRESET_ACTIVITY, diff --git a/tests/components/google_translate/test_tts.py b/tests/components/google_translate/test_tts.py index cc80d9c64b9..562f35655d0 100644 --- a/tests/components/google_translate/test_tts.py +++ b/tests/components/google_translate/test_tts.py @@ -7,7 +7,7 @@ from gtts import gTTSError import pytest from homeassistant.components import media_source, tts -from homeassistant.components.media_player.const import ( +from homeassistant.components.media_player import ( ATTR_MEDIA_CONTENT_ID, DOMAIN as DOMAIN_MP, SERVICE_PLAY_MEDIA, diff --git a/tests/components/gree/test_bridge.py b/tests/components/gree/test_bridge.py index 13522b1216b..24672606f34 100644 --- a/tests/components/gree/test_bridge.py +++ b/tests/components/gree/test_bridge.py @@ -4,7 +4,7 @@ from unittest.mock import patch import pytest -from homeassistant.components.climate.const import DOMAIN +from homeassistant.components.climate import DOMAIN from homeassistant.components.gree.const import COORDINATORS, DOMAIN as GREE import homeassistant.util.dt as dt_util diff --git a/tests/components/gree/test_climate.py b/tests/components/gree/test_climate.py index ac818c7fd32..de873726c44 100644 --- a/tests/components/gree/test_climate.py +++ b/tests/components/gree/test_climate.py @@ -6,8 +6,7 @@ from greeclimate.device import HorizontalSwing, VerticalSwing from greeclimate.exceptions import DeviceNotBoundError, DeviceTimeoutError import pytest -from homeassistant.components.climate import ClimateEntityFeature -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_CURRENT_TEMPERATURE, ATTR_FAN_MODE, ATTR_HVAC_MODE, @@ -32,6 +31,7 @@ from homeassistant.components.climate.const import ( SWING_HORIZONTAL, SWING_OFF, SWING_VERTICAL, + ClimateEntityFeature, HVACMode, ) from homeassistant.components.gree.climate import FAN_MODES_REVERSE, HVAC_MODES_REVERSE diff --git a/tests/components/heos/test_init.py b/tests/components/heos/test_init.py index 6edbf7d8543..f55e8102b33 100644 --- a/tests/components/heos/test_init.py +++ b/tests/components/heos/test_init.py @@ -15,7 +15,7 @@ from homeassistant.components.heos.const import ( DATA_SOURCE_MANAGER, DOMAIN, ) -from homeassistant.components.media_player.const import DOMAIN as MEDIA_PLAYER_DOMAIN +from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN from homeassistant.const import CONF_HOST from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.setup import async_setup_component diff --git a/tests/components/homekit/test_type_media_players.py b/tests/components/homekit/test_type_media_players.py index 7446b0d4c3a..dbdc2b0ba55 100644 --- a/tests/components/homekit/test_type_media_players.py +++ b/tests/components/homekit/test_type_media_players.py @@ -18,13 +18,13 @@ from homeassistant.components.homekit.type_media_players import ( MediaPlayer, TelevisionMediaPlayer, ) -from homeassistant.components.media_player import MediaPlayerDeviceClass -from homeassistant.components.media_player.const import ( +from homeassistant.components.media_player import ( ATTR_INPUT_SOURCE, ATTR_INPUT_SOURCE_LIST, ATTR_MEDIA_VOLUME_LEVEL, ATTR_MEDIA_VOLUME_MUTED, DOMAIN, + MediaPlayerDeviceClass, ) from homeassistant.const import ( ATTR_DEVICE_CLASS, diff --git a/tests/components/homekit_controller/specific_devices/test_ecobee3.py b/tests/components/homekit_controller/specific_devices/test_ecobee3.py index 27392afc9b0..69a7d4f809c 100644 --- a/tests/components/homekit_controller/specific_devices/test_ecobee3.py +++ b/tests/components/homekit_controller/specific_devices/test_ecobee3.py @@ -9,7 +9,7 @@ from unittest import mock from aiohomekit import AccessoryNotFoundError from aiohomekit.testing import FakePairing -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( SUPPORT_TARGET_HUMIDITY, SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, diff --git a/tests/components/homekit_controller/specific_devices/test_ecobee_501.py b/tests/components/homekit_controller/specific_devices/test_ecobee_501.py index 716ad6bc7c0..cf498a61e81 100644 --- a/tests/components/homekit_controller/specific_devices/test_ecobee_501.py +++ b/tests/components/homekit_controller/specific_devices/test_ecobee_501.py @@ -1,7 +1,7 @@ """Tests for Ecobee 501.""" -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( SUPPORT_FAN_MODE, SUPPORT_TARGET_HUMIDITY, SUPPORT_TARGET_TEMPERATURE, diff --git a/tests/components/homekit_controller/specific_devices/test_lennox_e30.py b/tests/components/homekit_controller/specific_devices/test_lennox_e30.py index 9bb31394cbd..1bb31241023 100644 --- a/tests/components/homekit_controller/specific_devices/test_lennox_e30.py +++ b/tests/components/homekit_controller/specific_devices/test_lennox_e30.py @@ -4,7 +4,7 @@ Regression tests for Aqara Gateway V3. https://github.com/home-assistant/core/issues/20885 """ -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, ) diff --git a/tests/components/homekit_controller/specific_devices/test_lg_tv.py b/tests/components/homekit_controller/specific_devices/test_lg_tv.py index 26909dabc59..22d29f7500d 100644 --- a/tests/components/homekit_controller/specific_devices/test_lg_tv.py +++ b/tests/components/homekit_controller/specific_devices/test_lg_tv.py @@ -1,6 +1,6 @@ """Make sure that handling real world LG HomeKit characteristics isn't broken.""" -from homeassistant.components.media_player.const import ( +from homeassistant.components.media_player import ( SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_SELECT_SOURCE, diff --git a/tests/components/homekit_controller/test_climate.py b/tests/components/homekit_controller/test_climate.py index 3a939948595..c5cad7015d8 100644 --- a/tests/components/homekit_controller/test_climate.py +++ b/tests/components/homekit_controller/test_climate.py @@ -8,7 +8,7 @@ from aiohomekit.model.characteristics import ( ) from aiohomekit.model.services import ServicesTypes -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( DOMAIN, SERVICE_SET_FAN_MODE, SERVICE_SET_HUMIDITY, diff --git a/tests/components/homematicip_cloud/test_climate.py b/tests/components/homematicip_cloud/test_climate.py index 93fa1700629..1f7b91d59a6 100644 --- a/tests/components/homematicip_cloud/test_climate.py +++ b/tests/components/homematicip_cloud/test_climate.py @@ -4,12 +4,12 @@ import datetime from homematicip.base.enums import AbsenceType from homematicip.functionalHomes import IndoorClimateHome -from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_CURRENT_TEMPERATURE, ATTR_HVAC_ACTION, ATTR_PRESET_MODE, ATTR_PRESET_MODES, + DOMAIN as CLIMATE_DOMAIN, PRESET_AWAY, PRESET_BOOST, PRESET_ECO, diff --git a/tests/components/kaleidescape/test_media_player.py b/tests/components/kaleidescape/test_media_player.py index 11f5d5f5f2b..f38c61d3e73 100644 --- a/tests/components/kaleidescape/test_media_player.py +++ b/tests/components/kaleidescape/test_media_player.py @@ -5,7 +5,7 @@ from unittest.mock import MagicMock from kaleidescape import const as kaleidescape_const from kaleidescape.device import Movie -from homeassistant.components.media_player.const import DOMAIN as MEDIA_PLAYER_DOMAIN +from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_MEDIA_NEXT_TRACK, diff --git a/tests/components/knx/test_climate.py b/tests/components/knx/test_climate.py index 38d329755d6..454b814638c 100644 --- a/tests/components/knx/test_climate.py +++ b/tests/components/knx/test_climate.py @@ -1,5 +1,5 @@ """Test KNX climate.""" -from homeassistant.components.climate.const import PRESET_ECO, PRESET_SLEEP, HVACMode +from homeassistant.components.climate import PRESET_ECO, PRESET_SLEEP, HVACMode from homeassistant.components.knx.schema import ClimateSchema from homeassistant.const import CONF_NAME, STATE_IDLE from homeassistant.core import HomeAssistant diff --git a/tests/components/kodi/test_device_trigger.py b/tests/components/kodi/test_device_trigger.py index 505473209ba..9e3295f5ac0 100644 --- a/tests/components/kodi/test_device_trigger.py +++ b/tests/components/kodi/test_device_trigger.py @@ -4,7 +4,7 @@ import pytest import homeassistant.components.automation as automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.kodi import DOMAIN -from homeassistant.components.media_player.const import DOMAIN as MP_DOMAIN +from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.setup import async_setup_component from . import init_integration diff --git a/tests/components/marytts/test_tts.py b/tests/components/marytts/test_tts.py index 60211f7dc0c..04f3f4f6dcd 100644 --- a/tests/components/marytts/test_tts.py +++ b/tests/components/marytts/test_tts.py @@ -6,7 +6,7 @@ from unittest.mock import patch import pytest from homeassistant.components import media_source, tts -from homeassistant.components.media_player.const import ( +from homeassistant.components.media_player import ( ATTR_MEDIA_CONTENT_ID, DOMAIN as DOMAIN_MP, SERVICE_PLAY_MEDIA, diff --git a/tests/components/media_player/common.py b/tests/components/media_player/common.py index 7b1c9cb45a1..db2ef76b210 100644 --- a/tests/components/media_player/common.py +++ b/tests/components/media_player/common.py @@ -3,7 +3,7 @@ All containing methods are legacy helpers that should not be used by new components. Instead call the service directly. """ -from homeassistant.components.media_player.const import ( +from homeassistant.components.media_player import ( ATTR_INPUT_SOURCE, ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, diff --git a/tests/components/media_player/test_recorder.py b/tests/components/media_player/test_recorder.py index 1d053a23cee..dd1329be81e 100644 --- a/tests/components/media_player/test_recorder.py +++ b/tests/components/media_player/test_recorder.py @@ -4,7 +4,7 @@ from __future__ import annotations from datetime import timedelta from homeassistant.components import media_player -from homeassistant.components.media_player.const import ( +from homeassistant.components.media_player import ( ATTR_ENTITY_PICTURE_LOCAL, ATTR_INPUT_SOURCE_LIST, ATTR_MEDIA_POSITION, diff --git a/tests/components/media_player/test_reproduce_state.py b/tests/components/media_player/test_reproduce_state.py index f880130d4bd..cc30058d4b1 100644 --- a/tests/components/media_player/test_reproduce_state.py +++ b/tests/components/media_player/test_reproduce_state.py @@ -2,7 +2,7 @@ import pytest -from homeassistant.components.media_player.const import ( +from homeassistant.components.media_player import ( ATTR_INPUT_SOURCE, ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, diff --git a/tests/components/melissa/test_climate.py b/tests/components/melissa/test_climate.py index 75d053dc011..9eb2a9fda78 100644 --- a/tests/components/melissa/test_climate.py +++ b/tests/components/melissa/test_climate.py @@ -2,7 +2,7 @@ import json from unittest.mock import AsyncMock, Mock, patch -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( FAN_HIGH, FAN_LOW, FAN_MEDIUM, diff --git a/tests/components/modbus/test_climate.py b/tests/components/modbus/test_climate.py index e19a2b18186..942f6997c21 100644 --- a/tests/components/modbus/test_climate.py +++ b/tests/components/modbus/test_climate.py @@ -1,8 +1,7 @@ """The tests for the Modbus climate component.""" import pytest -from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN -from homeassistant.components.climate.const import HVACMode +from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN, HVACMode from homeassistant.components.modbus.const import ( CONF_CLIMATES, CONF_DATA_TYPE, diff --git a/tests/components/monoprice/test_media_player.py b/tests/components/monoprice/test_media_player.py index 0a11d665395..0d2969853f4 100644 --- a/tests/components/monoprice/test_media_player.py +++ b/tests/components/monoprice/test_media_player.py @@ -4,7 +4,7 @@ from unittest.mock import patch from serial import SerialException -from homeassistant.components.media_player.const import ( +from homeassistant.components.media_player import ( ATTR_INPUT_SOURCE, ATTR_INPUT_SOURCE_LIST, ATTR_MEDIA_VOLUME_LEVEL, diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index ec2501e11d3..d7d278be160 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -7,8 +7,7 @@ import pytest import voluptuous as vol from homeassistant.components import climate, mqtt -from homeassistant.components.climate import DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_AUX_HEAT, ATTR_CURRENT_TEMPERATURE, ATTR_FAN_MODE, @@ -16,6 +15,8 @@ from homeassistant.components.climate.const import ( ATTR_SWING_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, + DEFAULT_MAX_TEMP, + DEFAULT_MIN_TEMP, PRESET_ECO, ClimateEntityFeature, HVACAction, diff --git a/tests/components/netatmo/test_climate.py b/tests/components/netatmo/test_climate.py index a90023e29f7..0d51a53ec71 100644 --- a/tests/components/netatmo/test_climate.py +++ b/tests/components/netatmo/test_climate.py @@ -2,18 +2,16 @@ from unittest.mock import patch from homeassistant.components.climate import ( + ATTR_HVAC_MODE, + ATTR_PRESET_MODE, DOMAIN as CLIMATE_DOMAIN, + PRESET_AWAY, + PRESET_BOOST, SERVICE_SET_HVAC_MODE, SERVICE_SET_PRESET_MODE, SERVICE_SET_TEMPERATURE, SERVICE_TURN_OFF, SERVICE_TURN_ON, -) -from homeassistant.components.climate.const import ( - ATTR_HVAC_MODE, - ATTR_PRESET_MODE, - PRESET_AWAY, - PRESET_BOOST, HVACMode, ) from homeassistant.components.netatmo.climate import PRESET_FROST_GUARD, PRESET_SCHEDULE diff --git a/tests/components/nexia/test_climate.py b/tests/components/nexia/test_climate.py index 797fbb4014c..bbe0c254edc 100644 --- a/tests/components/nexia/test_climate.py +++ b/tests/components/nexia/test_climate.py @@ -1,5 +1,5 @@ """The lock tests for the august platform.""" -from homeassistant.components.climate.const import HVACMode +from homeassistant.components.climate import HVACMode from .util import async_init_integration diff --git a/tests/components/plex/test_browse_media.py b/tests/components/plex/test_browse_media.py index 6af11a61191..0e08afeb0c2 100644 --- a/tests/components/plex/test_browse_media.py +++ b/tests/components/plex/test_browse_media.py @@ -4,7 +4,7 @@ from unittest.mock import Mock, patch from yarl import URL -from homeassistant.components.media_player.const import ( +from homeassistant.components.media_player import ( ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, ) diff --git a/tests/components/plugwise/test_climate.py b/tests/components/plugwise/test_climate.py index bf110cd8a91..58faeda8d7c 100644 --- a/tests/components/plugwise/test_climate.py +++ b/tests/components/plugwise/test_climate.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock from plugwise.exceptions import PlugwiseException import pytest -from homeassistant.components.climate.const import HVACMode +from homeassistant.components.climate import HVACMode from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError diff --git a/tests/components/samsungtv/test_init.py b/tests/components/samsungtv/test_init.py index 7d688a5febb..3b4e33ebf3e 100644 --- a/tests/components/samsungtv/test_init.py +++ b/tests/components/samsungtv/test_init.py @@ -3,7 +3,7 @@ from unittest.mock import Mock, patch import pytest -from homeassistant.components.media_player.const import DOMAIN, SUPPORT_TURN_ON +from homeassistant.components.media_player import DOMAIN, SUPPORT_TURN_ON from homeassistant.components.samsungtv.const import ( CONF_ON_ACTION, CONF_SSDP_MAIN_TV_AGENT_LOCATION, diff --git a/tests/components/sensibo/test_climate.py b/tests/components/sensibo/test_climate.py index b66bbd14afb..f9c3a7cb301 100644 --- a/tests/components/sensibo/test_climate.py +++ b/tests/components/sensibo/test_climate.py @@ -8,7 +8,7 @@ from pysensibo.model import SensiboData import pytest from voluptuous import MultipleInvalid -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_FAN_MODE, ATTR_HVAC_MODE, ATTR_SWING_MODE, diff --git a/tests/components/sensibo/test_entity.py b/tests/components/sensibo/test_entity.py index 818d9ddb924..68275be4cf7 100644 --- a/tests/components/sensibo/test_entity.py +++ b/tests/components/sensibo/test_entity.py @@ -6,7 +6,7 @@ from unittest.mock import patch from pysensibo.model import SensiboData import pytest -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_FAN_MODE, DOMAIN as CLIMATE_DOMAIN, SERVICE_SET_FAN_MODE, diff --git a/tests/components/smartthings/test_climate.py b/tests/components/smartthings/test_climate.py index 825b8259276..22532139dde 100644 --- a/tests/components/smartthings/test_climate.py +++ b/tests/components/smartthings/test_climate.py @@ -8,7 +8,7 @@ from pysmartthings import Attribute, Capability from pysmartthings.device import Status import pytest -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_CURRENT_HUMIDITY, ATTR_CURRENT_TEMPERATURE, ATTR_FAN_MODE, diff --git a/tests/components/smarttub/test_climate.py b/tests/components/smarttub/test_climate.py index b290475d245..c123968e7fd 100644 --- a/tests/components/smarttub/test_climate.py +++ b/tests/components/smarttub/test_climate.py @@ -1,7 +1,7 @@ """Test the SmartTub climate platform.""" import smarttub -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_CURRENT_TEMPERATURE, ATTR_HVAC_ACTION, ATTR_HVAC_MODE, diff --git a/tests/components/sonos/test_services.py b/tests/components/sonos/test_services.py index d0bf3bf3a06..7f2bfc2fb8a 100644 --- a/tests/components/sonos/test_services.py +++ b/tests/components/sonos/test_services.py @@ -3,8 +3,7 @@ from unittest.mock import Mock, patch import pytest -from homeassistant.components.media_player import DOMAIN as MP_DOMAIN -from homeassistant.components.media_player.const import SERVICE_JOIN +from homeassistant.components.media_player import DOMAIN as MP_DOMAIN, SERVICE_JOIN from homeassistant.components.sonos.const import DATA_SONOS from homeassistant.exceptions import HomeAssistantError diff --git a/tests/components/soundtouch/conftest.py b/tests/components/soundtouch/conftest.py index 21de9e2ed47..e944da89d8c 100644 --- a/tests/components/soundtouch/conftest.py +++ b/tests/components/soundtouch/conftest.py @@ -2,7 +2,7 @@ import pytest from requests_mock import Mocker -from homeassistant.components.media_player.const import DOMAIN as MEDIA_PLAYER_DOMAIN +from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN from homeassistant.components.soundtouch.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_NAME diff --git a/tests/components/soundtouch/test_media_player.py b/tests/components/soundtouch/test_media_player.py index 5105d07479c..f60ec4022ae 100644 --- a/tests/components/soundtouch/test_media_player.py +++ b/tests/components/soundtouch/test_media_player.py @@ -4,7 +4,7 @@ from typing import Any from requests_mock import Mocker -from homeassistant.components.media_player.const import ( +from homeassistant.components.media_player import ( ATTR_INPUT_SOURCE, ATTR_MEDIA_ALBUM_NAME, ATTR_MEDIA_ARTIST, diff --git a/tests/components/tts/test_notify.py b/tests/components/tts/test_notify.py index 912896dd3e2..d19567333dc 100644 --- a/tests/components/tts/test_notify.py +++ b/tests/components/tts/test_notify.py @@ -3,7 +3,7 @@ import pytest import yarl import homeassistant.components.media_player as media_player -from homeassistant.components.media_player.const import ( +from homeassistant.components.media_player import ( DOMAIN as DOMAIN_MP, SERVICE_PLAY_MEDIA, ) diff --git a/tests/components/unifiprotect/test_media_player.py b/tests/components/unifiprotect/test_media_player.py index c78718e3c06..1679d17c96c 100644 --- a/tests/components/unifiprotect/test_media_player.py +++ b/tests/components/unifiprotect/test_media_player.py @@ -8,7 +8,7 @@ import pytest from pyunifiprotect.data import Camera from pyunifiprotect.exceptions import StreamError -from homeassistant.components.media_player.const import ( +from homeassistant.components.media_player import ( ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_VOLUME_LEVEL, ) diff --git a/tests/components/universal/test_media_player.py b/tests/components/universal/test_media_player.py index 059a19caf45..a949cd76d59 100644 --- a/tests/components/universal/test_media_player.py +++ b/tests/components/universal/test_media_player.py @@ -9,7 +9,7 @@ from homeassistant import config as hass_config import homeassistant.components.input_number as input_number import homeassistant.components.input_select as input_select import homeassistant.components.media_player as media_player -from homeassistant.components.media_player.const import MediaPlayerEntityFeature +from homeassistant.components.media_player import MediaPlayerEntityFeature import homeassistant.components.switch as switch import homeassistant.components.universal.media_player as universal from homeassistant.const import ( diff --git a/tests/components/venstar/test_climate.py b/tests/components/venstar/test_climate.py index 15b5d4df7b5..b544853b76d 100644 --- a/tests/components/venstar/test_climate.py +++ b/tests/components/venstar/test_climate.py @@ -1,7 +1,7 @@ """The climate tests for the venstar integration.""" from unittest.mock import patch -from homeassistant.components.climate.const import ClimateEntityFeature +from homeassistant.components.climate import ClimateEntityFeature from .util import async_init_integration, mock_venstar_devices diff --git a/tests/components/venstar/util.py b/tests/components/venstar/util.py index aa53a9c0a8d..23e480272d0 100644 --- a/tests/components/venstar/util.py +++ b/tests/components/venstar/util.py @@ -2,7 +2,7 @@ import requests_mock -from homeassistant.components.climate.const import DOMAIN +from homeassistant.components.climate import DOMAIN from homeassistant.const import CONF_HOST, CONF_PLATFORM from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component diff --git a/tests/components/vera/test_climate.py b/tests/components/vera/test_climate.py index c4e10c6a420..9e5c4b607fe 100644 --- a/tests/components/vera/test_climate.py +++ b/tests/components/vera/test_climate.py @@ -3,7 +3,7 @@ from unittest.mock import MagicMock import pyvera as pv -from homeassistant.components.climate.const import FAN_AUTO, FAN_ON, HVACMode +from homeassistant.components.climate import FAN_AUTO, FAN_ON, HVACMode from homeassistant.core import HomeAssistant from .common import ComponentFactory, new_simple_controller_config diff --git a/tests/components/vizio/test_media_player.py b/tests/components/vizio/test_media_player.py index 1c637733927..2a5a2acacd1 100644 --- a/tests/components/vizio/test_media_player.py +++ b/tests/components/vizio/test_media_player.py @@ -21,6 +21,7 @@ import voluptuous as vol from homeassistant.components.media_player import ( ATTR_INPUT_SOURCE, + ATTR_INPUT_SOURCE_LIST, ATTR_MEDIA_VOLUME_LEVEL, ATTR_MEDIA_VOLUME_MUTED, ATTR_SOUND_MODE, @@ -37,7 +38,6 @@ from homeassistant.components.media_player import ( SERVICE_VOLUME_UP, MediaPlayerDeviceClass, ) -from homeassistant.components.media_player.const import ATTR_INPUT_SOURCE_LIST from homeassistant.components.vizio import validate_apps from homeassistant.components.vizio.const import ( CONF_ADDITIONAL_CONFIGS, diff --git a/tests/components/voicerss/test_tts.py b/tests/components/voicerss/test_tts.py index 099b280625f..2af43cf988f 100644 --- a/tests/components/voicerss/test_tts.py +++ b/tests/components/voicerss/test_tts.py @@ -7,7 +7,7 @@ import shutil import pytest from homeassistant.components import media_source, tts -from homeassistant.components.media_player.const import ( +from homeassistant.components.media_player import ( ATTR_MEDIA_CONTENT_ID, DOMAIN as DOMAIN_MP, SERVICE_PLAY_MEDIA, diff --git a/tests/components/whirlpool/test_climate.py b/tests/components/whirlpool/test_climate.py index f0d4c93f8d6..26dcd5dbf9f 100644 --- a/tests/components/whirlpool/test_climate.py +++ b/tests/components/whirlpool/test_climate.py @@ -5,7 +5,7 @@ from attr import dataclass import pytest import whirlpool -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_CURRENT_HUMIDITY, ATTR_CURRENT_TEMPERATURE, ATTR_FAN_MODE, diff --git a/tests/components/ws66i/test_media_player.py b/tests/components/ws66i/test_media_player.py index fbe6a7b2782..652c6a90346 100644 --- a/tests/components/ws66i/test_media_player.py +++ b/tests/components/ws66i/test_media_player.py @@ -2,13 +2,13 @@ from collections import defaultdict from unittest.mock import patch -from homeassistant.components.media_player import MediaPlayerEntityFeature -from homeassistant.components.media_player.const import ( +from homeassistant.components.media_player import ( ATTR_INPUT_SOURCE, ATTR_INPUT_SOURCE_LIST, ATTR_MEDIA_VOLUME_LEVEL, DOMAIN as MEDIA_PLAYER_DOMAIN, SERVICE_SELECT_SOURCE, + MediaPlayerEntityFeature, ) from homeassistant.components.ws66i.const import ( CONF_SOURCES, diff --git a/tests/components/yandextts/test_tts.py b/tests/components/yandextts/test_tts.py index 8549b51c341..ffa9579f577 100644 --- a/tests/components/yandextts/test_tts.py +++ b/tests/components/yandextts/test_tts.py @@ -7,7 +7,7 @@ import shutil import pytest from homeassistant.components import media_source, tts -from homeassistant.components.media_player.const import ( +from homeassistant.components.media_player import ( ATTR_MEDIA_CONTENT_ID, DOMAIN as DOMAIN_MP, SERVICE_PLAY_MEDIA, diff --git a/tests/components/zha/test_climate.py b/tests/components/zha/test_climate.py index a04b2c116e3..f1b900400ea 100644 --- a/tests/components/zha/test_climate.py +++ b/tests/components/zha/test_climate.py @@ -10,7 +10,7 @@ import zigpy.zcl.clusters from zigpy.zcl.clusters.hvac import Thermostat import zigpy.zcl.foundation as zcl_f -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_CURRENT_TEMPERATURE, ATTR_FAN_MODE, ATTR_FAN_MODES, diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index 0d633720639..ed7c23aef79 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -30,10 +30,7 @@ from zwave_js_server.model.controller import ( ) from zwave_js_server.model.node import Node -from homeassistant.components.websocket_api.const import ( - ERR_INVALID_FORMAT, - ERR_NOT_FOUND, -) +from homeassistant.components.websocket_api import ERR_INVALID_FORMAT, ERR_NOT_FOUND from homeassistant.components.zwave_js.api import ( ADDITIONAL_PROPERTIES, APPLICATION_VERSION, diff --git a/tests/components/zwave_js/test_climate.py b/tests/components/zwave_js/test_climate.py index a8b19b4d5cf..e0b8dbe569f 100644 --- a/tests/components/zwave_js/test_climate.py +++ b/tests/components/zwave_js/test_climate.py @@ -2,7 +2,7 @@ import pytest from zwave_js_server.event import Event -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_CURRENT_HUMIDITY, ATTR_CURRENT_TEMPERATURE, ATTR_FAN_MODE, From fa245e24f8a12f000fe6dacd4e414def6bdb077a Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 21 Sep 2022 11:34:04 -0600 Subject: [PATCH 625/955] Fix bug wherein RainMachine services use the wrong controller (#78780) --- .../components/rainmachine/__init__.py | 77 +++++++++++++------ 1 file changed, 52 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index 756dc9b958d..4d19dbc7bfc 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -2,9 +2,10 @@ from __future__ import annotations import asyncio +from collections.abc import Awaitable, Callable from dataclasses import dataclass from datetime import timedelta -from functools import partial +from functools import partial, wraps from typing import Any from regenmaschine import Client @@ -22,7 +23,7 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant, ServiceCall, callback -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError from homeassistant.helpers import ( aiohttp_client, config_validation as cv, @@ -152,9 +153,9 @@ class RainMachineData: @callback -def async_get_controller_for_service_call( +def async_get_entry_for_service_call( hass: HomeAssistant, call: ServiceCall -) -> Controller: +) -> ConfigEntry: """Get the controller related to a service call (by device ID).""" device_id = call.data[CONF_DEVICE_ID] device_registry = dr.async_get(hass) @@ -166,8 +167,7 @@ def async_get_controller_for_service_call( if (entry := hass.config_entries.async_get_entry(entry_id)) is None: continue if entry.domain == DOMAIN: - data: RainMachineData = hass.data[DOMAIN][entry_id] - return data.controller + return entry raise ValueError(f"No controller for device ID: {device_id}") @@ -288,15 +288,42 @@ async def async_setup_entry( # noqa: C901 entry.async_on_unload(entry.add_update_listener(async_reload_entry)) - async def async_pause_watering(call: ServiceCall) -> None: - """Pause watering for a set number of seconds.""" - controller = async_get_controller_for_service_call(hass, call) - await controller.watering.pause_all(call.data[CONF_SECONDS]) - await async_update_programs_and_zones(hass, entry) + def call_with_controller(update_programs_and_zones: bool = True) -> Callable: + """Hydrate a service call with the appropriate controller.""" - async def async_push_weather_data(call: ServiceCall) -> None: + def decorator(func: Callable) -> Callable[..., Awaitable]: + """Define the decorator.""" + + @wraps(func) + async def wrapper(call: ServiceCall) -> None: + """Wrap the service function.""" + entry = async_get_entry_for_service_call(hass, call) + data: RainMachineData = hass.data[DOMAIN][entry.entry_id] + + try: + await func(call, data.controller) + except RainMachineError as err: + raise HomeAssistantError( + f"Error while executing {func.__name__}: {err}" + ) from err + + if update_programs_and_zones: + await async_update_programs_and_zones(hass, entry) + + return wrapper + + return decorator + + @call_with_controller() + async def async_pause_watering(call: ServiceCall, controller: Controller) -> None: + """Pause watering for a set number of seconds.""" + await controller.watering.pause_all(call.data[CONF_SECONDS]) + + @call_with_controller(update_programs_and_zones=False) + async def async_push_weather_data( + call: ServiceCall, controller: Controller + ) -> None: """Push weather data to the device.""" - controller = async_get_controller_for_service_call(hass, call) await controller.parsers.post_data( { CONF_WEATHER: [ @@ -309,9 +336,11 @@ async def async_setup_entry( # noqa: C901 } ) - async def async_restrict_watering(call: ServiceCall) -> None: + @call_with_controller() + async def async_restrict_watering( + call: ServiceCall, controller: Controller + ) -> None: """Restrict watering for a time period.""" - controller = async_get_controller_for_service_call(hass, call) duration = call.data[CONF_DURATION] await controller.restrictions.set_universal( { @@ -319,30 +348,28 @@ async def async_setup_entry( # noqa: C901 "rainDelayDuration": duration.total_seconds(), }, ) - await async_update_programs_and_zones(hass, entry) - async def async_stop_all(call: ServiceCall) -> None: + @call_with_controller() + async def async_stop_all(call: ServiceCall, controller: Controller) -> None: """Stop all watering.""" - controller = async_get_controller_for_service_call(hass, call) await controller.watering.stop_all() - await async_update_programs_and_zones(hass, entry) - async def async_unpause_watering(call: ServiceCall) -> None: + @call_with_controller() + async def async_unpause_watering(call: ServiceCall, controller: Controller) -> None: """Unpause watering.""" - controller = async_get_controller_for_service_call(hass, call) await controller.watering.unpause_all() - await async_update_programs_and_zones(hass, entry) - async def async_unrestrict_watering(call: ServiceCall) -> None: + @call_with_controller() + async def async_unrestrict_watering( + call: ServiceCall, controller: Controller + ) -> None: """Unrestrict watering.""" - controller = async_get_controller_for_service_call(hass, call) await controller.restrictions.set_universal( { "rainDelayStartTime": round(as_timestamp(utcnow())), "rainDelayDuration": 0, }, ) - await async_update_programs_and_zones(hass, entry) for service_name, schema, method in ( ( From ca78b1a77d531eec9c890fcf4b0e7ba8b0f5cbff Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 21 Sep 2022 19:40:40 +0200 Subject: [PATCH 626/955] Add new energy utility (#78883) * Add new energy utility * Adjust STATISTIC_UNIT_TO_VALID_UNITS --- .../components/recorder/statistics.py | 21 ++---- .../components/recorder/websocket_api.py | 5 +- homeassistant/components/sensor/recorder.py | 9 ++- homeassistant/util/energy.py | 40 +++++++++++ tests/util/test_energy.py | 72 +++++++++++++++++++ 5 files changed, 124 insertions(+), 23 deletions(-) create mode 100644 homeassistant/util/energy.py create mode 100644 tests/util/test_energy.py diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 5f780b5f0c7..be568adbb25 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -26,8 +26,6 @@ import voluptuous as vol from homeassistant.const import ( ENERGY_KILO_WATT_HOUR, - ENERGY_MEGA_WATT_HOUR, - ENERGY_WATT_HOUR, POWER_WATT, PRESSURE_PA, TEMP_CELSIUS, @@ -41,6 +39,7 @@ from homeassistant.helpers.storage import STORAGE_DIR from homeassistant.helpers.typing import UNDEFINED, UndefinedType from homeassistant.util import ( dt as dt_util, + energy as energy_util, power as power_util, pressure as pressure_util, temperature as temperature_util, @@ -138,20 +137,12 @@ def _convert_energy_from_kwh(to_unit: str, value: float | None) -> float | None: """Convert energy in kWh to to_unit.""" if value is None: return None - if to_unit == ENERGY_MEGA_WATT_HOUR: - return value / 1000 - if to_unit == ENERGY_WATT_HOUR: - return value * 1000 - return value + return energy_util.convert(value, ENERGY_KILO_WATT_HOUR, to_unit) def _convert_energy_to_kwh(from_unit: str, value: float) -> float: """Convert energy in from_unit to kWh.""" - if from_unit == ENERGY_MEGA_WATT_HOUR: - return value * 1000 - if from_unit == ENERGY_WATT_HOUR: - return value / 1000 - return value + return energy_util.convert(value, from_unit, ENERGY_KILO_WATT_HOUR) def _convert_power_from_w(to_unit: str, value: float | None) -> float | None: @@ -196,11 +187,7 @@ STATISTIC_UNIT_TO_UNIT_CLASS: dict[str | None, str] = { } STATISTIC_UNIT_TO_VALID_UNITS: dict[str | None, Iterable[str | None]] = { - ENERGY_KILO_WATT_HOUR: [ - ENERGY_KILO_WATT_HOUR, - ENERGY_MEGA_WATT_HOUR, - ENERGY_WATT_HOUR, - ], + ENERGY_KILO_WATT_HOUR: energy_util.VALID_UNITS, POWER_WATT: power_util.VALID_UNITS, PRESSURE_PA: pressure_util.VALID_UNITS, TEMP_CELSIUS: temperature_util.VALID_UNITS, diff --git a/homeassistant/components/recorder/websocket_api.py b/homeassistant/components/recorder/websocket_api.py index 8d371aaa001..97625ba74f4 100644 --- a/homeassistant/components/recorder/websocket_api.py +++ b/homeassistant/components/recorder/websocket_api.py @@ -21,6 +21,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.json import JSON_DUMP from homeassistant.util import ( dt as dt_util, + energy as energy_util, power as power_util, pressure as pressure_util, temperature as temperature_util, @@ -120,9 +121,7 @@ async def ws_handle_get_statistics_during_period( vol.Required("period"): vol.Any("5minute", "hour", "day", "month"), vol.Optional("units"): vol.Schema( { - vol.Optional("energy"): vol.Any( - ENERGY_WATT_HOUR, ENERGY_KILO_WATT_HOUR, ENERGY_MEGA_WATT_HOUR - ), + vol.Optional("energy"): vol.In(energy_util.VALID_UNITS), vol.Optional("power"): vol.In(power_util.VALID_UNITS), vol.Optional("pressure"): vol.In(pressure_util.VALID_UNITS), vol.Optional("temperature"): vol.In(temperature_util.VALID_UNITS), diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index e0a76cc3588..40577e6962f 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -49,6 +49,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity import entity_sources from homeassistant.util import ( dt as dt_util, + energy as energy_util, power as power_util, pressure as pressure_util, temperature as temperature_util, @@ -86,9 +87,11 @@ DEVICE_CLASS_UNITS: dict[str, str] = { UNIT_CONVERSIONS: dict[str, dict[str, Callable]] = { # Convert energy to kWh SensorDeviceClass.ENERGY: { - ENERGY_KILO_WATT_HOUR: lambda x: x, - ENERGY_MEGA_WATT_HOUR: lambda x: x * 1000, - ENERGY_WATT_HOUR: lambda x: x / 1000, + ENERGY_KILO_WATT_HOUR: lambda x: x + / energy_util.UNIT_CONVERSION[ENERGY_KILO_WATT_HOUR], + ENERGY_MEGA_WATT_HOUR: lambda x: x + / energy_util.UNIT_CONVERSION[ENERGY_MEGA_WATT_HOUR], + ENERGY_WATT_HOUR: lambda x: x / energy_util.UNIT_CONVERSION[ENERGY_WATT_HOUR], }, # Convert power to W SensorDeviceClass.POWER: { diff --git a/homeassistant/util/energy.py b/homeassistant/util/energy.py new file mode 100644 index 00000000000..4d1bd10f4b2 --- /dev/null +++ b/homeassistant/util/energy.py @@ -0,0 +1,40 @@ +"""Energy util functions.""" +from __future__ import annotations + +from numbers import Number + +from homeassistant.const import ( + ENERGY_KILO_WATT_HOUR, + ENERGY_MEGA_WATT_HOUR, + ENERGY_WATT_HOUR, + UNIT_NOT_RECOGNIZED_TEMPLATE, +) + +VALID_UNITS: tuple[str, ...] = ( + ENERGY_WATT_HOUR, + ENERGY_KILO_WATT_HOUR, + ENERGY_MEGA_WATT_HOUR, +) + +UNIT_CONVERSION: dict[str, float] = { + ENERGY_WATT_HOUR: 1 * 1000, + ENERGY_KILO_WATT_HOUR: 1, + ENERGY_MEGA_WATT_HOUR: 1 / 1000, +} + + +def convert(value: float, unit_1: str, unit_2: str) -> float: + """Convert one unit of measurement to another.""" + if unit_1 not in VALID_UNITS: + raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(unit_1, "energy")) + if unit_2 not in VALID_UNITS: + raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(unit_2, "energy")) + + if not isinstance(value, Number): + raise TypeError(f"{value} is not of numeric type") + + if unit_1 == unit_2: + return value + + watts = value / UNIT_CONVERSION[unit_1] + return watts * UNIT_CONVERSION[unit_2] diff --git a/tests/util/test_energy.py b/tests/util/test_energy.py new file mode 100644 index 00000000000..d50bbecc7bf --- /dev/null +++ b/tests/util/test_energy.py @@ -0,0 +1,72 @@ +"""Test Home Assistant eneergy utility functions.""" +import pytest + +from homeassistant.const import ( + ENERGY_KILO_WATT_HOUR, + ENERGY_MEGA_WATT_HOUR, + ENERGY_WATT_HOUR, +) +import homeassistant.util.energy as energy_util + +INVALID_SYMBOL = "bob" +VALID_SYMBOL = ENERGY_KILO_WATT_HOUR + + +def test_convert_same_unit(): + """Test conversion from any unit to same unit.""" + assert energy_util.convert(2, ENERGY_WATT_HOUR, ENERGY_WATT_HOUR) == 2 + assert energy_util.convert(3, ENERGY_KILO_WATT_HOUR, ENERGY_KILO_WATT_HOUR) == 3 + assert energy_util.convert(4, ENERGY_MEGA_WATT_HOUR, ENERGY_MEGA_WATT_HOUR) == 4 + + +def test_convert_invalid_unit(): + """Test exception is thrown for invalid units.""" + with pytest.raises(ValueError): + energy_util.convert(5, INVALID_SYMBOL, VALID_SYMBOL) + + with pytest.raises(ValueError): + energy_util.convert(5, VALID_SYMBOL, INVALID_SYMBOL) + + +def test_convert_nonnumeric_value(): + """Test exception is thrown for nonnumeric type.""" + with pytest.raises(TypeError): + energy_util.convert("a", ENERGY_WATT_HOUR, ENERGY_KILO_WATT_HOUR) + + +def test_convert_from_wh(): + """Test conversion from Wh to other units.""" + watthours = 10 + assert ( + energy_util.convert(watthours, ENERGY_WATT_HOUR, ENERGY_KILO_WATT_HOUR) == 0.01 + ) + assert ( + energy_util.convert(watthours, ENERGY_WATT_HOUR, ENERGY_MEGA_WATT_HOUR) + == 0.00001 + ) + + +def test_convert_from_kwh(): + """Test conversion from kWh to other units.""" + kilowatthours = 10 + assert ( + energy_util.convert(kilowatthours, ENERGY_KILO_WATT_HOUR, ENERGY_WATT_HOUR) + == 10000 + ) + assert ( + energy_util.convert(kilowatthours, ENERGY_KILO_WATT_HOUR, ENERGY_MEGA_WATT_HOUR) + == 0.01 + ) + + +def test_convert_from_mwh(): + """Test conversion from W to other units.""" + megawatthours = 10 + assert ( + energy_util.convert(megawatthours, ENERGY_MEGA_WATT_HOUR, ENERGY_WATT_HOUR) + == 10000000 + ) + assert ( + energy_util.convert(megawatthours, ENERGY_MEGA_WATT_HOUR, ENERGY_KILO_WATT_HOUR) + == 10000 + ) From 4d167856a59fbc7c773ff40815df63a83eb134c6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 21 Sep 2022 08:02:01 -1000 Subject: [PATCH 627/955] Bump unifi-discovery to 1.1.7 (#78898) --- 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 5958da8f00e..abe881c6294 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Protect", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifiprotect", - "requirements": ["pyunifiprotect==4.2.0", "unifi-discovery==1.1.6"], + "requirements": ["pyunifiprotect==4.2.0", "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 a6f22aa535f..5a93852a53a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2439,7 +2439,7 @@ uasiren==0.0.1 ultraheat-api==0.4.3 # homeassistant.components.unifiprotect -unifi-discovery==1.1.6 +unifi-discovery==1.1.7 # homeassistant.components.unifiled unifiled==0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 31904cb485c..945693dc604 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1670,7 +1670,7 @@ uasiren==0.0.1 ultraheat-api==0.4.3 # homeassistant.components.unifiprotect -unifi-discovery==1.1.6 +unifi-discovery==1.1.7 # homeassistant.components.upb upb_lib==0.4.12 From 17ddc407844b3f6a61617e16316641ed57f73890 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 21 Sep 2022 08:02:34 -1000 Subject: [PATCH 628/955] Bump pylutron_caseta to 0.15.2 (#78900) --- homeassistant/components/lutron_caseta/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lutron_caseta/manifest.json b/homeassistant/components/lutron_caseta/manifest.json index 531ecb2a086..88849391e24 100644 --- a/homeassistant/components/lutron_caseta/manifest.json +++ b/homeassistant/components/lutron_caseta/manifest.json @@ -2,7 +2,7 @@ "domain": "lutron_caseta", "name": "Lutron Cas\u00e9ta", "documentation": "https://www.home-assistant.io/integrations/lutron_caseta", - "requirements": ["pylutron-caseta==0.15.1"], + "requirements": ["pylutron-caseta==0.15.2"], "config_flow": true, "zeroconf": ["_leap._tcp.local."], "homekit": { diff --git a/requirements_all.txt b/requirements_all.txt index 5a93852a53a..f9d73a9922b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1681,7 +1681,7 @@ pylitejet==0.3.0 pylitterbot==2022.9.5 # homeassistant.components.lutron_caseta -pylutron-caseta==0.15.1 +pylutron-caseta==0.15.2 # homeassistant.components.lutron pylutron==0.2.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 945693dc604..a45ada504ff 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1176,7 +1176,7 @@ pylitejet==0.3.0 pylitterbot==2022.9.5 # homeassistant.components.lutron_caseta -pylutron-caseta==0.15.1 +pylutron-caseta==0.15.2 # homeassistant.components.mailgun pymailgunner==1.4 From 0a8a5b973a198dff03fba432d73730b8342acdff Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 21 Sep 2022 08:02:54 -1000 Subject: [PATCH 629/955] Fix samsungtv to abort when ATTR_UPNP_MANUFACTURER is missing (#78895) --- .../components/samsungtv/config_flow.py | 2 +- .../components/samsungtv/test_config_flow.py | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/samsungtv/config_flow.py b/homeassistant/components/samsungtv/config_flow.py index 099f0afbae8..d7ad62bdf7a 100644 --- a/homeassistant/components/samsungtv/config_flow.py +++ b/homeassistant/components/samsungtv/config_flow.py @@ -458,7 +458,7 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) if hostname := urlparse(discovery_info.ssdp_location or "").hostname: self._host = hostname - self._manufacturer = discovery_info.upnp[ssdp.ATTR_UPNP_MANUFACTURER] + self._manufacturer = discovery_info.upnp.get(ssdp.ATTR_UPNP_MANUFACTURER) self._abort_if_manufacturer_is_not_samsung() # Set defaults, in case they cannot be extracted from device_info diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index 0b49a064a19..30bb1052702 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -100,6 +100,15 @@ MOCK_SSDP_DATA = ssdp.SsdpServiceInfo( ATTR_UPNP_UDN: "uuid:0d1cef00-00dc-1000-9c80-4844f7b172de", }, ) +MOCK_SSDP_DATA_NO_MANUFACTURER = ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location="https://fake_host:12345/test", + upnp={ + ATTR_UPNP_FRIENDLY_NAME: "[TV] fake_name", + ATTR_UPNP_UDN: "uuid:0d1cef00-00dc-1000-9c80-4844f7b172de", + }, +) MOCK_SSDP_DATA_NOPREFIX = ssdp.SsdpServiceInfo( ssdp_usn="mock_usn", @@ -521,6 +530,18 @@ async def test_ssdp(hass: HomeAssistant) -> None: assert result["result"].unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de" +@pytest.mark.usefixtures("remote", "rest_api_failing") +async def test_ssdp_no_manufacturer(hass: HomeAssistant) -> None: + """Test starting a flow from discovery when the manufacturer data is missing.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=MOCK_SSDP_DATA_NO_MANUFACTURER, + ) + assert result["type"] == "abort" + assert result["reason"] == "not_supported" + + @pytest.mark.parametrize( "data", [MOCK_SSDP_DATA_MAIN_TV_AGENT_ST, MOCK_SSDP_DATA_RENDERING_CONTROL_ST] ) From e079968ef4739d088eb23058f13f8ed4128de9af Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 21 Sep 2022 08:03:05 -1000 Subject: [PATCH 630/955] Handle timeout fetching bond token in config flow (#78896) --- homeassistant/components/bond/config_flow.py | 6 +++- tests/components/bond/test_config_flow.py | 38 ++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/bond/config_flow.py b/homeassistant/components/bond/config_flow.py index 09386c3587d..da8e6781bfa 100644 --- a/homeassistant/components/bond/config_flow.py +++ b/homeassistant/components/bond/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Bond integration.""" from __future__ import annotations +import asyncio import contextlib from http import HTTPStatus import logging @@ -83,7 +84,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): instead ask them to manually enter the token. """ host = self._discovered[CONF_HOST] - if not (token := await async_get_token(self.hass, host)): + try: + if not (token := await async_get_token(self.hass, host)): + return + except asyncio.TimeoutError: return self._discovered[CONF_ACCESS_TOKEN] = token diff --git a/tests/components/bond/test_config_flow.py b/tests/components/bond/test_config_flow.py index 15aa643abaf..a54360283e6 100644 --- a/tests/components/bond/test_config_flow.py +++ b/tests/components/bond/test_config_flow.py @@ -1,6 +1,7 @@ """Test the Bond config flow.""" from __future__ import annotations +import asyncio from http import HTTPStatus from typing import Any from unittest.mock import MagicMock, Mock, patch @@ -268,6 +269,43 @@ async def test_zeroconf_form_token_unavailable(hass: core.HomeAssistant): assert len(mock_setup_entry.mock_calls) == 1 +async def test_zeroconf_form_token_times_out(hass: core.HomeAssistant): + """Test we get the discovery form and we handle the token request timeout.""" + + with patch_bond_version(), patch_bond_token(side_effect=asyncio.TimeoutError): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf.ZeroconfServiceInfo( + host="test-host", + addresses=["test-host"], + hostname="mock_hostname", + name="ZXXX12345.some-other-tail-info", + port=None, + properties={}, + type="mock_type", + ), + ) + await hass.async_block_till_done() + assert result["type"] == "form" + assert result["errors"] == {} + + with patch_bond_version(), patch_bond_bridge(), patch_bond_device_ids(), _patch_async_setup_entry() as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_ACCESS_TOKEN: "test-token"}, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "bond-name" + assert result2["data"] == { + CONF_HOST: "test-host", + CONF_ACCESS_TOKEN: "test-token", + } + assert len(mock_setup_entry.mock_calls) == 1 + + async def test_zeroconf_form_with_token_available(hass: core.HomeAssistant): """Test we get the discovery form when we can get the token.""" From 420285f7ef1e170f599cf22c031987e2ceefa353 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Wed, 21 Sep 2022 11:08:28 -0700 Subject: [PATCH 631/955] Support announce and enqueue in forked-daapd (#77744) --- .../components/forked_daapd/media_player.py | 54 ++++++-- .../forked_daapd/test_media_player.py | 131 ++++++++++++++++-- 2 files changed, 164 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/forked_daapd/media_player.py b/homeassistant/components/forked_daapd/media_player.py index cb33c78fc52..389942a9f59 100644 --- a/homeassistant/components/forked_daapd/media_player.py +++ b/homeassistant/components/forked_daapd/media_player.py @@ -12,7 +12,10 @@ from pylibrespot_java import LibrespotJavaAPI from homeassistant.components import media_source from homeassistant.components.media_player import ( + ATTR_MEDIA_ANNOUNCE, + ATTR_MEDIA_ENQUEUE, BrowseMedia, + MediaPlayerEnqueue, MediaPlayerEntity, MediaPlayerState, MediaType, @@ -349,11 +352,8 @@ class ForkedDaapdMaster(MediaPlayerEntity): @callback def _update_queue(self, queue, event): self._queue = queue - if ( - self._tts_requested - and self._queue["count"] == 1 - and self._queue["items"][0]["uri"].find("tts_proxy") != -1 - ): + if self._tts_requested: + # Assume the change was due to the request self._tts_requested = False self._tts_queued = True @@ -669,10 +669,48 @@ class ForkedDaapdMaster(MediaPlayerEntity): if media_type == MediaType.MUSIC: media_id = async_process_play_media_url(self.hass, media_id) + elif media_type not in CAN_PLAY_TYPE: + _LOGGER.warning("Media type '%s' not supported", media_type) + return - await self._async_announce(media_id) - else: - _LOGGER.debug("Media type '%s' not supported", media_type) + if kwargs.get(ATTR_MEDIA_ANNOUNCE): + return await self._async_announce(media_id) + + # if kwargs[ATTR_MEDIA_ENQUEUE] is None, we assume MediaPlayerEnqueue.REPLACE + # if kwargs[ATTR_MEDIA_ENQUEUE] is True, we assume MediaPlayerEnqueue.ADD + # kwargs[ATTR_MEDIA_ENQUEUE] is assumed to never be False + # See https://github.com/home-assistant/architecture/issues/765 + enqueue: bool | MediaPlayerEnqueue = kwargs.get( + ATTR_MEDIA_ENQUEUE, MediaPlayerEnqueue.REPLACE + ) + if enqueue in {True, MediaPlayerEnqueue.ADD, MediaPlayerEnqueue.REPLACE}: + return await self._api.add_to_queue( + uris=media_id, + playback="start", + clear=enqueue == MediaPlayerEnqueue.REPLACE, + ) + + current_position = next( + ( + item["position"] + for item in self._queue["items"] + if item["id"] == self._player["item_id"] + ), + 0, + ) + if enqueue == MediaPlayerEnqueue.NEXT: + return await self._api.add_to_queue( + uris=media_id, + playback="start", + position=current_position + 1, + ) + # enqueue == MediaPlayerEnqueue.PLAY + return await self._api.add_to_queue( + uris=media_id, + playback="start", + position=current_position, + playback_from_position=current_position, + ) async def _async_announce(self, media_id: str) -> None: """Play a URI.""" diff --git a/tests/components/forked_daapd/test_media_player.py b/tests/components/forked_daapd/test_media_player.py index 8035ec99777..307eb8deea6 100644 --- a/tests/components/forked_daapd/test_media_player.py +++ b/tests/components/forked_daapd/test_media_player.py @@ -22,10 +22,12 @@ from homeassistant.components.media_player import ( ATTR_INPUT_SOURCE, ATTR_MEDIA_ALBUM_ARTIST, ATTR_MEDIA_ALBUM_NAME, + ATTR_MEDIA_ANNOUNCE, ATTR_MEDIA_ARTIST, ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_DURATION, + ATTR_MEDIA_ENQUEUE, ATTR_MEDIA_POSITION, ATTR_MEDIA_SEEK_POSITION, ATTR_MEDIA_SHUFFLE, @@ -49,6 +51,7 @@ from homeassistant.components.media_player import ( SERVICE_TURN_ON, SERVICE_VOLUME_MUTE, SERVICE_VOLUME_SET, + MediaPlayerEnqueue, MediaType, ) from homeassistant.config_entries import SOURCE_USER @@ -112,6 +115,28 @@ SAMPLE_PLAYER_STOPPED = { "item_progress_ms": 5, } +SAMPLE_QUEUE = { + "version": 833, + "count": 1, + "items": [ + { + "id": 12322, + "position": 0, + "track_id": 1234, + "title": "Some song", + "artist": "Some artist", + "album": "No album", + "album_artist": "The xx", + "artwork_url": "http://art", + "length_ms": 0, + "track_number": 1, + "media_kind": "music", + "data_kind": "url", + "uri": "library:track:5", + } + ], +} + SAMPLE_QUEUE_TTS = { "version": 833, "count": 1, @@ -291,7 +316,7 @@ async def get_request_return_values_fixture(): "config": SAMPLE_CONFIG, "outputs": SAMPLE_OUTPUTS_ON, "player": SAMPLE_PLAYER_PAUSED, - "queue": SAMPLE_QUEUE_TTS, + "queue": SAMPLE_QUEUE, } @@ -323,13 +348,12 @@ async def mock_api_object_fixture(hass, config_entry, get_request_return_values) await hass.async_block_till_done() async def add_to_queue_side_effect( - uris, playback=None, playback_from_position=None, clear=None + uris, playback=None, position=None, playback_from_position=None, clear=None ): await updater_update(["queue", "player"]) - mock_api.return_value.add_to_queue.side_effect = ( - add_to_queue_side_effect # for play_media testing - ) + # for play_media testing + mock_api.return_value.add_to_queue.side_effect = add_to_queue_side_effect async def pause_side_effect(): await updater_update(["player"]) @@ -361,8 +385,8 @@ def test_master_state(hass, mock_api_object): assert state.attributes[ATTR_MEDIA_DURATION] == 0.05 assert state.attributes[ATTR_MEDIA_POSITION] == 0.005 assert state.attributes[ATTR_MEDIA_TITLE] == "No album" # reversed for url - assert state.attributes[ATTR_MEDIA_ARTIST] == "Google" - assert state.attributes[ATTR_MEDIA_ALBUM_NAME] == "Short TTS file" # reversed + assert state.attributes[ATTR_MEDIA_ARTIST] == "Some artist" + assert state.attributes[ATTR_MEDIA_ALBUM_NAME] == "Some song" # reversed assert state.attributes[ATTR_MEDIA_ALBUM_ARTIST] == "The xx" assert state.attributes[ATTR_MEDIA_TRACK] == 1 assert not state.attributes[ATTR_MEDIA_SHUFFLE] @@ -562,16 +586,18 @@ async def test_async_play_media_from_paused(hass, mock_api_object): assert state.last_updated > initial_state.last_updated -async def test_async_play_media_from_stopped( +async def test_async_play_media_announcement_from_stopped( hass, get_request_return_values, mock_api_object ): - """Test async play media from stopped.""" + """Test async play media announcement (from stopped).""" updater_update = mock_api_object.start_websocket_handler.call_args[0][2] get_request_return_values["player"] = SAMPLE_PLAYER_STOPPED await updater_update(["player"]) await hass.async_block_till_done() initial_state = hass.states.get(TEST_MASTER_ENTITY_NAME) + + get_request_return_values["queue"] = SAMPLE_QUEUE_TTS await _service_call( hass, TEST_MASTER_ENTITY_NAME, @@ -579,6 +605,7 @@ async def test_async_play_media_from_stopped( { ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC, ATTR_MEDIA_CONTENT_ID: "http://example.com/somefile.mp3", + ATTR_MEDIA_ANNOUNCE: True, }, ) state = hass.states.get(TEST_MASTER_ENTITY_NAME) @@ -602,8 +629,8 @@ async def test_async_play_media_unsupported(hass, mock_api_object): assert state.last_updated == initial_state.last_updated -async def test_async_play_media_tts_timeout(hass, mock_api_object): - """Test async play media with TTS timeout.""" +async def test_async_play_media_announcement_tts_timeout(hass, mock_api_object): + """Test async play media announcement with TTS timeout.""" mock_api_object.add_to_queue.side_effect = None with patch("homeassistant.components.forked_daapd.media_player.TTS_TIMEOUT", 0): initial_state = hass.states.get(TEST_MASTER_ENTITY_NAME) @@ -614,6 +641,7 @@ async def test_async_play_media_tts_timeout(hass, mock_api_object): { ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC, ATTR_MEDIA_CONTENT_ID: "http://example.com/somefile.mp3", + ATTR_MEDIA_ANNOUNCE: True, }, ) state = hass.states.get(TEST_MASTER_ENTITY_NAME) @@ -713,8 +741,8 @@ async def test_librespot_java_stuff( assert state.attributes[ATTR_MEDIA_ALBUM_NAME] == "some album" -async def test_librespot_java_play_media(hass, pipe_control_api_object): - """Test play media with librespot-java pipe.""" +async def test_librespot_java_play_announcement(hass, pipe_control_api_object): + """Test play announcement with librespot-java pipe.""" initial_state = hass.states.get(TEST_MASTER_ENTITY_NAME) await _service_call( hass, @@ -723,6 +751,7 @@ async def test_librespot_java_play_media(hass, pipe_control_api_object): { ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC, ATTR_MEDIA_CONTENT_ID: "http://example.com/somefile.mp3", + ATTR_MEDIA_ANNOUNCE: True, }, ) state = hass.states.get(TEST_MASTER_ENTITY_NAME) @@ -783,3 +812,79 @@ async def test_websocket_disconnect(hass, mock_api_object): await hass.async_block_till_done() assert hass.states.get(TEST_MASTER_ENTITY_NAME).state == STATE_UNAVAILABLE assert hass.states.get(TEST_ZONE_ENTITY_NAMES[0]).state == STATE_UNAVAILABLE + + +async def test_async_play_media_enqueue(hass, mock_api_object): + """Test async play media with different enqueue options.""" + initial_state = hass.states.get(TEST_MASTER_ENTITY_NAME) + await _service_call( + hass, + TEST_MASTER_ENTITY_NAME, + SERVICE_PLAY_MEDIA, + { + ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC, + ATTR_MEDIA_CONTENT_ID: "http://example.com/play.mp3", + ATTR_MEDIA_ENQUEUE: MediaPlayerEnqueue.PLAY, + }, + ) + state = hass.states.get(TEST_MASTER_ENTITY_NAME) + assert state.state == initial_state.state + assert state.last_updated > initial_state.last_updated + mock_api_object.add_to_queue.assert_called_with( + uris="http://example.com/play.mp3", + playback="start", + position=0, + playback_from_position=0, + ) + await _service_call( + hass, + TEST_MASTER_ENTITY_NAME, + SERVICE_PLAY_MEDIA, + { + ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC, + ATTR_MEDIA_CONTENT_ID: "http://example.com/replace.mp3", + ATTR_MEDIA_ENQUEUE: MediaPlayerEnqueue.REPLACE, + }, + ) + mock_api_object.add_to_queue.assert_called_with( + uris="http://example.com/replace.mp3", playback="start", clear=True + ) + await _service_call( + hass, + TEST_MASTER_ENTITY_NAME, + SERVICE_PLAY_MEDIA, + { + ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC, + ATTR_MEDIA_CONTENT_ID: "http://example.com/add.mp3", + ATTR_MEDIA_ENQUEUE: MediaPlayerEnqueue.ADD, + }, + ) + mock_api_object.add_to_queue.assert_called_with( + uris="http://example.com/add.mp3", playback="start", clear=False + ) + await _service_call( + hass, + TEST_MASTER_ENTITY_NAME, + SERVICE_PLAY_MEDIA, + { + ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC, + ATTR_MEDIA_CONTENT_ID: "http://example.com/add.mp3", + ATTR_MEDIA_ENQUEUE: True, + }, + ) + mock_api_object.add_to_queue.assert_called_with( + uris="http://example.com/add.mp3", playback="start", clear=False + ) + await _service_call( + hass, + TEST_MASTER_ENTITY_NAME, + SERVICE_PLAY_MEDIA, + { + ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC, + ATTR_MEDIA_CONTENT_ID: "http://example.com/next.mp3", + ATTR_MEDIA_ENQUEUE: MediaPlayerEnqueue.NEXT, + }, + ) + mock_api_object.add_to_queue.assert_called_with( + uris="http://example.com/next.mp3", playback="start", position=1 + ) From 488b04fc8bc0c11bea4bcbbab419513a5d3e76a4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 21 Sep 2022 11:03:50 -1000 Subject: [PATCH 632/955] Handle default RSSI values from bleak in bluetooth (#78908) --- homeassistant/components/bluetooth/manager.py | 3 +- tests/components/bluetooth/test_manager.py | 56 +++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index 0dcf11fd1e2..06eb71b5a71 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -65,6 +65,7 @@ APPLE_START_BYTES_WANTED: Final = { } RSSI_SWITCH_THRESHOLD = 6 +NO_RSSI_VALUE = -1000 _LOGGER = logging.getLogger(__name__) @@ -88,7 +89,7 @@ def _prefer_previous_adv( STALE_ADVERTISEMENT_SECONDS, ) return False - if new.device.rssi - RSSI_SWITCH_THRESHOLD > old.device.rssi: + if new.device.rssi - RSSI_SWITCH_THRESHOLD > (old.device.rssi or NO_RSSI_VALUE): # If new advertisement is RSSI_SWITCH_THRESHOLD more, the new one is preferred if new.source != old.source: _LOGGER.debug( diff --git a/tests/components/bluetooth/test_manager.py b/tests/components/bluetooth/test_manager.py index c05b9424508..f3f3d1b3664 100644 --- a/tests/components/bluetooth/test_manager.py +++ b/tests/components/bluetooth/test_manager.py @@ -119,6 +119,62 @@ async def test_switching_adapters_based_on_rssi(hass, enable_bluetooth): ) +async def test_switching_adapters_based_on_zero_rssi(hass, enable_bluetooth): + """Test switching adapters based on zero rssi.""" + + address = "44:44:33:11:23:45" + + switchbot_device_no_rssi = BLEDevice(address, "wohand_poor_signal", rssi=0) + switchbot_adv_no_rssi = AdvertisementData( + local_name="wohand_no_rssi", service_uuids=[] + ) + inject_advertisement_with_source( + hass, switchbot_device_no_rssi, switchbot_adv_no_rssi, "hci0" + ) + + assert ( + bluetooth.async_ble_device_from_address(hass, address) + is switchbot_device_no_rssi + ) + + switchbot_device_good_signal = BLEDevice(address, "wohand_good_signal", rssi=-60) + switchbot_adv_good_signal = AdvertisementData( + local_name="wohand_good_signal", service_uuids=[] + ) + inject_advertisement_with_source( + hass, switchbot_device_good_signal, switchbot_adv_good_signal, "hci1" + ) + + assert ( + bluetooth.async_ble_device_from_address(hass, address) + is switchbot_device_good_signal + ) + + inject_advertisement_with_source( + hass, switchbot_device_good_signal, switchbot_adv_no_rssi, "hci0" + ) + assert ( + bluetooth.async_ble_device_from_address(hass, address) + is switchbot_device_good_signal + ) + + # We should not switch adapters unless the signal hits the threshold + switchbot_device_similar_signal = BLEDevice( + address, "wohand_similar_signal", rssi=-62 + ) + switchbot_adv_similar_signal = AdvertisementData( + local_name="wohand_similar_signal", service_uuids=[] + ) + + inject_advertisement_with_source( + hass, switchbot_device_similar_signal, switchbot_adv_similar_signal, "hci0" + ) + assert ( + bluetooth.async_ble_device_from_address(hass, address) + is switchbot_device_good_signal + ) + + async def test_switching_adapters_based_on_stale(hass, enable_bluetooth): """Test switching adapters based on the previous advertisement being stale.""" From bbb5d6772c633dc8f2bbeeee6c31e9f2972ac237 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 22 Sep 2022 00:27:20 +0000 Subject: [PATCH 633/955] [ci skip] Translation update --- .../components/kegtron/translations/ca.json | 22 ++++++++++ .../kegtron/translations/pt-BR.json | 22 ++++++++++ .../components/lidarr/translations/el.json | 42 +++++++++++++++++++ .../lidarr/translations/zh-Hant.json | 42 +++++++++++++++++++ 4 files changed, 128 insertions(+) create mode 100644 homeassistant/components/kegtron/translations/ca.json create mode 100644 homeassistant/components/kegtron/translations/pt-BR.json create mode 100644 homeassistant/components/lidarr/translations/el.json create mode 100644 homeassistant/components/lidarr/translations/zh-Hant.json diff --git a/homeassistant/components/kegtron/translations/ca.json b/homeassistant/components/kegtron/translations/ca.json new file mode 100644 index 00000000000..c121ff7408c --- /dev/null +++ b/homeassistant/components/kegtron/translations/ca.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", + "no_devices_found": "No s'han trobat dispositius a la xarxa", + "not_supported": "Dispositiu no compatible" + }, + "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/kegtron/translations/pt-BR.json b/homeassistant/components/kegtron/translations/pt-BR.json new file mode 100644 index 00000000000..0da7639fa2a --- /dev/null +++ b/homeassistant/components/kegtron/translations/pt-BR.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "A configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "no_devices_found": "Nenhum dispositivo encontrado na rede", + "not_supported": "Dispositivo n\u00e3o suportado" + }, + "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/lidarr/translations/el.json b/homeassistant/components/lidarr/translations/el.json new file mode 100644 index 00000000000..01a54904034 --- /dev/null +++ b/homeassistant/components/lidarr/translations/el.json @@ -0,0 +1,42 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af", + "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2" + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1", + "wrong_app": "\u0395\u03c0\u03af\u03c4\u03b5\u03c5\u03be\u03b7 \u03bb\u03b1\u03bd\u03b8\u03b1\u03c3\u03bc\u03ad\u03bd\u03b7\u03c2 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2. \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03ce \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac", + "zeroconf_failed": "\u03a4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API \u03b4\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b5. \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03ce \u03b5\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03c7\u03b5\u03b9\u03c1\u03bf\u03ba\u03af\u03bd\u03b7\u03c4\u03b1" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API" + }, + "description": "\u0397 \u03b5\u03bd\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03c4\u03bf\u03c5 Lidarr \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03c5\u03c4\u03b5\u03af \u03be\u03b1\u03bd\u03ac \u03bc\u03b5 \u03bc\u03b7 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03bf \u03c4\u03c1\u03cc\u03c0\u03bf \u03bc\u03b5 \u03c4\u03bf Lidarr API", + "title": "\u0395\u03c0\u03b1\u03bd\u03b1\u03bb\u03b7\u03c0\u03c4\u03b9\u03ba\u03cc\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2" + }, + "user": { + "data": { + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", + "url": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL", + "verify_ssl": "\u0395\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03cd\u03c3\u03c4\u03b5 \u03c4\u03bf \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03c4\u03b9\u03ba\u03cc SSL" + }, + "description": "\u03a4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03b1\u03bd\u03b1\u03ba\u03c4\u03b7\u03b8\u03b5\u03af \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1 \u03b5\u03ac\u03bd \u03c4\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03b4\u03b5\u03bd \u03ad\u03c7\u03bf\u03c5\u03bd \u03bf\u03c1\u03b9\u03c3\u03c4\u03b5\u03af \u03c3\u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae.\n\u03a4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03b2\u03c1\u03b5\u03b8\u03b5\u03af \u03c3\u03c4\u03b9\u03c2 \u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 > \u0393\u03b5\u03bd\u03b9\u03ba\u03ac \u03c3\u03c4\u03bf Lidarr Web UI." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "max_records": "\u0391\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03bc\u03ad\u03b3\u03b9\u03c3\u03c4\u03c9\u03bd \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03ce\u03bd \u03b3\u03b9\u03b1 \u03b5\u03bc\u03c6\u03ac\u03bd\u03b9\u03c3\u03b7 \u03c3\u03c4\u03b7\u03bd \u03b5\u03c0\u03b9\u03b8\u03c5\u03bc\u03b7\u03c4\u03ae \u03ba\u03b1\u03b9 \u03c3\u03c4\u03b7\u03bd \u03bf\u03c5\u03c1\u03ac", + "upcoming_days": "\u0391\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03b5\u03c0\u03b5\u03c1\u03c7\u03cc\u03bc\u03b5\u03bd\u03c9\u03bd \u03b7\u03bc\u03b5\u03c1\u03ce\u03bd \u03c0\u03bf\u03c5 \u03b8\u03b1 \u03b5\u03bc\u03c6\u03b1\u03bd\u03af\u03b6\u03bf\u03bd\u03c4\u03b1\u03b9 \u03c3\u03c4\u03bf \u03b7\u03bc\u03b5\u03c1\u03bf\u03bb\u03cc\u03b3\u03b9\u03bf" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lidarr/translations/zh-Hant.json b/homeassistant/components/lidarr/translations/zh-Hant.json new file mode 100644 index 00000000000..d4d5b860b20 --- /dev/null +++ b/homeassistant/components/lidarr/translations/zh-Hant.json @@ -0,0 +1,42 @@ +{ + "config": { + "abort": { + "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4", + "wrong_app": "\u5b58\u53d6\u61c9\u7528\u7a0b\u5f0f\u4e0d\u6b63\u78ba\uff0c\u8acb\u518d\u8a66\u4e00\u6b21", + "zeroconf_failed": "\u627e\u4e0d\u5230 API \u91d1\u9470\u3001\u8acb\u624b\u52d5\u8f38\u5165\u3002" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API \u91d1\u9470" + }, + "description": "Lidarr \u6574\u5408\u9700\u8981\u624b\u52d5\u91cd\u65b0\u8a8d\u8b49 Lidarr API", + "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" + }, + "user": { + "data": { + "api_key": "API \u91d1\u9470", + "url": "\u7db2\u5740", + "verify_ssl": "\u78ba\u8a8d SSL \u8a8d\u8b49" + }, + "description": "\u5047\u5982\u6c92\u6709\u65bc\u61c9\u7528\u7a0b\u5f0f\u4e2d\u8a2d\u5b9a\u767b\u5165\u6191\u8b49\uff0c\u5247\u53ef\u4ee5\u81ea\u52d5\u53d6\u5f97 API \u91d1\u9470\u3002\n\u91d1\u9470\u53ef\u4ee5\u65bc Lidarr Web \u4ecb\u9762\u4e2d\u8a2d\u5b9a\uff08Settings\uff09 > \u4e00\u822c\uff08General\uff09\u4e2d\u53d6\u5f97\u3002" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "max_records": "\u986f\u793a\u60f3\u8981\u8207\u6392\u968a\u6700\u9ad8\u7d00\u9304\u6578\u76ee", + "upcoming_days": "\u5373\u5c07\u5230\u4f86\u884c\u4e8b\u66c6\u986f\u793a\u5929\u6578" + } + } + } + } +} \ No newline at end of file From 0e0318dc53120f2f0fc00f32a05e62132bd8bc69 Mon Sep 17 00:00:00 2001 From: spycle <48740594+spycle@users.noreply.github.com> Date: Thu, 22 Sep 2022 02:44:37 +0100 Subject: [PATCH 634/955] Add Keymitt BLE integration (#76575) Co-authored-by: J. Nick Koston --- .coveragerc | 5 + CODEOWNERS | 2 + .../components/keymitt_ble/__init__.py | 50 +++++ .../components/keymitt_ble/config_flow.py | 157 ++++++++++++++++ homeassistant/components/keymitt_ble/const.py | 4 + .../components/keymitt_ble/coordinator.py | 56 ++++++ .../components/keymitt_ble/entity.py | 39 ++++ .../components/keymitt_ble/manifest.json | 22 +++ .../components/keymitt_ble/services.yaml | 46 +++++ .../components/keymitt_ble/strings.json | 27 +++ .../components/keymitt_ble/switch.py | 70 +++++++ .../keymitt_ble/translations/en.json | 27 +++ homeassistant/generated/bluetooth.py | 12 ++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/keymitt_ble/__init__.py | 83 +++++++++ tests/components/keymitt_ble/conftest.py | 8 + .../keymitt_ble/test_config_flow.py | 173 ++++++++++++++++++ 19 files changed, 788 insertions(+) create mode 100644 homeassistant/components/keymitt_ble/__init__.py create mode 100644 homeassistant/components/keymitt_ble/config_flow.py create mode 100644 homeassistant/components/keymitt_ble/const.py create mode 100644 homeassistant/components/keymitt_ble/coordinator.py create mode 100644 homeassistant/components/keymitt_ble/entity.py create mode 100644 homeassistant/components/keymitt_ble/manifest.json create mode 100644 homeassistant/components/keymitt_ble/services.yaml create mode 100644 homeassistant/components/keymitt_ble/strings.json create mode 100644 homeassistant/components/keymitt_ble/switch.py create mode 100644 homeassistant/components/keymitt_ble/translations/en.json create mode 100644 tests/components/keymitt_ble/__init__.py create mode 100644 tests/components/keymitt_ble/conftest.py create mode 100644 tests/components/keymitt_ble/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 352abd7bb7a..9aafc4300cb 100644 --- a/.coveragerc +++ b/.coveragerc @@ -626,6 +626,11 @@ omit = homeassistant/components/kef/* homeassistant/components/keyboard/* homeassistant/components/keyboard_remote/* + homeassistant/components/keymitt_ble/__init__.py + homeassistant/components/keymitt_ble/const.py + homeassistant/components/keymitt_ble/entity.py + homeassistant/components/keymitt_ble/switch.py + homeassistant/components/keymitt_ble/coordinator.py homeassistant/components/kira/* homeassistant/components/kiwi/lock.py homeassistant/components/kodi/__init__.py diff --git a/CODEOWNERS b/CODEOWNERS index 156a66b174f..1e3a06f16c7 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -580,6 +580,8 @@ build.json @home-assistant/supervisor /homeassistant/components/kegtron/ @Ernst79 /tests/components/kegtron/ @Ernst79 /homeassistant/components/keyboard_remote/ @bendavid @lanrat +/homeassistant/components/keymitt_ble/ @spycle +/tests/components/keymitt_ble/ @spycle /homeassistant/components/kmtronic/ @dgomes /tests/components/kmtronic/ @dgomes /homeassistant/components/knx/ @Julius2342 @farmio @marvin-w diff --git a/homeassistant/components/keymitt_ble/__init__.py b/homeassistant/components/keymitt_ble/__init__.py new file mode 100644 index 00000000000..1a7df4fe0a9 --- /dev/null +++ b/homeassistant/components/keymitt_ble/__init__.py @@ -0,0 +1,50 @@ +"""Integration to integrate Keymitt BLE devices with Home Assistant.""" +from __future__ import annotations + +import logging + +from microbot import MicroBotApiClient + +from homeassistant.components import bluetooth +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_ADDRESS, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady + +from .const import DOMAIN +from .coordinator import MicroBotDataUpdateCoordinator + +_LOGGER: logging.Logger = logging.getLogger(__package__) +PLATFORMS: list[str] = [Platform.SWITCH] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up this integration using UI.""" + hass.data.setdefault(DOMAIN, {}) + token: str = entry.data[CONF_ACCESS_TOKEN] + bdaddr: str = entry.data[CONF_ADDRESS] + ble_device = bluetooth.async_ble_device_from_address(hass, bdaddr) + if not ble_device: + raise ConfigEntryNotReady(f"Could not find MicroBot with address {bdaddr}") + client = MicroBotApiClient( + device=ble_device, + token=token, + ) + coordinator = MicroBotDataUpdateCoordinator( + hass, client=client, ble_device=ble_device + ) + + hass.data[DOMAIN][entry.entry_id] = coordinator + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + entry.async_on_unload(coordinator.async_start()) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Handle removal of an 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/keymitt_ble/config_flow.py b/homeassistant/components/keymitt_ble/config_flow.py new file mode 100644 index 00000000000..8a8a954abd6 --- /dev/null +++ b/homeassistant/components/keymitt_ble/config_flow.py @@ -0,0 +1,157 @@ +"""Adds config flow for MicroBot.""" +from __future__ import annotations + +import logging +from typing import Any + +from bleak.backends.device import BLEDevice +from microbot import ( + MicroBotAdvertisement, + MicroBotApiClient, + parse_advertisement_data, + randomid, +) +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_ACCESS_TOKEN, CONF_ADDRESS +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN + +_LOGGER: logging.Logger = logging.getLogger(__package__) + + +def short_address(address: str) -> str: + """Convert a Bluetooth address to a short address.""" + results = address.replace("-", ":").split(":") + return f"{results[0].upper()}{results[1].upper()}"[0:4] + + +def name_from_discovery(discovery: MicroBotAdvertisement) -> str: + """Get the name from a discovery.""" + return f'{discovery.data["local_name"]} {short_address(discovery.address)}' + + +class MicroBotConfigFlow(ConfigFlow, domain=DOMAIN): + """Config flow for MicroBot.""" + + VERSION = 1 + + def __init__(self): + """Initialize.""" + self._errors = {} + self._discovered_adv: MicroBotAdvertisement | None = None + self._discovered_advs: dict[str, MicroBotAdvertisement] = {} + self._client: MicroBotApiClient | None = None + self._ble_device: BLEDevice | None = None + self._name: str | None = None + self._bdaddr: str | None = None + + async def async_step_bluetooth( + self, discovery_info: BluetoothServiceInfoBleak + ) -> FlowResult: + """Handle the bluetooth discovery step.""" + _LOGGER.debug("Discovered bluetooth device: %s", discovery_info) + await self.async_set_unique_id(discovery_info.address) + self._abort_if_unique_id_configured() + self._ble_device = discovery_info.device + parsed = parse_advertisement_data( + discovery_info.device, discovery_info.advertisement + ) + self._discovered_adv = parsed + self.context["title_placeholders"] = { + "name": name_from_discovery(self._discovered_adv), + } + return await self.async_step_init() + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initialized by the user.""" + # This is for backwards compatibility. + return await self.async_step_init(user_input) + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Check if paired.""" + errors: dict[str, str] = {} + + if discovery := self._discovered_adv: + self._discovered_advs[discovery.address] = discovery + else: + current_addresses = self._async_current_ids() + for discovery_info in async_discovered_service_info(self.hass): + self._ble_device = discovery_info.device + address = discovery_info.address + if address in current_addresses or address in self._discovered_advs: + continue + parsed = parse_advertisement_data( + discovery_info.device, discovery_info.advertisement + ) + if parsed: + self._discovered_adv = parsed + self._discovered_advs[address] = parsed + + if not self._discovered_advs: + return self.async_abort(reason="no_unconfigured_devices") + + if user_input is not None: + self._name = name_from_discovery(self._discovered_adv) + self._bdaddr = user_input[CONF_ADDRESS] + await self.async_set_unique_id(self._bdaddr, raise_on_progress=False) + self._abort_if_unique_id_configured() + return await self.async_step_link() + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Required(CONF_ADDRESS): vol.In( + { + address: f"{parsed.data['local_name']} ({address})" + for address, parsed in self._discovered_advs.items() + } + ) + } + ), + errors=errors, + ) + + async def async_step_link( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Given a configured host, will ask the user to press the button to pair.""" + errors: dict[str, str] = {} + token = randomid(32) + self._client = MicroBotApiClient( + device=self._ble_device, + token=token, + ) + assert self._client is not None + if user_input is None: + await self._client.connect(init=True) + return self.async_show_form(step_id="link") + + if not self._client.is_connected(): + errors["base"] = "linking" + else: + await self._client.disconnect() + + if errors: + return self.async_show_form(step_id="link", errors=errors) + + assert self._name is not None + return self.async_create_entry( + title=self._name, + data=user_input + | { + CONF_ADDRESS: self._bdaddr, + CONF_ACCESS_TOKEN: token, + }, + ) diff --git a/homeassistant/components/keymitt_ble/const.py b/homeassistant/components/keymitt_ble/const.py new file mode 100644 index 00000000000..a10e7124226 --- /dev/null +++ b/homeassistant/components/keymitt_ble/const.py @@ -0,0 +1,4 @@ +"""Constants for Keymitt BLE.""" +# Base component constants +DOMAIN = "keymitt_ble" +MANUFACTURER = "Naran/Keymitt" diff --git a/homeassistant/components/keymitt_ble/coordinator.py b/homeassistant/components/keymitt_ble/coordinator.py new file mode 100644 index 00000000000..e3a995e3813 --- /dev/null +++ b/homeassistant/components/keymitt_ble/coordinator.py @@ -0,0 +1,56 @@ +"""Integration to integrate Keymitt BLE devices with Home Assistant.""" +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING, Any + +from microbot import MicroBotApiClient, parse_advertisement_data + +from homeassistant.components import bluetooth +from homeassistant.components.bluetooth.passive_update_coordinator import ( + PassiveBluetoothDataUpdateCoordinator, +) +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant, callback + +if TYPE_CHECKING: + from bleak.backends.device import BLEDevice + +_LOGGER: logging.Logger = logging.getLogger(__package__) +PLATFORMS: list[str] = [Platform.SWITCH] + + +class MicroBotDataUpdateCoordinator(PassiveBluetoothDataUpdateCoordinator): + """Class to manage fetching data from the MicroBot.""" + + def __init__( + self, + hass: HomeAssistant, + client: MicroBotApiClient, + ble_device: BLEDevice, + ) -> None: + """Initialize.""" + self.api: MicroBotApiClient = client + self.data: dict[str, Any] = {} + self.ble_device = ble_device + super().__init__( + hass, + _LOGGER, + ble_device.address, + bluetooth.BluetoothScanningMode.ACTIVE, + ) + + @callback + def _async_handle_bluetooth_event( + self, + service_info: bluetooth.BluetoothServiceInfoBleak, + change: bluetooth.BluetoothChange, + ) -> None: + """Handle a Bluetooth event.""" + if adv := parse_advertisement_data( + service_info.device, service_info.advertisement + ): + self.data = adv.data + _LOGGER.debug("%s: MicroBot data: %s", self.ble_device.address, self.data) + self.api.update_from_advertisement(adv) + super()._async_handle_bluetooth_event(service_info, change) diff --git a/homeassistant/components/keymitt_ble/entity.py b/homeassistant/components/keymitt_ble/entity.py new file mode 100644 index 00000000000..dcda4a94027 --- /dev/null +++ b/homeassistant/components/keymitt_ble/entity.py @@ -0,0 +1,39 @@ +"""MicroBot class.""" +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from homeassistant.components.bluetooth.passive_update_coordinator import ( + PassiveBluetoothCoordinatorEntity, +) +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.entity import DeviceInfo + +from .const import MANUFACTURER + +if TYPE_CHECKING: + from . import MicroBotDataUpdateCoordinator + + +class MicroBotEntity(PassiveBluetoothCoordinatorEntity): + """Generic entity for all MicroBots.""" + + coordinator: MicroBotDataUpdateCoordinator + + def __init__(self, coordinator, config_entry): + """Initialise the entity.""" + super().__init__(coordinator) + self._address = self.coordinator.ble_device.address + self._attr_name = "Push" + self._attr_unique_id = self._address + self._attr_device_info = DeviceInfo( + connections={(dr.CONNECTION_BLUETOOTH, self._address)}, + manufacturer=MANUFACTURER, + model="Push", + name="MicroBot", + ) + + @property + def data(self) -> dict[str, Any]: + """Return coordinator data for this entity.""" + return self.coordinator.data diff --git a/homeassistant/components/keymitt_ble/manifest.json b/homeassistant/components/keymitt_ble/manifest.json new file mode 100644 index 00000000000..445a2581bda --- /dev/null +++ b/homeassistant/components/keymitt_ble/manifest.json @@ -0,0 +1,22 @@ +{ + "domain": "keymitt_ble", + "name": "Keymitt MicroBot Push", + "documentation": "https://www.home-assistant.io/integrations/keymitt_ble", + "config_flow": true, + "bluetooth": [ + { + "service_uuid": "00001831-0000-1000-8000-00805f9b34fb" + }, + { + "service_data_uuid": "00001831-0000-1000-8000-00805f9b34fb" + }, + { + "local_name": "mib*" + } + ], + "codeowners": ["@spycle"], + "requirements": ["PyMicroBot==0.0.6"], + "iot_class": "assumed_state", + "dependencies": ["bluetooth"], + "loggers": ["keymitt_ble"] +} diff --git a/homeassistant/components/keymitt_ble/services.yaml b/homeassistant/components/keymitt_ble/services.yaml new file mode 100644 index 00000000000..c611577eb26 --- /dev/null +++ b/homeassistant/components/keymitt_ble/services.yaml @@ -0,0 +1,46 @@ +calibrate: + name: Calibrate + description: Calibration - Set depth, press & hold duration, and operation mode. Warning - this will send a push command to the device + fields: + entity_id: + name: Entity + description: Name of entity to calibrate + selector: + entity: + integration: keymitt_ble + domain: switch + depth: + name: Depth + description: Depth in percent + example: 50 + required: true + selector: + number: + mode: slider + step: 1 + min: 0 + max: 100 + unit_of_measurement: "%" + duration: + name: Duration + description: Duration in seconds + example: 1 + required: true + selector: + number: + mode: box + step: 1 + min: 0 + max: 60 + unit_of_measurement: seconds + mode: + name: Mode + description: normal | invert | toggle + example: "normal" + required: true + selector: + select: + options: + - "normal" + - "invert" + - "toggle" diff --git a/homeassistant/components/keymitt_ble/strings.json b/homeassistant/components/keymitt_ble/strings.json new file mode 100644 index 00000000000..3914a2f9a30 --- /dev/null +++ b/homeassistant/components/keymitt_ble/strings.json @@ -0,0 +1,27 @@ +{ + "config": { + "flow_title": "{name}", + "step": { + "init": { + "title": "Setup MicroBot device", + "data": { + "address": "Device address", + "name": "Name" + } + }, + "link": { + "title": "Pairing", + "description": "Press the button on the MicroBot Push when the LED is solid pink or green to register with Home Assistant." + } + }, + "error": { + "linking": "Failed to pair, please try again. Is the MicroBot in pairing mode?" + }, + "abort": { + "already_configured_device": "[%key:common::config_flow::abort::already_configured_device%]", + "no_unconfigured_devices": "No unconfigured devices found.", + "unknown": "[%key:common::config_flow::error::unknown%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + } + } +} diff --git a/homeassistant/components/keymitt_ble/switch.py b/homeassistant/components/keymitt_ble/switch.py new file mode 100644 index 00000000000..92decea53ca --- /dev/null +++ b/homeassistant/components/keymitt_ble/switch.py @@ -0,0 +1,70 @@ +"""Switch platform for MicroBot.""" +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +import voluptuous as vol + +from homeassistant.components.switch import SwitchEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_validation as cv, entity_platform + +from .const import DOMAIN +from .entity import MicroBotEntity + +if TYPE_CHECKING: + from . import MicroBotDataUpdateCoordinator + +CALIBRATE = "calibrate" +CALIBRATE_SCHEMA = { + vol.Required("depth"): cv.positive_int, + vol.Required("duration"): cv.positive_int, + vol.Required("mode"): vol.In(["normal", "invert", "toggle"]), +} + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: entity_platform.AddEntitiesCallback, +) -> None: + """Set up MicroBot based on a config entry.""" + coordinator: MicroBotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities([MicroBotBinarySwitch(coordinator, entry)]) + platform = entity_platform.async_get_current_platform() + platform.async_register_entity_service( + CALIBRATE, + CALIBRATE_SCHEMA, + "async_calibrate", + ) + + +class MicroBotBinarySwitch(MicroBotEntity, SwitchEntity): + """MicroBot switch class.""" + + _attr_has_entity_name = True + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn on the switch.""" + await self.coordinator.api.push_on() + self.async_write_ha_state() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off the switch.""" + await self.coordinator.api.push_off() + self.async_write_ha_state() + + @property + def is_on(self) -> bool: + """Return true if the switch is on.""" + return self.coordinator.api.is_on + + async def async_calibrate( + self, + depth: int, + duration: int, + mode: str, + ) -> None: + """Send calibration commands to the switch.""" + await self.coordinator.api.calibrate(depth, duration, mode) diff --git a/homeassistant/components/keymitt_ble/translations/en.json b/homeassistant/components/keymitt_ble/translations/en.json new file mode 100644 index 00000000000..ca5fa547770 --- /dev/null +++ b/homeassistant/components/keymitt_ble/translations/en.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured_device": "Device is already configured", + "cannot_connect": "Failed to connect", + "no_unconfigured_devices": "No unconfigured devices found.", + "unknown": "Unexpected error" + }, + "error": { + "linking": "Failed to pair, please try again. Is the MicroBot in pairing mode?" + }, + "flow_title": "{name}", + "step": { + "init": { + "data": { + "address": "Device address", + "name": "Name" + }, + "title": "Setup MicroBot device" + }, + "link": { + "description": "Press the button on the MicroBot Push when the LED is solid pink or green to register with Home Assistant.", + "title": "Pairing" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 3ba5883a70c..861960b1afd 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -153,6 +153,18 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ "connectable": False, "manufacturer_id": 65535, }, + { + "domain": "keymitt_ble", + "service_uuid": "00001831-0000-1000-8000-00805f9b34fb", + }, + { + "domain": "keymitt_ble", + "service_data_uuid": "00001831-0000-1000-8000-00805f9b34fb", + }, + { + "domain": "keymitt_ble", + "local_name": "mib*", + }, { "domain": "led_ble", "local_name": "LEDnet*", diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 993503c678a..a66e426c883 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -190,6 +190,7 @@ FLOWS = { "kaleidescape", "keenetic_ndms2", "kegtron", + "keymitt_ble", "kmtronic", "knx", "kodi", diff --git a/requirements_all.txt b/requirements_all.txt index f9d73a9922b..204540ebdff 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -22,6 +22,9 @@ PyFlick==0.0.2 # homeassistant.components.mvglive PyMVGLive==1.1.4 +# homeassistant.components.keymitt_ble +PyMicroBot==0.0.6 + # homeassistant.components.mobile_app # homeassistant.components.owntracks PyNaCl==1.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a45ada504ff..ef4f9f4e38f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -18,6 +18,9 @@ HAP-python==4.5.0 # homeassistant.components.flick_electric PyFlick==0.0.2 +# homeassistant.components.keymitt_ble +PyMicroBot==0.0.6 + # homeassistant.components.mobile_app # homeassistant.components.owntracks PyNaCl==1.5.0 diff --git a/tests/components/keymitt_ble/__init__.py b/tests/components/keymitt_ble/__init__.py new file mode 100644 index 00000000000..0b145970643 --- /dev/null +++ b/tests/components/keymitt_ble/__init__.py @@ -0,0 +1,83 @@ +"""Tests for the MicroBot integration.""" +from unittest.mock import patch + +from bleak.backends.device import BLEDevice +from bleak.backends.scanner import AdvertisementData + +from homeassistant.components.bluetooth import BluetoothServiceInfoBleak +from homeassistant.const import CONF_ADDRESS + +DOMAIN = "keymitt_ble" + +ENTRY_CONFIG = { + CONF_ADDRESS: "e7:89:43:99:99:99", +} + +USER_INPUT = { + CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", +} + +USER_INPUT_INVALID = { + CONF_ADDRESS: "invalid-mac", +} + + +def patch_async_setup_entry(return_value=True): + """Patch async setup entry to return True.""" + return patch( + "homeassistant.components.keymitt_ble.async_setup_entry", + return_value=return_value, + ) + + +SERVICE_INFO = BluetoothServiceInfoBleak( + name="mibp", + service_uuids=["00001831-0000-1000-8000-00805f9b34fb"], + address="aa:bb:cc:dd:ee:ff", + manufacturer_data={}, + service_data={}, + rssi=-60, + source="local", + advertisement=AdvertisementData( + local_name="mibp", + manufacturer_data={}, + service_uuids=["00001831-0000-1000-8000-00805f9b34fb"], + ), + device=BLEDevice("aa:bb:cc:dd:ee:ff", "mibp"), + time=0, + connectable=True, +) + + +class MockMicroBotApiClient: + """Mock MicroBotApiClient.""" + + def __init__(self, device, token): + """Mock init.""" + + async def connect(self, init): + """Mock connect.""" + + async def disconnect(self): + """Mock disconnect.""" + + def is_connected(self): + """Mock connected.""" + return True + + +class MockMicroBotApiClientFail: + """Mock MicroBotApiClient.""" + + def __init__(self, device, token): + """Mock init.""" + + async def connect(self, init): + """Mock connect.""" + + async def disconnect(self): + """Mock disconnect.""" + + def is_connected(self): + """Mock disconnected.""" + return False diff --git a/tests/components/keymitt_ble/conftest.py b/tests/components/keymitt_ble/conftest.py new file mode 100644 index 00000000000..3df082c4361 --- /dev/null +++ b/tests/components/keymitt_ble/conftest.py @@ -0,0 +1,8 @@ +"""Define fixtures available for all tests.""" + +import pytest + + +@pytest.fixture(autouse=True) +def mock_bluetooth(enable_bluetooth): + """Auto mock bluetooth.""" diff --git a/tests/components/keymitt_ble/test_config_flow.py b/tests/components/keymitt_ble/test_config_flow.py new file mode 100644 index 00000000000..81e6a0be8e7 --- /dev/null +++ b/tests/components/keymitt_ble/test_config_flow.py @@ -0,0 +1,173 @@ +"""Test the MicroBot config flow.""" + +from unittest.mock import ANY, patch + +from homeassistant.config_entries import SOURCE_BLUETOOTH, SOURCE_USER +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_ADDRESS +from homeassistant.data_entry_flow import FlowResultType + +from . import ( + SERVICE_INFO, + USER_INPUT, + MockMicroBotApiClient, + MockMicroBotApiClientFail, + patch_async_setup_entry, +) + +from tests.common import MockConfigEntry + +DOMAIN = "keymitt_ble" + + +async def test_bluetooth_discovery(hass): + """Test discovery via bluetooth with a valid device.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_BLUETOOTH}, + data=SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "init" + + with patch_async_setup_entry() as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + USER_INPUT, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.FORM + + assert len(mock_setup_entry.mock_calls) == 0 + + +async def test_bluetooth_discovery_already_setup(hass): + """Test discovery via bluetooth with a valid device when already setup.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", + }, + unique_id="aa:bb:cc:dd:ee:ff", + ) + entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_BLUETOOTH}, + data=SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_user_setup(hass): + """Test the user initiated form with valid mac.""" + + with patch( + "homeassistant.components.keymitt_ble.config_flow.async_discovered_service_info", + return_value=[SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "init" + assert result["errors"] == {} + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + USER_INPUT, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "link" + assert result2["errors"] is None + + with patch( + "homeassistant.components.keymitt_ble.config_flow.MicroBotApiClient", + MockMicroBotApiClient, + ), patch_async_setup_entry() as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + USER_INPUT, + ) + await hass.async_block_till_done() + + assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["result"].data == { + CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", + CONF_ACCESS_TOKEN: ANY, + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_user_setup_already_configured(hass): + """Test the user initiated form with valid mac.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", + }, + unique_id="aa:bb:cc:dd:ee:ff", + ) + entry.add_to_hass(hass) + with patch( + "homeassistant.components.keymitt_ble.config_flow.async_discovered_service_info", + return_value=[SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_unconfigured_devices" + + +async def test_user_no_devices(hass): + """Test the user initiated form with valid mac.""" + with patch( + "homeassistant.components.keymitt_ble.config_flow.async_discovered_service_info", + return_value=[], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_unconfigured_devices" + + +async def test_no_link(hass): + """Test the user initiated form with invalid response.""" + + with patch( + "homeassistant.components.keymitt_ble.config_flow.async_discovered_service_info", + return_value=[SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "init" + assert result["errors"] == {} + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + USER_INPUT, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "link" + with patch( + "homeassistant.components.keymitt_ble.config_flow.MicroBotApiClient", + MockMicroBotApiClientFail, + ), patch_async_setup_entry() as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + USER_INPUT, + ) + await hass.async_block_till_done() + + assert result3["type"] == FlowResultType.FORM + assert result3["step_id"] == "link" + assert result3["errors"] == {"base": "linking"} + + assert len(mock_setup_entry.mock_calls) == 0 From 56e5774e264d4bed79e2b12b58811294c8501faf Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 21 Sep 2022 22:31:14 -0400 Subject: [PATCH 635/955] Disable force update Netatmo (#78913) --- homeassistant/components/netatmo/data_handler.py | 4 ++-- tests/components/netatmo/test_camera.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/netatmo/data_handler.py b/homeassistant/components/netatmo/data_handler.py index 3a1ea73e311..50a3bed17ff 100644 --- a/homeassistant/components/netatmo/data_handler.py +++ b/homeassistant/components/netatmo/data_handler.py @@ -138,8 +138,8 @@ class NetatmoDataHandler: @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.""" diff --git a/tests/components/netatmo/test_camera.py b/tests/components/netatmo/test_camera.py index 0e10ce92288..5b01668925f 100644 --- a/tests/components/netatmo/test_camera.py +++ b/tests/components/netatmo/test_camera.py @@ -328,6 +328,7 @@ async def test_service_set_camera_light(hass, config_entry, netatmo_auth): ) +@pytest.mark.skip async def test_camera_reconnect_webhook(hass, config_entry): """Test webhook event on camera reconnect.""" fake_post_hits = 0 From d034fd2629deb6fd8e8f773889efb18afaec7e40 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Wed, 21 Sep 2022 21:02:40 -0700 Subject: [PATCH 636/955] Prompt user to remove application credentials when deleting config entries (#74825) * Prompt user to remove application credentials when deleting config entries * Adjust assertions on intermediate state in config entry tests * Add a callback hook to modify config entry remove result * Improve test coverage and simplify implementation * Register remove callback per domain * Update homeassistant/components/application_credentials/__init__.py Co-authored-by: Martin Hjelmare * Fix tests to use new variable name including domain * Add websocket command to return application credentials for an integration * Remove unnecessary diff * Apply suggestions from code review Co-authored-by: Martin Hjelmare Co-authored-by: Martin Hjelmare --- .../application_credentials/__init__.py | 55 ++++++++++++++++- .../application_credentials/test_init.py | 60 ++++++++++++++++--- 2 files changed, 104 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/application_credentials/__init__.py b/homeassistant/components/application_credentials/__init__.py index 6dd2d562307..811a637b4ef 100644 --- a/homeassistant/components/application_credentials/__init__.py +++ b/homeassistant/components/application_credentials/__init__.py @@ -15,6 +15,7 @@ import voluptuous as vol from homeassistant.components import websocket_api from homeassistant.components.websocket_api.connection import ActiveConnection +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_CLIENT_ID, CONF_CLIENT_SECRET, @@ -127,9 +128,7 @@ class ApplicationCredentialsStorageCollection(collection.StorageCollection): for item in self.async_items(): if item[CONF_DOMAIN] != domain: continue - auth_domain = ( - item[CONF_AUTH_DOMAIN] if CONF_AUTH_DOMAIN in item else item[CONF_ID] - ) + auth_domain = item.get(CONF_AUTH_DOMAIN, item[CONF_ID]) credentials[auth_domain] = ClientCredential( client_id=item[CONF_CLIENT_ID], client_secret=item[CONF_CLIENT_SECRET], @@ -156,6 +155,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ).async_setup(hass) websocket_api.async_register_command(hass, handle_integration_list) + websocket_api.async_register_command(hass, handle_config_entry) config_entry_oauth2_flow.async_add_implementation_provider( hass, DOMAIN, _async_provide_implementation @@ -234,6 +234,27 @@ async def _async_provide_implementation( ] +async def _async_config_entry_app_credentials( + hass: HomeAssistant, + config_entry: ConfigEntry, +) -> str | None: + """Return the item id of an application credential for an existing ConfigEntry.""" + if not await _get_platform(hass, config_entry.domain) or not ( + auth_domain := config_entry.data.get("auth_implementation") + ): + return None + + storage_collection = hass.data[DOMAIN][DATA_STORAGE] + for item in storage_collection.async_items(): + item_id = item[CONF_ID] + if ( + item[CONF_DOMAIN] == config_entry.domain + and item.get(CONF_AUTH_DOMAIN, item_id) == auth_domain + ): + return item_id + return None + + class ApplicationCredentialsProtocol(Protocol): """Define the format that application_credentials platforms may have. @@ -311,3 +332,31 @@ async def handle_integration_list( }, } connection.send_result(msg["id"], result) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "application_credentials/config_entry", + vol.Required("config_entry_id"): str, + } +) +@websocket_api.async_response +async def handle_config_entry( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: + """Return application credentials information for a config entry.""" + entry_id = msg["config_entry_id"] + config_entry = hass.config_entries.async_get_entry(entry_id) + if not config_entry: + connection.send_error( + msg["id"], + "invalid_config_entry_id", + f"Config entry not found: {entry_id}", + ) + return + result = {} + if application_credentials_id := await _async_config_entry_app_credentials( + hass, config_entry + ): + result["application_credentials_id"] = application_credentials_id + connection.send_result(msg["id"], result) diff --git a/tests/components/application_credentials/test_init.py b/tests/components/application_credentials/test_init.py index 3b7a414305f..04112e2a00d 100644 --- a/tests/components/application_credentials/test_init.py +++ b/tests/components/application_credentials/test_init.py @@ -30,7 +30,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.setup import async_setup_component -from tests.common import mock_platform +from tests.common import MockConfigEntry, mock_platform CLIENT_ID = "some-client-id" CLIENT_SECRET = "some-client-secret" @@ -90,10 +90,12 @@ async def mock_application_credentials_integration( authorization_server: AuthorizationServer, ): """Mock a application_credentials integration.""" - assert await async_setup_component(hass, "application_credentials", {}) - await setup_application_credentials_integration( - hass, TEST_DOMAIN, authorization_server - ) + with patch("homeassistant.loader.APPLICATION_CREDENTIALS", [TEST_DOMAIN]): + assert await async_setup_component(hass, "application_credentials", {}) + await setup_application_credentials_integration( + hass, TEST_DOMAIN, authorization_server + ) + yield class FakeConfigFlow(config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN): @@ -418,7 +420,7 @@ async def test_config_flow_no_credentials(hass): TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result.get("type") == data_entry_flow.FlowResultType.ABORT - assert result.get("reason") == "missing_configuration" + assert result.get("reason") == "missing_credentials" async def test_config_flow_other_domain( @@ -445,7 +447,7 @@ async def test_config_flow_other_domain( TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result.get("type") == data_entry_flow.FlowResultType.ABORT - assert result.get("reason") == "missing_configuration" + assert result.get("reason") == "missing_credentials" async def test_config_flow( @@ -483,6 +485,27 @@ async def test_config_flow( == "Cannot delete credential in use by integration fake_integration" ) + # Return information about the in use config entry + entries = hass.config_entries.async_entries(TEST_DOMAIN) + assert len(entries) == 1 + client = await ws_client() + result = await client.cmd_result( + "config_entry", {"config_entry_id": entries[0].entry_id} + ) + assert result.get("application_credentials_id") == ID + + # Delete the config entry + await hass.config_entries.async_remove(entries[0].entry_id) + + # Application credential can now be removed + resp = await client.cmd("delete", {"application_credentials_id": ID}) + assert resp.get("success") + + # Config entry information no longer found + result = await client.cmd("config_entry", {"config_entry_id": entries[0].entry_id}) + assert "error" in result + assert result["error"].get("code") == "invalid_config_entry_id" + async def test_config_flow_multiple_entries( hass: HomeAssistant, @@ -549,7 +572,7 @@ async def test_config_flow_create_delete_credential( TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result.get("type") == data_entry_flow.FlowResultType.ABORT - assert result.get("reason") == "missing_configuration" + assert result.get("reason") == "missing_credentials" @pytest.mark.parametrize("config_credential", [DEVELOPER_CREDENTIAL]) @@ -751,3 +774,24 @@ async def test_name( assert ( result["data"].get("auth_implementation") == "fake_integration_some_client_id" ) + + +async def test_remove_config_entry_without_app_credentials( + hass: HomeAssistant, + ws_client: ClientFixture, + authorization_server: AuthorizationServer, +): + """Test config entry removal for non-app credentials integration.""" + hass.config.components.add("other_domain") + config_entry = MockConfigEntry(domain="other_domain") + config_entry.add_to_hass(hass) + assert await async_setup_component(hass, "other_domain", {}) + + entries = hass.config_entries.async_entries("other_domain") + assert len(entries) == 1 + + client = await ws_client() + result = await client.cmd_result( + "config_entry", {"config_entry_id": entries[0].entry_id} + ) + assert "application_credential_id" not in result From e62e21ce462b4f732d7ffb4d9323e3096bdaba97 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 22 Sep 2022 01:10:13 -0400 Subject: [PATCH 637/955] Bump pytomorrowio to 0.3.5 (#78914) --- homeassistant/components/tomorrowio/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tomorrowio/manifest.json b/homeassistant/components/tomorrowio/manifest.json index 0823b5ac185..8c097d46eb7 100644 --- a/homeassistant/components/tomorrowio/manifest.json +++ b/homeassistant/components/tomorrowio/manifest.json @@ -3,7 +3,7 @@ "name": "Tomorrow.io", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tomorrowio", - "requirements": ["pytomorrowio==0.3.4"], + "requirements": ["pytomorrowio==0.3.5"], "codeowners": ["@raman325", "@lymanepp"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 204540ebdff..218718ace49 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2042,7 +2042,7 @@ pythonegardia==1.0.40 pytile==2022.02.0 # homeassistant.components.tomorrowio -pytomorrowio==0.3.4 +pytomorrowio==0.3.5 # homeassistant.components.touchline pytouchline==0.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ef4f9f4e38f..255c208ccaf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1402,7 +1402,7 @@ python_awair==0.2.4 pytile==2022.02.0 # homeassistant.components.tomorrowio -pytomorrowio==0.3.4 +pytomorrowio==0.3.5 # homeassistant.components.traccar pytraccar==1.0.0 From 39315b7fe3b13be5c026a5e7d7180ec3715ab882 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 22 Sep 2022 07:18:00 +0200 Subject: [PATCH 638/955] Introduce UnitConverter protocol (#78888) * Introduce ConversionUtility * Use ConversionUtility in number * Use ConversionUtility in sensor * Use ConversionUtility in sensor recorder * Add normalise to ConversionUtility * Revert changes to recorder.py * Reduce size of PR * Adjust recorder statistics * Rename variable * Rename * Apply suggestion Co-authored-by: Erik Montnemery * Apply suggestion Co-authored-by: Erik Montnemery * Apply suggestion Co-authored-by: Erik Montnemery Co-authored-by: Erik Montnemery --- homeassistant/components/number/__init__.py | 25 ++++++++---------- .../components/recorder/statistics.py | 23 +++++++++------- homeassistant/components/sensor/__init__.py | 26 ++++++++----------- homeassistant/helpers/typing.py | 12 ++++++++- homeassistant/util/energy.py | 16 ++++++------ homeassistant/util/power.py | 16 ++++++------ homeassistant/util/pressure.py | 16 ++++++------ homeassistant/util/temperature.py | 4 +-- 8 files changed, 72 insertions(+), 66 deletions(-) diff --git a/homeassistant/components/number/__init__.py b/homeassistant/components/number/__init__.py index 5f92bdfed8b..f3e9a1d9da1 100644 --- a/homeassistant/components/number/__init__.py +++ b/homeassistant/components/number/__init__.py @@ -28,7 +28,7 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import ExtraStoredData, RestoreEntity -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, UnitConverter from homeassistant.util import temperature as temperature_util from .const import ( @@ -70,12 +70,8 @@ class NumberMode(StrEnum): SLIDER = "slider" -UNIT_CONVERSIONS: dict[str, Callable[[float, str, str], float]] = { - NumberDeviceClass.TEMPERATURE: temperature_util.convert, -} - -VALID_UNITS: dict[str, tuple[str, ...]] = { - NumberDeviceClass.TEMPERATURE: temperature_util.VALID_UNITS, +UNIT_CONVERTERS: dict[str, UnitConverter] = { + NumberDeviceClass.TEMPERATURE: temperature_util, } # mypy: disallow-any-generics @@ -436,7 +432,7 @@ class NumberEntity(Entity): if ( native_unit_of_measurement != unit_of_measurement - and device_class in UNIT_CONVERSIONS + and device_class in UNIT_CONVERTERS ): assert native_unit_of_measurement assert unit_of_measurement @@ -446,7 +442,7 @@ class NumberEntity(Entity): # Suppress ValueError (Could not convert value to float) with suppress(ValueError): - value_new: float = UNIT_CONVERSIONS[device_class]( + value_new: float = UNIT_CONVERTERS[device_class].convert( value, native_unit_of_measurement, unit_of_measurement, @@ -467,12 +463,12 @@ class NumberEntity(Entity): if ( value is not None and native_unit_of_measurement != unit_of_measurement - and device_class in UNIT_CONVERSIONS + and device_class in UNIT_CONVERTERS ): assert native_unit_of_measurement assert unit_of_measurement - value = UNIT_CONVERSIONS[device_class]( + value = UNIT_CONVERTERS[device_class].convert( value, unit_of_measurement, native_unit_of_measurement, @@ -500,9 +496,10 @@ class NumberEntity(Entity): if ( (number_options := self.registry_entry.options.get(DOMAIN)) and (custom_unit := number_options.get(CONF_UNIT_OF_MEASUREMENT)) - and (device_class := self.device_class) in UNIT_CONVERSIONS - and self.native_unit_of_measurement in VALID_UNITS[device_class] - and custom_unit in VALID_UNITS[device_class] + and (device_class := self.device_class) in UNIT_CONVERTERS + and self.native_unit_of_measurement + in UNIT_CONVERTERS[device_class].VALID_UNITS + and custom_unit in UNIT_CONVERTERS[device_class].VALID_UNITS ): self._number_option_unit_of_measurement = custom_unit return diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index be568adbb25..15309d5ab46 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -36,7 +36,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry from homeassistant.helpers.json import JSONEncoder from homeassistant.helpers.storage import STORAGE_DIR -from homeassistant.helpers.typing import UNDEFINED, UndefinedType +from homeassistant.helpers.typing import UNDEFINED, UndefinedType, UnitConverter from homeassistant.util import ( dt as dt_util, energy as energy_util, @@ -186,12 +186,12 @@ STATISTIC_UNIT_TO_UNIT_CLASS: dict[str | None, str] = { VOLUME_CUBIC_METERS: "volume", } -STATISTIC_UNIT_TO_VALID_UNITS: dict[str | None, Iterable[str | None]] = { - ENERGY_KILO_WATT_HOUR: energy_util.VALID_UNITS, - POWER_WATT: power_util.VALID_UNITS, - PRESSURE_PA: pressure_util.VALID_UNITS, - TEMP_CELSIUS: temperature_util.VALID_UNITS, - VOLUME_CUBIC_METERS: volume_util.VALID_UNITS, +STATISTIC_UNIT_TO_UNIT_CONVERTER: dict[str | None, UnitConverter] = { + ENERGY_KILO_WATT_HOUR: energy_util, + POWER_WATT: power_util, + PRESSURE_PA: pressure_util, + TEMP_CELSIUS: temperature_util, + VOLUME_CUBIC_METERS: volume_util, } # Convert energy power, pressure, temperature and volume statistics from the @@ -243,7 +243,8 @@ def _get_statistic_to_display_unit_converter( else: display_unit = state_unit - if display_unit not in STATISTIC_UNIT_TO_VALID_UNITS[statistic_unit]: + unit_converter = STATISTIC_UNIT_TO_UNIT_CONVERTER[statistic_unit] + if display_unit not in unit_converter.VALID_UNITS: # Guard against invalid state unit in the DB return no_conversion @@ -1514,9 +1515,11 @@ def _validate_units(statistics_unit: str | None, state_unit: str | None) -> None """Raise if the statistics unit and state unit are not compatible.""" if statistics_unit == state_unit: return - if (valid_units := STATISTIC_UNIT_TO_VALID_UNITS.get(statistics_unit)) is None: + if ( + unit_converter := STATISTIC_UNIT_TO_UNIT_CONVERTER.get(statistics_unit) + ) is None: raise HomeAssistantError(f"Invalid units {statistics_unit},{state_unit}") - if state_unit not in valid_units: + if state_unit not in unit_converter.VALID_UNITS: raise HomeAssistantError(f"Invalid units {statistics_unit},{state_unit}") diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index a6bfd4189f8..530769f2873 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -1,7 +1,7 @@ """Component to interface with various sensors that can be monitored.""" from __future__ import annotations -from collections.abc import Callable, Mapping +from collections.abc import Mapping from contextlib import suppress from dataclasses import dataclass from datetime import date, datetime, timedelta, timezone @@ -56,7 +56,7 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import ExtraStoredData, RestoreEntity -from homeassistant.helpers.typing import ConfigType, StateType +from homeassistant.helpers.typing import ConfigType, StateType, UnitConverter from homeassistant.util import ( dt as dt_util, pressure as pressure_util, @@ -207,9 +207,9 @@ STATE_CLASS_TOTAL: Final = "total" STATE_CLASS_TOTAL_INCREASING: Final = "total_increasing" STATE_CLASSES: Final[list[str]] = [cls.value for cls in SensorStateClass] -UNIT_CONVERSIONS: dict[str, Callable[[float, str, str], float]] = { - SensorDeviceClass.PRESSURE: pressure_util.convert, - SensorDeviceClass.TEMPERATURE: temperature_util.convert, +UNIT_CONVERTERS: dict[str, UnitConverter] = { + SensorDeviceClass.PRESSURE: pressure_util, + SensorDeviceClass.TEMPERATURE: temperature_util, } UNIT_RATIOS: dict[str, dict[str, float]] = { @@ -221,11 +221,6 @@ UNIT_RATIOS: dict[str, dict[str, float]] = { }, } -VALID_UNITS: dict[str, tuple[str, ...]] = { - SensorDeviceClass.PRESSURE: pressure_util.VALID_UNITS, - SensorDeviceClass.TEMPERATURE: temperature_util.VALID_UNITS, -} - # mypy: disallow-any-generics @@ -431,7 +426,7 @@ class SensorEntity(Entity): if ( value is not None and native_unit_of_measurement != unit_of_measurement - and device_class in UNIT_CONVERSIONS + and device_class in UNIT_CONVERTERS ): assert unit_of_measurement assert native_unit_of_measurement @@ -453,7 +448,7 @@ class SensorEntity(Entity): # Suppress ValueError (Could not convert sensor_value to float) with suppress(ValueError): value_f = float(value) # type: ignore[arg-type] - value_f_new = UNIT_CONVERSIONS[device_class]( + value_f_new = UNIT_CONVERTERS[device_class].convert( value_f, native_unit_of_measurement, unit_of_measurement, @@ -482,9 +477,10 @@ class SensorEntity(Entity): if ( (sensor_options := self.registry_entry.options.get(DOMAIN)) and (custom_unit := sensor_options.get(CONF_UNIT_OF_MEASUREMENT)) - and (device_class := self.device_class) in UNIT_CONVERSIONS - and self.native_unit_of_measurement in VALID_UNITS[device_class] - and custom_unit in VALID_UNITS[device_class] + and (device_class := self.device_class) in UNIT_CONVERTERS + and self.native_unit_of_measurement + in UNIT_CONVERTERS[device_class].VALID_UNITS + and custom_unit in UNIT_CONVERTERS[device_class].VALID_UNITS ): self._sensor_option_unit_of_measurement = custom_unit return diff --git a/homeassistant/helpers/typing.py b/homeassistant/helpers/typing.py index a7430d1fe69..c679de288b1 100644 --- a/homeassistant/helpers/typing.py +++ b/homeassistant/helpers/typing.py @@ -1,7 +1,7 @@ """Typing Helpers for Home Assistant.""" from collections.abc import Mapping from enum import Enum -from typing import Any, Optional, Union +from typing import Any, Optional, Protocol, Union import homeassistant.core @@ -26,6 +26,16 @@ class UndefinedType(Enum): UNDEFINED = UndefinedType._singleton # pylint: disable=protected-access + +class UnitConverter(Protocol): + """Define the format of a conversion utility.""" + + VALID_UNITS: tuple[str, ...] + + def convert(self, value: float, from_unit: str, to_unit: str) -> float: + """Convert one unit of measurement to another.""" + + # The following types should not used and # are not present in the core code base. # They are kept in order not to break custom integrations diff --git a/homeassistant/util/energy.py b/homeassistant/util/energy.py index 4d1bd10f4b2..00695704751 100644 --- a/homeassistant/util/energy.py +++ b/homeassistant/util/energy.py @@ -23,18 +23,18 @@ UNIT_CONVERSION: dict[str, float] = { } -def convert(value: float, unit_1: str, unit_2: str) -> float: +def convert(value: float, from_unit: str, to_unit: str) -> float: """Convert one unit of measurement to another.""" - if unit_1 not in VALID_UNITS: - raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(unit_1, "energy")) - if unit_2 not in VALID_UNITS: - raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(unit_2, "energy")) + if from_unit not in VALID_UNITS: + raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(from_unit, "energy")) + if to_unit not in VALID_UNITS: + raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(to_unit, "energy")) if not isinstance(value, Number): raise TypeError(f"{value} is not of numeric type") - if unit_1 == unit_2: + if from_unit == to_unit: return value - watts = value / UNIT_CONVERSION[unit_1] - return watts * UNIT_CONVERSION[unit_2] + watthours = value / UNIT_CONVERSION[from_unit] + return watthours * UNIT_CONVERSION[to_unit] diff --git a/homeassistant/util/power.py b/homeassistant/util/power.py index 74be6d55377..ae4f4c249b6 100644 --- a/homeassistant/util/power.py +++ b/homeassistant/util/power.py @@ -20,18 +20,18 @@ UNIT_CONVERSION: dict[str, float] = { } -def convert(value: float, unit_1: str, unit_2: str) -> float: +def convert(value: float, from_unit: str, to_unit: str) -> float: """Convert one unit of measurement to another.""" - if unit_1 not in VALID_UNITS: - raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(unit_1, "power")) - if unit_2 not in VALID_UNITS: - raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(unit_2, "power")) + if from_unit not in VALID_UNITS: + raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(from_unit, "power")) + if to_unit not in VALID_UNITS: + raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(to_unit, "power")) if not isinstance(value, Number): raise TypeError(f"{value} is not of numeric type") - if unit_1 == unit_2: + if from_unit == to_unit: return value - watts = value / UNIT_CONVERSION[unit_1] - return watts * UNIT_CONVERSION[unit_2] + watts = value / UNIT_CONVERSION[from_unit] + return watts * UNIT_CONVERSION[to_unit] diff --git a/homeassistant/util/pressure.py b/homeassistant/util/pressure.py index dca94764de3..a07f9d777c3 100644 --- a/homeassistant/util/pressure.py +++ b/homeassistant/util/pressure.py @@ -42,18 +42,18 @@ UNIT_CONVERSION: dict[str, float] = { } -def convert(value: float, unit_1: str, unit_2: str) -> float: +def convert(value: float, from_unit: str, to_unit: str) -> float: """Convert one unit of measurement to another.""" - if unit_1 not in VALID_UNITS: - raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(unit_1, PRESSURE)) - if unit_2 not in VALID_UNITS: - raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(unit_2, PRESSURE)) + if from_unit not in VALID_UNITS: + raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(from_unit, PRESSURE)) + if to_unit not in VALID_UNITS: + raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(to_unit, PRESSURE)) if not isinstance(value, Number): raise TypeError(f"{value} is not of numeric type") - if unit_1 == unit_2: + if from_unit == to_unit: return value - pascals = value / UNIT_CONVERSION[unit_1] - return pascals * UNIT_CONVERSION[unit_2] + pascals = value / UNIT_CONVERSION[from_unit] + return pascals * UNIT_CONVERSION[to_unit] diff --git a/homeassistant/util/temperature.py b/homeassistant/util/temperature.py index d7b7597d6d0..06febd600e7 100644 --- a/homeassistant/util/temperature.py +++ b/homeassistant/util/temperature.py @@ -46,9 +46,9 @@ def convert( temperature: float, from_unit: str, to_unit: str, interval: bool = False ) -> float: """Convert a temperature from one unit to another.""" - if from_unit not in (TEMP_CELSIUS, TEMP_FAHRENHEIT, TEMP_KELVIN): + if from_unit not in VALID_UNITS: raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(from_unit, TEMPERATURE)) - if to_unit not in (TEMP_CELSIUS, TEMP_FAHRENHEIT, TEMP_KELVIN): + if to_unit not in VALID_UNITS: raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(to_unit, TEMPERATURE)) if from_unit == to_unit: From f5120872aa33ab234934042b58acaf7239a96c38 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Thu, 22 Sep 2022 08:43:30 +0200 Subject: [PATCH 639/955] Support for nibe heat pumps with local access (#78542) * Add nibe local integration * Add sensor platform * Enable sensor platform * Fix manifest * Correct domain after rename * Adjust tests for rename * Correct codeowners * Add requirements for tests * Grab coil by name * Switch to home assistant error * Config entry always exist * Switch to create task * Bump to 0.5.0 * Use new coils access * Remove unneeded check * Use single instance of logger * Test invalid ip * Don't allow coil to be None * Remove sleep * Initialize data in coordinator init * Add utils to ignore * Update homeassistant/components/nibe_heatpump/manifest.json Co-authored-by: J. Nick Koston * Use generator instead * Use tenacity as retry decorator * Use package instead of name to get logger * Skip broad exception handling * Catch missing coil exception * Add missing test Co-authored-by: J. Nick Koston --- .coveragerc | 2 + CODEOWNERS | 2 + .../components/nibe_heatpump/__init__.py | 226 ++++++++++++++++++ .../components/nibe_heatpump/config_flow.py | 133 +++++++++++ .../components/nibe_heatpump/const.py | 12 + .../components/nibe_heatpump/manifest.json | 9 + .../components/nibe_heatpump/sensor.py | 77 ++++++ .../components/nibe_heatpump/strings.json | 25 ++ .../nibe_heatpump/translations/en.json | 24 ++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 6 + requirements_test_all.txt | 6 + tests/components/nibe_heatpump/__init__.py | 1 + .../nibe_heatpump/test_config_flow.py | 172 +++++++++++++ 14 files changed, 696 insertions(+) create mode 100644 homeassistant/components/nibe_heatpump/__init__.py create mode 100644 homeassistant/components/nibe_heatpump/config_flow.py create mode 100644 homeassistant/components/nibe_heatpump/const.py create mode 100644 homeassistant/components/nibe_heatpump/manifest.json create mode 100644 homeassistant/components/nibe_heatpump/sensor.py create mode 100644 homeassistant/components/nibe_heatpump/strings.json create mode 100644 homeassistant/components/nibe_heatpump/translations/en.json create mode 100644 tests/components/nibe_heatpump/__init__.py create mode 100644 tests/components/nibe_heatpump/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 9aafc4300cb..fa31616541d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -833,6 +833,8 @@ omit = homeassistant/components/nextcloud/* homeassistant/components/nfandroidtv/__init__.py homeassistant/components/nfandroidtv/notify.py + homeassistant/components/nibe_heatpump/__init__.py + homeassistant/components/nibe_heatpump/sensor.py homeassistant/components/niko_home_control/light.py homeassistant/components/nilu/air_quality.py homeassistant/components/nissan_leaf/* diff --git a/CODEOWNERS b/CODEOWNERS index 1e3a06f16c7..9c25b5a3eed 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -747,6 +747,8 @@ build.json @home-assistant/supervisor /tests/components/nextdns/ @bieniu /homeassistant/components/nfandroidtv/ @tkdrob /tests/components/nfandroidtv/ @tkdrob +/homeassistant/components/nibe_heatpump/ @elupus +/tests/components/nibe_heatpump/ @elupus /homeassistant/components/nightscout/ @marciogranzotto /tests/components/nightscout/ @marciogranzotto /homeassistant/components/nilu/ @hfurubotten diff --git a/homeassistant/components/nibe_heatpump/__init__.py b/homeassistant/components/nibe_heatpump/__init__.py new file mode 100644 index 00000000000..343452c9f45 --- /dev/null +++ b/homeassistant/components/nibe_heatpump/__init__.py @@ -0,0 +1,226 @@ +"""The Nibe Heat Pump integration.""" +from __future__ import annotations + +from datetime import timedelta + +from nibe.coil import Coil +from nibe.connection import Connection +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 homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_IP_ADDRESS, CONF_MODEL, Platform +from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.entity import DeviceInfo, async_generate_entity_id +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + +from .const import ( + CONF_CONNECTION_TYPE, + CONF_CONNECTION_TYPE_NIBEGW, + CONF_LISTENING_PORT, + CONF_REMOTE_READ_PORT, + CONF_REMOTE_WRITE_PORT, + CONF_WORD_SWAP, + DOMAIN, + LOGGER, +) + +PLATFORMS: list[Platform] = [Platform.SENSOR] + + +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] + heatpump.initialize() + + connection_type = entry.data[CONF_CONNECTION_TYPE] + + if connection_type == CONF_CONNECTION_TYPE_NIBEGW: + connection = NibeGW( + heatpump, + entry.data[CONF_IP_ADDRESS], + entry.data[CONF_REMOTE_READ_PORT], + entry.data[CONF_REMOTE_WRITE_PORT], + listening_port=entry.data[CONF_LISTENING_PORT], + ) + else: + raise HomeAssistantError(f"Connection type {connection_type} is not supported.") + + await connection.start() + coordinator = Coordinator(hass, heatpump, connection) + + data = hass.data.setdefault(DOMAIN, {}) + data[entry.entry_id] = coordinator + + try: + await coordinator.async_config_entry_first_refresh() + except ConfigEntryNotReady: + await connection.stop() + raise + + reg = dr.async_get(hass) + reg.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, entry.unique_id or entry.entry_id)}, + manufacturer="NIBE Energy Systems", + model=heatpump.model.name, + name=heatpump.model.name, + ) + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + # Trigger a refresh again now that all platforms have registered + hass.async_create_task(coordinator.async_refresh()) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + coordinator: Coordinator = hass.data[DOMAIN].pop(entry.entry_id) + await coordinator.connection.stop() + + return unload_ok + + +class Coordinator(DataUpdateCoordinator[dict[int, Coil]]): + """Update coordinator for nibe heat pumps.""" + + config_entry: ConfigEntry + + def __init__( + self, + hass: HomeAssistant, + heatpump: HeatPump, + connection: Connection, + ) -> None: + """Initialize coordinator.""" + super().__init__( + hass, LOGGER, name="Nibe Heat Pump", update_interval=timedelta(seconds=60) + ) + + self.data = {} + self.connection = connection + self.heatpump = heatpump + + @property + def coils(self) -> list[Coil]: + """Return the full coil database.""" + return self.heatpump.get_coils() + + @property + def unique_id(self) -> str: + """Return unique id for this coordinator.""" + return self.config_entry.unique_id or self.config_entry.entry_id + + @property + def device_info(self) -> DeviceInfo: + """Return device information for the main device.""" + return DeviceInfo(identifiers={(DOMAIN, self.unique_id)}) + + 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 + return None + + def get_coil_float(self, coil: Coil) -> float | None: + """Return a coil with float and check for validity.""" + if value := self.get_coil_value(coil): + return float(value) + return None + + async def async_write_coil( + self, coil: Coil | None, value: int | float | str + ) -> None: + """Write coil and update state.""" + if not coil: + raise HomeAssistantError("No coil available") + + coil.value = value + coil = await self.connection.write_coil(coil) + + if self.data: + self.data[coil.address] = coil + self.async_update_listeners() + + async def _async_update_data(self) -> dict[int, Coil]: + @retry( + retry=retry_if_exception_type(CoilReadException), stop=stop_after_attempt(2) + ) + async def read_coil(coil: Coil): + return await self.connection.read_coil(coil) + + callbacks: dict[int, list[CALLBACK_TYPE]] = {} + for update_callback, context in list(self._listeners.values()): + assert isinstance(context, set) + for address in context: + callbacks.setdefault(address, []).append(update_callback) + + result: dict[int, Coil] = {} + + for address, callback_list in callbacks.items(): + try: + coil = self.heatpump.get_coil_by_address(address) + self.data[coil.address] = result[coil.address] = await read_coil(coil) + except (CoilReadException, RetryError) as exception: + self.logger.warning("Failed to update: %s", exception) + except CoilNotFoundException as exception: + self.logger.debug("Skipping missing coil: %s", exception) + + for update_callback in callback_list: + update_callback() + + return result + + +class CoilEntity(CoordinatorEntity[Coordinator]): + """Base for coil based entities.""" + + _attr_has_entity_name = True + _attr_entity_registry_enabled_default = False + + def __init__( + self, coordinator: Coordinator, coil: Coil, entity_format: str + ) -> None: + """Initialize base entity.""" + super().__init__(coordinator, {coil.address}) + self.entity_id = async_generate_entity_id( + entity_format, coil.name, hass=coordinator.hass + ) + self._attr_name = coil.title + self._attr_unique_id = f"{coordinator.unique_id}-{coil.address}" + self._attr_device_info = coordinator.device_info + self._coil = coil + + @property + def available(self) -> bool: + """Return if entity is available.""" + return self.coordinator.last_update_success and self._coil.address in ( + self.coordinator.data or {} + ) + + def _async_read_coil(self, coil: Coil): + """Update state of entity based on coil data.""" + + async def _async_write_coil(self, value: int | float | str): + """Write coil and update state.""" + await self.coordinator.async_write_coil(self._coil, value) + + def _handle_coordinator_update(self) -> None: + coil = self.coordinator.data.get(self._coil.address) + if coil is None: + return + + self._coil = coil + self._async_read_coil(coil) + self.async_write_ha_state() diff --git a/homeassistant/components/nibe_heatpump/config_flow.py b/homeassistant/components/nibe_heatpump/config_flow.py new file mode 100644 index 00000000000..14da4d478b2 --- /dev/null +++ b/homeassistant/components/nibe_heatpump/config_flow.py @@ -0,0 +1,133 @@ +"""Config flow for Nibe Heat Pump integration.""" +from __future__ import annotations + +import errno +from typing import Any + +from nibe.connection.nibegw import NibeGW +from nibe.exceptions import CoilNotFoundException, CoilReadException, CoilWriteException +from nibe.heatpump import HeatPump, Model +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_IP_ADDRESS, CONF_MODEL +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import config_validation as cv +from homeassistant.util.network import is_ipv4_address + +from .const import ( + CONF_CONNECTION_TYPE, + CONF_CONNECTION_TYPE_NIBEGW, + CONF_LISTENING_PORT, + CONF_REMOTE_READ_PORT, + CONF_REMOTE_WRITE_PORT, + CONF_WORD_SWAP, + DOMAIN, + LOGGER, +) + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_MODEL): vol.In([e.name for e in Model]), + vol.Required(CONF_IP_ADDRESS): str, + vol.Required(CONF_LISTENING_PORT): cv.port, + vol.Required(CONF_REMOTE_READ_PORT): cv.port, + vol.Required(CONF_REMOTE_WRITE_PORT): cv.port, + } +) + + +class FieldError(Exception): + """Field with invalid data.""" + + def __init__(self, message: str, field: str, error: str) -> None: + """Set up error.""" + super().__init__(message) + self.field = field + self.error = error + + +async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]: + """Validate the user input allows us to connect.""" + + if not is_ipv4_address(data[CONF_IP_ADDRESS]): + raise FieldError("Not a valid ipv4 address", CONF_IP_ADDRESS, "address") + + heatpump = HeatPump(Model[data[CONF_MODEL]]) + heatpump.initialize() + + connection = NibeGW( + heatpump, + data[CONF_IP_ADDRESS], + data[CONF_REMOTE_READ_PORT], + data[CONF_REMOTE_WRITE_PORT], + listening_port=data[CONF_LISTENING_PORT], + ) + + 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 + + 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 CoilNotFoundException as exception: + raise FieldError( + "Model selected doesn't seem to support expected coils", "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() + + return { + "title": f"{data[CONF_MODEL]} at {data[CONF_IP_ADDRESS]}", + CONF_WORD_SWAP: word_swap, + } + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Nibe Heat Pump.""" + + 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 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: + 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_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) diff --git a/homeassistant/components/nibe_heatpump/const.py b/homeassistant/components/nibe_heatpump/const.py new file mode 100644 index 00000000000..f1bcbf11127 --- /dev/null +++ b/homeassistant/components/nibe_heatpump/const.py @@ -0,0 +1,12 @@ +"""Constants for the Nibe Heat Pump integration.""" +import logging + +DOMAIN = "nibe_heatpump" +LOGGER = logging.getLogger(__package__) + +CONF_LISTENING_PORT = "listening_port" +CONF_REMOTE_READ_PORT = "remote_read_port" +CONF_REMOTE_WRITE_PORT = "remote_write_port" +CONF_WORD_SWAP = "word_swap" +CONF_CONNECTION_TYPE = "connection_type" +CONF_CONNECTION_TYPE_NIBEGW = "nibegw" diff --git a/homeassistant/components/nibe_heatpump/manifest.json b/homeassistant/components/nibe_heatpump/manifest.json new file mode 100644 index 00000000000..4b66b93d31b --- /dev/null +++ b/homeassistant/components/nibe_heatpump/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "nibe_heatpump", + "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"], + "codeowners": ["@elupus"], + "iot_class": "local_polling" +} diff --git a/homeassistant/components/nibe_heatpump/sensor.py b/homeassistant/components/nibe_heatpump/sensor.py new file mode 100644 index 00000000000..b6ea2e766a2 --- /dev/null +++ b/homeassistant/components/nibe_heatpump/sensor.py @@ -0,0 +1,77 @@ +"""The Nibe Heat Pump sensors.""" +from __future__ import annotations + +from nibe.coil import Coil + +from homeassistant.components.sensor import ( + ENTITY_ID_FORMAT, + SensorDeviceClass, + SensorEntity, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + ELECTRIC_CURRENT_AMPERE, + ELECTRIC_CURRENT_MILLIAMPERE, + ELECTRIC_POTENTIAL_MILLIVOLT, + ELECTRIC_POTENTIAL_VOLT, + ENERGY_KILO_WATT_HOUR, + ENERGY_MEGA_WATT_HOUR, + ENERGY_WATT_HOUR, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, + TEMP_KELVIN, + TIME_HOURS, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import DOMAIN, CoilEntity, Coordinator + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up platform.""" + + coordinator: Coordinator = hass.data[DOMAIN][config_entry.entry_id] + + async_add_entities( + Sensor(coordinator, coil) + for coil in coordinator.coils + if not coil.is_writable and not coil.is_boolean + ) + + +class Sensor(SensorEntity, CoilEntity): + """Sensor entity.""" + + _attr_entity_category = EntityCategory.DIAGNOSTIC + + def __init__(self, coordinator: Coordinator, coil: Coil) -> None: + """Initialize entity.""" + super().__init__(coordinator, coil, ENTITY_ID_FORMAT) + self._attr_native_unit_of_measurement = coil.unit + + unit = self.native_unit_of_measurement + if unit in {TEMP_CELSIUS, TEMP_FAHRENHEIT, TEMP_KELVIN}: + self._attr_device_class = SensorDeviceClass.TEMPERATURE + elif unit in {ELECTRIC_CURRENT_AMPERE, ELECTRIC_CURRENT_MILLIAMPERE}: + self._attr_device_class = SensorDeviceClass.CURRENT + elif unit in {ELECTRIC_POTENTIAL_VOLT, ELECTRIC_POTENTIAL_MILLIVOLT}: + self._attr_device_class = SensorDeviceClass.VOLTAGE + elif unit in {ENERGY_WATT_HOUR, ENERGY_KILO_WATT_HOUR, ENERGY_MEGA_WATT_HOUR}: + self._attr_device_class = SensorDeviceClass.ENERGY + elif unit in {TIME_HOURS}: + self._attr_device_class = SensorDeviceClass.DURATION + else: + self._attr_device_class = None + + if unit: + self._attr_state_class = SensorStateClass.MEASUREMENT + + def _async_read_coil(self, coil: Coil): + self._attr_native_value = coil.value diff --git a/homeassistant/components/nibe_heatpump/strings.json b/homeassistant/components/nibe_heatpump/strings.json new file mode 100644 index 00000000000..5b31ba178b3 --- /dev/null +++ b/homeassistant/components/nibe_heatpump/strings.json @@ -0,0 +1,25 @@ +{ + "config": { + "step": { + "user": { + "data": { + "ip_address": "Remote IP address", + "remote_read_port": "Remote read port", + "remote_write_port": "Remote write port", + "listening_port": "Local listening port" + } + } + }, + "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`.", + "address": "Invalid remote IP address specified. Address must be a IPV4 address.", + "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%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/nibe_heatpump/translations/en.json b/homeassistant/components/nibe_heatpump/translations/en.json new file mode 100644 index 00000000000..17120e20d88 --- /dev/null +++ b/homeassistant/components/nibe_heatpump/translations/en.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "address": "Invalid remote IP address specified. Address must be a IPV4 address.", + "address_in_use": "The selected listening port is already in use on this system. Reconfigure your gateway device to use a different address if the conflict can not be resolved.", + "read": "Error on read request from pump. Verify your `Remote read port` or `Remote IP address`.", + "unknown": "Unexpected error", + "write": "Error on write request to pump. Verify your `Remote write port` or `Remote IP address`." + }, + "step": { + "user": { + "data": { + "ip_address": "Remote IP address", + "listening_port": "Local listening port", + "remote_read_port": "Remote read port", + "remote_write_port": "Remote write port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index a66e426c883..b375cb4a340 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -254,6 +254,7 @@ FLOWS = { "nexia", "nextdns", "nfandroidtv", + "nibe_heatpump", "nightscout", "nina", "nmap_tracker", diff --git a/requirements_all.txt b/requirements_all.txt index 218718ace49..5f9b0c30780 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1142,6 +1142,9 @@ nextcord==2.0.0a8 # homeassistant.components.nextdns nextdns==1.1.1 +# homeassistant.components.nibe_heatpump +nibe==0.5.0 + # homeassistant.components.niko_home_control niko-home-control==0.2.1 @@ -2369,6 +2372,9 @@ temescal==0.5 # homeassistant.components.temper temperusb==1.5.3 +# 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 255c208ccaf..073c3ebd95f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -823,6 +823,9 @@ nextcord==2.0.0a8 # homeassistant.components.nextdns nextdns==1.1.1 +# homeassistant.components.nibe_heatpump +nibe==0.5.0 + # homeassistant.components.nfandroidtv notifications-android-tv==0.1.5 @@ -1621,6 +1624,9 @@ 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/__init__.py b/tests/components/nibe_heatpump/__init__.py new file mode 100644 index 00000000000..2f440d208e7 --- /dev/null +++ b/tests/components/nibe_heatpump/__init__.py @@ -0,0 +1 @@ +"""Tests for the Nibe Heat Pump integration.""" diff --git a/tests/components/nibe_heatpump/test_config_flow.py b/tests/components/nibe_heatpump/test_config_flow.py new file mode 100644 index 00000000000..68c01bf91c8 --- /dev/null +++ b/tests/components/nibe_heatpump/test_config_flow.py @@ -0,0 +1,172 @@ +"""Test the Nibe Heat Pump config flow.""" +import errno +from unittest.mock import Mock, patch + +from nibe.coil import Coil +from nibe.connection import Connection +from nibe.exceptions import CoilNotFoundException, CoilReadException, CoilWriteException +from pytest import fixture + +from homeassistant import config_entries +from homeassistant.components.nibe_heatpump import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +MOCK_FLOW_USERDATA = { + "model": "F1155", + "ip_address": "127.0.0.1", + "listening_port": 9999, + "remote_read_port": 10000, + "remote_write_port": 10001, +} + + +@fixture(autouse=True, name="mock_connection") +async def fixture_mock_connection(): + """Make sure we have a dummy connection.""" + with patch( + "homeassistant.components.nibe_heatpump.config_flow.NibeGW", spec=Connection + ) as mock_connection: + yield mock_connection + + +@fixture(autouse=True, name="mock_setup_entry") +async def fixture_mock_setup(): + """Make sure we never actually run setup.""" + with patch( + "homeassistant.components.nibe_heatpump.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry + + +async def test_form( + hass: HomeAssistant, mock_connection: Mock, mock_setup_entry: Mock +) -> 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 + + 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 + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], MOCK_FLOW_USERDATA + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "F1155 at 127.0.0.1" + assert result2["data"] == { + "model": "F1155", + "ip_address": "127.0.0.1", + "listening_port": 9999, + "remote_read_port": 10000, + "remote_write_port": 10001, + "word_swap": True, + "connection_type": "nibegw", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_address_inuse(hass: HomeAssistant, mock_connection: Mock) -> None: + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + error = OSError() + error.errno = errno.EADDRINUSE + mock_connection.return_value.start.side_effect = error + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], MOCK_FLOW_USERDATA + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"listening_port": "address_in_use"} + + +async def test_read_timeout(hass: HomeAssistant, mock_connection: Mock) -> None: + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + mock_connection.return_value.read_coil.side_effect = CoilReadException() + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], MOCK_FLOW_USERDATA + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "read"} + + +async def test_write_timeout(hass: HomeAssistant, mock_connection: Mock) -> None: + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + mock_connection.return_value.write_coil.side_effect = CoilWriteException() + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], MOCK_FLOW_USERDATA + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "write"} + + +async def test_unexpected_exception(hass: HomeAssistant, mock_connection: Mock) -> None: + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + mock_connection.return_value.read_coil.side_effect = Exception() + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], MOCK_FLOW_USERDATA + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "unknown"} + + +async def test_invalid_ip(hass: HomeAssistant, mock_connection: Mock) -> None: + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + mock_connection.return_value.read_coil.side_effect = Exception() + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {**MOCK_FLOW_USERDATA, "ip_address": "abcd"} + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"ip_address": "address"} + + +async def test_model_missing_coil(hass: HomeAssistant, mock_connection: Mock) -> None: + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + mock_connection.return_value.read_coil.side_effect = CoilNotFoundException() + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {**MOCK_FLOW_USERDATA} + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "model"} From 713fb874a8a5b946d7ad02eb1dc805708b8c65eb Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 22 Sep 2022 08:50:08 +0200 Subject: [PATCH 640/955] Add NORMALISED_UNIT to UnitConverter (#78920) * Add NORMALISED_UNIT to UnitConverter * Adjust statistics * Rename --- .../components/recorder/statistics.py | 55 ++++++++----------- homeassistant/components/sensor/recorder.py | 26 +++++---- homeassistant/helpers/typing.py | 1 + homeassistant/util/energy.py | 2 + homeassistant/util/power.py | 2 + homeassistant/util/pressure.py | 2 + homeassistant/util/temperature.py | 2 + homeassistant/util/volume.py | 2 + 8 files changed, 49 insertions(+), 43 deletions(-) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 15309d5ab46..d1d9dd2a658 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -24,13 +24,6 @@ from sqlalchemy.sql.lambdas import StatementLambdaElement from sqlalchemy.sql.selectable import Subquery import voluptuous as vol -from homeassistant.const import ( - ENERGY_KILO_WATT_HOUR, - POWER_WATT, - PRESSURE_PA, - TEMP_CELSIUS, - VOLUME_CUBIC_METERS, -) from homeassistant.core import Event, HomeAssistant, callback, valid_entity_id from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry @@ -137,61 +130,61 @@ def _convert_energy_from_kwh(to_unit: str, value: float | None) -> float | None: """Convert energy in kWh to to_unit.""" if value is None: return None - return energy_util.convert(value, ENERGY_KILO_WATT_HOUR, to_unit) + return energy_util.convert(value, energy_util.NORMALIZED_UNIT, to_unit) def _convert_energy_to_kwh(from_unit: str, value: float) -> float: """Convert energy in from_unit to kWh.""" - return energy_util.convert(value, from_unit, ENERGY_KILO_WATT_HOUR) + return energy_util.convert(value, from_unit, energy_util.NORMALIZED_UNIT) def _convert_power_from_w(to_unit: str, value: float | None) -> float | None: """Convert power in W to to_unit.""" if value is None: return None - return power_util.convert(value, POWER_WATT, to_unit) + return power_util.convert(value, power_util.NORMALIZED_UNIT, to_unit) def _convert_pressure_from_pa(to_unit: str, value: float | None) -> float | None: """Convert pressure in Pa to to_unit.""" if value is None: return None - return pressure_util.convert(value, PRESSURE_PA, to_unit) + return pressure_util.convert(value, pressure_util.NORMALIZED_UNIT, to_unit) def _convert_temperature_from_c(to_unit: str, value: float | None) -> float | None: """Convert temperature in °C to to_unit.""" if value is None: return None - return temperature_util.convert(value, TEMP_CELSIUS, to_unit) + return temperature_util.convert(value, temperature_util.NORMALIZED_UNIT, to_unit) def _convert_volume_from_m3(to_unit: str, value: float | None) -> float | None: """Convert volume in m³ to to_unit.""" if value is None: return None - return volume_util.convert(value, VOLUME_CUBIC_METERS, to_unit) + return volume_util.convert(value, volume_util.NORMALIZED_UNIT, to_unit) def _convert_volume_to_m3(from_unit: str, value: float) -> float: """Convert volume in from_unit to m³.""" - return volume_util.convert(value, from_unit, VOLUME_CUBIC_METERS) + return volume_util.convert(value, from_unit, volume_util.NORMALIZED_UNIT) STATISTIC_UNIT_TO_UNIT_CLASS: dict[str | None, str] = { - ENERGY_KILO_WATT_HOUR: "energy", - POWER_WATT: "power", - PRESSURE_PA: "pressure", - TEMP_CELSIUS: "temperature", - VOLUME_CUBIC_METERS: "volume", + energy_util.NORMALIZED_UNIT: "energy", + power_util.NORMALIZED_UNIT: "power", + pressure_util.NORMALIZED_UNIT: "pressure", + temperature_util.NORMALIZED_UNIT: "temperature", + volume_util.NORMALIZED_UNIT: "volume", } STATISTIC_UNIT_TO_UNIT_CONVERTER: dict[str | None, UnitConverter] = { - ENERGY_KILO_WATT_HOUR: energy_util, - POWER_WATT: power_util, - PRESSURE_PA: pressure_util, - TEMP_CELSIUS: temperature_util, - VOLUME_CUBIC_METERS: volume_util, + energy_util.NORMALIZED_UNIT: energy_util, + power_util.NORMALIZED_UNIT: power_util, + pressure_util.NORMALIZED_UNIT: pressure_util, + temperature_util.NORMALIZED_UNIT: temperature_util, + volume_util.NORMALIZED_UNIT: volume_util, } # Convert energy power, pressure, temperature and volume statistics from the @@ -199,19 +192,19 @@ STATISTIC_UNIT_TO_UNIT_CONVERTER: dict[str | None, UnitConverter] = { STATISTIC_UNIT_TO_DISPLAY_UNIT_FUNCTIONS: dict[ str, Callable[[str, float | None], float | None] ] = { - ENERGY_KILO_WATT_HOUR: _convert_energy_from_kwh, - POWER_WATT: _convert_power_from_w, - PRESSURE_PA: _convert_pressure_from_pa, - TEMP_CELSIUS: _convert_temperature_from_c, - VOLUME_CUBIC_METERS: _convert_volume_from_m3, + energy_util.NORMALIZED_UNIT: _convert_energy_from_kwh, + power_util.NORMALIZED_UNIT: _convert_power_from_w, + pressure_util.NORMALIZED_UNIT: _convert_pressure_from_pa, + temperature_util.NORMALIZED_UNIT: _convert_temperature_from_c, + volume_util.NORMALIZED_UNIT: _convert_volume_from_m3, } # Convert energy and volume statistics from the display unit configured by the user # to the normalized unit used for statistics. # This is used to support adjusting statistics in the display unit DISPLAY_UNIT_TO_STATISTIC_UNIT_FUNCTIONS: dict[str, Callable[[str, float], float]] = { - ENERGY_KILO_WATT_HOUR: _convert_energy_to_kwh, - VOLUME_CUBIC_METERS: _convert_volume_to_m3, + energy_util.NORMALIZED_UNIT: _convert_energy_to_kwh, + volume_util.NORMALIZED_UNIT: _convert_volume_to_m3, } _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 40577e6962f..9bd068aa469 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -47,6 +47,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, State from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity import entity_sources +from homeassistant.helpers.typing import UnitConverter from homeassistant.util import ( dt as dt_util, energy as energy_util, @@ -75,13 +76,12 @@ DEFAULT_STATISTICS = { STATE_CLASS_TOTAL_INCREASING: {"sum"}, } -# Normalized units which will be stored in the statistics table -DEVICE_CLASS_UNITS: dict[str, str] = { - SensorDeviceClass.ENERGY: ENERGY_KILO_WATT_HOUR, - SensorDeviceClass.POWER: POWER_WATT, - SensorDeviceClass.PRESSURE: PRESSURE_PA, - SensorDeviceClass.TEMPERATURE: TEMP_CELSIUS, - SensorDeviceClass.GAS: VOLUME_CUBIC_METERS, +UNIT_CONVERTERS: dict[str, UnitConverter] = { + SensorDeviceClass.ENERGY: energy_util, + SensorDeviceClass.POWER: power_util, + SensorDeviceClass.PRESSURE: pressure_util, + SensorDeviceClass.TEMPERATURE: temperature_util, + SensorDeviceClass.GAS: volume_util, } UNIT_CONVERSIONS: dict[str, dict[str, Callable]] = { @@ -272,7 +272,7 @@ def _normalize_states( fstates.append((UNIT_CONVERSIONS[device_class][state_unit](fstate), state)) - return DEVICE_CLASS_UNITS[device_class], state_unit, fstates + return UNIT_CONVERTERS[device_class].NORMALIZED_UNIT, state_unit, fstates def _suggest_report_issue(hass: HomeAssistant, entity_id: str) -> str: @@ -503,7 +503,7 @@ def _compile_statistics( # noqa: C901 "compiled statistics (%s). Generation of long term statistics " "will be suppressed unless the unit changes back to %s. " "Go to %s to fix this", - "normalized " if device_class in DEVICE_CLASS_UNITS else "", + "normalized " if device_class in UNIT_CONVERTERS else "", entity_id, normalized_unit, old_metadata[1]["unit_of_measurement"], @@ -668,7 +668,7 @@ def list_statistic_ids( if state_unit not in UNIT_CONVERSIONS[device_class]: continue - statistics_unit = DEVICE_CLASS_UNITS[device_class] + statistics_unit = UNIT_CONVERTERS[device_class].NORMALIZED_UNIT result[state.entity_id] = { "has_mean": "mean" in provided_statistics, "has_sum": "sum" in provided_statistics, @@ -732,7 +732,7 @@ def validate_statistics( }, ) ) - elif metadata_unit != DEVICE_CLASS_UNITS[device_class]: + elif metadata_unit != UNIT_CONVERTERS[device_class].NORMALIZED_UNIT: # The unit in metadata is not supported for this device class validation_result[entity_id].append( statistics.ValidationIssue( @@ -741,7 +741,9 @@ def validate_statistics( "statistic_id": entity_id, "device_class": device_class, "metadata_unit": metadata_unit, - "supported_unit": DEVICE_CLASS_UNITS[device_class], + "supported_unit": UNIT_CONVERTERS[ + device_class + ].NORMALIZED_UNIT, }, ) ) diff --git a/homeassistant/helpers/typing.py b/homeassistant/helpers/typing.py index c679de288b1..a6b4862f03b 100644 --- a/homeassistant/helpers/typing.py +++ b/homeassistant/helpers/typing.py @@ -31,6 +31,7 @@ class UnitConverter(Protocol): """Define the format of a conversion utility.""" VALID_UNITS: tuple[str, ...] + NORMALIZED_UNIT: str def convert(self, value: float, from_unit: str, to_unit: str) -> float: """Convert one unit of measurement to another.""" diff --git a/homeassistant/util/energy.py b/homeassistant/util/energy.py index 00695704751..551b34be397 100644 --- a/homeassistant/util/energy.py +++ b/homeassistant/util/energy.py @@ -22,6 +22,8 @@ UNIT_CONVERSION: dict[str, float] = { ENERGY_MEGA_WATT_HOUR: 1 / 1000, } +NORMALIZED_UNIT = ENERGY_KILO_WATT_HOUR + def convert(value: float, from_unit: str, to_unit: str) -> float: """Convert one unit of measurement to another.""" diff --git a/homeassistant/util/power.py b/homeassistant/util/power.py index ae4f4c249b6..bafa56b38c2 100644 --- a/homeassistant/util/power.py +++ b/homeassistant/util/power.py @@ -19,6 +19,8 @@ UNIT_CONVERSION: dict[str, float] = { POWER_KILO_WATT: 1 / 1000, } +NORMALIZED_UNIT = POWER_WATT + def convert(value: float, from_unit: str, to_unit: str) -> float: """Convert one unit of measurement to another.""" diff --git a/homeassistant/util/pressure.py b/homeassistant/util/pressure.py index a07f9d777c3..b17899eee61 100644 --- a/homeassistant/util/pressure.py +++ b/homeassistant/util/pressure.py @@ -41,6 +41,8 @@ UNIT_CONVERSION: dict[str, float] = { PRESSURE_MMHG: 1 / 133.322, } +NORMALIZED_UNIT = PRESSURE_PA + def convert(value: float, from_unit: str, to_unit: str) -> float: """Convert one unit of measurement to another.""" diff --git a/homeassistant/util/temperature.py b/homeassistant/util/temperature.py index 06febd600e7..c89ce90ecf9 100644 --- a/homeassistant/util/temperature.py +++ b/homeassistant/util/temperature.py @@ -13,6 +13,8 @@ VALID_UNITS: tuple[str, ...] = ( TEMP_KELVIN, ) +NORMALIZED_UNIT = TEMP_CELSIUS + def fahrenheit_to_celsius(fahrenheit: float, interval: bool = False) -> float: """Convert a temperature in Fahrenheit to Celsius.""" diff --git a/homeassistant/util/volume.py b/homeassistant/util/volume.py index 498368e7e2b..5cc6dbc58dd 100644 --- a/homeassistant/util/volume.py +++ b/homeassistant/util/volume.py @@ -41,6 +41,8 @@ UNIT_CONVERSION: dict[str, float] = { VOLUME_CUBIC_FEET: 1 / CUBIC_FOOT_TO_CUBIC_METER, } +NORMALIZED_UNIT = VOLUME_CUBIC_METERS + def liter_to_gallon(liter: float) -> float: """Convert a volume measurement in Liter to Gallon.""" From da4ceea6471dc04740806b4e39a10ae225182db9 Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Thu, 22 Sep 2022 08:53:29 +0200 Subject: [PATCH 641/955] Bump bimmer_connected to 0.10.4 (#78910) Co-authored-by: rikroe --- homeassistant/components/bmw_connected_drive/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index f540176a837..98b6861fd49 100644 --- a/homeassistant/components/bmw_connected_drive/manifest.json +++ b/homeassistant/components/bmw_connected_drive/manifest.json @@ -2,7 +2,7 @@ "domain": "bmw_connected_drive", "name": "BMW Connected Drive", "documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive", - "requirements": ["bimmer_connected==0.10.2"], + "requirements": ["bimmer_connected==0.10.4"], "codeowners": ["@gerard33", "@rikroe"], "config_flow": true, "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 5f9b0c30780..fa9c397b511 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -403,7 +403,7 @@ beautifulsoup4==4.11.1 bellows==0.33.1 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.10.2 +bimmer_connected==0.10.4 # homeassistant.components.bizkaibus bizkaibus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 073c3ebd95f..1ff4779d3fe 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -327,7 +327,7 @@ beautifulsoup4==4.11.1 bellows==0.33.1 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.10.2 +bimmer_connected==0.10.4 # homeassistant.components.bluetooth bleak-retry-connector==1.17.1 From a286600b031b6455c1ea586c207150452e388306 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 22 Sep 2022 04:09:37 -0400 Subject: [PATCH 642/955] Add debug logging to tomorrowio and mask API key (#78915) Co-authored-by: Martin Hjelmare --- .../components/tomorrowio/__init__.py | 41 ++++++++++++++++--- homeassistant/components/tomorrowio/const.py | 4 ++ 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/tomorrowio/__init__.py b/homeassistant/components/tomorrowio/__init__.py index cef4662d1a4..adb3006b24e 100644 --- a/homeassistant/components/tomorrowio/__init__.py +++ b/homeassistant/components/tomorrowio/__init__.py @@ -3,7 +3,6 @@ from __future__ import annotations import asyncio from datetime import timedelta -import logging from math import ceil from typing import Any @@ -40,6 +39,7 @@ from .const import ( CONF_TIMESTEP, DOMAIN, INTEGRATION_NAME, + LOGGER, TMRW_ATTR_CARBON_MONOXIDE, TMRW_ATTR_CHINA_AQI, TMRW_ATTR_CHINA_HEALTH_CONCERN, @@ -78,8 +78,6 @@ from .const import ( TMRW_ATTR_WIND_SPEED, ) -_LOGGER = logging.getLogger(__name__) - PLATFORMS = [SENSOR_DOMAIN, WEATHER_DOMAIN] @@ -110,6 +108,18 @@ def async_set_update_interval( (24 * 60 * len(entries) * api.num_api_requests) / (api.max_requests_per_day * 0.9) ) + LOGGER.debug( + ( + "Number of config entries: %s\n" + "Number of API Requests per call: %s\n" + "Max requests per day: %s\n" + "Update interval: %s minutes" + ), + len(entries), + api.num_api_requests, + api.max_requests_per_day, + minutes, + ) return timedelta(minutes=minutes) @@ -126,7 +136,7 @@ def async_migrate_entry_from_climacell( data = entry.data.copy() old_config_entry_id = data.pop("old_config_entry_id") hass.config_entries.async_update_entry(entry, data=data) - _LOGGER.debug( + LOGGER.debug( ( "Setting up imported climacell entry %s for the first time as " "tomorrowio entry %s" @@ -152,7 +162,7 @@ def async_migrate_entry_from_climacell( new_device_id=device.id, ) assert entity_entry - _LOGGER.debug( + LOGGER.debug( "Migrated %s from %s to %s", entity_entry.entity_id, old_platform, @@ -238,7 +248,7 @@ class TomorrowioDataUpdateCoordinator(DataUpdateCoordinator): self.entry_id_to_location_dict: dict[str, str] = {} self._coordinator_ready: asyncio.Event | None = None - super().__init__(hass, _LOGGER, name=f"{DOMAIN}_{self._api.api_key}") + super().__init__(hass, LOGGER, name=f"{DOMAIN}_{self._api.api_key_masked}") def add_entry_to_location_dict(self, entry: ConfigEntry) -> None: """Add an entry to the location dict.""" @@ -253,9 +263,17 @@ class TomorrowioDataUpdateCoordinator(DataUpdateCoordinator): # may start setup before we finish setting the initial data and we don't want # to do multiple refreshes on startup. if self._coordinator_ready is None: + LOGGER.debug( + "Setting up coordinator for API key %s, loading data for all entries", + self._api.api_key_masked, + ) self._coordinator_ready = asyncio.Event() for entry_ in async_get_entries_by_api_key(self.hass, self._api.api_key): self.add_entry_to_location_dict(entry_) + LOGGER.debug( + "Loaded %s entries, initiating first refresh", + len(self.entry_id_to_location_dict), + ) await self.async_config_entry_first_refresh() self._coordinator_ready.set() else: @@ -265,6 +283,13 @@ class TomorrowioDataUpdateCoordinator(DataUpdateCoordinator): # don't need to schedule a refresh if entry.entry_id in self.entry_id_to_location_dict: return + LOGGER.debug( + ( + "Adding new entry to existing coordinator for API key %s, doing a " + "partial refresh" + ), + self._api.api_key_masked, + ) # We need a refresh, but it's going to be a partial refresh so we can # minimize repeat API calls self.add_entry_to_location_dict(entry) @@ -294,6 +319,10 @@ class TomorrowioDataUpdateCoordinator(DataUpdateCoordinator): ): data = self.data + LOGGER.debug( + "Fetching data for %s entries", + len(set(self.entry_id_to_location_dict) - set(data)), + ) for entry_id, location in self.entry_id_to_location_dict.items(): if entry_id in data: continue diff --git a/homeassistant/components/tomorrowio/const.py b/homeassistant/components/tomorrowio/const.py index b09cbf8adc0..e6f4d50a257 100644 --- a/homeassistant/components/tomorrowio/const.py +++ b/homeassistant/components/tomorrowio/const.py @@ -1,6 +1,8 @@ """Constants for the Tomorrow.io integration.""" from __future__ import annotations +import logging + from pytomorrowio.const import DAILY, HOURLY, NOWCAST, WeatherCode from homeassistant.components.weather import ( @@ -18,6 +20,8 @@ from homeassistant.components.weather import ( ATTR_CONDITION_WINDY, ) +LOGGER = logging.getLogger(__package__) + CONF_TIMESTEP = "timestep" FORECAST_TYPES = [DAILY, HOURLY, NOWCAST] From 9f62a2992889695fc8d15c5ab34eddaba4125183 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 22 Sep 2022 10:48:34 +0200 Subject: [PATCH 643/955] Bump actions/stale from 5 to 6.0.0 (#78922) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Franck Nijhof --- .github/workflows/stale.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index d3c30dc6506..f6f7d24fff1 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -17,7 +17,7 @@ jobs: # - No PRs marked as no-stale # - No issues marked as no-stale or help-wanted - name: 90 days stale issues & PRs policy - uses: actions/stale@v5 + uses: actions/stale@v6.0.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} days-before-stale: 90 @@ -54,7 +54,7 @@ jobs: # - No PRs marked as no-stale or new-integrations # - No issues (-1) - name: 30 days stale PRs policy - uses: actions/stale@v5 + uses: actions/stale@v6.0.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} days-before-stale: 30 @@ -79,7 +79,7 @@ jobs: # - No Issues marked as no-stale or help-wanted # - No PRs (-1) - name: Needs more information stale issues policy - uses: actions/stale@v5 + uses: actions/stale@v6.0.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} only-labels: "needs-more-information" From 27d1c1f471ce79858f50e932eff762020e5b9680 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Thu, 22 Sep 2022 12:17:04 +0200 Subject: [PATCH 644/955] Nibe Heat Pump after merge fixups (#78931) --- .../components/nibe_heatpump/__init__.py | 41 +++---- .../components/nibe_heatpump/config_flow.py | 2 +- .../components/nibe_heatpump/sensor.py | 107 ++++++++++++++---- .../components/nibe_heatpump/strings.json | 3 - .../nibe_heatpump/translations/en.json | 6 +- .../nibe_heatpump/test_config_flow.py | 10 ++ 6 files changed, 119 insertions(+), 50 deletions(-) diff --git a/homeassistant/components/nibe_heatpump/__init__.py b/homeassistant/components/nibe_heatpump/__init__.py index 343452c9f45..7285f5ce642 100644 --- a/homeassistant/components/nibe_heatpump/__init__.py +++ b/homeassistant/components/nibe_heatpump/__init__.py @@ -1,6 +1,7 @@ """The Nibe Heat Pump integration.""" from __future__ import annotations +from collections import defaultdict from datetime import timedelta from nibe.coil import Coil @@ -11,14 +12,20 @@ from nibe.heatpump import HeatPump, Model from tenacity import RetryError, retry, retry_if_exception_type, stop_after_attempt from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_IP_ADDRESS, CONF_MODEL, Platform +from homeassistant.const import ( + CONF_IP_ADDRESS, + CONF_MODEL, + EVENT_HOMEASSISTANT_STOP, + Platform, +) from homeassistant.core import CALLBACK_TYPE, HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr from homeassistant.helpers.entity import DeviceInfo, async_generate_entity_id from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, + UpdateFailed, ) from .const import ( @@ -33,6 +40,7 @@ from .const import ( ) PLATFORMS: list[Platform] = [Platform.SENSOR] +COIL_READ_RETRIES = 5 async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -40,7 +48,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: heatpump = HeatPump(Model[entry.data[CONF_MODEL]]) heatpump.word_swap = entry.data[CONF_WORD_SWAP] - heatpump.initialize() + await hass.async_add_executor_job(heatpump.initialize) connection_type = entry.data[CONF_CONNECTION_TYPE] @@ -56,17 +64,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise HomeAssistantError(f"Connection type {connection_type} is not supported.") await connection.start() + + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, connection.stop) + ) + coordinator = Coordinator(hass, heatpump, connection) data = hass.data.setdefault(DOMAIN, {}) data[entry.entry_id] = coordinator - try: - await coordinator.async_config_entry_first_refresh() - except ConfigEntryNotReady: - await connection.stop() - raise - reg = dr.async_get(hass) reg.async_get_or_create( config_entry_id=entry.entry_id, @@ -139,13 +146,8 @@ class Coordinator(DataUpdateCoordinator[dict[int, Coil]]): return float(value) return None - async def async_write_coil( - self, coil: Coil | None, value: int | float | str - ) -> None: + async def async_write_coil(self, coil: Coil, value: int | float | str) -> None: """Write coil and update state.""" - if not coil: - raise HomeAssistantError("No coil available") - coil.value = value coil = await self.connection.write_coil(coil) @@ -155,16 +157,17 @@ class Coordinator(DataUpdateCoordinator[dict[int, Coil]]): async def _async_update_data(self) -> dict[int, Coil]: @retry( - retry=retry_if_exception_type(CoilReadException), stop=stop_after_attempt(2) + 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) - callbacks: dict[int, list[CALLBACK_TYPE]] = {} + callbacks: dict[int, list[CALLBACK_TYPE]] = defaultdict(list) for update_callback, context in list(self._listeners.values()): assert isinstance(context, set) for address in context: - callbacks.setdefault(address, []).append(update_callback) + callbacks[address].append(update_callback) result: dict[int, Coil] = {} @@ -173,7 +176,7 @@ class Coordinator(DataUpdateCoordinator[dict[int, Coil]]): coil = self.heatpump.get_coil_by_address(address) self.data[coil.address] = result[coil.address] = await read_coil(coil) except (CoilReadException, RetryError) as exception: - self.logger.warning("Failed to update: %s", exception) + raise UpdateFailed(f"Failed to update: {exception}") from exception except CoilNotFoundException as exception: self.logger.debug("Skipping missing coil: %s", exception) diff --git a/homeassistant/components/nibe_heatpump/config_flow.py b/homeassistant/components/nibe_heatpump/config_flow.py index 14da4d478b2..28fafdb3a37 100644 --- a/homeassistant/components/nibe_heatpump/config_flow.py +++ b/homeassistant/components/nibe_heatpump/config_flow.py @@ -115,7 +115,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): try: info = await validate_input(self.hass, user_input) except FieldError as exception: - LOGGER.exception("Validation error") + LOGGER.debug("Validation error %s", exception) errors[exception.field] = exception.error except Exception: # pylint: disable=broad-except LOGGER.exception("Unexpected exception") diff --git a/homeassistant/components/nibe_heatpump/sensor.py b/homeassistant/components/nibe_heatpump/sensor.py index b6ea2e766a2..b0bc816dad6 100644 --- a/homeassistant/components/nibe_heatpump/sensor.py +++ b/homeassistant/components/nibe_heatpump/sensor.py @@ -7,6 +7,7 @@ from homeassistant.components.sensor import ( ENTITY_ID_FORMAT, SensorDeviceClass, SensorEntity, + SensorEntityDescription, SensorStateClass, ) from homeassistant.config_entries import ConfigEntry @@ -20,7 +21,6 @@ from homeassistant.const import ( ENERGY_WATT_HOUR, TEMP_CELSIUS, TEMP_FAHRENHEIT, - TEMP_KELVIN, TIME_HOURS, ) from homeassistant.core import HomeAssistant @@ -29,6 +29,78 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import DOMAIN, CoilEntity, Coordinator +UNIT_DESCRIPTIONS = { + "°C": SensorEntityDescription( + key="°C", + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=TEMP_CELSIUS, + ), + "°F": SensorEntityDescription( + key="°F", + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=TEMP_FAHRENHEIT, + ), + "A": SensorEntityDescription( + key="A", + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + ), + "mA": SensorEntityDescription( + key="mA", + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=ELECTRIC_CURRENT_MILLIAMPERE, + ), + "V": SensorEntityDescription( + key="V", + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + ), + "mV": SensorEntityDescription( + key="mV", + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=ELECTRIC_POTENTIAL_MILLIVOLT, + ), + "Wh": SensorEntityDescription( + key="Wh", + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.TOTAL_INCREASING, + native_unit_of_measurement=ENERGY_WATT_HOUR, + ), + "kWh": SensorEntityDescription( + key="kWh", + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.TOTAL_INCREASING, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + ), + "MWh": SensorEntityDescription( + key="MWh", + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.TOTAL_INCREASING, + native_unit_of_measurement=ENERGY_MEGA_WATT_HOUR, + ), + "h": SensorEntityDescription( + key="h", + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.DURATION, + native_unit_of_measurement=TIME_HOURS, + ), +} + async def async_setup_entry( hass: HomeAssistant, @@ -40,38 +112,27 @@ async def async_setup_entry( coordinator: Coordinator = hass.data[DOMAIN][config_entry.entry_id] async_add_entities( - Sensor(coordinator, coil) + Sensor(coordinator, coil, UNIT_DESCRIPTIONS.get(coil.unit)) for coil in coordinator.coils if not coil.is_writable and not coil.is_boolean ) -class Sensor(SensorEntity, CoilEntity): +class Sensor(CoilEntity, SensorEntity): """Sensor entity.""" - _attr_entity_category = EntityCategory.DIAGNOSTIC - - def __init__(self, coordinator: Coordinator, coil: Coil) -> None: + def __init__( + self, + coordinator: Coordinator, + coil: Coil, + entity_description: SensorEntityDescription | None, + ) -> None: """Initialize entity.""" super().__init__(coordinator, coil, ENTITY_ID_FORMAT) - self._attr_native_unit_of_measurement = coil.unit - - unit = self.native_unit_of_measurement - if unit in {TEMP_CELSIUS, TEMP_FAHRENHEIT, TEMP_KELVIN}: - self._attr_device_class = SensorDeviceClass.TEMPERATURE - elif unit in {ELECTRIC_CURRENT_AMPERE, ELECTRIC_CURRENT_MILLIAMPERE}: - self._attr_device_class = SensorDeviceClass.CURRENT - elif unit in {ELECTRIC_POTENTIAL_VOLT, ELECTRIC_POTENTIAL_MILLIVOLT}: - self._attr_device_class = SensorDeviceClass.VOLTAGE - elif unit in {ENERGY_WATT_HOUR, ENERGY_KILO_WATT_HOUR, ENERGY_MEGA_WATT_HOUR}: - self._attr_device_class = SensorDeviceClass.ENERGY - elif unit in {TIME_HOURS}: - self._attr_device_class = SensorDeviceClass.DURATION + if entity_description: + self.entity_description = entity_description else: - self._attr_device_class = None - - if unit: - self._attr_state_class = SensorStateClass.MEASUREMENT + self._attr_native_unit_of_measurement = coil.unit def _async_read_coil(self, coil: Coil): self._attr_native_value = coil.value diff --git a/homeassistant/components/nibe_heatpump/strings.json b/homeassistant/components/nibe_heatpump/strings.json index 5b31ba178b3..45e55b61083 100644 --- a/homeassistant/components/nibe_heatpump/strings.json +++ b/homeassistant/components/nibe_heatpump/strings.json @@ -17,9 +17,6 @@ "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%]" - }, - "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } } } diff --git a/homeassistant/components/nibe_heatpump/translations/en.json b/homeassistant/components/nibe_heatpump/translations/en.json index 17120e20d88..6d85cbcfcb3 100644 --- a/homeassistant/components/nibe_heatpump/translations/en.json +++ b/homeassistant/components/nibe_heatpump/translations/en.json @@ -1,11 +1,9 @@ { "config": { - "abort": { - "already_configured": "Device is already configured" - }, "error": { "address": "Invalid remote IP address specified. Address must be a IPV4 address.", - "address_in_use": "The selected listening port is already in use on this system. Reconfigure your gateway device to use a different address if the conflict can not be resolved.", + "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`.", "unknown": "Unexpected error", "write": "Error on write request to pump. Verify your `Remote write port` or `Remote IP address`." diff --git a/tests/components/nibe_heatpump/test_config_flow.py b/tests/components/nibe_heatpump/test_config_flow.py index 68c01bf91c8..2647102ba5a 100644 --- a/tests/components/nibe_heatpump/test_config_flow.py +++ b/tests/components/nibe_heatpump/test_config_flow.py @@ -91,6 +91,16 @@ async def test_address_inuse(hass: HomeAssistant, mock_connection: Mock) -> None 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 + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], MOCK_FLOW_USERDATA + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "unknown"} + async def test_read_timeout(hass: HomeAssistant, mock_connection: Mock) -> None: """Test we handle cannot connect error.""" From 6002377d4f7389f3f7415f3395d8481eff0ea502 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 22 Sep 2022 14:15:22 +0200 Subject: [PATCH 645/955] Convert UnitConverter protocol to a class (#78934) * Convert UnitConverter protocl to a class * Remove logic change * Use TypeVar * Remove NORMALIZED_UNIT from pressure * Reduce size of PR * Reduce some more * Once more * Once more * Remove DEVICE_CLASS --- homeassistant/components/number/__init__.py | 8 +-- .../components/recorder/statistics.py | 22 ++++--- homeassistant/components/sensor/__init__.py | 17 +++--- homeassistant/components/sensor/recorder.py | 21 ++++--- homeassistant/helpers/typing.py | 12 +--- homeassistant/util/unit_conversion.py | 60 +++++++++++++++++++ 6 files changed, 103 insertions(+), 37 deletions(-) create mode 100644 homeassistant/util/unit_conversion.py diff --git a/homeassistant/components/number/__init__.py b/homeassistant/components/number/__init__.py index f3e9a1d9da1..0012a4b77ff 100644 --- a/homeassistant/components/number/__init__.py +++ b/homeassistant/components/number/__init__.py @@ -28,8 +28,8 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import ExtraStoredData, RestoreEntity -from homeassistant.helpers.typing import ConfigType, UnitConverter -from homeassistant.util import temperature as temperature_util +from homeassistant.helpers.typing import ConfigType +from homeassistant.util.unit_conversion import BaseUnitConverter, TemperatureConverter from .const import ( ATTR_MAX, @@ -70,8 +70,8 @@ class NumberMode(StrEnum): SLIDER = "slider" -UNIT_CONVERTERS: dict[str, UnitConverter] = { - NumberDeviceClass.TEMPERATURE: temperature_util, +UNIT_CONVERTERS: dict[str, type[BaseUnitConverter]] = { + NumberDeviceClass.TEMPERATURE: TemperatureConverter, } # mypy: disallow-any-generics diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index d1d9dd2a658..bb38f35cad2 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -29,7 +29,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry from homeassistant.helpers.json import JSONEncoder from homeassistant.helpers.storage import STORAGE_DIR -from homeassistant.helpers.typing import UNDEFINED, UndefinedType, UnitConverter +from homeassistant.helpers.typing import UNDEFINED, UndefinedType from homeassistant.util import ( dt as dt_util, energy as energy_util, @@ -38,6 +38,14 @@ from homeassistant.util import ( temperature as temperature_util, volume as volume_util, ) +from homeassistant.util.unit_conversion import ( + BaseUnitConverter, + EnergyConverter, + PowerConverter, + PressureConverter, + TemperatureConverter, + VolumeConverter, +) from .const import DOMAIN, MAX_ROWS_TO_PURGE, SupportedDialect from .db_schema import Statistics, StatisticsMeta, StatisticsRuns, StatisticsShortTerm @@ -179,12 +187,12 @@ STATISTIC_UNIT_TO_UNIT_CLASS: dict[str | None, str] = { volume_util.NORMALIZED_UNIT: "volume", } -STATISTIC_UNIT_TO_UNIT_CONVERTER: dict[str | None, UnitConverter] = { - energy_util.NORMALIZED_UNIT: energy_util, - power_util.NORMALIZED_UNIT: power_util, - pressure_util.NORMALIZED_UNIT: pressure_util, - temperature_util.NORMALIZED_UNIT: temperature_util, - volume_util.NORMALIZED_UNIT: volume_util, +STATISTIC_UNIT_TO_UNIT_CONVERTER: dict[str | None, type[BaseUnitConverter]] = { + energy_util.NORMALIZED_UNIT: EnergyConverter, + power_util.NORMALIZED_UNIT: PowerConverter, + pressure_util.NORMALIZED_UNIT: PressureConverter, + temperature_util.NORMALIZED_UNIT: TemperatureConverter, + volume_util.NORMALIZED_UNIT: VolumeConverter, } # Convert energy power, pressure, temperature and volume statistics from the diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 530769f2873..e15bd851952 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -56,11 +56,12 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import ExtraStoredData, RestoreEntity -from homeassistant.helpers.typing import ConfigType, StateType, UnitConverter -from homeassistant.util import ( - dt as dt_util, - pressure as pressure_util, - temperature as temperature_util, +from homeassistant.helpers.typing import ConfigType, StateType +from homeassistant.util import dt as dt_util, pressure as pressure_util +from homeassistant.util.unit_conversion import ( + BaseUnitConverter, + PressureConverter, + TemperatureConverter, ) from .const import CONF_STATE_CLASS # noqa: F401 @@ -207,9 +208,9 @@ STATE_CLASS_TOTAL: Final = "total" STATE_CLASS_TOTAL_INCREASING: Final = "total_increasing" STATE_CLASSES: Final[list[str]] = [cls.value for cls in SensorStateClass] -UNIT_CONVERTERS: dict[str, UnitConverter] = { - SensorDeviceClass.PRESSURE: pressure_util, - SensorDeviceClass.TEMPERATURE: temperature_util, +UNIT_CONVERTERS: dict[str, type[BaseUnitConverter]] = { + SensorDeviceClass.PRESSURE: PressureConverter, + SensorDeviceClass.TEMPERATURE: TemperatureConverter, } UNIT_RATIOS: dict[str, dict[str, float]] = { diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 9bd068aa469..c7ace30af6e 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -47,7 +47,6 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, State from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity import entity_sources -from homeassistant.helpers.typing import UnitConverter from homeassistant.util import ( dt as dt_util, energy as energy_util, @@ -56,6 +55,14 @@ from homeassistant.util import ( temperature as temperature_util, volume as volume_util, ) +from homeassistant.util.unit_conversion import ( + BaseUnitConverter, + EnergyConverter, + PowerConverter, + PressureConverter, + TemperatureConverter, + VolumeConverter, +) from . import ( ATTR_LAST_RESET, @@ -76,12 +83,12 @@ DEFAULT_STATISTICS = { STATE_CLASS_TOTAL_INCREASING: {"sum"}, } -UNIT_CONVERTERS: dict[str, UnitConverter] = { - SensorDeviceClass.ENERGY: energy_util, - SensorDeviceClass.POWER: power_util, - SensorDeviceClass.PRESSURE: pressure_util, - SensorDeviceClass.TEMPERATURE: temperature_util, - SensorDeviceClass.GAS: volume_util, +UNIT_CONVERTERS: dict[str, type[BaseUnitConverter]] = { + SensorDeviceClass.ENERGY: EnergyConverter, + SensorDeviceClass.POWER: PowerConverter, + SensorDeviceClass.PRESSURE: PressureConverter, + SensorDeviceClass.TEMPERATURE: TemperatureConverter, + SensorDeviceClass.GAS: VolumeConverter, } UNIT_CONVERSIONS: dict[str, dict[str, Callable]] = { diff --git a/homeassistant/helpers/typing.py b/homeassistant/helpers/typing.py index a6b4862f03b..0e3edab71b0 100644 --- a/homeassistant/helpers/typing.py +++ b/homeassistant/helpers/typing.py @@ -1,7 +1,7 @@ """Typing Helpers for Home Assistant.""" from collections.abc import Mapping from enum import Enum -from typing import Any, Optional, Protocol, Union +from typing import Any, Optional, Union import homeassistant.core @@ -27,16 +27,6 @@ class UndefinedType(Enum): UNDEFINED = UndefinedType._singleton # pylint: disable=protected-access -class UnitConverter(Protocol): - """Define the format of a conversion utility.""" - - VALID_UNITS: tuple[str, ...] - NORMALIZED_UNIT: str - - def convert(self, value: float, from_unit: str, to_unit: str) -> float: - """Convert one unit of measurement to another.""" - - # The following types should not used and # are not present in the core code base. # They are kept in order not to break custom integrations diff --git a/homeassistant/util/unit_conversion.py b/homeassistant/util/unit_conversion.py new file mode 100644 index 00000000000..bc0ec09794f --- /dev/null +++ b/homeassistant/util/unit_conversion.py @@ -0,0 +1,60 @@ +"""Typing Helpers for Home Assistant.""" +from __future__ import annotations + +from collections.abc import Callable + +from . import ( + energy as energy_util, + power as power_util, + pressure as pressure_util, + temperature as temperature_util, + volume as volume_util, +) + + +class BaseUnitConverter: + """Define the format of a conversion utility.""" + + NORMALIZED_UNIT: str + VALID_UNITS: tuple[str, ...] + convert: Callable[[float, str, str], float] + + +class EnergyConverter(BaseUnitConverter): + """Utility to convert energy values.""" + + NORMALIZED_UNIT = energy_util.NORMALIZED_UNIT + VALID_UNITS = energy_util.VALID_UNITS + convert = energy_util.convert + + +class PowerConverter(BaseUnitConverter): + """Utility to convert power values.""" + + NORMALIZED_UNIT = power_util.NORMALIZED_UNIT + VALID_UNITS = power_util.VALID_UNITS + convert = power_util.convert + + +class PressureConverter(BaseUnitConverter): + """Utility to convert pressure values.""" + + NORMALIZED_UNIT = pressure_util.NORMALIZED_UNIT + VALID_UNITS = pressure_util.VALID_UNITS + convert = pressure_util.convert + + +class TemperatureConverter(BaseUnitConverter): + """Utility to convert temperature values.""" + + NORMALIZED_UNIT = temperature_util.NORMALIZED_UNIT + VALID_UNITS = temperature_util.VALID_UNITS + convert = temperature_util.convert + + +class VolumeConverter(BaseUnitConverter): + """Utility to convert volume values.""" + + NORMALIZED_UNIT = volume_util.NORMALIZED_UNIT + VALID_UNITS = volume_util.VALID_UNITS + convert = volume_util.convert From c97817bb0eaa7595d1c7fc0fffbc7192132f2e67 Mon Sep 17 00:00:00 2001 From: Jafar Atili Date: Thu, 22 Sep 2022 15:20:32 +0300 Subject: [PATCH 646/955] Add Button platform to switchbee integration (#78386) * Added Button platform to switchbee integration * fixed review comments * Addressed CR comments * Update homeassistant/components/switchbee/button.py Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> * Update homeassistant/components/switchbee/button.py Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> * Update homeassistant/components/switchbee/button.py Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> * removed the zone name from the entity name * Re-add space Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- .coveragerc | 1 + .../components/switchbee/__init__.py | 2 +- homeassistant/components/switchbee/button.py | 50 +++++++++++++++++++ .../components/switchbee/coordinator.py | 1 + 4 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/switchbee/button.py diff --git a/.coveragerc b/.coveragerc index fa31616541d..54d79c3aeda 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1218,6 +1218,7 @@ omit = homeassistant/components/swiss_public_transport/sensor.py homeassistant/components/swisscom/device_tracker.py homeassistant/components/switchbee/__init__.py + homeassistant/components/switchbee/button.py homeassistant/components/switchbee/const.py homeassistant/components/switchbee/coordinator.py homeassistant/components/switchbee/switch.py diff --git a/homeassistant/components/switchbee/__init__.py b/homeassistant/components/switchbee/__init__.py index a5523c51a7d..12cce234bf1 100644 --- a/homeassistant/components/switchbee/__init__.py +++ b/homeassistant/components/switchbee/__init__.py @@ -13,7 +13,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN from .coordinator import SwitchBeeCoordinator -PLATFORMS: list[Platform] = [Platform.SWITCH] +PLATFORMS: list[Platform] = [Platform.BUTTON, Platform.SWITCH] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/switchbee/button.py b/homeassistant/components/switchbee/button.py new file mode 100644 index 00000000000..fc2c7373b7e --- /dev/null +++ b/homeassistant/components/switchbee/button.py @@ -0,0 +1,50 @@ +"""Support for SwitchBee scenario button.""" + +from switchbee.api import SwitchBeeError +from switchbee.device import ApiStateCommand, DeviceType, SwitchBeeBaseDevice + +from homeassistant.components.button import ButtonEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import SwitchBeeCoordinator + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up Switchbee button.""" + coordinator: SwitchBeeCoordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + SwitchBeeButton(switchbee_device, coordinator) + for switchbee_device in coordinator.data.values() + if switchbee_device.type == DeviceType.Scenario + ) + + +class SwitchBeeButton(CoordinatorEntity[SwitchBeeCoordinator], ButtonEntity): + """Representation of an Switchbee button.""" + + def __init__( + self, + device: SwitchBeeBaseDevice, + coordinator: SwitchBeeCoordinator, + ) -> None: + """Initialize the Switchbee switch.""" + super().__init__(coordinator) + self._attr_name = device.name + self._device_id = device.id + self._attr_unique_id = f"{coordinator.mac_formated}-{device.id}" + + async def async_press(self) -> None: + """Fire the scenario in the SwitchBee hub.""" + try: + await self.coordinator.api.set_state(self._device_id, ApiStateCommand.ON) + except SwitchBeeError as exp: + raise HomeAssistantError( + f"Failed to fire scenario {self.name}, {str(exp)}" + ) from exp diff --git a/homeassistant/components/switchbee/coordinator.py b/homeassistant/components/switchbee/coordinator.py index d43f3602e29..72a487d812b 100644 --- a/homeassistant/components/switchbee/coordinator.py +++ b/homeassistant/components/switchbee/coordinator.py @@ -54,6 +54,7 @@ class SwitchBeeCoordinator(DataUpdateCoordinator[dict[int, SwitchBeeBaseDevice]] DeviceType.TimedSwitch, DeviceType.GroupSwitch, DeviceType.TimedPowerSwitch, + DeviceType.Scenario, ] ) except SwitchBeeError as exp: From 0767cdd9356cbc6c2a259bde49800b85231a864f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 22 Sep 2022 15:39:49 +0200 Subject: [PATCH 647/955] Move energy and power utilites to unit_conversion (#78950) * Move energy and power utilites to unit_conversion * Move tests --- .../components/recorder/statistics.py | 22 +++-- .../components/recorder/websocket_api.py | 7 +- homeassistant/components/sensor/recorder.py | 13 ++- homeassistant/util/energy.py | 42 --------- homeassistant/util/power.py | 39 -------- homeassistant/util/unit_conversion.py | 81 ++++++++++++++--- tests/util/test_energy.py | 72 --------------- tests/util/test_power.py | 41 --------- tests/util/test_unit_conversion.py | 89 +++++++++++++++++++ 9 files changed, 176 insertions(+), 230 deletions(-) delete mode 100644 homeassistant/util/energy.py delete mode 100644 homeassistant/util/power.py delete mode 100644 tests/util/test_energy.py delete mode 100644 tests/util/test_power.py create mode 100644 tests/util/test_unit_conversion.py diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index bb38f35cad2..523833dfd74 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -32,8 +32,6 @@ from homeassistant.helpers.storage import STORAGE_DIR from homeassistant.helpers.typing import UNDEFINED, UndefinedType from homeassistant.util import ( dt as dt_util, - energy as energy_util, - power as power_util, pressure as pressure_util, temperature as temperature_util, volume as volume_util, @@ -138,19 +136,19 @@ def _convert_energy_from_kwh(to_unit: str, value: float | None) -> float | None: """Convert energy in kWh to to_unit.""" if value is None: return None - return energy_util.convert(value, energy_util.NORMALIZED_UNIT, to_unit) + return EnergyConverter.convert(value, EnergyConverter.NORMALIZED_UNIT, to_unit) def _convert_energy_to_kwh(from_unit: str, value: float) -> float: """Convert energy in from_unit to kWh.""" - return energy_util.convert(value, from_unit, energy_util.NORMALIZED_UNIT) + return EnergyConverter.convert(value, from_unit, EnergyConverter.NORMALIZED_UNIT) def _convert_power_from_w(to_unit: str, value: float | None) -> float | None: """Convert power in W to to_unit.""" if value is None: return None - return power_util.convert(value, power_util.NORMALIZED_UNIT, to_unit) + return PowerConverter.convert(value, PowerConverter.NORMALIZED_UNIT, to_unit) def _convert_pressure_from_pa(to_unit: str, value: float | None) -> float | None: @@ -180,16 +178,16 @@ def _convert_volume_to_m3(from_unit: str, value: float) -> float: STATISTIC_UNIT_TO_UNIT_CLASS: dict[str | None, str] = { - energy_util.NORMALIZED_UNIT: "energy", - power_util.NORMALIZED_UNIT: "power", + EnergyConverter.NORMALIZED_UNIT: "energy", + PowerConverter.NORMALIZED_UNIT: "power", pressure_util.NORMALIZED_UNIT: "pressure", temperature_util.NORMALIZED_UNIT: "temperature", volume_util.NORMALIZED_UNIT: "volume", } STATISTIC_UNIT_TO_UNIT_CONVERTER: dict[str | None, type[BaseUnitConverter]] = { - energy_util.NORMALIZED_UNIT: EnergyConverter, - power_util.NORMALIZED_UNIT: PowerConverter, + EnergyConverter.NORMALIZED_UNIT: EnergyConverter, + PowerConverter.NORMALIZED_UNIT: PowerConverter, pressure_util.NORMALIZED_UNIT: PressureConverter, temperature_util.NORMALIZED_UNIT: TemperatureConverter, volume_util.NORMALIZED_UNIT: VolumeConverter, @@ -200,8 +198,8 @@ STATISTIC_UNIT_TO_UNIT_CONVERTER: dict[str | None, type[BaseUnitConverter]] = { STATISTIC_UNIT_TO_DISPLAY_UNIT_FUNCTIONS: dict[ str, Callable[[str, float | None], float | None] ] = { - energy_util.NORMALIZED_UNIT: _convert_energy_from_kwh, - power_util.NORMALIZED_UNIT: _convert_power_from_w, + EnergyConverter.NORMALIZED_UNIT: _convert_energy_from_kwh, + PowerConverter.NORMALIZED_UNIT: _convert_power_from_w, pressure_util.NORMALIZED_UNIT: _convert_pressure_from_pa, temperature_util.NORMALIZED_UNIT: _convert_temperature_from_c, volume_util.NORMALIZED_UNIT: _convert_volume_from_m3, @@ -211,7 +209,7 @@ STATISTIC_UNIT_TO_DISPLAY_UNIT_FUNCTIONS: dict[ # to the normalized unit used for statistics. # This is used to support adjusting statistics in the display unit DISPLAY_UNIT_TO_STATISTIC_UNIT_FUNCTIONS: dict[str, Callable[[str, float], float]] = { - energy_util.NORMALIZED_UNIT: _convert_energy_to_kwh, + EnergyConverter.NORMALIZED_UNIT: _convert_energy_to_kwh, volume_util.NORMALIZED_UNIT: _convert_volume_to_m3, } diff --git a/homeassistant/components/recorder/websocket_api.py b/homeassistant/components/recorder/websocket_api.py index 97625ba74f4..42273e11670 100644 --- a/homeassistant/components/recorder/websocket_api.py +++ b/homeassistant/components/recorder/websocket_api.py @@ -21,11 +21,10 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.json import JSON_DUMP from homeassistant.util import ( dt as dt_util, - energy as energy_util, - power as power_util, pressure as pressure_util, temperature as temperature_util, ) +from homeassistant.util.unit_conversion import EnergyConverter, PowerConverter from .const import MAX_QUEUE_BACKLOG from .statistics import ( @@ -121,8 +120,8 @@ async def ws_handle_get_statistics_during_period( vol.Required("period"): vol.Any("5minute", "hour", "day", "month"), vol.Optional("units"): vol.Schema( { - vol.Optional("energy"): vol.In(energy_util.VALID_UNITS), - vol.Optional("power"): vol.In(power_util.VALID_UNITS), + vol.Optional("energy"): vol.In(EnergyConverter.VALID_UNITS), + vol.Optional("power"): vol.In(PowerConverter.VALID_UNITS), vol.Optional("pressure"): vol.In(pressure_util.VALID_UNITS), vol.Optional("temperature"): vol.In(temperature_util.VALID_UNITS), vol.Optional("volume"): vol.Any(VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS), diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index c7ace30af6e..062ee0103d7 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -49,8 +49,6 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity import entity_sources from homeassistant.util import ( dt as dt_util, - energy as energy_util, - power as power_util, pressure as pressure_util, temperature as temperature_util, volume as volume_util, @@ -95,15 +93,16 @@ UNIT_CONVERSIONS: dict[str, dict[str, Callable]] = { # Convert energy to kWh SensorDeviceClass.ENERGY: { ENERGY_KILO_WATT_HOUR: lambda x: x - / energy_util.UNIT_CONVERSION[ENERGY_KILO_WATT_HOUR], + / EnergyConverter.UNIT_CONVERSION[ENERGY_KILO_WATT_HOUR], ENERGY_MEGA_WATT_HOUR: lambda x: x - / energy_util.UNIT_CONVERSION[ENERGY_MEGA_WATT_HOUR], - ENERGY_WATT_HOUR: lambda x: x / energy_util.UNIT_CONVERSION[ENERGY_WATT_HOUR], + / EnergyConverter.UNIT_CONVERSION[ENERGY_MEGA_WATT_HOUR], + ENERGY_WATT_HOUR: lambda x: x + / EnergyConverter.UNIT_CONVERSION[ENERGY_WATT_HOUR], }, # Convert power to W SensorDeviceClass.POWER: { - POWER_WATT: lambda x: x / power_util.UNIT_CONVERSION[POWER_WATT], - POWER_KILO_WATT: lambda x: x / power_util.UNIT_CONVERSION[POWER_KILO_WATT], + POWER_WATT: lambda x: x / PowerConverter.UNIT_CONVERSION[POWER_WATT], + POWER_KILO_WATT: lambda x: x / PowerConverter.UNIT_CONVERSION[POWER_KILO_WATT], }, # Convert pressure to Pa # Note: pressure_util.convert is bypassed to avoid redundant error checking diff --git a/homeassistant/util/energy.py b/homeassistant/util/energy.py deleted file mode 100644 index 551b34be397..00000000000 --- a/homeassistant/util/energy.py +++ /dev/null @@ -1,42 +0,0 @@ -"""Energy util functions.""" -from __future__ import annotations - -from numbers import Number - -from homeassistant.const import ( - ENERGY_KILO_WATT_HOUR, - ENERGY_MEGA_WATT_HOUR, - ENERGY_WATT_HOUR, - UNIT_NOT_RECOGNIZED_TEMPLATE, -) - -VALID_UNITS: tuple[str, ...] = ( - ENERGY_WATT_HOUR, - ENERGY_KILO_WATT_HOUR, - ENERGY_MEGA_WATT_HOUR, -) - -UNIT_CONVERSION: dict[str, float] = { - ENERGY_WATT_HOUR: 1 * 1000, - ENERGY_KILO_WATT_HOUR: 1, - ENERGY_MEGA_WATT_HOUR: 1 / 1000, -} - -NORMALIZED_UNIT = ENERGY_KILO_WATT_HOUR - - -def convert(value: float, from_unit: str, to_unit: str) -> float: - """Convert one unit of measurement to another.""" - if from_unit not in VALID_UNITS: - raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(from_unit, "energy")) - if to_unit not in VALID_UNITS: - raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(to_unit, "energy")) - - if not isinstance(value, Number): - raise TypeError(f"{value} is not of numeric type") - - if from_unit == to_unit: - return value - - watthours = value / UNIT_CONVERSION[from_unit] - return watthours * UNIT_CONVERSION[to_unit] diff --git a/homeassistant/util/power.py b/homeassistant/util/power.py deleted file mode 100644 index bafa56b38c2..00000000000 --- a/homeassistant/util/power.py +++ /dev/null @@ -1,39 +0,0 @@ -"""Power util functions.""" -from __future__ import annotations - -from numbers import Number - -from homeassistant.const import ( - POWER_KILO_WATT, - POWER_WATT, - UNIT_NOT_RECOGNIZED_TEMPLATE, -) - -VALID_UNITS: tuple[str, ...] = ( - POWER_WATT, - POWER_KILO_WATT, -) - -UNIT_CONVERSION: dict[str, float] = { - POWER_WATT: 1, - POWER_KILO_WATT: 1 / 1000, -} - -NORMALIZED_UNIT = POWER_WATT - - -def convert(value: float, from_unit: str, to_unit: str) -> float: - """Convert one unit of measurement to another.""" - if from_unit not in VALID_UNITS: - raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(from_unit, "power")) - if to_unit not in VALID_UNITS: - raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(to_unit, "power")) - - if not isinstance(value, Number): - raise TypeError(f"{value} is not of numeric type") - - if from_unit == to_unit: - return value - - watts = value / UNIT_CONVERSION[from_unit] - return watts * UNIT_CONVERSION[to_unit] diff --git a/homeassistant/util/unit_conversion.py b/homeassistant/util/unit_conversion.py index bc0ec09794f..b01d039056e 100644 --- a/homeassistant/util/unit_conversion.py +++ b/homeassistant/util/unit_conversion.py @@ -2,10 +2,21 @@ from __future__ import annotations from collections.abc import Callable +from numbers import Number + +from homeassistant.const import ( + ENERGY_KILO_WATT_HOUR, + ENERGY_MEGA_WATT_HOUR, + ENERGY_WATT_HOUR, + POWER_KILO_WATT, + POWER_WATT, + PRESSURE_PA, + TEMP_CELSIUS, + UNIT_NOT_RECOGNIZED_TEMPLATE, + VOLUME_CUBIC_METERS, +) from . import ( - energy as energy_util, - power as power_util, pressure as pressure_util, temperature as temperature_util, volume as volume_util, @@ -20,26 +31,70 @@ class BaseUnitConverter: convert: Callable[[float, str, str], float] -class EnergyConverter(BaseUnitConverter): +class BaseUnitConverterWithUnitConversion(BaseUnitConverter): + """Define the format of a conversion utility.""" + + DEVICE_CLASS: str + UNIT_CONVERSION: dict[str, float] + + @classmethod + def convert(cls, value: float, from_unit: str, to_unit: str) -> float: + """Convert one unit of measurement to another.""" + if from_unit not in cls.VALID_UNITS: + raise ValueError( + UNIT_NOT_RECOGNIZED_TEMPLATE.format(from_unit, cls.DEVICE_CLASS) + ) + if to_unit not in cls.VALID_UNITS: + raise ValueError( + UNIT_NOT_RECOGNIZED_TEMPLATE.format(to_unit, cls.DEVICE_CLASS) + ) + + if not isinstance(value, Number): + raise TypeError(f"{value} is not of numeric type") + + if from_unit == to_unit: + return value + + new_value = value / cls.UNIT_CONVERSION[from_unit] + return new_value * cls.UNIT_CONVERSION[to_unit] + + +class EnergyConverter(BaseUnitConverterWithUnitConversion): """Utility to convert energy values.""" - NORMALIZED_UNIT = energy_util.NORMALIZED_UNIT - VALID_UNITS = energy_util.VALID_UNITS - convert = energy_util.convert + DEVICE_CLASS = "energy" + NORMALIZED_UNIT = ENERGY_KILO_WATT_HOUR + UNIT_CONVERSION: dict[str, float] = { + ENERGY_WATT_HOUR: 1 * 1000, + ENERGY_KILO_WATT_HOUR: 1, + ENERGY_MEGA_WATT_HOUR: 1 / 1000, + } + VALID_UNITS: tuple[str, ...] = ( + ENERGY_WATT_HOUR, + ENERGY_KILO_WATT_HOUR, + ENERGY_MEGA_WATT_HOUR, + ) -class PowerConverter(BaseUnitConverter): +class PowerConverter(BaseUnitConverterWithUnitConversion): """Utility to convert power values.""" - NORMALIZED_UNIT = power_util.NORMALIZED_UNIT - VALID_UNITS = power_util.VALID_UNITS - convert = power_util.convert + DEVICE_CLASS = "power" + NORMALIZED_UNIT = POWER_WATT + UNIT_CONVERSION: dict[str, float] = { + POWER_WATT: 1, + POWER_KILO_WATT: 1 / 1000, + } + VALID_UNITS: tuple[str, ...] = ( + POWER_WATT, + POWER_KILO_WATT, + ) class PressureConverter(BaseUnitConverter): """Utility to convert pressure values.""" - NORMALIZED_UNIT = pressure_util.NORMALIZED_UNIT + NORMALIZED_UNIT = PRESSURE_PA VALID_UNITS = pressure_util.VALID_UNITS convert = pressure_util.convert @@ -47,7 +102,7 @@ class PressureConverter(BaseUnitConverter): class TemperatureConverter(BaseUnitConverter): """Utility to convert temperature values.""" - NORMALIZED_UNIT = temperature_util.NORMALIZED_UNIT + NORMALIZED_UNIT = TEMP_CELSIUS VALID_UNITS = temperature_util.VALID_UNITS convert = temperature_util.convert @@ -55,6 +110,6 @@ class TemperatureConverter(BaseUnitConverter): class VolumeConverter(BaseUnitConverter): """Utility to convert volume values.""" - NORMALIZED_UNIT = volume_util.NORMALIZED_UNIT + NORMALIZED_UNIT = VOLUME_CUBIC_METERS VALID_UNITS = volume_util.VALID_UNITS convert = volume_util.convert diff --git a/tests/util/test_energy.py b/tests/util/test_energy.py deleted file mode 100644 index d50bbecc7bf..00000000000 --- a/tests/util/test_energy.py +++ /dev/null @@ -1,72 +0,0 @@ -"""Test Home Assistant eneergy utility functions.""" -import pytest - -from homeassistant.const import ( - ENERGY_KILO_WATT_HOUR, - ENERGY_MEGA_WATT_HOUR, - ENERGY_WATT_HOUR, -) -import homeassistant.util.energy as energy_util - -INVALID_SYMBOL = "bob" -VALID_SYMBOL = ENERGY_KILO_WATT_HOUR - - -def test_convert_same_unit(): - """Test conversion from any unit to same unit.""" - assert energy_util.convert(2, ENERGY_WATT_HOUR, ENERGY_WATT_HOUR) == 2 - assert energy_util.convert(3, ENERGY_KILO_WATT_HOUR, ENERGY_KILO_WATT_HOUR) == 3 - assert energy_util.convert(4, ENERGY_MEGA_WATT_HOUR, ENERGY_MEGA_WATT_HOUR) == 4 - - -def test_convert_invalid_unit(): - """Test exception is thrown for invalid units.""" - with pytest.raises(ValueError): - energy_util.convert(5, INVALID_SYMBOL, VALID_SYMBOL) - - with pytest.raises(ValueError): - energy_util.convert(5, VALID_SYMBOL, INVALID_SYMBOL) - - -def test_convert_nonnumeric_value(): - """Test exception is thrown for nonnumeric type.""" - with pytest.raises(TypeError): - energy_util.convert("a", ENERGY_WATT_HOUR, ENERGY_KILO_WATT_HOUR) - - -def test_convert_from_wh(): - """Test conversion from Wh to other units.""" - watthours = 10 - assert ( - energy_util.convert(watthours, ENERGY_WATT_HOUR, ENERGY_KILO_WATT_HOUR) == 0.01 - ) - assert ( - energy_util.convert(watthours, ENERGY_WATT_HOUR, ENERGY_MEGA_WATT_HOUR) - == 0.00001 - ) - - -def test_convert_from_kwh(): - """Test conversion from kWh to other units.""" - kilowatthours = 10 - assert ( - energy_util.convert(kilowatthours, ENERGY_KILO_WATT_HOUR, ENERGY_WATT_HOUR) - == 10000 - ) - assert ( - energy_util.convert(kilowatthours, ENERGY_KILO_WATT_HOUR, ENERGY_MEGA_WATT_HOUR) - == 0.01 - ) - - -def test_convert_from_mwh(): - """Test conversion from W to other units.""" - megawatthours = 10 - assert ( - energy_util.convert(megawatthours, ENERGY_MEGA_WATT_HOUR, ENERGY_WATT_HOUR) - == 10000000 - ) - assert ( - energy_util.convert(megawatthours, ENERGY_MEGA_WATT_HOUR, ENERGY_KILO_WATT_HOUR) - == 10000 - ) diff --git a/tests/util/test_power.py b/tests/util/test_power.py deleted file mode 100644 index 89a7f0abd47..00000000000 --- a/tests/util/test_power.py +++ /dev/null @@ -1,41 +0,0 @@ -"""Test Home Assistant power utility functions.""" -import pytest - -from homeassistant.const import POWER_KILO_WATT, POWER_WATT -import homeassistant.util.power as power_util - -INVALID_SYMBOL = "bob" -VALID_SYMBOL = POWER_WATT - - -def test_convert_same_unit(): - """Test conversion from any unit to same unit.""" - assert power_util.convert(2, POWER_WATT, POWER_WATT) == 2 - assert power_util.convert(3, POWER_KILO_WATT, POWER_KILO_WATT) == 3 - - -def test_convert_invalid_unit(): - """Test exception is thrown for invalid units.""" - with pytest.raises(ValueError): - power_util.convert(5, INVALID_SYMBOL, VALID_SYMBOL) - - with pytest.raises(ValueError): - power_util.convert(5, VALID_SYMBOL, INVALID_SYMBOL) - - -def test_convert_nonnumeric_value(): - """Test exception is thrown for nonnumeric type.""" - with pytest.raises(TypeError): - power_util.convert("a", POWER_WATT, POWER_KILO_WATT) - - -def test_convert_from_kw(): - """Test conversion from kW to other units.""" - kilowatts = 10 - assert power_util.convert(kilowatts, POWER_KILO_WATT, POWER_WATT) == 10000 - - -def test_convert_from_w(): - """Test conversion from W to other units.""" - watts = 10 - assert power_util.convert(watts, POWER_WATT, POWER_KILO_WATT) == 0.01 diff --git a/tests/util/test_unit_conversion.py b/tests/util/test_unit_conversion.py new file mode 100644 index 00000000000..477152f8729 --- /dev/null +++ b/tests/util/test_unit_conversion.py @@ -0,0 +1,89 @@ +"""Test Home Assistant eneergy utility functions.""" +import pytest + +from homeassistant.const import ( + ENERGY_KILO_WATT_HOUR, + ENERGY_MEGA_WATT_HOUR, + ENERGY_WATT_HOUR, + POWER_KILO_WATT, + POWER_WATT, +) +from homeassistant.util.unit_conversion import ( + BaseUnitConverter, + EnergyConverter, + PowerConverter, +) + +INVALID_SYMBOL = "bob" + + +@pytest.mark.parametrize( + "converter,valid_unit", + [ + (EnergyConverter, ENERGY_WATT_HOUR), + (EnergyConverter, ENERGY_KILO_WATT_HOUR), + (EnergyConverter, ENERGY_MEGA_WATT_HOUR), + (PowerConverter, POWER_WATT), + (PowerConverter, POWER_KILO_WATT), + ], +) +def test_convert_same_unit(converter: type[BaseUnitConverter], valid_unit: str) -> None: + """Test conversion from any valid unit to same unit.""" + assert converter.convert(2, valid_unit, valid_unit) == 2 + + +@pytest.mark.parametrize( + "converter,valid_unit", + [ + (EnergyConverter, ENERGY_KILO_WATT_HOUR), + (PowerConverter, POWER_WATT), + ], +) +def test_convert_invalid_unit( + converter: type[BaseUnitConverter], valid_unit: str +) -> None: + """Test exception is thrown for invalid units.""" + with pytest.raises(ValueError): + converter.convert(5, INVALID_SYMBOL, valid_unit) + + with pytest.raises(ValueError): + EnergyConverter.convert(5, valid_unit, INVALID_SYMBOL) + + +@pytest.mark.parametrize( + "converter,from_unit,to_unit", + [ + (EnergyConverter, ENERGY_WATT_HOUR, ENERGY_KILO_WATT_HOUR), + (PowerConverter, POWER_WATT, POWER_KILO_WATT), + ], +) +def test_convert_nonnumeric_value( + converter: type[BaseUnitConverter], from_unit: str, to_unit: str +) -> None: + """Test exception is thrown for nonnumeric type.""" + with pytest.raises(TypeError): + converter.convert("a", from_unit, to_unit) + + +@pytest.mark.parametrize( + "converter,value,from_unit,expected,to_unit", + [ + (EnergyConverter, 10, ENERGY_WATT_HOUR, 0.01, ENERGY_KILO_WATT_HOUR), + (EnergyConverter, 10, ENERGY_WATT_HOUR, 0.00001, ENERGY_MEGA_WATT_HOUR), + (EnergyConverter, 10, ENERGY_KILO_WATT_HOUR, 10000, ENERGY_WATT_HOUR), + (EnergyConverter, 10, ENERGY_KILO_WATT_HOUR, 0.01, ENERGY_MEGA_WATT_HOUR), + (EnergyConverter, 10, ENERGY_MEGA_WATT_HOUR, 10000000, ENERGY_WATT_HOUR), + (EnergyConverter, 10, ENERGY_MEGA_WATT_HOUR, 10000, ENERGY_KILO_WATT_HOUR), + (PowerConverter, 10, POWER_KILO_WATT, 10000, POWER_WATT), + (PowerConverter, 10, POWER_WATT, 0.01, POWER_KILO_WATT), + ], +) +def test_convert( + converter: type[BaseUnitConverter], + value: float, + from_unit: str, + expected: float, + to_unit: str, +) -> None: + """Test conversion to other units.""" + assert converter.convert(value, from_unit, to_unit) == expected From 523d8d246b8ad3f72883d98e66731358bb282c55 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 22 Sep 2022 16:44:09 +0200 Subject: [PATCH 648/955] Move pressure utility to unit_conversion (#78953) --- .../components/recorder/statistics.py | 13 +- .../components/recorder/websocket_api.py | 12 +- homeassistant/components/sensor/__init__.py | 4 +- homeassistant/components/sensor/recorder.py | 17 +- homeassistant/util/pressure.py | 46 +--- homeassistant/util/unit_conversion.py | 51 +++-- tests/util/test_unit_conversion.py | 203 +++++++++++++++++- 7 files changed, 268 insertions(+), 78 deletions(-) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 523833dfd74..b7362d1a381 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -32,7 +32,6 @@ from homeassistant.helpers.storage import STORAGE_DIR from homeassistant.helpers.typing import UNDEFINED, UndefinedType from homeassistant.util import ( dt as dt_util, - pressure as pressure_util, temperature as temperature_util, volume as volume_util, ) @@ -155,7 +154,7 @@ def _convert_pressure_from_pa(to_unit: str, value: float | None) -> float | None """Convert pressure in Pa to to_unit.""" if value is None: return None - return pressure_util.convert(value, pressure_util.NORMALIZED_UNIT, to_unit) + return PressureConverter.convert(value, PressureConverter.NORMALIZED_UNIT, to_unit) def _convert_temperature_from_c(to_unit: str, value: float | None) -> float | None: @@ -178,9 +177,9 @@ def _convert_volume_to_m3(from_unit: str, value: float) -> float: STATISTIC_UNIT_TO_UNIT_CLASS: dict[str | None, str] = { - EnergyConverter.NORMALIZED_UNIT: "energy", - PowerConverter.NORMALIZED_UNIT: "power", - pressure_util.NORMALIZED_UNIT: "pressure", + EnergyConverter.NORMALIZED_UNIT: EnergyConverter.UNIT_CLASS, + PowerConverter.NORMALIZED_UNIT: PowerConverter.UNIT_CLASS, + PressureConverter.NORMALIZED_UNIT: PressureConverter.UNIT_CLASS, temperature_util.NORMALIZED_UNIT: "temperature", volume_util.NORMALIZED_UNIT: "volume", } @@ -188,7 +187,7 @@ STATISTIC_UNIT_TO_UNIT_CLASS: dict[str | None, str] = { STATISTIC_UNIT_TO_UNIT_CONVERTER: dict[str | None, type[BaseUnitConverter]] = { EnergyConverter.NORMALIZED_UNIT: EnergyConverter, PowerConverter.NORMALIZED_UNIT: PowerConverter, - pressure_util.NORMALIZED_UNIT: PressureConverter, + PressureConverter.NORMALIZED_UNIT: PressureConverter, temperature_util.NORMALIZED_UNIT: TemperatureConverter, volume_util.NORMALIZED_UNIT: VolumeConverter, } @@ -200,7 +199,7 @@ STATISTIC_UNIT_TO_DISPLAY_UNIT_FUNCTIONS: dict[ ] = { EnergyConverter.NORMALIZED_UNIT: _convert_energy_from_kwh, PowerConverter.NORMALIZED_UNIT: _convert_power_from_w, - pressure_util.NORMALIZED_UNIT: _convert_pressure_from_pa, + PressureConverter.NORMALIZED_UNIT: _convert_pressure_from_pa, temperature_util.NORMALIZED_UNIT: _convert_temperature_from_c, volume_util.NORMALIZED_UNIT: _convert_volume_from_m3, } diff --git a/homeassistant/components/recorder/websocket_api.py b/homeassistant/components/recorder/websocket_api.py index 42273e11670..0c1ede5eb0a 100644 --- a/homeassistant/components/recorder/websocket_api.py +++ b/homeassistant/components/recorder/websocket_api.py @@ -19,12 +19,12 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback, valid_entity_id from homeassistant.helpers import config_validation as cv from homeassistant.helpers.json import JSON_DUMP -from homeassistant.util import ( - dt as dt_util, - pressure as pressure_util, - temperature as temperature_util, +from homeassistant.util import dt as dt_util, temperature as temperature_util +from homeassistant.util.unit_conversion import ( + EnergyConverter, + PowerConverter, + PressureConverter, ) -from homeassistant.util.unit_conversion import EnergyConverter, PowerConverter from .const import MAX_QUEUE_BACKLOG from .statistics import ( @@ -122,7 +122,7 @@ async def ws_handle_get_statistics_during_period( { vol.Optional("energy"): vol.In(EnergyConverter.VALID_UNITS), vol.Optional("power"): vol.In(PowerConverter.VALID_UNITS), - vol.Optional("pressure"): vol.In(pressure_util.VALID_UNITS), + vol.Optional("pressure"): vol.In(PressureConverter.VALID_UNITS), vol.Optional("temperature"): vol.In(temperature_util.VALID_UNITS), vol.Optional("volume"): vol.Any(VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS), } diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index e15bd851952..ab19f053a22 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -57,7 +57,7 @@ from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import ExtraStoredData, RestoreEntity from homeassistant.helpers.typing import ConfigType, StateType -from homeassistant.util import dt as dt_util, pressure as pressure_util +from homeassistant.util import dt as dt_util from homeassistant.util.unit_conversion import ( BaseUnitConverter, PressureConverter, @@ -214,7 +214,7 @@ UNIT_CONVERTERS: dict[str, type[BaseUnitConverter]] = { } UNIT_RATIOS: dict[str, dict[str, float]] = { - SensorDeviceClass.PRESSURE: pressure_util.UNIT_CONVERSION, + SensorDeviceClass.PRESSURE: PressureConverter.UNIT_CONVERSION, SensorDeviceClass.TEMPERATURE: { TEMP_CELSIUS: 1.0, TEMP_FAHRENHEIT: 1.8, diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 062ee0103d7..a66ef54304a 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -49,7 +49,6 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity import entity_sources from homeassistant.util import ( dt as dt_util, - pressure as pressure_util, temperature as temperature_util, volume as volume_util, ) @@ -105,15 +104,15 @@ UNIT_CONVERSIONS: dict[str, dict[str, Callable]] = { POWER_KILO_WATT: lambda x: x / PowerConverter.UNIT_CONVERSION[POWER_KILO_WATT], }, # Convert pressure to Pa - # Note: pressure_util.convert is bypassed to avoid redundant error checking + # Note: PressureConverter.convert is bypassed to avoid redundant error checking SensorDeviceClass.PRESSURE: { - PRESSURE_BAR: lambda x: x / pressure_util.UNIT_CONVERSION[PRESSURE_BAR], - PRESSURE_HPA: lambda x: x / pressure_util.UNIT_CONVERSION[PRESSURE_HPA], - PRESSURE_INHG: lambda x: x / pressure_util.UNIT_CONVERSION[PRESSURE_INHG], - PRESSURE_KPA: lambda x: x / pressure_util.UNIT_CONVERSION[PRESSURE_KPA], - PRESSURE_MBAR: lambda x: x / pressure_util.UNIT_CONVERSION[PRESSURE_MBAR], - PRESSURE_PA: lambda x: x / pressure_util.UNIT_CONVERSION[PRESSURE_PA], - PRESSURE_PSI: lambda x: x / pressure_util.UNIT_CONVERSION[PRESSURE_PSI], + PRESSURE_BAR: lambda x: x / PressureConverter.UNIT_CONVERSION[PRESSURE_BAR], + PRESSURE_HPA: lambda x: x / PressureConverter.UNIT_CONVERSION[PRESSURE_HPA], + PRESSURE_INHG: lambda x: x / PressureConverter.UNIT_CONVERSION[PRESSURE_INHG], + PRESSURE_KPA: lambda x: x / PressureConverter.UNIT_CONVERSION[PRESSURE_KPA], + PRESSURE_MBAR: lambda x: x / PressureConverter.UNIT_CONVERSION[PRESSURE_MBAR], + PRESSURE_PA: lambda x: x / PressureConverter.UNIT_CONVERSION[PRESSURE_PA], + PRESSURE_PSI: lambda x: x / PressureConverter.UNIT_CONVERSION[PRESSURE_PSI], }, # Convert temperature to °C # Note: temperature_util.convert is bypassed to avoid redundant error checking diff --git a/homeassistant/util/pressure.py b/homeassistant/util/pressure.py index b17899eee61..adcadf6dfdb 100644 --- a/homeassistant/util/pressure.py +++ b/homeassistant/util/pressure.py @@ -1,9 +1,7 @@ """Pressure util functions.""" from __future__ import annotations -from numbers import Number - -from homeassistant.const import ( +from homeassistant.const import ( # pylint: disable=unused-import # noqa: F401 PRESSURE, PRESSURE_BAR, PRESSURE_CBAR, @@ -17,45 +15,13 @@ from homeassistant.const import ( UNIT_NOT_RECOGNIZED_TEMPLATE, ) -VALID_UNITS: tuple[str, ...] = ( - PRESSURE_PA, - PRESSURE_HPA, - PRESSURE_KPA, - PRESSURE_BAR, - PRESSURE_CBAR, - PRESSURE_MBAR, - PRESSURE_INHG, - PRESSURE_PSI, - PRESSURE_MMHG, -) +from .unit_conversion import PressureConverter -UNIT_CONVERSION: dict[str, float] = { - PRESSURE_PA: 1, - PRESSURE_HPA: 1 / 100, - PRESSURE_KPA: 1 / 1000, - PRESSURE_BAR: 1 / 100000, - PRESSURE_CBAR: 1 / 1000, - PRESSURE_MBAR: 1 / 100, - PRESSURE_INHG: 1 / 3386.389, - PRESSURE_PSI: 1 / 6894.757, - PRESSURE_MMHG: 1 / 133.322, -} - -NORMALIZED_UNIT = PRESSURE_PA +UNIT_CONVERSION: dict[str, float] = PressureConverter.UNIT_CONVERSION +VALID_UNITS: tuple[str, ...] = PressureConverter.VALID_UNITS def convert(value: float, from_unit: str, to_unit: str) -> float: """Convert one unit of measurement to another.""" - if from_unit not in VALID_UNITS: - raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(from_unit, PRESSURE)) - if to_unit not in VALID_UNITS: - raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(to_unit, PRESSURE)) - - if not isinstance(value, Number): - raise TypeError(f"{value} is not of numeric type") - - if from_unit == to_unit: - return value - - pascals = value / UNIT_CONVERSION[from_unit] - return pascals * UNIT_CONVERSION[to_unit] + # Need to add warning when core migration finished + return PressureConverter.convert(value, from_unit, to_unit) diff --git a/homeassistant/util/unit_conversion.py b/homeassistant/util/unit_conversion.py index b01d039056e..3d82a76e2d1 100644 --- a/homeassistant/util/unit_conversion.py +++ b/homeassistant/util/unit_conversion.py @@ -10,17 +10,21 @@ from homeassistant.const import ( ENERGY_WATT_HOUR, POWER_KILO_WATT, POWER_WATT, + PRESSURE_BAR, + PRESSURE_CBAR, + PRESSURE_HPA, + PRESSURE_INHG, + PRESSURE_KPA, + PRESSURE_MBAR, + PRESSURE_MMHG, PRESSURE_PA, + PRESSURE_PSI, TEMP_CELSIUS, UNIT_NOT_RECOGNIZED_TEMPLATE, VOLUME_CUBIC_METERS, ) -from . import ( - pressure as pressure_util, - temperature as temperature_util, - volume as volume_util, -) +from . import temperature as temperature_util, volume as volume_util class BaseUnitConverter: @@ -34,7 +38,7 @@ class BaseUnitConverter: class BaseUnitConverterWithUnitConversion(BaseUnitConverter): """Define the format of a conversion utility.""" - DEVICE_CLASS: str + UNIT_CLASS: str UNIT_CONVERSION: dict[str, float] @classmethod @@ -42,11 +46,11 @@ class BaseUnitConverterWithUnitConversion(BaseUnitConverter): """Convert one unit of measurement to another.""" if from_unit not in cls.VALID_UNITS: raise ValueError( - UNIT_NOT_RECOGNIZED_TEMPLATE.format(from_unit, cls.DEVICE_CLASS) + UNIT_NOT_RECOGNIZED_TEMPLATE.format(from_unit, cls.UNIT_CLASS) ) if to_unit not in cls.VALID_UNITS: raise ValueError( - UNIT_NOT_RECOGNIZED_TEMPLATE.format(to_unit, cls.DEVICE_CLASS) + UNIT_NOT_RECOGNIZED_TEMPLATE.format(to_unit, cls.UNIT_CLASS) ) if not isinstance(value, Number): @@ -62,7 +66,7 @@ class BaseUnitConverterWithUnitConversion(BaseUnitConverter): class EnergyConverter(BaseUnitConverterWithUnitConversion): """Utility to convert energy values.""" - DEVICE_CLASS = "energy" + UNIT_CLASS = "energy" NORMALIZED_UNIT = ENERGY_KILO_WATT_HOUR UNIT_CONVERSION: dict[str, float] = { ENERGY_WATT_HOUR: 1 * 1000, @@ -79,7 +83,7 @@ class EnergyConverter(BaseUnitConverterWithUnitConversion): class PowerConverter(BaseUnitConverterWithUnitConversion): """Utility to convert power values.""" - DEVICE_CLASS = "power" + UNIT_CLASS = "power" NORMALIZED_UNIT = POWER_WATT UNIT_CONVERSION: dict[str, float] = { POWER_WATT: 1, @@ -91,12 +95,33 @@ class PowerConverter(BaseUnitConverterWithUnitConversion): ) -class PressureConverter(BaseUnitConverter): +class PressureConverter(BaseUnitConverterWithUnitConversion): """Utility to convert pressure values.""" + UNIT_CLASS = "pressure" NORMALIZED_UNIT = PRESSURE_PA - VALID_UNITS = pressure_util.VALID_UNITS - convert = pressure_util.convert + UNIT_CONVERSION: dict[str, float] = { + PRESSURE_PA: 1, + PRESSURE_HPA: 1 / 100, + PRESSURE_KPA: 1 / 1000, + PRESSURE_BAR: 1 / 100000, + PRESSURE_CBAR: 1 / 1000, + PRESSURE_MBAR: 1 / 100, + PRESSURE_INHG: 1 / 3386.389, + PRESSURE_PSI: 1 / 6894.757, + PRESSURE_MMHG: 1 / 133.322, + } + VALID_UNITS: tuple[str, ...] = ( + PRESSURE_PA, + PRESSURE_HPA, + PRESSURE_KPA, + PRESSURE_BAR, + PRESSURE_CBAR, + PRESSURE_MBAR, + PRESSURE_INHG, + PRESSURE_PSI, + PRESSURE_MMHG, + ) class TemperatureConverter(BaseUnitConverter): diff --git a/tests/util/test_unit_conversion.py b/tests/util/test_unit_conversion.py index 477152f8729..f3dfd033be7 100644 --- a/tests/util/test_unit_conversion.py +++ b/tests/util/test_unit_conversion.py @@ -7,11 +7,20 @@ from homeassistant.const import ( ENERGY_WATT_HOUR, POWER_KILO_WATT, POWER_WATT, + PRESSURE_CBAR, + PRESSURE_HPA, + PRESSURE_INHG, + PRESSURE_KPA, + PRESSURE_MBAR, + PRESSURE_MMHG, + PRESSURE_PA, + PRESSURE_PSI, ) from homeassistant.util.unit_conversion import ( BaseUnitConverter, EnergyConverter, PowerConverter, + PressureConverter, ) INVALID_SYMBOL = "bob" @@ -25,6 +34,14 @@ INVALID_SYMBOL = "bob" (EnergyConverter, ENERGY_MEGA_WATT_HOUR), (PowerConverter, POWER_WATT), (PowerConverter, POWER_KILO_WATT), + (PressureConverter, PRESSURE_PA), + (PressureConverter, PRESSURE_HPA), + (PressureConverter, PRESSURE_MBAR), + (PressureConverter, PRESSURE_INHG), + (PressureConverter, PRESSURE_KPA), + (PressureConverter, PRESSURE_CBAR), + (PressureConverter, PRESSURE_MMHG), + (PressureConverter, PRESSURE_PSI), ], ) def test_convert_same_unit(converter: type[BaseUnitConverter], valid_unit: str) -> None: @@ -37,6 +54,7 @@ def test_convert_same_unit(converter: type[BaseUnitConverter], valid_unit: str) [ (EnergyConverter, ENERGY_KILO_WATT_HOUR), (PowerConverter, POWER_WATT), + (PressureConverter, PRESSURE_PA), ], ) def test_convert_invalid_unit( @@ -47,7 +65,7 @@ def test_convert_invalid_unit( converter.convert(5, INVALID_SYMBOL, valid_unit) with pytest.raises(ValueError): - EnergyConverter.convert(5, valid_unit, INVALID_SYMBOL) + converter.convert(5, valid_unit, INVALID_SYMBOL) @pytest.mark.parametrize( @@ -55,6 +73,7 @@ def test_convert_invalid_unit( [ (EnergyConverter, ENERGY_WATT_HOUR, ENERGY_KILO_WATT_HOUR), (PowerConverter, POWER_WATT, POWER_KILO_WATT), + (PressureConverter, PRESSURE_HPA, PRESSURE_INHG), ], ) def test_convert_nonnumeric_value( @@ -76,6 +95,188 @@ def test_convert_nonnumeric_value( (EnergyConverter, 10, ENERGY_MEGA_WATT_HOUR, 10000, ENERGY_KILO_WATT_HOUR), (PowerConverter, 10, POWER_KILO_WATT, 10000, POWER_WATT), (PowerConverter, 10, POWER_WATT, 0.01, POWER_KILO_WATT), + ( + PressureConverter, + 1000, + PRESSURE_HPA, + pytest.approx(14.5037743897), + PRESSURE_PSI, + ), + ( + PressureConverter, + 1000, + PRESSURE_HPA, + pytest.approx(29.5299801647), + PRESSURE_INHG, + ), + ( + PressureConverter, + 1000, + PRESSURE_HPA, + pytest.approx(100000), + PRESSURE_PA, + ), + ( + PressureConverter, + 1000, + PRESSURE_HPA, + pytest.approx(100), + PRESSURE_KPA, + ), + ( + PressureConverter, + 1000, + PRESSURE_HPA, + pytest.approx(1000), + PRESSURE_MBAR, + ), + ( + PressureConverter, + 1000, + PRESSURE_HPA, + pytest.approx(100), + PRESSURE_CBAR, + ), + ( + PressureConverter, + 100, + PRESSURE_KPA, + pytest.approx(14.5037743897), + PRESSURE_PSI, + ), + ( + PressureConverter, + 100, + PRESSURE_KPA, + pytest.approx(29.5299801647), + PRESSURE_INHG, + ), + ( + PressureConverter, + 100, + PRESSURE_KPA, + pytest.approx(100000), + PRESSURE_PA, + ), + ( + PressureConverter, + 100, + PRESSURE_KPA, + pytest.approx(1000), + PRESSURE_HPA, + ), + ( + PressureConverter, + 100, + PRESSURE_KPA, + pytest.approx(1000), + PRESSURE_MBAR, + ), + ( + PressureConverter, + 100, + PRESSURE_KPA, + pytest.approx(100), + PRESSURE_CBAR, + ), + ( + PressureConverter, + 30, + PRESSURE_INHG, + pytest.approx(14.7346266155), + PRESSURE_PSI, + ), + ( + PressureConverter, + 30, + PRESSURE_INHG, + pytest.approx(101.59167), + PRESSURE_KPA, + ), + ( + PressureConverter, + 30, + PRESSURE_INHG, + pytest.approx(1015.9167), + PRESSURE_HPA, + ), + ( + PressureConverter, + 30, + PRESSURE_INHG, + pytest.approx(101591.67), + PRESSURE_PA, + ), + ( + PressureConverter, + 30, + PRESSURE_INHG, + pytest.approx(1015.9167), + PRESSURE_MBAR, + ), + ( + PressureConverter, + 30, + PRESSURE_INHG, + pytest.approx(101.59167), + PRESSURE_CBAR, + ), + ( + PressureConverter, + 30, + PRESSURE_INHG, + pytest.approx(762.002), + PRESSURE_MMHG, + ), + ( + PressureConverter, + 30, + PRESSURE_MMHG, + pytest.approx(0.580102), + PRESSURE_PSI, + ), + ( + PressureConverter, + 30, + PRESSURE_MMHG, + pytest.approx(3.99966), + PRESSURE_KPA, + ), + ( + PressureConverter, + 30, + PRESSURE_MMHG, + pytest.approx(39.9966), + PRESSURE_HPA, + ), + ( + PressureConverter, + 30, + PRESSURE_MMHG, + pytest.approx(3999.66), + PRESSURE_PA, + ), + ( + PressureConverter, + 30, + PRESSURE_MMHG, + pytest.approx(39.9966), + PRESSURE_MBAR, + ), + ( + PressureConverter, + 30, + PRESSURE_MMHG, + pytest.approx(3.99966), + PRESSURE_CBAR, + ), + ( + PressureConverter, + 30, + PRESSURE_MMHG, + pytest.approx(1.181099), + PRESSURE_INHG, + ), ], ) def test_convert( From c8491c440480d4834c8b29596dde848fd95364b2 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 22 Sep 2022 17:49:45 +0200 Subject: [PATCH 649/955] Move volume utility to unit_conversion (#78955) * Move volume utility to unit_conversion * Split tests --- .../components/recorder/statistics.py | 18 +- homeassistant/components/sensor/recorder.py | 9 +- homeassistant/util/unit_conversion.py | 37 +- homeassistant/util/volume.py | 51 +-- tests/util/test_unit_conversion.py | 323 +++++++----------- 5 files changed, 182 insertions(+), 256 deletions(-) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index b7362d1a381..f2ae6ee5e70 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -30,11 +30,7 @@ from homeassistant.helpers import entity_registry from homeassistant.helpers.json import JSONEncoder from homeassistant.helpers.storage import STORAGE_DIR from homeassistant.helpers.typing import UNDEFINED, UndefinedType -from homeassistant.util import ( - dt as dt_util, - temperature as temperature_util, - volume as volume_util, -) +from homeassistant.util import dt as dt_util, temperature as temperature_util from homeassistant.util.unit_conversion import ( BaseUnitConverter, EnergyConverter, @@ -168,12 +164,12 @@ def _convert_volume_from_m3(to_unit: str, value: float | None) -> float | None: """Convert volume in m³ to to_unit.""" if value is None: return None - return volume_util.convert(value, volume_util.NORMALIZED_UNIT, to_unit) + return VolumeConverter.convert(value, VolumeConverter.NORMALIZED_UNIT, to_unit) def _convert_volume_to_m3(from_unit: str, value: float) -> float: """Convert volume in from_unit to m³.""" - return volume_util.convert(value, from_unit, volume_util.NORMALIZED_UNIT) + return VolumeConverter.convert(value, from_unit, VolumeConverter.NORMALIZED_UNIT) STATISTIC_UNIT_TO_UNIT_CLASS: dict[str | None, str] = { @@ -181,7 +177,7 @@ STATISTIC_UNIT_TO_UNIT_CLASS: dict[str | None, str] = { PowerConverter.NORMALIZED_UNIT: PowerConverter.UNIT_CLASS, PressureConverter.NORMALIZED_UNIT: PressureConverter.UNIT_CLASS, temperature_util.NORMALIZED_UNIT: "temperature", - volume_util.NORMALIZED_UNIT: "volume", + VolumeConverter.NORMALIZED_UNIT: "volume", } STATISTIC_UNIT_TO_UNIT_CONVERTER: dict[str | None, type[BaseUnitConverter]] = { @@ -189,7 +185,7 @@ STATISTIC_UNIT_TO_UNIT_CONVERTER: dict[str | None, type[BaseUnitConverter]] = { PowerConverter.NORMALIZED_UNIT: PowerConverter, PressureConverter.NORMALIZED_UNIT: PressureConverter, temperature_util.NORMALIZED_UNIT: TemperatureConverter, - volume_util.NORMALIZED_UNIT: VolumeConverter, + VolumeConverter.NORMALIZED_UNIT: VolumeConverter, } # Convert energy power, pressure, temperature and volume statistics from the @@ -201,7 +197,7 @@ STATISTIC_UNIT_TO_DISPLAY_UNIT_FUNCTIONS: dict[ PowerConverter.NORMALIZED_UNIT: _convert_power_from_w, PressureConverter.NORMALIZED_UNIT: _convert_pressure_from_pa, temperature_util.NORMALIZED_UNIT: _convert_temperature_from_c, - volume_util.NORMALIZED_UNIT: _convert_volume_from_m3, + VolumeConverter.NORMALIZED_UNIT: _convert_volume_from_m3, } # Convert energy and volume statistics from the display unit configured by the user @@ -209,7 +205,7 @@ STATISTIC_UNIT_TO_DISPLAY_UNIT_FUNCTIONS: dict[ # This is used to support adjusting statistics in the display unit DISPLAY_UNIT_TO_STATISTIC_UNIT_FUNCTIONS: dict[str, Callable[[str, float], float]] = { EnergyConverter.NORMALIZED_UNIT: _convert_energy_to_kwh, - volume_util.NORMALIZED_UNIT: _convert_volume_to_m3, + VolumeConverter.NORMALIZED_UNIT: _convert_volume_to_m3, } _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index a66ef54304a..0f028214b25 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -47,11 +47,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, State from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity import entity_sources -from homeassistant.util import ( - dt as dt_util, - temperature as temperature_util, - volume as volume_util, -) +from homeassistant.util import dt as dt_util, temperature as temperature_util from homeassistant.util.unit_conversion import ( BaseUnitConverter, EnergyConverter, @@ -124,7 +120,8 @@ UNIT_CONVERSIONS: dict[str, dict[str, Callable]] = { # Convert volume to cubic meter SensorDeviceClass.GAS: { VOLUME_CUBIC_METERS: lambda x: x, - VOLUME_CUBIC_FEET: volume_util.cubic_feet_to_cubic_meter, + VOLUME_CUBIC_FEET: lambda x: x + / VolumeConverter.UNIT_CONVERSION[VOLUME_CUBIC_FEET], }, } diff --git a/homeassistant/util/unit_conversion.py b/homeassistant/util/unit_conversion.py index 3d82a76e2d1..b2b4a8fda63 100644 --- a/homeassistant/util/unit_conversion.py +++ b/homeassistant/util/unit_conversion.py @@ -21,10 +21,23 @@ from homeassistant.const import ( PRESSURE_PSI, TEMP_CELSIUS, UNIT_NOT_RECOGNIZED_TEMPLATE, + VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS, + VOLUME_FLUID_OUNCE, + VOLUME_GALLONS, + VOLUME_LITERS, + VOLUME_MILLILITERS, ) -from . import temperature as temperature_util, volume as volume_util +from . import temperature as temperature_util +from .distance import FOOT_TO_M, IN_TO_M + +# Volume conversion constants +_L_TO_CUBIC_METER = 0.001 # 1 L = 0.001 m³ +_ML_TO_CUBIC_METER = 0.001 * _L_TO_CUBIC_METER # 1 mL = 0.001 L +_GALLON_TO_CUBIC_METER = 231 * pow(IN_TO_M, 3) # US gallon is 231 cubic inches +_FLUID_OUNCE_TO_CUBIC_METER = _GALLON_TO_CUBIC_METER / 128 # 128 fl. oz. in a US gallon +_CUBIC_FOOT_TO_CUBIC_METER = pow(FOOT_TO_M, 3) class BaseUnitConverter: @@ -132,9 +145,25 @@ class TemperatureConverter(BaseUnitConverter): convert = temperature_util.convert -class VolumeConverter(BaseUnitConverter): +class VolumeConverter(BaseUnitConverterWithUnitConversion): """Utility to convert volume values.""" + UNIT_CLASS = "volume" NORMALIZED_UNIT = VOLUME_CUBIC_METERS - VALID_UNITS = volume_util.VALID_UNITS - convert = volume_util.convert + # Units in terms of m³ + UNIT_CONVERSION: dict[str, float] = { + VOLUME_LITERS: 1 / _L_TO_CUBIC_METER, + VOLUME_MILLILITERS: 1 / _ML_TO_CUBIC_METER, + VOLUME_GALLONS: 1 / _GALLON_TO_CUBIC_METER, + VOLUME_FLUID_OUNCE: 1 / _FLUID_OUNCE_TO_CUBIC_METER, + VOLUME_CUBIC_METERS: 1, + VOLUME_CUBIC_FEET: 1 / _CUBIC_FOOT_TO_CUBIC_METER, + } + VALID_UNITS: tuple[str, ...] = ( + VOLUME_LITERS, + VOLUME_MILLILITERS, + VOLUME_GALLONS, + VOLUME_FLUID_OUNCE, + VOLUME_CUBIC_METERS, + VOLUME_CUBIC_FEET, + ) diff --git a/homeassistant/util/volume.py b/homeassistant/util/volume.py index 5cc6dbc58dd..1e1ea20fbff 100644 --- a/homeassistant/util/volume.py +++ b/homeassistant/util/volume.py @@ -1,9 +1,7 @@ """Volume conversion util functions.""" from __future__ import annotations -from numbers import Number - -from homeassistant.const import ( +from homeassistant.const import ( # pylint: disable=unused-import # noqa: F401 UNIT_NOT_RECOGNIZED_TEMPLATE, VOLUME, VOLUME_CUBIC_FEET, @@ -14,69 +12,40 @@ from homeassistant.const import ( VOLUME_MILLILITERS, ) -from .distance import FOOT_TO_M, IN_TO_M +from .unit_conversion import VolumeConverter -VALID_UNITS: tuple[str, ...] = ( - VOLUME_LITERS, - VOLUME_MILLILITERS, - VOLUME_GALLONS, - VOLUME_FLUID_OUNCE, - VOLUME_CUBIC_METERS, - VOLUME_CUBIC_FEET, -) - -L_TO_CUBIC_METER = 0.001 # 1 L = 0.001 m³ -ML_TO_CUBIC_METER = 0.001 * L_TO_CUBIC_METER # 1 mL = 0.001 L -GALLON_TO_CUBIC_METER = 231 * pow(IN_TO_M, 3) # US gallon is 231 cubic inches -FLUID_OUNCE_TO_CUBIC_METER = GALLON_TO_CUBIC_METER / 128 # 128 fl. oz. in a US gallon -CUBIC_FOOT_TO_CUBIC_METER = pow(FOOT_TO_M, 3) - -# Units in terms of m³ -UNIT_CONVERSION: dict[str, float] = { - VOLUME_LITERS: 1 / L_TO_CUBIC_METER, - VOLUME_MILLILITERS: 1 / ML_TO_CUBIC_METER, - VOLUME_GALLONS: 1 / GALLON_TO_CUBIC_METER, - VOLUME_FLUID_OUNCE: 1 / FLUID_OUNCE_TO_CUBIC_METER, - VOLUME_CUBIC_METERS: 1, - VOLUME_CUBIC_FEET: 1 / CUBIC_FOOT_TO_CUBIC_METER, -} - -NORMALIZED_UNIT = VOLUME_CUBIC_METERS +UNIT_CONVERSION = VolumeConverter.UNIT_CONVERSION +VALID_UNITS = VolumeConverter.VALID_UNITS def liter_to_gallon(liter: float) -> float: """Convert a volume measurement in Liter to Gallon.""" + # Need to add warning when core migration finished return _convert(liter, VOLUME_LITERS, VOLUME_GALLONS) def gallon_to_liter(gallon: float) -> float: """Convert a volume measurement in Gallon to Liter.""" + # Need to add warning when core migration finished return _convert(gallon, VOLUME_GALLONS, VOLUME_LITERS) def cubic_meter_to_cubic_feet(cubic_meter: float) -> float: """Convert a volume measurement in cubic meter to cubic feet.""" + # Need to add warning when core migration finished return _convert(cubic_meter, VOLUME_CUBIC_METERS, VOLUME_CUBIC_FEET) def cubic_feet_to_cubic_meter(cubic_feet: float) -> float: """Convert a volume measurement in cubic feet to cubic meter.""" + # Need to add warning when core migration finished return _convert(cubic_feet, VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS) def convert(volume: float, from_unit: str, to_unit: str) -> float: """Convert a volume from one unit to another.""" - if from_unit not in VALID_UNITS: - raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(from_unit, VOLUME)) - if to_unit not in VALID_UNITS: - raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(to_unit, VOLUME)) - - if not isinstance(volume, Number): - raise TypeError(f"{volume} is not of numeric type") - - if from_unit == to_unit: - return volume - return _convert(volume, from_unit, to_unit) + # Need to add warning when core migration finished + return VolumeConverter.convert(volume, from_unit, to_unit) def _convert(volume: float, from_unit: str, to_unit: str) -> float: diff --git a/tests/util/test_unit_conversion.py b/tests/util/test_unit_conversion.py index f3dfd033be7..b4183375926 100644 --- a/tests/util/test_unit_conversion.py +++ b/tests/util/test_unit_conversion.py @@ -15,12 +15,19 @@ from homeassistant.const import ( PRESSURE_MMHG, PRESSURE_PA, PRESSURE_PSI, + VOLUME_CUBIC_FEET, + VOLUME_CUBIC_METERS, + VOLUME_FLUID_OUNCE, + VOLUME_GALLONS, + VOLUME_LITERS, + VOLUME_MILLILITERS, ) from homeassistant.util.unit_conversion import ( BaseUnitConverter, EnergyConverter, PowerConverter, PressureConverter, + VolumeConverter, ) INVALID_SYMBOL = "bob" @@ -42,6 +49,10 @@ INVALID_SYMBOL = "bob" (PressureConverter, PRESSURE_CBAR), (PressureConverter, PRESSURE_MMHG), (PressureConverter, PRESSURE_PSI), + (VolumeConverter, VOLUME_LITERS), + (VolumeConverter, VOLUME_MILLILITERS), + (VolumeConverter, VOLUME_GALLONS), + (VolumeConverter, VOLUME_FLUID_OUNCE), ], ) def test_convert_same_unit(converter: type[BaseUnitConverter], valid_unit: str) -> None: @@ -55,6 +66,7 @@ def test_convert_same_unit(converter: type[BaseUnitConverter], valid_unit: str) (EnergyConverter, ENERGY_KILO_WATT_HOUR), (PowerConverter, POWER_WATT), (PressureConverter, PRESSURE_PA), + (VolumeConverter, VOLUME_LITERS), ], ) def test_convert_invalid_unit( @@ -74,6 +86,7 @@ def test_convert_invalid_unit( (EnergyConverter, ENERGY_WATT_HOUR, ENERGY_KILO_WATT_HOUR), (PowerConverter, POWER_WATT, POWER_KILO_WATT), (PressureConverter, PRESSURE_HPA, PRESSURE_INHG), + (VolumeConverter, VOLUME_GALLONS, VOLUME_LITERS), ], ) def test_convert_nonnumeric_value( @@ -85,206 +98,128 @@ def test_convert_nonnumeric_value( @pytest.mark.parametrize( - "converter,value,from_unit,expected,to_unit", + "value,from_unit,expected,to_unit", [ - (EnergyConverter, 10, ENERGY_WATT_HOUR, 0.01, ENERGY_KILO_WATT_HOUR), - (EnergyConverter, 10, ENERGY_WATT_HOUR, 0.00001, ENERGY_MEGA_WATT_HOUR), - (EnergyConverter, 10, ENERGY_KILO_WATT_HOUR, 10000, ENERGY_WATT_HOUR), - (EnergyConverter, 10, ENERGY_KILO_WATT_HOUR, 0.01, ENERGY_MEGA_WATT_HOUR), - (EnergyConverter, 10, ENERGY_MEGA_WATT_HOUR, 10000000, ENERGY_WATT_HOUR), - (EnergyConverter, 10, ENERGY_MEGA_WATT_HOUR, 10000, ENERGY_KILO_WATT_HOUR), - (PowerConverter, 10, POWER_KILO_WATT, 10000, POWER_WATT), - (PowerConverter, 10, POWER_WATT, 0.01, POWER_KILO_WATT), - ( - PressureConverter, - 1000, - PRESSURE_HPA, - pytest.approx(14.5037743897), - PRESSURE_PSI, - ), - ( - PressureConverter, - 1000, - PRESSURE_HPA, - pytest.approx(29.5299801647), - PRESSURE_INHG, - ), - ( - PressureConverter, - 1000, - PRESSURE_HPA, - pytest.approx(100000), - PRESSURE_PA, - ), - ( - PressureConverter, - 1000, - PRESSURE_HPA, - pytest.approx(100), - PRESSURE_KPA, - ), - ( - PressureConverter, - 1000, - PRESSURE_HPA, - pytest.approx(1000), - PRESSURE_MBAR, - ), - ( - PressureConverter, - 1000, - PRESSURE_HPA, - pytest.approx(100), - PRESSURE_CBAR, - ), - ( - PressureConverter, - 100, - PRESSURE_KPA, - pytest.approx(14.5037743897), - PRESSURE_PSI, - ), - ( - PressureConverter, - 100, - PRESSURE_KPA, - pytest.approx(29.5299801647), - PRESSURE_INHG, - ), - ( - PressureConverter, - 100, - PRESSURE_KPA, - pytest.approx(100000), - PRESSURE_PA, - ), - ( - PressureConverter, - 100, - PRESSURE_KPA, - pytest.approx(1000), - PRESSURE_HPA, - ), - ( - PressureConverter, - 100, - PRESSURE_KPA, - pytest.approx(1000), - PRESSURE_MBAR, - ), - ( - PressureConverter, - 100, - PRESSURE_KPA, - pytest.approx(100), - PRESSURE_CBAR, - ), - ( - PressureConverter, - 30, - PRESSURE_INHG, - pytest.approx(14.7346266155), - PRESSURE_PSI, - ), - ( - PressureConverter, - 30, - PRESSURE_INHG, - pytest.approx(101.59167), - PRESSURE_KPA, - ), - ( - PressureConverter, - 30, - PRESSURE_INHG, - pytest.approx(1015.9167), - PRESSURE_HPA, - ), - ( - PressureConverter, - 30, - PRESSURE_INHG, - pytest.approx(101591.67), - PRESSURE_PA, - ), - ( - PressureConverter, - 30, - PRESSURE_INHG, - pytest.approx(1015.9167), - PRESSURE_MBAR, - ), - ( - PressureConverter, - 30, - PRESSURE_INHG, - pytest.approx(101.59167), - PRESSURE_CBAR, - ), - ( - PressureConverter, - 30, - PRESSURE_INHG, - pytest.approx(762.002), - PRESSURE_MMHG, - ), - ( - PressureConverter, - 30, - PRESSURE_MMHG, - pytest.approx(0.580102), - PRESSURE_PSI, - ), - ( - PressureConverter, - 30, - PRESSURE_MMHG, - pytest.approx(3.99966), - PRESSURE_KPA, - ), - ( - PressureConverter, - 30, - PRESSURE_MMHG, - pytest.approx(39.9966), - PRESSURE_HPA, - ), - ( - PressureConverter, - 30, - PRESSURE_MMHG, - pytest.approx(3999.66), - PRESSURE_PA, - ), - ( - PressureConverter, - 30, - PRESSURE_MMHG, - pytest.approx(39.9966), - PRESSURE_MBAR, - ), - ( - PressureConverter, - 30, - PRESSURE_MMHG, - pytest.approx(3.99966), - PRESSURE_CBAR, - ), - ( - PressureConverter, - 30, - PRESSURE_MMHG, - pytest.approx(1.181099), - PRESSURE_INHG, - ), + (10, ENERGY_WATT_HOUR, 0.01, ENERGY_KILO_WATT_HOUR), + (10, ENERGY_WATT_HOUR, 0.00001, ENERGY_MEGA_WATT_HOUR), + (10, ENERGY_KILO_WATT_HOUR, 10000, ENERGY_WATT_HOUR), + (10, ENERGY_KILO_WATT_HOUR, 0.01, ENERGY_MEGA_WATT_HOUR), + (10, ENERGY_MEGA_WATT_HOUR, 10000000, ENERGY_WATT_HOUR), + (10, ENERGY_MEGA_WATT_HOUR, 10000, ENERGY_KILO_WATT_HOUR), ], ) -def test_convert( - converter: type[BaseUnitConverter], +def test_energy_convert( value: float, from_unit: str, expected: float, to_unit: str, ) -> None: """Test conversion to other units.""" - assert converter.convert(value, from_unit, to_unit) == expected + assert EnergyConverter.convert(value, from_unit, to_unit) == expected + + +@pytest.mark.parametrize( + "value,from_unit,expected,to_unit", + [ + (10, POWER_KILO_WATT, 10000, POWER_WATT), + (10, POWER_WATT, 0.01, POWER_KILO_WATT), + ], +) +def test_power_convert( + value: float, + from_unit: str, + expected: float, + to_unit: str, +) -> None: + """Test conversion to other units.""" + assert PowerConverter.convert(value, from_unit, to_unit) == expected + + +@pytest.mark.parametrize( + "value,from_unit,expected,to_unit", + [ + (1000, PRESSURE_HPA, pytest.approx(14.5037743897), PRESSURE_PSI), + (1000, PRESSURE_HPA, pytest.approx(29.5299801647), PRESSURE_INHG), + (1000, PRESSURE_HPA, pytest.approx(100000), PRESSURE_PA), + (1000, PRESSURE_HPA, pytest.approx(100), PRESSURE_KPA), + (1000, PRESSURE_HPA, pytest.approx(1000), PRESSURE_MBAR), + (1000, PRESSURE_HPA, pytest.approx(100), PRESSURE_CBAR), + (100, PRESSURE_KPA, pytest.approx(14.5037743897), PRESSURE_PSI), + (100, PRESSURE_KPA, pytest.approx(29.5299801647), PRESSURE_INHG), + (100, PRESSURE_KPA, pytest.approx(100000), PRESSURE_PA), + (100, PRESSURE_KPA, pytest.approx(1000), PRESSURE_HPA), + (100, PRESSURE_KPA, pytest.approx(1000), PRESSURE_MBAR), + (100, PRESSURE_KPA, pytest.approx(100), PRESSURE_CBAR), + (30, PRESSURE_INHG, pytest.approx(14.7346266155), PRESSURE_PSI), + (30, PRESSURE_INHG, pytest.approx(101.59167), PRESSURE_KPA), + (30, PRESSURE_INHG, pytest.approx(1015.9167), PRESSURE_HPA), + (30, PRESSURE_INHG, pytest.approx(101591.67), PRESSURE_PA), + (30, PRESSURE_INHG, pytest.approx(1015.9167), PRESSURE_MBAR), + (30, PRESSURE_INHG, pytest.approx(101.59167), PRESSURE_CBAR), + (30, PRESSURE_INHG, pytest.approx(762.002), PRESSURE_MMHG), + (30, PRESSURE_MMHG, pytest.approx(0.580102), PRESSURE_PSI), + (30, PRESSURE_MMHG, pytest.approx(3.99966), PRESSURE_KPA), + (30, PRESSURE_MMHG, pytest.approx(39.9966), PRESSURE_HPA), + (30, PRESSURE_MMHG, pytest.approx(3999.66), PRESSURE_PA), + (30, PRESSURE_MMHG, pytest.approx(39.9966), PRESSURE_MBAR), + (30, PRESSURE_MMHG, pytest.approx(3.99966), PRESSURE_CBAR), + (30, PRESSURE_MMHG, pytest.approx(1.181099), PRESSURE_INHG), + ], +) +def test_pressure_convert( + value: float, + from_unit: str, + expected: float, + to_unit: str, +) -> None: + """Test conversion to other units.""" + assert PressureConverter.convert(value, from_unit, to_unit) == expected + + +@pytest.mark.parametrize( + "value,from_unit,expected,to_unit", + [ + (5, VOLUME_LITERS, pytest.approx(1.32086), VOLUME_GALLONS), + (5, VOLUME_GALLONS, pytest.approx(18.92706), VOLUME_LITERS), + (5, VOLUME_CUBIC_METERS, pytest.approx(176.5733335), VOLUME_CUBIC_FEET), + (500, VOLUME_CUBIC_FEET, pytest.approx(14.1584233), VOLUME_CUBIC_METERS), + (500, VOLUME_CUBIC_FEET, pytest.approx(14.1584233), VOLUME_CUBIC_METERS), + (500, VOLUME_CUBIC_FEET, pytest.approx(478753.2467), VOLUME_FLUID_OUNCE), + (500, VOLUME_CUBIC_FEET, pytest.approx(3740.25974), VOLUME_GALLONS), + (500, VOLUME_CUBIC_FEET, pytest.approx(14158.42329599), VOLUME_LITERS), + (500, VOLUME_CUBIC_FEET, pytest.approx(14158423.29599), VOLUME_MILLILITERS), + (500, VOLUME_CUBIC_METERS, 500, VOLUME_CUBIC_METERS), + (500, VOLUME_CUBIC_METERS, pytest.approx(16907011.35), VOLUME_FLUID_OUNCE), + (500, VOLUME_CUBIC_METERS, pytest.approx(132086.02617), VOLUME_GALLONS), + (500, VOLUME_CUBIC_METERS, 500000, VOLUME_LITERS), + (500, VOLUME_CUBIC_METERS, 500000000, VOLUME_MILLILITERS), + (500, VOLUME_FLUID_OUNCE, pytest.approx(0.52218967), VOLUME_CUBIC_FEET), + (500, VOLUME_FLUID_OUNCE, pytest.approx(0.014786764), VOLUME_CUBIC_METERS), + (500, VOLUME_FLUID_OUNCE, 3.90625, VOLUME_GALLONS), + (500, VOLUME_FLUID_OUNCE, pytest.approx(14.786764), VOLUME_LITERS), + (500, VOLUME_FLUID_OUNCE, pytest.approx(14786.764), VOLUME_MILLILITERS), + (500, VOLUME_GALLONS, pytest.approx(66.84027), VOLUME_CUBIC_FEET), + (500, VOLUME_GALLONS, pytest.approx(1.892706), VOLUME_CUBIC_METERS), + (500, VOLUME_GALLONS, 64000, VOLUME_FLUID_OUNCE), + (500, VOLUME_GALLONS, pytest.approx(1892.70589), VOLUME_LITERS), + (500, VOLUME_GALLONS, pytest.approx(1892705.89), VOLUME_MILLILITERS), + (500, VOLUME_LITERS, pytest.approx(17.65733), VOLUME_CUBIC_FEET), + (500, VOLUME_LITERS, 0.5, VOLUME_CUBIC_METERS), + (500, VOLUME_LITERS, pytest.approx(16907.011), VOLUME_FLUID_OUNCE), + (500, VOLUME_LITERS, pytest.approx(132.086), VOLUME_GALLONS), + (500, VOLUME_LITERS, 500000, VOLUME_MILLILITERS), + (500, VOLUME_MILLILITERS, pytest.approx(0.01765733), VOLUME_CUBIC_FEET), + (500, VOLUME_MILLILITERS, 0.0005, VOLUME_CUBIC_METERS), + (500, VOLUME_MILLILITERS, pytest.approx(16.907), VOLUME_FLUID_OUNCE), + (500, VOLUME_MILLILITERS, pytest.approx(0.132086), VOLUME_GALLONS), + (500, VOLUME_MILLILITERS, 0.5, VOLUME_LITERS), + ], +) +def test_volume_convert( + value: float, + from_unit: str, + expected: float, + to_unit: str, +) -> None: + """Test conversion to other units.""" + assert VolumeConverter.convert(value, from_unit, to_unit) == expected From e46180ee76d02e98fc226fc21bcc1adfc4744554 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Thu, 22 Sep 2022 17:57:20 +0200 Subject: [PATCH 650/955] Add binary sensor platform to Nibe Heatpump (#78927) * Add binary_sensor platform * Enable binary_sensor platform * Add binary_sensor to coveragerc * Apply suggestions from code review Co-authored-by: Martin Hjelmare Co-authored-by: Martin Hjelmare --- .coveragerc | 1 + .../components/nibe_heatpump/__init__.py | 2 +- .../components/nibe_heatpump/binary_sensor.py | 41 +++++++++++++++++++ 3 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/nibe_heatpump/binary_sensor.py diff --git a/.coveragerc b/.coveragerc index 54d79c3aeda..ca2a0a5010b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -835,6 +835,7 @@ omit = homeassistant/components/nfandroidtv/notify.py homeassistant/components/nibe_heatpump/__init__.py homeassistant/components/nibe_heatpump/sensor.py + homeassistant/components/nibe_heatpump/binary_sensor.py homeassistant/components/niko_home_control/light.py homeassistant/components/nilu/air_quality.py homeassistant/components/nissan_leaf/* diff --git a/homeassistant/components/nibe_heatpump/__init__.py b/homeassistant/components/nibe_heatpump/__init__.py index 7285f5ce642..79b6b50e843 100644 --- a/homeassistant/components/nibe_heatpump/__init__.py +++ b/homeassistant/components/nibe_heatpump/__init__.py @@ -39,7 +39,7 @@ from .const import ( LOGGER, ) -PLATFORMS: list[Platform] = [Platform.SENSOR] +PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR] COIL_READ_RETRIES = 5 diff --git a/homeassistant/components/nibe_heatpump/binary_sensor.py b/homeassistant/components/nibe_heatpump/binary_sensor.py new file mode 100644 index 00000000000..dda530f8457 --- /dev/null +++ b/homeassistant/components/nibe_heatpump/binary_sensor.py @@ -0,0 +1,41 @@ +"""The Nibe Heat Pump binary sensors.""" +from __future__ import annotations + +from nibe.coil import Coil + +from homeassistant.components.binary_sensor import ENTITY_ID_FORMAT, BinarySensorEntity +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 DOMAIN, CoilEntity, Coordinator + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up platform.""" + + coordinator: Coordinator = hass.data[DOMAIN][config_entry.entry_id] + + async_add_entities( + BinarySensor(coordinator, coil) + for coil in coordinator.coils + if not coil.is_writable and coil.is_boolean + ) + + +class BinarySensor(CoilEntity, BinarySensorEntity): + """Binary sensor entity.""" + + _attr_entity_category = EntityCategory.DIAGNOSTIC + + def __init__(self, coordinator: Coordinator, coil: Coil) -> None: + """Initialize entity.""" + super().__init__(coordinator, coil, ENTITY_ID_FORMAT) + + def _async_read_coil(self, coil: Coil) -> None: + self._attr_is_on = coil.value == "ON" From 090d0041229337dfba1bbe28df2cd5e943d90006 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Thu, 22 Sep 2022 18:17:53 +0200 Subject: [PATCH 651/955] Bump motionblinds to 0.6.13 (#78946) --- homeassistant/components/motion_blinds/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/motion_blinds/manifest.json b/homeassistant/components/motion_blinds/manifest.json index 90ad330bd40..e6e4c50c7fe 100644 --- a/homeassistant/components/motion_blinds/manifest.json +++ b/homeassistant/components/motion_blinds/manifest.json @@ -3,7 +3,7 @@ "name": "Motion Blinds", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/motion_blinds", - "requirements": ["motionblinds==0.6.12"], + "requirements": ["motionblinds==0.6.13"], "dependencies": ["network"], "dhcp": [ { "registered_devices": true }, diff --git a/requirements_all.txt b/requirements_all.txt index fa9c397b511..1d8d5121d1a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1089,7 +1089,7 @@ moat-ble==0.1.1 moehlenhoff-alpha2==1.2.1 # homeassistant.components.motion_blinds -motionblinds==0.6.12 +motionblinds==0.6.13 # homeassistant.components.motioneye motioneye-client==0.3.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1ff4779d3fe..dee61850d69 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -785,7 +785,7 @@ moat-ble==0.1.1 moehlenhoff-alpha2==1.2.1 # homeassistant.components.motion_blinds -motionblinds==0.6.12 +motionblinds==0.6.13 # homeassistant.components.motioneye motioneye-client==0.3.12 From ddf56baf7ac15f456c9849223c0ed5e97c863bb6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 22 Sep 2022 18:31:50 +0200 Subject: [PATCH 652/955] Move temperature utility to unit_conversion (#78960) --- .../components/recorder/statistics.py | 14 +-- .../components/recorder/websocket_api.py | 5 +- homeassistant/components/sensor/recorder.py | 8 +- homeassistant/util/temperature.py | 59 +++------- homeassistant/util/unit_conversion.py | 102 +++++++++++++++--- tests/util/test_unit_conversion.py | 48 +++++++++ 6 files changed, 166 insertions(+), 70 deletions(-) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index f2ae6ee5e70..4e77ba84401 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -30,7 +30,7 @@ from homeassistant.helpers import entity_registry from homeassistant.helpers.json import JSONEncoder from homeassistant.helpers.storage import STORAGE_DIR from homeassistant.helpers.typing import UNDEFINED, UndefinedType -from homeassistant.util import dt as dt_util, temperature as temperature_util +from homeassistant.util import dt as dt_util from homeassistant.util.unit_conversion import ( BaseUnitConverter, EnergyConverter, @@ -157,7 +157,9 @@ def _convert_temperature_from_c(to_unit: str, value: float | None) -> float | No """Convert temperature in °C to to_unit.""" if value is None: return None - return temperature_util.convert(value, temperature_util.NORMALIZED_UNIT, to_unit) + return TemperatureConverter.convert( + value, TemperatureConverter.NORMALIZED_UNIT, to_unit + ) def _convert_volume_from_m3(to_unit: str, value: float | None) -> float | None: @@ -176,15 +178,15 @@ STATISTIC_UNIT_TO_UNIT_CLASS: dict[str | None, str] = { EnergyConverter.NORMALIZED_UNIT: EnergyConverter.UNIT_CLASS, PowerConverter.NORMALIZED_UNIT: PowerConverter.UNIT_CLASS, PressureConverter.NORMALIZED_UNIT: PressureConverter.UNIT_CLASS, - temperature_util.NORMALIZED_UNIT: "temperature", - VolumeConverter.NORMALIZED_UNIT: "volume", + TemperatureConverter.NORMALIZED_UNIT: TemperatureConverter.UNIT_CLASS, + VolumeConverter.NORMALIZED_UNIT: VolumeConverter.UNIT_CLASS, } STATISTIC_UNIT_TO_UNIT_CONVERTER: dict[str | None, type[BaseUnitConverter]] = { EnergyConverter.NORMALIZED_UNIT: EnergyConverter, PowerConverter.NORMALIZED_UNIT: PowerConverter, PressureConverter.NORMALIZED_UNIT: PressureConverter, - temperature_util.NORMALIZED_UNIT: TemperatureConverter, + TemperatureConverter.NORMALIZED_UNIT: TemperatureConverter, VolumeConverter.NORMALIZED_UNIT: VolumeConverter, } @@ -196,7 +198,7 @@ STATISTIC_UNIT_TO_DISPLAY_UNIT_FUNCTIONS: dict[ EnergyConverter.NORMALIZED_UNIT: _convert_energy_from_kwh, PowerConverter.NORMALIZED_UNIT: _convert_power_from_w, PressureConverter.NORMALIZED_UNIT: _convert_pressure_from_pa, - temperature_util.NORMALIZED_UNIT: _convert_temperature_from_c, + TemperatureConverter.NORMALIZED_UNIT: _convert_temperature_from_c, VolumeConverter.NORMALIZED_UNIT: _convert_volume_from_m3, } diff --git a/homeassistant/components/recorder/websocket_api.py b/homeassistant/components/recorder/websocket_api.py index 0c1ede5eb0a..69c96a86c10 100644 --- a/homeassistant/components/recorder/websocket_api.py +++ b/homeassistant/components/recorder/websocket_api.py @@ -19,11 +19,12 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback, valid_entity_id from homeassistant.helpers import config_validation as cv from homeassistant.helpers.json import JSON_DUMP -from homeassistant.util import dt as dt_util, temperature as temperature_util +from homeassistant.util import dt as dt_util from homeassistant.util.unit_conversion import ( EnergyConverter, PowerConverter, PressureConverter, + TemperatureConverter, ) from .const import MAX_QUEUE_BACKLOG @@ -123,7 +124,7 @@ async def ws_handle_get_statistics_during_period( vol.Optional("energy"): vol.In(EnergyConverter.VALID_UNITS), vol.Optional("power"): vol.In(PowerConverter.VALID_UNITS), vol.Optional("pressure"): vol.In(PressureConverter.VALID_UNITS), - vol.Optional("temperature"): vol.In(temperature_util.VALID_UNITS), + vol.Optional("temperature"): vol.In(TemperatureConverter.VALID_UNITS), vol.Optional("volume"): vol.Any(VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS), } ), diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 0f028214b25..5ce764cfc90 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -47,7 +47,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, State from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity import entity_sources -from homeassistant.util import dt as dt_util, temperature as temperature_util +from homeassistant.util import dt as dt_util from homeassistant.util.unit_conversion import ( BaseUnitConverter, EnergyConverter, @@ -111,11 +111,11 @@ UNIT_CONVERSIONS: dict[str, dict[str, Callable]] = { PRESSURE_PSI: lambda x: x / PressureConverter.UNIT_CONVERSION[PRESSURE_PSI], }, # Convert temperature to °C - # Note: temperature_util.convert is bypassed to avoid redundant error checking + # Note: TemperatureConverter.convert is bypassed to avoid redundant error checking SensorDeviceClass.TEMPERATURE: { TEMP_CELSIUS: lambda x: x, - TEMP_FAHRENHEIT: temperature_util.fahrenheit_to_celsius, - TEMP_KELVIN: temperature_util.kelvin_to_celsius, + TEMP_FAHRENHEIT: TemperatureConverter.fahrenheit_to_celsius, + TEMP_KELVIN: TemperatureConverter.kelvin_to_celsius, }, # Convert volume to cubic meter SensorDeviceClass.GAS: { diff --git a/homeassistant/util/temperature.py b/homeassistant/util/temperature.py index c89ce90ecf9..26ce3863519 100644 --- a/homeassistant/util/temperature.py +++ b/homeassistant/util/temperature.py @@ -1,5 +1,5 @@ """Temperature util functions.""" -from homeassistant.const import ( +from homeassistant.const import ( # pylint: disable=unused-import # noqa: F401 TEMP_CELSIUS, TEMP_FAHRENHEIT, TEMP_KELVIN, @@ -7,69 +7,40 @@ from homeassistant.const import ( UNIT_NOT_RECOGNIZED_TEMPLATE, ) -VALID_UNITS: tuple[str, ...] = ( - TEMP_CELSIUS, - TEMP_FAHRENHEIT, - TEMP_KELVIN, -) +from .unit_conversion import TemperatureConverter -NORMALIZED_UNIT = TEMP_CELSIUS +VALID_UNITS = TemperatureConverter.VALID_UNITS def fahrenheit_to_celsius(fahrenheit: float, interval: bool = False) -> float: """Convert a temperature in Fahrenheit to Celsius.""" - if interval: - return fahrenheit / 1.8 - return (fahrenheit - 32.0) / 1.8 + # Need to add warning when core migration finished + return TemperatureConverter.fahrenheit_to_celsius(fahrenheit, interval) def kelvin_to_celsius(kelvin: float, interval: bool = False) -> float: """Convert a temperature in Kelvin to Celsius.""" - if interval: - return kelvin - return kelvin - 273.15 + # Need to add warning when core migration finished + return TemperatureConverter.kelvin_to_celsius(kelvin, interval) def celsius_to_fahrenheit(celsius: float, interval: bool = False) -> float: """Convert a temperature in Celsius to Fahrenheit.""" - if interval: - return celsius * 1.8 - return celsius * 1.8 + 32.0 + # Need to add warning when core migration finished + return TemperatureConverter.celsius_to_fahrenheit(celsius, interval) def celsius_to_kelvin(celsius: float, interval: bool = False) -> float: """Convert a temperature in Celsius to Fahrenheit.""" - if interval: - return celsius - return celsius + 273.15 + # Need to add warning when core migration finished + return TemperatureConverter.celsius_to_kelvin(celsius, interval) def convert( temperature: float, from_unit: str, to_unit: str, interval: bool = False ) -> float: """Convert a temperature from one unit to another.""" - if from_unit not in VALID_UNITS: - raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(from_unit, TEMPERATURE)) - if to_unit not in VALID_UNITS: - raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(to_unit, TEMPERATURE)) - - if from_unit == to_unit: - return temperature - - if from_unit == TEMP_CELSIUS: - if to_unit == TEMP_FAHRENHEIT: - return celsius_to_fahrenheit(temperature, interval) - # kelvin - return celsius_to_kelvin(temperature, interval) - - if from_unit == TEMP_FAHRENHEIT: - if to_unit == TEMP_CELSIUS: - return fahrenheit_to_celsius(temperature, interval) - # kelvin - return celsius_to_kelvin(fahrenheit_to_celsius(temperature, interval), interval) - - # from_unit == kelvin - if to_unit == TEMP_CELSIUS: - return kelvin_to_celsius(temperature, interval) - # fahrenheit - return celsius_to_fahrenheit(kelvin_to_celsius(temperature, interval), interval) + # Need to add warning when core migration finished + return TemperatureConverter.convert( + temperature, from_unit, to_unit, interval=interval + ) diff --git a/homeassistant/util/unit_conversion.py b/homeassistant/util/unit_conversion.py index b2b4a8fda63..fbc5c05b706 100644 --- a/homeassistant/util/unit_conversion.py +++ b/homeassistant/util/unit_conversion.py @@ -1,7 +1,7 @@ """Typing Helpers for Home Assistant.""" from __future__ import annotations -from collections.abc import Callable +from abc import abstractmethod from numbers import Number from homeassistant.const import ( @@ -20,6 +20,8 @@ from homeassistant.const import ( PRESSURE_PA, PRESSURE_PSI, TEMP_CELSIUS, + TEMP_FAHRENHEIT, + TEMP_KELVIN, UNIT_NOT_RECOGNIZED_TEMPLATE, VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS, @@ -29,7 +31,6 @@ from homeassistant.const import ( VOLUME_MILLILITERS, ) -from . import temperature as temperature_util from .distance import FOOT_TO_M, IN_TO_M # Volume conversion constants @@ -43,20 +44,13 @@ _CUBIC_FOOT_TO_CUBIC_METER = pow(FOOT_TO_M, 3) class BaseUnitConverter: """Define the format of a conversion utility.""" + UNIT_CLASS: str NORMALIZED_UNIT: str VALID_UNITS: tuple[str, ...] - convert: Callable[[float, str, str], float] - - -class BaseUnitConverterWithUnitConversion(BaseUnitConverter): - """Define the format of a conversion utility.""" - - UNIT_CLASS: str - UNIT_CONVERSION: dict[str, float] @classmethod - def convert(cls, value: float, from_unit: str, to_unit: str) -> float: - """Convert one unit of measurement to another.""" + def _check_arguments(cls, value: float, from_unit: str, to_unit: str) -> None: + """Check that arguments are all valid.""" if from_unit not in cls.VALID_UNITS: raise ValueError( UNIT_NOT_RECOGNIZED_TEMPLATE.format(from_unit, cls.UNIT_CLASS) @@ -69,6 +63,22 @@ class BaseUnitConverterWithUnitConversion(BaseUnitConverter): if not isinstance(value, Number): raise TypeError(f"{value} is not of numeric type") + @classmethod + @abstractmethod + def convert(cls, value: float, from_unit: str, to_unit: str) -> float: + """Convert one unit of measurement to another.""" + + +class BaseUnitConverterWithUnitConversion(BaseUnitConverter): + """Define the format of a conversion utility.""" + + UNIT_CONVERSION: dict[str, float] + + @classmethod + def convert(cls, value: float, from_unit: str, to_unit: str) -> float: + """Convert one unit of measurement to another.""" + cls._check_arguments(value, from_unit, to_unit) + if from_unit == to_unit: return value @@ -140,9 +150,73 @@ class PressureConverter(BaseUnitConverterWithUnitConversion): class TemperatureConverter(BaseUnitConverter): """Utility to convert temperature values.""" + UNIT_CLASS = "temperature" NORMALIZED_UNIT = TEMP_CELSIUS - VALID_UNITS = temperature_util.VALID_UNITS - convert = temperature_util.convert + VALID_UNITS: tuple[str, ...] = ( + TEMP_CELSIUS, + TEMP_FAHRENHEIT, + TEMP_KELVIN, + ) + + @classmethod + def convert( + cls, value: float, from_unit: str, to_unit: str, *, interval: bool = False + ) -> float: + """Convert a temperature from one unit to another.""" + cls._check_arguments(value, from_unit, to_unit) + + if from_unit == to_unit: + return value + + if from_unit == TEMP_CELSIUS: + if to_unit == TEMP_FAHRENHEIT: + return cls.celsius_to_fahrenheit(value, interval) + # kelvin + return cls.celsius_to_kelvin(value, interval) + + if from_unit == TEMP_FAHRENHEIT: + if to_unit == TEMP_CELSIUS: + return cls.fahrenheit_to_celsius(value, interval) + # kelvin + return cls.celsius_to_kelvin( + cls.fahrenheit_to_celsius(value, interval), interval + ) + + # from_unit == kelvin + if to_unit == TEMP_CELSIUS: + return cls.kelvin_to_celsius(value, interval) + # fahrenheit + return cls.celsius_to_fahrenheit( + cls.kelvin_to_celsius(value, interval), interval + ) + + @classmethod + def fahrenheit_to_celsius(cls, fahrenheit: float, interval: bool = False) -> float: + """Convert a temperature in Fahrenheit to Celsius.""" + if interval: + return fahrenheit / 1.8 + return (fahrenheit - 32.0) / 1.8 + + @classmethod + def kelvin_to_celsius(cls, kelvin: float, interval: bool = False) -> float: + """Convert a temperature in Kelvin to Celsius.""" + if interval: + return kelvin + return kelvin - 273.15 + + @classmethod + def celsius_to_fahrenheit(cls, celsius: float, interval: bool = False) -> float: + """Convert a temperature in Celsius to Fahrenheit.""" + if interval: + return celsius * 1.8 + return celsius * 1.8 + 32.0 + + @classmethod + def celsius_to_kelvin(cls, celsius: float, interval: bool = False) -> float: + """Convert a temperature in Celsius to Kelvin.""" + if interval: + return celsius + return celsius + 273.15 class VolumeConverter(BaseUnitConverterWithUnitConversion): diff --git a/tests/util/test_unit_conversion.py b/tests/util/test_unit_conversion.py index b4183375926..3f06393be3e 100644 --- a/tests/util/test_unit_conversion.py +++ b/tests/util/test_unit_conversion.py @@ -15,6 +15,9 @@ from homeassistant.const import ( PRESSURE_MMHG, PRESSURE_PA, PRESSURE_PSI, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, + TEMP_KELVIN, VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS, VOLUME_FLUID_OUNCE, @@ -27,6 +30,7 @@ from homeassistant.util.unit_conversion import ( EnergyConverter, PowerConverter, PressureConverter, + TemperatureConverter, VolumeConverter, ) @@ -49,6 +53,9 @@ INVALID_SYMBOL = "bob" (PressureConverter, PRESSURE_CBAR), (PressureConverter, PRESSURE_MMHG), (PressureConverter, PRESSURE_PSI), + (TemperatureConverter, TEMP_CELSIUS), + (TemperatureConverter, TEMP_FAHRENHEIT), + (TemperatureConverter, TEMP_KELVIN), (VolumeConverter, VOLUME_LITERS), (VolumeConverter, VOLUME_MILLILITERS), (VolumeConverter, VOLUME_GALLONS), @@ -66,6 +73,7 @@ def test_convert_same_unit(converter: type[BaseUnitConverter], valid_unit: str) (EnergyConverter, ENERGY_KILO_WATT_HOUR), (PowerConverter, POWER_WATT), (PressureConverter, PRESSURE_PA), + (TemperatureConverter, TEMP_CELSIUS), (VolumeConverter, VOLUME_LITERS), ], ) @@ -86,6 +94,7 @@ def test_convert_invalid_unit( (EnergyConverter, ENERGY_WATT_HOUR, ENERGY_KILO_WATT_HOUR), (PowerConverter, POWER_WATT, POWER_KILO_WATT), (PressureConverter, PRESSURE_HPA, PRESSURE_INHG), + (TemperatureConverter, TEMP_CELSIUS, TEMP_FAHRENHEIT), (VolumeConverter, VOLUME_GALLONS, VOLUME_LITERS), ], ) @@ -176,6 +185,45 @@ def test_pressure_convert( assert PressureConverter.convert(value, from_unit, to_unit) == expected +@pytest.mark.parametrize( + "value,from_unit,expected,to_unit", + [ + (100, TEMP_CELSIUS, 212, TEMP_FAHRENHEIT), + (100, TEMP_CELSIUS, 373.15, TEMP_KELVIN), + (100, TEMP_FAHRENHEIT, pytest.approx(37.77777777777778), TEMP_CELSIUS), + (100, TEMP_FAHRENHEIT, pytest.approx(310.92777777777775), TEMP_KELVIN), + (100, TEMP_KELVIN, pytest.approx(-173.15), TEMP_CELSIUS), + (100, TEMP_KELVIN, pytest.approx(-279.66999999999996), TEMP_FAHRENHEIT), + ], +) +def test_temperature_convert( + value: float, from_unit: str, expected: float, to_unit: str +) -> None: + """Test conversion to other units.""" + assert TemperatureConverter.convert(value, from_unit, to_unit) == expected + + +@pytest.mark.parametrize( + "value,from_unit,expected,to_unit", + [ + (100, TEMP_CELSIUS, 180, TEMP_FAHRENHEIT), + (100, TEMP_CELSIUS, 100, TEMP_KELVIN), + (100, TEMP_FAHRENHEIT, pytest.approx(55.55555555555556), TEMP_CELSIUS), + (100, TEMP_FAHRENHEIT, pytest.approx(55.55555555555556), TEMP_KELVIN), + (100, TEMP_KELVIN, 100, TEMP_CELSIUS), + (100, TEMP_KELVIN, 180, TEMP_FAHRENHEIT), + ], +) +def test_temperature_convert_with_interval( + value: float, from_unit: str, expected: float, to_unit: str +) -> None: + """Test conversion to other units.""" + assert ( + TemperatureConverter.convert(value, from_unit, to_unit, interval=True) + == expected + ) + + @pytest.mark.parametrize( "value,from_unit,expected,to_unit", [ From 5c7d40cccf473c3549900949fe410dbe9d2e1a19 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 22 Sep 2022 18:55:57 +0200 Subject: [PATCH 653/955] Rename property in Plugwise EntityDescription (#78935) --- homeassistant/components/plugwise/select.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/plugwise/select.py b/homeassistant/components/plugwise/select.py index 7afe76e1a8a..989f56adcf3 100644 --- a/homeassistant/components/plugwise/select.py +++ b/homeassistant/components/plugwise/select.py @@ -24,8 +24,8 @@ class PlugwiseSelectDescriptionMixin: """Mixin values for Plugwise Select entities.""" command: Callable[[Smile, str, str], Awaitable[Any]] - current_option: str - options: str + current_option_key: str + options_key: str @dataclass @@ -41,8 +41,8 @@ SELECT_TYPES = ( name="Thermostat schedule", icon="mdi:calendar-clock", command=lambda api, loc, opt: api.set_schedule_state(loc, opt, STATE_ON), - current_option="selected_schedule", - options="available_schedules", + current_option_key="selected_schedule", + options_key="available_schedules", ), PlugwiseSelectEntityDescription( key="select_regulation_mode", @@ -50,8 +50,8 @@ SELECT_TYPES = ( icon="mdi:hvac", entity_category=EntityCategory.CONFIG, command=lambda api, loc, opt: api.set_regulation_mode(opt), - current_option="regulation_mode", - options="regulation_modes", + current_option_key="regulation_mode", + options_key="regulation_modes", ), ) @@ -69,7 +69,10 @@ async def async_setup_entry( entities: list[PlugwiseSelectEntity] = [] for device_id, device in coordinator.data.devices.items(): for description in SELECT_TYPES: - if description.options in device and len(device[description.options]) > 1: + if ( + description.options_key in device + and len(device[description.options_key]) > 1 + ): entities.append( PlugwiseSelectEntity(coordinator, device_id, description) ) @@ -96,12 +99,12 @@ class PlugwiseSelectEntity(PlugwiseEntity, SelectEntity): @property def current_option(self) -> str: """Return the selected entity option to represent the entity state.""" - return self.device[self.entity_description.current_option] + return self.device[self.entity_description.current_option_key] @property def options(self) -> list[str]: """Return the selectable entity options.""" - return self.device[self.entity_description.options] + return self.device[self.entity_description.options_key] async def async_select_option(self, option: str) -> None: """Change to the selected entity option.""" From 9692cdaf2deae2690b67262c2f3a0f2c22462740 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 22 Sep 2022 21:02:31 +0200 Subject: [PATCH 654/955] Make _is_valid_unit private in unit system (#78924) --- homeassistant/util/unit_system.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/util/unit_system.py b/homeassistant/util/unit_system.py index e964fee798c..5d8334c6686 100644 --- a/homeassistant/util/unit_system.py +++ b/homeassistant/util/unit_system.py @@ -53,7 +53,7 @@ WIND_SPEED_UNITS = speed_util.VALID_UNITS TEMPERATURE_UNITS: tuple[str, ...] = (TEMP_FAHRENHEIT, TEMP_CELSIUS) -def is_valid_unit(unit: str, unit_type: str) -> bool: +def _is_valid_unit(unit: str, unit_type: str) -> bool: """Check if the unit is valid for it's type.""" if unit_type == LENGTH: units = LENGTH_UNITS @@ -101,7 +101,7 @@ class UnitSystem: (mass, MASS), (pressure, PRESSURE), ) - if not is_valid_unit(unit, unit_type) + if not _is_valid_unit(unit, unit_type) ) if errors: From 48744bfd6828511cef5cbd9678b0e8e708ba54ef Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 22 Sep 2022 13:19:33 -0600 Subject: [PATCH 655/955] Replace RainMachine freeze protection temperature sensor with a select (#76484) * Migrate two RainMachine binary sensors to config-category switches * Removal * Replace RainMachine freeze protection temperature sensor with a select * Fix CI * Show options in current unit system * Have message include what entity is replacing this sensor * Don't define a method for every dataclass instance * Add issue registry through helper * Breaking change -> deprecation * Naming * Translations * Remove extraneous list * Don't swallow exception * Don't be prematurely defensive * Better Repairs instructions --- .coveragerc | 1 + .../components/rainmachine/__init__.py | 1 + .../components/rainmachine/select.py | 154 ++++++++++++++++++ .../components/rainmachine/sensor.py | 23 ++- .../components/rainmachine/strings.json | 13 ++ .../rainmachine/translations/en.json | 13 ++ homeassistant/components/rainmachine/util.py | 61 ++++++- 7 files changed, 263 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/rainmachine/select.py diff --git a/.coveragerc b/.coveragerc index ca2a0a5010b..6cff79350d2 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1015,6 +1015,7 @@ omit = homeassistant/components/rainmachine/binary_sensor.py homeassistant/components/rainmachine/button.py homeassistant/components/rainmachine/model.py + homeassistant/components/rainmachine/select.py homeassistant/components/rainmachine/sensor.py homeassistant/components/rainmachine/switch.py homeassistant/components/rainmachine/update.py diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index 4d19dbc7bfc..ff52b74ab16 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -58,6 +58,7 @@ CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) PLATFORMS = [ Platform.BINARY_SENSOR, Platform.BUTTON, + Platform.SELECT, Platform.SENSOR, Platform.SWITCH, Platform.UPDATE, diff --git a/homeassistant/components/rainmachine/select.py b/homeassistant/components/rainmachine/select.py new file mode 100644 index 00000000000..82dddfb8f3a --- /dev/null +++ b/homeassistant/components/rainmachine/select.py @@ -0,0 +1,154 @@ +"""Support for RainMachine selects.""" +from __future__ import annotations + +from dataclasses import dataclass + +from regenmaschine.errors import RainMachineError + +from homeassistant.components.select import SelectEntity, SelectEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_UNIT_SYSTEM_IMPERIAL +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import RainMachineData, RainMachineEntity +from .const import DATA_RESTRICTIONS_UNIVERSAL, DOMAIN +from .model import ( + RainMachineEntityDescription, + RainMachineEntityDescriptionMixinDataKey, +) +from .util import key_exists + + +@dataclass +class RainMachineSelectDescription( + SelectEntityDescription, + RainMachineEntityDescription, + RainMachineEntityDescriptionMixinDataKey, +): + """Describe a generic RainMachine select.""" + + +@dataclass +class FreezeProtectionSelectOption: + """Define an option for a freeze selection select.""" + + api_value: float + imperial_label: str + metric_label: str + + +@dataclass +class FreezeProtectionTemperatureMixin: + """Define an entity description mixin to include an options list.""" + + options: list[FreezeProtectionSelectOption] + + +@dataclass +class FreezeProtectionSelectDescription( + RainMachineSelectDescription, FreezeProtectionTemperatureMixin +): + """Describe a freeze protection temperature select.""" + + +TYPE_FREEZE_PROTECTION_TEMPERATURE = "freeze_protection_temperature" + +SELECT_DESCRIPTIONS = ( + FreezeProtectionSelectDescription( + key=TYPE_FREEZE_PROTECTION_TEMPERATURE, + name="Freeze protection temperature", + icon="mdi:thermometer", + entity_category=EntityCategory.CONFIG, + api_category=DATA_RESTRICTIONS_UNIVERSAL, + data_key="freezeProtectTemp", + options=[ + FreezeProtectionSelectOption( + api_value=0.0, + imperial_label="32°F", + metric_label="0°C", + ), + FreezeProtectionSelectOption( + api_value=2.0, + imperial_label="35.6°F", + metric_label="2°C", + ), + FreezeProtectionSelectOption( + api_value=5.0, + imperial_label="41°F", + metric_label="5°C", + ), + FreezeProtectionSelectOption( + api_value=10.0, + imperial_label="50°F", + metric_label="10°C", + ), + ], + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up RainMachine selects based on a config entry.""" + data: RainMachineData = hass.data[DOMAIN][entry.entry_id] + + entity_map = { + TYPE_FREEZE_PROTECTION_TEMPERATURE: FreezeProtectionTemperatureSelect, + } + + async_add_entities( + entity_map[description.key](entry, data, description, hass.config.units.name) + for description in SELECT_DESCRIPTIONS + if ( + (coordinator := data.coordinators[description.api_category]) is not None + and coordinator.data + and key_exists(coordinator.data, description.data_key) + ) + ) + + +class FreezeProtectionTemperatureSelect(RainMachineEntity, SelectEntity): + """Define a RainMachine select.""" + + entity_description: FreezeProtectionSelectDescription + + def __init__( + self, + entry: ConfigEntry, + data: RainMachineData, + description: FreezeProtectionSelectDescription, + unit_system: str, + ) -> None: + """Initialize.""" + super().__init__(entry, data, description) + + self._api_value_to_label_map = {} + self._label_to_api_value_map = {} + + for option in description.options: + if unit_system == CONF_UNIT_SYSTEM_IMPERIAL: + label = option.imperial_label + else: + label = option.metric_label + self._api_value_to_label_map[option.api_value] = label + self._label_to_api_value_map[label] = option.api_value + + self._attr_options = list(self._label_to_api_value_map) + + async def async_select_option(self, option: str) -> None: + """Change the selected option.""" + try: + await self._data.controller.restrictions.set_universal( + {self.entity_description.data_key: self._label_to_api_value_map[option]} + ) + except RainMachineError as err: + raise ValueError(f"Error while setting {self.name}: {err}") from err + + @callback + def update_from_latest_data(self) -> None: + """Update the entity when new data is received.""" + raw_value = self.coordinator.data[self.entity_description.data_key] + self._attr_current_option = self._api_value_to_label_map[raw_value] diff --git a/homeassistant/components/rainmachine/sensor.py b/homeassistant/components/rainmachine/sensor.py index 32364e08199..97772c6033a 100644 --- a/homeassistant/components/rainmachine/sensor.py +++ b/homeassistant/components/rainmachine/sensor.py @@ -6,6 +6,7 @@ from datetime import datetime, timedelta from typing import Any, cast from homeassistant.components.sensor import ( + DOMAIN as SENSOR_DOMAIN, RestoreSensor, SensorDeviceClass, SensorEntity, @@ -32,7 +33,13 @@ from .model import ( RainMachineEntityDescriptionMixinDataKey, RainMachineEntityDescriptionMixinUid, ) -from .util import RUN_STATE_MAP, RunStates, key_exists +from .util import ( + RUN_STATE_MAP, + EntityDomainReplacementStrategy, + RunStates, + async_finish_entity_domain_replacements, + key_exists, +) DEFAULT_ZONE_COMPLETION_TIME_WOBBLE_TOLERANCE = timedelta(seconds=5) @@ -127,6 +134,20 @@ async def async_setup_entry( """Set up RainMachine sensors based on a config entry.""" data: RainMachineData = hass.data[DOMAIN][entry.entry_id] + async_finish_entity_domain_replacements( + hass, + entry, + ( + EntityDomainReplacementStrategy( + SENSOR_DOMAIN, + f"{data.controller.mac}_freeze_protect_temp", + f"select.{data.controller.name.lower()}_freeze_protect_temperature", + breaks_in_ha_version="2022.12.0", + remove_old_entity=False, + ), + ), + ) + api_category_sensor_map = { DATA_PROVISION_SETTINGS: ProvisionSettingsSensor, DATA_RESTRICTIONS_UNIVERSAL: UniversalRestrictionsSensor, diff --git a/homeassistant/components/rainmachine/strings.json b/homeassistant/components/rainmachine/strings.json index 7634c0a69c5..95b92e99294 100644 --- a/homeassistant/components/rainmachine/strings.json +++ b/homeassistant/components/rainmachine/strings.json @@ -27,5 +27,18 @@ } } } + }, + "issues": { + "replaced_old_entity": { + "title": "The {old_entity_id} entity will be removed", + "fix_flow": { + "step": { + "confirm": { + "title": "The {old_entity_id} entity will be removed", + "description": "Update any automations or scripts that use this entity to instead use `{replacement_entity_id}`." + } + } + } + } } } diff --git a/homeassistant/components/rainmachine/translations/en.json b/homeassistant/components/rainmachine/translations/en.json index 9369eeae4c8..3e5d824ee08 100644 --- a/homeassistant/components/rainmachine/translations/en.json +++ b/homeassistant/components/rainmachine/translations/en.json @@ -18,6 +18,19 @@ } } }, + "issues": { + "replaced_old_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "Update any automations or scripts that use this entity to instead use `{replacement_entity_id}`.", + "title": "The {old_entity_id} entity will be removed" + } + } + }, + "title": "The {old_entity_id} entity will be removed" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/rainmachine/util.py b/homeassistant/components/rainmachine/util.py index dc772690ec5..3c66d530cf4 100644 --- a/homeassistant/components/rainmachine/util.py +++ b/homeassistant/components/rainmachine/util.py @@ -1,20 +1,23 @@ """Define RainMachine utilities.""" from __future__ import annotations -from collections.abc import Awaitable, Callable +from collections.abc import Awaitable, Callable, Iterable +from dataclasses import dataclass from datetime import timedelta from typing import Any from homeassistant.backports.enum import StrEnum from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import LOGGER +from .const import DOMAIN, LOGGER SIGNAL_REBOOT_COMPLETED = "rainmachine_reboot_completed_{0}" SIGNAL_REBOOT_REQUESTED = "rainmachine_reboot_requested_{0}" @@ -35,6 +38,60 @@ RUN_STATE_MAP = { } +@dataclass +class EntityDomainReplacementStrategy: + """Define an entity replacement.""" + + old_domain: str + old_unique_id: str + replacement_entity_id: str + breaks_in_ha_version: str + remove_old_entity: bool = True + + +@callback +def async_finish_entity_domain_replacements( + hass: HomeAssistant, + entry: ConfigEntry, + entity_replacement_strategies: Iterable[EntityDomainReplacementStrategy], +) -> None: + """Remove old entities and create a repairs issue with info on their replacement.""" + ent_reg = entity_registry.async_get(hass) + for strategy in entity_replacement_strategies: + try: + [registry_entry] = [ + registry_entry + for registry_entry in ent_reg.entities.values() + if registry_entry.config_entry_id == entry.entry_id + and registry_entry.domain == strategy.old_domain + and registry_entry.unique_id == strategy.old_unique_id + ] + except ValueError: + continue + + old_entity_id = registry_entry.entity_id + translation_key = "replaced_old_entity" + + async_create_issue( + hass, + DOMAIN, + f"{translation_key}_{old_entity_id}", + breaks_in_ha_version=strategy.breaks_in_ha_version, + is_fixable=True, + is_persistent=True, + severity=IssueSeverity.WARNING, + translation_key=translation_key, + translation_placeholders={ + "old_entity_id": old_entity_id, + "replacement_entity_id": strategy.replacement_entity_id, + }, + ) + + if strategy.remove_old_entity: + LOGGER.info('Removing old entity: "%s"', old_entity_id) + ent_reg.async_remove(old_entity_id) + + def key_exists(data: dict[str, Any], search_key: str) -> bool: """Return whether a key exists in a nested dict.""" for key, value in data.items(): From 8297317f2ac271a91be39b91b7669e2ca29e2372 Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Thu, 22 Sep 2022 18:14:43 -0600 Subject: [PATCH 656/955] Bump pylitterbot to 2022.9.6 (#78970) --- 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 44a7fd837ed..ed813983674 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.9.5"], + "requirements": ["pylitterbot==2022.9.6"], "codeowners": ["@natekspencer", "@tkdrob"], "dhcp": [{ "hostname": "litter-robot4" }], "iot_class": "cloud_push", diff --git a/requirements_all.txt b/requirements_all.txt index 1d8d5121d1a..4a528e1ce9f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1684,7 +1684,7 @@ pylibrespot-java==0.1.0 pylitejet==0.3.0 # homeassistant.components.litterrobot -pylitterbot==2022.9.5 +pylitterbot==2022.9.6 # homeassistant.components.lutron_caseta pylutron-caseta==0.15.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dee61850d69..a4141abffe0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1179,7 +1179,7 @@ pylibrespot-java==0.1.0 pylitejet==0.3.0 # homeassistant.components.litterrobot -pylitterbot==2022.9.5 +pylitterbot==2022.9.6 # homeassistant.components.lutron_caseta pylutron-caseta==0.15.2 From bbe19e6255266c5909bcf5ff429712b6aeb58c5b Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 23 Sep 2022 00:32:50 +0000 Subject: [PATCH 657/955] [ci skip] Translation update --- .../components/bluetooth/translations/pl.json | 6 +++ .../components/denonavr/translations/bg.json | 9 ++++ .../components/dsmr/translations/el.json | 2 + .../components/ecowitt/translations/ca.json | 2 +- .../components/guardian/translations/id.json | 2 +- .../components/guardian/translations/pl.json | 13 +++++- .../components/ibeacon/translations/id.json | 3 +- .../components/ibeacon/translations/pl.json | 23 ++++++++++ .../components/kegtron/translations/bg.json | 21 ++++++++++ .../components/kegtron/translations/de.json | 22 ++++++++++ .../components/kegtron/translations/el.json | 22 ++++++++++ .../components/kegtron/translations/es.json | 22 ++++++++++ .../components/kegtron/translations/fr.json | 22 ++++++++++ .../components/kegtron/translations/hu.json | 22 ++++++++++ .../components/kegtron/translations/id.json | 22 ++++++++++ .../components/kegtron/translations/pl.json | 22 ++++++++++ .../kegtron/translations/zh-Hant.json | 22 ++++++++++ .../keymitt_ble/translations/bg.json | 19 +++++++++ .../keymitt_ble/translations/ca.json | 27 ++++++++++++ .../keymitt_ble/translations/de.json | 27 ++++++++++++ .../keymitt_ble/translations/el.json | 27 ++++++++++++ .../keymitt_ble/translations/es.json | 27 ++++++++++++ .../keymitt_ble/translations/fr.json | 26 ++++++++++++ .../keymitt_ble/translations/hu.json | 27 ++++++++++++ .../keymitt_ble/translations/id.json | 27 ++++++++++++ .../keymitt_ble/translations/pl.json | 27 ++++++++++++ .../keymitt_ble/translations/zh-Hant.json | 27 ++++++++++++ .../components/kraken/translations/el.json | 4 ++ .../components/lametric/translations/pl.json | 3 +- .../components/lidarr/translations/bg.json | 18 ++++++++ .../components/lidarr/translations/hu.json | 31 +++++++++++++- .../components/lidarr/translations/id.json | 42 +++++++++++++++++++ .../components/lidarr/translations/pl.json | 42 +++++++++++++++++++ .../litterrobot/translations/sensor.pl.json | 3 ++ .../moon/translations/sensor.pl.json | 4 +- .../components/netatmo/translations/bg.json | 3 +- .../nibe_heatpump/translations/bg.json | 10 +++++ .../nibe_heatpump/translations/ca.json | 25 +++++++++++ .../nibe_heatpump/translations/de.json | 25 +++++++++++ .../nibe_heatpump/translations/el.json | 25 +++++++++++ .../nibe_heatpump/translations/en.json | 3 ++ .../nibe_heatpump/translations/es.json | 25 +++++++++++ .../nibe_heatpump/translations/fr.json | 20 +++++++++ .../nibe_heatpump/translations/id.json | 25 +++++++++++ .../nibe_heatpump/translations/pl.json | 25 +++++++++++ .../nibe_heatpump/translations/zh-Hant.json | 25 +++++++++++ .../components/openuv/translations/id.json | 2 + .../components/openuv/translations/pl.json | 10 +++++ .../rainmachine/translations/el.json | 13 ++++++ .../rainmachine/translations/fr.json | 13 ++++++ .../rainmachine/translations/id.json | 13 ++++++ .../components/roon/translations/el.json | 4 ++ .../simplisafe/translations/id.json | 1 + .../simplisafe/translations/pl.json | 6 +++ .../components/sun/translations/pl.json | 2 +- .../components/switchbot/translations/el.json | 4 ++ .../components/tasmota/translations/id.json | 10 +++++ .../components/tasmota/translations/pl.json | 10 +++++ .../components/tile/translations/bg.json | 9 ++++ .../components/upnp/translations/el.json | 4 ++ .../components/vizio/translations/bg.json | 1 + .../volvooncall/translations/pl.json | 1 + .../components/zha/translations/pl.json | 1 + .../components/zwave_js/translations/id.json | 6 +++ .../components/zwave_js/translations/pl.json | 6 +++ 65 files changed, 982 insertions(+), 10 deletions(-) create mode 100644 homeassistant/components/ibeacon/translations/pl.json create mode 100644 homeassistant/components/kegtron/translations/bg.json create mode 100644 homeassistant/components/kegtron/translations/de.json create mode 100644 homeassistant/components/kegtron/translations/el.json create mode 100644 homeassistant/components/kegtron/translations/es.json create mode 100644 homeassistant/components/kegtron/translations/fr.json create mode 100644 homeassistant/components/kegtron/translations/hu.json create mode 100644 homeassistant/components/kegtron/translations/id.json create mode 100644 homeassistant/components/kegtron/translations/pl.json create mode 100644 homeassistant/components/kegtron/translations/zh-Hant.json create mode 100644 homeassistant/components/keymitt_ble/translations/bg.json create mode 100644 homeassistant/components/keymitt_ble/translations/ca.json create mode 100644 homeassistant/components/keymitt_ble/translations/de.json create mode 100644 homeassistant/components/keymitt_ble/translations/el.json create mode 100644 homeassistant/components/keymitt_ble/translations/es.json create mode 100644 homeassistant/components/keymitt_ble/translations/fr.json create mode 100644 homeassistant/components/keymitt_ble/translations/hu.json create mode 100644 homeassistant/components/keymitt_ble/translations/id.json create mode 100644 homeassistant/components/keymitt_ble/translations/pl.json create mode 100644 homeassistant/components/keymitt_ble/translations/zh-Hant.json create mode 100644 homeassistant/components/lidarr/translations/bg.json create mode 100644 homeassistant/components/lidarr/translations/id.json create mode 100644 homeassistant/components/lidarr/translations/pl.json create mode 100644 homeassistant/components/nibe_heatpump/translations/bg.json create mode 100644 homeassistant/components/nibe_heatpump/translations/ca.json create mode 100644 homeassistant/components/nibe_heatpump/translations/de.json create mode 100644 homeassistant/components/nibe_heatpump/translations/el.json create mode 100644 homeassistant/components/nibe_heatpump/translations/es.json create mode 100644 homeassistant/components/nibe_heatpump/translations/fr.json create mode 100644 homeassistant/components/nibe_heatpump/translations/id.json create mode 100644 homeassistant/components/nibe_heatpump/translations/pl.json create mode 100644 homeassistant/components/nibe_heatpump/translations/zh-Hant.json diff --git a/homeassistant/components/bluetooth/translations/pl.json b/homeassistant/components/bluetooth/translations/pl.json index 4c1a692e1ba..2de99ab69fe 100644 --- a/homeassistant/components/bluetooth/translations/pl.json +++ b/homeassistant/components/bluetooth/translations/pl.json @@ -29,6 +29,12 @@ } } }, + "issues": { + "haos_outdated": { + "description": "Aby poprawi\u0107 niezawodno\u015b\u0107 i wydajno\u015b\u0107 Bluetooth, zdecydowanie zalecamy aktualizacj\u0119 Home Assistant OS do wersji 9.0 lub nowszej.", + "title": "Aktualizacja Home Assistant OS do wersji 9.0 lub nowszej" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/denonavr/translations/bg.json b/homeassistant/components/denonavr/translations/bg.json index 4b4384d0bc9..d1ce0ce7e22 100644 --- a/homeassistant/components/denonavr/translations/bg.json +++ b/homeassistant/components/denonavr/translations/bg.json @@ -14,5 +14,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "show_all_sources": "\u041f\u043e\u043a\u0430\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 \u0432\u0441\u0438\u0447\u043a\u0438 \u0438\u0437\u0442\u043e\u0447\u043d\u0438\u0446\u0438" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/dsmr/translations/el.json b/homeassistant/components/dsmr/translations/el.json index 77f2ad8910d..44d598fe811 100644 --- a/homeassistant/components/dsmr/translations/el.json +++ b/homeassistant/components/dsmr/translations/el.json @@ -11,6 +11,8 @@ "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" }, "step": { + "one": "\u03ba\u03b5\u03bd\u03cc", + "other": "\u03ba\u03b5\u03bd\u03cc", "setup_network": { "data": { "dsmr_version": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7\u03c2 DSMR", diff --git a/homeassistant/components/ecowitt/translations/ca.json b/homeassistant/components/ecowitt/translations/ca.json index beac6285b0a..5dd00992145 100644 --- a/homeassistant/components/ecowitt/translations/ca.json +++ b/homeassistant/components/ecowitt/translations/ca.json @@ -11,7 +11,7 @@ "user": { "data": { "path": "Cam\u00ed amb testimoni de seguretat", - "port": "Escoltant el port" + "port": "Port d'escolta" }, "description": "Est\u00e0s segur que vols configurar Ecowitt?" } diff --git a/homeassistant/components/guardian/translations/id.json b/homeassistant/components/guardian/translations/id.json index 131debefb94..e62f48eae8f 100644 --- a/homeassistant/components/guardian/translations/id.json +++ b/homeassistant/components/guardian/translations/id.json @@ -23,7 +23,7 @@ "fix_flow": { "step": { "confirm": { - "description": "Perbarui semua otomasi atau skrip yang menggunakan layanan ini untuk menggunakan layanan `{alternate_service}` dengan ID entitas target `{alternate_target}`. Kemudian, klik KIRIM di bawah ini untuk menandai masalah ini sebagai terselesaikan.", + "description": "Perbarui semua otomasi atau skrip yang menggunakan layanan ini untuk menggunakan layanan `{alternate_service}` dengan ID entitas target `{alternate_target}`.", "title": "Layanan {deprecated_service} akan dihapus" } } diff --git a/homeassistant/components/guardian/translations/pl.json b/homeassistant/components/guardian/translations/pl.json index c86f98b3b8e..f1cb2893050 100644 --- a/homeassistant/components/guardian/translations/pl.json +++ b/homeassistant/components/guardian/translations/pl.json @@ -23,12 +23,23 @@ "fix_flow": { "step": { "confirm": { - "description": "Zaktualizuj wszystkie automatyzacje lub skrypty, kt\u00f3re u\u017cywaj\u0105 tej us\u0142ugi, aby zamiast tego u\u017cywa\u0142y us\u0142ugi `{alternate_service}` z encj\u0105 docelow\u0105 `{alternate_target}`. Nast\u0119pnie kliknij ZATWIERD\u0179 poni\u017cej, aby oznaczy\u0107 ten problem jako rozwi\u0105zany.", + "description": "Zaktualizuj wszystkie automatyzacje lub skrypty, kt\u00f3re u\u017cywaj\u0105 tej us\u0142ugi, aby zamiast tego u\u017cywa\u0142y us\u0142ugi `{alternate_service}` z encj\u0105 docelow\u0105 `{alternate_target}`.", "title": "Us\u0142uga {deprecated_service} zostanie usuni\u0119ta" } } }, "title": "Us\u0142uga {deprecated_service} zostanie usuni\u0119ta" + }, + "replaced_old_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "Zaktualizuj wszystkie automatyzacje lub skrypty, kt\u00f3re u\u017cywaj\u0105 tej encji, aby zamiast tego u\u017cywa\u0142y `{replacement_entity_id}`.", + "title": "Encja {old_entity_id} zostanie usuni\u0119ta" + } + } + }, + "title": "Encja {old_entity_id} zostanie usuni\u0119ta" } } } \ No newline at end of file diff --git a/homeassistant/components/ibeacon/translations/id.json b/homeassistant/components/ibeacon/translations/id.json index 2c7a1adbd5e..730581f1033 100644 --- a/homeassistant/components/ibeacon/translations/id.json +++ b/homeassistant/components/ibeacon/translations/id.json @@ -15,7 +15,8 @@ "init": { "data": { "min_rssi": "RSSI minimum" - } + }, + "description": "iBeacon dengan nilai RSSI lebih rendah dari RSSI Minimum akan diabaikan. Jika integrasi melihat iBeacon tetangga, meningkatkan nilai ini mungkin akan membantu." } } } diff --git a/homeassistant/components/ibeacon/translations/pl.json b/homeassistant/components/ibeacon/translations/pl.json new file mode 100644 index 00000000000..4a98f2316ea --- /dev/null +++ b/homeassistant/components/ibeacon/translations/pl.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "bluetooth_not_available": "Co najmniej jeden adapter lub pilot Bluetooth musi by\u0107 skonfigurowany do korzystania z iBeacon Tracker.", + "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." + }, + "step": { + "user": { + "description": "Chcesz skonfigurowa\u0107 iBeacon Tracker?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "min_rssi": "Minimalny RSSI" + }, + "description": "Sygna\u0142y iBeacon o warto\u015bci RSSI ni\u017cszej ni\u017c Minimalny RSSI b\u0119d\u0105 ignorowane. Je\u015bli integracja widzi s\u0105siednie iBeacons, zwi\u0119kszenie tej warto\u015bci mo\u017ce pom\u00f3c." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kegtron/translations/bg.json b/homeassistant/components/kegtron/translations/bg.json new file mode 100644 index 00000000000..af9a13197df --- /dev/null +++ b/homeassistant/components/kegtron/translations/bg.json @@ -0,0 +1,21 @@ +{ + "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 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\u0430", + "not_supported": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u043d\u0435 \u0441\u0435 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\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/kegtron/translations/de.json b/homeassistant/components/kegtron/translations/de.json new file mode 100644 index 00000000000..4c5720ec6fb --- /dev/null +++ b/homeassistant/components/kegtron/translations/de.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden", + "not_supported": "Ger\u00e4t nicht unterst\u00fctzt" + }, + "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/kegtron/translations/el.json b/homeassistant/components/kegtron/translations/el.json new file mode 100644 index 00000000000..cdb57c8ac1b --- /dev/null +++ b/homeassistant/components/kegtron/translations/el.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "already_in_progress": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", + "no_devices_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf", + "not_supported": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name};" + }, + "user": { + "data": { + "address": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b3\u03b9\u03b1 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kegtron/translations/es.json b/homeassistant/components/kegtron/translations/es.json new file mode 100644 index 00000000000..ae0ab01acdf --- /dev/null +++ b/homeassistant/components/kegtron/translations/es.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", + "no_devices_found": "No se encontraron dispositivos en la red", + "not_supported": "Dispositivo no compatible" + }, + "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/kegtron/translations/fr.json b/homeassistant/components/kegtron/translations/fr.json new file mode 100644 index 00000000000..8ddb4af4dbc --- /dev/null +++ b/homeassistant/components/kegtron/translations/fr.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", + "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau", + "not_supported": "Appareil non pris en charge" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Voulez-vous configurer {name}\u00a0?" + }, + "user": { + "data": { + "address": "Appareil" + }, + "description": "S\u00e9lectionnez l'appareil \u00e0 configurer" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kegtron/translations/hu.json b/homeassistant/components/kegtron/translations/hu.json new file mode 100644 index 00000000000..97fbb5b9408 --- /dev/null +++ b/homeassistant/components/kegtron/translations/hu.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", + "not_supported": "Eszk\u00f6z nem t\u00e1mogatott" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name}?" + }, + "user": { + "data": { + "address": "Eszk\u00f6z" + }, + "description": "V\u00e1lassza ki a be\u00e1ll\u00edtani k\u00edv\u00e1nt eszk\u00f6zt" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kegtron/translations/id.json b/homeassistant/components/kegtron/translations/id.json new file mode 100644 index 00000000000..573eb39ed15 --- /dev/null +++ b/homeassistant/components/kegtron/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/kegtron/translations/pl.json b/homeassistant/components/kegtron/translations/pl.json new file mode 100644 index 00000000000..4715905a2e9 --- /dev/null +++ b/homeassistant/components/kegtron/translations/pl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "already_in_progress": "Konfiguracja jest ju\u017c w toku", + "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci", + "not_supported": "Urz\u0105dzenie nie jest obs\u0142ugiwane" + }, + "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/kegtron/translations/zh-Hant.json b/homeassistant/components/kegtron/translations/zh-Hant.json new file mode 100644 index 00000000000..64ae1f19094 --- /dev/null +++ b/homeassistant/components/kegtron/translations/zh-Hant.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", + "not_supported": "\u88dd\u7f6e\u4e0d\u652f\u63f4" + }, + "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/keymitt_ble/translations/bg.json b/homeassistant/components/keymitt_ble/translations/bg.json new file mode 100644 index 00000000000..4f10191a3d1 --- /dev/null +++ b/homeassistant/components/keymitt_ble/translations/bg.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured_device": "\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", + "no_unconfigured_devices": "\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 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430.", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "flow_title": "{name}", + "step": { + "init": { + "data": { + "address": "\u0410\u0434\u0440\u0435\u0441 \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e", + "name": "\u0418\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/keymitt_ble/translations/ca.json b/homeassistant/components/keymitt_ble/translations/ca.json new file mode 100644 index 00000000000..afae4bbd458 --- /dev/null +++ b/homeassistant/components/keymitt_ble/translations/ca.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured_device": "El dispositiu ja est\u00e0 configurat", + "cannot_connect": "Ha fallat la connexi\u00f3", + "no_unconfigured_devices": "No s'han trobat dispositius no configurats.", + "unknown": "Error inesperat" + }, + "error": { + "linking": "No s'ha pogut vincular, torna-ho a provar. El MicroBot est\u00e0 en mode vinculaci\u00f3?" + }, + "flow_title": "{name}", + "step": { + "init": { + "data": { + "address": "Adre\u00e7a del dispositiu", + "name": "Nom" + }, + "title": "Configuraci\u00f3 de dispositiu MicroBot" + }, + "link": { + "description": "Prem el bot\u00f3 del MicroBot Push quan el LED estigui enc\u00e8s de color rosa o verd per registrar-lo a Home Assistant.", + "title": "Vinculaci\u00f3" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/keymitt_ble/translations/de.json b/homeassistant/components/keymitt_ble/translations/de.json new file mode 100644 index 00000000000..a03d9c725be --- /dev/null +++ b/homeassistant/components/keymitt_ble/translations/de.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured_device": "Ger\u00e4t ist bereits konfiguriert", + "cannot_connect": "Verbindung fehlgeschlagen", + "no_unconfigured_devices": "Keine unkonfigurierten Ger\u00e4te gefunden.", + "unknown": "Unerwarteter Fehler" + }, + "error": { + "linking": "Pairing fehlgeschlagen, bitte versuche es erneut. Befindet sich der MicroBot im Kopplungsmodus?" + }, + "flow_title": "{name}", + "step": { + "init": { + "data": { + "address": "Ger\u00e4teadresse", + "name": "Name" + }, + "title": "MicroBot-Ger\u00e4t einrichten" + }, + "link": { + "description": "Dr\u00fccke die Taste am MicroBot Push, wenn die LED durchgehend rosa oder gr\u00fcn leuchtet, um sich bei Home Assistant zu registrieren.", + "title": "Kopplung" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/keymitt_ble/translations/el.json b/homeassistant/components/keymitt_ble/translations/el.json new file mode 100644 index 00000000000..bb6521f4b36 --- /dev/null +++ b/homeassistant/components/keymitt_ble/translations/el.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured_device": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "no_unconfigured_devices": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03bc\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03bc\u03ad\u03bd\u03b5\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2.", + "unknown": "\u0391\u03c0\u03c1\u03bf\u03c3\u03b4\u03cc\u03ba\u03b7\u03c4\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "error": { + "linking": "\u0397 \u03b1\u03bd\u03c4\u03b9\u03c3\u03c4\u03bf\u03af\u03c7\u03b9\u03c3\u03b7 \u03b1\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5, \u03c0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03ce \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac. \u0395\u03af\u03bd\u03b1\u03b9 \u03c4\u03bf MicroBot \u03c3\u03b5 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03b1\u03bd\u03c4\u03b9\u03c3\u03c4\u03bf\u03af\u03c7\u03b9\u03c3\u03b7\u03c2;" + }, + "flow_title": "{name}", + "step": { + "init": { + "data": { + "address": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2", + "name": "\u039f\u03bd\u03bf\u03bc\u03b1" + }, + "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 MicroBot" + }, + "link": { + "description": "\u03a0\u03b1\u03c4\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af \u03c3\u03c4\u03bf MicroBot Push \u03cc\u03c4\u03b1\u03bd \u03b7 \u03bb\u03c5\u03c7\u03bd\u03af\u03b1 LED \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c4\u03b1\u03b8\u03b5\u03c1\u03ac \u03c1\u03bf\u03b6 \u03ae \u03c0\u03c1\u03ac\u03c3\u03b9\u03bd\u03b7 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf Home Assistant.", + "title": "\u0391\u03bd\u03c4\u03b9\u03c3\u03c4\u03bf\u03af\u03c7\u03b9\u03c3\u03b7" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/keymitt_ble/translations/es.json b/homeassistant/components/keymitt_ble/translations/es.json new file mode 100644 index 00000000000..62b31234780 --- /dev/null +++ b/homeassistant/components/keymitt_ble/translations/es.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured_device": "El dispositivo ya est\u00e1 configurado", + "cannot_connect": "No se pudo conectar", + "no_unconfigured_devices": "No se encontraron dispositivos no configurados.", + "unknown": "Error inesperado" + }, + "error": { + "linking": "No se pudo emparejar, por favor, int\u00e9ntalo de nuevo. \u00bfEst\u00e1 el MicroBot en modo de emparejamiento?" + }, + "flow_title": "{name}", + "step": { + "init": { + "data": { + "address": "Direcci\u00f3n del dispositivo", + "name": "Nombre" + }, + "title": "Configurar el dispositivo MicroBot" + }, + "link": { + "description": "Pulsa el bot\u00f3n en el MicroBot Push cuando el LED est\u00e9 fijo en rosa o verde para registrarlo con Home Assistant.", + "title": "Emparejamiento" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/keymitt_ble/translations/fr.json b/homeassistant/components/keymitt_ble/translations/fr.json new file mode 100644 index 00000000000..2b425195d11 --- /dev/null +++ b/homeassistant/components/keymitt_ble/translations/fr.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured_device": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "cannot_connect": "\u00c9chec de connexion", + "no_unconfigured_devices": "Aucun appareil non configur\u00e9 n'a \u00e9t\u00e9 trouv\u00e9.", + "unknown": "Erreur inattendue" + }, + "error": { + "linking": "\u00c9chec de l'appairage, veuillez r\u00e9essayer. Le MicroBot est-il en mode d'appairage\u00a0?" + }, + "flow_title": "{name}", + "step": { + "init": { + "data": { + "address": "Adresse de l'appareil", + "name": "Nom" + }, + "title": "Configurer l'appareil MicroBot" + }, + "link": { + "title": "Appairage" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/keymitt_ble/translations/hu.json b/homeassistant/components/keymitt_ble/translations/hu.json new file mode 100644 index 00000000000..0792bf4ad23 --- /dev/null +++ b/homeassistant/components/keymitt_ble/translations/hu.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured_device": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "no_unconfigured_devices": "Nem tal\u00e1lhat\u00f3 konfigur\u00e1latlan eszk\u00f6z.", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "error": { + "linking": "Nem siker\u00fclt p\u00e1ros\u00edtani, k\u00e9rem, pr\u00f3b\u00e1lja \u00fajra. A MicroBot p\u00e1ros\u00edt\u00e1si m\u00f3dban van?" + }, + "flow_title": "{name}", + "step": { + "init": { + "data": { + "address": "Eszk\u00f6z c\u00edme", + "name": "N\u00e9v" + }, + "title": "MicroBot eszk\u00f6z be\u00e1ll\u00edt\u00e1sa" + }, + "link": { + "description": "Nyomja meg a MicroBoton a Push gombot, amikor a LED r\u00f3zsasz\u00edn vagy z\u00f6ld sz\u00ednnel vil\u00e1g\u00edt, hogy az regisztr\u00e1l\u00f3djon a Home Assistant rendszerbe.", + "title": "P\u00e1ros\u00edt\u00e1s" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/keymitt_ble/translations/id.json b/homeassistant/components/keymitt_ble/translations/id.json new file mode 100644 index 00000000000..e94cd17c5c8 --- /dev/null +++ b/homeassistant/components/keymitt_ble/translations/id.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured_device": "Perangkat sudah dikonfigurasi", + "cannot_connect": "Gagal terhubung", + "no_unconfigured_devices": "Tidak ditemukan perangkat yang tidak dikonfigurasi.", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "error": { + "linking": "Gagal memasangkan, silakan coba lagi. Apakah MicroBot dalam mode pairing?" + }, + "flow_title": "{name}", + "step": { + "init": { + "data": { + "address": "Alamat perangkat", + "name": "Nama" + }, + "title": "Siapkan perangkat MicroBot" + }, + "link": { + "description": "Tekan tombol pada MicroBot Push ketika LED berwarna merah muda atau hijau untuk mendaftarkannya ke Home Assistant.", + "title": "Pemasangan" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/keymitt_ble/translations/pl.json b/homeassistant/components/keymitt_ble/translations/pl.json new file mode 100644 index 00000000000..4f7f8f59a1c --- /dev/null +++ b/homeassistant/components/keymitt_ble/translations/pl.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured_device": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "no_unconfigured_devices": "Nie znaleziono nieskonfigurowanych urz\u0105dze\u0144.", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "error": { + "linking": "Nie uda\u0142o si\u0119 sparowa\u0107, spr\u00f3buj ponownie. Czy MicroBot jest w trybie parowania?" + }, + "flow_title": "{name}", + "step": { + "init": { + "data": { + "address": "Adres urz\u0105dzenia", + "name": "Nazwa" + }, + "title": "Konfiguracja urz\u0105dzenia MicroBot" + }, + "link": { + "description": "Naci\u015bnij przycisk na MicroBot Push, gdy dioda LED \u015bwieci na r\u00f3\u017cowo lub zielono, aby zarejestrowa\u0107 go w Home Assistant.", + "title": "Parowanie" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/keymitt_ble/translations/zh-Hant.json b/homeassistant/components/keymitt_ble/translations/zh-Hant.json new file mode 100644 index 00000000000..7f48cd5e633 --- /dev/null +++ b/homeassistant/components/keymitt_ble/translations/zh-Hant.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured_device": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "no_unconfigured_devices": "\u627e\u4e0d\u5230\u4efb\u4f55\u672a\u8a2d\u5b9a\u88dd\u7f6e\u3002", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "error": { + "linking": "\u914d\u5c0d\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002MicroBot \u662f\u5426\u8655\u65bc\u914d\u5c0d\u6a21\u5f0f\uff1f" + }, + "flow_title": "{name}", + "step": { + "init": { + "data": { + "address": "\u88dd\u7f6e\u4f4d\u5740", + "name": "\u540d\u7a31" + }, + "title": "\u8a2d\u5b9a MicroBot \u88dd\u7f6e" + }, + "link": { + "description": "\u6309\u4f4f MicroBot \u4e0a\u7684\u6309\u9215\u76f4\u5230\u5e38\u4eae\u7c89\u8272\u6216\u7da0\u8272\uff0c\u4ee5\u8a3b\u518a\u81f3 Home Assistant\u3002", + "title": "\u914d\u5c0d\u4e2d" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kraken/translations/el.json b/homeassistant/components/kraken/translations/el.json index 252d1a5ebd2..aa3c6780ac9 100644 --- a/homeassistant/components/kraken/translations/el.json +++ b/homeassistant/components/kraken/translations/el.json @@ -5,6 +5,10 @@ }, "step": { "user": { + "data": { + "one": "\u03ba\u03b5\u03bd\u03cc", + "other": "\u03ba\u03b5\u03bd\u03cc" + }, "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03be\u03b5\u03ba\u03b9\u03bd\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7;" } } diff --git a/homeassistant/components/lametric/translations/pl.json b/homeassistant/components/lametric/translations/pl.json index 5cc897a8e66..8ba3215cbe2 100644 --- a/homeassistant/components/lametric/translations/pl.json +++ b/homeassistant/components/lametric/translations/pl.json @@ -7,7 +7,8 @@ "link_local_address": "Po\u0142\u0105czenie lokalnego adresu nie jest obs\u0142ugiwane", "missing_configuration": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105.", "no_devices": "Autoryzowany u\u017cytkownik nie posiada urz\u0105dze\u0144 LaMetric", - "no_url_available": "Brak dost\u0119pnego adresu URL. Aby uzyska\u0107 informacje na temat tego b\u0142\u0119du, [sprawd\u017a sekcj\u0119 pomocy] ({docs_url})" + "no_url_available": "Brak dost\u0119pnego adresu URL. Aby uzyska\u0107 informacje na temat tego b\u0142\u0119du, [sprawd\u017a sekcj\u0119 pomocy] ({docs_url})", + "unknown": "Nieoczekiwany b\u0142\u0105d" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", diff --git a/homeassistant/components/lidarr/translations/bg.json b/homeassistant/components/lidarr/translations/bg.json new file mode 100644 index 00000000000..682e8687712 --- /dev/null +++ b/homeassistant/components/lidarr/translations/bg.json @@ -0,0 +1,18 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447" + }, + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" + }, + "user": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447", + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lidarr/translations/hu.json b/homeassistant/components/lidarr/translations/hu.json index 6a18f68959e..f1b35c83d98 100644 --- a/homeassistant/components/lidarr/translations/hu.json +++ b/homeassistant/components/lidarr/translations/hu.json @@ -7,7 +7,36 @@ "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", - "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt", + "wrong_app": "Helytelen alkalmaz\u00e1s \u00e9rhet\u0151 el. K\u00e9rj\u00fck, pr\u00f3b\u00e1lja \u00fajra", + "zeroconf_failed": "Az API-kulcs nem tal\u00e1lhat\u00f3. K\u00e9rem, adja meg" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API kulcs" + }, + "description": "A Lidarr integr\u00e1ci\u00f3t manu\u00e1lisan \u00fajra kell hiteles\u00edteni a Lidarr API-val.", + "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" + }, + "user": { + "data": { + "api_key": "API kulcs", + "url": "URL", + "verify_ssl": "SSL-tan\u00fas\u00edtv\u00e1ny ellen\u0151rz\u00e9se" + }, + "description": "Az API-kulcs automatikusan lek\u00e9rhet\u0151, ha a bejelentkez\u00e9si hiteles\u00edt\u0151 adatok nem lettek be\u00e1ll\u00edtva az alkalmaz\u00e1sban.\nAz API-kulcs a Lidarr webes felhaszn\u00e1l\u00f3i fel\u00fclet Be\u00e1ll\u00edt\u00e1sok > \u00c1ltal\u00e1nos men\u00fcpontj\u00e1ban tal\u00e1lhat\u00f3." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "max_records": "A keresett \u00e9s a v\u00e1r\u00f3list\u00e1n megjelen\u00edtend\u0151 maxim\u00e1lis rekordok sz\u00e1ma", + "upcoming_days": "A napt\u00e1rban megjelen\u00edtend\u0151 k\u00f6vetkez\u0151 napok sz\u00e1ma" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/lidarr/translations/id.json b/homeassistant/components/lidarr/translations/id.json new file mode 100644 index 00000000000..1514a016dc1 --- /dev/null +++ b/homeassistant/components/lidarr/translations/id.json @@ -0,0 +1,42 @@ +{ + "config": { + "abort": { + "already_configured": "Layanan sudah dikonfigurasi", + "reauth_successful": "Autentikasi ulang berhasil" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan", + "wrong_app": "Aplikasi yang salah tercapai. Silakan coba lagi", + "zeroconf_failed": "Kunci API tidak ditemukan. Silakan masukkan secara manual" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Kunci API" + }, + "description": "Integrasi Lidarr perlu diautentikasi ulang secara manual dengan Lidarr API", + "title": "Autentikasi Ulang Integrasi" + }, + "user": { + "data": { + "api_key": "Kunci API", + "url": "URL", + "verify_ssl": "Verifikasi sertifikat SSL" + }, + "description": "Kunci API dapat diambil secara otomatis jika kredensial login tidak diatur dalam aplikasi.\nKunci API Anda dapat ditemukan di Settings > General di antarmuka web Lidarr." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "max_records": "Jumlah data maksimum untuk ditampilkan pada wanted dan queue", + "upcoming_days": "Jumlah hari yang akan datang untuk ditampilkan pada kalender" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lidarr/translations/pl.json b/homeassistant/components/lidarr/translations/pl.json new file mode 100644 index 00000000000..33d0deee79b --- /dev/null +++ b/homeassistant/components/lidarr/translations/pl.json @@ -0,0 +1,42 @@ +{ + "config": { + "abort": { + "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d", + "wrong_app": "Osi\u0105gni\u0119to nieprawid\u0142ow\u0105 aplikacj\u0119. Spr\u00f3buj ponownie", + "zeroconf_failed": "Nie znaleziono klucza API. Prosz\u0119 wpisa\u0107 go r\u0119cznie." + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Klucz API" + }, + "description": "Integracja Lidarr musi zosta\u0107 r\u0119cznie ponownie uwierzytelniona za pomoc\u0105 interfejsu API Lidarr", + "title": "Ponownie uwierzytelnij integracj\u0119" + }, + "user": { + "data": { + "api_key": "Klucz API", + "url": "URL", + "verify_ssl": "Weryfikacja certyfikatu SSL" + }, + "description": "Klucz API mo\u017ce zosta\u0107 pobrany automatycznie, je\u015bli dane logowania nie zosta\u0142y ustawione w aplikacji.\nTw\u00f3j klucz API mo\u017cesz znale\u017a\u0107 w Ustawienia > Og\u00f3lne, na swoim koncie Lidarr." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "max_records": "Maksymalna liczba wpis\u00f3w do wy\u015bwietlenia w poszukiwanych i w kolejce", + "upcoming_days": "Liczba nadchodz\u0105cych dni do wy\u015bwietlenia w kalendarzu" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/sensor.pl.json b/homeassistant/components/litterrobot/translations/sensor.pl.json index b3063c7ca62..7c70072307e 100644 --- a/homeassistant/components/litterrobot/translations/sensor.pl.json +++ b/homeassistant/components/litterrobot/translations/sensor.pl.json @@ -4,6 +4,7 @@ "br": "pokrywa otwarta", "ccc": "cykl czyszczenia zako\u0144czony", "ccp": "cykl czyszczenia w toku", + "cd": "wykryto kota", "csf": "b\u0142\u0105d sensora obecno\u015bci", "csi": "przerwa w pracy sensora", "cst": "czas pracy sensora", @@ -19,6 +20,8 @@ "otf": "b\u0142\u0105d nadmiernego momentu obrotowego", "p": "wstrzymany", "pd": "detektor obecno\u015bci", + "pwrd": "wy\u0142\u0105czanie", + "pwru": "w\u0142\u0105czanie", "rdy": "gotowy", "scf": "b\u0142\u0105d sensora obecno\u015bci podczas uruchamiania", "sdf": "szuflada pe\u0142na podczas uruchamiania", diff --git a/homeassistant/components/moon/translations/sensor.pl.json b/homeassistant/components/moon/translations/sensor.pl.json index f70cd8f38a0..616db5be621 100644 --- a/homeassistant/components/moon/translations/sensor.pl.json +++ b/homeassistant/components/moon/translations/sensor.pl.json @@ -5,9 +5,9 @@ "full_moon": "pe\u0142nia", "last_quarter": "ostatnia kwadra", "new_moon": "n\u00f3w", - "waning_crescent": "Sierp ubywaj\u0105cy", + "waning_crescent": "sierp ubywaj\u0105cy", "waning_gibbous": "ubywaj\u0105cy garbaty", - "waxing_crescent": "Sierp przybywaj\u0105cy", + "waxing_crescent": "sierp przybywaj\u0105cy", "waxing_gibbous": "przybywaj\u0105cy garbaty" } } diff --git a/homeassistant/components/netatmo/translations/bg.json b/homeassistant/components/netatmo/translations/bg.json index 30cbd4c7167..5e771b9224b 100644 --- a/homeassistant/components/netatmo/translations/bg.json +++ b/homeassistant/components/netatmo/translations/bg.json @@ -21,7 +21,8 @@ "step": { "public_weather": { "data": { - "area_name": "\u0418\u043c\u0435 \u043d\u0430 \u043e\u0431\u043b\u0430\u0441\u0442\u0442\u0430" + "area_name": "\u0418\u043c\u0435 \u043d\u0430 \u043e\u0431\u043b\u0430\u0441\u0442\u0442\u0430", + "show_on_map": "\u041f\u043e\u043a\u0430\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 \u043a\u0430\u0440\u0442\u0430\u0442\u0430" } } } diff --git a/homeassistant/components/nibe_heatpump/translations/bg.json b/homeassistant/components/nibe_heatpump/translations/bg.json new file mode 100644 index 00000000000..88f52d84269 --- /dev/null +++ b/homeassistant/components/nibe_heatpump/translations/bg.json @@ -0,0 +1,10 @@ +{ + "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": { + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nibe_heatpump/translations/ca.json b/homeassistant/components/nibe_heatpump/translations/ca.json new file mode 100644 index 00000000000..95cc0f32841 --- /dev/null +++ b/homeassistant/components/nibe_heatpump/translations/ca.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "address": "Adre\u00e7a IP remota inv\u00e0lida. L'adre\u00e7a ha de ser una adre\u00e7a IPV4.", + "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.", + "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." + }, + "step": { + "user": { + "data": { + "ip_address": "Adre\u00e7a IP remota", + "listening_port": "Port local d'escolta", + "remote_read_port": "Port remot de lectura", + "remote_write_port": "Port remot d'escriptura" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nibe_heatpump/translations/de.json b/homeassistant/components/nibe_heatpump/translations/de.json new file mode 100644 index 00000000000..8eda1b68b8b --- /dev/null +++ b/homeassistant/components/nibe_heatpump/translations/de.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "address": "Ung\u00fcltige Remote-IP-Adresse angegeben. Die Adresse muss eine IPV4-Adresse 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.", + "unknown": "Unerwarteter Fehler", + "write": "Fehler bei Schreibanforderung an Pumpe. \u00dcberpr\u00fcfe deinen \u201eRemote-Schreibport\u201c oder \u201eRemote-IP-Adresse\u201c." + }, + "step": { + "user": { + "data": { + "ip_address": "Remote-IP-Adresse", + "listening_port": "Lokaler Leseport", + "remote_read_port": "Remote-Leseport", + "remote_write_port": "Remote-Schreibport" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nibe_heatpump/translations/el.json b/homeassistant/components/nibe_heatpump/translations/el.json new file mode 100644 index 00000000000..a740bc43742 --- /dev/null +++ b/homeassistant/components/nibe_heatpump/translations/el.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" + }, + "error": { + "address": "\u039a\u03b1\u03b8\u03bf\u03c1\u03af\u03c3\u03c4\u03b7\u03ba\u03b5 \u03bc\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b1\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP. \u0397 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IPV4.", + "address_in_use": "\u0397 \u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03b7 \u03b8\u03cd\u03c1\u03b1 \u03b1\u03ba\u03c1\u03cc\u03b1\u03c3\u03b7\u03c2 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1.", + "model": "\u03a4\u03bf \u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03bf \u03bc\u03bf\u03bd\u03c4\u03ad\u03bb\u03bf \u03b4\u03b5\u03bd \u03c6\u03b1\u03af\u03bd\u03b5\u03c4\u03b1\u03b9 \u03bd\u03b1 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03b9 modbus40", + "read": "\u03a3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03c3\u03c4\u03bf \u03b1\u03af\u03c4\u03b7\u03bc\u03b1 \u03b1\u03bd\u03ac\u03b3\u03bd\u03c9\u03c3\u03b7\u03c2 \u03b1\u03c0\u03cc \u03c4\u03b7\u03bd \u03b1\u03bd\u03c4\u03bb\u03af\u03b1. \u0395\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03cd\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \"\u0391\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7 \u03b8\u03cd\u03c1\u03b1 \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2\" \u03ae \u03c4\u03b7\u03bd \"\u0391\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP\".", + "unknown": "\u0391\u03c0\u03c1\u03bf\u03c3\u03b4\u03cc\u03ba\u03b7\u03c4\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1", + "write": "\u03a3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03c3\u03c4\u03bf \u03b1\u03af\u03c4\u03b7\u03bc\u03b1 \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2 \u03c3\u03c4\u03b7\u03bd \u03b1\u03bd\u03c4\u03bb\u03af\u03b1. \u0395\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03cd\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \"\u0391\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7 \u03b8\u03cd\u03c1\u03b1 \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2\" \u03ae \u03c4\u03b7\u03bd \"\u0391\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP\"." + }, + "step": { + "user": { + "data": { + "ip_address": "\u0391\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP", + "listening_port": "\u03a4\u03bf\u03c0\u03b9\u03ba\u03ae \u03b8\u03cd\u03c1\u03b1 \u03b1\u03ba\u03c1\u03cc\u03b1\u03c3\u03b7\u03c2", + "remote_read_port": "\u0391\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7 \u03b8\u03cd\u03c1\u03b1 \u03b1\u03bd\u03ac\u03b3\u03bd\u03c9\u03c3\u03b7\u03c2", + "remote_write_port": "\u0391\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7 \u03b8\u03cd\u03c1\u03b1 \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2" + } + } + } + } +} \ 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 6d85cbcfcb3..74dd8313e95 100644 --- a/homeassistant/components/nibe_heatpump/translations/en.json +++ b/homeassistant/components/nibe_heatpump/translations/en.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Device is already configured" + }, "error": { "address": "Invalid remote IP address specified. Address must be a IPV4 address.", "address_in_use": "The selected listening port is already in use on this system.", diff --git a/homeassistant/components/nibe_heatpump/translations/es.json b/homeassistant/components/nibe_heatpump/translations/es.json new file mode 100644 index 00000000000..60c228a28e7 --- /dev/null +++ b/homeassistant/components/nibe_heatpump/translations/es.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "address": "Se especific\u00f3 una direcci\u00f3n IP remota no v\u00e1lida. La direcci\u00f3n debe ser una direcci\u00f3n IPv4.", + "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`.", + "unknown": "Error inesperado", + "write": "Error en la solicitud de escritura a la bomba. Verifica tu `Puerto de escritura remoto` o `Direcci\u00f3n IP remota`." + }, + "step": { + "user": { + "data": { + "ip_address": "Direcci\u00f3n IP remota", + "listening_port": "Puerto de escucha local", + "remote_read_port": "Puerto de lectura remoto", + "remote_write_port": "Puerto de escritura remoto" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nibe_heatpump/translations/fr.json b/homeassistant/components/nibe_heatpump/translations/fr.json new file mode 100644 index 00000000000..dc28a729aea --- /dev/null +++ b/homeassistant/components/nibe_heatpump/translations/fr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "ip_address": "Adresse IP distante", + "listening_port": "Port d'\u00e9coute local", + "remote_read_port": "Port de lecture distant", + "remote_write_port": "Port d'\u00e9criture distant" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nibe_heatpump/translations/id.json b/homeassistant/components/nibe_heatpump/translations/id.json new file mode 100644 index 00000000000..53e3d202877 --- /dev/null +++ b/homeassistant/components/nibe_heatpump/translations/id.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "address": "Alamat IP jarak jauh yang ditentukan tidak valid. Alamat harus berupa alamat IPv4.", + "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.", + "unknown": "Kesalahan yang tidak diharapkan", + "write": "Kesalahan pada permintaan tulis ke pompa. Verifikasi `Port tulis jarak jauh` atau `Alamat IP jarak jauh` Anda." + }, + "step": { + "user": { + "data": { + "ip_address": "Alamat IP jarak jauh", + "listening_port": "Port mendengarkan lokal", + "remote_read_port": "Port baca jarak jauh", + "remote_write_port": "Port tulis jarak jauh" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nibe_heatpump/translations/pl.json b/homeassistant/components/nibe_heatpump/translations/pl.json new file mode 100644 index 00000000000..7a179ad7326 --- /dev/null +++ b/homeassistant/components/nibe_heatpump/translations/pl.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "address": "Podano nieprawid\u0142owy zdalny adres IP. Adres musi by\u0107 adresem IPV4.", + "address_in_use": "Wybrany port nas\u0142uchiwania jest ju\u017c u\u017cywany w tym systemie.", + "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", + "write": "B\u0142\u0105d przy \u017c\u0105daniu zapisu do pompy. Sprawd\u017a \u201eZdalny port zapisu\u201d lub \u201eZdalny adres IP\u201d." + }, + "step": { + "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" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nibe_heatpump/translations/zh-Hant.json b/homeassistant/components/nibe_heatpump/translations/zh-Hant.json new file mode 100644 index 00000000000..a2bb8c8f023 --- /dev/null +++ b/homeassistant/components/nibe_heatpump/translations/zh-Hant.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "address": "\u6307\u5b9a\u7684\u9060\u7aef IP \u4f4d\u5740\u7121\u6548\u3002\u4f4d\u5740\u5fc5\u9808\u70ba IPV4 \u4f4d\u5740\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", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4", + "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": { + "data": { + "ip_address": "\u9060\u7aef IP \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" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openuv/translations/id.json b/homeassistant/components/openuv/translations/id.json index 2ed7f57d3b9..6ad5ee1f8d9 100644 --- a/homeassistant/components/openuv/translations/id.json +++ b/homeassistant/components/openuv/translations/id.json @@ -20,9 +20,11 @@ }, "issues": { "deprecated_service_multiple_alternate_targets": { + "description": "Perbarui semua otomasi atau skrip yang menggunakan layanan ini untuk menggunakan layanan `{alternate_service}` dengan salah satu ID entitas berikut sebagai target: `{alternate_targets}`.", "title": "Layanan {deprecated_service} dalam proses penghapusan" }, "deprecated_service_single_alternate_target": { + "description": "Perbarui semua otomasi atau skrip yang menggunakan layanan ini untuk menggunakan layanan `{alternate_service}` dengan `{alternate_targets}` sebagai target.", "title": "Layanan {deprecated_service} dalam proses penghapusan" } }, diff --git a/homeassistant/components/openuv/translations/pl.json b/homeassistant/components/openuv/translations/pl.json index 6aff15beef1..6578e6fcf84 100644 --- a/homeassistant/components/openuv/translations/pl.json +++ b/homeassistant/components/openuv/translations/pl.json @@ -18,6 +18,16 @@ } } }, + "issues": { + "deprecated_service_multiple_alternate_targets": { + "description": "Zaktualizuj wszystkie automatyzacje lub skrypty, kt\u00f3re u\u017cywaj\u0105 tej us\u0142ugi, aby zamiast tego u\u017cywa\u0142y us\u0142ugi `{alternate_service}` z jednym z tych identyfikator\u00f3w encji jako docelow\u0105: `{alternate_targets}`.", + "title": "Us\u0142uga {deprecated_service} zostanie usuni\u0119ta" + }, + "deprecated_service_single_alternate_target": { + "description": "Zaktualizuj wszystkie automatyzacje lub skrypty korzystaj\u0105ce z tej us\u0142ugi, aby zamiast tego u\u017cywa\u0142y us\u0142ugi `{alternate_service}` z `{alternate_targets}` jako docelow\u0105.", + "title": "Us\u0142uga {deprecated_service} zostanie usuni\u0119ta" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/rainmachine/translations/el.json b/homeassistant/components/rainmachine/translations/el.json index a244cc58ab3..313f2bfc1fb 100644 --- a/homeassistant/components/rainmachine/translations/el.json +++ b/homeassistant/components/rainmachine/translations/el.json @@ -18,6 +18,19 @@ } } }, + "issues": { + "replaced_old_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u0395\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03c3\u03c4\u03b5 \u03cc\u03bb\u03bf\u03c5\u03c2 \u03c4\u03bf\u03c5\u03c2 \u03b1\u03c5\u03c4\u03bf\u03bc\u03b1\u03c4\u03b9\u03c3\u03bc\u03bf\u03cd\u03c2 \u03ae \u03c4\u03b1 \u03c3\u03b5\u03bd\u03ac\u03c1\u03b9\u03b1 \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03bf\u03cd\u03bd \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7\u03bd \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 \u03ce\u03c3\u03c4\u03b5 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03bf\u03cd\u03bd \u03b1\u03bd\u03c4\u03af \u03b3\u03b9\u03b1 \u03b1\u03c5\u03c4\u03ae\u03bd \u03c4\u03bf `{replacement_entity_id}`.", + "title": "\u0397 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 {old_entity_id} \u03b8\u03b1 \u03b1\u03c6\u03b1\u03b9\u03c1\u03b5\u03b8\u03b5\u03af" + } + } + }, + "title": "\u0397 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 {old_entity_id} \u03b8\u03b1 \u03b1\u03c6\u03b1\u03b9\u03c1\u03b5\u03b8\u03b5\u03af" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/rainmachine/translations/fr.json b/homeassistant/components/rainmachine/translations/fr.json index d9a6c011755..63f5af55527 100644 --- a/homeassistant/components/rainmachine/translations/fr.json +++ b/homeassistant/components/rainmachine/translations/fr.json @@ -18,6 +18,19 @@ } } }, + "issues": { + "replaced_old_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "Modifiez tout script ou automatisation utilisant cette entit\u00e9 afin qu'ils utilisent `{replacement_entity_id}` \u00e0 la place.", + "title": "L'entit\u00e9 {old_entity_id} sera supprim\u00e9e" + } + } + }, + "title": "L'entit\u00e9 {old_entity_id} sera supprim\u00e9e" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/rainmachine/translations/id.json b/homeassistant/components/rainmachine/translations/id.json index bcbb9126b2a..8223fb4a792 100644 --- a/homeassistant/components/rainmachine/translations/id.json +++ b/homeassistant/components/rainmachine/translations/id.json @@ -18,6 +18,19 @@ } } }, + "issues": { + "replaced_old_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "Perbarui semua otomasi atau skrip yang menggunakan entitas ini untuk menggunakan `{replacement_entity_id}`.", + "title": "Entitas {old_entity_id} akan dihapus" + } + } + }, + "title": "Entitas {old_entity_id} akan dihapus" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/roon/translations/el.json b/homeassistant/components/roon/translations/el.json index 1216ff032a0..058bd0369a6 100644 --- a/homeassistant/components/roon/translations/el.json +++ b/homeassistant/components/roon/translations/el.json @@ -18,6 +18,10 @@ "link": { "description": "\u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03bf\u03c4\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03c3\u03c4\u03bf Roon. \u0391\u03c6\u03bf\u03cd \u03ba\u03ac\u03bd\u03b5\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03b7\u03bd \u03c5\u03c0\u03bf\u03b2\u03bf\u03bb\u03ae, \u03bc\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae Roon Core, \u03b1\u03bd\u03bf\u03af\u03be\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03ba\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf HomeAssistant \u03c3\u03c4\u03b7\u03bd \u03ba\u03b1\u03c1\u03c4\u03ad\u03bb\u03b1 \u0395\u03c0\u03b5\u03ba\u03c4\u03ac\u03c3\u03b5\u03b9\u03c2.", "title": "\u0395\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03bf\u03c4\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf HomeAssistant \u03c3\u03c4\u03bf Roon" + }, + "user": { + "one": "\u03ba\u03b5\u03bd\u03cc", + "other": "\u03ba\u03b5\u03bd\u03cc" } } } diff --git a/homeassistant/components/simplisafe/translations/id.json b/homeassistant/components/simplisafe/translations/id.json index a0509622f33..0bb888eceed 100644 --- a/homeassistant/components/simplisafe/translations/id.json +++ b/homeassistant/components/simplisafe/translations/id.json @@ -41,6 +41,7 @@ }, "issues": { "deprecated_service": { + "description": "Perbarui semua otomasi atau skrip yang menggunakan layanan ini untuk menggunakan layanan `{alternate_service}` dengan ID entitas target `{alternate_target}`. Kemudian, klik KIRIM di bawah ini untuk menandai masalah ini sebagai terselesaikan.", "title": "Layanan {deprecated_service} dalam proses penghapusan" } }, diff --git a/homeassistant/components/simplisafe/translations/pl.json b/homeassistant/components/simplisafe/translations/pl.json index c3eb5433359..0af42cadfd3 100644 --- a/homeassistant/components/simplisafe/translations/pl.json +++ b/homeassistant/components/simplisafe/translations/pl.json @@ -39,6 +39,12 @@ } } }, + "issues": { + "deprecated_service": { + "description": "Zaktualizuj wszystkie automatyzacje lub skrypty, kt\u00f3re u\u017cywaj\u0105 tej us\u0142ugi, aby zamiast tego u\u017cywa\u0142y us\u0142ugi `{alternate_service}` z encj\u0105 docelow\u0105 `{alternate_target}`. Nast\u0119pnie kliknij ZATWIERD\u0179 poni\u017cej, aby oznaczy\u0107 ten problem jako rozwi\u0105zany.", + "title": "Us\u0142uga {deprecated_service} zostanie usuni\u0119ta" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/sun/translations/pl.json b/homeassistant/components/sun/translations/pl.json index 0bd977db506..7e95dd568c6 100644 --- a/homeassistant/components/sun/translations/pl.json +++ b/homeassistant/components/sun/translations/pl.json @@ -12,7 +12,7 @@ "state": { "_": { "above_horizon": "nad horyzontem", - "below_horizon": "Poni\u017cej horyzontu" + "below_horizon": "poni\u017cej horyzontu" } }, "title": "S\u0142o\u0144ce" diff --git a/homeassistant/components/switchbot/translations/el.json b/homeassistant/components/switchbot/translations/el.json index 14a5af2818a..df151167751 100644 --- a/homeassistant/components/switchbot/translations/el.json +++ b/homeassistant/components/switchbot/translations/el.json @@ -7,6 +7,10 @@ "switchbot_unsupported_type": "\u039c\u03b7 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03bf\u03c2 \u03c4\u03cd\u03c0\u03bf\u03c2 Switchbot.", "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, + "error": { + "one": "\u03ba\u03b5\u03bd\u03cc", + "other": "\u03ba\u03b5\u03bd\u03cc" + }, "flow_title": "{name}", "step": { "confirm": { diff --git a/homeassistant/components/tasmota/translations/id.json b/homeassistant/components/tasmota/translations/id.json index 23ca02192db..acf952f9b93 100644 --- a/homeassistant/components/tasmota/translations/id.json +++ b/homeassistant/components/tasmota/translations/id.json @@ -16,5 +16,15 @@ "description": "Ingin menyiapkan Tasmota?" } } + }, + "issues": { + "topic_duplicated": { + "description": "Beberapa perangkat Tasmota sedang berbagi topik {topic}.\n\n Perangkat Tasmota terkait masalah ini yaitu: {offenders}.", + "title": "Beberapa perangkat Tasmota sedang berbagi topik yang sama." + }, + "topic_no_prefix": { + "description": "Perangkat Tasmota {name} dengan IP {ip} tidak menyertakan `%prefix%` dalam fulltopic-nya.\n\nEntitas untuk perangkat ini dinonaktifkan hingga konfigurasi telah diperbaiki.", + "title": "Perangkat Tasmota {name} memiliki topik MQTT yang tidak valid" + } } } \ No newline at end of file diff --git a/homeassistant/components/tasmota/translations/pl.json b/homeassistant/components/tasmota/translations/pl.json index 70ffeb5c7e2..b5e7d06b24b 100644 --- a/homeassistant/components/tasmota/translations/pl.json +++ b/homeassistant/components/tasmota/translations/pl.json @@ -16,5 +16,15 @@ "description": "Czy chcesz rozpocz\u0105\u0107 konfiguracj\u0119?" } } + }, + "issues": { + "topic_duplicated": { + "description": "Kilka urz\u0105dze\u0144 Tasmota wsp\u00f3\u0142dzieli ten sam topic {topic}.\n\nUrz\u0105dzenia Tasmota z tym problemem: {offenders}.", + "title": "Kilka urz\u0105dze\u0144 Tasmota ma ten sam topic" + }, + "topic_no_prefix": { + "description": "Urz\u0105dzenie Tasmota \"{name}\" z IP {ip} nie zawiera `%prefix%` w swoim fulltopic.\n\nEncje dla tego urz\u0105dzenia s\u0105 wy\u0142\u0105czone do czasu poprawienia konfiguracji.", + "title": "Urz\u0105dzenie Tasmota \"{name}\" ma nieprawid\u0142owy topic MQTT" + } } } \ No newline at end of file diff --git a/homeassistant/components/tile/translations/bg.json b/homeassistant/components/tile/translations/bg.json index a1394d94305..516ddb3d015 100644 --- a/homeassistant/components/tile/translations/bg.json +++ b/homeassistant/components/tile/translations/bg.json @@ -20,5 +20,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "show_inactive": "\u041f\u043e\u043a\u0430\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 \u043d\u0435\u0430\u043a\u0442\u0438\u0432\u043d\u0438\u0442\u0435 \u043f\u043b\u043e\u0447\u043a\u0438" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/upnp/translations/el.json b/homeassistant/components/upnp/translations/el.json index 8e98495d4b7..27c9c926286 100644 --- a/homeassistant/components/upnp/translations/el.json +++ b/homeassistant/components/upnp/translations/el.json @@ -5,6 +5,10 @@ "incomplete_discovery": "\u0395\u03bb\u03bb\u03b9\u03c0\u03ae\u03c2 \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7", "no_devices_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf" }, + "error": { + "one": "\u03ba\u03b5\u03bd\u03cc", + "other": "\u03ba\u03b5\u03bd\u03cc" + }, "flow_title": "{name}", "step": { "ssdp_confirm": { diff --git a/homeassistant/components/vizio/translations/bg.json b/homeassistant/components/vizio/translations/bg.json index 962de7286c7..074c44f7191 100644 --- a/homeassistant/components/vizio/translations/bg.json +++ b/homeassistant/components/vizio/translations/bg.json @@ -10,6 +10,7 @@ "step": { "user": { "data": { + "device_class": "\u0422\u0438\u043f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e", "host": "\u0425\u043e\u0441\u0442", "name": "\u0418\u043c\u0435" } diff --git a/homeassistant/components/volvooncall/translations/pl.json b/homeassistant/components/volvooncall/translations/pl.json index abf266a4f60..5bdf7a253b5 100644 --- a/homeassistant/components/volvooncall/translations/pl.json +++ b/homeassistant/components/volvooncall/translations/pl.json @@ -15,6 +15,7 @@ "password": "Has\u0142o", "region": "Region", "scandinavian_miles": "U\u017cywaj skandynawskich mil", + "unit_system": "System metryczny", "username": "Nazwa u\u017cytkownika" } } diff --git a/homeassistant/components/zha/translations/pl.json b/homeassistant/components/zha/translations/pl.json index 17563a1b8f3..6ac69d568a7 100644 --- a/homeassistant/components/zha/translations/pl.json +++ b/homeassistant/components/zha/translations/pl.json @@ -173,6 +173,7 @@ "options": { "abort": { "not_zha_device": "To urz\u0105dzenie nie jest urz\u0105dzeniem zha", + "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja.", "usb_probe_failed": "Nie uda\u0142o si\u0119 sondowa\u0107 urz\u0105dzenia USB" }, "error": { diff --git a/homeassistant/components/zwave_js/translations/id.json b/homeassistant/components/zwave_js/translations/id.json index 340266e9a9b..1aa3c5258f5 100644 --- a/homeassistant/components/zwave_js/translations/id.json +++ b/homeassistant/components/zwave_js/translations/id.json @@ -91,6 +91,12 @@ "zwave_js.value_updated.value": "Perubahan nilai pada Nilai Z-Wave JS" } }, + "issues": { + "invalid_server_version": { + "description": "Versi Z-Wave JS Server yang dijalankan saat ini terlalu lama untuk versi Home Assistant ini. Harap perbarui Z-Wave JS Server ke versi terbaru untuk memperbaiki masalah ini.", + "title": "Diperlukan versi terbaru dari Z-Wave JS Server" + } + }, "options": { "abort": { "addon_get_discovery_info_failed": "Gagal mendapatkan info penemuan add-on Z-Wave JS.", diff --git a/homeassistant/components/zwave_js/translations/pl.json b/homeassistant/components/zwave_js/translations/pl.json index 3929aa668ef..a4d5519491b 100644 --- a/homeassistant/components/zwave_js/translations/pl.json +++ b/homeassistant/components/zwave_js/translations/pl.json @@ -91,6 +91,12 @@ "zwave_js.value_updated.value": "zmieni si\u0119 warto\u015b\u0107 na Z-Wave JS" } }, + "issues": { + "invalid_server_version": { + "description": "Aktualnie uruchomiona wersja serwera Z-Wave JS jest za stara dla tej wersji Home Assistanta. Zaktualizuj serwer Z-Wave JS do najnowszej wersji, aby rozwi\u0105za\u0107 ten problem.", + "title": "Wymagana nowsza wersja serwera Z-Wave JS" + } + }, "options": { "abort": { "addon_get_discovery_info_failed": "Nie uda\u0142o si\u0119 uzyska\u0107 informacji wykrywania dodatku Z-Wave JS", From 27599ea0ee73efd6a4e21a62ba2c6b5f44c7e036 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 23 Sep 2022 03:54:22 +0200 Subject: [PATCH 658/955] Minor tweaks of hassfest and loader.py (#78929) --- homeassistant/loader.py | 4 ++-- script/hassfest/config_flow.py | 8 ++++---- script/hassfest/model.py | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/loader.py b/homeassistant/loader.py index f6da5048d45..9e326f00ffd 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -129,7 +129,7 @@ class Manifest(TypedDict, total=False): name: str disabled: str domain: str - integration_type: Literal["integration", "helper"] + integration_type: Literal["integration", "hardware", "helper"] dependencies: list[str] after_dependencies: list[str] requirements: list[str] @@ -558,7 +558,7 @@ class Integration: return self.manifest.get("iot_class") @property - def integration_type(self) -> Literal["integration", "helper"]: + def integration_type(self) -> Literal["integration", "hardware", "helper"]: """Return the integration type.""" return self.manifest.get("integration_type", "integration") diff --git a/script/hassfest/config_flow.py b/script/hassfest/config_flow.py index c3c5610a48f..b5af4da6cb4 100644 --- a/script/hassfest/config_flow.py +++ b/script/hassfest/config_flow.py @@ -18,7 +18,7 @@ FLOWS = {} UNIQUE_ID_IGNORE = {"huawei_lte", "mqtt", "adguard"} -def validate_integration(config: Config, integration: Integration): +def _validate_integration(config: Config, integration: Integration): """Validate config flow of an integration.""" config_flow_file = integration.path / "config_flow.py" @@ -67,7 +67,7 @@ def validate_integration(config: Config, integration: Integration): ) -def generate_and_validate(integrations: dict[str, Integration], config: Config): +def _generate_and_validate(integrations: dict[str, Integration], config: Config): """Validate and generate config flow data.""" domains = { "integration": [], @@ -80,7 +80,7 @@ def generate_and_validate(integrations: dict[str, Integration], config: Config): if not integration.manifest or not integration.config_flow: continue - validate_integration(config, integration) + _validate_integration(config, integration) domains[integration.integration_type].append(domain) @@ -90,7 +90,7 @@ def generate_and_validate(integrations: dict[str, Integration], config: Config): def validate(integrations: dict[str, Integration], config: Config): """Validate config flow file.""" config_flow_path = config.root / "homeassistant/generated/config_flows.py" - config.cache["config_flow"] = content = generate_and_validate(integrations, config) + config.cache["config_flow"] = content = _generate_and_validate(integrations, config) if config.specific_integrations: return diff --git a/script/hassfest/model.py b/script/hassfest/model.py index d4e1fbf806a..ab0de56077d 100644 --- a/script/hassfest/model.py +++ b/script/hassfest/model.py @@ -127,7 +127,7 @@ class Integration: self.errors.append(Error(*args, **kwargs)) def add_warning(self, *args: Any, **kwargs: Any) -> None: - """Add an warning.""" + """Add a warning.""" self.warnings.append(Error(*args, **kwargs)) def load_manifest(self) -> None: From 6b0c9b6a6aadee7a3e0f554784281ee8a45f33a7 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 23 Sep 2022 03:58:15 +0200 Subject: [PATCH 659/955] Simplify energy settings (#78947) --- homeassistant/components/energy/data.py | 18 ++++----- homeassistant/components/energy/sensor.py | 38 +++++++------------ homeassistant/components/energy/validate.py | 12 +++--- tests/components/energy/test_sensor.py | 15 -------- tests/components/energy/test_validate.py | 10 ----- tests/components/energy/test_websocket_api.py | 7 ---- 6 files changed, 28 insertions(+), 72 deletions(-) diff --git a/homeassistant/components/energy/data.py b/homeassistant/components/energy/data.py index 3b3952136be..bc7903203c4 100644 --- a/homeassistant/components/energy/data.py +++ b/homeassistant/components/energy/data.py @@ -32,12 +32,11 @@ class FlowFromGridSourceType(TypedDict): stat_energy_from: str # statistic_id of costs ($) incurred from the energy meter - # If set to None and entity_energy_from and entity_energy_price are configured, + # If set to None and entity_energy_price or number_energy_price are configured, # an EnergyCostSensor will be automatically created stat_cost: str | None # Used to generate costs if stat_cost is set to None - entity_energy_from: str | None # entity_id of an energy meter (kWh), entity_id of the energy meter for stat_energy_from entity_energy_price: str | None # entity_id of an entity providing price ($/kWh) number_energy_price: float | None # Price for energy ($/kWh) @@ -49,12 +48,11 @@ class FlowToGridSourceType(TypedDict): stat_energy_to: str # statistic_id of compensation ($) received for contributing back - # If set to None and entity_energy_from and entity_energy_price are configured, + # If set to None and entity_energy_price or number_energy_price are configured, # an EnergyCostSensor will be automatically created stat_compensation: str | None # Used to generate costs if stat_compensation is set to None - entity_energy_to: str | None # entity_id of an energy meter (kWh), entity_id of the energy meter for stat_energy_to entity_energy_price: str | None # entity_id of an entity providing price ($/kWh) number_energy_price: float | None # Price for energy ($/kWh) @@ -96,12 +94,11 @@ class GasSourceType(TypedDict): stat_energy_from: str # statistic_id of costs ($) incurred from the energy meter - # If set to None and entity_energy_from and entity_energy_price are configured, + # If set to None and entity_energy_price or number_energy_price are configured, # an EnergyCostSensor will be automatically created stat_cost: str | None # Used to generate costs if stat_cost is set to None - entity_energy_from: str | None # entity_id of an gas meter (m³), entity_id of the gas meter for stat_energy_from entity_energy_price: str | None # entity_id of an entity providing price ($/m³) number_energy_price: float | None # Price for energy ($/m³) @@ -145,7 +142,8 @@ FLOW_FROM_GRID_SOURCE_SCHEMA = vol.All( { vol.Required("stat_energy_from"): str, vol.Optional("stat_cost"): vol.Any(str, None), - vol.Optional("entity_energy_from"): vol.Any(str, None), + # entity_energy_from was removed in HA Core 2022.10 + vol.Remove("entity_energy_from"): vol.Any(str, None), vol.Optional("entity_energy_price"): vol.Any(str, None), vol.Optional("number_energy_price"): vol.Any(vol.Coerce(float), None), } @@ -158,7 +156,8 @@ FLOW_TO_GRID_SOURCE_SCHEMA = vol.Schema( { vol.Required("stat_energy_to"): str, vol.Optional("stat_compensation"): vol.Any(str, None), - vol.Optional("entity_energy_to"): vol.Any(str, None), + # entity_energy_to was removed in HA Core 2022.10 + vol.Remove("entity_energy_to"): vol.Any(str, None), vol.Optional("entity_energy_price"): vol.Any(str, None), vol.Optional("number_energy_price"): vol.Any(vol.Coerce(float), None), } @@ -216,7 +215,8 @@ GAS_SOURCE_SCHEMA = vol.Schema( vol.Required("type"): "gas", vol.Required("stat_energy_from"): str, vol.Optional("stat_cost"): vol.Any(str, None), - vol.Optional("entity_energy_from"): vol.Any(str, None), + # entity_energy_from was removed in HA Core 2022.10 + vol.Remove("entity_energy_from"): vol.Any(str, None), vol.Optional("entity_energy_price"): vol.Any(str, None), vol.Optional("number_energy_price"): vol.Any(vol.Coerce(float), None), } diff --git a/homeassistant/components/energy/sensor.py b/homeassistant/components/energy/sensor.py index 602fc09f602..db156a2d6cc 100644 --- a/homeassistant/components/energy/sensor.py +++ b/homeassistant/components/energy/sensor.py @@ -67,7 +67,6 @@ class SourceAdapter: source_type: Literal["grid", "gas"] flow_type: Literal["flow_from", "flow_to", None] stat_energy_key: Literal["stat_energy_from", "stat_energy_to"] - entity_energy_key: Literal["entity_energy_from", "entity_energy_to"] total_money_key: Literal["stat_cost", "stat_compensation"] name_suffix: str entity_id_suffix: str @@ -78,7 +77,6 @@ SOURCE_ADAPTERS: Final = ( "grid", "flow_from", "stat_energy_from", - "entity_energy_from", "stat_cost", "Cost", "cost", @@ -87,7 +85,6 @@ SOURCE_ADAPTERS: Final = ( "grid", "flow_to", "stat_energy_to", - "entity_energy_to", "stat_compensation", "Compensation", "compensation", @@ -96,7 +93,6 @@ SOURCE_ADAPTERS: Final = ( "gas", None, "stat_energy_from", - "entity_energy_from", "stat_cost", "Cost", "cost", @@ -183,13 +179,9 @@ class SensorManager: # Make sure the right data is there # If the entity existed, we don't pop it from to_remove so it's removed - if ( - config.get(adapter.entity_energy_key) is None - or not valid_entity_id(config[adapter.entity_energy_key]) - or ( - config.get("entity_energy_price") is None - and config.get("number_energy_price") is None - ) + if not valid_entity_id(config[adapter.stat_energy_key]) or ( + config.get("entity_energy_price") is None + and config.get("number_energy_price") is None ): return @@ -224,9 +216,7 @@ class EnergyCostSensor(SensorEntity): super().__init__() self._adapter = adapter - self.entity_id = ( - f"{config[adapter.entity_energy_key]}_{adapter.entity_id_suffix}" - ) + self.entity_id = f"{config[adapter.stat_energy_key]}_{adapter.entity_id_suffix}" self._attr_device_class = SensorDeviceClass.MONETARY self._attr_state_class = SensorStateClass.TOTAL self._config = config @@ -246,7 +236,7 @@ class EnergyCostSensor(SensorEntity): def _update_cost(self) -> None: """Update incurred costs.""" energy_state = self.hass.states.get( - cast(str, self._config[self._adapter.entity_energy_key]) + cast(str, self._config[self._adapter.stat_energy_key]) ) if energy_state is None: @@ -344,7 +334,7 @@ class EnergyCostSensor(SensorEntity): self._reset(energy_state_copy) elif state_class == SensorStateClass.TOTAL_INCREASING and reset_detected( self.hass, - cast(str, self._config[self._adapter.entity_energy_key]), + cast(str, self._config[self._adapter.stat_energy_key]), energy, float(self._last_energy_sensor_state.state), self._last_energy_sensor_state, @@ -362,13 +352,11 @@ class EnergyCostSensor(SensorEntity): async def async_added_to_hass(self) -> None: """Register callbacks.""" - energy_state = self.hass.states.get( - self._config[self._adapter.entity_energy_key] - ) + energy_state = self.hass.states.get(self._config[self._adapter.stat_energy_key]) if energy_state: name = energy_state.name else: - name = split_entity_id(self._config[self._adapter.entity_energy_key])[ + name = split_entity_id(self._config[self._adapter.stat_energy_key])[ 0 ].replace("_", " ") @@ -378,7 +366,7 @@ class EnergyCostSensor(SensorEntity): # Store stat ID in hass.data so frontend can look it up self.hass.data[DOMAIN]["cost_sensors"][ - self._config[self._adapter.entity_energy_key] + self._config[self._adapter.stat_energy_key] ] = self.entity_id @callback @@ -390,7 +378,7 @@ class EnergyCostSensor(SensorEntity): self.async_on_remove( async_track_state_change_event( self.hass, - cast(str, self._config[self._adapter.entity_energy_key]), + cast(str, self._config[self._adapter.stat_energy_key]), async_state_changed_listener, ) ) @@ -404,7 +392,7 @@ class EnergyCostSensor(SensorEntity): async def async_will_remove_from_hass(self) -> None: """Handle removing from hass.""" self.hass.data[DOMAIN]["cost_sensors"].pop( - self._config[self._adapter.entity_energy_key] + self._config[self._adapter.stat_energy_key] ) await super().async_will_remove_from_hass() @@ -423,10 +411,10 @@ class EnergyCostSensor(SensorEntity): """Return the unique ID of the sensor.""" entity_registry = er.async_get(self.hass) if registry_entry := entity_registry.async_get( - self._config[self._adapter.entity_energy_key] + self._config[self._adapter.stat_energy_key] ): prefix = registry_entry.id else: - prefix = self._config[self._adapter.entity_energy_key] + prefix = self._config[self._adapter.stat_energy_key] return f"{prefix}_{self._adapter.source_type}_{self._adapter.entity_id_suffix}" diff --git a/homeassistant/components/energy/validate.py b/homeassistant/components/energy/validate.py index 9d6b3bd53c7..b704330fa28 100644 --- a/homeassistant/components/energy/validate.py +++ b/homeassistant/components/energy/validate.py @@ -322,7 +322,7 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: ) ) - if flow.get("entity_energy_from") is not None and ( + if ( flow.get("entity_energy_price") is not None or flow.get("number_energy_price") is not None ): @@ -330,7 +330,7 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: functools.partial( _async_validate_auto_generated_cost_entity, hass, - flow["entity_energy_from"], + flow["stat_energy_from"], source_result, ) ) @@ -373,7 +373,7 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: ) ) - if flow.get("entity_energy_to") is not None and ( + if ( flow.get("entity_energy_price") is not None or flow.get("number_energy_price") is not None ): @@ -381,7 +381,7 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: functools.partial( _async_validate_auto_generated_cost_entity, hass, - flow["entity_energy_to"], + flow["stat_energy_to"], source_result, ) ) @@ -424,7 +424,7 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: ) ) - if source.get("entity_energy_from") is not None and ( + if ( source.get("entity_energy_price") is not None or source.get("number_energy_price") is not None ): @@ -432,7 +432,7 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: functools.partial( _async_validate_auto_generated_cost_entity, hass, - source["entity_energy_from"], + source["stat_energy_from"], source_result, ) ) diff --git a/tests/components/energy/test_sensor.py b/tests/components/energy/test_sensor.py index 9fa82ead2a1..364fdeb365b 100644 --- a/tests/components/energy/test_sensor.py +++ b/tests/components/energy/test_sensor.py @@ -58,7 +58,6 @@ async def test_cost_sensor_no_states(hass, hass_storage, setup_integration) -> N "flow_from": [ { "stat_energy_from": "foo", - "entity_energy_from": "foo", "stat_cost": None, "entity_energy_price": "bar", "number_energy_price": None, @@ -85,7 +84,6 @@ async def test_cost_sensor_attributes(hass, hass_storage, setup_integration) -> "flow_from": [ { "stat_energy_from": "sensor.energy_consumption", - "entity_energy_from": "sensor.energy_consumption", "stat_cost": None, "entity_energy_price": None, "number_energy_price": 1, @@ -155,7 +153,6 @@ async def test_cost_sensor_price_entity_total_increasing( "flow_from": [ { "stat_energy_from": "sensor.energy_consumption", - "entity_energy_from": "sensor.energy_consumption", "stat_cost": None, "entity_energy_price": price_entity, "number_energy_price": fixed_price, @@ -166,7 +163,6 @@ async def test_cost_sensor_price_entity_total_increasing( "flow_to": [ { "stat_energy_to": "sensor.energy_production", - "entity_energy_to": "sensor.energy_production", "stat_compensation": None, "entity_energy_price": price_entity, "number_energy_price": fixed_price, @@ -361,7 +357,6 @@ async def test_cost_sensor_price_entity_total( "flow_from": [ { "stat_energy_from": "sensor.energy_consumption", - "entity_energy_from": "sensor.energy_consumption", "stat_cost": None, "entity_energy_price": price_entity, "number_energy_price": fixed_price, @@ -372,7 +367,6 @@ async def test_cost_sensor_price_entity_total( "flow_to": [ { "stat_energy_to": "sensor.energy_production", - "entity_energy_to": "sensor.energy_production", "stat_compensation": None, "entity_energy_price": price_entity, "number_energy_price": fixed_price, @@ -569,7 +563,6 @@ async def test_cost_sensor_price_entity_total_no_reset( "flow_from": [ { "stat_energy_from": "sensor.energy_consumption", - "entity_energy_from": "sensor.energy_consumption", "stat_cost": None, "entity_energy_price": price_entity, "number_energy_price": fixed_price, @@ -580,7 +573,6 @@ async def test_cost_sensor_price_entity_total_no_reset( "flow_to": [ { "stat_energy_to": "sensor.energy_production", - "entity_energy_to": "sensor.energy_production", "stat_compensation": None, "entity_energy_price": price_entity, "number_energy_price": fixed_price, @@ -728,7 +720,6 @@ async def test_cost_sensor_handle_energy_units( "flow_from": [ { "stat_energy_from": "sensor.energy_consumption", - "entity_energy_from": "sensor.energy_consumption", "stat_cost": None, "entity_energy_price": None, "number_energy_price": 0.5, @@ -798,7 +789,6 @@ async def test_cost_sensor_handle_price_units( "flow_from": [ { "stat_energy_from": "sensor.energy_consumption", - "entity_energy_from": "sensor.energy_consumption", "stat_cost": None, "entity_energy_price": "sensor.energy_price", "number_energy_price": None, @@ -856,7 +846,6 @@ async def test_cost_sensor_handle_gas( { "type": "gas", "stat_energy_from": "sensor.gas_consumption", - "entity_energy_from": "sensor.gas_consumption", "stat_cost": None, "entity_energy_price": None, "number_energy_price": 0.5, @@ -907,7 +896,6 @@ async def test_cost_sensor_handle_gas_kwh( { "type": "gas", "stat_energy_from": "sensor.gas_consumption", - "entity_energy_from": "sensor.gas_consumption", "stat_cost": None, "entity_energy_price": None, "number_energy_price": 0.5, @@ -961,7 +949,6 @@ async def test_cost_sensor_wrong_state_class( "flow_from": [ { "stat_energy_from": "sensor.energy_consumption", - "entity_energy_from": "sensor.energy_consumption", "stat_cost": None, "entity_energy_price": None, "number_energy_price": 0.5, @@ -1023,7 +1010,6 @@ async def test_cost_sensor_state_class_measurement_no_reset( "flow_from": [ { "stat_energy_from": "sensor.energy_consumption", - "entity_energy_from": "sensor.energy_consumption", "stat_cost": None, "entity_energy_price": None, "number_energy_price": 0.5, @@ -1072,7 +1058,6 @@ async def test_inherit_source_unique_id(hass, hass_storage, setup_integration): { "type": "gas", "stat_energy_from": "sensor.gas_consumption", - "entity_energy_from": "sensor.gas_consumption", "stat_cost": None, "entity_energy_price": None, "number_energy_price": 0.5, diff --git a/tests/components/energy/test_validate.py b/tests/components/energy/test_validate.py index fe71663d41b..9e78e91f7f7 100644 --- a/tests/components/energy/test_validate.py +++ b/tests/components/energy/test_validate.py @@ -580,7 +580,6 @@ async def test_validation_grid_price_not_exist( "flow_from": [ { "stat_energy_from": "sensor.grid_consumption_1", - "entity_energy_from": "sensor.grid_consumption_1", "entity_energy_price": "sensor.grid_price_1", "number_energy_price": None, } @@ -588,7 +587,6 @@ async def test_validation_grid_price_not_exist( "flow_to": [ { "stat_energy_to": "sensor.grid_production_1", - "entity_energy_to": "sensor.grid_production_1", "entity_energy_price": None, "number_energy_price": 0.10, } @@ -657,7 +655,6 @@ async def test_validation_grid_auto_cost_entity_errors( "flow_from": [ { "stat_energy_from": "sensor.grid_consumption_1", - "entity_energy_from": None, "entity_energy_price": None, "number_energy_price": 0.20, } @@ -665,7 +662,6 @@ async def test_validation_grid_auto_cost_entity_errors( "flow_to": [ { "stat_energy_to": "sensor.grid_production_1", - "entity_energy_to": "invalid", "entity_energy_price": None, "number_energy_price": 0.10, } @@ -731,7 +727,6 @@ async def test_validation_grid_price_errors( "flow_from": [ { "stat_energy_from": "sensor.grid_consumption_1", - "entity_energy_from": "sensor.grid_consumption_1", "entity_energy_price": "sensor.grid_price_1", "number_energy_price": None, } @@ -778,13 +773,11 @@ async def test_validation_gas( { "type": "gas", "stat_energy_from": "sensor.gas_consumption_4", - "entity_energy_from": "sensor.gas_consumption_4", "entity_energy_price": "sensor.gas_price_1", }, { "type": "gas", "stat_energy_from": "sensor.gas_consumption_3", - "entity_energy_from": "sensor.gas_consumption_3", "entity_energy_price": "sensor.gas_price_2", }, ] @@ -890,7 +883,6 @@ async def test_validation_gas_no_costs_tracking( "type": "gas", "stat_energy_from": "sensor.gas_consumption_1", "stat_cost": None, - "entity_energy_from": None, "entity_energy_price": None, "number_energy_price": None, }, @@ -926,7 +918,6 @@ async def test_validation_grid_no_costs_tracking( { "stat_energy_from": "sensor.grid_energy", "stat_cost": None, - "entity_energy_from": "sensor.grid_energy", "entity_energy_price": None, "number_energy_price": None, }, @@ -935,7 +926,6 @@ async def test_validation_grid_no_costs_tracking( { "stat_energy_to": "sensor.grid_energy", "stat_cost": None, - "entity_energy_to": "sensor.grid_energy", "entity_energy_price": None, "number_energy_price": None, }, diff --git a/tests/components/energy/test_websocket_api.py b/tests/components/energy/test_websocket_api.py index 8adc091305c..cbe0e47124f 100644 --- a/tests/components/energy/test_websocket_api.py +++ b/tests/components/energy/test_websocket_api.py @@ -97,14 +97,12 @@ async def test_save_preferences( { "stat_energy_from": "sensor.heat_pump_meter", "stat_cost": "heat_pump_kwh_cost", - "entity_energy_from": None, "entity_energy_price": None, "number_energy_price": None, }, { "stat_energy_from": "sensor.heat_pump_meter_2", "stat_cost": None, - "entity_energy_from": "sensor.heat_pump_meter_2", "entity_energy_price": None, "number_energy_price": 0.20, }, @@ -113,14 +111,12 @@ async def test_save_preferences( { "stat_energy_to": "sensor.return_to_grid_peak", "stat_compensation": None, - "entity_energy_to": None, "entity_energy_price": None, "number_energy_price": None, }, { "stat_energy_to": "sensor.return_to_grid_offpeak", "stat_compensation": None, - "entity_energy_to": "sensor.return_to_grid_offpeak", "entity_energy_price": None, "number_energy_price": 0.20, }, @@ -181,7 +177,6 @@ async def test_save_preferences( { "stat_energy_from": "sensor.heat_pump_meter", "stat_cost": None, - "entity_energy_from": None, "entity_energy_price": None, "number_energy_price": None, } @@ -221,14 +216,12 @@ async def test_handle_duplicate_from_stat(hass, hass_ws_client) -> None: { "stat_energy_from": "sensor.heat_pump_meter", "stat_cost": None, - "entity_energy_from": None, "entity_energy_price": None, "number_energy_price": None, }, { "stat_energy_from": "sensor.heat_pump_meter", "stat_cost": None, - "entity_energy_from": None, "entity_energy_price": None, "number_energy_price": None, }, From 0ccb495209bd334d0df1f82513abf70747512c29 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 22 Sep 2022 22:16:24 -0400 Subject: [PATCH 660/955] Radarr Config Flow (#78965) --- CODEOWNERS | 2 + homeassistant/components/radarr/__init__.py | 116 ++++- .../components/radarr/config_flow.py | 147 ++++++ homeassistant/components/radarr/const.py | 11 + .../components/radarr/coordinator.py | 83 ++++ homeassistant/components/radarr/manifest.json | 7 +- homeassistant/components/radarr/sensor.py | 311 +++++------- homeassistant/components/radarr/strings.json | 48 ++ .../components/radarr/translations/en.json | 48 ++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 1 + requirements_test_all.txt | 1 + tests/components/radarr/__init__.py | 205 +++++++- tests/components/radarr/fixtures/command.json | 32 ++ tests/components/radarr/fixtures/movie.json | 118 +++++ .../radarr/fixtures/rootfolder-linux.json | 8 + .../radarr/fixtures/rootfolder-windows.json | 8 + .../radarr/fixtures/system-status.json | 28 ++ tests/components/radarr/test_config_flow.py | 227 +++++++++ tests/components/radarr/test_init.py | 58 +++ tests/components/radarr/test_sensor.py | 465 ++---------------- 21 files changed, 1291 insertions(+), 634 deletions(-) create mode 100644 homeassistant/components/radarr/config_flow.py create mode 100644 homeassistant/components/radarr/const.py create mode 100644 homeassistant/components/radarr/coordinator.py create mode 100644 homeassistant/components/radarr/strings.json create mode 100644 homeassistant/components/radarr/translations/en.json create mode 100644 tests/components/radarr/fixtures/command.json create mode 100644 tests/components/radarr/fixtures/movie.json create mode 100644 tests/components/radarr/fixtures/rootfolder-linux.json create mode 100644 tests/components/radarr/fixtures/rootfolder-windows.json create mode 100644 tests/components/radarr/fixtures/system-status.json create mode 100644 tests/components/radarr/test_config_flow.py create mode 100644 tests/components/radarr/test_init.py diff --git a/CODEOWNERS b/CODEOWNERS index 9c25b5a3eed..0b9c577b003 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -888,6 +888,8 @@ build.json @home-assistant/supervisor /tests/components/qwikswitch/ @kellerza /homeassistant/components/rachio/ @bdraco /tests/components/rachio/ @bdraco +/homeassistant/components/radarr/ @tkdrob +/tests/components/radarr/ @tkdrob /homeassistant/components/radio_browser/ @frenck /tests/components/radio_browser/ @frenck /homeassistant/components/radiotherm/ @bdraco @vinnyfuria diff --git a/homeassistant/components/radarr/__init__.py b/homeassistant/components/radarr/__init__.py index 24377725bfc..467f95780e8 100644 --- a/homeassistant/components/radarr/__init__.py +++ b/homeassistant/components/radarr/__init__.py @@ -1 +1,115 @@ -"""The radarr component.""" +"""The Radarr component.""" +from __future__ import annotations + +from aiopyarr.models.host_configuration import PyArrHostConfiguration +from aiopyarr.radarr_client import RadarrClient + +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_API_KEY, + CONF_PLATFORM, + CONF_URL, + CONF_VERIFY_SSL, + Platform, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue +from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DEFAULT_NAME, DOMAIN +from .coordinator import ( + DiskSpaceDataUpdateCoordinator, + MoviesDataUpdateCoordinator, + RadarrDataUpdateCoordinator, + StatusDataUpdateCoordinator, +) + +PLATFORMS = [Platform.SENSOR] + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the Steam integration.""" + if SENSOR_DOMAIN not in config: + return True + + for entry in config[SENSOR_DOMAIN]: + if entry[CONF_PLATFORM] == DOMAIN: + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2022.10.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + ) + async_create_issue( + hass, + DOMAIN, + "removed_attributes", + breaks_in_ha_version="2022.10.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="removed_attributes", + ) + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Radarr from a config entry.""" + host_configuration = PyArrHostConfiguration( + api_token=entry.data[CONF_API_KEY], + verify_ssl=entry.data[CONF_VERIFY_SSL], + url=entry.data[CONF_URL], + ) + radarr = RadarrClient( + host_configuration=host_configuration, + session=async_get_clientsession(hass, entry.data[CONF_VERIFY_SSL]), + ) + coordinators: dict[str, RadarrDataUpdateCoordinator] = { + "status": StatusDataUpdateCoordinator(hass, host_configuration, radarr), + "disk_space": DiskSpaceDataUpdateCoordinator(hass, host_configuration, radarr), + "movie": MoviesDataUpdateCoordinator(hass, host_configuration, radarr), + } + # Temporary, until we add diagnostic entities + _version = None + for coordinator in coordinators.values(): + await coordinator.async_config_entry_first_refresh() + if isinstance(coordinator, StatusDataUpdateCoordinator): + _version = coordinator.data + coordinator.system_version = _version + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinators + 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 + + +class RadarrEntity(CoordinatorEntity[RadarrDataUpdateCoordinator]): + """Defines a base Radarr entity.""" + + coordinator: RadarrDataUpdateCoordinator + + @property + def device_info(self) -> DeviceInfo: + """Return device information about the Radarr instance.""" + return DeviceInfo( + configuration_url=self.coordinator.host_configuration.url, + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, self.coordinator.config_entry.entry_id)}, + manufacturer=DEFAULT_NAME, + name=self.coordinator.config_entry.title, + sw_version=self.coordinator.system_version, + ) diff --git a/homeassistant/components/radarr/config_flow.py b/homeassistant/components/radarr/config_flow.py new file mode 100644 index 00000000000..af74922402a --- /dev/null +++ b/homeassistant/components/radarr/config_flow.py @@ -0,0 +1,147 @@ +"""Config flow for Radarr.""" +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any + +from aiohttp import ClientConnectorError +from aiopyarr import exceptions +from aiopyarr.models.host_configuration import PyArrHostConfiguration +from aiopyarr.radarr_client import RadarrClient +import voluptuous as vol + +from homeassistant.config_entries import ConfigEntry, ConfigFlow +from homeassistant.const import ( + CONF_API_KEY, + CONF_HOST, + CONF_PORT, + CONF_SSL, + CONF_URL, + 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 DEFAULT_NAME, DEFAULT_URL, DOMAIN, LOGGER + + +class RadarrConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Radarr.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize the flow.""" + self.entry: ConfigEntry | None = None + + async def async_step_reauth(self, _: Mapping[str, Any]) -> FlowResult: + """Handle configuration by re-auth.""" + self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: + """Confirm reauth dialog.""" + if user_input is not None: + return await self.async_step_user() + + self._set_confirm_only() + return self.async_show_form(step_id="reauth_confirm") + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initiated by the user.""" + errors = {} + + if user_input is None: + user_input = dict(self.entry.data) if self.entry else None + + else: + try: + result = await validate_input(self.hass, user_input) + if isinstance(result, tuple): + user_input[CONF_API_KEY] = result[1] + elif isinstance(result, str): + errors = {"base": result} + except exceptions.ArrAuthenticationException: + errors = {"base": "invalid_auth"} + except (ClientConnectorError, exceptions.ArrConnectionException): + errors = {"base": "cannot_connect"} + except exceptions.ArrException: + errors = {"base": "unknown"} + if not errors: + if self.entry: + self.hass.config_entries.async_update_entry( + self.entry, data=user_input + ) + await self.hass.config_entries.async_reload(self.entry.entry_id) + + return self.async_abort(reason="reauth_successful") + + return self.async_create_entry( + title=DEFAULT_NAME, + data=user_input, + ) + + user_input = user_input or {} + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required( + CONF_URL, default=user_input.get(CONF_URL, DEFAULT_URL) + ): str, + vol.Optional(CONF_API_KEY): str, + vol.Optional( + CONF_VERIFY_SSL, + default=user_input.get(CONF_VERIFY_SSL, False), + ): bool, + } + ), + errors=errors, + ) + + async def async_step_import(self, config: dict[str, Any]) -> FlowResult: + """Import a config entry from configuration.yaml.""" + for entry in self._async_current_entries(): + if entry.data[CONF_API_KEY] == config[CONF_API_KEY]: + _part = config[CONF_API_KEY][0:4] + _msg = f"Radarr yaml config with partial key {_part} has been imported. Please remove it" + LOGGER.warning(_msg) + return self.async_abort(reason="already_configured") + proto = "https" if config[CONF_SSL] else "http" + host_port = f"{config[CONF_HOST]}:{config[CONF_PORT]}" + path = "" + if config["urlbase"].rstrip("/") not in ("", "/", "/api"): + path = config["urlbase"].rstrip("/") + return self.async_create_entry( + title=DEFAULT_NAME, + data={ + CONF_URL: f"{proto}://{host_port}{path}", + CONF_API_KEY: config[CONF_API_KEY], + CONF_VERIFY_SSL: False, + }, + ) + + +async def validate_input( + hass: HomeAssistant, data: dict[str, Any] +) -> tuple[str, str, str] | str | None: + """Validate the user input allows us to connect.""" + host_configuration = PyArrHostConfiguration( + api_token=data.get(CONF_API_KEY, ""), + verify_ssl=data[CONF_VERIFY_SSL], + url=data[CONF_URL], + ) + radarr = RadarrClient( + host_configuration=host_configuration, + session=async_get_clientsession(hass), + ) + if CONF_API_KEY not in data: + return await radarr.async_try_zeroconf() + await radarr.async_get_system_status() + return None diff --git a/homeassistant/components/radarr/const.py b/homeassistant/components/radarr/const.py new file mode 100644 index 00000000000..b3320cf63a4 --- /dev/null +++ b/homeassistant/components/radarr/const.py @@ -0,0 +1,11 @@ +"""Constants for Radarr.""" +import logging +from typing import Final + +DOMAIN: Final = "radarr" + +# Defaults +DEFAULT_NAME = "Radarr" +DEFAULT_URL = "http://127.0.0.1:7878" + +LOGGER = logging.getLogger(__package__) diff --git a/homeassistant/components/radarr/coordinator.py b/homeassistant/components/radarr/coordinator.py new file mode 100644 index 00000000000..a66455c8cc3 --- /dev/null +++ b/homeassistant/components/radarr/coordinator.py @@ -0,0 +1,83 @@ +"""Data update coordinator for the Radarr integration.""" +from __future__ import annotations + +from abc import abstractmethod +from datetime import timedelta +from typing import Generic, TypeVar, cast + +from aiopyarr import RootFolder, exceptions +from aiopyarr.models.host_configuration import PyArrHostConfiguration +from aiopyarr.radarr_client import RadarrClient + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN, LOGGER + +T = TypeVar("T", str, list[RootFolder], int) + + +class RadarrDataUpdateCoordinator(DataUpdateCoordinator, Generic[T]): + """Data update coordinator for the Radarr integration.""" + + config_entry: ConfigEntry + + def __init__( + self, + hass: HomeAssistant, + host_configuration: PyArrHostConfiguration, + api_client: RadarrClient, + ) -> None: + """Initialize the coordinator.""" + super().__init__( + hass=hass, + logger=LOGGER, + name=DOMAIN, + update_interval=timedelta(seconds=30), + ) + self.api_client = api_client + self.host_configuration = host_configuration + self.system_version: str | None = None + + async def _async_update_data(self) -> T: + """Get the latest data from Radarr.""" + try: + return await self._fetch_data() + + except exceptions.ArrConnectionException as ex: + raise UpdateFailed(ex) from ex + except exceptions.ArrAuthenticationException as ex: + raise ConfigEntryAuthFailed( + "API Key is no longer valid. Please reauthenticate" + ) from ex + + @abstractmethod + async def _fetch_data(self) -> T: + """Fetch the actual data.""" + raise NotImplementedError + + +class StatusDataUpdateCoordinator(RadarrDataUpdateCoordinator): + """Status update coordinator for Radarr.""" + + async def _fetch_data(self) -> str: + """Fetch the data.""" + return (await self.api_client.async_get_system_status()).version + + +class DiskSpaceDataUpdateCoordinator(RadarrDataUpdateCoordinator): + """Disk space update coordinator for Radarr.""" + + async def _fetch_data(self) -> list[RootFolder]: + """Fetch the data.""" + return cast(list, await self.api_client.async_get_root_folders()) + + +class MoviesDataUpdateCoordinator(RadarrDataUpdateCoordinator): + """Movies update coordinator.""" + + async def _fetch_data(self) -> int: + """Fetch the movies data.""" + return len(cast(list, await self.api_client.async_get_movies())) diff --git a/homeassistant/components/radarr/manifest.json b/homeassistant/components/radarr/manifest.json index 611b4a33f3b..3ecb4247d87 100644 --- a/homeassistant/components/radarr/manifest.json +++ b/homeassistant/components/radarr/manifest.json @@ -2,6 +2,9 @@ "domain": "radarr", "name": "Radarr", "documentation": "https://www.home-assistant.io/integrations/radarr", - "codeowners": [], - "iot_class": "local_polling" + "requirements": ["aiopyarr==22.7.0"], + "codeowners": ["@tkdrob"], + "config_flow": true, + "iot_class": "local_polling", + "loggers": ["aiopyarr"] } diff --git a/homeassistant/components/radarr/sensor.py b/homeassistant/components/radarr/sensor.py index c0c10c5b1b3..ac678198cd7 100644 --- a/homeassistant/components/radarr/sensor.py +++ b/homeassistant/components/radarr/sensor.py @@ -1,13 +1,12 @@ """Support for Radarr.""" from __future__ import annotations -from datetime import datetime, timedelta -from http import HTTPStatus -import logging -import time -from typing import Any +from collections.abc import Callable +from copy import deepcopy +from dataclasses import dataclass +from typing import Generic -import requests +from aiopyarr import RootFolder import voluptuous as vol from homeassistant.components.sensor import ( @@ -15,6 +14,7 @@ from homeassistant.components.sensor import ( SensorEntity, SensorEntityDescription, ) +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_API_KEY, CONF_HOST, @@ -22,245 +22,164 @@ from homeassistant.const import ( CONF_PORT, CONF_SSL, DATA_BYTES, - DATA_EXABYTES, DATA_GIGABYTES, DATA_KILOBYTES, DATA_MEGABYTES, - DATA_PETABYTES, - DATA_TERABYTES, - DATA_YOTTABYTES, - DATA_ZETTABYTES, ) from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.util import dt as dt_util +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType -_LOGGER = logging.getLogger(__name__) +from . import RadarrEntity +from .const import DEFAULT_NAME, DOMAIN +from .coordinator import RadarrDataUpdateCoordinator, T -CONF_DAYS = "days" -CONF_INCLUDED = "include_paths" -CONF_UNIT = "unit" -CONF_URLBASE = "urlbase" -DEFAULT_HOST = "localhost" -DEFAULT_PORT = 7878 -DEFAULT_URLBASE = "" -DEFAULT_DAYS = "1" -DEFAULT_UNIT = DATA_GIGABYTES +def get_space(coordinator: RadarrDataUpdateCoordinator, name: str) -> str: + """Get space.""" + space = [ + mount.freeSpace / 1024 ** BYTE_SIZES.index(DATA_GIGABYTES) + for mount in coordinator.data + if name in mount.path + ] + return f"{space[0]:.2f}" -SCAN_INTERVAL = timedelta(minutes=10) -SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( - SensorEntityDescription( - key="diskspace", - name="Disk Space", +def get_modified_description( + description: RadarrSensorEntityDescription, mount: RootFolder +) -> tuple[RadarrSensorEntityDescription, str]: + """Return modified description and folder name.""" + desc = deepcopy(description) + name = mount.path.rsplit("/")[-1].rsplit("\\")[-1] + desc.key = f"{description.key}_{name}" + desc.name = f"{description.name} {name}".capitalize() + return desc, name + + +@dataclass +class RadarrSensorEntityDescriptionMixIn(Generic[T]): + """Mixin for required keys.""" + + value: Callable[[RadarrDataUpdateCoordinator[T], str], str] + + +@dataclass +class RadarrSensorEntityDescription( + SensorEntityDescription, RadarrSensorEntityDescriptionMixIn[T], Generic[T] +): + """Class to describe a Radarr sensor.""" + + description_fn: Callable[ + [RadarrSensorEntityDescription, RootFolder], + tuple[RadarrSensorEntityDescription, str] | None, + ] = lambda _, __: None + + +SENSOR_TYPES: dict[str, RadarrSensorEntityDescription] = { + "disk_space": RadarrSensorEntityDescription( + key="disk_space", + name="Disk space", native_unit_of_measurement=DATA_GIGABYTES, icon="mdi:harddisk", + value=get_space, + description_fn=get_modified_description, ), - SensorEntityDescription( - key="upcoming", - name="Upcoming", - native_unit_of_measurement="Movies", - icon="mdi:television", - ), - SensorEntityDescription( + "movie": RadarrSensorEntityDescription( key="movies", name="Movies", native_unit_of_measurement="Movies", icon="mdi:television", + entity_registry_enabled_default=False, + value=lambda coordinator, _: coordinator.data, ), - SensorEntityDescription( - key="commands", - name="Commands", - native_unit_of_measurement="Commands", - icon="mdi:code-braces", - ), - SensorEntityDescription( - key="status", - name="Status", - native_unit_of_measurement="Status", - icon="mdi:information", - ), -) - -SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES] - -ENDPOINTS = { - "diskspace": "{0}://{1}:{2}/{3}api/diskspace", - "upcoming": "{0}://{1}:{2}/{3}api/calendar?start={4}&end={5}", - "movies": "{0}://{1}:{2}/{3}api/movie", - "commands": "{0}://{1}:{2}/{3}api/command", - "status": "{0}://{1}:{2}/{3}api/system/status", } -# Support to Yottabytes for the future, why not +SENSOR_KEYS: list[str] = [description.key for description in SENSOR_TYPES.values()] + BYTE_SIZES = [ DATA_BYTES, DATA_KILOBYTES, DATA_MEGABYTES, DATA_GIGABYTES, - DATA_TERABYTES, - DATA_PETABYTES, - DATA_EXABYTES, - DATA_ZETTABYTES, - DATA_YOTTABYTES, ] +# Deprecated in Home Assistant 2022.10 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_KEY): cv.string, - vol.Optional(CONF_DAYS, default=DEFAULT_DAYS): cv.string, - vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, - vol.Optional(CONF_INCLUDED, default=[]): cv.ensure_list, + vol.Optional("days", default=1): cv.string, + vol.Optional(CONF_HOST, default="localhost"): cv.string, + vol.Optional("include_paths", default=[]): cv.ensure_list, vol.Optional(CONF_MONITORED_CONDITIONS, default=["movies"]): vol.All( cv.ensure_list, [vol.In(SENSOR_KEYS)] ), - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_PORT, default=7878): cv.port, vol.Optional(CONF_SSL, default=False): cv.boolean, - vol.Optional(CONF_UNIT, default=DEFAULT_UNIT): vol.In(BYTE_SIZES), - vol.Optional(CONF_URLBASE, default=DEFAULT_URLBASE): cv.string, + vol.Optional("unit", default=DATA_GIGABYTES): cv.string, + vol.Optional("urlbase", default=""): cv.string, } ) +PARALLEL_UPDATES = 1 -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 Radarr platform.""" - conditions = config[CONF_MONITORED_CONDITIONS] - # deprecated in 2022.3 - entities = [ - RadarrSensor(hass, config, description) - for description in SENSOR_TYPES - if description.key in conditions + 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 Radarr sensors based on a config entry.""" + coordinators: dict[str, RadarrDataUpdateCoordinator] = hass.data[DOMAIN][ + entry.entry_id ] - add_entities(entities, True) + entities = [] + for coordinator_type, description in SENSOR_TYPES.items(): + coordinator = coordinators[coordinator_type] + if coordinator_type != "disk_space": + entities.append(RadarrSensor(coordinator, description)) + else: + entities.extend( + RadarrSensor(coordinator, *get_modified_description(description, mount)) + for mount in coordinator.data + if description.description_fn + ) + async_add_entities(entities) -class RadarrSensor(SensorEntity): +class RadarrSensor(RadarrEntity, SensorEntity): """Implementation of the Radarr sensor.""" - def __init__(self, hass, conf, description: SensorEntityDescription): - """Create Radarr entity.""" - self.entity_description = description + coordinator: RadarrDataUpdateCoordinator + entity_description: RadarrSensorEntityDescription - self.conf = conf - self.host = conf.get(CONF_HOST) - self.port = conf.get(CONF_PORT) - self.urlbase = conf.get(CONF_URLBASE) - if self.urlbase: - self.urlbase = f"{self.urlbase.strip('/')}/" - self.apikey = conf.get(CONF_API_KEY) - self.included = conf.get(CONF_INCLUDED) - self.days = int(conf.get(CONF_DAYS)) - self.ssl = "https" if conf.get(CONF_SSL) else "http" - self.data: list[Any] = [] - self._attr_name = f"Radarr {description.name}" - if description.key == "diskspace": - self._attr_native_unit_of_measurement = conf.get(CONF_UNIT) - self._attr_available = False + def __init__( + self, + coordinator: RadarrDataUpdateCoordinator, + description: RadarrSensorEntityDescription, + folder_name: str = "", + ) -> None: + """Create Radarr entity.""" + super().__init__(coordinator) + self.entity_description = description + self._attr_name = f"{DEFAULT_NAME} {description.name}" + self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{description.key}" + self.folder_name = folder_name @property - def extra_state_attributes(self): - """Return the state attributes of the sensor.""" - attributes = {} - sensor_type = self.entity_description.key - if sensor_type == "upcoming": - for movie in self.data: - attributes[to_key(movie)] = get_release_date(movie) - elif sensor_type == "commands": - for command in self.data: - attributes[command["name"]] = command["state"] - elif sensor_type == "diskspace": - for data in self.data: - free_space = to_unit(data["freeSpace"], self.native_unit_of_measurement) - total_space = to_unit( - data["totalSpace"], self.native_unit_of_measurement - ) - percentage_used = ( - 0 if total_space == 0 else free_space / total_space * 100 - ) - attributes[data["path"]] = "{:.2f}/{:.2f}{} ({:.2f}%)".format( - free_space, - total_space, - self.native_unit_of_measurement, - percentage_used, - ) - elif sensor_type == "movies": - for movie in self.data: - attributes[to_key(movie)] = movie["downloaded"] - elif sensor_type == "status": - attributes = self.data - - return attributes - - def update(self): - """Update the data for the sensor.""" - sensor_type = self.entity_description.key - time_zone = dt_util.get_time_zone(self.hass.config.time_zone) - start = get_date(time_zone) - end = get_date(time_zone, self.days) - try: - res = requests.get( - ENDPOINTS[sensor_type].format( - self.ssl, self.host, self.port, self.urlbase, start, end - ), - headers={"X-Api-Key": self.apikey}, - timeout=10, - ) - except OSError: - _LOGGER.warning("Host %s is not available", self.host) - self._attr_available = False - self._attr_native_value = None - return - - if res.status_code == HTTPStatus.OK: - if sensor_type in ("upcoming", "movies", "commands"): - self.data = res.json() - self._attr_native_value = len(self.data) - elif sensor_type == "diskspace": - # If included paths are not provided, use all data - if self.included == []: - self.data = res.json() - else: - # Filter to only show lists that are included - self.data = list( - filter(lambda x: x["path"] in self.included, res.json()) - ) - self._attr_native_value = "{:.2f}".format( - to_unit( - sum(data["freeSpace"] for data in self.data), - self.native_unit_of_measurement, - ) - ) - elif sensor_type == "status": - self.data = res.json() - self._attr_native_value = self.data["version"] - self._attr_available = True - - -def get_date(zone, offset=0): - """Get date based on timezone and offset of days.""" - day = 60 * 60 * 24 - return datetime.date(datetime.fromtimestamp(time.time() + day * offset, tz=zone)) - - -def get_release_date(data): - """Get release date.""" - if not (date := data.get("physicalRelease")): - date = data.get("inCinemas") - return date - - -def to_key(data): - """Get key.""" - return "{} ({})".format(data["title"], data["year"]) - - -def to_unit(value, unit): - """Convert bytes to give unit.""" - return value / 1024 ** BYTE_SIZES.index(unit) + def native_value(self) -> StateType: + """Return the state of the sensor.""" + return self.entity_description.value(self.coordinator, self.folder_name) diff --git a/homeassistant/components/radarr/strings.json b/homeassistant/components/radarr/strings.json new file mode 100644 index 00000000000..47e7aebce02 --- /dev/null +++ b/homeassistant/components/radarr/strings.json @@ -0,0 +1,48 @@ +{ + "config": { + "step": { + "user": { + "description": "API key can be retrieved automatically if login credentials were not set in application.\nYour API key can be found in Settings > General in the Radarr Web UI.", + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]", + "url": "[%key:common::config_flow::data::url%]", + "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]" + } + }, + "reauth_confirm": { + "title": "[%key:common::config_flow::title::reauth%]", + "description": "The Radarr integration needs to be manually re-authenticated with the Radarr API" + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "zeroconf_failed": "API key not found. Please enter it manually", + "wrong_app": "Incorrect application reached. Please try again", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + } + }, + "options": { + "step": { + "init": { + "data": { + "upcoming_days": "Number of upcoming days to display" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "title": "The Radarr YAML configuration is being removed", + "description": "Configuring Radarr using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Radarr YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + }, + "removed_attributes": { + "title": "Changes to the Radarr integration", + "description": "Some breaking changes has been made in disabling the Movies count sensor out of caution.\n\nThis sensor can cause problems with massive databases. If you still wish to use it, you may do so.\n\nMovie names are no longer included as attributes in the movies sensor.\n\nUpcoming has been removed. It is being modernized as calendar items should be. Disk space is now split into different sensors, one for each folder.\n\nStatus and commands have been removed as they don't appear to have real value for automations." + } + } +} diff --git a/homeassistant/components/radarr/translations/en.json b/homeassistant/components/radarr/translations/en.json new file mode 100644 index 00000000000..7172eed0021 --- /dev/null +++ b/homeassistant/components/radarr/translations/en.json @@ -0,0 +1,48 @@ +{ + "config": { + "abort": { + "already_configured": "Service is already configured", + "reauth_successful": "Re-authentication was successful" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "zeroconf_failed": "API key not found. Please enter it manually", + "wrong_app": "Incorrect application reached. Please try again", + "unknown": "Unexpected error" + }, + "step": { + "reauth_confirm": { + "title": "Reauthenticate Integration", + "description": "The Radarr integration needs to be manually re-authenticated with the Radarr API" + }, + "user": { + "description": "API key can be retrieved automatically if login credentials were not set in application.\nYour API key can be found in Settings > General in the Radarr Web UI.", + "data": { + "api_key": "API Key", + "url": "URL", + "verify_ssl": "Verify SSL certificate" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "upcoming_days": "Number of upcoming days to display" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "title": "The Radarr YAML configuration is being removed", + "description": "Configuring Radarr using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Radarr YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + }, + "removed_attributes": { + "title": "Changes to the Radarr integration", + "description": "Some breaking changes has been made in disabling the Movies count sensor out of caution.\n\nThis sensor can cause problems with massive databases. If you still wish to use it, you may do so.\n\nMovie names are no longer included as attributes in the movies sensor.\n\nUpcoming has been removed. It is being modernized as calendar items should be. Disk space is now split into different sensors, one for each folder.\n\nStatus and commands have been removed as they don't appear to have real value for automations." + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index b375cb4a340..21154b35a7a 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -305,6 +305,7 @@ FLOWS = { "qingping", "qnap_qsw", "rachio", + "radarr", "radio_browser", "radiotherm", "rainforest_eagle", diff --git a/requirements_all.txt b/requirements_all.txt index 4a528e1ce9f..1309e0bddff 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -235,6 +235,7 @@ aiopvapi==2.0.1 aiopvpc==3.0.0 # homeassistant.components.lidarr +# homeassistant.components.radarr # homeassistant.components.sonarr aiopyarr==22.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a4141abffe0..1c1c093f107 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -210,6 +210,7 @@ aiopvapi==2.0.1 aiopvpc==3.0.0 # homeassistant.components.lidarr +# homeassistant.components.radarr # homeassistant.components.sonarr aiopyarr==22.7.0 diff --git a/tests/components/radarr/__init__.py b/tests/components/radarr/__init__.py index 13cc76db384..c631291b37b 100644 --- a/tests/components/radarr/__init__.py +++ b/tests/components/radarr/__init__.py @@ -1 +1,204 @@ -"""Tests for the radarr component.""" +"""Tests for the Radarr component.""" +from http import HTTPStatus +from unittest.mock import patch + +from aiohttp.client_exceptions import ClientError + +from homeassistant.components.radarr.const import DOMAIN +from homeassistant.const import ( + CONF_API_KEY, + CONF_HOST, + CONF_MONITORED_CONDITIONS, + CONF_PORT, + CONF_SSL, + CONF_URL, + CONF_VERIFY_SSL, + CONTENT_TYPE_JSON, +) +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry, load_fixture +from tests.test_util.aiohttp import AiohttpClientMocker + +URL = "http://192.168.1.189:7887/test" +API_KEY = "MOCK_API_KEY" + +MOCK_REAUTH_INPUT = {CONF_API_KEY: "test-api-key-reauth"} + +MOCK_USER_INPUT = { + CONF_URL: URL, + CONF_API_KEY: API_KEY, + CONF_VERIFY_SSL: False, +} + +CONF_IMPORT_DATA = { + CONF_API_KEY: API_KEY, + CONF_HOST: "192.168.1.189", + CONF_MONITORED_CONDITIONS: ["Stream count"], + CONF_PORT: "7887", + "urlbase": "/test", + CONF_SSL: False, +} + +CONF_DATA = { + CONF_URL: URL, + CONF_API_KEY: API_KEY, + CONF_VERIFY_SSL: False, +} + + +def mock_connection( + aioclient_mock: AiohttpClientMocker, + url: str = URL, + error: bool = False, + invalid_auth: bool = False, + windows: bool = False, +) -> None: + """Mock radarr connection.""" + if error: + mock_connection_error( + aioclient_mock, + url=url, + ) + return + + if invalid_auth: + mock_connection_invalid_auth( + aioclient_mock, + url=url, + ) + return + + aioclient_mock.get( + f"{url}/api/v3/system/status", + text=load_fixture("radarr/system-status.json"), + headers={"Content-Type": CONTENT_TYPE_JSON}, + ) + + aioclient_mock.get( + f"{url}/api/v3/command", + text=load_fixture("radarr/command.json"), + headers={"Content-Type": CONTENT_TYPE_JSON}, + ) + + if windows: + aioclient_mock.get( + f"{url}/api/v3/rootfolder", + text=load_fixture("radarr/rootfolder-windows.json"), + headers={"Content-Type": CONTENT_TYPE_JSON}, + ) + else: + aioclient_mock.get( + f"{url}/api/v3/rootfolder", + text=load_fixture("radarr/rootfolder-linux.json"), + headers={"Content-Type": CONTENT_TYPE_JSON}, + ) + + aioclient_mock.get( + f"{url}/api/v3/movie", + text=load_fixture("radarr/movie.json"), + headers={"Content-Type": CONTENT_TYPE_JSON}, + ) + + +def mock_connection_error( + aioclient_mock: AiohttpClientMocker, + url: str = URL, +) -> None: + """Mock radarr connection errors.""" + aioclient_mock.get(f"{url}/api/v3/system/status", exc=ClientError) + + +def mock_connection_invalid_auth( + aioclient_mock: AiohttpClientMocker, + url: str = URL, +) -> None: + """Mock radarr invalid auth errors.""" + aioclient_mock.get(f"{url}/api/v3/system/status", status=HTTPStatus.UNAUTHORIZED) + aioclient_mock.get(f"{url}/api/v3/command", status=HTTPStatus.UNAUTHORIZED) + aioclient_mock.get(f"{url}/api/v3/movie", status=HTTPStatus.UNAUTHORIZED) + aioclient_mock.get(f"{url}/api/v3/rootfolder", status=HTTPStatus.UNAUTHORIZED) + + +def mock_connection_server_error( + aioclient_mock: AiohttpClientMocker, + url: str = URL, +) -> None: + """Mock radarr server errors.""" + aioclient_mock.get( + f"{url}/api/v3/system/status", status=HTTPStatus.INTERNAL_SERVER_ERROR + ) + aioclient_mock.get(f"{url}/api/v3/command", status=HTTPStatus.INTERNAL_SERVER_ERROR) + aioclient_mock.get(f"{url}/api/v3/movie", status=HTTPStatus.INTERNAL_SERVER_ERROR) + aioclient_mock.get( + f"{url}/api/v3/rootfolder", status=HTTPStatus.INTERNAL_SERVER_ERROR + ) + + +async def setup_integration( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + url: str = URL, + api_key: str = API_KEY, + unique_id: str = None, + skip_entry_setup: bool = False, + connection_error: bool = False, + invalid_auth: bool = False, + windows: bool = False, +) -> MockConfigEntry: + """Set up the radarr integration in Home Assistant.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=unique_id, + data={ + CONF_URL: url, + CONF_API_KEY: api_key, + CONF_VERIFY_SSL: False, + }, + ) + + entry.add_to_hass(hass) + + mock_connection( + aioclient_mock, + url=url, + error=connection_error, + invalid_auth=invalid_auth, + windows=windows, + ) + + if not skip_entry_setup: + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert await async_setup_component(hass, DOMAIN, {}) + + return entry + + +def patch_async_setup_entry(return_value=True): + """Patch the async entry setup of radarr.""" + return patch( + "homeassistant.components.radarr.async_setup_entry", + return_value=return_value, + ) + + +def patch_radarr(): + """Patch radarr api.""" + return patch("homeassistant.components.radarr.RadarrClient.async_get_system_status") + + +def create_entry(hass: HomeAssistant) -> MockConfigEntry: + """Create Efergy entry in Home Assistant.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_URL: URL, + CONF_API_KEY: API_KEY, + CONF_VERIFY_SSL: False, + }, + ) + + entry.add_to_hass(hass) + return entry diff --git a/tests/components/radarr/fixtures/command.json b/tests/components/radarr/fixtures/command.json new file mode 100644 index 00000000000..51b4d669cf5 --- /dev/null +++ b/tests/components/radarr/fixtures/command.json @@ -0,0 +1,32 @@ +[ + { + "name": "MessagingCleanup", + "commandName": "Messaging Cleanup", + "message": "Completed", + "body": { + "sendUpdatesToClient": false, + "updateScheduledTask": true, + "completionMessage": "Completed", + "requiresDiskAccess": false, + "isExclusive": false, + "isTypeExclusive": false, + "name": "MessagingCleanup", + "lastExecutionTime": "2021-11-29T19:57:46Z", + "lastStartTime": "2021-11-29T19:57:46Z", + "trigger": "scheduled", + "suppressMessages": false + }, + "priority": "low", + "status": "completed", + "queued": "2021-11-29T20:03:16Z", + "started": "2021-11-29T20:03:16Z", + "ended": "2021-11-29T20:03:16Z", + "duration": "00:00:00.0102456", + "trigger": "scheduled", + "stateChangeTime": "2021-11-29T20:03:16Z", + "sendUpdatesToClient": false, + "updateScheduledTask": true, + "lastExecutionTime": "2021-11-29T19:57:46Z", + "id": 1987776 + } +] diff --git a/tests/components/radarr/fixtures/movie.json b/tests/components/radarr/fixtures/movie.json new file mode 100644 index 00000000000..0f974859631 --- /dev/null +++ b/tests/components/radarr/fixtures/movie.json @@ -0,0 +1,118 @@ +[ + { + "id": 0, + "title": "string", + "originalTitle": "string", + "alternateTitles": [ + { + "sourceType": "tmdb", + "movieId": 1, + "title": "string", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1 + } + ], + "sortTitle": "string", + "sizeOnDisk": 0, + "overview": "string", + "inCinemas": "string", + "physicalRelease": "string", + "images": [ + { + "coverType": "poster", + "url": "string", + "remoteUrl": "string" + } + ], + "website": "string", + "year": 0, + "hasFile": true, + "youTubeTrailerId": "string", + "studio": "string", + "path": "string", + "rootFolderPath": "string", + "qualityProfileId": 0, + "monitored": true, + "minimumAvailability": "announced", + "isAvailable": true, + "folderName": "string", + "runtime": 0, + "cleanTitle": "string", + "imdbId": "string", + "tmdbId": 0, + "titleSlug": "string", + "certification": "string", + "genres": ["string"], + "tags": [0], + "added": "string", + "ratings": { + "votes": 0, + "value": 0 + }, + "movieFile": { + "movieId": 0, + "relativePath": "string", + "path": "string", + "size": 916662234, + "dateAdded": "2020-11-26T02:00:35Z", + "indexerFlags": 1, + "quality": { + "quality": { + "id": 14, + "name": "WEBRip-720p", + "source": "webrip", + "resolution": 720, + "modifier": "none" + }, + "revision": { + "version": 1, + "real": 0, + "isRepack": false + } + }, + "mediaInfo": { + "audioBitrate": 0, + "audioChannels": 2, + "audioCodec": "AAC", + "audioLanguages": "", + "audioStreamCount": 1, + "videoBitDepth": 8, + "videoBitrate": 1000000, + "videoCodec": "x264", + "videoFps": 25.0, + "resolution": "1280x534", + "runTime": "1:49:06", + "scanType": "Progressive", + "subtitles": "" + }, + "originalFilePath": "string", + "qualityCutoffNotMet": true, + "languages": [ + { + "id": 26, + "name": "Hindi" + } + ], + "edition": "", + "id": 35361 + }, + "collection": { + "name": "string", + "tmdbId": 0, + "images": [ + { + "coverType": "poster", + "url": "string", + "remoteUrl": "string" + } + ] + }, + "status": "deleted" + } +] diff --git a/tests/components/radarr/fixtures/rootfolder-linux.json b/tests/components/radarr/fixtures/rootfolder-linux.json new file mode 100644 index 00000000000..04610f0cc5c --- /dev/null +++ b/tests/components/radarr/fixtures/rootfolder-linux.json @@ -0,0 +1,8 @@ +[ + { + "path": "/downloads", + "freeSpace": 282500064232, + "unmappedFolders": [], + "id": 1 + } +] diff --git a/tests/components/radarr/fixtures/rootfolder-windows.json b/tests/components/radarr/fixtures/rootfolder-windows.json new file mode 100644 index 00000000000..0e903d13942 --- /dev/null +++ b/tests/components/radarr/fixtures/rootfolder-windows.json @@ -0,0 +1,8 @@ +[ + { + "path": "D:\\Downloads\\TV", + "freeSpace": 282500064232, + "unmappedFolders": [], + "id": 1 + } +] diff --git a/tests/components/radarr/fixtures/system-status.json b/tests/components/radarr/fixtures/system-status.json new file mode 100644 index 00000000000..9c6de2045e4 --- /dev/null +++ b/tests/components/radarr/fixtures/system-status.json @@ -0,0 +1,28 @@ +{ + "version": "10.0.0.34882", + "buildTime": "2020-09-01T23:23:23.9621974Z", + "isDebug": true, + "isProduction": false, + "isAdmin": false, + "isUserInteractive": true, + "startupPath": "C:\\ProgramData\\Radarr", + "appData": "C:\\ProgramData\\Radarr", + "osName": "Windows", + "osVersion": "10.0.18363.0", + "isNetCore": true, + "isMono": false, + "isLinux": false, + "isOsx": false, + "isWindows": true, + "isDocker": false, + "mode": "console", + "branch": "nightly", + "authentication": "none", + "sqliteVersion": "3.32.1", + "migrationVersion": 180, + "urlBase": "", + "runtimeVersion": "3.1.10", + "runtimeName": "netCore", + "startTime": "2020-09-01T23:50:20.2415965Z", + "packageUpdateMechanism": "builtIn" +} diff --git a/tests/components/radarr/test_config_flow.py b/tests/components/radarr/test_config_flow.py new file mode 100644 index 00000000000..ffd6b5f5759 --- /dev/null +++ b/tests/components/radarr/test_config_flow.py @@ -0,0 +1,227 @@ +"""Test Radarr config flow.""" +from unittest.mock import AsyncMock, patch + +from aiopyarr import ArrException + +from homeassistant import data_entry_flow +from homeassistant.components.radarr.const import DEFAULT_NAME, DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH, SOURCE_USER +from homeassistant.const import CONF_API_KEY, CONF_SOURCE, CONF_URL, CONF_VERIFY_SSL +from homeassistant.core import HomeAssistant + +from . import ( + API_KEY, + CONF_DATA, + CONF_IMPORT_DATA, + MOCK_REAUTH_INPUT, + MOCK_USER_INPUT, + URL, + mock_connection, + mock_connection_error, + mock_connection_invalid_auth, + patch_async_setup_entry, + setup_integration, +) + +from tests.common import MockConfigEntry +from tests.test_util.aiohttp import AiohttpClientMocker + + +def _patch_setup(): + return patch("homeassistant.components.radarr.async_setup_entry") + + +async def test_flow_import(hass: HomeAssistant): + """Test import step.""" + with patch( + "homeassistant.components.radarr.config_flow.RadarrClient.async_get_system_status", + return_value=AsyncMock(), + ), _patch_setup(): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=CONF_IMPORT_DATA, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == DEFAULT_NAME + assert result["data"] == CONF_DATA | { + CONF_URL: "http://192.168.1.189:7887/test" + } + assert result["data"][CONF_URL] == "http://192.168.1.189:7887/test" + + +async def test_flow_import_already_configured(hass: HomeAssistant): + """Test import step already configured.""" + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_INPUT) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.radarr.config_flow.RadarrClient.async_get_system_status", + return_value=AsyncMock(), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=CONF_IMPORT_DATA, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_show_user_form(hass: HomeAssistant) -> None: + """Test that the user set up form is served.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_USER}, + ) + + assert result["step_id"] == "user" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + +async def test_cannot_connect( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we show user form on connection error.""" + mock_connection_error(aioclient_mock) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_USER}, + data=MOCK_USER_INPUT, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_invalid_auth( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we show user form on invalid auth.""" + mock_connection_invalid_auth(aioclient_mock) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=MOCK_USER_INPUT + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "invalid_auth"} + + +async def test_wrong_app(hass: HomeAssistant) -> None: + """Test we show user form on wrong app.""" + with patch( + "homeassistant.components.radarr.config_flow.RadarrClient.async_try_zeroconf", + return_value="wrong_app", + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_USER}, + data={CONF_URL: URL, CONF_VERIFY_SSL: False}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "wrong_app"} + + +async def test_unknown_error(hass: HomeAssistant) -> None: + """Test we show user form on unknown error.""" + with patch( + "homeassistant.components.radarr.config_flow.RadarrClient.async_get_system_status", + side_effect=ArrException, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_USER}, + data=MOCK_USER_INPUT, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "unknown"} + + +async def test_zero_conf(hass: HomeAssistant) -> None: + """Test the manual flow for zero config.""" + with patch( + "homeassistant.components.radarr.config_flow.RadarrClient.async_try_zeroconf", + return_value=("v3", API_KEY, "/test"), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_USER}, + data={CONF_URL: URL, CONF_VERIFY_SSL: False}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == DEFAULT_NAME + assert result["data"] == CONF_DATA + + +async def test_full_reauth_flow_implementation( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the manual reauth flow from start to finish.""" + entry = await setup_integration(hass, aioclient_mock) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + CONF_SOURCE: SOURCE_REAUTH, + "entry_id": entry.entry_id, + "unique_id": entry.unique_id, + }, + data=entry.data, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "reauth_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + with patch_async_setup_entry() as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MOCK_REAUTH_INPUT + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "reauth_successful" + + assert entry.data == CONF_DATA | {CONF_API_KEY: "test-api-key-reauth"} + + mock_setup_entry.assert_called_once() + + +async def test_full_user_flow_implementation( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the full manual user flow from start to finish.""" + mock_connection(aioclient_mock) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_USER}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + with patch_async_setup_entry(): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=MOCK_USER_INPUT, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == DEFAULT_NAME + assert result["data"] == CONF_DATA + assert result["data"][CONF_URL] == "http://192.168.1.189:7887/test" diff --git a/tests/components/radarr/test_init.py b/tests/components/radarr/test_init.py new file mode 100644 index 00000000000..a6d77d884d4 --- /dev/null +++ b/tests/components/radarr/test_init.py @@ -0,0 +1,58 @@ +"""Test Radarr integration.""" +from aiopyarr import exceptions + +from homeassistant.components.radarr.const import DEFAULT_NAME, DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr + +from . import create_entry, patch_radarr, setup_integration + +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def test_setup(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker): + """Test unload.""" + entry = await setup_integration(hass, aioclient_mock) + assert entry.state == ConfigEntryState.LOADED + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state is ConfigEntryState.NOT_LOADED + assert not hass.data.get(DOMAIN) + + +async def test_async_setup_entry_not_ready( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +): + """Test that it throws ConfigEntryNotReady when exception occurs during setup.""" + entry = await setup_integration(hass, aioclient_mock, connection_error=True) + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert entry.state == ConfigEntryState.SETUP_RETRY + assert not hass.data.get(DOMAIN) + + +async def test_async_setup_entry_auth_failed(hass: HomeAssistant): + """Test that it throws ConfigEntryAuthFailed when authentication fails.""" + entry = create_entry(hass) + with patch_radarr() as radarrmock: + radarrmock.side_effect = exceptions.ArrAuthenticationException + await hass.config_entries.async_setup(entry.entry_id) + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert entry.state == ConfigEntryState.SETUP_ERROR + assert not hass.data.get(DOMAIN) + + +async def test_device_info(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker): + """Test device info.""" + entry = await setup_integration(hass, aioclient_mock) + device_registry = dr.async_get(hass) + await hass.async_block_till_done() + device = device_registry.async_get_device({(DOMAIN, entry.entry_id)}) + + assert device.configuration_url == "http://192.168.1.189:7887/test" + assert device.identifiers == {(DOMAIN, entry.entry_id)} + assert device.manufacturer == DEFAULT_NAME + assert device.name == "Mock Title" + assert device.sw_version == "10.0.0.34882" diff --git a/tests/components/radarr/test_sensor.py b/tests/components/radarr/test_sensor.py index aa8ccd74667..a95885f1b1b 100644 --- a/tests/components/radarr/test_sensor.py +++ b/tests/components/radarr/test_sensor.py @@ -1,441 +1,38 @@ -"""The tests for the Radarr platform.""" -from unittest.mock import patch +"""The tests for Radarr sensor platform.""" +from datetime import timedelta -import pytest +from homeassistant.components.radarr.sensor import SENSOR_TYPES +from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT +from homeassistant.core import HomeAssistant +import homeassistant.util.dt as dt_util -from homeassistant.const import DATA_GIGABYTES -from homeassistant.setup import async_setup_component +from . import setup_integration + +from tests.common import async_fire_time_changed +from tests.test_util.aiohttp import AiohttpClientMocker -def mocked_exception(*args, **kwargs): - """Mock exception thrown by requests.get.""" - raise OSError +async def test_sensors(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker): + """Test for successfully setting up the Radarr platform.""" + for description in SENSOR_TYPES.values(): + description.entity_registry_enabled_default = True + await setup_integration(hass, aioclient_mock) + + next_update = dt_util.utcnow() + timedelta(seconds=30) + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + + state = hass.states.get("sensor.radarr_disk_space_downloads") + assert state.state == "263.10" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "GB" + state = hass.states.get("sensor.radarr_movies") + assert state.state == "1" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "Movies" -def mocked_requests_get(*args, **kwargs): - """Mock requests.get invocations.""" +async def test_windows(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker): + """Test for successfully setting up the Radarr platform on Windows.""" + await setup_integration(hass, aioclient_mock, windows=True) - class MockResponse: - """Class to represent a mocked response.""" - - def __init__(self, json_data, status_code): - """Initialize the mock response class.""" - self.json_data = json_data - self.status_code = status_code - - def json(self): - """Return the json of the response.""" - return self.json_data - - url = str(args[0]) - if "api/calendar" in url: - return MockResponse( - [ - { - "title": "Resident Evil", - "sortTitle": "resident evil final chapter", - "sizeOnDisk": 0, - "status": "announced", - "overview": "Alice, Jill, Claire, Chris, Leon, Ada, and...", - "inCinemas": "2017-01-25T00:00:00Z", - "physicalRelease": "2017-01-27T00:00:00Z", - "images": [ - { - "coverType": "poster", - "url": ( - "/radarr/MediaCover/12/poster.jpg" - "?lastWrite=636208663600000000" - ), - }, - { - "coverType": "banner", - "url": ( - "/radarr/MediaCover/12/banner.jpg" - "?lastWrite=636208663600000000" - ), - }, - ], - "website": "", - "downloaded": "false", - "year": 2017, - "hasFile": "false", - "youTubeTrailerId": "B5yxr7lmxhg", - "studio": "Impact Pictures", - "path": "/path/to/Resident Evil The Final Chapter (2017)", - "profileId": 3, - "monitored": "false", - "runtime": 106, - "lastInfoSync": "2017-01-24T14:52:40.315434Z", - "cleanTitle": "residentevilfinalchapter", - "imdbId": "tt2592614", - "tmdbId": 173897, - "titleSlug": "resident-evil-the-final-chapter-2017", - "genres": ["Action", "Horror", "Science Fiction"], - "tags": [], - "added": "2017-01-24T14:52:39.989964Z", - "ratings": {"votes": 363, "value": 4.3}, - "alternativeTitles": ["Resident Evil: Rising"], - "qualityProfileId": 3, - "id": 12, - } - ], - 200, - ) - if "api/command" in url: - return MockResponse( - [ - { - "name": "RescanMovie", - "startedOn": "0001-01-01T00:00:00Z", - "stateChangeTime": "2014-02-05T05:09:09.2366139Z", - "sendUpdatesToClient": "true", - "state": "pending", - "id": 24, - } - ], - 200, - ) - if "api/movie" in url: - return MockResponse( - [ - { - "title": "Assassin's Creed", - "sortTitle": "assassins creed", - "sizeOnDisk": 0, - "status": "released", - "overview": "Lynch discovers he is a descendant of...", - "inCinemas": "2016-12-21T00:00:00Z", - "images": [ - { - "coverType": "poster", - "url": ( - "/radarr/MediaCover/1/poster.jpg" - "?lastWrite=636200219330000000" - ), - }, - { - "coverType": "banner", - "url": ( - "/radarr/MediaCover/1/banner.jpg" - "?lastWrite=636200219340000000" - ), - }, - ], - "website": "https://www.ubisoft.com/en-US/", - "downloaded": "false", - "year": 2016, - "hasFile": "false", - "youTubeTrailerId": "pgALJgMjXN4", - "studio": "20th Century Fox", - "path": "/path/to/Assassin's Creed (2016)", - "profileId": 6, - "monitored": "true", - "runtime": 115, - "lastInfoSync": "2017-01-23T22:05:32.365337Z", - "cleanTitle": "assassinscreed", - "imdbId": "tt2094766", - "tmdbId": 121856, - "titleSlug": "assassins-creed-121856", - "genres": ["Action", "Adventure", "Fantasy", "Science Fiction"], - "tags": [], - "added": "2017-01-14T20:18:52.938244Z", - "ratings": {"votes": 711, "value": 5.2}, - "alternativeTitles": ["Assassin's Creed: The IMAX Experience"], - "qualityProfileId": 6, - "id": 1, - } - ], - 200, - ) - if "api/diskspace" in url: - return MockResponse( - [ - { - "path": "/data", - "label": "", - "freeSpace": 282500067328, - "totalSpace": 499738734592, - } - ], - 200, - ) - if "api/system/status" in url: - return MockResponse( - { - "version": "0.2.0.210", - "buildTime": "2017-01-22T23:12:49Z", - "isDebug": "false", - "isProduction": "true", - "isAdmin": "false", - "isUserInteractive": "false", - "startupPath": "/path/to/radarr", - "appData": "/path/to/radarr/data", - "osVersion": "4.8.13.1", - "isMonoRuntime": "true", - "isMono": "true", - "isLinux": "true", - "isOsx": "false", - "isWindows": "false", - "branch": "develop", - "authentication": "forms", - "sqliteVersion": "3.16.2", - "urlBase": "", - "runtimeVersion": ( - "4.6.1 (Stable 4.6.1.3/abb06f1 Mon Oct 3 07:57:59 UTC 2016)" - ), - }, - 200, - ) - return MockResponse({"error": "Unauthorized"}, 401) - - -async def test_diskspace_no_paths(hass): - """Test getting all disk space.""" - config = { - "sensor": { - "platform": "radarr", - "api_key": "foo", - "days": "2", - "unit": DATA_GIGABYTES, - "include_paths": [], - "monitored_conditions": ["diskspace"], - } - } - - with patch( - "requests.get", - side_effect=mocked_requests_get, - ): - assert await async_setup_component(hass, "sensor", config) - await hass.async_block_till_done() - - entity = hass.states.get("sensor.radarr_disk_space") - assert entity is not None - assert entity.state == "263.10" - assert entity.attributes["icon"] == "mdi:harddisk" - assert entity.attributes["unit_of_measurement"] == DATA_GIGABYTES - assert entity.attributes["friendly_name"] == "Radarr Disk Space" - assert entity.attributes["/data"] == "263.10/465.42GB (56.53%)" - - -async def test_diskspace_paths(hass): - """Test getting diskspace for included paths.""" - config = { - "sensor": { - "platform": "radarr", - "api_key": "foo", - "days": "2", - "unit": DATA_GIGABYTES, - "include_paths": ["/data"], - "monitored_conditions": ["diskspace"], - } - } - - with patch( - "requests.get", - side_effect=mocked_requests_get, - ): - assert await async_setup_component(hass, "sensor", config) - await hass.async_block_till_done() - - entity = hass.states.get("sensor.radarr_disk_space") - assert entity is not None - assert entity.state == "263.10" - assert entity.attributes["icon"] == "mdi:harddisk" - assert entity.attributes["unit_of_measurement"] == DATA_GIGABYTES - assert entity.attributes["friendly_name"] == "Radarr Disk Space" - assert entity.attributes["/data"] == "263.10/465.42GB (56.53%)" - - -async def test_commands(hass): - """Test getting running commands.""" - config = { - "sensor": { - "platform": "radarr", - "api_key": "foo", - "days": "2", - "unit": DATA_GIGABYTES, - "include_paths": ["/data"], - "monitored_conditions": ["commands"], - } - } - - with patch( - "requests.get", - side_effect=mocked_requests_get, - ): - assert await async_setup_component(hass, "sensor", config) - await hass.async_block_till_done() - - entity = hass.states.get("sensor.radarr_commands") - assert entity is not None - assert int(entity.state) == 1 - assert entity.attributes["icon"] == "mdi:code-braces" - assert entity.attributes["unit_of_measurement"] == "Commands" - assert entity.attributes["friendly_name"] == "Radarr Commands" - assert entity.attributes["RescanMovie"] == "pending" - - -async def test_movies(hass): - """Test getting the number of movies.""" - config = { - "sensor": { - "platform": "radarr", - "api_key": "foo", - "days": "2", - "unit": DATA_GIGABYTES, - "include_paths": ["/data"], - "monitored_conditions": ["movies"], - } - } - - with patch( - "requests.get", - side_effect=mocked_requests_get, - ): - assert await async_setup_component(hass, "sensor", config) - await hass.async_block_till_done() - - entity = hass.states.get("sensor.radarr_movies") - assert entity is not None - assert int(entity.state) == 1 - assert entity.attributes["icon"] == "mdi:television" - assert entity.attributes["unit_of_measurement"] == "Movies" - assert entity.attributes["friendly_name"] == "Radarr Movies" - assert entity.attributes["Assassin's Creed (2016)"] == "false" - - -async def test_upcoming_multiple_days(hass): - """Test the upcoming movies for multiple days.""" - config = { - "sensor": { - "platform": "radarr", - "api_key": "foo", - "days": "2", - "unit": DATA_GIGABYTES, - "include_paths": ["/data"], - "monitored_conditions": ["upcoming"], - } - } - - with patch( - "requests.get", - side_effect=mocked_requests_get, - ): - assert await async_setup_component(hass, "sensor", config) - await hass.async_block_till_done() - - entity = hass.states.get("sensor.radarr_upcoming") - assert entity is not None - assert int(entity.state) == 1 - assert entity.attributes["icon"] == "mdi:television" - assert entity.attributes["unit_of_measurement"] == "Movies" - assert entity.attributes["friendly_name"] == "Radarr Upcoming" - assert entity.attributes["Resident Evil (2017)"] == "2017-01-27T00:00:00Z" - - -@pytest.mark.skip -async def test_upcoming_today(hass): - """Test filtering for a single day. - - Radarr needs to respond with at least 2 days. - """ - config = { - "sensor": { - "platform": "radarr", - "api_key": "foo", - "days": "1", - "unit": DATA_GIGABYTES, - "include_paths": ["/data"], - "monitored_conditions": ["upcoming"], - } - } - with patch( - "requests.get", - side_effect=mocked_requests_get, - ): - assert await async_setup_component(hass, "sensor", config) - await hass.async_block_till_done() - entity = hass.states.get("sensor.radarr_upcoming") - assert int(entity.state) == 1 - assert entity.attributes["icon"] == "mdi:television" - assert entity.attributes["unit_of_measurement"] == "Movies" - assert entity.attributes["friendly_name"] == "Radarr Upcoming" - assert entity.attributes["Resident Evil (2017)"] == "2017-01-27T00:00:00Z" - - -async def test_system_status(hass): - """Test the getting of the system status.""" - config = { - "sensor": { - "platform": "radarr", - "api_key": "foo", - "days": "2", - "unit": DATA_GIGABYTES, - "include_paths": ["/data"], - "monitored_conditions": ["status"], - } - } - with patch( - "requests.get", - side_effect=mocked_requests_get, - ): - assert await async_setup_component(hass, "sensor", config) - await hass.async_block_till_done() - entity = hass.states.get("sensor.radarr_status") - assert entity is not None - assert entity.state == "0.2.0.210" - assert entity.attributes["icon"] == "mdi:information" - assert entity.attributes["friendly_name"] == "Radarr Status" - assert entity.attributes["osVersion"] == "4.8.13.1" - - -async def test_ssl(hass): - """Test SSL being enabled.""" - config = { - "sensor": { - "platform": "radarr", - "api_key": "foo", - "days": "1", - "unit": DATA_GIGABYTES, - "include_paths": ["/data"], - "monitored_conditions": ["upcoming"], - "ssl": "true", - } - } - with patch( - "requests.get", - side_effect=mocked_requests_get, - ): - assert await async_setup_component(hass, "sensor", config) - await hass.async_block_till_done() - entity = hass.states.get("sensor.radarr_upcoming") - assert entity is not None - assert int(entity.state) == 1 - assert entity.attributes["icon"] == "mdi:television" - assert entity.attributes["unit_of_measurement"] == "Movies" - assert entity.attributes["friendly_name"] == "Radarr Upcoming" - assert entity.attributes["Resident Evil (2017)"] == "2017-01-27T00:00:00Z" - - -async def test_exception_handling(hass): - """Test exception being handled.""" - config = { - "sensor": { - "platform": "radarr", - "api_key": "foo", - "days": "1", - "unit": DATA_GIGABYTES, - "include_paths": ["/data"], - "monitored_conditions": ["upcoming"], - } - } - with patch( - "requests.get", - side_effect=mocked_exception, - ): - assert await async_setup_component(hass, "sensor", config) - await hass.async_block_till_done() - entity = hass.states.get("sensor.radarr_upcoming") - assert entity is not None - assert entity.state == "unavailable" + state = hass.states.get("sensor.radarr_disk_space_tv") + assert state.state == "263.10" From 2b8d582894dc23b5ac897282d22dca441749460a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 22 Sep 2022 17:39:00 -1000 Subject: [PATCH 661/955] Remove min rssi setting from iBeacon (#78843) --- .../components/ibeacon/config_flow.py | 45 +------------------ homeassistant/components/ibeacon/const.py | 3 -- .../components/ibeacon/coordinator.py | 10 ----- .../components/ibeacon/translations/en.json | 10 ----- tests/components/ibeacon/test_config_flow.py | 22 +-------- tests/components/ibeacon/test_coordinator.py | 35 +-------------- 6 files changed, 3 insertions(+), 122 deletions(-) diff --git a/homeassistant/components/ibeacon/config_flow.py b/homeassistant/components/ibeacon/config_flow.py index 0cfe425f1f9..f4d36c2e617 100644 --- a/homeassistant/components/ibeacon/config_flow.py +++ b/homeassistant/components/ibeacon/config_flow.py @@ -3,19 +3,11 @@ from __future__ import annotations from typing import Any -import voluptuous as vol - from homeassistant import config_entries from homeassistant.components import bluetooth -from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult -from homeassistant.helpers.selector import ( - NumberSelector, - NumberSelectorConfig, - NumberSelectorMode, -) -from .const import CONF_MIN_RSSI, DEFAULT_MIN_RSSI, DOMAIN +from .const import DOMAIN class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @@ -37,38 +29,3 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry(title="iBeacon Tracker", data={}) return self.async_show_form(step_id="user") - - @staticmethod - @callback - def async_get_options_flow( - config_entry: config_entries.ConfigEntry, - ) -> OptionsFlowHandler: - """Get the options flow for this handler.""" - return OptionsFlowHandler(config_entry) - - -class OptionsFlowHandler(config_entries.OptionsFlow): - """Handle a option flow for iBeacons.""" - - def __init__(self, entry: config_entries.ConfigEntry) -> None: - """Initialize options flow.""" - self.entry = entry - - async def async_step_init(self, user_input: dict | None = None) -> FlowResult: - """Handle options flow.""" - if user_input is not None: - return self.async_create_entry(title="", data=user_input) - - data_schema = vol.Schema( - { - vol.Required( - CONF_MIN_RSSI, - default=self.entry.options.get(CONF_MIN_RSSI) or DEFAULT_MIN_RSSI, - ): NumberSelector( - NumberSelectorConfig( - min=-120, max=-30, step=1, mode=NumberSelectorMode.SLIDER - ) - ), - } - ) - return self.async_show_form(step_id="init", data_schema=data_schema) diff --git a/homeassistant/components/ibeacon/const.py b/homeassistant/components/ibeacon/const.py index 2a8b760c8d3..9b7a5a81dd3 100644 --- a/homeassistant/components/ibeacon/const.py +++ b/homeassistant/components/ibeacon/const.py @@ -28,6 +28,3 @@ UPDATE_INTERVAL = timedelta(seconds=60) MAX_IDS = 10 CONF_IGNORE_ADDRESSES = "ignore_addresses" - -CONF_MIN_RSSI = "min_rssi" -DEFAULT_MIN_RSSI = -85 diff --git a/homeassistant/components/ibeacon/coordinator.py b/homeassistant/components/ibeacon/coordinator.py index 58f973c0a7d..3a18c77678a 100644 --- a/homeassistant/components/ibeacon/coordinator.py +++ b/homeassistant/components/ibeacon/coordinator.py @@ -23,8 +23,6 @@ from homeassistant.helpers.event import async_track_time_interval from .const import ( CONF_IGNORE_ADDRESSES, - CONF_MIN_RSSI, - DEFAULT_MIN_RSSI, DOMAIN, MAX_IDS, SIGNAL_IBEACON_DEVICE_NEW, @@ -110,7 +108,6 @@ class IBeaconCoordinator: """Initialize the Coordinator.""" self.hass = hass self._entry = entry - self._min_rssi = entry.options.get(CONF_MIN_RSSI) or DEFAULT_MIN_RSSI self._dev_reg = registry # iBeacon devices that do not follow the spec @@ -200,8 +197,6 @@ class IBeaconCoordinator: """Update from a bluetooth callback.""" if service_info.address in self._ignore_addresses: return - if service_info.rssi < self._min_rssi: - return if not (parsed := parse(service_info)): return group_id = f"{parsed.uuid}_{parsed.major}_{parsed.minor}" @@ -270,10 +265,6 @@ class IBeaconCoordinator: cancel() self._unavailable_trackers.clear() - async def _entry_updated(self, hass: HomeAssistant, entry: ConfigEntry) -> None: - """Handle options update.""" - self._min_rssi = entry.options.get(CONF_MIN_RSSI) or DEFAULT_MIN_RSSI - @callback def _async_check_unavailable_groups_with_random_macs(self) -> None: """Check for random mac groups that have not been seen in a while and mark them as unavailable.""" @@ -349,7 +340,6 @@ class IBeaconCoordinator: """Start the Coordinator.""" self._async_restore_from_registry() entry = self._entry - entry.async_on_unload(entry.add_update_listener(self._entry_updated)) entry.async_on_unload( bluetooth.async_register_callback( self.hass, diff --git a/homeassistant/components/ibeacon/translations/en.json b/homeassistant/components/ibeacon/translations/en.json index 1125e778b19..c1e64281104 100644 --- a/homeassistant/components/ibeacon/translations/en.json +++ b/homeassistant/components/ibeacon/translations/en.json @@ -9,15 +9,5 @@ "description": "Do you want to setup iBeacon Tracker?" } } - }, - "options": { - "step": { - "init": { - "data": { - "min_rssi": "Minimum RSSI" - }, - "description": "iBeacons with an RSSI value lower than the Minimum RSSI will be ignored. If the integration is seeing neighboring iBeacons, increasing this value may help." - } - } } } \ No newline at end of file diff --git a/tests/components/ibeacon/test_config_flow.py b/tests/components/ibeacon/test_config_flow.py index bab465e5d75..c58faf5eff6 100644 --- a/tests/components/ibeacon/test_config_flow.py +++ b/tests/components/ibeacon/test_config_flow.py @@ -3,7 +3,7 @@ from unittest.mock import patch from homeassistant import config_entries -from homeassistant.components.ibeacon.const import CONF_MIN_RSSI, DOMAIN +from homeassistant.components.ibeacon.const import DOMAIN from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -45,23 +45,3 @@ async def test_setup_user_already_setup(hass, enable_bluetooth): ) assert result["type"] == FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" - - -async def test_options_flow(hass, enable_bluetooth): - """Test setting up via user when already setup .""" - entry = MockConfigEntry(domain=DOMAIN) - entry.add_to_hass(hass) - await hass.config_entries.async_setup(entry.entry_id) - - result = await hass.config_entries.options.async_init(entry.entry_id) - await hass.async_block_till_done() - - assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "init" - - result2 = await hass.config_entries.options.async_configure( - result["flow_id"], user_input={CONF_MIN_RSSI: -70} - ) - - assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["data"] == {CONF_MIN_RSSI: -70} diff --git a/tests/components/ibeacon/test_coordinator.py b/tests/components/ibeacon/test_coordinator.py index d8981de8fa9..a732b8ec2d3 100644 --- a/tests/components/ibeacon/test_coordinator.py +++ b/tests/components/ibeacon/test_coordinator.py @@ -5,7 +5,7 @@ from dataclasses import replace import pytest -from homeassistant.components.ibeacon.const import CONF_MIN_RSSI, DOMAIN +from homeassistant.components.ibeacon.const import DOMAIN from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo from . import BLUECHARM_BEACON_SERVICE_INFO @@ -54,39 +54,6 @@ async def test_many_groups_same_address_ignored(hass): assert hass.states.get("sensor.bluecharm_177999_8105_estimated_distance") is None -async def test_ignore_anything_less_than_min_rssi(hass): - """Test entities are not created when below the min rssi.""" - entry = MockConfigEntry( - domain=DOMAIN, - data={CONF_MIN_RSSI: -60}, - ) - entry.add_to_hass(hass) - - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - inject_bluetooth_service_info( - hass, replace(BLUECHARM_BEACON_SERVICE_INFO, rssi=-100) - ) - await hass.async_block_till_done() - - assert hass.states.get("sensor.bluecharm_177999_8105_estimated_distance") is None - - inject_bluetooth_service_info( - hass, - replace( - BLUECHARM_BEACON_SERVICE_INFO, - rssi=-10, - service_uuids=["0000180f-0000-1000-8000-00805f9b34fb"], - ), - ) - await hass.async_block_till_done() - - assert ( - hass.states.get("sensor.bluecharm_177999_8105_estimated_distance") is not None - ) - - async def test_ignore_not_ibeacons(hass): """Test we ignore non-ibeacon data.""" entry = MockConfigEntry( From a7da155a2adf36e471fd9178fb674e752463e204 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 22 Sep 2022 17:39:23 -1000 Subject: [PATCH 662/955] Bump yalexs to 1.2.2 (#78978) --- homeassistant/components/august/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index c688aa1a775..3aef3f5960d 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -2,7 +2,7 @@ "domain": "august", "name": "August", "documentation": "https://www.home-assistant.io/integrations/august", - "requirements": ["yalexs==1.2.1"], + "requirements": ["yalexs==1.2.2"], "codeowners": ["@bdraco"], "dhcp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index 1309e0bddff..0644da2698e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2571,7 +2571,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==1.9.2 # homeassistant.components.august -yalexs==1.2.1 +yalexs==1.2.2 # homeassistant.components.yeelight yeelight==0.7.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1c1c093f107..ed60bd9a80c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1775,7 +1775,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==1.9.2 # homeassistant.components.august -yalexs==1.2.1 +yalexs==1.2.2 # homeassistant.components.yeelight yeelight==0.7.10 From d1da6ea04dce672e53e147143444c1d142426052 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 22 Sep 2022 17:39:38 -1000 Subject: [PATCH 663/955] Fix flapping bluetooth scanner test (#78961) --- tests/components/bluetooth/test_scanner.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/components/bluetooth/test_scanner.py b/tests/components/bluetooth/test_scanner.py index e11d0c57837..91e8ab50971 100644 --- a/tests/components/bluetooth/test_scanner.py +++ b/tests/components/bluetooth/test_scanner.py @@ -1,4 +1,5 @@ """Tests for the Bluetooth integration scanners.""" +from datetime import timedelta import time from unittest.mock import MagicMock, patch @@ -241,9 +242,11 @@ async def test_recovery_from_dbus_restart(hass, one_adapter): # We hit the timer, so we restart the scanner with patch( "homeassistant.components.bluetooth.scanner.MONOTONIC_TIME", - return_value=start_time_monotonic + SCANNER_WATCHDOG_TIMEOUT, + return_value=start_time_monotonic + SCANNER_WATCHDOG_TIMEOUT + 20, ): - async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) + async_fire_time_changed( + hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL + timedelta(seconds=20) + ) await hass.async_block_till_done() assert called_start == 2 From 95e3572277a29b7a3d6276852a237405a8389175 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Fri, 23 Sep 2022 03:05:55 -0400 Subject: [PATCH 664/955] Retire climacell entirely (#78901) * Retire climacell entirely * remove fixtures * remove const file * Remove all traces of climacell integration * missed some --- CODEOWNERS | 2 - .../components/climacell/__init__.py | 329 ------ .../components/climacell/config_flow.py | 52 - homeassistant/components/climacell/const.py | 225 ---- .../components/climacell/manifest.json | 11 - homeassistant/components/climacell/sensor.py | 88 -- .../components/climacell/strings.json | 13 - .../components/climacell/strings.sensor.json | 27 - .../components/climacell/translations/af.json | 9 - .../components/climacell/translations/ca.json | 13 - .../components/climacell/translations/de.json | 13 - .../components/climacell/translations/el.json | 13 - .../components/climacell/translations/en.json | 13 - .../climacell/translations/es-419.json | 13 - .../components/climacell/translations/es.json | 13 - .../components/climacell/translations/et.json | 13 - .../components/climacell/translations/fr.json | 13 - .../components/climacell/translations/hu.json | 13 - .../components/climacell/translations/id.json | 13 - .../components/climacell/translations/it.json | 13 - .../components/climacell/translations/ja.json | 13 - .../components/climacell/translations/ko.json | 13 - .../components/climacell/translations/nl.json | 13 - .../components/climacell/translations/no.json | 13 - .../components/climacell/translations/pl.json | 13 - .../climacell/translations/pt-BR.json | 13 - .../components/climacell/translations/ru.json | 13 - .../climacell/translations/sensor.bg.json | 8 - .../climacell/translations/sensor.ca.json | 27 - .../climacell/translations/sensor.de.json | 27 - .../climacell/translations/sensor.el.json | 27 - .../climacell/translations/sensor.en.json | 27 - .../climacell/translations/sensor.es-419.json | 27 - .../climacell/translations/sensor.es.json | 27 - .../climacell/translations/sensor.et.json | 27 - .../climacell/translations/sensor.fr.json | 27 - .../climacell/translations/sensor.he.json | 7 - .../climacell/translations/sensor.hu.json | 27 - .../climacell/translations/sensor.id.json | 27 - .../climacell/translations/sensor.is.json | 12 - .../climacell/translations/sensor.it.json | 27 - .../climacell/translations/sensor.ja.json | 27 - .../climacell/translations/sensor.ko.json | 7 - .../climacell/translations/sensor.lv.json | 27 - .../climacell/translations/sensor.nl.json | 27 - .../climacell/translations/sensor.no.json | 27 - .../climacell/translations/sensor.pl.json | 27 - .../climacell/translations/sensor.pt-BR.json | 27 - .../climacell/translations/sensor.pt.json | 7 - .../climacell/translations/sensor.ru.json | 27 - .../climacell/translations/sensor.sk.json | 7 - .../climacell/translations/sensor.sv.json | 27 - .../climacell/translations/sensor.tr.json | 27 - .../translations/sensor.zh-Hant.json | 27 - .../components/climacell/translations/sv.json | 13 - .../components/climacell/translations/tr.json | 13 - .../climacell/translations/zh-Hant.json | 13 - homeassistant/components/climacell/weather.py | 361 ------- .../components/tomorrowio/__init__.py | 86 +- .../components/tomorrowio/config_flow.py | 56 - homeassistant/components/tomorrowio/const.py | 30 - requirements_all.txt | 3 - requirements_test_all.txt | 3 - tests/components/climacell/__init__.py | 1 - tests/components/climacell/conftest.py | 26 - tests/components/climacell/const.py | 19 - .../climacell/fixtures/v3_forecast_daily.json | 992 ------------------ .../fixtures/v3_forecast_hourly.json | 752 ------------- .../fixtures/v3_forecast_nowcast.json | 782 -------------- .../climacell/fixtures/v3_realtime.json | 102 -- .../components/climacell/test_config_flow.py | 46 - tests/components/climacell/test_const.py | 14 - tests/components/climacell/test_init.py | 106 -- tests/components/climacell/test_sensor.py | 148 --- tests/components/climacell/test_weather.py | 223 ---- tests/components/tomorrowio/conftest.py | 19 - .../components/tomorrowio/test_config_flow.py | 75 +- tests/components/tomorrowio/test_init.py | 131 +-- 78 files changed, 9 insertions(+), 5610 deletions(-) delete mode 100644 homeassistant/components/climacell/__init__.py delete mode 100644 homeassistant/components/climacell/config_flow.py delete mode 100644 homeassistant/components/climacell/const.py delete mode 100644 homeassistant/components/climacell/manifest.json delete mode 100644 homeassistant/components/climacell/sensor.py delete mode 100644 homeassistant/components/climacell/strings.json delete mode 100644 homeassistant/components/climacell/strings.sensor.json delete mode 100644 homeassistant/components/climacell/translations/af.json delete mode 100644 homeassistant/components/climacell/translations/ca.json delete mode 100644 homeassistant/components/climacell/translations/de.json delete mode 100644 homeassistant/components/climacell/translations/el.json delete mode 100644 homeassistant/components/climacell/translations/en.json delete mode 100644 homeassistant/components/climacell/translations/es-419.json delete mode 100644 homeassistant/components/climacell/translations/es.json delete mode 100644 homeassistant/components/climacell/translations/et.json delete mode 100644 homeassistant/components/climacell/translations/fr.json delete mode 100644 homeassistant/components/climacell/translations/hu.json delete mode 100644 homeassistant/components/climacell/translations/id.json delete mode 100644 homeassistant/components/climacell/translations/it.json delete mode 100644 homeassistant/components/climacell/translations/ja.json delete mode 100644 homeassistant/components/climacell/translations/ko.json delete mode 100644 homeassistant/components/climacell/translations/nl.json delete mode 100644 homeassistant/components/climacell/translations/no.json delete mode 100644 homeassistant/components/climacell/translations/pl.json delete mode 100644 homeassistant/components/climacell/translations/pt-BR.json delete mode 100644 homeassistant/components/climacell/translations/ru.json delete mode 100644 homeassistant/components/climacell/translations/sensor.bg.json delete mode 100644 homeassistant/components/climacell/translations/sensor.ca.json delete mode 100644 homeassistant/components/climacell/translations/sensor.de.json delete mode 100644 homeassistant/components/climacell/translations/sensor.el.json delete mode 100644 homeassistant/components/climacell/translations/sensor.en.json delete mode 100644 homeassistant/components/climacell/translations/sensor.es-419.json delete mode 100644 homeassistant/components/climacell/translations/sensor.es.json delete mode 100644 homeassistant/components/climacell/translations/sensor.et.json delete mode 100644 homeassistant/components/climacell/translations/sensor.fr.json delete mode 100644 homeassistant/components/climacell/translations/sensor.he.json delete mode 100644 homeassistant/components/climacell/translations/sensor.hu.json delete mode 100644 homeassistant/components/climacell/translations/sensor.id.json delete mode 100644 homeassistant/components/climacell/translations/sensor.is.json delete mode 100644 homeassistant/components/climacell/translations/sensor.it.json delete mode 100644 homeassistant/components/climacell/translations/sensor.ja.json delete mode 100644 homeassistant/components/climacell/translations/sensor.ko.json delete mode 100644 homeassistant/components/climacell/translations/sensor.lv.json delete mode 100644 homeassistant/components/climacell/translations/sensor.nl.json delete mode 100644 homeassistant/components/climacell/translations/sensor.no.json delete mode 100644 homeassistant/components/climacell/translations/sensor.pl.json delete mode 100644 homeassistant/components/climacell/translations/sensor.pt-BR.json delete mode 100644 homeassistant/components/climacell/translations/sensor.pt.json delete mode 100644 homeassistant/components/climacell/translations/sensor.ru.json delete mode 100644 homeassistant/components/climacell/translations/sensor.sk.json delete mode 100644 homeassistant/components/climacell/translations/sensor.sv.json delete mode 100644 homeassistant/components/climacell/translations/sensor.tr.json delete mode 100644 homeassistant/components/climacell/translations/sensor.zh-Hant.json delete mode 100644 homeassistant/components/climacell/translations/sv.json delete mode 100644 homeassistant/components/climacell/translations/tr.json delete mode 100644 homeassistant/components/climacell/translations/zh-Hant.json delete mode 100644 homeassistant/components/climacell/weather.py delete mode 100644 tests/components/climacell/__init__.py delete mode 100644 tests/components/climacell/conftest.py delete mode 100644 tests/components/climacell/const.py delete mode 100644 tests/components/climacell/fixtures/v3_forecast_daily.json delete mode 100644 tests/components/climacell/fixtures/v3_forecast_hourly.json delete mode 100644 tests/components/climacell/fixtures/v3_forecast_nowcast.json delete mode 100644 tests/components/climacell/fixtures/v3_realtime.json delete mode 100644 tests/components/climacell/test_config_flow.py delete mode 100644 tests/components/climacell/test_const.py delete mode 100644 tests/components/climacell/test_init.py delete mode 100644 tests/components/climacell/test_sensor.py delete mode 100644 tests/components/climacell/test_weather.py diff --git a/CODEOWNERS b/CODEOWNERS index 0b9c577b003..917a279228a 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -177,8 +177,6 @@ build.json @home-assistant/supervisor /homeassistant/components/cisco_ios/ @fbradyirl /homeassistant/components/cisco_mobility_express/ @fbradyirl /homeassistant/components/cisco_webex_teams/ @fbradyirl -/homeassistant/components/climacell/ @raman325 -/tests/components/climacell/ @raman325 /homeassistant/components/climate/ @home-assistant/core /tests/components/climate/ @home-assistant/core /homeassistant/components/cloud/ @home-assistant/cloud diff --git a/homeassistant/components/climacell/__init__.py b/homeassistant/components/climacell/__init__.py deleted file mode 100644 index 4f2ad7c5889..00000000000 --- a/homeassistant/components/climacell/__init__.py +++ /dev/null @@ -1,329 +0,0 @@ -"""The ClimaCell integration.""" -from __future__ import annotations - -from datetime import timedelta -import logging -from math import ceil -from typing import Any - -from pyclimacell import ClimaCellV3, ClimaCellV4 -from pyclimacell.const import CURRENT, DAILY, FORECASTS, HOURLY, NOWCAST -from pyclimacell.exceptions import ( - CantConnectException, - InvalidAPIKeyException, - RateLimitedException, - UnknownException, -) - -from homeassistant.components.tomorrowio import DOMAIN as TOMORROW_DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import ( - CONF_API_KEY, - CONF_API_VERSION, - CONF_LATITUDE, - CONF_LONGITUDE, - CONF_NAME, - Platform, -) -from homeassistant.core import HomeAssistant -from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, - UpdateFailed, -) - -from .const import ( - ATTRIBUTION, - CC_V3_ATTR_CLOUD_COVER, - CC_V3_ATTR_CONDITION, - CC_V3_ATTR_HUMIDITY, - CC_V3_ATTR_OZONE, - CC_V3_ATTR_PRECIPITATION, - CC_V3_ATTR_PRECIPITATION_DAILY, - CC_V3_ATTR_PRECIPITATION_PROBABILITY, - CC_V3_ATTR_PRECIPITATION_TYPE, - CC_V3_ATTR_PRESSURE, - CC_V3_ATTR_TEMPERATURE, - CC_V3_ATTR_VISIBILITY, - CC_V3_ATTR_WIND_DIRECTION, - CC_V3_ATTR_WIND_GUST, - CC_V3_ATTR_WIND_SPEED, - CC_V3_SENSOR_TYPES, - CONF_TIMESTEP, - DEFAULT_TIMESTEP, - DOMAIN, - MAX_REQUESTS_PER_DAY, -) - -_LOGGER = logging.getLogger(__name__) - -PLATFORMS = [Platform.SENSOR, Platform.WEATHER] - - -def _set_update_interval(hass: HomeAssistant, current_entry: ConfigEntry) -> timedelta: - """Recalculate update_interval based on existing ClimaCell instances and update them.""" - api_calls = 4 if current_entry.data[CONF_API_VERSION] == 3 else 2 - # We check how many ClimaCell configured instances are using the same API key and - # calculate interval to not exceed allowed numbers of requests. Divide 90% of - # MAX_REQUESTS_PER_DAY by 4 because every update requires four API calls and we want - # a buffer in the number of API calls left at the end of the day. - other_instance_entry_ids = [ - entry.entry_id - for entry in hass.config_entries.async_entries(DOMAIN) - if entry.entry_id != current_entry.entry_id - and entry.data[CONF_API_KEY] == current_entry.data[CONF_API_KEY] - ] - - interval = timedelta( - minutes=( - ceil( - (24 * 60 * (len(other_instance_entry_ids) + 1) * api_calls) - / (MAX_REQUESTS_PER_DAY * 0.9) - ) - ) - ) - - for entry_id in other_instance_entry_ids: - if entry_id in hass.data[DOMAIN]: - hass.data[DOMAIN][entry_id].update_interval = interval - - return interval - - -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Set up ClimaCell API from a config entry.""" - hass.data.setdefault(DOMAIN, {}) - - params: dict[str, Any] = {} - # If config entry options not set up, set them up - if not entry.options: - params["options"] = { - CONF_TIMESTEP: DEFAULT_TIMESTEP, - } - else: - # Use valid timestep if it's invalid - timestep = entry.options[CONF_TIMESTEP] - if timestep not in (1, 5, 15, 30): - if timestep <= 2: - timestep = 1 - elif timestep <= 7: - timestep = 5 - elif timestep <= 20: - timestep = 15 - else: - timestep = 30 - new_options = entry.options.copy() - new_options[CONF_TIMESTEP] = timestep - params["options"] = new_options - # Add API version if not found - if CONF_API_VERSION not in entry.data: - new_data = entry.data.copy() - new_data[CONF_API_VERSION] = 3 - params["data"] = new_data - - if params: - hass.config_entries.async_update_entry(entry, **params) - - hass.async_create_task( - hass.config_entries.flow.async_init( - TOMORROW_DOMAIN, - context={"source": SOURCE_IMPORT, "old_config_entry_id": entry.entry_id}, - data=entry.data, - ) - ) - - # Eventually we will remove the code that sets up the platforms and force users to - # migrate. This will only impact users still on the V3 API because we can't - # automatically migrate them, but for V4 users, we can skip the platform setup. - if entry.data[CONF_API_VERSION] == 4: - return True - - api = ClimaCellV3( - entry.data[CONF_API_KEY], - entry.data.get(CONF_LATITUDE, hass.config.latitude), - entry.data.get(CONF_LONGITUDE, hass.config.longitude), - session=async_get_clientsession(hass), - ) - - coordinator = ClimaCellDataUpdateCoordinator( - hass, - entry, - api, - _set_update_interval(hass, entry), - ) - - await coordinator.async_config_entry_first_refresh() - - 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, config_entry: ConfigEntry) -> bool: - """Unload a config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms( - config_entry, PLATFORMS - ) - - hass.data[DOMAIN].pop(config_entry.entry_id, None) - if not hass.data[DOMAIN]: - hass.data.pop(DOMAIN) - - return unload_ok - - -class ClimaCellDataUpdateCoordinator(DataUpdateCoordinator): - """Define an object to hold ClimaCell data.""" - - def __init__( - self, - hass: HomeAssistant, - config_entry: ConfigEntry, - api: ClimaCellV3 | ClimaCellV4, - update_interval: timedelta, - ) -> None: - """Initialize.""" - - self._config_entry = config_entry - self._api_version = config_entry.data[CONF_API_VERSION] - self._api = api - self.name = config_entry.data[CONF_NAME] - self.data = {CURRENT: {}, FORECASTS: {}} - - super().__init__( - hass, - _LOGGER, - name=config_entry.data[CONF_NAME], - update_interval=update_interval, - ) - - async def _async_update_data(self) -> dict[str, Any]: - """Update data via library.""" - data: dict[str, Any] = {FORECASTS: {}} - try: - data[CURRENT] = await self._api.realtime( - [ - CC_V3_ATTR_TEMPERATURE, - CC_V3_ATTR_HUMIDITY, - CC_V3_ATTR_PRESSURE, - CC_V3_ATTR_WIND_SPEED, - CC_V3_ATTR_WIND_DIRECTION, - CC_V3_ATTR_CONDITION, - CC_V3_ATTR_VISIBILITY, - CC_V3_ATTR_OZONE, - CC_V3_ATTR_WIND_GUST, - CC_V3_ATTR_CLOUD_COVER, - CC_V3_ATTR_PRECIPITATION_TYPE, - *(sensor_type.key for sensor_type in CC_V3_SENSOR_TYPES), - ] - ) - data[FORECASTS][HOURLY] = await self._api.forecast_hourly( - [ - CC_V3_ATTR_TEMPERATURE, - CC_V3_ATTR_WIND_SPEED, - CC_V3_ATTR_WIND_DIRECTION, - CC_V3_ATTR_CONDITION, - CC_V3_ATTR_PRECIPITATION, - CC_V3_ATTR_PRECIPITATION_PROBABILITY, - ], - None, - timedelta(hours=24), - ) - - data[FORECASTS][DAILY] = await self._api.forecast_daily( - [ - CC_V3_ATTR_TEMPERATURE, - CC_V3_ATTR_WIND_SPEED, - CC_V3_ATTR_WIND_DIRECTION, - CC_V3_ATTR_CONDITION, - CC_V3_ATTR_PRECIPITATION_DAILY, - CC_V3_ATTR_PRECIPITATION_PROBABILITY, - ], - None, - timedelta(days=14), - ) - - data[FORECASTS][NOWCAST] = await self._api.forecast_nowcast( - [ - CC_V3_ATTR_TEMPERATURE, - CC_V3_ATTR_WIND_SPEED, - CC_V3_ATTR_WIND_DIRECTION, - CC_V3_ATTR_CONDITION, - CC_V3_ATTR_PRECIPITATION, - ], - None, - timedelta( - minutes=min(300, self._config_entry.options[CONF_TIMESTEP] * 30) - ), - self._config_entry.options[CONF_TIMESTEP], - ) - except ( - CantConnectException, - InvalidAPIKeyException, - RateLimitedException, - UnknownException, - ) as error: - raise UpdateFailed from error - - return data - - -class ClimaCellEntity(CoordinatorEntity[ClimaCellDataUpdateCoordinator]): - """Base ClimaCell Entity.""" - - def __init__( - self, - config_entry: ConfigEntry, - coordinator: ClimaCellDataUpdateCoordinator, - api_version: int, - ) -> None: - """Initialize ClimaCell Entity.""" - super().__init__(coordinator) - self.api_version = api_version - self._config_entry = config_entry - - @staticmethod - def _get_cc_value( - weather_dict: dict[str, Any], key: str - ) -> int | float | str | None: - """ - Return property from weather_dict. - - Used for V3 API. - """ - items = weather_dict.get(key, {}) - # Handle cases where value returned is a list. - # Optimistically find the best value to return. - if isinstance(items, list): - if len(items) == 1: - return items[0].get("value") - return next( - (item.get("value") for item in items if "max" in item), - next( - (item.get("value") for item in items if "min" in item), - items[0].get("value", None), - ), - ) - - return items.get("value") - - @property - def attribution(self): - """Return the attribution.""" - return ATTRIBUTION - - @property - def device_info(self) -> DeviceInfo: - """Return device registry information.""" - return DeviceInfo( - entry_type=DeviceEntryType.SERVICE, - identifiers={(DOMAIN, self._config_entry.data[CONF_API_KEY])}, - manufacturer="ClimaCell", - name="ClimaCell", - sw_version=f"v{self.api_version}", - ) diff --git a/homeassistant/components/climacell/config_flow.py b/homeassistant/components/climacell/config_flow.py deleted file mode 100644 index 07b85e4a4ab..00000000000 --- a/homeassistant/components/climacell/config_flow.py +++ /dev/null @@ -1,52 +0,0 @@ -"""Config flow for ClimaCell integration.""" -from __future__ import annotations - -from typing import Any - -import voluptuous as vol - -from homeassistant import config_entries -from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult - -from .const import CONF_TIMESTEP, DEFAULT_TIMESTEP, DOMAIN - - -class ClimaCellOptionsConfigFlow(config_entries.OptionsFlow): - """Handle ClimaCell options.""" - - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: - """Initialize ClimaCell options flow.""" - self._config_entry = config_entry - - async def async_step_init( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Manage the ClimaCell options.""" - if user_input is not None: - return self.async_create_entry(title="", data=user_input) - - options_schema = { - vol.Required( - CONF_TIMESTEP, - default=self._config_entry.options.get(CONF_TIMESTEP, DEFAULT_TIMESTEP), - ): vol.In([1, 5, 15, 30]), - } - - return self.async_show_form( - step_id="init", data_schema=vol.Schema(options_schema) - ) - - -class ClimaCellConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): - """Handle a config flow for ClimaCell Weather API.""" - - VERSION = 1 - - @staticmethod - @callback - def async_get_options_flow( - config_entry: config_entries.ConfigEntry, - ) -> ClimaCellOptionsConfigFlow: - """Get the options flow for this handler.""" - return ClimaCellOptionsConfigFlow(config_entry) diff --git a/homeassistant/components/climacell/const.py b/homeassistant/components/climacell/const.py deleted file mode 100644 index f7ca21259e1..00000000000 --- a/homeassistant/components/climacell/const.py +++ /dev/null @@ -1,225 +0,0 @@ -"""Constants for the ClimaCell integration.""" -from __future__ import annotations - -from collections.abc import Callable -from dataclasses import dataclass -from enum import IntEnum - -from pyclimacell.const import DAILY, HOURLY, NOWCAST, V3PollenIndex - -from homeassistant.components.sensor import SensorDeviceClass, SensorEntityDescription -from homeassistant.components.weather import ( - ATTR_CONDITION_CLEAR_NIGHT, - ATTR_CONDITION_CLOUDY, - ATTR_CONDITION_FOG, - ATTR_CONDITION_HAIL, - ATTR_CONDITION_LIGHTNING, - ATTR_CONDITION_PARTLYCLOUDY, - ATTR_CONDITION_POURING, - ATTR_CONDITION_RAINY, - ATTR_CONDITION_SNOWY, - ATTR_CONDITION_SNOWY_RAINY, - ATTR_CONDITION_SUNNY, - ATTR_CONDITION_WINDY, -) -from homeassistant.const import ( - CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT, - CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - CONCENTRATION_PARTS_PER_BILLION, - CONCENTRATION_PARTS_PER_MILLION, -) - -CONF_TIMESTEP = "timestep" -FORECAST_TYPES = [DAILY, HOURLY, NOWCAST] - -DEFAULT_NAME = "ClimaCell" -DEFAULT_TIMESTEP = 15 -DEFAULT_FORECAST_TYPE = DAILY -DOMAIN = "climacell" -ATTRIBUTION = "Powered by ClimaCell" - -MAX_REQUESTS_PER_DAY = 100 - -CLEAR_CONDITIONS = {"night": ATTR_CONDITION_CLEAR_NIGHT, "day": ATTR_CONDITION_SUNNY} - -MAX_FORECASTS = { - DAILY: 14, - HOURLY: 24, - NOWCAST: 30, -} - -# Additional attributes -ATTR_WIND_GUST = "wind_gust" -ATTR_CLOUD_COVER = "cloud_cover" -ATTR_PRECIPITATION_TYPE = "precipitation_type" - - -@dataclass -class ClimaCellSensorEntityDescription(SensorEntityDescription): - """Describes a ClimaCell sensor entity.""" - - unit_imperial: str | None = None - unit_metric: str | None = None - metric_conversion: Callable[[float], float] | float = 1.0 - is_metric_check: bool | None = None - device_class: str | None = None - value_map: IntEnum | None = None - - def __post_init__(self) -> None: - """Post initialization.""" - units = (self.unit_imperial, self.unit_metric) - if any(u is not None for u in units) and any(u is None for u in units): - raise RuntimeError( - "`unit_imperial` and `unit_metric` both need to be None or both need " - "to be defined." - ) - - -# V3 constants -CONDITIONS_V3 = { - "breezy": ATTR_CONDITION_WINDY, - "freezing_rain_heavy": ATTR_CONDITION_SNOWY_RAINY, - "freezing_rain": ATTR_CONDITION_SNOWY_RAINY, - "freezing_rain_light": ATTR_CONDITION_SNOWY_RAINY, - "freezing_drizzle": ATTR_CONDITION_SNOWY_RAINY, - "ice_pellets_heavy": ATTR_CONDITION_HAIL, - "ice_pellets": ATTR_CONDITION_HAIL, - "ice_pellets_light": ATTR_CONDITION_HAIL, - "snow_heavy": ATTR_CONDITION_SNOWY, - "snow": ATTR_CONDITION_SNOWY, - "snow_light": ATTR_CONDITION_SNOWY, - "flurries": ATTR_CONDITION_SNOWY, - "tstorm": ATTR_CONDITION_LIGHTNING, - "rain_heavy": ATTR_CONDITION_POURING, - "rain": ATTR_CONDITION_RAINY, - "rain_light": ATTR_CONDITION_RAINY, - "drizzle": ATTR_CONDITION_RAINY, - "fog_light": ATTR_CONDITION_FOG, - "fog": ATTR_CONDITION_FOG, - "cloudy": ATTR_CONDITION_CLOUDY, - "mostly_cloudy": ATTR_CONDITION_CLOUDY, - "partly_cloudy": ATTR_CONDITION_PARTLYCLOUDY, -} - -# Weather attributes -CC_V3_ATTR_TIMESTAMP = "observation_time" -CC_V3_ATTR_TEMPERATURE = "temp" -CC_V3_ATTR_TEMPERATURE_HIGH = "max" -CC_V3_ATTR_TEMPERATURE_LOW = "min" -CC_V3_ATTR_PRESSURE = "baro_pressure" -CC_V3_ATTR_HUMIDITY = "humidity" -CC_V3_ATTR_WIND_SPEED = "wind_speed" -CC_V3_ATTR_WIND_DIRECTION = "wind_direction" -CC_V3_ATTR_OZONE = "o3" -CC_V3_ATTR_CONDITION = "weather_code" -CC_V3_ATTR_VISIBILITY = "visibility" -CC_V3_ATTR_PRECIPITATION = "precipitation" -CC_V3_ATTR_PRECIPITATION_DAILY = "precipitation_accumulation" -CC_V3_ATTR_PRECIPITATION_PROBABILITY = "precipitation_probability" -CC_V3_ATTR_WIND_GUST = "wind_gust" -CC_V3_ATTR_CLOUD_COVER = "cloud_cover" -CC_V3_ATTR_PRECIPITATION_TYPE = "precipitation_type" - -# Sensor attributes -CC_V3_ATTR_PARTICULATE_MATTER_25 = "pm25" -CC_V3_ATTR_PARTICULATE_MATTER_10 = "pm10" -CC_V3_ATTR_NITROGEN_DIOXIDE = "no2" -CC_V3_ATTR_CARBON_MONOXIDE = "co" -CC_V3_ATTR_SULFUR_DIOXIDE = "so2" -CC_V3_ATTR_EPA_AQI = "epa_aqi" -CC_V3_ATTR_EPA_PRIMARY_POLLUTANT = "epa_primary_pollutant" -CC_V3_ATTR_EPA_HEALTH_CONCERN = "epa_health_concern" -CC_V3_ATTR_CHINA_AQI = "china_aqi" -CC_V3_ATTR_CHINA_PRIMARY_POLLUTANT = "china_primary_pollutant" -CC_V3_ATTR_CHINA_HEALTH_CONCERN = "china_health_concern" -CC_V3_ATTR_POLLEN_TREE = "pollen_tree" -CC_V3_ATTR_POLLEN_WEED = "pollen_weed" -CC_V3_ATTR_POLLEN_GRASS = "pollen_grass" -CC_V3_ATTR_FIRE_INDEX = "fire_index" - -CC_V3_SENSOR_TYPES = ( - ClimaCellSensorEntityDescription( - key=CC_V3_ATTR_OZONE, - name="Ozone", - unit_imperial=CONCENTRATION_PARTS_PER_BILLION, - unit_metric=CONCENTRATION_PARTS_PER_BILLION, - ), - ClimaCellSensorEntityDescription( - key=CC_V3_ATTR_PARTICULATE_MATTER_25, - name="Particulate Matter < 2.5 μm", - unit_imperial=CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT, - unit_metric=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - metric_conversion=3.2808399**3, - is_metric_check=False, - ), - ClimaCellSensorEntityDescription( - key=CC_V3_ATTR_PARTICULATE_MATTER_10, - name="Particulate Matter < 10 μm", - unit_imperial=CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT, - unit_metric=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - metric_conversion=3.2808399**3, - is_metric_check=False, - ), - ClimaCellSensorEntityDescription( - key=CC_V3_ATTR_NITROGEN_DIOXIDE, - name="Nitrogen Dioxide", - unit_imperial=CONCENTRATION_PARTS_PER_BILLION, - unit_metric=CONCENTRATION_PARTS_PER_BILLION, - ), - ClimaCellSensorEntityDescription( - key=CC_V3_ATTR_CARBON_MONOXIDE, - name="Carbon Monoxide", - unit_imperial=CONCENTRATION_PARTS_PER_MILLION, - unit_metric=CONCENTRATION_PARTS_PER_MILLION, - device_class=SensorDeviceClass.CO, - ), - ClimaCellSensorEntityDescription( - key=CC_V3_ATTR_SULFUR_DIOXIDE, - name="Sulfur Dioxide", - unit_imperial=CONCENTRATION_PARTS_PER_BILLION, - unit_metric=CONCENTRATION_PARTS_PER_BILLION, - ), - ClimaCellSensorEntityDescription( - key=CC_V3_ATTR_EPA_AQI, - name="US EPA Air Quality Index", - ), - ClimaCellSensorEntityDescription( - key=CC_V3_ATTR_EPA_PRIMARY_POLLUTANT, - name="US EPA Primary Pollutant", - ), - ClimaCellSensorEntityDescription( - key=CC_V3_ATTR_EPA_HEALTH_CONCERN, - name="US EPA Health Concern", - ), - ClimaCellSensorEntityDescription( - key=CC_V3_ATTR_CHINA_AQI, - name="China MEP Air Quality Index", - ), - ClimaCellSensorEntityDescription( - key=CC_V3_ATTR_CHINA_PRIMARY_POLLUTANT, - name="China MEP Primary Pollutant", - ), - ClimaCellSensorEntityDescription( - key=CC_V3_ATTR_CHINA_HEALTH_CONCERN, - name="China MEP Health Concern", - ), - ClimaCellSensorEntityDescription( - key=CC_V3_ATTR_POLLEN_TREE, - name="Tree Pollen Index", - value_map=V3PollenIndex, - ), - ClimaCellSensorEntityDescription( - key=CC_V3_ATTR_POLLEN_WEED, - name="Weed Pollen Index", - value_map=V3PollenIndex, - ), - ClimaCellSensorEntityDescription( - key=CC_V3_ATTR_POLLEN_GRASS, - name="Grass Pollen Index", - value_map=V3PollenIndex, - ), - ClimaCellSensorEntityDescription( - key=CC_V3_ATTR_FIRE_INDEX, - name="Fire Index", - ), -) diff --git a/homeassistant/components/climacell/manifest.json b/homeassistant/components/climacell/manifest.json deleted file mode 100644 index f0eee5ef0da..00000000000 --- a/homeassistant/components/climacell/manifest.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "domain": "climacell", - "name": "ClimaCell", - "config_flow": false, - "documentation": "https://www.home-assistant.io/integrations/climacell", - "requirements": ["pyclimacell==0.18.2"], - "after_dependencies": ["tomorrowio"], - "codeowners": ["@raman325"], - "iot_class": "cloud_polling", - "loggers": ["pyclimacell"] -} diff --git a/homeassistant/components/climacell/sensor.py b/homeassistant/components/climacell/sensor.py deleted file mode 100644 index 4eb9dddb9c3..00000000000 --- a/homeassistant/components/climacell/sensor.py +++ /dev/null @@ -1,88 +0,0 @@ -"""Sensor component that handles additional ClimaCell data for your location.""" -from __future__ import annotations - -from pyclimacell.const import CURRENT - -from homeassistant.components.sensor import SensorEntity -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_VERSION, CONF_NAME -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.util import slugify - -from . import ClimaCellDataUpdateCoordinator, ClimaCellEntity -from .const import CC_V3_SENSOR_TYPES, DOMAIN, ClimaCellSensorEntityDescription - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up a config entry.""" - coordinator = hass.data[DOMAIN][config_entry.entry_id] - api_version = config_entry.data[CONF_API_VERSION] - entities = [ - ClimaCellV3SensorEntity( - hass, config_entry, coordinator, api_version, description - ) - for description in CC_V3_SENSOR_TYPES - ] - async_add_entities(entities) - - -class ClimaCellV3SensorEntity(ClimaCellEntity, SensorEntity): - """Sensor entity that talks to ClimaCell v3 API to retrieve non-weather data.""" - - entity_description: ClimaCellSensorEntityDescription - - def __init__( - self, - hass: HomeAssistant, - config_entry: ConfigEntry, - coordinator: ClimaCellDataUpdateCoordinator, - api_version: int, - description: ClimaCellSensorEntityDescription, - ) -> None: - """Initialize ClimaCell Sensor Entity.""" - super().__init__(config_entry, coordinator, api_version) - self.entity_description = description - self._attr_entity_registry_enabled_default = False - self._attr_name = f"{self._config_entry.data[CONF_NAME]} - {description.name}" - self._attr_unique_id = ( - f"{self._config_entry.unique_id}_{slugify(description.name)}" - ) - self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: self.attribution} - self._attr_native_unit_of_measurement = ( - description.unit_metric - if hass.config.units.is_metric - else description.unit_imperial - ) - - @property - def native_value(self) -> str | int | float | None: - """Return the state.""" - state = self._get_cc_value( - self.coordinator.data[CURRENT], self.entity_description.key - ) - if ( - state is not None - and not isinstance(state, str) - and self.entity_description.unit_imperial is not None - and self.entity_description.metric_conversion != 1.0 - and self.entity_description.is_metric_check is not None - and self.hass.config.units.is_metric - == self.entity_description.is_metric_check - ): - conversion = self.entity_description.metric_conversion - # When conversion is a callable, we assume it's a single input function - if callable(conversion): - return round(conversion(state), 4) - - return round(state * conversion, 4) - - if self.entity_description.value_map is not None and state is not None: - # mypy bug: "Literal[IntEnum.value]" not callable - return self.entity_description.value_map(state).name.lower() # type: ignore[misc] - - return state diff --git a/homeassistant/components/climacell/strings.json b/homeassistant/components/climacell/strings.json deleted file mode 100644 index 25ddee09dd0..00000000000 --- a/homeassistant/components/climacell/strings.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "options": { - "step": { - "init": { - "title": "Update ClimaCell Options", - "description": "If you choose to enable the `nowcast` forecast entity, you can configure the number of minutes between each forecast. The number of forecasts provided depends on the number of minutes chosen between forecasts.", - "data": { - "timestep": "Min. Between NowCast Forecasts" - } - } - } - } -} diff --git a/homeassistant/components/climacell/strings.sensor.json b/homeassistant/components/climacell/strings.sensor.json deleted file mode 100644 index 1864a034043..00000000000 --- a/homeassistant/components/climacell/strings.sensor.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "state": { - "climacell__pollen_index": { - "none": "None", - "very_low": "Very Low", - "low": "Low", - "medium": "Medium", - "high": "High", - "very_high": "Very High" - }, - "climacell__health_concern": { - "good": "Good", - "moderate": "Moderate", - "unhealthy_for_sensitive_groups": "Unhealthy for Sensitive Groups", - "unhealthy": "Unhealthy", - "very_unhealthy": "Very Unhealthy", - "hazardous": "Hazardous" - }, - "climacell__precipitation_type": { - "none": "None", - "rain": "Rain", - "snow": "Snow", - "freezing_rain": "Freezing Rain", - "ice_pellets": "Ice Pellets" - } - } -} diff --git a/homeassistant/components/climacell/translations/af.json b/homeassistant/components/climacell/translations/af.json deleted file mode 100644 index d05e07e4eff..00000000000 --- a/homeassistant/components/climacell/translations/af.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "options": { - "step": { - "init": { - "title": "Update [%key:component::climacell::title%] opties" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/ca.json b/homeassistant/components/climacell/translations/ca.json deleted file mode 100644 index 2b6abb46737..00000000000 --- a/homeassistant/components/climacell/translations/ca.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "options": { - "step": { - "init": { - "data": { - "timestep": "Minuts entre previsions NowCast" - }, - "description": "Si decideixes activar l'entitat de previsi\u00f3 `nowcast`, podr\u00e0s configurar l'interval en minuts entre cada previsi\u00f3. El nombre de previsions proporcionades dep\u00e8n d'aquest interval de minuts.", - "title": "Actualitzaci\u00f3 d'opcions de ClimaCell" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/de.json b/homeassistant/components/climacell/translations/de.json deleted file mode 100644 index 7c3e929dde2..00000000000 --- a/homeassistant/components/climacell/translations/de.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "options": { - "step": { - "init": { - "data": { - "timestep": "Minuten zwischen den NowCast Kurzvorhersagen" - }, - "description": "Wenn du die Vorhersage-Entitit\u00e4t \"Kurzvorhersage\" aktivierst, kannst du die Anzahl der Minuten zwischen den einzelnen Vorhersagen konfigurieren. Die Anzahl der bereitgestellten Vorhersagen h\u00e4ngt von der Anzahl der zwischen den Vorhersagen gew\u00e4hlten Minuten ab.", - "title": "ClimaCell-Optionen aktualisieren" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/el.json b/homeassistant/components/climacell/translations/el.json deleted file mode 100644 index 392573f693c..00000000000 --- a/homeassistant/components/climacell/translations/el.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "options": { - "step": { - "init": { - "data": { - "timestep": "\u039b\u03b5\u03c0\u03c4\u03ac \u03bc\u03b5\u03c4\u03b1\u03be\u03cd \u03c4\u03c9\u03bd \u03b4\u03b5\u03bb\u03c4\u03af\u03c9\u03bd NowCast" - }, - "description": "\u0395\u03ac\u03bd \u03b5\u03c0\u03b9\u03bb\u03ad\u03be\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 \u03b4\u03b5\u03bb\u03c4\u03af\u03c9\u03bd 'nowcast', \u03bc\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc \u03c4\u03c9\u03bd \u03bb\u03b5\u03c0\u03c4\u03ce\u03bd \u03bc\u03b5\u03c4\u03b1\u03be\u03cd \u03ba\u03ac\u03b8\u03b5 \u03b4\u03b5\u03bb\u03c4\u03af\u03bf\u03c5. \u039f \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03c4\u03c9\u03bd \u03b4\u03b5\u03bb\u03c4\u03af\u03c9\u03bd \u03c0\u03bf\u03c5 \u03c0\u03b1\u03c1\u03ad\u03c7\u03bf\u03bd\u03c4\u03b1\u03b9 \u03b5\u03be\u03b1\u03c1\u03c4\u03ac\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf\u03bd \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc \u03c4\u03c9\u03bd \u03bb\u03b5\u03c0\u03c4\u03ce\u03bd \u03c0\u03bf\u03c5 \u03b5\u03c0\u03b9\u03bb\u03ad\u03b3\u03bf\u03bd\u03c4\u03b1\u03b9 \u03bc\u03b5\u03c4\u03b1\u03be\u03cd \u03c4\u03c9\u03bd \u03b4\u03b5\u03bb\u03c4\u03af\u03c9\u03bd.", - "title": "\u0395\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ce\u03bd ClimaCell" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/en.json b/homeassistant/components/climacell/translations/en.json deleted file mode 100644 index a35be85d5b2..00000000000 --- a/homeassistant/components/climacell/translations/en.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "options": { - "step": { - "init": { - "data": { - "timestep": "Min. Between NowCast Forecasts" - }, - "description": "If you choose to enable the `nowcast` forecast entity, you can configure the number of minutes between each forecast. The number of forecasts provided depends on the number of minutes chosen between forecasts.", - "title": "Update ClimaCell Options" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/es-419.json b/homeassistant/components/climacell/translations/es-419.json deleted file mode 100644 index 449ad1ba367..00000000000 --- a/homeassistant/components/climacell/translations/es-419.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "options": { - "step": { - "init": { - "data": { - "timestep": "Min. entre pron\u00f3sticos de NowCast" - }, - "description": "Si elige habilitar la entidad de pron\u00f3stico \"nowcast\", puede configurar el n\u00famero de minutos entre cada pron\u00f3stico. El n\u00famero de pron\u00f3sticos proporcionados depende del n\u00famero de minutos elegidos entre los pron\u00f3sticos.", - "title": "Actualizar opciones de ClimaCell" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/es.json b/homeassistant/components/climacell/translations/es.json deleted file mode 100644 index 270d72bd58c..00000000000 --- a/homeassistant/components/climacell/translations/es.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "options": { - "step": { - "init": { - "data": { - "timestep": "Min. entre pron\u00f3sticos de NowCast" - }, - "description": "Si eliges habilitar la entidad de pron\u00f3stico `nowcast`, puedes configurar la cantidad de minutos entre cada pron\u00f3stico. La cantidad de pron\u00f3sticos proporcionados depende de la cantidad de minutos elegidos entre los pron\u00f3sticos.", - "title": "Actualizar opciones de ClimaCell" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/et.json b/homeassistant/components/climacell/translations/et.json deleted file mode 100644 index 5d915a87d80..00000000000 --- a/homeassistant/components/climacell/translations/et.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "options": { - "step": { - "init": { - "data": { - "timestep": "Minuteid NowCasti prognooside vahel" - }, - "description": "Kui otsustad lubada \"nowcast\" prognoosi\u00fcksuse, saad seadistada minutite arvu iga prognoosi vahel. Esitatavate prognooside arv s\u00f5ltub prognooside vahel valitud minutite arvust.", - "title": "V\u00e4rskenda [%key:component::climacell::title%] suvandeid" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/fr.json b/homeassistant/components/climacell/translations/fr.json deleted file mode 100644 index b2c1285ecc9..00000000000 --- a/homeassistant/components/climacell/translations/fr.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "options": { - "step": { - "init": { - "data": { - "timestep": "Min. entre les pr\u00e9visions NowCast" - }, - "description": "Si vous choisissez d'activer l'entit\u00e9 de pr\u00e9vision \u00ab\u00a0nowcast\u00a0\u00bb, vous pouvez configurer le nombre de minutes entre chaque pr\u00e9vision. Le nombre de pr\u00e9visions fournies d\u00e9pend du nombre de minutes choisies entre les pr\u00e9visions.", - "title": "Mettre \u00e0 jour les options ClimaCell" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/hu.json b/homeassistant/components/climacell/translations/hu.json deleted file mode 100644 index 4cad1eaaa0f..00000000000 --- a/homeassistant/components/climacell/translations/hu.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "options": { - "step": { - "init": { - "data": { - "timestep": "Min. A NowCast el\u0151rejelz\u00e9sek k\u00f6z\u00f6tt" - }, - "description": "Ha a `nowcast` el\u0151rejelz\u00e9si entit\u00e1s enged\u00e9lyez\u00e9s\u00e9t v\u00e1lasztja, be\u00e1ll\u00edthatja az egyes el\u0151rejelz\u00e9sek k\u00f6z\u00f6tti percek sz\u00e1m\u00e1t. A megadott el\u0151rejelz\u00e9sek sz\u00e1ma az el\u0151rejelz\u00e9sek k\u00f6z\u00f6tt kiv\u00e1lasztott percek sz\u00e1m\u00e1t\u00f3l f\u00fcgg.", - "title": "ClimaCell be\u00e1ll\u00edt\u00e1sok friss\u00edt\u00e9se" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/id.json b/homeassistant/components/climacell/translations/id.json deleted file mode 100644 index 4d020351665..00000000000 --- a/homeassistant/components/climacell/translations/id.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "options": { - "step": { - "init": { - "data": { - "timestep": "Jarak Interval Prakiraan NowCast dalam Menit" - }, - "description": "Jika Anda memilih untuk mengaktifkan entitas prakiraan `nowcast`, Anda dapat mengonfigurasi jarak interval prakiraan dalam menit. Jumlah prakiraan yang diberikan tergantung pada nilai interval yang dipilih.", - "title": "Perbarui Opsi ClimaCell" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/it.json b/homeassistant/components/climacell/translations/it.json deleted file mode 100644 index b9667d6bfb1..00000000000 --- a/homeassistant/components/climacell/translations/it.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "options": { - "step": { - "init": { - "data": { - "timestep": "Minuti tra le previsioni di NowCast" - }, - "description": "Se scegli di abilitare l'entit\u00e0 di previsione `nowcast`, puoi configurare il numero di minuti tra ogni previsione. Il numero di previsioni fornite dipende dal numero di minuti scelti tra le previsioni.", - "title": "Aggiorna le opzioni di ClimaCell" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/ja.json b/homeassistant/components/climacell/translations/ja.json deleted file mode 100644 index e2742d11435..00000000000 --- a/homeassistant/components/climacell/translations/ja.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "options": { - "step": { - "init": { - "data": { - "timestep": "\u6700\u5c0f\u3002 NowCast \u4e88\u6e2c\u306e\u9593" - }, - "description": "`nowcast` forecast(\u4e88\u6e2c) \u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u3092\u6709\u52b9\u306b\u3059\u308b\u3053\u3068\u3092\u9078\u629e\u3057\u305f\u5834\u5408\u3001\u5404\u4e88\u6e2c\u9593\u306e\u5206\u6570\u3092\u8a2d\u5b9a\u3059\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u3059\u3002\u63d0\u4f9b\u3055\u308c\u308bforecast(\u4e88\u6e2c)\u306e\u6570\u306f\u3001forecast(\u4e88\u6e2c)\u306e\u9593\u306b\u9078\u629e\u3057\u305f\u5206\u6570\u306b\u4f9d\u5b58\u3057\u307e\u3059\u3002", - "title": "ClimaCell \u30aa\u30d7\u30b7\u30e7\u30f3\u306e\u66f4\u65b0" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/ko.json b/homeassistant/components/climacell/translations/ko.json deleted file mode 100644 index 8accc07410d..00000000000 --- a/homeassistant/components/climacell/translations/ko.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "options": { - "step": { - "init": { - "data": { - "timestep": "\ub2e8\uae30\uc608\uce21 \uc77c\uae30\uc608\ubcf4 \uac04 \ucd5c\uc18c \uc2dc\uac04" - }, - "description": "`nowcast` \uc77c\uae30\uc608\ubcf4 \uad6c\uc131\uc694\uc18c\ub97c \uc0ac\uc6a9\ud558\ub3c4\ub85d \uc120\ud0dd\ud55c \uacbd\uc6b0 \uac01 \uc77c\uae30\uc608\ubcf4 \uc0ac\uc774\uc758 \uc2dc\uac04(\ubd84)\uc744 \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uc81c\uacf5\ub41c \uc77c\uae30\uc608\ubcf4 \ud69f\uc218\ub294 \uc608\uce21 \uac04 \uc120\ud0dd\ud55c \uc2dc\uac04(\ubd84)\uc5d0 \ub530\ub77c \ub2ec\ub77c\uc9d1\ub2c8\ub2e4.", - "title": "[%key:component::climacell::title%] \uc635\uc158 \uc5c5\ub370\uc774\ud2b8\ud558\uae30" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/nl.json b/homeassistant/components/climacell/translations/nl.json deleted file mode 100644 index a895fa8234d..00000000000 --- a/homeassistant/components/climacell/translations/nl.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "options": { - "step": { - "init": { - "data": { - "timestep": "Min. Tussen NowCast-voorspellingen" - }, - "description": "Als u ervoor kiest om de `nowcast` voorspellingsentiteit in te schakelen, kan u het aantal minuten tussen elke voorspelling configureren. Het aantal voorspellingen hangt af van het aantal gekozen minuten tussen de voorspellingen.", - "title": "Update ClimaCell Opties" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/no.json b/homeassistant/components/climacell/translations/no.json deleted file mode 100644 index 9f050624967..00000000000 --- a/homeassistant/components/climacell/translations/no.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "options": { - "step": { - "init": { - "data": { - "timestep": "Min. mellom NowCast prognoser" - }, - "description": "Hvis du velger \u00e5 aktivere \u00abnowcast\u00bb -varselentiteten, kan du konfigurere antall minutter mellom hver prognose. Antall angitte prognoser avhenger av antall minutter som er valgt mellom prognosene.", - "title": "Oppdater ClimaCell-alternativer" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/pl.json b/homeassistant/components/climacell/translations/pl.json deleted file mode 100644 index 5f69764ffab..00000000000 --- a/homeassistant/components/climacell/translations/pl.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "options": { - "step": { - "init": { - "data": { - "timestep": "Czas (min) mi\u0119dzy prognozami NowCast" - }, - "description": "Je\u015bli zdecydujesz si\u0119 w\u0142\u0105czy\u0107 encj\u0119 prognozy \u201enowcast\u201d, mo\u017cesz skonfigurowa\u0107 liczb\u0119 minut mi\u0119dzy ka\u017cd\u0105 prognoz\u0105. Liczba dostarczonych prognoz zale\u017cy od liczby minut wybranych mi\u0119dzy prognozami.", - "title": "Opcje aktualizacji ClimaCell" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/pt-BR.json b/homeassistant/components/climacell/translations/pt-BR.json deleted file mode 100644 index b7e71d45971..00000000000 --- a/homeassistant/components/climacell/translations/pt-BR.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "options": { - "step": { - "init": { - "data": { - "timestep": "M\u00ednimo entre previs\u00f5es NowCast" - }, - "description": "Se voc\u00ea optar por ativar a entidade de previs\u00e3o `nowcast`, poder\u00e1 configurar o n\u00famero de minutos entre cada previs\u00e3o. O n\u00famero de previs\u00f5es fornecidas depende do n\u00famero de minutos escolhidos entre as previs\u00f5es.", - "title": "Atualizar as op\u00e7\u00f5es do ClimaCell" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/ru.json b/homeassistant/components/climacell/translations/ru.json deleted file mode 100644 index 9f3219ce4d6..00000000000 --- a/homeassistant/components/climacell/translations/ru.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "options": { - "step": { - "init": { - "data": { - "timestep": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f (\u0432 \u043c\u0438\u043d\u0443\u0442\u0430\u0445)" - }, - "description": "\u0415\u0441\u043b\u0438 \u0412\u044b \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0443\u0435\u0442\u0435 \u043e\u0431\u044a\u0435\u043a\u0442 \u043f\u0440\u043e\u0433\u043d\u043e\u0437\u0430 'nowcast', \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0438\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f \u043f\u0440\u043e\u0433\u043d\u043e\u0437\u0430.", - "title": "\u041e\u0431\u043d\u043e\u0432\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 ClimaCell" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.bg.json b/homeassistant/components/climacell/translations/sensor.bg.json deleted file mode 100644 index 04f393f1d99..00000000000 --- a/homeassistant/components/climacell/translations/sensor.bg.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "state": { - "climacell__precipitation_type": { - "rain": "\u0414\u044a\u0436\u0434", - "snow": "\u0421\u043d\u044f\u0433" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.ca.json b/homeassistant/components/climacell/translations/sensor.ca.json deleted file mode 100644 index 359857925da..00000000000 --- a/homeassistant/components/climacell/translations/sensor.ca.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "state": { - "climacell__health_concern": { - "good": "Bo", - "hazardous": "Perill\u00f3s", - "moderate": "Moderat", - "unhealthy": "Poc saludable", - "unhealthy_for_sensitive_groups": "No saludable per a grups sensibles", - "very_unhealthy": "Gens saludable" - }, - "climacell__pollen_index": { - "high": "Alt", - "low": "Baix", - "medium": "Mitj\u00e0", - "none": "Cap", - "very_high": "Molt alt", - "very_low": "Molt baix" - }, - "climacell__precipitation_type": { - "freezing_rain": "Pluja congelada", - "ice_pellets": "Gran\u00eds", - "none": "Cap", - "rain": "Pluja", - "snow": "Neu" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.de.json b/homeassistant/components/climacell/translations/sensor.de.json deleted file mode 100644 index 93a1e5e8e98..00000000000 --- a/homeassistant/components/climacell/translations/sensor.de.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "state": { - "climacell__health_concern": { - "good": "Gut", - "hazardous": "Gef\u00e4hrlich", - "moderate": "M\u00e4\u00dfig", - "unhealthy": "Ungesund", - "unhealthy_for_sensitive_groups": "Ungesund f\u00fcr sensible Gruppen", - "very_unhealthy": "Sehr ungesund" - }, - "climacell__pollen_index": { - "high": "Hoch", - "low": "Niedrig", - "medium": "Mittel", - "none": "Keine", - "very_high": "Sehr hoch", - "very_low": "Sehr niedrig" - }, - "climacell__precipitation_type": { - "freezing_rain": "Gefrierender Regen", - "ice_pellets": "Graupel", - "none": "Keine", - "rain": "Regen", - "snow": "Schnee" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.el.json b/homeassistant/components/climacell/translations/sensor.el.json deleted file mode 100644 index facd86ed7c6..00000000000 --- a/homeassistant/components/climacell/translations/sensor.el.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "state": { - "climacell__health_concern": { - "good": "\u039a\u03b1\u03bb\u03cc", - "hazardous": "\u0395\u03c0\u03b9\u03ba\u03af\u03bd\u03b4\u03c5\u03bd\u03bf", - "moderate": "\u039c\u03ad\u03c4\u03c1\u03b9\u03bf", - "unhealthy": "\u0391\u03bd\u03b8\u03c5\u03b3\u03b9\u03b5\u03b9\u03bd\u03cc", - "unhealthy_for_sensitive_groups": "\u0391\u03bd\u03b8\u03c5\u03b3\u03b9\u03b5\u03b9\u03bd\u03cc \u03b3\u03b9\u03b1 \u03b5\u03c5\u03b1\u03af\u03c3\u03b8\u03b7\u03c4\u03b5\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b5\u03c2", - "very_unhealthy": "\u03a0\u03bf\u03bb\u03cd \u0391\u03bd\u03b8\u03c5\u03b3\u03b9\u03b5\u03b9\u03bd\u03cc" - }, - "climacell__pollen_index": { - "high": "\u03a5\u03c8\u03b7\u03bb\u03cc", - "low": "\u03a7\u03b1\u03bc\u03b7\u03bb\u03cc", - "medium": "\u039c\u03b5\u03c3\u03b1\u03af\u03bf", - "none": "\u03a4\u03af\u03c0\u03bf\u03c4\u03b1", - "very_high": "\u03a0\u03bf\u03bb\u03cd \u03c5\u03c8\u03b7\u03bb\u03cc", - "very_low": "\u03a0\u03bf\u03bb\u03cd \u03c7\u03b1\u03bc\u03b7\u03bb\u03cc" - }, - "climacell__precipitation_type": { - "freezing_rain": "\u03a0\u03b1\u03b3\u03c9\u03bc\u03ad\u03bd\u03b7 \u03b2\u03c1\u03bf\u03c7\u03ae", - "ice_pellets": "\u03a0\u03ad\u03bb\u03bb\u03b5\u03c4 \u03c0\u03ac\u03b3\u03bf\u03c5", - "none": "\u03a4\u03af\u03c0\u03bf\u03c4\u03b1", - "rain": "\u0392\u03c1\u03bf\u03c7\u03ae", - "snow": "\u03a7\u03b9\u03cc\u03bd\u03b9" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.en.json b/homeassistant/components/climacell/translations/sensor.en.json deleted file mode 100644 index 0cb1d27aaec..00000000000 --- a/homeassistant/components/climacell/translations/sensor.en.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "state": { - "climacell__health_concern": { - "good": "Good", - "hazardous": "Hazardous", - "moderate": "Moderate", - "unhealthy": "Unhealthy", - "unhealthy_for_sensitive_groups": "Unhealthy for Sensitive Groups", - "very_unhealthy": "Very Unhealthy" - }, - "climacell__pollen_index": { - "high": "High", - "low": "Low", - "medium": "Medium", - "none": "None", - "very_high": "Very High", - "very_low": "Very Low" - }, - "climacell__precipitation_type": { - "freezing_rain": "Freezing Rain", - "ice_pellets": "Ice Pellets", - "none": "None", - "rain": "Rain", - "snow": "Snow" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.es-419.json b/homeassistant/components/climacell/translations/sensor.es-419.json deleted file mode 100644 index 127177e84b4..00000000000 --- a/homeassistant/components/climacell/translations/sensor.es-419.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "state": { - "climacell__health_concern": { - "good": "Bueno", - "hazardous": "Peligroso", - "moderate": "Moderado", - "unhealthy": "Insalubre", - "unhealthy_for_sensitive_groups": "Insalubre para grupos sensibles", - "very_unhealthy": "Muy poco saludable" - }, - "climacell__pollen_index": { - "high": "Alto", - "low": "Bajo", - "medium": "Medio", - "none": "Ninguno", - "very_high": "Muy alto", - "very_low": "Muy bajo" - }, - "climacell__precipitation_type": { - "freezing_rain": "Lluvia helada", - "ice_pellets": "Gr\u00e1nulos de hielo", - "none": "Ninguno", - "rain": "Lluvia", - "snow": "Nieve" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.es.json b/homeassistant/components/climacell/translations/sensor.es.json deleted file mode 100644 index 4cb1b34eb21..00000000000 --- a/homeassistant/components/climacell/translations/sensor.es.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "state": { - "climacell__health_concern": { - "good": "Bueno", - "hazardous": "Peligroso", - "moderate": "Moderado", - "unhealthy": "No saludable", - "unhealthy_for_sensitive_groups": "No es saludable para grupos sensibles", - "very_unhealthy": "Muy poco saludable" - }, - "climacell__pollen_index": { - "high": "Alto", - "low": "Bajo", - "medium": "Medio", - "none": "Ninguno", - "very_high": "Muy alto", - "very_low": "Muy bajo" - }, - "climacell__precipitation_type": { - "freezing_rain": "Lluvia helada", - "ice_pellets": "Granizo", - "none": "Ninguna", - "rain": "Lluvia", - "snow": "Nieve" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.et.json b/homeassistant/components/climacell/translations/sensor.et.json deleted file mode 100644 index a0b7ac0562b..00000000000 --- a/homeassistant/components/climacell/translations/sensor.et.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "state": { - "climacell__health_concern": { - "good": "Normaalne", - "hazardous": "Ohtlik", - "moderate": "M\u00f5\u00f5dukas", - "unhealthy": "Ebatervislik", - "unhealthy_for_sensitive_groups": "Ebatervislik riskir\u00fchmale", - "very_unhealthy": "V\u00e4ga ebatervislik" - }, - "climacell__pollen_index": { - "high": "K\u00f5rge", - "low": "Madal", - "medium": "Keskmine", - "none": "Puudub", - "very_high": "V\u00e4ga k\u00f5rge", - "very_low": "V\u00e4ga madal" - }, - "climacell__precipitation_type": { - "freezing_rain": "J\u00e4\u00e4vihm", - "ice_pellets": "J\u00e4\u00e4kruubid", - "none": "Sademeid pole", - "rain": "Vihm", - "snow": "Lumi" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.fr.json b/homeassistant/components/climacell/translations/sensor.fr.json deleted file mode 100644 index acff91fc570..00000000000 --- a/homeassistant/components/climacell/translations/sensor.fr.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "state": { - "climacell__health_concern": { - "good": "Bon", - "hazardous": "Dangereux", - "moderate": "Mod\u00e9r\u00e9", - "unhealthy": "Mauvais pour la sant\u00e9", - "unhealthy_for_sensitive_groups": "Mauvaise qualit\u00e9 pour les groupes sensibles", - "very_unhealthy": "Tr\u00e8s mauvais pour la sant\u00e9" - }, - "climacell__pollen_index": { - "high": "Haut", - "low": "Faible", - "medium": "Moyen", - "none": "Aucun", - "very_high": "Tr\u00e8s \u00e9lev\u00e9", - "very_low": "Tr\u00e8s faible" - }, - "climacell__precipitation_type": { - "freezing_rain": "Pluie vergla\u00e7ante", - "ice_pellets": "Gr\u00e9sil", - "none": "Aucun", - "rain": "Pluie", - "snow": "Neige" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.he.json b/homeassistant/components/climacell/translations/sensor.he.json deleted file mode 100644 index 2a509464928..00000000000 --- a/homeassistant/components/climacell/translations/sensor.he.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "state": { - "climacell__health_concern": { - "unhealthy_for_sensitive_groups": "\u05dc\u05d0 \u05d1\u05e8\u05d9\u05d0 \u05dc\u05e7\u05d1\u05d5\u05e6\u05d5\u05ea \u05e8\u05d2\u05d9\u05e9\u05d5\u05ea" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.hu.json b/homeassistant/components/climacell/translations/sensor.hu.json deleted file mode 100644 index 656a460f429..00000000000 --- a/homeassistant/components/climacell/translations/sensor.hu.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "state": { - "climacell__health_concern": { - "good": "J\u00f3", - "hazardous": "Vesz\u00e9lyes", - "moderate": "M\u00e9rs\u00e9kelt", - "unhealthy": "Eg\u00e9szs\u00e9gtelen", - "unhealthy_for_sensitive_groups": "Eg\u00e9szs\u00e9gtelen \u00e9rz\u00e9keny csoportok sz\u00e1m\u00e1ra", - "very_unhealthy": "Nagyon eg\u00e9szs\u00e9gtelen" - }, - "climacell__pollen_index": { - "high": "Magas", - "low": "Alacsony", - "medium": "K\u00f6zepes", - "none": "Nincs", - "very_high": "Nagyon magas", - "very_low": "Nagyon alacsony" - }, - "climacell__precipitation_type": { - "freezing_rain": "Havas es\u0151", - "ice_pellets": "\u00d3nos es\u0151", - "none": "Nincs", - "rain": "Es\u0151", - "snow": "Havaz\u00e1s" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.id.json b/homeassistant/components/climacell/translations/sensor.id.json deleted file mode 100644 index 37ac0f7d876..00000000000 --- a/homeassistant/components/climacell/translations/sensor.id.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "state": { - "climacell__health_concern": { - "good": "Bagus", - "hazardous": "Berbahaya", - "moderate": "Sedang", - "unhealthy": "Tidak Sehat", - "unhealthy_for_sensitive_groups": "Tidak Sehat untuk Kelompok Sensitif", - "very_unhealthy": "Sangat Tidak Sehat" - }, - "climacell__pollen_index": { - "high": "Tinggi", - "low": "Rendah", - "medium": "Sedang", - "none": "Tidak Ada", - "very_high": "Sangat Tinggi", - "very_low": "Sangat Rendah" - }, - "climacell__precipitation_type": { - "freezing_rain": "Hujan Beku", - "ice_pellets": "Hujan Es", - "none": "Tidak Ada", - "rain": "Hujan", - "snow": "Salju" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.is.json b/homeassistant/components/climacell/translations/sensor.is.json deleted file mode 100644 index bc22f9c67a9..00000000000 --- a/homeassistant/components/climacell/translations/sensor.is.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "state": { - "climacell__health_concern": { - "hazardous": "H\u00e6ttulegt", - "unhealthy": "\u00d3hollt" - }, - "climacell__precipitation_type": { - "rain": "Rigning", - "snow": "Snj\u00f3r" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.it.json b/homeassistant/components/climacell/translations/sensor.it.json deleted file mode 100644 index b9326be886e..00000000000 --- a/homeassistant/components/climacell/translations/sensor.it.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "state": { - "climacell__health_concern": { - "good": "Buono", - "hazardous": "Pericoloso", - "moderate": "Moderato", - "unhealthy": "Malsano", - "unhealthy_for_sensitive_groups": "Malsano per gruppi sensibili", - "very_unhealthy": "Molto malsano" - }, - "climacell__pollen_index": { - "high": "Alto", - "low": "Basso", - "medium": "Medio", - "none": "Nessuno", - "very_high": "Molto alto", - "very_low": "Molto basso" - }, - "climacell__precipitation_type": { - "freezing_rain": "Grandine", - "ice_pellets": "Pioggia gelata", - "none": "Nessuno", - "rain": "Pioggia", - "snow": "Neve" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.ja.json b/homeassistant/components/climacell/translations/sensor.ja.json deleted file mode 100644 index 6d8df99ca70..00000000000 --- a/homeassistant/components/climacell/translations/sensor.ja.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "state": { - "climacell__health_concern": { - "good": "\u826f\u597d", - "hazardous": "\u5371\u967a", - "moderate": "\u7a4f\u3084\u304b\u306a", - "unhealthy": "\u4e0d\u5065\u5eb7", - "unhealthy_for_sensitive_groups": "\u654f\u611f\u306a\u30b0\u30eb\u30fc\u30d7\u306b\u3068\u3063\u3066\u306f\u4e0d\u5065\u5eb7", - "very_unhealthy": "\u975e\u5e38\u306b\u4e0d\u5065\u5eb7" - }, - "climacell__pollen_index": { - "high": "\u9ad8\u3044", - "low": "\u4f4e\u3044", - "medium": "\u4e2d", - "none": "\u306a\u3057", - "very_high": "\u975e\u5e38\u306b\u9ad8\u3044", - "very_low": "\u3068\u3066\u3082\u4f4e\u3044" - }, - "climacell__precipitation_type": { - "freezing_rain": "\u51cd\u3066\u3064\u304f\u96e8", - "ice_pellets": "\u51cd\u96e8", - "none": "\u306a\u3057", - "rain": "\u96e8", - "snow": "\u96ea" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.ko.json b/homeassistant/components/climacell/translations/sensor.ko.json deleted file mode 100644 index e5ec616959e..00000000000 --- a/homeassistant/components/climacell/translations/sensor.ko.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "state": { - "climacell__precipitation_type": { - "snow": "\ub208" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.lv.json b/homeassistant/components/climacell/translations/sensor.lv.json deleted file mode 100644 index a0010b4e4a8..00000000000 --- a/homeassistant/components/climacell/translations/sensor.lv.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "state": { - "climacell__health_concern": { - "good": "Labs", - "hazardous": "B\u012bstams", - "moderate": "M\u0113rens", - "unhealthy": "Nevesel\u012bgs", - "unhealthy_for_sensitive_groups": "Nevesel\u012bgs jut\u012bg\u0101m grup\u0101m", - "very_unhealthy": "\u013boti nevesel\u012bgs" - }, - "climacell__pollen_index": { - "high": "Augsts", - "low": "Zems", - "medium": "Vid\u0113js", - "none": "Nav", - "very_high": "\u013boti augsts", - "very_low": "\u013boti zems" - }, - "climacell__precipitation_type": { - "freezing_rain": "Sasalsto\u0161s lietus", - "ice_pellets": "Krusa", - "none": "Nav", - "rain": "Lietus", - "snow": "Sniegs" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.nl.json b/homeassistant/components/climacell/translations/sensor.nl.json deleted file mode 100644 index 710198156d1..00000000000 --- a/homeassistant/components/climacell/translations/sensor.nl.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "state": { - "climacell__health_concern": { - "good": "Goed", - "hazardous": "Gevaarlijk", - "moderate": "Gematigd", - "unhealthy": "Ongezond", - "unhealthy_for_sensitive_groups": "Ongezond voor gevoelige groepen", - "very_unhealthy": "Zeer ongezond" - }, - "climacell__pollen_index": { - "high": "Hoog", - "low": "Laag", - "medium": "Medium", - "none": "Geen", - "very_high": "Zeer Hoog", - "very_low": "Zeer Laag" - }, - "climacell__precipitation_type": { - "freezing_rain": "IJzel", - "ice_pellets": "IJskorrels", - "none": "Geen", - "rain": "Regen", - "snow": "Sneeuw" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.no.json b/homeassistant/components/climacell/translations/sensor.no.json deleted file mode 100644 index 10f2a02db72..00000000000 --- a/homeassistant/components/climacell/translations/sensor.no.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "state": { - "climacell__health_concern": { - "good": "Bra", - "hazardous": "Farlig", - "moderate": "Moderat", - "unhealthy": "Usunt", - "unhealthy_for_sensitive_groups": "Usunt for sensitive grupper", - "very_unhealthy": "Veldig usunt" - }, - "climacell__pollen_index": { - "high": "H\u00f8y", - "low": "Lav", - "medium": "Medium", - "none": "Ingen", - "very_high": "Veldig h\u00f8y", - "very_low": "Veldig lav" - }, - "climacell__precipitation_type": { - "freezing_rain": "Underkj\u00f8lt regn", - "ice_pellets": "Is tapper", - "none": "Ingen", - "rain": "Regn", - "snow": "Sn\u00f8" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.pl.json b/homeassistant/components/climacell/translations/sensor.pl.json deleted file mode 100644 index 67a0217a7ea..00000000000 --- a/homeassistant/components/climacell/translations/sensor.pl.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "state": { - "climacell__health_concern": { - "good": "dobre", - "hazardous": "niebezpieczne", - "moderate": "umiarkowane", - "unhealthy": "niezdrowe", - "unhealthy_for_sensitive_groups": "niezdrowe dla grup wra\u017cliwych", - "very_unhealthy": "bardzo niezdrowe" - }, - "climacell__pollen_index": { - "high": "wysokie", - "low": "niskie", - "medium": "\u015brednie", - "none": "brak", - "very_high": "bardzo wysokie", - "very_low": "bardzo niskie" - }, - "climacell__precipitation_type": { - "freezing_rain": "marzn\u0105cy deszcz", - "ice_pellets": "granulki lodu", - "none": "brak", - "rain": "deszcz", - "snow": "\u015bnieg" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.pt-BR.json b/homeassistant/components/climacell/translations/sensor.pt-BR.json deleted file mode 100644 index eb3814331b9..00000000000 --- a/homeassistant/components/climacell/translations/sensor.pt-BR.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "state": { - "climacell__health_concern": { - "good": "Bom", - "hazardous": "Perigosos", - "moderate": "Moderado", - "unhealthy": "Pouco saud\u00e1vel", - "unhealthy_for_sensitive_groups": "Insalubre para grupos sens\u00edveis", - "very_unhealthy": "Muito prejudicial \u00e0 sa\u00fade" - }, - "climacell__pollen_index": { - "high": "Alto", - "low": "Baixo", - "medium": "M\u00e9dio", - "none": "Nenhum", - "very_high": "Muito alto", - "very_low": "Muito baixo" - }, - "climacell__precipitation_type": { - "freezing_rain": "Chuva congelante", - "ice_pellets": "Granizo", - "none": "Nenhum", - "rain": "Chuva", - "snow": "Neve" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.pt.json b/homeassistant/components/climacell/translations/sensor.pt.json deleted file mode 100644 index 30ba0f75808..00000000000 --- a/homeassistant/components/climacell/translations/sensor.pt.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "state": { - "climacell__health_concern": { - "unhealthy_for_sensitive_groups": "Pouco saud\u00e1vel para grupos sens\u00edveis" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.ru.json b/homeassistant/components/climacell/translations/sensor.ru.json deleted file mode 100644 index 3a5d1a07a7e..00000000000 --- a/homeassistant/components/climacell/translations/sensor.ru.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "state": { - "climacell__health_concern": { - "good": "\u0425\u043e\u0440\u043e\u0448\u043e", - "hazardous": "\u041e\u043f\u0430\u0441\u043d\u043e", - "moderate": "\u0421\u0440\u0435\u0434\u043d\u0435", - "unhealthy": "\u0412\u0440\u0435\u0434\u043d\u043e", - "unhealthy_for_sensitive_groups": "\u0412\u0440\u0435\u0434\u043d\u043e \u0434\u043b\u044f \u0443\u044f\u0437\u0432\u0438\u043c\u044b\u0445 \u0433\u0440\u0443\u043f\u043f", - "very_unhealthy": "\u041e\u0447\u0435\u043d\u044c \u0432\u0440\u0435\u0434\u043d\u043e" - }, - "climacell__pollen_index": { - "high": "\u0412\u044b\u0441\u043e\u043a\u0438\u0439", - "low": "\u041d\u0438\u0437\u043a\u0438\u0439", - "medium": "\u0421\u0440\u0435\u0434\u043d\u0438\u0439", - "none": "\u041e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442", - "very_high": "\u041e\u0447\u0435\u043d\u044c \u0432\u044b\u0441\u043e\u043a\u0438\u0439", - "very_low": "\u041e\u0447\u0435\u043d\u044c \u043d\u0438\u0437\u043a\u0438\u0439" - }, - "climacell__precipitation_type": { - "freezing_rain": "\u041b\u0435\u0434\u044f\u043d\u043e\u0439 \u0434\u043e\u0436\u0434\u044c", - "ice_pellets": "\u041b\u0435\u0434\u044f\u043d\u0430\u044f \u043a\u0440\u0443\u043f\u0430", - "none": "\u041e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442", - "rain": "\u0414\u043e\u0436\u0434\u044c", - "snow": "\u0421\u043d\u0435\u0433" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.sk.json b/homeassistant/components/climacell/translations/sensor.sk.json deleted file mode 100644 index 843169b2f3b..00000000000 --- a/homeassistant/components/climacell/translations/sensor.sk.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "state": { - "climacell__health_concern": { - "unhealthy": "Nezdrav\u00e9" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.sv.json b/homeassistant/components/climacell/translations/sensor.sv.json deleted file mode 100644 index d6172566c7a..00000000000 --- a/homeassistant/components/climacell/translations/sensor.sv.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "state": { - "climacell__health_concern": { - "good": "Bra", - "hazardous": "Farligt", - "moderate": "M\u00e5ttligt", - "unhealthy": "Oh\u00e4lsosamt", - "unhealthy_for_sensitive_groups": "Oh\u00e4lsosamt f\u00f6r k\u00e4nsliga grupper", - "very_unhealthy": "Mycket oh\u00e4lsosamt" - }, - "climacell__pollen_index": { - "high": "H\u00f6gt", - "low": "L\u00e5gt", - "medium": "Medium", - "none": "Inget", - "very_high": "V\u00e4ldigt h\u00f6gt", - "very_low": "V\u00e4ldigt l\u00e5gt" - }, - "climacell__precipitation_type": { - "freezing_rain": "Underkylt regn", - "ice_pellets": "Hagel", - "none": "Ingen", - "rain": "Regn", - "snow": "Sn\u00f6" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.tr.json b/homeassistant/components/climacell/translations/sensor.tr.json deleted file mode 100644 index 6c58f82bb94..00000000000 --- a/homeassistant/components/climacell/translations/sensor.tr.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "state": { - "climacell__health_concern": { - "good": "\u0130yi", - "hazardous": "Tehlikeli", - "moderate": "Il\u0131ml\u0131", - "unhealthy": "Sa\u011fl\u0131ks\u0131z", - "unhealthy_for_sensitive_groups": "Hassas Gruplar \u0130\u00e7in Sa\u011fl\u0131ks\u0131z", - "very_unhealthy": "\u00c7ok Sa\u011fl\u0131ks\u0131z" - }, - "climacell__pollen_index": { - "high": "Y\u00fcksek", - "low": "D\u00fc\u015f\u00fck", - "medium": "Orta", - "none": "Hi\u00e7biri", - "very_high": "\u00c7ok Y\u00fcksek", - "very_low": "\u00c7ok D\u00fc\u015f\u00fck" - }, - "climacell__precipitation_type": { - "freezing_rain": "Dondurucu Ya\u011fmur", - "ice_pellets": "Buz Peletleri", - "none": "Hi\u00e7biri", - "rain": "Ya\u011fmur", - "snow": "Kar" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.zh-Hant.json b/homeassistant/components/climacell/translations/sensor.zh-Hant.json deleted file mode 100644 index c9898fcfe4d..00000000000 --- a/homeassistant/components/climacell/translations/sensor.zh-Hant.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "state": { - "climacell__health_concern": { - "good": "\u826f\u597d", - "hazardous": "\u5371\u96aa", - "moderate": "\u4e2d\u7b49", - "unhealthy": "\u4e0d\u5065\u5eb7", - "unhealthy_for_sensitive_groups": "\u5c0d\u654f\u611f\u65cf\u7fa4\u4e0d\u5065\u5eb7", - "very_unhealthy": "\u975e\u5e38\u4e0d\u5065\u5eb7" - }, - "climacell__pollen_index": { - "high": "\u9ad8", - "low": "\u4f4e", - "medium": "\u4e2d", - "none": "\u7121", - "very_high": "\u6975\u9ad8", - "very_low": "\u6975\u4f4e" - }, - "climacell__precipitation_type": { - "freezing_rain": "\u51cd\u96e8", - "ice_pellets": "\u51b0\u73e0", - "none": "\u7121", - "rain": "\u4e0b\u96e8", - "snow": "\u4e0b\u96ea" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sv.json b/homeassistant/components/climacell/translations/sv.json deleted file mode 100644 index 2382ec64324..00000000000 --- a/homeassistant/components/climacell/translations/sv.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "options": { - "step": { - "init": { - "data": { - "timestep": "Min. Mellan NowCast-prognoser" - }, - "description": "Om du v\u00e4ljer att aktivera \"nowcast\"-prognosentiteten kan du konfigurera antalet minuter mellan varje prognos. Antalet prognoser som tillhandah\u00e5lls beror p\u00e5 antalet minuter som v\u00e4ljs mellan prognoserna.", - "title": "Uppdatera ClimaCell-alternativ" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/tr.json b/homeassistant/components/climacell/translations/tr.json deleted file mode 100644 index 54e24f813e4..00000000000 --- a/homeassistant/components/climacell/translations/tr.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "options": { - "step": { - "init": { - "data": { - "timestep": "Min. NowCast Tahminleri Aras\u0131nda" - }, - "description": "'Nowcast' tahmin varl\u0131\u011f\u0131n\u0131 etkinle\u015ftirmeyi se\u00e7erseniz, her tahmin aras\u0131ndaki dakika say\u0131s\u0131n\u0131 yap\u0131land\u0131rabilirsiniz. Sa\u011flanan tahmin say\u0131s\u0131, tahminler aras\u0131nda se\u00e7ilen dakika say\u0131s\u0131na ba\u011fl\u0131d\u0131r.", - "title": "ClimaCell Se\u00e7eneklerini G\u00fcncelleyin" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/zh-Hant.json b/homeassistant/components/climacell/translations/zh-Hant.json deleted file mode 100644 index 309b39ab242..00000000000 --- a/homeassistant/components/climacell/translations/zh-Hant.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "options": { - "step": { - "init": { - "data": { - "timestep": "NowCast \u9810\u5831\u9593\u9694\u5206\u9418" - }, - "description": "\u5047\u5982\u9078\u64c7\u958b\u555f `nowcast` \u9810\u5831\u5be6\u9ad4\u3001\u5c07\u53ef\u4ee5\u8a2d\u5b9a\u9810\u5831\u983b\u7387\u9593\u9694\u5206\u9418\u6578\u3002\u6839\u64da\u6240\u8f38\u5165\u7684\u9593\u9694\u6642\u9593\u5c07\u6c7a\u5b9a\u9810\u5831\u7684\u6578\u76ee\u3002", - "title": "\u66f4\u65b0 ClimaCell \u9078\u9805" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/weather.py b/homeassistant/components/climacell/weather.py deleted file mode 100644 index 73ff6361041..00000000000 --- a/homeassistant/components/climacell/weather.py +++ /dev/null @@ -1,361 +0,0 @@ -"""Weather component that handles meteorological data for your location.""" -from __future__ import annotations - -from abc import abstractmethod -from collections.abc import Mapping -from datetime import datetime -from typing import Any, cast - -from pyclimacell.const import CURRENT, DAILY, FORECASTS, HOURLY, NOWCAST - -from homeassistant.components.weather import ( - ATTR_FORECAST_CONDITION, - ATTR_FORECAST_NATIVE_PRECIPITATION, - ATTR_FORECAST_NATIVE_TEMP, - ATTR_FORECAST_NATIVE_TEMP_LOW, - ATTR_FORECAST_NATIVE_WIND_SPEED, - ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_TIME, - ATTR_FORECAST_WIND_BEARING, - WeatherEntity, -) -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - CONF_API_VERSION, - CONF_NAME, - LENGTH_INCHES, - LENGTH_MILES, - PRESSURE_INHG, - SPEED_MILES_PER_HOUR, - TEMP_FAHRENHEIT, -) -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.sun import is_up -from homeassistant.util import dt as dt_util -from homeassistant.util.speed import convert as speed_convert - -from . import ClimaCellDataUpdateCoordinator, ClimaCellEntity -from .const import ( - ATTR_CLOUD_COVER, - ATTR_PRECIPITATION_TYPE, - ATTR_WIND_GUST, - CC_V3_ATTR_CLOUD_COVER, - CC_V3_ATTR_CONDITION, - CC_V3_ATTR_HUMIDITY, - CC_V3_ATTR_OZONE, - CC_V3_ATTR_PRECIPITATION, - CC_V3_ATTR_PRECIPITATION_DAILY, - CC_V3_ATTR_PRECIPITATION_PROBABILITY, - CC_V3_ATTR_PRECIPITATION_TYPE, - CC_V3_ATTR_PRESSURE, - CC_V3_ATTR_TEMPERATURE, - CC_V3_ATTR_TEMPERATURE_HIGH, - CC_V3_ATTR_TEMPERATURE_LOW, - CC_V3_ATTR_TIMESTAMP, - CC_V3_ATTR_VISIBILITY, - CC_V3_ATTR_WIND_DIRECTION, - CC_V3_ATTR_WIND_GUST, - CC_V3_ATTR_WIND_SPEED, - CLEAR_CONDITIONS, - CONDITIONS_V3, - CONF_TIMESTEP, - DEFAULT_FORECAST_TYPE, - DOMAIN, -) - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up a config entry.""" - coordinator = hass.data[DOMAIN][config_entry.entry_id] - api_version = config_entry.data[CONF_API_VERSION] - entities = [ - ClimaCellV3WeatherEntity(config_entry, coordinator, api_version, forecast_type) - for forecast_type in (DAILY, HOURLY, NOWCAST) - ] - async_add_entities(entities) - - -class BaseClimaCellWeatherEntity(ClimaCellEntity, WeatherEntity): - """Base ClimaCell weather entity.""" - - _attr_native_precipitation_unit = LENGTH_INCHES - _attr_native_pressure_unit = PRESSURE_INHG - _attr_native_temperature_unit = TEMP_FAHRENHEIT - _attr_native_visibility_unit = LENGTH_MILES - _attr_native_wind_speed_unit = SPEED_MILES_PER_HOUR - - def __init__( - self, - config_entry: ConfigEntry, - coordinator: ClimaCellDataUpdateCoordinator, - api_version: int, - forecast_type: str, - ) -> None: - """Initialize ClimaCell Weather Entity.""" - super().__init__(config_entry, coordinator, api_version) - self.forecast_type = forecast_type - self._attr_entity_registry_enabled_default = ( - forecast_type == DEFAULT_FORECAST_TYPE - ) - self._attr_name = f"{config_entry.data[CONF_NAME]} - {forecast_type.title()}" - self._attr_unique_id = f"{config_entry.unique_id}_{forecast_type}" - - @staticmethod - @abstractmethod - def _translate_condition( - condition: str | int | None, sun_is_up: bool = True - ) -> str | None: - """Translate ClimaCell condition into an HA condition.""" - - def _forecast_dict( - self, - forecast_dt: datetime, - use_datetime: bool, - condition: int | str, - precipitation: float | None, - precipitation_probability: float | None, - temp: float | None, - temp_low: float | None, - wind_direction: float | None, - wind_speed: float | None, - ) -> dict[str, Any]: - """Return formatted Forecast dict from ClimaCell forecast data.""" - if use_datetime: - translated_condition = self._translate_condition( - condition, is_up(self.hass, forecast_dt) - ) - else: - translated_condition = self._translate_condition(condition, True) - - data = { - ATTR_FORECAST_TIME: forecast_dt.isoformat(), - ATTR_FORECAST_CONDITION: translated_condition, - ATTR_FORECAST_NATIVE_PRECIPITATION: precipitation, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: precipitation_probability, - ATTR_FORECAST_NATIVE_TEMP: temp, - ATTR_FORECAST_NATIVE_TEMP_LOW: temp_low, - ATTR_FORECAST_WIND_BEARING: wind_direction, - ATTR_FORECAST_NATIVE_WIND_SPEED: wind_speed, - } - - return {k: v for k, v in data.items() if v is not None} - - @property - def extra_state_attributes(self) -> Mapping[str, Any] | None: - """Return additional state attributes.""" - cloud_cover = self.cloud_cover - attrs = { - ATTR_CLOUD_COVER: cloud_cover, - ATTR_PRECIPITATION_TYPE: self.precipitation_type, - } - if (wind_gust := self.wind_gust) is not None: - attrs[ATTR_WIND_GUST] = round( - speed_convert(wind_gust, SPEED_MILES_PER_HOUR, self._wind_speed_unit), 4 - ) - return attrs - - @property - @abstractmethod - def cloud_cover(self): - """Return cloud cover.""" - - @property - @abstractmethod - def wind_gust(self): - """Return wind gust speed.""" - - @property - @abstractmethod - def precipitation_type(self): - """Return precipitation type.""" - - @property - @abstractmethod - def _pressure(self): - """Return the raw pressure.""" - - @property - def native_pressure(self): - """Return the pressure.""" - return self._pressure - - @property - @abstractmethod - def _wind_speed(self): - """Return the raw wind speed.""" - - @property - def native_wind_speed(self): - """Return the wind speed.""" - return self._wind_speed - - @property - @abstractmethod - def _visibility(self): - """Return the raw visibility.""" - - @property - def native_visibility(self): - """Return the visibility.""" - return self._visibility - - -class ClimaCellV3WeatherEntity(BaseClimaCellWeatherEntity): - """Entity that talks to ClimaCell v3 API to retrieve weather data.""" - - @staticmethod - def _translate_condition( - condition: int | str | None, sun_is_up: bool = True - ) -> str | None: - """Translate ClimaCell condition into an HA condition.""" - if not condition: - return None - condition = cast(str, condition) - if "clear" in condition.lower(): - if sun_is_up: - return CLEAR_CONDITIONS["day"] - return CLEAR_CONDITIONS["night"] - return CONDITIONS_V3[condition] - - @property - def native_temperature(self): - """Return the platform temperature.""" - return self._get_cc_value( - self.coordinator.data[CURRENT], CC_V3_ATTR_TEMPERATURE - ) - - @property - def _pressure(self): - """Return the raw pressure.""" - return self._get_cc_value(self.coordinator.data[CURRENT], CC_V3_ATTR_PRESSURE) - - @property - def humidity(self): - """Return the humidity.""" - return self._get_cc_value(self.coordinator.data[CURRENT], CC_V3_ATTR_HUMIDITY) - - @property - def wind_gust(self): - """Return the wind gust speed.""" - return self._get_cc_value(self.coordinator.data[CURRENT], CC_V3_ATTR_WIND_GUST) - - @property - def cloud_cover(self): - """Return the cloud cover.""" - return self._get_cc_value( - self.coordinator.data[CURRENT], CC_V3_ATTR_CLOUD_COVER - ) - - @property - def precipitation_type(self): - """Return precipitation type.""" - return self._get_cc_value( - self.coordinator.data[CURRENT], CC_V3_ATTR_PRECIPITATION_TYPE - ) - - @property - def _wind_speed(self): - """Return the raw wind speed.""" - return self._get_cc_value(self.coordinator.data[CURRENT], CC_V3_ATTR_WIND_SPEED) - - @property - def wind_bearing(self): - """Return the wind bearing.""" - return self._get_cc_value( - self.coordinator.data[CURRENT], CC_V3_ATTR_WIND_DIRECTION - ) - - @property - def ozone(self): - """Return the O3 (ozone) level.""" - return self._get_cc_value(self.coordinator.data[CURRENT], CC_V3_ATTR_OZONE) - - @property - def condition(self): - """Return the condition.""" - return self._translate_condition( - self._get_cc_value(self.coordinator.data[CURRENT], CC_V3_ATTR_CONDITION), - is_up(self.hass), - ) - - @property - def _visibility(self): - """Return the raw visibility.""" - return self._get_cc_value(self.coordinator.data[CURRENT], CC_V3_ATTR_VISIBILITY) - - @property - def forecast(self): - """Return the forecast.""" - # Check if forecasts are available - raw_forecasts = self.coordinator.data.get(FORECASTS, {}).get(self.forecast_type) - if not raw_forecasts: - return None - - forecasts = [] - - # Set default values (in cases where keys don't exist), None will be - # returned. Override properties per forecast type as needed - for forecast in raw_forecasts: - forecast_dt = dt_util.parse_datetime( - self._get_cc_value(forecast, CC_V3_ATTR_TIMESTAMP) - ) - use_datetime = True - condition = self._get_cc_value(forecast, CC_V3_ATTR_CONDITION) - precipitation = self._get_cc_value(forecast, CC_V3_ATTR_PRECIPITATION) - precipitation_probability = self._get_cc_value( - forecast, CC_V3_ATTR_PRECIPITATION_PROBABILITY - ) - temp = self._get_cc_value(forecast, CC_V3_ATTR_TEMPERATURE) - temp_low = None - wind_direction = self._get_cc_value(forecast, CC_V3_ATTR_WIND_DIRECTION) - wind_speed = self._get_cc_value(forecast, CC_V3_ATTR_WIND_SPEED) - - if self.forecast_type == DAILY: - use_datetime = False - forecast_dt = dt_util.start_of_local_day(forecast_dt) - precipitation = self._get_cc_value( - forecast, CC_V3_ATTR_PRECIPITATION_DAILY - ) - temp = next( - ( - self._get_cc_value(item, CC_V3_ATTR_TEMPERATURE_HIGH) - for item in forecast[CC_V3_ATTR_TEMPERATURE] - if "max" in item - ), - temp, - ) - temp_low = next( - ( - self._get_cc_value(item, CC_V3_ATTR_TEMPERATURE_LOW) - for item in forecast[CC_V3_ATTR_TEMPERATURE] - if "min" in item - ), - temp_low, - ) - elif self.forecast_type == NOWCAST and precipitation: - # Precipitation is forecasted in CONF_TIMESTEP increments but in a - # per hour rate, so value needs to be converted to an amount. - precipitation = ( - precipitation / 60 * self._config_entry.options[CONF_TIMESTEP] - ) - - forecasts.append( - self._forecast_dict( - forecast_dt, - use_datetime, - condition, - precipitation, - precipitation_probability, - temp, - temp_low, - wind_direction, - wind_speed, - ) - ) - - return forecasts diff --git a/homeassistant/components/tomorrowio/__init__.py b/homeassistant/components/tomorrowio/__init__.py index adb3006b24e..956b0901dd7 100644 --- a/homeassistant/components/tomorrowio/__init__.py +++ b/homeassistant/components/tomorrowio/__init__.py @@ -17,7 +17,7 @@ from pytomorrowio.exceptions import ( from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_API_KEY, CONF_LATITUDE, @@ -25,8 +25,8 @@ from homeassistant.const import ( CONF_LONGITUDE, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -123,86 +123,10 @@ def async_set_update_interval( return timedelta(minutes=minutes) -@callback -def async_migrate_entry_from_climacell( - hass: HomeAssistant, - dev_reg: dr.DeviceRegistry, - entry: ConfigEntry, - device: dr.DeviceEntry, -) -> None: - """Migrate a config entry from a Climacell entry.""" - # Remove the old config entry ID from the entry data so we don't try this again - # on the next setup - data = entry.data.copy() - old_config_entry_id = data.pop("old_config_entry_id") - hass.config_entries.async_update_entry(entry, data=data) - LOGGER.debug( - ( - "Setting up imported climacell entry %s for the first time as " - "tomorrowio entry %s" - ), - old_config_entry_id, - entry.entry_id, - ) - - ent_reg = er.async_get(hass) - for entity_entry in er.async_entries_for_config_entry(ent_reg, old_config_entry_id): - old_platform = entity_entry.platform - # In case the API key has changed due to a V3 -> V4 change, we need to - # generate the new entity's unique ID - new_unique_id = ( - f"{entry.data[CONF_API_KEY]}_" - f"{'_'.join(entity_entry.unique_id.split('_')[1:])}" - ) - ent_reg.async_update_entity_platform( - entity_entry.entity_id, - DOMAIN, - new_unique_id=new_unique_id, - new_config_entry_id=entry.entry_id, - new_device_id=device.id, - ) - assert entity_entry - LOGGER.debug( - "Migrated %s from %s to %s", - entity_entry.entity_id, - old_platform, - DOMAIN, - ) - - # We only have one device in the registry but we will do a loop just in case - for old_device in dr.async_entries_for_config_entry(dev_reg, old_config_entry_id): - if old_device.name_by_user: - dev_reg.async_update_device(device.id, name_by_user=old_device.name_by_user) - - # Remove the old config entry and now the entry is fully migrated - hass.async_create_task(hass.config_entries.async_remove(old_config_entry_id)) - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Tomorrow.io API from a config entry.""" hass.data.setdefault(DOMAIN, {}) - # Let's precreate the device so that if this is a first time setup for a config - # entry imported from a ClimaCell entry, we can apply customizations from the old - # device. - dev_reg = dr.async_get(hass) - device = dev_reg.async_get_or_create( - config_entry_id=entry.entry_id, - identifiers={(DOMAIN, entry.data[CONF_API_KEY])}, - name=INTEGRATION_NAME, - manufacturer=INTEGRATION_NAME, - sw_version="v4", - entry_type=dr.DeviceEntryType.SERVICE, - ) - - # If this is an import and we still have the old config entry ID in the entry data, - # it means we are setting this entry up for the first time after a migration from - # ClimaCell to Tomorrow.io. In order to preserve any customizations on the ClimaCell - # entities, we need to remove each old entity, creating a new entity in its place - # but attached to this entry. - if entry.source == SOURCE_IMPORT and "old_config_entry_id" in entry.data: - async_migrate_entry_from_climacell(hass, dev_reg, entry, device) - api_key = entry.data[CONF_API_KEY] # If coordinator already exists for this API key, we'll use that, otherwise # we have to create a new one @@ -408,10 +332,10 @@ class TomorrowioEntity(CoordinatorEntity[TomorrowioDataUpdateCoordinator]): self._config_entry = config_entry self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, self._config_entry.data[CONF_API_KEY])}, - name="Tomorrow.io", - manufacturer="Tomorrow.io", + name=INTEGRATION_NAME, + manufacturer=INTEGRATION_NAME, sw_version=f"v{self.api_version}", - entry_type=dr.DeviceEntryType.SERVICE, + entry_type=DeviceEntryType.SERVICE, ) def _get_current_property(self, property_name: str) -> int | str | float | None: diff --git a/homeassistant/components/tomorrowio/config_flow.py b/homeassistant/components/tomorrowio/config_flow.py index 71df06394ed..9f512dde4cb 100644 --- a/homeassistant/components/tomorrowio/config_flow.py +++ b/homeassistant/components/tomorrowio/config_flow.py @@ -17,7 +17,6 @@ from homeassistant import config_entries, core from homeassistant.components.zone import async_active_zone from homeassistant.const import ( CONF_API_KEY, - CONF_API_VERSION, CONF_FRIENDLY_NAME, CONF_LATITUDE, CONF_LOCATION, @@ -30,14 +29,10 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.selector import LocationSelector, LocationSelectorConfig from .const import ( - AUTO_MIGRATION_MESSAGE, - CC_DOMAIN, CONF_TIMESTEP, DEFAULT_NAME, DEFAULT_TIMESTEP, DOMAIN, - INTEGRATION_NAME, - MANUAL_MIGRATION_MESSAGE, TMRW_ATTR_TEMPERATURE, ) @@ -62,10 +57,6 @@ def _get_config_schema( vol.Required(CONF_API_KEY, default=input_dict.get(CONF_API_KEY)): str, } - # For imports we just need to ask for the API key - if source == config_entries.SOURCE_IMPORT: - return vol.Schema(api_key_schema, extra=vol.REMOVE_EXTRA) - default_location = ( input_dict[CONF_LOCATION] if CONF_LOCATION in input_dict @@ -125,11 +116,6 @@ class TomorrowioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - def __init__(self) -> None: - """Initialize config flow.""" - self._showed_import_message = 0 - self._import_config: dict[str, Any] | None = None - @staticmethod @callback def async_get_options_flow( @@ -144,18 +130,6 @@ class TomorrowioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle the initial step.""" errors = {} if user_input is not None: - # Grab the API key and add it to the rest of the config before continuing - if self._import_config: - self._import_config[CONF_API_KEY] = user_input[CONF_API_KEY] - self._import_config[CONF_LOCATION] = { - CONF_LATITUDE: self._import_config.pop( - CONF_LATITUDE, self.hass.config.latitude - ), - CONF_LONGITUDE: self._import_config.pop( - CONF_LONGITUDE, self.hass.config.longitude - ), - } - user_input = self._import_config.copy() await self.async_set_unique_id( unique_id=_get_unique_id(self.hass, user_input) ) @@ -189,15 +163,6 @@ class TomorrowioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if not errors: options: Mapping[str, Any] = {CONF_TIMESTEP: DEFAULT_TIMESTEP} - # Store the old config entry ID and retrieve options to recreate the entry - if self.source == config_entries.SOURCE_IMPORT: - old_config_entry_id = self.context["old_config_entry_id"] - old_config_entry = self.hass.config_entries.async_get_entry( - old_config_entry_id - ) - assert old_config_entry - options = dict(old_config_entry.options) - user_input["old_config_entry_id"] = old_config_entry_id return self.async_create_entry( title=user_input[CONF_NAME], data=user_input, @@ -209,24 +174,3 @@ class TomorrowioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): data_schema=_get_config_schema(self.hass, self.source, user_input), errors=errors, ) - - async def async_step_import(self, import_config: dict) -> FlowResult: - """Import from config.""" - # Store import config for later - self._import_config = dict(import_config) - if self._import_config.pop(CONF_API_VERSION, 3) == 3: - # Clear API key from import config - self._import_config[CONF_API_KEY] = "" - self.hass.components.persistent_notification.async_create( - MANUAL_MIGRATION_MESSAGE, - INTEGRATION_NAME, - f"{CC_DOMAIN}_to_{DOMAIN}_new_api_key_needed", - ) - return await self.async_step_user() - - self.hass.components.persistent_notification.async_create( - AUTO_MIGRATION_MESSAGE, - INTEGRATION_NAME, - f"{CC_DOMAIN}_to_{DOMAIN}", - ) - return await self.async_step_user(self._import_config) diff --git a/homeassistant/components/tomorrowio/const.py b/homeassistant/components/tomorrowio/const.py index e6f4d50a257..a6af4ef5819 100644 --- a/homeassistant/components/tomorrowio/const.py +++ b/homeassistant/components/tomorrowio/const.py @@ -27,7 +27,6 @@ FORECAST_TYPES = [DAILY, HOURLY, NOWCAST] DEFAULT_TIMESTEP = 15 DEFAULT_FORECAST_TYPE = DAILY -CC_DOMAIN = "climacell" DOMAIN = "tomorrowio" INTEGRATION_NAME = "Tomorrow.io" DEFAULT_NAME = INTEGRATION_NAME @@ -116,32 +115,3 @@ TMRW_ATTR_PRESSURE_SURFACE_LEVEL = "pressureSurfaceLevel" TMRW_ATTR_SOLAR_GHI = "solarGHI" TMRW_ATTR_CLOUD_BASE = "cloudBase" TMRW_ATTR_CLOUD_CEILING = "cloudCeiling" - -MANUAL_MIGRATION_MESSAGE = ( - "As part of [ClimaCell's rebranding to Tomorrow.io](https://www.tomorrow.io/blog/my-last-day-as-ceo-of-climacell/) " - "we will migrate your existing ClimaCell config entry (or config " - "entries) to the new Tomorrow.io integration, but because **the " - " V3 API is now deprecated**, you will need to get a new V4 API " - "key from [Tomorrow.io](https://app.tomorrow.io/development/keys)." - " Once that is done, visit the " - "[Integrations Configuration](/config/integrations) page and " - "click Configure on the Tomorrow.io card(s) to submit the new " - "key. Once your key has been validated, your config entry will " - "automatically be migrated. The new integration is a drop in " - "replacement and your existing entities will be migrated over, " - "just note that the location of the integration card on the " - "[Integrations Configuration](/config/integrations) page has changed " - "since the integration name has changed." -) - -AUTO_MIGRATION_MESSAGE = ( - "As part of [ClimaCell's rebranding to Tomorrow.io](https://www.tomorrow.io/blog/my-last-day-as-ceo-of-climacell/) " - "we have automatically migrated your existing ClimaCell config entry " - "(or as many of your ClimaCell config entries as we could) to the new " - "Tomorrow.io integration. There is nothing you need to do since the " - "new integration is a drop in replacement and your existing entities " - "have been migrated over, just note that the location of the " - "integration card on the " - "[Integrations Configuration](/config/integrations) page has changed " - "since the integration name has changed." -) diff --git a/requirements_all.txt b/requirements_all.txt index 0644da2698e..a59bc227e27 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1480,9 +1480,6 @@ pychromecast==12.1.4 # homeassistant.components.pocketcasts pycketcasts==1.0.1 -# homeassistant.components.climacell -pyclimacell==0.18.2 - # homeassistant.components.cmus pycmus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ed60bd9a80c..18a0dcb52fc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1044,9 +1044,6 @@ pycfdns==1.2.2 # homeassistant.components.cast pychromecast==12.1.4 -# homeassistant.components.climacell -pyclimacell==0.18.2 - # homeassistant.components.comfoconnect pycomfoconnect==0.4 diff --git a/tests/components/climacell/__init__.py b/tests/components/climacell/__init__.py deleted file mode 100644 index 04ebc3c14c3..00000000000 --- a/tests/components/climacell/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the ClimaCell Weather API integration.""" diff --git a/tests/components/climacell/conftest.py b/tests/components/climacell/conftest.py deleted file mode 100644 index f762dc8d6f9..00000000000 --- a/tests/components/climacell/conftest.py +++ /dev/null @@ -1,26 +0,0 @@ -"""Configure py.test.""" -import json -from unittest.mock import patch - -import pytest - -from tests.common import load_fixture - - -@pytest.fixture(name="climacell_config_entry_update") -def climacell_config_entry_update_fixture(): - """Mock valid climacell config entry setup.""" - with patch( - "homeassistant.components.climacell.ClimaCellV3.realtime", - return_value=json.loads(load_fixture("v3_realtime.json", "climacell")), - ), patch( - "homeassistant.components.climacell.ClimaCellV3.forecast_hourly", - return_value=json.loads(load_fixture("v3_forecast_hourly.json", "climacell")), - ), patch( - "homeassistant.components.climacell.ClimaCellV3.forecast_daily", - return_value=json.loads(load_fixture("v3_forecast_daily.json", "climacell")), - ), patch( - "homeassistant.components.climacell.ClimaCellV3.forecast_nowcast", - return_value=json.loads(load_fixture("v3_forecast_nowcast.json", "climacell")), - ): - yield diff --git a/tests/components/climacell/const.py b/tests/components/climacell/const.py deleted file mode 100644 index d6487bfa6ce..00000000000 --- a/tests/components/climacell/const.py +++ /dev/null @@ -1,19 +0,0 @@ -"""Constants for climacell tests.""" - -from homeassistant.const import ( - CONF_API_KEY, - CONF_API_VERSION, - CONF_LATITUDE, - CONF_LONGITUDE, - CONF_NAME, -) - -API_KEY = "aa" - -API_V3_ENTRY_DATA = { - CONF_NAME: "ClimaCell", - CONF_API_KEY: API_KEY, - CONF_LATITUDE: 80.0, - CONF_LONGITUDE: 80.0, - CONF_API_VERSION: 3, -} diff --git a/tests/components/climacell/fixtures/v3_forecast_daily.json b/tests/components/climacell/fixtures/v3_forecast_daily.json deleted file mode 100644 index 4cf4527f6d9..00000000000 --- a/tests/components/climacell/fixtures/v3_forecast_daily.json +++ /dev/null @@ -1,992 +0,0 @@ -[ - { - "temp": [ - { - "observation_time": "2021-03-07T11:00:00Z", - "min": { - "value": 23.47, - "units": "F" - } - }, - { - "observation_time": "2021-03-07T21:00:00Z", - "max": { - "value": 44.88, - "units": "F" - } - } - ], - "precipitation_accumulation": { - "value": 0, - "units": "in" - }, - "precipitation_probability": { - "value": 0, - "units": "%" - }, - "wind_speed": [ - { - "observation_time": "2021-03-08T00:00:00Z", - "min": { - "value": 2.58, - "units": "mph" - } - }, - { - "observation_time": "2021-03-07T19:00:00Z", - "max": { - "value": 7.67, - "units": "mph" - } - } - ], - "wind_direction": [ - { - "observation_time": "2021-03-08T00:00:00Z", - "min": { - "value": 72.1, - "units": "degrees" - } - }, - { - "observation_time": "2021-03-07T19:00:00Z", - "max": { - "value": 313.49, - "units": "degrees" - } - } - ], - "weather_code": { - "value": "clear" - }, - "observation_time": { - "value": "2021-03-07" - }, - "lat": 38.90694, - "lon": -77.03012 - }, - { - "temp": [ - { - "observation_time": "2021-03-08T11:00:00Z", - "min": { - "value": 24.79, - "units": "F" - } - }, - { - "observation_time": "2021-03-08T21:00:00Z", - "max": { - "value": 49.42, - "units": "F" - } - } - ], - "precipitation_accumulation": { - "value": 0, - "units": "in" - }, - "precipitation_probability": { - "value": 0, - "units": "%" - }, - "wind_speed": [ - { - "observation_time": "2021-03-08T22:00:00Z", - "min": { - "value": 1.97, - "units": "mph" - } - }, - { - "observation_time": "2021-03-08T13:00:00Z", - "max": { - "value": 7.24, - "units": "mph" - } - } - ], - "wind_direction": [ - { - "observation_time": "2021-03-08T22:00:00Z", - "min": { - "value": 268.74, - "units": "degrees" - } - }, - { - "observation_time": "2021-03-08T13:00:00Z", - "max": { - "value": 324.8, - "units": "degrees" - } - } - ], - "weather_code": { - "value": "cloudy" - }, - "observation_time": { - "value": "2021-03-08" - }, - "lat": 38.90694, - "lon": -77.03012 - }, - { - "temp": [ - { - "observation_time": "2021-03-09T11:00:00Z", - "min": { - "value": 31.48, - "units": "F" - } - }, - { - "observation_time": "2021-03-09T21:00:00Z", - "max": { - "value": 66.98, - "units": "F" - } - } - ], - "precipitation_accumulation": { - "value": 0, - "units": "in" - }, - "precipitation_probability": { - "value": 0, - "units": "%" - }, - "wind_speed": [ - { - "observation_time": "2021-03-09T22:00:00Z", - "min": { - "value": 3.35, - "units": "mph" - } - }, - { - "observation_time": "2021-03-09T19:00:00Z", - "max": { - "value": 7.05, - "units": "mph" - } - } - ], - "wind_direction": [ - { - "observation_time": "2021-03-09T22:00:00Z", - "min": { - "value": 279.37, - "units": "degrees" - } - }, - { - "observation_time": "2021-03-09T19:00:00Z", - "max": { - "value": 253.12, - "units": "degrees" - } - } - ], - "weather_code": { - "value": "mostly_cloudy" - }, - "observation_time": { - "value": "2021-03-09" - }, - "lat": 38.90694, - "lon": -77.03012 - }, - { - "temp": [ - { - "observation_time": "2021-03-10T11:00:00Z", - "min": { - "value": 37.32, - "units": "F" - } - }, - { - "observation_time": "2021-03-10T20:00:00Z", - "max": { - "value": 65.28, - "units": "F" - } - } - ], - "precipitation_accumulation": { - "value": 0, - "units": "in" - }, - "precipitation_probability": { - "value": 0, - "units": "%" - }, - "wind_speed": [ - { - "observation_time": "2021-03-10T05:00:00Z", - "min": { - "value": 2.13, - "units": "mph" - } - }, - { - "observation_time": "2021-03-10T21:00:00Z", - "max": { - "value": 9.42, - "units": "mph" - } - } - ], - "wind_direction": [ - { - "observation_time": "2021-03-10T05:00:00Z", - "min": { - "value": 342.01, - "units": "degrees" - } - }, - { - "observation_time": "2021-03-10T21:00:00Z", - "max": { - "value": 193.22, - "units": "degrees" - } - } - ], - "weather_code": { - "value": "cloudy" - }, - "observation_time": { - "value": "2021-03-10" - }, - "lat": 38.90694, - "lon": -77.03012 - }, - { - "temp": [ - { - "observation_time": "2021-03-11T12:00:00Z", - "min": { - "value": 48.69, - "units": "F" - } - }, - { - "observation_time": "2021-03-11T21:00:00Z", - "max": { - "value": 67.37, - "units": "F" - } - } - ], - "precipitation_accumulation": { - "value": 0, - "units": "in" - }, - "precipitation_probability": { - "value": 5, - "units": "%" - }, - "wind_speed": [ - { - "observation_time": "2021-03-11T02:00:00Z", - "min": { - "value": 8.82, - "units": "mph" - } - }, - { - "observation_time": "2021-03-12T01:00:00Z", - "max": { - "value": 14.47, - "units": "mph" - } - } - ], - "wind_direction": [ - { - "observation_time": "2021-03-11T02:00:00Z", - "min": { - "value": 176.84, - "units": "degrees" - } - }, - { - "observation_time": "2021-03-12T01:00:00Z", - "max": { - "value": 210.63, - "units": "degrees" - } - } - ], - "weather_code": { - "value": "cloudy" - }, - "observation_time": { - "value": "2021-03-11" - }, - "lat": 38.90694, - "lon": -77.03012 - }, - { - "temp": [ - { - "observation_time": "2021-03-12T12:00:00Z", - "min": { - "value": 53.83, - "units": "F" - } - }, - { - "observation_time": "2021-03-12T18:00:00Z", - "max": { - "value": 67.91, - "units": "F" - } - } - ], - "precipitation_accumulation": { - "value": 0.0018, - "units": "in" - }, - "precipitation_probability": { - "value": 25, - "units": "%" - }, - "wind_speed": [ - { - "observation_time": "2021-03-13T00:00:00Z", - "min": { - "value": 4.98, - "units": "mph" - } - }, - { - "observation_time": "2021-03-12T02:00:00Z", - "max": { - "value": 15.69, - "units": "mph" - } - } - ], - "wind_direction": [ - { - "observation_time": "2021-03-13T00:00:00Z", - "min": { - "value": 329.35, - "units": "degrees" - } - }, - { - "observation_time": "2021-03-12T02:00:00Z", - "max": { - "value": 211.47, - "units": "degrees" - } - } - ], - "weather_code": { - "value": "cloudy" - }, - "observation_time": { - "value": "2021-03-12" - }, - "lat": 38.90694, - "lon": -77.03012 - }, - { - "temp": [ - { - "observation_time": "2021-03-14T00:00:00Z", - "min": { - "value": 45.48, - "units": "F" - } - }, - { - "observation_time": "2021-03-13T03:00:00Z", - "max": { - "value": 60.42, - "units": "F" - } - } - ], - "precipitation_accumulation": { - "value": 0, - "units": "in" - }, - "precipitation_probability": { - "value": 25, - "units": "%" - }, - "wind_speed": [ - { - "observation_time": "2021-03-13T03:00:00Z", - "min": { - "value": 2.91, - "units": "mph" - } - }, - { - "observation_time": "2021-03-13T21:00:00Z", - "max": { - "value": 9.72, - "units": "mph" - } - } - ], - "wind_direction": [ - { - "observation_time": "2021-03-13T03:00:00Z", - "min": { - "value": 202.04, - "units": "degrees" - } - }, - { - "observation_time": "2021-03-13T21:00:00Z", - "max": { - "value": 64.38, - "units": "degrees" - } - } - ], - "weather_code": { - "value": "cloudy" - }, - "observation_time": { - "value": "2021-03-13" - }, - "lat": 38.90694, - "lon": -77.03012 - }, - { - "temp": [ - { - "observation_time": "2021-03-15T00:00:00Z", - "min": { - "value": 37.81, - "units": "F" - } - }, - { - "observation_time": "2021-03-14T03:00:00Z", - "max": { - "value": 43.58, - "units": "F" - } - } - ], - "precipitation_accumulation": { - "value": 0.0423, - "units": "in" - }, - "precipitation_probability": { - "value": 75, - "units": "%" - }, - "wind_speed": [ - { - "observation_time": "2021-03-14T06:00:00Z", - "min": { - "value": 5.34, - "units": "mph" - } - }, - { - "observation_time": "2021-03-14T21:00:00Z", - "max": { - "value": 16.25, - "units": "mph" - } - } - ], - "wind_direction": [ - { - "observation_time": "2021-03-14T06:00:00Z", - "min": { - "value": 57.52, - "units": "degrees" - } - }, - { - "observation_time": "2021-03-14T21:00:00Z", - "max": { - "value": 83.23, - "units": "degrees" - } - } - ], - "weather_code": { - "value": "rain_light" - }, - "observation_time": { - "value": "2021-03-14" - }, - "lat": 38.90694, - "lon": -77.03012 - }, - { - "temp": [ - { - "observation_time": "2021-03-16T00:00:00Z", - "min": { - "value": 32.31, - "units": "F" - } - }, - { - "observation_time": "2021-03-15T09:00:00Z", - "max": { - "value": 34.21, - "units": "F" - } - } - ], - "precipitation_accumulation": { - "value": 0.2876, - "units": "in" - }, - "precipitation_probability": { - "value": 95, - "units": "%" - }, - "wind_speed": [ - { - "observation_time": "2021-03-16T00:00:00Z", - "min": { - "value": 11.7, - "units": "mph" - } - }, - { - "observation_time": "2021-03-15T18:00:00Z", - "max": { - "value": 15.89, - "units": "mph" - } - } - ], - "wind_direction": [ - { - "observation_time": "2021-03-16T00:00:00Z", - "min": { - "value": 63.67, - "units": "degrees" - } - }, - { - "observation_time": "2021-03-15T18:00:00Z", - "max": { - "value": 59.49, - "units": "degrees" - } - } - ], - "weather_code": { - "value": "snow_heavy" - }, - "observation_time": { - "value": "2021-03-15" - }, - "lat": 38.90694, - "lon": -77.03012 - }, - { - "temp": [ - { - "observation_time": "2021-03-16T12:00:00Z", - "min": { - "value": 29.1, - "units": "F" - } - }, - { - "observation_time": "2021-03-16T21:00:00Z", - "max": { - "value": 43, - "units": "F" - } - } - ], - "precipitation_accumulation": { - "value": 0.0002, - "units": "in" - }, - "precipitation_probability": { - "value": 5, - "units": "%" - }, - "wind_speed": [ - { - "observation_time": "2021-03-16T18:00:00Z", - "min": { - "value": 4.98, - "units": "mph" - } - }, - { - "observation_time": "2021-03-16T03:00:00Z", - "max": { - "value": 9.77, - "units": "mph" - } - } - ], - "wind_direction": [ - { - "observation_time": "2021-03-16T18:00:00Z", - "min": { - "value": 80.47, - "units": "degrees" - } - }, - { - "observation_time": "2021-03-16T03:00:00Z", - "max": { - "value": 58.98, - "units": "degrees" - } - } - ], - "weather_code": { - "value": "cloudy" - }, - "observation_time": { - "value": "2021-03-16" - }, - "lat": 38.90694, - "lon": -77.03012 - }, - { - "temp": [ - { - "observation_time": "2021-03-17T12:00:00Z", - "min": { - "value": 34.32, - "units": "F" - } - }, - { - "observation_time": "2021-03-17T21:00:00Z", - "max": { - "value": 52.4, - "units": "F" - } - } - ], - "precipitation_accumulation": { - "value": 0, - "units": "in" - }, - "precipitation_probability": { - "value": 0, - "units": "%" - }, - "wind_speed": [ - { - "observation_time": "2021-03-18T00:00:00Z", - "min": { - "value": 4.49, - "units": "mph" - } - }, - { - "observation_time": "2021-03-17T03:00:00Z", - "max": { - "value": 6.71, - "units": "mph" - } - } - ], - "wind_direction": [ - { - "observation_time": "2021-03-18T00:00:00Z", - "min": { - "value": 116.64, - "units": "degrees" - } - }, - { - "observation_time": "2021-03-17T03:00:00Z", - "max": { - "value": 111.51, - "units": "degrees" - } - } - ], - "weather_code": { - "value": "cloudy" - }, - "observation_time": { - "value": "2021-03-17" - }, - "lat": 38.90694, - "lon": -77.03012 - }, - { - "temp": [ - { - "observation_time": "2021-03-18T12:00:00Z", - "min": { - "value": 41.99, - "units": "F" - } - }, - { - "observation_time": "2021-03-18T21:00:00Z", - "max": { - "value": 54.07, - "units": "F" - } - } - ], - "precipitation_accumulation": { - "value": 0, - "units": "in" - }, - "precipitation_probability": { - "value": 5, - "units": "%" - }, - "wind_speed": [ - { - "observation_time": "2021-03-18T06:00:00Z", - "min": { - "value": 2.77, - "units": "mph" - } - }, - { - "observation_time": "2021-03-18T03:00:00Z", - "max": { - "value": 5.22, - "units": "mph" - } - } - ], - "wind_direction": [ - { - "observation_time": "2021-03-18T06:00:00Z", - "min": { - "value": 119.5, - "units": "degrees" - } - }, - { - "observation_time": "2021-03-18T03:00:00Z", - "max": { - "value": 135.5, - "units": "degrees" - } - } - ], - "weather_code": { - "value": "cloudy" - }, - "observation_time": { - "value": "2021-03-18" - }, - "lat": 38.90694, - "lon": -77.03012 - }, - { - "temp": [ - { - "observation_time": "2021-03-19T12:00:00Z", - "min": { - "value": 40.48, - "units": "F" - } - }, - { - "observation_time": "2021-03-19T18:00:00Z", - "max": { - "value": 48.94, - "units": "F" - } - } - ], - "precipitation_accumulation": { - "value": 0.007, - "units": "in" - }, - "precipitation_probability": { - "value": 45, - "units": "%" - }, - "wind_speed": [ - { - "observation_time": "2021-03-19T03:00:00Z", - "min": { - "value": 5.43, - "units": "mph" - } - }, - { - "observation_time": "2021-03-20T00:00:00Z", - "max": { - "value": 11.1, - "units": "mph" - } - } - ], - "wind_direction": [ - { - "observation_time": "2021-03-19T03:00:00Z", - "min": { - "value": 50.18, - "units": "degrees" - } - }, - { - "observation_time": "2021-03-20T00:00:00Z", - "max": { - "value": 86.96, - "units": "degrees" - } - } - ], - "weather_code": { - "value": "cloudy" - }, - "observation_time": { - "value": "2021-03-19" - }, - "lat": 38.90694, - "lon": -77.03012 - }, - { - "temp": [ - { - "observation_time": "2021-03-21T00:00:00Z", - "min": { - "value": 37.56, - "units": "F" - } - }, - { - "observation_time": "2021-03-20T03:00:00Z", - "max": { - "value": 41.05, - "units": "F" - } - } - ], - "precipitation_accumulation": { - "value": 0.0485, - "units": "in" - }, - "precipitation_probability": { - "value": 55, - "units": "%" - }, - "wind_speed": [ - { - "observation_time": "2021-03-20T03:00:00Z", - "min": { - "value": 10.9, - "units": "mph" - } - }, - { - "observation_time": "2021-03-20T21:00:00Z", - "max": { - "value": 17.35, - "units": "mph" - } - } - ], - "wind_direction": [ - { - "observation_time": "2021-03-20T03:00:00Z", - "min": { - "value": 70.56, - "units": "degrees" - } - }, - { - "observation_time": "2021-03-20T21:00:00Z", - "max": { - "value": 58.55, - "units": "degrees" - } - } - ], - "weather_code": { - "value": "drizzle" - }, - "observation_time": { - "value": "2021-03-20" - }, - "lat": 38.90694, - "lon": -77.03012 - }, - { - "temp": [ - { - "observation_time": "2021-03-21T12:00:00Z", - "min": { - "value": 33.66, - "units": "F" - } - }, - { - "observation_time": "2021-03-21T21:00:00Z", - "max": { - "value": 44.3, - "units": "F" - } - } - ], - "precipitation_accumulation": { - "value": 0.0017, - "units": "in" - }, - "precipitation_probability": { - "value": 20, - "units": "%" - }, - "wind_speed": [ - { - "observation_time": "2021-03-22T00:00:00Z", - "min": { - "value": 8.65, - "units": "mph" - } - }, - { - "observation_time": "2021-03-21T03:00:00Z", - "max": { - "value": 16.53, - "units": "mph" - } - } - ], - "wind_direction": [ - { - "observation_time": "2021-03-22T00:00:00Z", - "min": { - "value": 64.92, - "units": "degrees" - } - }, - { - "observation_time": "2021-03-21T03:00:00Z", - "max": { - "value": 57.74, - "units": "degrees" - } - } - ], - "weather_code": { - "value": "cloudy" - }, - "observation_time": { - "value": "2021-03-21" - }, - "lat": 38.90694, - "lon": -77.03012 - } -] diff --git a/tests/components/climacell/fixtures/v3_forecast_hourly.json b/tests/components/climacell/fixtures/v3_forecast_hourly.json deleted file mode 100644 index e6c18890809..00000000000 --- a/tests/components/climacell/fixtures/v3_forecast_hourly.json +++ /dev/null @@ -1,752 +0,0 @@ -[ - { - "lon": -77.03012, - "lat": 38.90694, - "temp": { - "value": 42.75, - "units": "F" - }, - "precipitation": { - "value": 0, - "units": "in/hr" - }, - "precipitation_probability": { - "value": 0, - "units": "%" - }, - "wind_speed": { - "value": 8.99, - "units": "mph" - }, - "wind_direction": { - "value": 320.22, - "units": "degrees" - }, - "weather_code": { - "value": "clear" - }, - "observation_time": { - "value": "2021-03-07T18:00:00.000Z" - } - }, - { - "lon": -77.03012, - "lat": 38.90694, - "temp": { - "value": 44.29, - "units": "F" - }, - "precipitation": { - "value": 0, - "units": "in/hr" - }, - "precipitation_probability": { - "value": 0, - "units": "%" - }, - "wind_speed": { - "value": 9.65, - "units": "mph" - }, - "wind_direction": { - "value": 326.14, - "units": "degrees" - }, - "weather_code": { - "value": "clear" - }, - "observation_time": { - "value": "2021-03-07T19:00:00.000Z" - } - }, - { - "lon": -77.03012, - "lat": 38.90694, - "temp": { - "value": 45.3, - "units": "F" - }, - "precipitation": { - "value": 0, - "units": "in/hr" - }, - "precipitation_probability": { - "value": 0, - "units": "%" - }, - "wind_speed": { - "value": 9.28, - "units": "mph" - }, - "wind_direction": { - "value": 322.01, - "units": "degrees" - }, - "weather_code": { - "value": "clear" - }, - "observation_time": { - "value": "2021-03-07T20:00:00.000Z" - } - }, - { - "lon": -77.03012, - "lat": 38.90694, - "temp": { - "value": 45.26, - "units": "F" - }, - "precipitation": { - "value": 0, - "units": "in/hr" - }, - "precipitation_probability": { - "value": 0, - "units": "%" - }, - "wind_speed": { - "value": 9.12, - "units": "mph" - }, - "wind_direction": { - "value": 323.71, - "units": "degrees" - }, - "weather_code": { - "value": "clear" - }, - "observation_time": { - "value": "2021-03-07T21:00:00.000Z" - } - }, - { - "lon": -77.03012, - "lat": 38.90694, - "temp": { - "value": 44.83, - "units": "F" - }, - "precipitation": { - "value": 0, - "units": "in/hr" - }, - "precipitation_probability": { - "value": 0, - "units": "%" - }, - "wind_speed": { - "value": 7.27, - "units": "mph" - }, - "wind_direction": { - "value": 319.88, - "units": "degrees" - }, - "weather_code": { - "value": "clear" - }, - "observation_time": { - "value": "2021-03-07T22:00:00.000Z" - } - }, - { - "lon": -77.03012, - "lat": 38.90694, - "temp": { - "value": 41.7, - "units": "F" - }, - "precipitation": { - "value": 0, - "units": "in/hr" - }, - "precipitation_probability": { - "value": 0, - "units": "%" - }, - "wind_speed": { - "value": 4.37, - "units": "mph" - }, - "wind_direction": { - "value": 320.69, - "units": "degrees" - }, - "weather_code": { - "value": "clear" - }, - "observation_time": { - "value": "2021-03-07T23:00:00.000Z" - } - }, - { - "lon": -77.03012, - "lat": 38.90694, - "temp": { - "value": 38.04, - "units": "F" - }, - "precipitation": { - "value": 0, - "units": "in/hr" - }, - "precipitation_probability": { - "value": 0, - "units": "%" - }, - "wind_speed": { - "value": 5.45, - "units": "mph" - }, - "wind_direction": { - "value": 351.54, - "units": "degrees" - }, - "weather_code": { - "value": "clear" - }, - "observation_time": { - "value": "2021-03-08T00:00:00.000Z" - } - }, - { - "lon": -77.03012, - "lat": 38.90694, - "temp": { - "value": 35.88, - "units": "F" - }, - "precipitation": { - "value": 0, - "units": "in/hr" - }, - "precipitation_probability": { - "value": 0, - "units": "%" - }, - "wind_speed": { - "value": 5.31, - "units": "mph" - }, - "wind_direction": { - "value": 20.6, - "units": "degrees" - }, - "weather_code": { - "value": "clear" - }, - "observation_time": { - "value": "2021-03-08T01:00:00.000Z" - } - }, - { - "lon": -77.03012, - "lat": 38.90694, - "temp": { - "value": 34.34, - "units": "F" - }, - "precipitation": { - "value": 0, - "units": "in/hr" - }, - "precipitation_probability": { - "value": 0, - "units": "%" - }, - "wind_speed": { - "value": 5.78, - "units": "mph" - }, - "wind_direction": { - "value": 11.22, - "units": "degrees" - }, - "weather_code": { - "value": "clear" - }, - "observation_time": { - "value": "2021-03-08T02:00:00.000Z" - } - }, - { - "lon": -77.03012, - "lat": 38.90694, - "temp": { - "value": 33.3, - "units": "F" - }, - "precipitation": { - "value": 0, - "units": "in/hr" - }, - "precipitation_probability": { - "value": 0, - "units": "%" - }, - "wind_speed": { - "value": 5.73, - "units": "mph" - }, - "wind_direction": { - "value": 15.46, - "units": "degrees" - }, - "weather_code": { - "value": "clear" - }, - "observation_time": { - "value": "2021-03-08T03:00:00.000Z" - } - }, - { - "lon": -77.03012, - "lat": 38.90694, - "temp": { - "value": 31.74, - "units": "F" - }, - "precipitation": { - "value": 0, - "units": "in/hr" - }, - "precipitation_probability": { - "value": 0, - "units": "%" - }, - "wind_speed": { - "value": 4.44, - "units": "mph" - }, - "wind_direction": { - "value": 26.07, - "units": "degrees" - }, - "weather_code": { - "value": "clear" - }, - "observation_time": { - "value": "2021-03-08T04:00:00.000Z" - } - }, - { - "lon": -77.03012, - "lat": 38.90694, - "temp": { - "value": 29.98, - "units": "F" - }, - "precipitation": { - "value": 0, - "units": "in/hr" - }, - "precipitation_probability": { - "value": 0, - "units": "%" - }, - "wind_speed": { - "value": 4.33, - "units": "mph" - }, - "wind_direction": { - "value": 23.7, - "units": "degrees" - }, - "weather_code": { - "value": "clear" - }, - "observation_time": { - "value": "2021-03-08T05:00:00.000Z" - } - }, - { - "lon": -77.03012, - "lat": 38.90694, - "temp": { - "value": 27.34, - "units": "F" - }, - "precipitation": { - "value": 0, - "units": "in/hr" - }, - "precipitation_probability": { - "value": 0, - "units": "%" - }, - "wind_speed": { - "value": 4.7, - "units": "mph" - }, - "wind_direction": { - "value": 354.56, - "units": "degrees" - }, - "weather_code": { - "value": "clear" - }, - "observation_time": { - "value": "2021-03-08T06:00:00.000Z" - } - }, - { - "lon": -77.03012, - "lat": 38.90694, - "temp": { - "value": 26.61, - "units": "F" - }, - "precipitation": { - "value": 0, - "units": "in/hr" - }, - "precipitation_probability": { - "value": 0, - "units": "%" - }, - "wind_speed": { - "value": 4.94, - "units": "mph" - }, - "wind_direction": { - "value": 349.63, - "units": "degrees" - }, - "weather_code": { - "value": "clear" - }, - "observation_time": { - "value": "2021-03-08T07:00:00.000Z" - } - }, - { - "lon": -77.03012, - "lat": 38.90694, - "temp": { - "value": 25.96, - "units": "F" - }, - "precipitation": { - "value": 0, - "units": "in/hr" - }, - "precipitation_probability": { - "value": 0, - "units": "%" - }, - "wind_speed": { - "value": 4.61, - "units": "mph" - }, - "wind_direction": { - "value": 336.74, - "units": "degrees" - }, - "weather_code": { - "value": "clear" - }, - "observation_time": { - "value": "2021-03-08T08:00:00.000Z" - } - }, - { - "lon": -77.03012, - "lat": 38.90694, - "temp": { - "value": 25.72, - "units": "F" - }, - "precipitation": { - "value": 0, - "units": "in/hr" - }, - "precipitation_probability": { - "value": 0, - "units": "%" - }, - "wind_speed": { - "value": 4.22, - "units": "mph" - }, - "wind_direction": { - "value": 332.71, - "units": "degrees" - }, - "weather_code": { - "value": "clear" - }, - "observation_time": { - "value": "2021-03-08T09:00:00.000Z" - } - }, - { - "lon": -77.03012, - "lat": 38.90694, - "temp": { - "value": 25.68, - "units": "F" - }, - "precipitation": { - "value": 0, - "units": "in/hr" - }, - "precipitation_probability": { - "value": 0, - "units": "%" - }, - "wind_speed": { - "value": 4.56, - "units": "mph" - }, - "wind_direction": { - "value": 328.58, - "units": "degrees" - }, - "weather_code": { - "value": "clear" - }, - "observation_time": { - "value": "2021-03-08T10:00:00.000Z" - } - }, - { - "lon": -77.03012, - "lat": 38.90694, - "temp": { - "value": 31.02, - "units": "F" - }, - "precipitation": { - "value": 0, - "units": "in/hr" - }, - "precipitation_probability": { - "value": 0, - "units": "%" - }, - "wind_speed": { - "value": 2.8, - "units": "mph" - }, - "wind_direction": { - "value": 322.27, - "units": "degrees" - }, - "weather_code": { - "value": "clear" - }, - "observation_time": { - "value": "2021-03-08T11:00:00.000Z" - } - }, - { - "lon": -77.03012, - "lat": 38.90694, - "temp": { - "value": 31.04, - "units": "F" - }, - "precipitation": { - "value": 0, - "units": "in/hr" - }, - "precipitation_probability": { - "value": 0, - "units": "%" - }, - "wind_speed": { - "value": 2.82, - "units": "mph" - }, - "wind_direction": { - "value": 325.27, - "units": "degrees" - }, - "weather_code": { - "value": "clear" - }, - "observation_time": { - "value": "2021-03-08T12:00:00.000Z" - } - }, - { - "lat": 38.90694, - "lon": -77.03012, - "temp": { - "value": 29.95, - "units": "F" - }, - "precipitation": { - "value": 0, - "units": "in/hr" - }, - "precipitation_probability": { - "value": 0, - "units": "%" - }, - "wind_speed": { - "value": 7.24, - "units": "mph" - }, - "wind_direction": { - "value": 324.8, - "units": "degrees" - }, - "weather_code": { - "value": "mostly_clear" - }, - "observation_time": { - "value": "2021-03-08T13:00:00.000Z" - } - }, - { - "lat": 38.90694, - "lon": -77.03012, - "temp": { - "value": 34.02, - "units": "F" - }, - "precipitation": { - "value": 0, - "units": "in/hr" - }, - "precipitation_probability": { - "value": 0, - "units": "%" - }, - "wind_speed": { - "value": 6.28, - "units": "mph" - }, - "wind_direction": { - "value": 335.16, - "units": "degrees" - }, - "weather_code": { - "value": "partly_cloudy" - }, - "observation_time": { - "value": "2021-03-08T14:00:00.000Z" - } - }, - { - "lat": 38.90694, - "lon": -77.03012, - "temp": { - "value": 37.78, - "units": "F" - }, - "precipitation": { - "value": 0, - "units": "in/hr" - }, - "precipitation_probability": { - "value": 0, - "units": "%" - }, - "wind_speed": { - "value": 5.8, - "units": "mph" - }, - "wind_direction": { - "value": 324.49, - "units": "degrees" - }, - "weather_code": { - "value": "cloudy" - }, - "observation_time": { - "value": "2021-03-08T15:00:00.000Z" - } - }, - { - "lat": 38.90694, - "lon": -77.03012, - "temp": { - "value": 40.57, - "units": "F" - }, - "precipitation": { - "value": 0, - "units": "in/hr" - }, - "precipitation_probability": { - "value": 0, - "units": "%" - }, - "wind_speed": { - "value": 5.5, - "units": "mph" - }, - "wind_direction": { - "value": 310.68, - "units": "degrees" - }, - "weather_code": { - "value": "mostly_cloudy" - }, - "observation_time": { - "value": "2021-03-08T16:00:00.000Z" - } - }, - { - "lat": 38.90694, - "lon": -77.03012, - "temp": { - "value": 42.83, - "units": "F" - }, - "precipitation": { - "value": 0, - "units": "in/hr" - }, - "precipitation_probability": { - "value": 0, - "units": "%" - }, - "wind_speed": { - "value": 5.47, - "units": "mph" - }, - "wind_direction": { - "value": 304.18, - "units": "degrees" - }, - "weather_code": { - "value": "mostly_clear" - }, - "observation_time": { - "value": "2021-03-08T17:00:00.000Z" - } - }, - { - "lat": 38.90694, - "lon": -77.03012, - "temp": { - "value": 45.07, - "units": "F" - }, - "precipitation": { - "value": 0, - "units": "in/hr" - }, - "precipitation_probability": { - "value": 0, - "units": "%" - }, - "wind_speed": { - "value": 4.88, - "units": "mph" - }, - "wind_direction": { - "value": 301.19, - "units": "degrees" - }, - "weather_code": { - "value": "clear" - }, - "observation_time": { - "value": "2021-03-08T18:00:00.000Z" - } - } -] diff --git a/tests/components/climacell/fixtures/v3_forecast_nowcast.json b/tests/components/climacell/fixtures/v3_forecast_nowcast.json deleted file mode 100644 index 9439f944401..00000000000 --- a/tests/components/climacell/fixtures/v3_forecast_nowcast.json +++ /dev/null @@ -1,782 +0,0 @@ -[ - { - "lat": 38.90694, - "lon": -77.03012, - "temp": { - "value": 44.14, - "units": "F" - }, - "wind_speed": { - "value": 9.58, - "units": "mph" - }, - "precipitation": { - "value": 0, - "units": "in/hr" - }, - "wind_direction": { - "value": 320.22, - "units": "degrees" - }, - "observation_time": { - "value": "2021-03-07T18:54:06.493Z" - }, - "weather_code": { - "value": "clear" - } - }, - { - "lat": 38.90694, - "lon": -77.03012, - "temp": { - "value": 44.17, - "units": "F" - }, - "wind_speed": { - "value": 9.59, - "units": "mph" - }, - "precipitation": { - "value": 0, - "units": "in/hr" - }, - "wind_direction": { - "value": 320.22, - "units": "degrees" - }, - "observation_time": { - "value": "2021-03-07T18:55:06.493Z" - }, - "weather_code": { - "value": "clear" - } - }, - { - "lat": 38.90694, - "lon": -77.03012, - "temp": { - "value": 44.19, - "units": "F" - }, - "wind_speed": { - "value": 9.6, - "units": "mph" - }, - "precipitation": { - "value": 0, - "units": "in/hr" - }, - "wind_direction": { - "value": 320.22, - "units": "degrees" - }, - "observation_time": { - "value": "2021-03-07T18:56:06.493Z" - }, - "weather_code": { - "value": "clear" - } - }, - { - "lat": 38.90694, - "lon": -77.03012, - "temp": { - "value": 44.22, - "units": "F" - }, - "wind_speed": { - "value": 9.61, - "units": "mph" - }, - "precipitation": { - "value": 0, - "units": "in/hr" - }, - "wind_direction": { - "value": 320.22, - "units": "degrees" - }, - "observation_time": { - "value": "2021-03-07T18:57:06.493Z" - }, - "weather_code": { - "value": "clear" - } - }, - { - "lat": 38.90694, - "lon": -77.03012, - "temp": { - "value": 44.24, - "units": "F" - }, - "wind_speed": { - "value": 9.62, - "units": "mph" - }, - "precipitation": { - "value": 0, - "units": "in/hr" - }, - "wind_direction": { - "value": 320.22, - "units": "degrees" - }, - "observation_time": { - "value": "2021-03-07T18:58:06.493Z" - }, - "weather_code": { - "value": "clear" - } - }, - { - "lat": 38.90694, - "lon": -77.03012, - "temp": { - "value": 44.27, - "units": "F" - }, - "wind_speed": { - "value": 9.64, - "units": "mph" - }, - "precipitation": { - "value": 0, - "units": "in/hr" - }, - "wind_direction": { - "value": 320.22, - "units": "degrees" - }, - "observation_time": { - "value": "2021-03-07T18:59:06.493Z" - }, - "weather_code": { - "value": "clear" - } - }, - { - "lat": 38.90694, - "lon": -77.03012, - "temp": { - "value": 44.29, - "units": "F" - }, - "wind_speed": { - "value": 9.65, - "units": "mph" - }, - "precipitation": { - "value": 0, - "units": "in/hr" - }, - "wind_direction": { - "value": 326.14, - "units": "degrees" - }, - "observation_time": { - "value": "2021-03-07T19:00:06.493Z" - }, - "weather_code": { - "value": "clear" - } - }, - { - "lat": 38.90694, - "lon": -77.03012, - "temp": { - "value": 44.31, - "units": "F" - }, - "wind_speed": { - "value": 9.64, - "units": "mph" - }, - "precipitation": { - "value": 0, - "units": "in/hr" - }, - "wind_direction": { - "value": 326.14, - "units": "degrees" - }, - "observation_time": { - "value": "2021-03-07T19:01:06.493Z" - }, - "weather_code": { - "value": "clear" - } - }, - { - "lat": 38.90694, - "lon": -77.03012, - "temp": { - "value": 44.33, - "units": "F" - }, - "wind_speed": { - "value": 9.63, - "units": "mph" - }, - "precipitation": { - "value": 0, - "units": "in/hr" - }, - "wind_direction": { - "value": 326.14, - "units": "degrees" - }, - "observation_time": { - "value": "2021-03-07T19:02:06.493Z" - }, - "weather_code": { - "value": "clear" - } - }, - { - "lat": 38.90694, - "lon": -77.03012, - "temp": { - "value": 44.34, - "units": "F" - }, - "wind_speed": { - "value": 9.63, - "units": "mph" - }, - "precipitation": { - "value": 0, - "units": "in/hr" - }, - "wind_direction": { - "value": 326.14, - "units": "degrees" - }, - "observation_time": { - "value": "2021-03-07T19:03:06.493Z" - }, - "weather_code": { - "value": "clear" - } - }, - { - "lat": 38.90694, - "lon": -77.03012, - "temp": { - "value": 44.36, - "units": "F" - }, - "wind_speed": { - "value": 9.62, - "units": "mph" - }, - "precipitation": { - "value": 0, - "units": "in/hr" - }, - "wind_direction": { - "value": 326.14, - "units": "degrees" - }, - "observation_time": { - "value": "2021-03-07T19:04:06.493Z" - }, - "weather_code": { - "value": "clear" - } - }, - { - "lat": 38.90694, - "lon": -77.03012, - "temp": { - "value": 44.38, - "units": "F" - }, - "wind_speed": { - "value": 9.61, - "units": "mph" - }, - "precipitation": { - "value": 0, - "units": "in/hr" - }, - "wind_direction": { - "value": 326.14, - "units": "degrees" - }, - "observation_time": { - "value": "2021-03-07T19:05:06.493Z" - }, - "weather_code": { - "value": "clear" - } - }, - { - "lat": 38.90694, - "lon": -77.03012, - "temp": { - "value": 44.4, - "units": "F" - }, - "wind_speed": { - "value": 9.61, - "units": "mph" - }, - "precipitation": { - "value": 0, - "units": "in/hr" - }, - "wind_direction": { - "value": 326.14, - "units": "degrees" - }, - "observation_time": { - "value": "2021-03-07T19:06:06.493Z" - }, - "weather_code": { - "value": "clear" - } - }, - { - "lat": 38.90694, - "lon": -77.03012, - "temp": { - "value": 44.41, - "units": "F" - }, - "wind_speed": { - "value": 9.6, - "units": "mph" - }, - "precipitation": { - "value": 0, - "units": "in/hr" - }, - "wind_direction": { - "value": 326.14, - "units": "degrees" - }, - "observation_time": { - "value": "2021-03-07T19:07:06.493Z" - }, - "weather_code": { - "value": "clear" - } - }, - { - "lat": 38.90694, - "lon": -77.03012, - "temp": { - "value": 44.43, - "units": "F" - }, - "wind_speed": { - "value": 9.6, - "units": "mph" - }, - "precipitation": { - "value": 0, - "units": "in/hr" - }, - "wind_direction": { - "value": 326.14, - "units": "degrees" - }, - "observation_time": { - "value": "2021-03-07T19:08:06.493Z" - }, - "weather_code": { - "value": "clear" - } - }, - { - "lat": 38.90694, - "lon": -77.03012, - "temp": { - "value": 44.45, - "units": "F" - }, - "wind_speed": { - "value": 9.59, - "units": "mph" - }, - "precipitation": { - "value": 0, - "units": "in/hr" - }, - "wind_direction": { - "value": 326.14, - "units": "degrees" - }, - "observation_time": { - "value": "2021-03-07T19:09:06.493Z" - }, - "weather_code": { - "value": "clear" - } - }, - { - "lat": 38.90694, - "lon": -77.03012, - "temp": { - "value": 44.46, - "units": "F" - }, - "wind_speed": { - "value": 9.58, - "units": "mph" - }, - "precipitation": { - "value": 0, - "units": "in/hr" - }, - "wind_direction": { - "value": 326.14, - "units": "degrees" - }, - "observation_time": { - "value": "2021-03-07T19:10:06.493Z" - }, - "weather_code": { - "value": "clear" - } - }, - { - "lat": 38.90694, - "lon": -77.03012, - "temp": { - "value": 44.48, - "units": "F" - }, - "wind_speed": { - "value": 9.58, - "units": "mph" - }, - "precipitation": { - "value": 0, - "units": "in/hr" - }, - "wind_direction": { - "value": 326.14, - "units": "degrees" - }, - "observation_time": { - "value": "2021-03-07T19:11:06.493Z" - }, - "weather_code": { - "value": "clear" - } - }, - { - "lat": 38.90694, - "lon": -77.03012, - "temp": { - "value": 44.5, - "units": "F" - }, - "wind_speed": { - "value": 9.57, - "units": "mph" - }, - "precipitation": { - "value": 0, - "units": "in/hr" - }, - "wind_direction": { - "value": 326.14, - "units": "degrees" - }, - "observation_time": { - "value": "2021-03-07T19:12:06.493Z" - }, - "weather_code": { - "value": "clear" - } - }, - { - "lat": 38.90694, - "lon": -77.03012, - "temp": { - "value": 44.51, - "units": "F" - }, - "wind_speed": { - "value": 9.57, - "units": "mph" - }, - "precipitation": { - "value": 0, - "units": "in/hr" - }, - "wind_direction": { - "value": 326.14, - "units": "degrees" - }, - "observation_time": { - "value": "2021-03-07T19:13:06.493Z" - }, - "weather_code": { - "value": "clear" - } - }, - { - "lat": 38.90694, - "lon": -77.03012, - "temp": { - "value": 44.53, - "units": "F" - }, - "wind_speed": { - "value": 9.56, - "units": "mph" - }, - "precipitation": { - "value": 0, - "units": "in/hr" - }, - "wind_direction": { - "value": 326.14, - "units": "degrees" - }, - "observation_time": { - "value": "2021-03-07T19:14:06.493Z" - }, - "weather_code": { - "value": "clear" - } - }, - { - "lat": 38.90694, - "lon": -77.03012, - "temp": { - "value": 44.55, - "units": "F" - }, - "wind_speed": { - "value": 9.55, - "units": "mph" - }, - "precipitation": { - "value": 0, - "units": "in/hr" - }, - "wind_direction": { - "value": 326.14, - "units": "degrees" - }, - "observation_time": { - "value": "2021-03-07T19:15:06.493Z" - }, - "weather_code": { - "value": "clear" - } - }, - { - "lat": 38.90694, - "lon": -77.03012, - "temp": { - "value": 44.56, - "units": "F" - }, - "wind_speed": { - "value": 9.55, - "units": "mph" - }, - "precipitation": { - "value": 0, - "units": "in/hr" - }, - "wind_direction": { - "value": 326.14, - "units": "degrees" - }, - "observation_time": { - "value": "2021-03-07T19:16:06.493Z" - }, - "weather_code": { - "value": "clear" - } - }, - { - "lat": 38.90694, - "lon": -77.03012, - "temp": { - "value": 44.58, - "units": "F" - }, - "wind_speed": { - "value": 9.54, - "units": "mph" - }, - "precipitation": { - "value": 0, - "units": "in/hr" - }, - "wind_direction": { - "value": 326.14, - "units": "degrees" - }, - "observation_time": { - "value": "2021-03-07T19:17:06.493Z" - }, - "weather_code": { - "value": "clear" - } - }, - { - "lat": 38.90694, - "lon": -77.03012, - "temp": { - "value": 44.6, - "units": "F" - }, - "wind_speed": { - "value": 9.54, - "units": "mph" - }, - "precipitation": { - "value": 0, - "units": "in/hr" - }, - "wind_direction": { - "value": 326.14, - "units": "degrees" - }, - "observation_time": { - "value": "2021-03-07T19:18:06.493Z" - }, - "weather_code": { - "value": "clear" - } - }, - { - "lat": 38.90694, - "lon": -77.03012, - "temp": { - "value": 44.61, - "units": "F" - }, - "wind_speed": { - "value": 9.53, - "units": "mph" - }, - "precipitation": { - "value": 0, - "units": "in/hr" - }, - "wind_direction": { - "value": 326.14, - "units": "degrees" - }, - "observation_time": { - "value": "2021-03-07T19:19:06.493Z" - }, - "weather_code": { - "value": "clear" - } - }, - { - "lat": 38.90694, - "lon": -77.03012, - "temp": { - "value": 44.63, - "units": "F" - }, - "wind_speed": { - "value": 9.52, - "units": "mph" - }, - "precipitation": { - "value": 0, - "units": "in/hr" - }, - "wind_direction": { - "value": 326.14, - "units": "degrees" - }, - "observation_time": { - "value": "2021-03-07T19:20:06.493Z" - }, - "weather_code": { - "value": "clear" - } - }, - { - "lat": 38.90694, - "lon": -77.03012, - "temp": { - "value": 44.65, - "units": "F" - }, - "wind_speed": { - "value": 9.52, - "units": "mph" - }, - "precipitation": { - "value": 0, - "units": "in/hr" - }, - "wind_direction": { - "value": 326.14, - "units": "degrees" - }, - "observation_time": { - "value": "2021-03-07T19:21:06.493Z" - }, - "weather_code": { - "value": "clear" - } - }, - { - "lat": 38.90694, - "lon": -77.03012, - "temp": { - "value": 44.66, - "units": "F" - }, - "wind_speed": { - "value": 9.51, - "units": "mph" - }, - "precipitation": { - "value": 0, - "units": "in/hr" - }, - "wind_direction": { - "value": 326.14, - "units": "degrees" - }, - "observation_time": { - "value": "2021-03-07T19:22:06.493Z" - }, - "weather_code": { - "value": "clear" - } - }, - { - "lat": 38.90694, - "lon": -77.03012, - "temp": { - "value": 44.68, - "units": "F" - }, - "wind_speed": { - "value": 9.51, - "units": "mph" - }, - "precipitation": { - "value": 0, - "units": "in/hr" - }, - "wind_direction": { - "value": 326.14, - "units": "degrees" - }, - "observation_time": { - "value": "2021-03-07T19:23:06.493Z" - }, - "weather_code": { - "value": "clear" - } - } -] diff --git a/tests/components/climacell/fixtures/v3_realtime.json b/tests/components/climacell/fixtures/v3_realtime.json deleted file mode 100644 index 4c3880b139a..00000000000 --- a/tests/components/climacell/fixtures/v3_realtime.json +++ /dev/null @@ -1,102 +0,0 @@ -{ - "lat": 38.90694, - "lon": -77.03012, - "temp": { - "value": 43.93, - "units": "F" - }, - "wind_speed": { - "value": 9.09, - "units": "mph" - }, - "baro_pressure": { - "value": 30.3605, - "units": "inHg" - }, - "visibility": { - "value": 6.21, - "units": "mi" - }, - "humidity": { - "value": 24.5, - "units": "%" - }, - "wind_direction": { - "value": 320.31, - "units": "degrees" - }, - "weather_code": { - "value": "clear" - }, - "o3": { - "value": 52.625, - "units": "ppb" - }, - "wind_gust": { - "value": 14.96, - "units": "mph" - }, - "precipitation_type": { - "value": "rain" - }, - "cloud_cover": { - "value": 100, - "units": "%" - }, - "fire_index": { - "value": 9 - }, - "epa_aqi": { - "value": 22.3125 - }, - "epa_primary_pollutant": { - "value": "pm25" - }, - "china_aqi": { - "value": 27 - }, - "china_primary_pollutant": { - "value": "pm10" - }, - "pm25": { - "value": 5.3125, - "units": "\u00b5g/m3" - }, - "pm10": { - "value": 27, - "units": "\u00b5g/m3" - }, - "no2": { - "value": 14.1875, - "units": "ppb" - }, - "co": { - "value": 0.875, - "units": "ppm" - }, - "so2": { - "value": 2, - "units": "ppb" - }, - "epa_health_concern": { - "value": "Good" - }, - "china_health_concern": { - "value": "Good" - }, - "pollen_tree": { - "value": 0, - "units": "Climacell Pollen Index" - }, - "pollen_weed": { - "value": 0, - "units": "Climacell Pollen Index" - }, - "pollen_grass": { - "value": 0, - "units": "Climacell Pollen Index" - }, - "observation_time": { - "value": "2021-03-07T18:54:06.055Z" - } -} diff --git a/tests/components/climacell/test_config_flow.py b/tests/components/climacell/test_config_flow.py deleted file mode 100644 index 69bcf5f9819..00000000000 --- a/tests/components/climacell/test_config_flow.py +++ /dev/null @@ -1,46 +0,0 @@ -"""Test the ClimaCell config flow.""" -from homeassistant import data_entry_flow -from homeassistant.components.climacell.const import ( - CONF_TIMESTEP, - DEFAULT_TIMESTEP, - DOMAIN, -) -from homeassistant.config_entries import SOURCE_USER -from homeassistant.core import HomeAssistant - -from .const import API_V3_ENTRY_DATA - -from tests.common import MockConfigEntry - - -async def test_options_flow( - hass: HomeAssistant, climacell_config_entry_update: None -) -> None: - """Test options config flow for climacell.""" - entry = MockConfigEntry( - domain=DOMAIN, - data=API_V3_ENTRY_DATA, - source=SOURCE_USER, - unique_id="test", - version=1, - ) - entry.add_to_hass(hass) - - await hass.config_entries.async_setup(entry.entry_id) - - assert entry.options[CONF_TIMESTEP] == DEFAULT_TIMESTEP - assert CONF_TIMESTEP not in entry.data - - result = await hass.config_entries.options.async_init(entry.entry_id, data=None) - - 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={CONF_TIMESTEP: 1} - ) - - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert result["title"] == "" - assert result["data"][CONF_TIMESTEP] == 1 - assert entry.options[CONF_TIMESTEP] == 1 diff --git a/tests/components/climacell/test_const.py b/tests/components/climacell/test_const.py deleted file mode 100644 index d0354c6a107..00000000000 --- a/tests/components/climacell/test_const.py +++ /dev/null @@ -1,14 +0,0 @@ -"""Tests for ClimaCell const.""" -import pytest - -from homeassistant.components.climacell.const import ClimaCellSensorEntityDescription -from homeassistant.const import TEMP_FAHRENHEIT - - -async def test_post_init(): - """Test post initialization check for ClimaCellSensorEntityDescription.""" - - with pytest.raises(RuntimeError): - ClimaCellSensorEntityDescription( - key="a", name="b", unit_imperial=TEMP_FAHRENHEIT - ) diff --git a/tests/components/climacell/test_init.py b/tests/components/climacell/test_init.py deleted file mode 100644 index baddd46c19d..00000000000 --- a/tests/components/climacell/test_init.py +++ /dev/null @@ -1,106 +0,0 @@ -"""Tests for Climacell init.""" -from unittest.mock import patch - -import pytest - -from homeassistant.components.climacell.const import CONF_TIMESTEP, DOMAIN -from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN -from homeassistant.const import CONF_API_VERSION -from homeassistant.core import HomeAssistant - -from .const import API_V3_ENTRY_DATA - -from tests.common import MockConfigEntry - - -async def test_load_and_unload( - hass: HomeAssistant, - climacell_config_entry_update: pytest.fixture, -) -> None: - """Test loading and unloading entry.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - data=API_V3_ENTRY_DATA, - unique_id="test", - version=1, - ) - config_entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - assert len(hass.states.async_entity_ids(WEATHER_DOMAIN)) == 1 - - assert await hass.config_entries.async_remove(config_entry.entry_id) - await hass.async_block_till_done() - assert len(hass.states.async_entity_ids(WEATHER_DOMAIN)) == 0 - - -async def test_v3_load_and_unload( - hass: HomeAssistant, - climacell_config_entry_update: pytest.fixture, -) -> None: - """Test loading and unloading v3 entry.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - data={k: v for k, v in API_V3_ENTRY_DATA.items() if k != CONF_API_VERSION}, - unique_id="test", - version=1, - ) - config_entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - assert len(hass.states.async_entity_ids(WEATHER_DOMAIN)) == 1 - - assert await hass.config_entries.async_remove(config_entry.entry_id) - await hass.async_block_till_done() - assert len(hass.states.async_entity_ids(WEATHER_DOMAIN)) == 0 - - -async def test_v4_load_and_unload( - hass: HomeAssistant, - climacell_config_entry_update: pytest.fixture, -) -> None: - """Test loading and unloading v3 entry.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - data={ - CONF_API_VERSION: 4, - **{k: v for k, v in API_V3_ENTRY_DATA.items() if k != CONF_API_VERSION}, - }, - unique_id="test", - version=1, - ) - config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.tomorrowio.async_setup_entry", return_value=True - ): - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - assert len(hass.states.async_entity_ids(WEATHER_DOMAIN)) == 0 - - -@pytest.mark.parametrize( - "old_timestep, new_timestep", [(2, 1), (7, 5), (20, 15), (21, 30)] -) -async def test_migrate_timestep( - hass: HomeAssistant, - climacell_config_entry_update: pytest.fixture, - old_timestep: int, - new_timestep: int, -) -> None: - """Test migration to standardized timestep.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - data=API_V3_ENTRY_DATA, - options={CONF_TIMESTEP: old_timestep}, - unique_id="test", - version=1, - ) - config_entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - assert config_entry.version == 1 - assert ( - CONF_API_VERSION in config_entry.data - and config_entry.data[CONF_API_VERSION] == 3 - ) - assert config_entry.options[CONF_TIMESTEP] == new_timestep diff --git a/tests/components/climacell/test_sensor.py b/tests/components/climacell/test_sensor.py deleted file mode 100644 index 3412a5c35f0..00000000000 --- a/tests/components/climacell/test_sensor.py +++ /dev/null @@ -1,148 +0,0 @@ -"""Tests for Climacell sensor entities.""" -from __future__ import annotations - -from datetime import datetime -from typing import Any -from unittest.mock import patch - -import pytest - -from homeassistant.components.climacell.const import ATTRIBUTION, DOMAIN -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.const import ATTR_ATTRIBUTION -from homeassistant.core import HomeAssistant, State, callback -from homeassistant.helpers.entity_registry import async_get -from homeassistant.util import dt as dt_util - -from .const import API_V3_ENTRY_DATA - -from tests.common import MockConfigEntry - -CC_SENSOR_ENTITY_ID = "sensor.climacell_{}" - -O3 = "ozone" -CO = "carbon_monoxide" -NO2 = "nitrogen_dioxide" -SO2 = "sulfur_dioxide" -PM25 = "particulate_matter_2_5_mm" -PM10 = "particulate_matter_10_mm" -MEP_AQI = "china_mep_air_quality_index" -MEP_HEALTH_CONCERN = "china_mep_health_concern" -MEP_PRIMARY_POLLUTANT = "china_mep_primary_pollutant" -EPA_AQI = "us_epa_air_quality_index" -EPA_HEALTH_CONCERN = "us_epa_health_concern" -EPA_PRIMARY_POLLUTANT = "us_epa_primary_pollutant" -FIRE_INDEX = "fire_index" -GRASS_POLLEN = "grass_pollen_index" -WEED_POLLEN = "weed_pollen_index" -TREE_POLLEN = "tree_pollen_index" -FEELS_LIKE = "feels_like" -DEW_POINT = "dew_point" -PRESSURE_SURFACE_LEVEL = "pressure_surface_level" -SNOW_ACCUMULATION = "snow_accumulation" -ICE_ACCUMULATION = "ice_accumulation" -GHI = "global_horizontal_irradiance" -CLOUD_BASE = "cloud_base" -CLOUD_COVER = "cloud_cover" -CLOUD_CEILING = "cloud_ceiling" -WIND_GUST = "wind_gust" -PRECIPITATION_TYPE = "precipitation_type" - -V3_FIELDS = [ - O3, - CO, - NO2, - SO2, - PM25, - PM10, - MEP_AQI, - MEP_HEALTH_CONCERN, - MEP_PRIMARY_POLLUTANT, - EPA_AQI, - EPA_HEALTH_CONCERN, - EPA_PRIMARY_POLLUTANT, - FIRE_INDEX, - GRASS_POLLEN, - WEED_POLLEN, - TREE_POLLEN, -] - -V4_FIELDS = [ - *V3_FIELDS, - FEELS_LIKE, - DEW_POINT, - PRESSURE_SURFACE_LEVEL, - GHI, - CLOUD_BASE, - CLOUD_COVER, - CLOUD_CEILING, - WIND_GUST, - PRECIPITATION_TYPE, -] - - -@callback -def _enable_entity(hass: HomeAssistant, entity_name: str) -> None: - """Enable disabled entity.""" - ent_reg = async_get(hass) - entry = ent_reg.async_get(entity_name) - updated_entry = ent_reg.async_update_entity( - entry.entity_id, **{"disabled_by": None} - ) - assert updated_entry != entry - assert updated_entry.disabled is False - - -async def _setup( - hass: HomeAssistant, sensors: list[str], config: dict[str, Any] -) -> State: - """Set up entry and return entity state.""" - with patch( - "homeassistant.util.dt.utcnow", - return_value=datetime(2021, 3, 6, 23, 59, 59, tzinfo=dt_util.UTC), - ): - config_entry = MockConfigEntry( - domain=DOMAIN, - data=config, - unique_id="test", - version=1, - ) - config_entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - for entity_name in sensors: - _enable_entity(hass, CC_SENSOR_ENTITY_ID.format(entity_name)) - await hass.async_block_till_done() - assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == len(sensors) - - -def check_sensor_state(hass: HomeAssistant, entity_name: str, value: str): - """Check the state of a ClimaCell sensor.""" - state = hass.states.get(CC_SENSOR_ENTITY_ID.format(entity_name)) - assert state - assert state.state == value - assert state.attributes[ATTR_ATTRIBUTION] == ATTRIBUTION - - -async def test_v3_sensor( - hass: HomeAssistant, - climacell_config_entry_update: pytest.fixture, -) -> None: - """Test v3 sensor data.""" - await _setup(hass, V3_FIELDS, API_V3_ENTRY_DATA) - check_sensor_state(hass, O3, "52.625") - check_sensor_state(hass, CO, "0.875") - check_sensor_state(hass, NO2, "14.1875") - check_sensor_state(hass, SO2, "2") - check_sensor_state(hass, PM25, "5.3125") - check_sensor_state(hass, PM10, "27") - check_sensor_state(hass, MEP_AQI, "27") - check_sensor_state(hass, MEP_HEALTH_CONCERN, "Good") - check_sensor_state(hass, MEP_PRIMARY_POLLUTANT, "pm10") - check_sensor_state(hass, EPA_AQI, "22.3125") - check_sensor_state(hass, EPA_HEALTH_CONCERN, "Good") - check_sensor_state(hass, EPA_PRIMARY_POLLUTANT, "pm25") - check_sensor_state(hass, FIRE_INDEX, "9") - check_sensor_state(hass, GRASS_POLLEN, "minimal_to_none") - check_sensor_state(hass, WEED_POLLEN, "minimal_to_none") - check_sensor_state(hass, TREE_POLLEN, "minimal_to_none") diff --git a/tests/components/climacell/test_weather.py b/tests/components/climacell/test_weather.py deleted file mode 100644 index e3326f267d4..00000000000 --- a/tests/components/climacell/test_weather.py +++ /dev/null @@ -1,223 +0,0 @@ -"""Tests for Climacell weather entity.""" -from __future__ import annotations - -from datetime import datetime -from typing import Any -from unittest.mock import patch - -import pytest - -from homeassistant.components.climacell.const import ( - ATTR_CLOUD_COVER, - ATTR_PRECIPITATION_TYPE, - ATTR_WIND_GUST, - ATTRIBUTION, - DOMAIN, -) -from homeassistant.components.weather import ( - ATTR_CONDITION_CLOUDY, - ATTR_CONDITION_RAINY, - ATTR_CONDITION_SNOWY, - ATTR_CONDITION_SUNNY, - ATTR_FORECAST, - ATTR_FORECAST_CONDITION, - ATTR_FORECAST_PRECIPITATION, - ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, - ATTR_FORECAST_TIME, - ATTR_WEATHER_HUMIDITY, - ATTR_WEATHER_OZONE, - ATTR_WEATHER_PRESSURE, - ATTR_WEATHER_TEMPERATURE, - ATTR_WEATHER_VISIBILITY, - ATTR_WEATHER_WIND_BEARING, - ATTR_WEATHER_WIND_SPEED, - DOMAIN as WEATHER_DOMAIN, -) -from homeassistant.const import ATTR_ATTRIBUTION, ATTR_FRIENDLY_NAME -from homeassistant.core import HomeAssistant, State, callback -from homeassistant.helpers.entity_registry import async_get -from homeassistant.util import dt as dt_util - -from .const import API_V3_ENTRY_DATA - -from tests.common import MockConfigEntry - - -@callback -def _enable_entity(hass: HomeAssistant, entity_name: str) -> None: - """Enable disabled entity.""" - ent_reg = async_get(hass) - entry = ent_reg.async_get(entity_name) - updated_entry = ent_reg.async_update_entity( - entry.entity_id, **{"disabled_by": None} - ) - assert updated_entry != entry - assert updated_entry.disabled is False - - -async def _setup(hass: HomeAssistant, config: dict[str, Any]) -> State: - """Set up entry and return entity state.""" - with patch( - "homeassistant.util.dt.utcnow", - return_value=datetime(2021, 3, 6, 23, 59, 59, tzinfo=dt_util.UTC), - ): - config_entry = MockConfigEntry( - domain=DOMAIN, - data=config, - unique_id="test", - version=1, - ) - config_entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - for entity_name in ("hourly", "nowcast"): - _enable_entity(hass, f"weather.climacell_{entity_name}") - await hass.async_block_till_done() - assert len(hass.states.async_entity_ids(WEATHER_DOMAIN)) == 3 - - return hass.states.get("weather.climacell_daily") - - -async def test_v3_weather( - hass: HomeAssistant, - climacell_config_entry_update: pytest.fixture, -) -> None: - """Test v3 weather data.""" - weather_state = await _setup(hass, API_V3_ENTRY_DATA) - assert weather_state.state == ATTR_CONDITION_SUNNY - assert weather_state.attributes[ATTR_ATTRIBUTION] == ATTRIBUTION - assert weather_state.attributes[ATTR_FORECAST] == [ - { - ATTR_FORECAST_CONDITION: ATTR_CONDITION_SUNNY, - ATTR_FORECAST_TIME: "2021-03-07T00:00:00-08:00", - ATTR_FORECAST_PRECIPITATION: 0, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, - ATTR_FORECAST_TEMP: 7.2, - ATTR_FORECAST_TEMP_LOW: -4.7, - }, - { - ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, - ATTR_FORECAST_TIME: "2021-03-08T00:00:00-08:00", - ATTR_FORECAST_PRECIPITATION: 0, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, - ATTR_FORECAST_TEMP: 9.7, - ATTR_FORECAST_TEMP_LOW: -4.0, - }, - { - ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, - ATTR_FORECAST_TIME: "2021-03-09T00:00:00-08:00", - ATTR_FORECAST_PRECIPITATION: 0, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, - ATTR_FORECAST_TEMP: 19.4, - ATTR_FORECAST_TEMP_LOW: -0.3, - }, - { - ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, - ATTR_FORECAST_TIME: "2021-03-10T00:00:00-08:00", - ATTR_FORECAST_PRECIPITATION: 0, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, - ATTR_FORECAST_TEMP: 18.5, - ATTR_FORECAST_TEMP_LOW: 3.0, - }, - { - ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, - ATTR_FORECAST_TIME: "2021-03-11T00:00:00-08:00", - ATTR_FORECAST_PRECIPITATION: 0, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: 5, - ATTR_FORECAST_TEMP: 19.7, - ATTR_FORECAST_TEMP_LOW: 9.3, - }, - { - ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, - ATTR_FORECAST_TIME: "2021-03-12T00:00:00-08:00", - ATTR_FORECAST_PRECIPITATION: 0.05, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: 25, - ATTR_FORECAST_TEMP: 19.9, - ATTR_FORECAST_TEMP_LOW: 12.1, - }, - { - ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, - ATTR_FORECAST_TIME: "2021-03-13T00:00:00-08:00", - ATTR_FORECAST_PRECIPITATION: 0, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: 25, - ATTR_FORECAST_TEMP: 15.8, - ATTR_FORECAST_TEMP_LOW: 7.5, - }, - { - ATTR_FORECAST_CONDITION: ATTR_CONDITION_RAINY, - ATTR_FORECAST_TIME: "2021-03-14T00:00:00-08:00", - ATTR_FORECAST_PRECIPITATION: 1.07, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: 75, - ATTR_FORECAST_TEMP: 6.4, - ATTR_FORECAST_TEMP_LOW: 3.2, - }, - { - ATTR_FORECAST_CONDITION: ATTR_CONDITION_SNOWY, - ATTR_FORECAST_TIME: "2021-03-15T00:00:00-07:00", # DST starts - ATTR_FORECAST_PRECIPITATION: 7.31, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: 95, - ATTR_FORECAST_TEMP: 1.2, - ATTR_FORECAST_TEMP_LOW: 0.2, - }, - { - ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, - ATTR_FORECAST_TIME: "2021-03-16T00:00:00-07:00", - ATTR_FORECAST_PRECIPITATION: 0.01, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: 5, - ATTR_FORECAST_TEMP: 6.1, - ATTR_FORECAST_TEMP_LOW: -1.6, - }, - { - ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, - ATTR_FORECAST_TIME: "2021-03-17T00:00:00-07:00", - ATTR_FORECAST_PRECIPITATION: 0, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, - ATTR_FORECAST_TEMP: 11.3, - ATTR_FORECAST_TEMP_LOW: 1.3, - }, - { - ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, - ATTR_FORECAST_TIME: "2021-03-18T00:00:00-07:00", - ATTR_FORECAST_PRECIPITATION: 0, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: 5, - ATTR_FORECAST_TEMP: 12.3, - ATTR_FORECAST_TEMP_LOW: 5.6, - }, - { - ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, - ATTR_FORECAST_TIME: "2021-03-19T00:00:00-07:00", - ATTR_FORECAST_PRECIPITATION: 0.18, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: 45, - ATTR_FORECAST_TEMP: 9.4, - ATTR_FORECAST_TEMP_LOW: 4.7, - }, - { - ATTR_FORECAST_CONDITION: ATTR_CONDITION_RAINY, - ATTR_FORECAST_TIME: "2021-03-20T00:00:00-07:00", - ATTR_FORECAST_PRECIPITATION: 1.23, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: 55, - ATTR_FORECAST_TEMP: 5.0, - ATTR_FORECAST_TEMP_LOW: 3.1, - }, - { - ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, - ATTR_FORECAST_TIME: "2021-03-21T00:00:00-07:00", - ATTR_FORECAST_PRECIPITATION: 0.04, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: 20, - ATTR_FORECAST_TEMP: 6.8, - ATTR_FORECAST_TEMP_LOW: 0.9, - }, - ] - assert weather_state.attributes[ATTR_FRIENDLY_NAME] == "ClimaCell - Daily" - assert weather_state.attributes[ATTR_WEATHER_HUMIDITY] == 24 - assert weather_state.attributes[ATTR_WEATHER_OZONE] == 52.625 - assert weather_state.attributes[ATTR_WEATHER_PRESSURE] == 1028.12 - assert weather_state.attributes[ATTR_WEATHER_TEMPERATURE] == 6.6 - assert weather_state.attributes[ATTR_WEATHER_VISIBILITY] == 9.99 - assert weather_state.attributes[ATTR_WEATHER_WIND_BEARING] == 320.31 - assert weather_state.attributes[ATTR_WEATHER_WIND_SPEED] == 14.63 - assert weather_state.attributes[ATTR_CLOUD_COVER] == 100 - assert weather_state.attributes[ATTR_WIND_GUST] == 24.0758 - assert weather_state.attributes[ATTR_PRECIPITATION_TYPE] == "rain" diff --git a/tests/components/tomorrowio/conftest.py b/tests/components/tomorrowio/conftest.py index 9c1fa5baa06..2d36d68c57a 100644 --- a/tests/components/tomorrowio/conftest.py +++ b/tests/components/tomorrowio/conftest.py @@ -33,22 +33,3 @@ def tomorrowio_config_entry_update_fixture(): mock_max_requests_per_day.return_value = 100 mock_num_api_requests.return_value = 2 yield mock_update - - -@pytest.fixture(name="climacell_config_entry_update") -def climacell_config_entry_update_fixture(): - """Mock valid climacell config entry setup.""" - with patch( - "homeassistant.components.climacell.ClimaCellV3.realtime", - return_value={}, - ), patch( - "homeassistant.components.climacell.ClimaCellV3.forecast_hourly", - return_value={}, - ), patch( - "homeassistant.components.climacell.ClimaCellV3.forecast_daily", - return_value={}, - ), patch( - "homeassistant.components.climacell.ClimaCellV3.forecast_nowcast", - return_value={}, - ): - yield diff --git a/tests/components/tomorrowio/test_config_flow.py b/tests/components/tomorrowio/test_config_flow.py index 245dabffcc4..301b9bef554 100644 --- a/tests/components/tomorrowio/test_config_flow.py +++ b/tests/components/tomorrowio/test_config_flow.py @@ -9,7 +9,6 @@ from pytomorrowio.exceptions import ( ) from homeassistant import data_entry_flow -from homeassistant.components.climacell import DOMAIN as CC_DOMAIN from homeassistant.components.tomorrowio.config_flow import ( _get_config_schema, _get_unique_id, @@ -20,10 +19,9 @@ from homeassistant.components.tomorrowio.const import ( DEFAULT_TIMESTEP, DOMAIN, ) -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER, ConfigEntryState +from homeassistant.config_entries import SOURCE_USER from homeassistant.const import ( CONF_API_KEY, - CONF_API_VERSION, CONF_LATITUDE, CONF_LOCATION, CONF_LONGITUDE, @@ -36,7 +34,6 @@ from homeassistant.setup import async_setup_component from .const import API_KEY, MIN_CONFIG from tests.common import MockConfigEntry -from tests.components.climacell.const import API_V3_ENTRY_DATA async def test_user_flow_minimum_fields(hass: HomeAssistant) -> None: @@ -210,73 +207,3 @@ async def test_options_flow(hass: HomeAssistant) -> None: assert result["title"] == "" assert result["data"][CONF_TIMESTEP] == 1 assert entry.options[CONF_TIMESTEP] == 1 - - -async def test_import_flow_v4(hass: HomeAssistant) -> None: - """Test import flow for climacell v4 config entry.""" - user_config = API_V3_ENTRY_DATA.copy() - user_config[CONF_API_VERSION] = 4 - old_entry = MockConfigEntry( - domain=CC_DOMAIN, - data=user_config, - source=SOURCE_USER, - unique_id="test", - version=1, - ) - old_entry.add_to_hass(hass) - await hass.config_entries.async_setup(old_entry.entry_id) - await hass.async_block_till_done() - assert old_entry.state != ConfigEntryState.LOADED - - assert len(hass.config_entries.async_entries(CC_DOMAIN)) == 0 - assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - entry = hass.config_entries.async_entries(DOMAIN)[0] - assert "old_config_entry_id" not in entry.data - assert CONF_API_VERSION not in entry.data - - -async def test_import_flow_v3( - hass: HomeAssistant, climacell_config_entry_update -) -> None: - """Test import flow for climacell v3 config entry.""" - user_config = API_V3_ENTRY_DATA - old_entry = MockConfigEntry( - domain=CC_DOMAIN, - data=user_config, - source=SOURCE_USER, - unique_id="test", - version=1, - ) - old_entry.add_to_hass(hass) - await hass.config_entries.async_setup(old_entry.entry_id) - assert old_entry.state == ConfigEntryState.LOADED - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT, "old_config_entry_id": old_entry.entry_id}, - data=old_entry.data, - ) - - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "user" - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_API_KEY: "this is a test"} - ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert result["data"] == { - CONF_API_KEY: "this is a test", - CONF_LOCATION: { - CONF_LATITUDE: 80.0, - CONF_LONGITUDE: 80.0, - }, - CONF_NAME: "ClimaCell", - "old_config_entry_id": old_entry.entry_id, - } - - await hass.async_block_till_done() - - assert len(hass.config_entries.async_entries(CC_DOMAIN)) == 0 - assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - entry = hass.config_entries.async_entries(DOMAIN)[0] - assert "old_config_entry_id" not in entry.data - assert CONF_API_VERSION not in entry.data diff --git a/tests/components/tomorrowio/test_init.py b/tests/components/tomorrowio/test_init.py index 411675427b1..5fd954859b1 100644 --- a/tests/components/tomorrowio/test_init.py +++ b/tests/components/tomorrowio/test_init.py @@ -2,30 +2,20 @@ from datetime import timedelta from unittest.mock import patch -from homeassistant.components.climacell import CONF_TIMESTEP, DOMAIN as CC_DOMAIN from homeassistant.components.tomorrowio.config_flow import ( _get_config_schema, _get_unique_id, ) -from homeassistant.components.tomorrowio.const import DOMAIN +from homeassistant.components.tomorrowio.const import CONF_TIMESTEP, DOMAIN from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER -from homeassistant.const import ( - CONF_API_KEY, - CONF_API_VERSION, - CONF_LATITUDE, - CONF_LOCATION, - CONF_LONGITUDE, - CONF_NAME, -) +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_API_KEY, CONF_NAME from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.util import dt as dt_util from .const import MIN_CONFIG from tests.common import MockConfigEntry, async_fire_time_changed -from tests.components.climacell.const import API_V3_ENTRY_DATA NEW_NAME = "New Name" @@ -126,118 +116,3 @@ async def test_update_intervals( assert len(tomorrowio_config_entry_update.call_args_list) == 2 tomorrowio_config_entry_update.reset_mock() - - -async def test_climacell_migration_logic( - hass: HomeAssistant, climacell_config_entry_update -) -> None: - """Test that climacell config entry is properly migrated.""" - old_data = API_V3_ENTRY_DATA.copy() - old_data[CONF_API_KEY] = "v3apikey" - old_config_entry = MockConfigEntry( - domain=CC_DOMAIN, - data=old_data, - unique_id="v3apikey_80.0_80.0", - version=1, - ) - old_config_entry.add_to_hass(hass) - # Let's create a device and update its name - dev_reg = dr.async_get(hass) - old_device = dev_reg.async_get_or_create( - config_entry_id=old_config_entry.entry_id, - identifiers={(CC_DOMAIN, old_data[CONF_API_KEY])}, - manufacturer="ClimaCell", - sw_version="v4", - entry_type="service", - name="ClimaCell", - ) - dev_reg.async_update_device(old_device.id, name_by_user=NEW_NAME) - # Now let's create some entity and update some things to see if everything migrates - # over - ent_reg = er.async_get(hass) - old_entity_daily = ent_reg.async_get_or_create( - "weather", - CC_DOMAIN, - "v3apikey_80.0_80.0_daily", - config_entry=old_config_entry, - original_name="ClimaCell - Daily", - suggested_object_id="climacell_daily", - device_id=old_device.id, - ) - old_entity_hourly = ent_reg.async_get_or_create( - "weather", - CC_DOMAIN, - "v3apikey_80.0_80.0_hourly", - config_entry=old_config_entry, - original_name="ClimaCell - Hourly", - suggested_object_id="climacell_hourly", - device_id=old_device.id, - disabled_by=er.RegistryEntryDisabler.USER, - ) - old_entity_nowcast = ent_reg.async_get_or_create( - "weather", - CC_DOMAIN, - "v3apikey_80.0_80.0_nowcast", - config_entry=old_config_entry, - original_name="ClimaCell - Nowcast", - suggested_object_id="climacell_nowcast", - device_id=old_device.id, - ) - ent_reg.async_update_entity(old_entity_daily.entity_id, name=NEW_NAME) - - # Now let's create a new tomorrowio config entry that is supposedly created from a - # climacell import and see what happens - we are also changing the API key to ensure - # that things work as expected - new_data = API_V3_ENTRY_DATA.copy() - new_data[CONF_LOCATION] = { - CONF_LATITUDE: float(new_data.pop(CONF_LATITUDE)), - CONF_LONGITUDE: float(new_data.pop(CONF_LONGITUDE)), - } - new_data[CONF_API_VERSION] = 4 - new_data["old_config_entry_id"] = old_config_entry.entry_id - config_entry = MockConfigEntry( - domain=DOMAIN, - data=new_data, - unique_id=_get_unique_id(hass, new_data), - version=1, - source=SOURCE_IMPORT, - ) - config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - # Check that the old device no longer exists - assert dev_reg.async_get(old_device.id) is None - - # Check that the new device was created and that it has the correct name - assert ( - dr.async_entries_for_config_entry(dev_reg, config_entry.entry_id)[ - 0 - ].name_by_user - == NEW_NAME - ) - - # Check that the new entities match the old ones (minus the default name) - new_entity_daily = ent_reg.async_get(old_entity_daily.entity_id) - assert new_entity_daily.platform == DOMAIN - assert new_entity_daily.name == NEW_NAME - assert new_entity_daily.original_name == "ClimaCell - Daily" - assert new_entity_daily.device_id != old_device.id - assert new_entity_daily.unique_id == f"{_get_unique_id(hass, new_data)}_daily" - assert new_entity_daily.disabled_by is None - - new_entity_hourly = ent_reg.async_get(old_entity_hourly.entity_id) - assert new_entity_hourly.platform == DOMAIN - assert new_entity_hourly.name is None - assert new_entity_hourly.original_name == "ClimaCell - Hourly" - assert new_entity_hourly.device_id != old_device.id - assert new_entity_hourly.unique_id == f"{_get_unique_id(hass, new_data)}_hourly" - assert new_entity_hourly.disabled_by == er.RegistryEntryDisabler.USER - - new_entity_nowcast = ent_reg.async_get(old_entity_nowcast.entity_id) - assert new_entity_nowcast.platform == DOMAIN - assert new_entity_nowcast.name is None - assert new_entity_nowcast.original_name == "ClimaCell - Nowcast" - assert new_entity_nowcast.device_id != old_device.id - assert new_entity_nowcast.unique_id == f"{_get_unique_id(hass, new_data)}_nowcast" - assert new_entity_nowcast.disabled_by is None From 4200778eafa30efb2a5140256b24899861178e30 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 23 Sep 2022 14:23:59 +0200 Subject: [PATCH 665/955] Move distance and speed util to unit_conversion (#78967) --- homeassistant/util/distance.py | 54 +--------- homeassistant/util/speed.py | 62 +++-------- homeassistant/util/unit_conversion.py | 90 +++++++++++++++- tests/util/test_unit_conversion.py | 147 ++++++++++++++++++++++++++ 4 files changed, 252 insertions(+), 101 deletions(-) diff --git a/homeassistant/util/distance.py b/homeassistant/util/distance.py index 4a7445c46ed..7eaa1d23fd9 100644 --- a/homeassistant/util/distance.py +++ b/homeassistant/util/distance.py @@ -1,9 +1,7 @@ """Distance util functions.""" from __future__ import annotations -from numbers import Number - -from homeassistant.const import ( +from homeassistant.const import ( # pylint: disable=unused-import # noqa: F401 LENGTH, LENGTH_CENTIMETERS, LENGTH_FEET, @@ -16,53 +14,11 @@ from homeassistant.const import ( UNIT_NOT_RECOGNIZED_TEMPLATE, ) -VALID_UNITS: tuple[str, ...] = ( - LENGTH_KILOMETERS, - LENGTH_MILES, - LENGTH_FEET, - LENGTH_METERS, - LENGTH_CENTIMETERS, - LENGTH_MILLIMETERS, - LENGTH_INCHES, - LENGTH_YARD, -) +from .unit_conversion import DistanceConverter -MM_TO_M = 0.001 # 1 mm = 0.001 m -CM_TO_M = 0.01 # 1 cm = 0.01 m -KM_TO_M = 1000 # 1 km = 1000 m - -IN_TO_M = 0.0254 # 1 inch = 0.0254 m -FOOT_TO_M = IN_TO_M * 12 # 12 inches = 1 foot (0.3048 m) -YARD_TO_M = FOOT_TO_M * 3 # 3 feet = 1 yard (0.9144 m) -MILE_TO_M = YARD_TO_M * 1760 # 1760 yard = 1 mile (1609.344 m) - -NAUTICAL_MILE_TO_M = 1852 # 1 nautical mile = 1852 m - -UNIT_CONVERSION: dict[str, float] = { - LENGTH_METERS: 1, - LENGTH_MILLIMETERS: 1 / MM_TO_M, - LENGTH_CENTIMETERS: 1 / CM_TO_M, - LENGTH_KILOMETERS: 1 / KM_TO_M, - LENGTH_INCHES: 1 / IN_TO_M, - LENGTH_FEET: 1 / FOOT_TO_M, - LENGTH_YARD: 1 / YARD_TO_M, - LENGTH_MILES: 1 / MILE_TO_M, -} +VALID_UNITS = DistanceConverter.VALID_UNITS -def convert(value: float, unit_1: str, unit_2: str) -> float: +def convert(value: float, from_unit: str, to_unit: str) -> float: """Convert one unit of measurement to another.""" - if unit_1 not in VALID_UNITS: - raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(unit_1, LENGTH)) - if unit_2 not in VALID_UNITS: - raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(unit_2, LENGTH)) - - if not isinstance(value, Number): - raise TypeError(f"{value} is not of numeric type") - - if unit_1 == unit_2 or unit_1 not in VALID_UNITS: - return value - - meters: float = value / UNIT_CONVERSION[unit_1] - - return meters * UNIT_CONVERSION[unit_2] + return DistanceConverter.convert(value, from_unit, to_unit) diff --git a/homeassistant/util/speed.py b/homeassistant/util/speed.py index 823c65b59b0..31993549586 100644 --- a/homeassistant/util/speed.py +++ b/homeassistant/util/speed.py @@ -1,9 +1,7 @@ """Distance util functions.""" from __future__ import annotations -from numbers import Number - -from homeassistant.const import ( +from homeassistant.const import ( # pylint: disable=unused-import # noqa: F401 SPEED, SPEED_FEET_PER_SECOND, SPEED_INCHES_PER_DAY, @@ -16,54 +14,20 @@ from homeassistant.const import ( UNIT_NOT_RECOGNIZED_TEMPLATE, ) -from .distance import ( - FOOT_TO_M, - IN_TO_M, - KM_TO_M, - MILE_TO_M, - MM_TO_M, - NAUTICAL_MILE_TO_M, +from .unit_conversion import ( # pylint: disable=unused-import # noqa: F401 + _FOOT_TO_M as FOOT_TO_M, + _HRS_TO_SECS as HRS_TO_SECS, + _IN_TO_M as IN_TO_M, + _KM_TO_M as KM_TO_M, + _MILE_TO_M as MILE_TO_M, + _NAUTICAL_MILE_TO_M as NAUTICAL_MILE_TO_M, + SpeedConverter, ) -VALID_UNITS: tuple[str, ...] = ( - SPEED_FEET_PER_SECOND, - SPEED_INCHES_PER_DAY, - SPEED_INCHES_PER_HOUR, - SPEED_KILOMETERS_PER_HOUR, - SPEED_KNOTS, - SPEED_METERS_PER_SECOND, - SPEED_MILES_PER_HOUR, - SPEED_MILLIMETERS_PER_DAY, -) - -HRS_TO_SECS = 60 * 60 # 1 hr = 3600 seconds -DAYS_TO_SECS = 24 * HRS_TO_SECS # 1 day = 24 hours = 86400 seconds - -# Units in terms of m/s -UNIT_CONVERSION: dict[str, float] = { - SPEED_FEET_PER_SECOND: 1 / FOOT_TO_M, - SPEED_INCHES_PER_DAY: DAYS_TO_SECS / IN_TO_M, - SPEED_INCHES_PER_HOUR: HRS_TO_SECS / IN_TO_M, - SPEED_KILOMETERS_PER_HOUR: HRS_TO_SECS / KM_TO_M, - SPEED_KNOTS: HRS_TO_SECS / NAUTICAL_MILE_TO_M, - SPEED_METERS_PER_SECOND: 1, - SPEED_MILES_PER_HOUR: HRS_TO_SECS / MILE_TO_M, - SPEED_MILLIMETERS_PER_DAY: DAYS_TO_SECS / MM_TO_M, -} +UNIT_CONVERSION = SpeedConverter.UNIT_CONVERSION +VALID_UNITS = SpeedConverter.VALID_UNITS -def convert(value: float, unit_1: str, unit_2: str) -> float: +def convert(value: float, from_unit: str, to_unit: str) -> float: """Convert one unit of measurement to another.""" - if unit_1 not in VALID_UNITS: - raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(unit_1, SPEED)) - if unit_2 not in VALID_UNITS: - raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(unit_2, SPEED)) - - if not isinstance(value, Number): - raise TypeError(f"{value} is not of numeric type") - - if unit_1 == unit_2: - return value - - meters_per_second = value / UNIT_CONVERSION[unit_1] - return meters_per_second * UNIT_CONVERSION[unit_2] + return SpeedConverter.convert(value, from_unit, to_unit) diff --git a/homeassistant/util/unit_conversion.py b/homeassistant/util/unit_conversion.py index fbc5c05b706..363c1bf5f3c 100644 --- a/homeassistant/util/unit_conversion.py +++ b/homeassistant/util/unit_conversion.py @@ -8,6 +8,14 @@ from homeassistant.const import ( ENERGY_KILO_WATT_HOUR, ENERGY_MEGA_WATT_HOUR, ENERGY_WATT_HOUR, + LENGTH_CENTIMETERS, + LENGTH_FEET, + LENGTH_INCHES, + LENGTH_KILOMETERS, + LENGTH_METERS, + LENGTH_MILES, + LENGTH_MILLIMETERS, + LENGTH_YARD, POWER_KILO_WATT, POWER_WATT, PRESSURE_BAR, @@ -19,6 +27,14 @@ from homeassistant.const import ( PRESSURE_MMHG, PRESSURE_PA, PRESSURE_PSI, + SPEED_FEET_PER_SECOND, + SPEED_INCHES_PER_DAY, + SPEED_INCHES_PER_HOUR, + SPEED_KILOMETERS_PER_HOUR, + SPEED_KNOTS, + SPEED_METERS_PER_SECOND, + SPEED_MILES_PER_HOUR, + SPEED_MILLIMETERS_PER_DAY, TEMP_CELSIUS, TEMP_FAHRENHEIT, TEMP_KELVIN, @@ -31,14 +47,28 @@ from homeassistant.const import ( VOLUME_MILLILITERS, ) -from .distance import FOOT_TO_M, IN_TO_M +# Distance conversion constants +_MM_TO_M = 0.001 # 1 mm = 0.001 m +_CM_TO_M = 0.01 # 1 cm = 0.01 m +_KM_TO_M = 1000 # 1 km = 1000 m + +_IN_TO_M = 0.0254 # 1 inch = 0.0254 m +_FOOT_TO_M = _IN_TO_M * 12 # 12 inches = 1 foot (0.3048 m) +_YARD_TO_M = _FOOT_TO_M * 3 # 3 feet = 1 yard (0.9144 m) +_MILE_TO_M = _YARD_TO_M * 1760 # 1760 yard = 1 mile (1609.344 m) + +_NAUTICAL_MILE_TO_M = 1852 # 1 nautical mile = 1852 m + +# Duration conversion constants +_HRS_TO_SECS = 60 * 60 # 1 hr = 3600 seconds +_DAYS_TO_SECS = 24 * _HRS_TO_SECS # 1 day = 24 hours = 86400 seconds # Volume conversion constants _L_TO_CUBIC_METER = 0.001 # 1 L = 0.001 m³ _ML_TO_CUBIC_METER = 0.001 * _L_TO_CUBIC_METER # 1 mL = 0.001 L -_GALLON_TO_CUBIC_METER = 231 * pow(IN_TO_M, 3) # US gallon is 231 cubic inches +_GALLON_TO_CUBIC_METER = 231 * pow(_IN_TO_M, 3) # US gallon is 231 cubic inches _FLUID_OUNCE_TO_CUBIC_METER = _GALLON_TO_CUBIC_METER / 128 # 128 fl. oz. in a US gallon -_CUBIC_FOOT_TO_CUBIC_METER = pow(FOOT_TO_M, 3) +_CUBIC_FOOT_TO_CUBIC_METER = pow(_FOOT_TO_M, 3) class BaseUnitConverter: @@ -86,6 +116,33 @@ class BaseUnitConverterWithUnitConversion(BaseUnitConverter): return new_value * cls.UNIT_CONVERSION[to_unit] +class DistanceConverter(BaseUnitConverterWithUnitConversion): + """Utility to convert distance values.""" + + UNIT_CLASS = "distance" + NORMALIZED_UNIT = LENGTH_METERS + UNIT_CONVERSION: dict[str, float] = { + LENGTH_METERS: 1, + LENGTH_MILLIMETERS: 1 / _MM_TO_M, + LENGTH_CENTIMETERS: 1 / _CM_TO_M, + LENGTH_KILOMETERS: 1 / _KM_TO_M, + LENGTH_INCHES: 1 / _IN_TO_M, + LENGTH_FEET: 1 / _FOOT_TO_M, + LENGTH_YARD: 1 / _YARD_TO_M, + LENGTH_MILES: 1 / _MILE_TO_M, + } + VALID_UNITS: tuple[str, ...] = ( + LENGTH_KILOMETERS, + LENGTH_MILES, + LENGTH_FEET, + LENGTH_METERS, + LENGTH_CENTIMETERS, + LENGTH_MILLIMETERS, + LENGTH_INCHES, + LENGTH_YARD, + ) + + class EnergyConverter(BaseUnitConverterWithUnitConversion): """Utility to convert energy values.""" @@ -147,6 +204,33 @@ class PressureConverter(BaseUnitConverterWithUnitConversion): ) +class SpeedConverter(BaseUnitConverterWithUnitConversion): + """Utility to convert speed values.""" + + UNIT_CLASS = "speed" + NORMALIZED_UNIT = SPEED_METERS_PER_SECOND + UNIT_CONVERSION: dict[str, float] = { + SPEED_FEET_PER_SECOND: 1 / _FOOT_TO_M, + SPEED_INCHES_PER_DAY: _DAYS_TO_SECS / _IN_TO_M, + SPEED_INCHES_PER_HOUR: _HRS_TO_SECS / _IN_TO_M, + SPEED_KILOMETERS_PER_HOUR: _HRS_TO_SECS / _KM_TO_M, + SPEED_KNOTS: _HRS_TO_SECS / _NAUTICAL_MILE_TO_M, + SPEED_METERS_PER_SECOND: 1, + SPEED_MILES_PER_HOUR: _HRS_TO_SECS / _MILE_TO_M, + SPEED_MILLIMETERS_PER_DAY: _DAYS_TO_SECS / _MM_TO_M, + } + VALID_UNITS: tuple[str, ...] = ( + SPEED_FEET_PER_SECOND, + SPEED_INCHES_PER_DAY, + SPEED_INCHES_PER_HOUR, + SPEED_KILOMETERS_PER_HOUR, + SPEED_KNOTS, + SPEED_METERS_PER_SECOND, + SPEED_MILES_PER_HOUR, + SPEED_MILLIMETERS_PER_DAY, + ) + + class TemperatureConverter(BaseUnitConverter): """Utility to convert temperature values.""" diff --git a/tests/util/test_unit_conversion.py b/tests/util/test_unit_conversion.py index 3f06393be3e..fb31624d255 100644 --- a/tests/util/test_unit_conversion.py +++ b/tests/util/test_unit_conversion.py @@ -5,6 +5,14 @@ from homeassistant.const import ( ENERGY_KILO_WATT_HOUR, ENERGY_MEGA_WATT_HOUR, ENERGY_WATT_HOUR, + LENGTH_CENTIMETERS, + LENGTH_FEET, + LENGTH_INCHES, + LENGTH_KILOMETERS, + LENGTH_METERS, + LENGTH_MILES, + LENGTH_MILLIMETERS, + LENGTH_YARD, POWER_KILO_WATT, POWER_WATT, PRESSURE_CBAR, @@ -15,6 +23,14 @@ from homeassistant.const import ( PRESSURE_MMHG, PRESSURE_PA, PRESSURE_PSI, + SPEED_FEET_PER_SECOND, + SPEED_INCHES_PER_DAY, + SPEED_INCHES_PER_HOUR, + SPEED_KILOMETERS_PER_HOUR, + SPEED_KNOTS, + SPEED_METERS_PER_SECOND, + SPEED_MILES_PER_HOUR, + SPEED_MILLIMETERS_PER_DAY, TEMP_CELSIUS, TEMP_FAHRENHEIT, TEMP_KELVIN, @@ -27,9 +43,11 @@ from homeassistant.const import ( ) from homeassistant.util.unit_conversion import ( BaseUnitConverter, + DistanceConverter, EnergyConverter, PowerConverter, PressureConverter, + SpeedConverter, TemperatureConverter, VolumeConverter, ) @@ -40,6 +58,14 @@ INVALID_SYMBOL = "bob" @pytest.mark.parametrize( "converter,valid_unit", [ + (DistanceConverter, LENGTH_KILOMETERS), + (DistanceConverter, LENGTH_METERS), + (DistanceConverter, LENGTH_CENTIMETERS), + (DistanceConverter, LENGTH_MILLIMETERS), + (DistanceConverter, LENGTH_MILES), + (DistanceConverter, LENGTH_YARD), + (DistanceConverter, LENGTH_FEET), + (DistanceConverter, LENGTH_INCHES), (EnergyConverter, ENERGY_WATT_HOUR), (EnergyConverter, ENERGY_KILO_WATT_HOUR), (EnergyConverter, ENERGY_MEGA_WATT_HOUR), @@ -53,6 +79,14 @@ INVALID_SYMBOL = "bob" (PressureConverter, PRESSURE_CBAR), (PressureConverter, PRESSURE_MMHG), (PressureConverter, PRESSURE_PSI), + (SpeedConverter, SPEED_FEET_PER_SECOND), + (SpeedConverter, SPEED_INCHES_PER_DAY), + (SpeedConverter, SPEED_INCHES_PER_HOUR), + (SpeedConverter, SPEED_KILOMETERS_PER_HOUR), + (SpeedConverter, SPEED_KNOTS), + (SpeedConverter, SPEED_METERS_PER_SECOND), + (SpeedConverter, SPEED_MILES_PER_HOUR), + (SpeedConverter, SPEED_MILLIMETERS_PER_DAY), (TemperatureConverter, TEMP_CELSIUS), (TemperatureConverter, TEMP_FAHRENHEIT), (TemperatureConverter, TEMP_KELVIN), @@ -70,9 +104,11 @@ def test_convert_same_unit(converter: type[BaseUnitConverter], valid_unit: str) @pytest.mark.parametrize( "converter,valid_unit", [ + (DistanceConverter, LENGTH_KILOMETERS), (EnergyConverter, ENERGY_KILO_WATT_HOUR), (PowerConverter, POWER_WATT), (PressureConverter, PRESSURE_PA), + (SpeedConverter, SPEED_KILOMETERS_PER_HOUR), (TemperatureConverter, TEMP_CELSIUS), (VolumeConverter, VOLUME_LITERS), ], @@ -91,9 +127,11 @@ def test_convert_invalid_unit( @pytest.mark.parametrize( "converter,from_unit,to_unit", [ + (DistanceConverter, LENGTH_KILOMETERS, LENGTH_METERS), (EnergyConverter, ENERGY_WATT_HOUR, ENERGY_KILO_WATT_HOUR), (PowerConverter, POWER_WATT, POWER_KILO_WATT), (PressureConverter, PRESSURE_HPA, PRESSURE_INHG), + (SpeedConverter, SPEED_KILOMETERS_PER_HOUR, SPEED_MILES_PER_HOUR), (TemperatureConverter, TEMP_CELSIUS, TEMP_FAHRENHEIT), (VolumeConverter, VOLUME_GALLONS, VOLUME_LITERS), ], @@ -106,6 +144,77 @@ def test_convert_nonnumeric_value( converter.convert("a", from_unit, to_unit) +@pytest.mark.parametrize( + "value,from_unit,expected,to_unit", + [ + (5, LENGTH_MILES, pytest.approx(8.04672), LENGTH_KILOMETERS), + (5, LENGTH_MILES, pytest.approx(8046.72), LENGTH_METERS), + (5, LENGTH_MILES, pytest.approx(804672.0), LENGTH_CENTIMETERS), + (5, LENGTH_MILES, pytest.approx(8046720.0), LENGTH_MILLIMETERS), + (5, LENGTH_MILES, pytest.approx(8800.0), LENGTH_YARD), + (5, LENGTH_MILES, pytest.approx(26400.0008448), LENGTH_FEET), + (5, LENGTH_MILES, pytest.approx(316800.171072), LENGTH_INCHES), + (5, LENGTH_YARD, pytest.approx(0.0045720000000000005), LENGTH_KILOMETERS), + (5, LENGTH_YARD, pytest.approx(4.572), LENGTH_METERS), + (5, LENGTH_YARD, pytest.approx(457.2), LENGTH_CENTIMETERS), + (5, LENGTH_YARD, pytest.approx(4572), LENGTH_MILLIMETERS), + (5, LENGTH_YARD, pytest.approx(0.002840908212), LENGTH_MILES), + (5, LENGTH_YARD, pytest.approx(15.00000048), LENGTH_FEET), + (5, LENGTH_YARD, pytest.approx(180.0000972), LENGTH_INCHES), + (5000, LENGTH_FEET, pytest.approx(1.524), LENGTH_KILOMETERS), + (5000, LENGTH_FEET, pytest.approx(1524), LENGTH_METERS), + (5000, LENGTH_FEET, pytest.approx(152400.0), LENGTH_CENTIMETERS), + (5000, LENGTH_FEET, pytest.approx(1524000.0), LENGTH_MILLIMETERS), + (5000, LENGTH_FEET, pytest.approx(0.9469694040000001), LENGTH_MILES), + (5000, LENGTH_FEET, pytest.approx(1666.66667), LENGTH_YARD), + (5000, LENGTH_FEET, pytest.approx(60000.032400000004), LENGTH_INCHES), + (5000, LENGTH_INCHES, pytest.approx(0.127), LENGTH_KILOMETERS), + (5000, LENGTH_INCHES, pytest.approx(127.0), LENGTH_METERS), + (5000, LENGTH_INCHES, pytest.approx(12700.0), LENGTH_CENTIMETERS), + (5000, LENGTH_INCHES, pytest.approx(127000.0), LENGTH_MILLIMETERS), + (5000, LENGTH_INCHES, pytest.approx(0.078914117), LENGTH_MILES), + (5000, LENGTH_INCHES, pytest.approx(138.88889), LENGTH_YARD), + (5000, LENGTH_INCHES, pytest.approx(416.66668), LENGTH_FEET), + (5, LENGTH_KILOMETERS, pytest.approx(5000), LENGTH_METERS), + (5, LENGTH_KILOMETERS, pytest.approx(500000), LENGTH_CENTIMETERS), + (5, LENGTH_KILOMETERS, pytest.approx(5000000), LENGTH_MILLIMETERS), + (5, LENGTH_KILOMETERS, pytest.approx(3.106855), LENGTH_MILES), + (5, LENGTH_KILOMETERS, pytest.approx(5468.066), LENGTH_YARD), + (5, LENGTH_KILOMETERS, pytest.approx(16404.2), LENGTH_FEET), + (5, LENGTH_KILOMETERS, pytest.approx(196850.5), LENGTH_INCHES), + (5000, LENGTH_METERS, pytest.approx(5), LENGTH_KILOMETERS), + (5000, LENGTH_METERS, pytest.approx(500000), LENGTH_CENTIMETERS), + (5000, LENGTH_METERS, pytest.approx(5000000), LENGTH_MILLIMETERS), + (5000, LENGTH_METERS, pytest.approx(3.106855), LENGTH_MILES), + (5000, LENGTH_METERS, pytest.approx(5468.066), LENGTH_YARD), + (5000, LENGTH_METERS, pytest.approx(16404.2), LENGTH_FEET), + (5000, LENGTH_METERS, pytest.approx(196850.5), LENGTH_INCHES), + (500000, LENGTH_CENTIMETERS, pytest.approx(5), LENGTH_KILOMETERS), + (500000, LENGTH_CENTIMETERS, pytest.approx(5000), LENGTH_METERS), + (500000, LENGTH_CENTIMETERS, pytest.approx(5000000), LENGTH_MILLIMETERS), + (500000, LENGTH_CENTIMETERS, pytest.approx(3.106855), LENGTH_MILES), + (500000, LENGTH_CENTIMETERS, pytest.approx(5468.066), LENGTH_YARD), + (500000, LENGTH_CENTIMETERS, pytest.approx(16404.2), LENGTH_FEET), + (500000, LENGTH_CENTIMETERS, pytest.approx(196850.5), LENGTH_INCHES), + (5000000, LENGTH_MILLIMETERS, pytest.approx(5), LENGTH_KILOMETERS), + (5000000, LENGTH_MILLIMETERS, pytest.approx(5000), LENGTH_METERS), + (5000000, LENGTH_MILLIMETERS, pytest.approx(500000), LENGTH_CENTIMETERS), + (5000000, LENGTH_MILLIMETERS, pytest.approx(3.106855), LENGTH_MILES), + (5000000, LENGTH_MILLIMETERS, pytest.approx(5468.066), LENGTH_YARD), + (5000000, LENGTH_MILLIMETERS, pytest.approx(16404.2), LENGTH_FEET), + (5000000, LENGTH_MILLIMETERS, pytest.approx(196850.5), LENGTH_INCHES), + ], +) +def test_distance_convert( + value: float, + from_unit: str, + expected: float, + to_unit: str, +) -> None: + """Test conversion to other units.""" + assert DistanceConverter.convert(value, from_unit, to_unit) == expected + + @pytest.mark.parametrize( "value,from_unit,expected,to_unit", [ @@ -185,6 +294,44 @@ def test_pressure_convert( assert PressureConverter.convert(value, from_unit, to_unit) == expected +@pytest.mark.parametrize( + "value,from_unit,expected,to_unit", + [ + # 5 km/h / 1.609 km/mi = 3.10686 mi/h + (5, SPEED_KILOMETERS_PER_HOUR, pytest.approx(3.106856), SPEED_MILES_PER_HOUR), + # 5 mi/h * 1.609 km/mi = 8.04672 km/h + (5, SPEED_MILES_PER_HOUR, 8.04672, SPEED_KILOMETERS_PER_HOUR), + # 5 in/day * 25.4 mm/in = 127 mm/day + (5, SPEED_INCHES_PER_DAY, 127, SPEED_MILLIMETERS_PER_DAY), + # 5 mm/day / 25.4 mm/in = 0.19685 in/day + (5, SPEED_MILLIMETERS_PER_DAY, pytest.approx(0.1968504), SPEED_INCHES_PER_DAY), + # 5 in/hr * 24 hr/day = 3048 mm/day + (5, SPEED_INCHES_PER_HOUR, 3048, SPEED_MILLIMETERS_PER_DAY), + # 5 m/s * 39.3701 in/m * 3600 s/hr = 708661 + (5, SPEED_METERS_PER_SECOND, pytest.approx(708661.42), SPEED_INCHES_PER_HOUR), + # 5000 in/h / 39.3701 in/m / 3600 s/h = 0.03528 m/s + ( + 5000, + SPEED_INCHES_PER_HOUR, + pytest.approx(0.0352778), + SPEED_METERS_PER_SECOND, + ), + # 5 kt * 1852 m/nmi / 3600 s/h = 2.5722 m/s + (5, SPEED_KNOTS, pytest.approx(2.57222), SPEED_METERS_PER_SECOND), + # 5 ft/s * 0.3048 m/ft = 1.524 m/s + (5, SPEED_FEET_PER_SECOND, pytest.approx(1.524), SPEED_METERS_PER_SECOND), + ], +) +def test_speed_convert( + value: float, + from_unit: str, + expected: float, + to_unit: str, +) -> None: + """Test conversion to other units.""" + assert SpeedConverter.convert(value, from_unit, to_unit) == expected + + @pytest.mark.parametrize( "value,from_unit,expected,to_unit", [ From 83b426deb0e14689d71943102b12f9086bdf2073 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 23 Sep 2022 14:27:27 +0200 Subject: [PATCH 666/955] Adjust normalization routines in recorder statistics (#78966) --- .../components/recorder/statistics.py | 88 +++---------------- 1 file changed, 14 insertions(+), 74 deletions(-) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 4e77ba84401..3be5aa70a74 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -127,53 +127,6 @@ QUERY_STATISTIC_META_ID = [ ] -def _convert_energy_from_kwh(to_unit: str, value: float | None) -> float | None: - """Convert energy in kWh to to_unit.""" - if value is None: - return None - return EnergyConverter.convert(value, EnergyConverter.NORMALIZED_UNIT, to_unit) - - -def _convert_energy_to_kwh(from_unit: str, value: float) -> float: - """Convert energy in from_unit to kWh.""" - return EnergyConverter.convert(value, from_unit, EnergyConverter.NORMALIZED_UNIT) - - -def _convert_power_from_w(to_unit: str, value: float | None) -> float | None: - """Convert power in W to to_unit.""" - if value is None: - return None - return PowerConverter.convert(value, PowerConverter.NORMALIZED_UNIT, to_unit) - - -def _convert_pressure_from_pa(to_unit: str, value: float | None) -> float | None: - """Convert pressure in Pa to to_unit.""" - if value is None: - return None - return PressureConverter.convert(value, PressureConverter.NORMALIZED_UNIT, to_unit) - - -def _convert_temperature_from_c(to_unit: str, value: float | None) -> float | None: - """Convert temperature in °C to to_unit.""" - if value is None: - return None - return TemperatureConverter.convert( - value, TemperatureConverter.NORMALIZED_UNIT, to_unit - ) - - -def _convert_volume_from_m3(to_unit: str, value: float | None) -> float | None: - """Convert volume in m³ to to_unit.""" - if value is None: - return None - return VolumeConverter.convert(value, VolumeConverter.NORMALIZED_UNIT, to_unit) - - -def _convert_volume_to_m3(from_unit: str, value: float) -> float: - """Convert volume in from_unit to m³.""" - return VolumeConverter.convert(value, from_unit, VolumeConverter.NORMALIZED_UNIT) - - STATISTIC_UNIT_TO_UNIT_CLASS: dict[str | None, str] = { EnergyConverter.NORMALIZED_UNIT: EnergyConverter.UNIT_CLASS, PowerConverter.NORMALIZED_UNIT: PowerConverter.UNIT_CLASS, @@ -190,25 +143,6 @@ STATISTIC_UNIT_TO_UNIT_CONVERTER: dict[str | None, type[BaseUnitConverter]] = { VolumeConverter.NORMALIZED_UNIT: VolumeConverter, } -# Convert energy power, pressure, temperature and volume statistics from the -# normalized unit used for statistics to the unit configured by the user -STATISTIC_UNIT_TO_DISPLAY_UNIT_FUNCTIONS: dict[ - str, Callable[[str, float | None], float | None] -] = { - EnergyConverter.NORMALIZED_UNIT: _convert_energy_from_kwh, - PowerConverter.NORMALIZED_UNIT: _convert_power_from_w, - PressureConverter.NORMALIZED_UNIT: _convert_pressure_from_pa, - TemperatureConverter.NORMALIZED_UNIT: _convert_temperature_from_c, - VolumeConverter.NORMALIZED_UNIT: _convert_volume_from_m3, -} - -# Convert energy and volume statistics from the display unit configured by the user -# to the normalized unit used for statistics. -# This is used to support adjusting statistics in the display unit -DISPLAY_UNIT_TO_STATISTIC_UNIT_FUNCTIONS: dict[str, Callable[[str, float], float]] = { - EnergyConverter.NORMALIZED_UNIT: _convert_energy_to_kwh, - VolumeConverter.NORMALIZED_UNIT: _convert_volume_to_m3, -} _LOGGER = logging.getLogger(__name__) @@ -227,9 +161,7 @@ def _get_statistic_to_display_unit_converter( if statistic_unit is None: return no_conversion - if ( - convert_fn := STATISTIC_UNIT_TO_DISPLAY_UNIT_FUNCTIONS.get(statistic_unit) - ) is None: + if (converter := STATISTIC_UNIT_TO_UNIT_CONVERTER.get(statistic_unit)) is None: return no_conversion display_unit: str | None @@ -244,7 +176,15 @@ def _get_statistic_to_display_unit_converter( # Guard against invalid state unit in the DB return no_conversion - return partial(convert_fn, display_unit) + def from_normalized_unit( + val: float | None, conv: type[BaseUnitConverter], to_unit: str + ) -> float | None: + """Return val.""" + if val is None: + return val + return conv.convert(val, from_unit=conv.NORMALIZED_UNIT, to_unit=to_unit) + + return partial(from_normalized_unit, conv=converter, to_unit=display_unit) def _get_display_to_statistic_unit_converter( @@ -260,12 +200,12 @@ def _get_display_to_statistic_unit_converter( if statistic_unit is None: return no_conversion - if ( - convert_fn := DISPLAY_UNIT_TO_STATISTIC_UNIT_FUNCTIONS.get(statistic_unit) - ) is None: + if (converter := STATISTIC_UNIT_TO_UNIT_CONVERTER.get(statistic_unit)) is None: return no_conversion - return partial(convert_fn, display_unit) + return partial( + converter.convert, from_unit=display_unit, to_unit=converter.NORMALIZED_UNIT + ) @dataclasses.dataclass From 7c460cc641f16077943585473be40a416783b924 Mon Sep 17 00:00:00 2001 From: Artem Draft Date: Fri, 23 Sep 2022 16:03:43 +0300 Subject: [PATCH 667/955] Add PSK auth and SSDP discovery to Bravia TV (#77772) --- homeassistant/components/braviatv/__init__.py | 11 +- .../components/braviatv/config_flow.py | 96 +++++++-- homeassistant/components/braviatv/const.py | 1 + .../components/braviatv/coordinator.py | 11 +- .../components/braviatv/manifest.json | 6 + .../components/braviatv/strings.json | 12 +- .../components/braviatv/translations/en.json | 12 +- homeassistant/generated/ssdp.py | 6 + tests/components/braviatv/test_config_flow.py | 183 +++++++++++++++++- 9 files changed, 302 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/braviatv/__init__.py b/homeassistant/components/braviatv/__init__.py index 539dd980ffc..18c1a9a2d89 100644 --- a/homeassistant/components/braviatv/__init__.py +++ b/homeassistant/components/braviatv/__init__.py @@ -11,7 +11,7 @@ from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PIN, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_create_clientsession -from .const import CONF_IGNORED_SOURCES, DOMAIN +from .const import CONF_IGNORED_SOURCES, CONF_USE_PSK, DOMAIN from .coordinator import BraviaTVCoordinator PLATFORMS: Final[list[Platform]] = [Platform.MEDIA_PLAYER, Platform.REMOTE] @@ -22,13 +22,20 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b host = config_entry.data[CONF_HOST] mac = config_entry.data[CONF_MAC] pin = config_entry.data[CONF_PIN] + use_psk = config_entry.data.get(CONF_USE_PSK, False) ignored_sources = config_entry.options.get(CONF_IGNORED_SOURCES, []) session = async_create_clientsession( hass, cookie_jar=CookieJar(unsafe=True, quote_cookie=False) ) client = BraviaTV(host, mac, session=session) - coordinator = BraviaTVCoordinator(hass, client, pin, ignored_sources) + coordinator = BraviaTVCoordinator( + hass=hass, + client=client, + pin=pin, + use_psk=use_psk, + ignored_sources=ignored_sources, + ) config_entry.async_on_unload(config_entry.add_update_listener(update_listener)) await coordinator.async_config_entry_first_refresh() diff --git a/homeassistant/components/braviatv/config_flow.py b/homeassistant/components/braviatv/config_flow.py index 75a8d5873ef..e6bf5a44019 100644 --- a/homeassistant/components/braviatv/config_flow.py +++ b/homeassistant/components/braviatv/config_flow.py @@ -2,14 +2,16 @@ from __future__ import annotations from typing import Any +from urllib.parse import urlparse from aiohttp import CookieJar -from pybravia import BraviaTV, BraviaTVError, BraviaTVNotSupported +from pybravia import BraviaTV, BraviaTVAuthError, BraviaTVError, BraviaTVNotSupported import voluptuous as vol from homeassistant import config_entries +from homeassistant.components import ssdp from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PIN +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_PIN from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_create_clientsession @@ -23,6 +25,7 @@ from .const import ( ATTR_MODEL, CLIENTID_PREFIX, CONF_IGNORED_SOURCES, + CONF_USE_PSK, DOMAIN, NICKNAME, ) @@ -33,10 +36,9 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - client: BraviaTV - def __init__(self) -> None: """Initialize config flow.""" + self.client: BraviaTV | None = None self.device_config: dict[str, Any] = {} @staticmethod @@ -45,11 +47,28 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Bravia TV options callback.""" return BraviaTVOptionsFlowHandler(config_entry) - async def async_init_device(self) -> FlowResult: - """Initialize and create Bravia TV device from config.""" - pin = self.device_config[CONF_PIN] + def create_client(self) -> None: + """Create Bravia TV client from config.""" + host = self.device_config[CONF_HOST] + session = async_create_clientsession( + self.hass, + cookie_jar=CookieJar(unsafe=True, quote_cookie=False), + ) + self.client = BraviaTV(host=host, session=session) - await self.client.connect(pin=pin, clientid=CLIENTID_PREFIX, nickname=NICKNAME) + async def async_create_device(self) -> FlowResult: + """Initialize and create Bravia TV device from config.""" + assert self.client + + pin = self.device_config[CONF_PIN] + use_psk = self.device_config[CONF_USE_PSK] + + if use_psk: + await self.client.connect(psk=pin) + else: + await self.client.connect( + pin=pin, clientid=CLIENTID_PREFIX, nickname=NICKNAME + ) await self.client.set_wol_mode(True) system_info = await self.client.get_system_info() @@ -72,13 +91,8 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: host = user_input[CONF_HOST] if is_host_valid(host): - session = async_create_clientsession( - self.hass, - cookie_jar=CookieJar(unsafe=True, quote_cookie=False), - ) - self.client = BraviaTV(host=host, session=session) self.device_config[CONF_HOST] = host - + self.create_client() return await self.async_step_authorize() errors[CONF_HOST] = "invalid_host" @@ -92,18 +106,23 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_authorize( self, user_input: dict[str, Any] | None = None ) -> FlowResult: - """Get PIN from the Bravia TV device.""" + """Authorize Bravia TV device.""" errors: dict[str, str] = {} if user_input is not None: self.device_config[CONF_PIN] = user_input[CONF_PIN] + self.device_config[CONF_USE_PSK] = user_input[CONF_USE_PSK] try: - return await self.async_init_device() + return await self.async_create_device() + except BraviaTVAuthError: + errors["base"] = "invalid_auth" except BraviaTVNotSupported: errors["base"] = "unsupported_model" except BraviaTVError: errors["base"] = "cannot_connect" + assert self.client + try: await self.client.pair(CLIENTID_PREFIX, NICKNAME) except BraviaTVError: @@ -111,10 +130,53 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="authorize", - data_schema=vol.Schema({vol.Required(CONF_PIN, default=""): str}), + data_schema=vol.Schema( + { + vol.Required(CONF_PIN, default=""): str, + vol.Required(CONF_USE_PSK, default=False): bool, + } + ), errors=errors, ) + async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: + """Handle a discovered device.""" + parsed_url = urlparse(discovery_info.ssdp_location) + host = parsed_url.hostname + + await self.async_set_unique_id(discovery_info.upnp[ssdp.ATTR_UPNP_UDN]) + self._abort_if_unique_id_configured(updates={CONF_HOST: host}) + self._async_abort_entries_match({CONF_HOST: host}) + + scalarweb_info = discovery_info.upnp["X_ScalarWebAPI_DeviceInfo"] + service_types = scalarweb_info["X_ScalarWebAPI_ServiceList"][ + "X_ScalarWebAPI_ServiceType" + ] + + if "videoScreen" not in service_types: + return self.async_abort(reason="not_bravia_device") + + model_name = discovery_info.upnp[ssdp.ATTR_UPNP_MODEL_NAME] + friendly_name = discovery_info.upnp[ssdp.ATTR_UPNP_FRIENDLY_NAME] + + self.context["title_placeholders"] = { + CONF_NAME: f"{model_name} ({friendly_name})", + CONF_HOST: host, + } + + self.device_config[CONF_HOST] = host + return await self.async_step_confirm() + + async def async_step_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Allow the user to confirm adding the device.""" + if user_input is not None: + self.create_client() + return await self.async_step_authorize() + + return self.async_show_form(step_id="confirm") + class BraviaTVOptionsFlowHandler(config_entries.OptionsFlow): """Config flow options for Bravia TV.""" diff --git a/homeassistant/components/braviatv/const.py b/homeassistant/components/braviatv/const.py index 6ed8efd3739..8855499914c 100644 --- a/homeassistant/components/braviatv/const.py +++ b/homeassistant/components/braviatv/const.py @@ -9,6 +9,7 @@ ATTR_MANUFACTURER: Final = "Sony" ATTR_MODEL: Final = "model" CONF_IGNORED_SOURCES: Final = "ignored_sources" +CONF_USE_PSK: Final = "use_psk" CLIENTID_PREFIX: Final = "HomeAssistant" DOMAIN: Final = "braviatv" diff --git a/homeassistant/components/braviatv/coordinator.py b/homeassistant/components/braviatv/coordinator.py index d190c00b1c0..1f134d8e2de 100644 --- a/homeassistant/components/braviatv/coordinator.py +++ b/homeassistant/components/braviatv/coordinator.py @@ -59,12 +59,14 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): hass: HomeAssistant, client: BraviaTV, pin: str, + use_psk: bool, ignored_sources: list[str], ) -> None: """Initialize Bravia TV Client.""" self.client = client self.pin = pin + self.use_psk = use_psk self.ignored_sources = ignored_sources self.source: str | None = None self.source_list: list[str] = [] @@ -110,9 +112,12 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): """Connect and fetch data.""" try: if not self.connected: - await self.client.connect( - pin=self.pin, clientid=CLIENTID_PREFIX, nickname=NICKNAME - ) + if self.use_psk: + await self.client.connect(psk=self.pin) + else: + await self.client.connect( + pin=self.pin, clientid=CLIENTID_PREFIX, nickname=NICKNAME + ) self.connected = True power_status = await self.client.get_power_status() diff --git a/homeassistant/components/braviatv/manifest.json b/homeassistant/components/braviatv/manifest.json index dca9d65cff0..556643d7856 100644 --- a/homeassistant/components/braviatv/manifest.json +++ b/homeassistant/components/braviatv/manifest.json @@ -4,6 +4,12 @@ "documentation": "https://www.home-assistant.io/integrations/braviatv", "requirements": ["pybravia==0.2.2"], "codeowners": ["@bieniu", "@Drafteed"], + "ssdp": [ + { + "st": "urn:schemas-sony-com:service:ScalarWebAPI:1", + "manufacturer": "Sony Corporation" + } + ], "config_flow": true, "iot_class": "local_polling", "loggers": ["pybravia"] diff --git a/homeassistant/components/braviatv/strings.json b/homeassistant/components/braviatv/strings.json index c00b143a442..f6c35f2b8ca 100644 --- a/homeassistant/components/braviatv/strings.json +++ b/homeassistant/components/braviatv/strings.json @@ -9,20 +9,26 @@ }, "authorize": { "title": "Authorize Sony Bravia TV", - "description": "Enter the PIN code shown on the Sony Bravia TV. \n\nIf the PIN code is not shown, you have to unregister Home Assistant on your TV, go to: Settings -> Network -> Remote device settings -> Unregister remote device.", + "description": "Enter the PIN code shown on the Sony Bravia TV. \n\nIf the PIN code is not shown, you have to unregister Home Assistant on your TV, go to: Settings -> Network -> Remote device settings -> Deregister remote device. \n\nYou can use PSK (Pre-Shared-Key) instead of PIN. PSK is a user-defined secret key used for access control. This authentication method is recommended as more stable. To enable PSK on your TV, go to: Settings -> Network -> Home Network Setup -> IP Control. Then check «Use PSK authentication» box and enter your PSK instead of PIN.", "data": { - "pin": "[%key:common::config_flow::data::pin%]" + "pin": "[%key:common::config_flow::data::pin%]", + "use_psk": "Use PSK authentication" } + }, + "confirm": { + "description": "[%key:common::config_flow::description::confirm_setup%]" } }, "error": { "invalid_host": "[%key:common::config_flow::error::invalid_host%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "unsupported_model": "Your TV model is not supported." }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "no_ip_control": "IP Control is disabled on your TV or the TV is not supported." + "no_ip_control": "IP Control is disabled on your TV or the TV is not supported.", + "not_bravia_device": "The device is not a Bravia TV." } }, "options": { diff --git a/homeassistant/components/braviatv/translations/en.json b/homeassistant/components/braviatv/translations/en.json index 45722d13b9a..7401fda7324 100644 --- a/homeassistant/components/braviatv/translations/en.json +++ b/homeassistant/components/braviatv/translations/en.json @@ -2,21 +2,27 @@ "config": { "abort": { "already_configured": "Device is already configured", - "no_ip_control": "IP Control is disabled on your TV or the TV is not supported." + "no_ip_control": "IP Control is disabled on your TV or the TV is not supported.", + "not_bravia_device": "The device is not a Bravia TV." }, "error": { "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", "invalid_host": "Invalid hostname or IP address", "unsupported_model": "Your TV model is not supported." }, "step": { "authorize": { "data": { - "pin": "PIN Code" + "pin": "PIN Code", + "use_psk": "Use PSK authentication" }, - "description": "Enter the PIN code shown on the Sony Bravia TV. \n\nIf the PIN code is not shown, you have to unregister Home Assistant on your TV, go to: Settings -> Network -> Remote device settings -> Unregister remote device.", + "description": "Enter the PIN code shown on the Sony Bravia TV. \n\nIf the PIN code is not shown, you have to unregister Home Assistant on your TV, go to: Settings -> Network -> Remote device settings -> Deregister remote device. \n\nYou can use PSK (Pre-Shared-Key) instead of PIN. PSK is a user-defined secret key used for access control. This authentication method is recommended as more stable. To enable PSK on your TV, go to: Settings -> Network -> Home Network Setup -> IP Control. Then check \u00abUse PSK authentication\u00bb box and enter your PSK instead of PIN.", "title": "Authorize Sony Bravia TV" }, + "confirm": { + "description": "Do you want to start set up?" + }, "user": { "data": { "host": "Host" diff --git a/homeassistant/generated/ssdp.py b/homeassistant/generated/ssdp.py index 029afcd64fe..d7a67f579d8 100644 --- a/homeassistant/generated/ssdp.py +++ b/homeassistant/generated/ssdp.py @@ -15,6 +15,12 @@ SSDP = { "manufacturer": "AXIS", }, ], + "braviatv": [ + { + "manufacturer": "Sony Corporation", + "st": "urn:schemas-sony-com:service:ScalarWebAPI:1" + } + ], "control4": [ { "st": "c4:director", diff --git a/tests/components/braviatv/test_config_flow.py b/tests/components/braviatv/test_config_flow.py index a105f20d3ee..64986e9d973 100644 --- a/tests/components/braviatv/test_config_flow.py +++ b/tests/components/braviatv/test_config_flow.py @@ -1,11 +1,16 @@ """Define tests for the Bravia TV config flow.""" from unittest.mock import patch -from pybravia import BraviaTVConnectionError, BraviaTVNotSupported +from pybravia import BraviaTVAuthError, BraviaTVConnectionError, BraviaTVNotSupported from homeassistant import data_entry_flow -from homeassistant.components.braviatv.const import CONF_IGNORED_SOURCES, DOMAIN -from homeassistant.config_entries import SOURCE_USER +from homeassistant.components import ssdp +from homeassistant.components.braviatv.const import ( + CONF_IGNORED_SOURCES, + CONF_USE_PSK, + DOMAIN, +) +from homeassistant.config_entries import SOURCE_SSDP, SOURCE_USER from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PIN from tests.common import MockConfigEntry @@ -31,6 +36,44 @@ BRAVIA_SOURCES = [ {"title": "AV/Component", "uri": "extInput:component?port=1"}, ] +BRAVIA_SSDP = ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location="http://bravia-host:52323/dmr.xml", + upnp={ + ssdp.ATTR_UPNP_UDN: "uuid:1234", + ssdp.ATTR_UPNP_FRIENDLY_NAME: "Living TV", + ssdp.ATTR_UPNP_MODEL_NAME: "KE-55XH9096", + "X_ScalarWebAPI_DeviceInfo": { + "X_ScalarWebAPI_ServiceList": { + "X_ScalarWebAPI_ServiceType": [ + "guide", + "system", + "audio", + "avContent", + "videoScreen", + ], + }, + }, + }, +) + +FAKE_BRAVIA_SSDP = ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location="http://soundbar-host:52323/dmr.xml", + upnp={ + ssdp.ATTR_UPNP_UDN: "uuid:1234", + ssdp.ATTR_UPNP_FRIENDLY_NAME: "Sony Audio Device", + ssdp.ATTR_UPNP_MODEL_NAME: "HT-S700RF", + "X_ScalarWebAPI_DeviceInfo": { + "X_ScalarWebAPI_ServiceList": { + "X_ScalarWebAPI_ServiceType": ["guide", "system", "audio", "avContent"], + }, + }, + }, +) + async def test_show_form(hass): """Test that the form is served with no input.""" @@ -42,6 +85,83 @@ async def test_show_form(hass): assert result["step_id"] == SOURCE_USER +async def test_ssdp_discovery(hass): + """Test that the device is discovered.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_SSDP}, + data=BRAVIA_SSDP, + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "confirm" + + with patch("pybravia.BraviaTV.connect"), patch("pybravia.BraviaTV.pair"), patch( + "pybravia.BraviaTV.set_wol_mode" + ), patch( + "pybravia.BraviaTV.get_system_info", + return_value=BRAVIA_SYSTEM_INFO, + ), patch( + "homeassistant.components.braviatv.async_setup_entry", return_value=True + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "authorize" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_PIN: "1234", CONF_USE_PSK: False} + ) + + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["result"].unique_id == "very_unique_string" + assert result["title"] == "TV-Model" + assert result["data"] == { + CONF_HOST: "bravia-host", + CONF_PIN: "1234", + CONF_USE_PSK: False, + CONF_MAC: "AA:BB:CC:DD:EE:FF", + } + + +async def test_ssdp_discovery_fake(hass): + """Test that not Bravia device is not discovered.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_SSDP}, + data=FAKE_BRAVIA_SSDP, + ) + + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "not_bravia_device" + + +async def test_ssdp_discovery_exist(hass): + """Test that the existed device is not discovered.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + unique_id="very_unique_string", + data={ + CONF_HOST: "bravia-host", + CONF_PIN: "1234", + CONF_MAC: "AA:BB:CC:DD:EE:FF", + }, + title="TV-Model", + ) + config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_SSDP}, + data=BRAVIA_SSDP, + ) + + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "already_configured" + + async def test_user_invalid_host(hass): """Test that errors are shown when the host is invalid.""" result = await hass.config_entries.flow.async_init( @@ -51,6 +171,22 @@ async def test_user_invalid_host(hass): assert result["errors"] == {CONF_HOST: "invalid_host"} +async def test_authorize_invalid_auth(hass): + """Test that authorization errors shown on the authorization step.""" + with patch( + "pybravia.BraviaTV.connect", + side_effect=BraviaTVAuthError, + ), patch("pybravia.BraviaTV.pair"): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "bravia-host"} + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_PIN: "1234"} + ) + + assert result["errors"] == {"base": "invalid_auth"} + + async def test_authorize_cannot_connect(hass): """Test that errors are shown when cannot connect to host at the authorize step.""" with patch( @@ -114,7 +250,6 @@ async def test_duplicate_error(hass): "pybravia.BraviaTV.get_system_info", return_value=BRAVIA_SYSTEM_INFO, ): - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "bravia-host"} ) @@ -136,7 +271,6 @@ async def test_create_entry(hass): ), patch( "homeassistant.components.braviatv.async_setup_entry", return_value=True ): - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "bravia-host"} ) @@ -145,7 +279,7 @@ async def test_create_entry(hass): assert result["step_id"] == "authorize" result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_PIN: "1234"} + result["flow_id"], user_input={CONF_PIN: "1234", CONF_USE_PSK: False} ) assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY @@ -154,6 +288,7 @@ async def test_create_entry(hass): assert result["data"] == { CONF_HOST: "bravia-host", CONF_PIN: "1234", + CONF_USE_PSK: False, CONF_MAC: "AA:BB:CC:DD:EE:FF", } @@ -168,7 +303,6 @@ async def test_create_entry_with_ipv6_address(hass): ), patch( "homeassistant.components.braviatv.async_setup_entry", return_value=True ): - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, @@ -179,7 +313,7 @@ async def test_create_entry_with_ipv6_address(hass): assert result["step_id"] == "authorize" result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_PIN: "1234"} + result["flow_id"], user_input={CONF_PIN: "1234", CONF_USE_PSK: False} ) assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY @@ -188,6 +322,39 @@ async def test_create_entry_with_ipv6_address(hass): assert result["data"] == { CONF_HOST: "2001:db8::1428:57ab", CONF_PIN: "1234", + CONF_USE_PSK: False, + CONF_MAC: "AA:BB:CC:DD:EE:FF", + } + + +async def test_create_entry_psk(hass): + """Test that the user step works with PSK auth.""" + with patch("pybravia.BraviaTV.connect"), patch("pybravia.BraviaTV.pair"), patch( + "pybravia.BraviaTV.set_wol_mode" + ), patch( + "pybravia.BraviaTV.get_system_info", + return_value=BRAVIA_SYSTEM_INFO, + ), patch( + "homeassistant.components.braviatv.async_setup_entry", return_value=True + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "bravia-host"} + ) + + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "authorize" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_PIN: "mypsk", CONF_USE_PSK: True} + ) + + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["result"].unique_id == "very_unique_string" + assert result["title"] == "TV-Model" + assert result["data"] == { + CONF_HOST: "bravia-host", + CONF_PIN: "mypsk", + CONF_USE_PSK: True, CONF_MAC: "AA:BB:CC:DD:EE:FF", } From ab4c1ebfd6ab79abfc4e214853f71afba2380099 Mon Sep 17 00:00:00 2001 From: Artem Draft Date: Fri, 23 Sep 2022 16:39:24 +0300 Subject: [PATCH 668/955] Add Button platform to Bravia TV (#78093) * Add Button platform to Bravia TV * Add button.py to coveragerc * improve callable type --- .coveragerc | 1 + homeassistant/components/braviatv/__init__.py | 6 +- homeassistant/components/braviatv/button.py | 89 +++++++++++++++++++ .../components/braviatv/coordinator.py | 10 +++ 4 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/braviatv/button.py diff --git a/.coveragerc b/.coveragerc index 6cff79350d2..71a4b652ce1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -139,6 +139,7 @@ omit = homeassistant/components/bosch_shc/sensor.py homeassistant/components/bosch_shc/switch.py homeassistant/components/braviatv/__init__.py + homeassistant/components/braviatv/button.py homeassistant/components/braviatv/const.py homeassistant/components/braviatv/coordinator.py homeassistant/components/braviatv/entity.py diff --git a/homeassistant/components/braviatv/__init__.py b/homeassistant/components/braviatv/__init__.py index 18c1a9a2d89..d06482e5c71 100644 --- a/homeassistant/components/braviatv/__init__.py +++ b/homeassistant/components/braviatv/__init__.py @@ -14,7 +14,11 @@ from homeassistant.helpers.aiohttp_client import async_create_clientsession from .const import CONF_IGNORED_SOURCES, CONF_USE_PSK, DOMAIN from .coordinator import BraviaTVCoordinator -PLATFORMS: Final[list[Platform]] = [Platform.MEDIA_PLAYER, Platform.REMOTE] +PLATFORMS: Final[list[Platform]] = [ + Platform.BUTTON, + Platform.MEDIA_PLAYER, + Platform.REMOTE, +] async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/braviatv/button.py b/homeassistant/components/braviatv/button.py new file mode 100644 index 00000000000..6cc0cb393c5 --- /dev/null +++ b/homeassistant/components/braviatv/button.py @@ -0,0 +1,89 @@ +"""Button support for Bravia TV.""" +from __future__ import annotations + +from collections.abc import Callable, Coroutine +from dataclasses import dataclass + +from homeassistant.components.button import ( + ButtonDeviceClass, + ButtonEntity, + ButtonEntityDescription, +) +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 .const import DOMAIN +from .coordinator import BraviaTVCoordinator +from .entity import BraviaTVEntity + + +@dataclass +class BraviaTVButtonDescriptionMixin: + """Mixin to describe a Bravia TV Button entity.""" + + press_action: Callable[[BraviaTVCoordinator], Coroutine] + + +@dataclass +class BraviaTVButtonDescription( + ButtonEntityDescription, BraviaTVButtonDescriptionMixin +): + """Bravia TV Button description.""" + + +BUTTONS: tuple[BraviaTVButtonDescription, ...] = ( + BraviaTVButtonDescription( + key="reboot", + name="Reboot", + device_class=ButtonDeviceClass.RESTART, + entity_category=EntityCategory.CONFIG, + press_action=lambda coordinator: coordinator.async_reboot_device(), + ), + BraviaTVButtonDescription( + key="terminate_apps", + name="Terminate apps", + entity_category=EntityCategory.CONFIG, + press_action=lambda coordinator: coordinator.async_terminate_apps(), + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Bravia TV Button entities.""" + + coordinator = hass.data[DOMAIN][config_entry.entry_id] + unique_id = config_entry.unique_id + assert unique_id is not None + + async_add_entities( + BraviaTVButton(coordinator, unique_id, config_entry.title, description) + for description in BUTTONS + ) + + +class BraviaTVButton(BraviaTVEntity, ButtonEntity): + """Representation of a Bravia TV Button.""" + + entity_description: BraviaTVButtonDescription + + def __init__( + self, + coordinator: BraviaTVCoordinator, + unique_id: str, + model: str, + description: BraviaTVButtonDescription, + ) -> None: + """Initialize the button.""" + super().__init__(coordinator, unique_id, model) + self._attr_unique_id = f"{unique_id}_{description.key}" + self.entity_description = description + + async def async_press(self) -> None: + """Trigger the button action.""" + await self.entity_description.press_action(self.coordinator) diff --git a/homeassistant/components/braviatv/coordinator.py b/homeassistant/components/braviatv/coordinator.py index 1f134d8e2de..3bfd815f990 100644 --- a/homeassistant/components/braviatv/coordinator.py +++ b/homeassistant/components/braviatv/coordinator.py @@ -287,3 +287,13 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): cmd, commands_keys, ) + + @catch_braviatv_errors + async def async_reboot_device(self) -> None: + """Send command to reboot the device.""" + await self.client.reboot() + + @catch_braviatv_errors + async def async_terminate_apps(self) -> None: + """Send command to terminate all applications.""" + await self.client.terminate_apps() From 55d90c0abc556245ccbb22def02538b81efa7223 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Fri, 23 Sep 2022 15:42:11 +0200 Subject: [PATCH 669/955] Correct ssdp generation for bravia (#79002) Correct ssdp generation --- homeassistant/generated/ssdp.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/generated/ssdp.py b/homeassistant/generated/ssdp.py index d7a67f579d8..c77f3f6a68b 100644 --- a/homeassistant/generated/ssdp.py +++ b/homeassistant/generated/ssdp.py @@ -18,8 +18,8 @@ SSDP = { "braviatv": [ { "manufacturer": "Sony Corporation", - "st": "urn:schemas-sony-com:service:ScalarWebAPI:1" - } + "st": "urn:schemas-sony-com:service:ScalarWebAPI:1", + }, ], "control4": [ { From 08ebb9f31a1ca56e1573ed20daecd426505f5201 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 23 Sep 2022 15:55:17 +0200 Subject: [PATCH 670/955] Test sum AND mean in recorder tests (#78998) --- .../components/recorder/test_websocket_api.py | 133 ++++++++++++------ 1 file changed, 93 insertions(+), 40 deletions(-) diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index e8214f0209e..008894250be 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -30,16 +30,46 @@ from .common import ( from tests.common import async_fire_time_changed +ENERGY_SENSOR_KWH_ATTRIBUTES = { + "device_class": "energy", + "state_class": "total", + "unit_of_measurement": "kWh", +} +ENERGY_SENSOR_WH_ATTRIBUTES = { + "device_class": "energy", + "state_class": "total", + "unit_of_measurement": "Wh", +} +GAS_SENSOR_FT3_ATTRIBUTES = { + "device_class": "gas", + "state_class": "total", + "unit_of_measurement": "ft³", +} +GAS_SENSOR_M3_ATTRIBUTES = { + "device_class": "gas", + "state_class": "total", + "unit_of_measurement": "m³", +} POWER_SENSOR_KW_ATTRIBUTES = { "device_class": "power", "state_class": "measurement", "unit_of_measurement": "kW", } +POWER_SENSOR_W_ATTRIBUTES = { + "device_class": "power", + "state_class": "measurement", + "unit_of_measurement": "W", +} PRESSURE_SENSOR_HPA_ATTRIBUTES = { "device_class": "pressure", "state_class": "measurement", "unit_of_measurement": "hPa", } +PRESSURE_SENSOR_PA_ATTRIBUTES = { + "device_class": "pressure", + "state_class": "measurement", + "unit_of_measurement": "Pa", +} TEMPERATURE_SENSOR_C_ATTRIBUTES = { "device_class": "temperature", "state_class": "measurement", @@ -50,16 +80,6 @@ TEMPERATURE_SENSOR_F_ATTRIBUTES = { "state_class": "measurement", "unit_of_measurement": "°F", } -ENERGY_SENSOR_KWH_ATTRIBUTES = { - "device_class": "energy", - "state_class": "total", - "unit_of_measurement": "kWh", -} -GAS_SENSOR_M3_ATTRIBUTES = { - "device_class": "gas", - "state_class": "total", - "unit_of_measurement": "m³", -} @pytest.mark.parametrize( @@ -544,14 +564,18 @@ async def test_statistics_during_period_bad_end_time( @pytest.mark.parametrize( "units, attributes, display_unit, statistics_unit, unit_class", [ + (IMPERIAL_SYSTEM, ENERGY_SENSOR_WH_ATTRIBUTES, "Wh", "kWh", "energy"), + (METRIC_SYSTEM, ENERGY_SENSOR_WH_ATTRIBUTES, "Wh", "kWh", "energy"), + (IMPERIAL_SYSTEM, GAS_SENSOR_FT3_ATTRIBUTES, "ft³", "m³", "volume"), + (METRIC_SYSTEM, GAS_SENSOR_FT3_ATTRIBUTES, "ft³", "m³", "volume"), (IMPERIAL_SYSTEM, POWER_SENSOR_KW_ATTRIBUTES, "kW", "W", "power"), (METRIC_SYSTEM, POWER_SENSOR_KW_ATTRIBUTES, "kW", "W", "power"), + (IMPERIAL_SYSTEM, PRESSURE_SENSOR_HPA_ATTRIBUTES, "hPa", "Pa", "pressure"), + (METRIC_SYSTEM, PRESSURE_SENSOR_HPA_ATTRIBUTES, "hPa", "Pa", "pressure"), (IMPERIAL_SYSTEM, TEMPERATURE_SENSOR_C_ATTRIBUTES, "°C", "°C", "temperature"), (METRIC_SYSTEM, TEMPERATURE_SENSOR_C_ATTRIBUTES, "°C", "°C", "temperature"), (IMPERIAL_SYSTEM, TEMPERATURE_SENSOR_F_ATTRIBUTES, "°F", "°C", "temperature"), (METRIC_SYSTEM, TEMPERATURE_SENSOR_F_ATTRIBUTES, "°F", "°C", "temperature"), - (IMPERIAL_SYSTEM, PRESSURE_SENSOR_HPA_ATTRIBUTES, "hPa", "Pa", "pressure"), - (METRIC_SYSTEM, PRESSURE_SENSOR_HPA_ATTRIBUTES, "hPa", "Pa", "pressure"), ], ) async def test_list_statistic_ids( @@ -566,6 +590,8 @@ async def test_list_statistic_ids( ): """Test list_statistic_ids.""" now = dt_util.utcnow() + has_mean = attributes["state_class"] == "measurement" + has_sum = not has_mean hass.config.units = units await async_setup_component(hass, "sensor", {}) @@ -586,8 +612,8 @@ async def test_list_statistic_ids( assert response["result"] == [ { "statistic_id": "sensor.test", - "has_mean": True, - "has_sum": False, + "has_mean": has_mean, + "has_sum": has_sum, "name": None, "source": "recorder", "display_unit_of_measurement": display_unit, @@ -608,8 +634,8 @@ async def test_list_statistic_ids( assert response["result"] == [ { "statistic_id": "sensor.test", - "has_mean": True, - "has_sum": False, + "has_mean": has_mean, + "has_sum": has_sum, "name": None, "source": "recorder", "display_unit_of_measurement": display_unit, @@ -629,25 +655,42 @@ async def test_list_statistic_ids( ) response = await client.receive_json() assert response["success"] - assert response["result"] == [ - { - "statistic_id": "sensor.test", - "has_mean": True, - "has_sum": False, - "name": None, - "source": "recorder", - "display_unit_of_measurement": display_unit, - "statistics_unit_of_measurement": statistics_unit, - "unit_class": unit_class, - } - ] + if has_mean: + assert response["result"] == [ + { + "statistic_id": "sensor.test", + "has_mean": has_mean, + "has_sum": has_sum, + "name": None, + "source": "recorder", + "display_unit_of_measurement": display_unit, + "statistics_unit_of_measurement": statistics_unit, + "unit_class": unit_class, + } + ] + else: + assert response["result"] == [] await client.send_json( {"id": 6, "type": "recorder/list_statistic_ids", "statistic_type": "sum"} ) response = await client.receive_json() assert response["success"] - assert response["result"] == [] + if has_sum: + assert response["result"] == [ + { + "statistic_id": "sensor.test", + "has_mean": has_mean, + "has_sum": has_sum, + "name": None, + "source": "recorder", + "display_unit_of_measurement": display_unit, + "statistics_unit_of_measurement": statistics_unit, + "unit_class": unit_class, + } + ] + else: + assert response["result"] == [] async def test_validate_statistics(hass, hass_ws_client, recorder_mock): @@ -1045,8 +1088,16 @@ async def test_backup_end_without_start( @pytest.mark.parametrize( "units, attributes, unit, unit_class", [ - (METRIC_SYSTEM, GAS_SENSOR_M3_ATTRIBUTES, "m³", "volume"), (METRIC_SYSTEM, ENERGY_SENSOR_KWH_ATTRIBUTES, "kWh", "energy"), + (METRIC_SYSTEM, ENERGY_SENSOR_WH_ATTRIBUTES, "kWh", "energy"), + (METRIC_SYSTEM, GAS_SENSOR_FT3_ATTRIBUTES, "m³", "volume"), + (METRIC_SYSTEM, GAS_SENSOR_M3_ATTRIBUTES, "m³", "volume"), + (METRIC_SYSTEM, POWER_SENSOR_W_ATTRIBUTES, "W", "power"), + (METRIC_SYSTEM, POWER_SENSOR_KW_ATTRIBUTES, "W", "power"), + (METRIC_SYSTEM, PRESSURE_SENSOR_PA_ATTRIBUTES, "Pa", "pressure"), + (METRIC_SYSTEM, PRESSURE_SENSOR_HPA_ATTRIBUTES, "Pa", "pressure"), + (METRIC_SYSTEM, TEMPERATURE_SENSOR_C_ATTRIBUTES, "°C", "temperature"), + (METRIC_SYSTEM, TEMPERATURE_SENSOR_F_ATTRIBUTES, "°C", "temperature"), ], ) async def test_get_statistics_metadata( @@ -1054,6 +1105,8 @@ async def test_get_statistics_metadata( ): """Test get_statistics_metadata.""" now = dt_util.utcnow() + has_mean = attributes["state_class"] == "measurement" + has_sum = not has_mean hass.config.units = units await async_setup_component(hass, "sensor", {}) @@ -1096,8 +1149,8 @@ async def test_get_statistics_metadata( }, ) external_energy_metadata_1 = { - "has_mean": False, - "has_sum": True, + "has_mean": has_mean, + "has_sum": has_sum, "name": "Total imported energy", "source": "test", "state_unit_of_measurement": unit, @@ -1123,8 +1176,8 @@ async def test_get_statistics_metadata( { "statistic_id": "test:total_gas", "display_unit_of_measurement": unit, - "has_mean": False, - "has_sum": True, + "has_mean": has_mean, + "has_sum": has_sum, "name": "Total imported energy", "source": "test", "statistics_unit_of_measurement": unit, @@ -1150,9 +1203,9 @@ async def test_get_statistics_metadata( assert response["result"] == [ { "statistic_id": "sensor.test", - "display_unit_of_measurement": unit, - "has_mean": False, - "has_sum": True, + "display_unit_of_measurement": attributes["unit_of_measurement"], + "has_mean": has_mean, + "has_sum": has_sum, "name": None, "source": "recorder", "statistics_unit_of_measurement": unit, @@ -1178,9 +1231,9 @@ async def test_get_statistics_metadata( assert response["result"] == [ { "statistic_id": "sensor.test", - "display_unit_of_measurement": unit, - "has_mean": False, - "has_sum": True, + "display_unit_of_measurement": attributes["unit_of_measurement"], + "has_mean": has_mean, + "has_sum": has_sum, "name": None, "source": "recorder", "statistics_unit_of_measurement": unit, From 67779089cd066fcdba510aa9743c3d5817d5d3fe Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Fri, 23 Sep 2022 15:23:33 +0100 Subject: [PATCH 671/955] Bump pyipma to 3.0.5 (#78936) * fix #78928 and review of #78332 * address comment --- homeassistant/components/ipma/__init__.py | 30 +++++++++------------ homeassistant/components/ipma/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 16 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/ipma/__init__.py b/homeassistant/components/ipma/__init__.py index 315362247a2..eec16a0c811 100644 --- a/homeassistant/components/ipma/__init__.py +++ b/homeassistant/components/ipma/__init__.py @@ -1,4 +1,5 @@ """Component for the Portuguese weather service - IPMA.""" +import asyncio import logging import async_timeout @@ -22,36 +23,31 @@ PLATFORMS = [Platform.WEATHER] _LOGGER = logging.getLogger(__name__) -async def async_get_api(hass): - """Get the pyipma api object.""" - websession = async_get_clientsession(hass) - return IPMA_API(websession) - - async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up IPMA station as config entry.""" latitude = config_entry.data[CONF_LATITUDE] longitude = config_entry.data[CONF_LONGITUDE] - api = await async_get_api(hass) + api = IPMA_API(async_get_clientsession(hass)) + try: async with async_timeout.timeout(30): location = await Location.get(api, float(latitude), float(longitude)) - - _LOGGER.debug( - "Initializing for coordinates %s, %s -> station %s (%d, %d)", - latitude, - longitude, - location.station, - location.id_station, - location.global_id_local, - ) - except IPMAException as err: + except (IPMAException, asyncio.TimeoutError) as err: raise ConfigEntryNotReady( f"Could not get location for ({latitude},{longitude})" ) from err + _LOGGER.debug( + "Initializing for coordinates %s, %s -> station %s (%d, %d)", + latitude, + longitude, + location.station, + location.id_station, + location.global_id_local, + ) + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][config_entry.entry_id] = {DATA_API: api, DATA_LOCATION: location} diff --git a/homeassistant/components/ipma/manifest.json b/homeassistant/components/ipma/manifest.json index 23558600373..36dca71e957 100644 --- a/homeassistant/components/ipma/manifest.json +++ b/homeassistant/components/ipma/manifest.json @@ -3,7 +3,7 @@ "name": "Instituto Portugu\u00eas do Mar e Atmosfera (IPMA)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ipma", - "requirements": ["pyipma==3.0.4"], + "requirements": ["pyipma==3.0.5"], "codeowners": ["@dgomes", "@abmantis"], "iot_class": "cloud_polling", "loggers": ["geopy", "pyipma"] diff --git a/requirements_all.txt b/requirements_all.txt index a59bc227e27..39ac68b68c0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1622,7 +1622,7 @@ pyinsteon==1.2.0 pyintesishome==1.8.0 # homeassistant.components.ipma -pyipma==3.0.4 +pyipma==3.0.5 # homeassistant.components.ipp pyipp==0.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 18a0dcb52fc..50fd38c35e3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1132,7 +1132,7 @@ pyicloud==1.0.0 pyinsteon==1.2.0 # homeassistant.components.ipma -pyipma==3.0.4 +pyipma==3.0.5 # homeassistant.components.ipp pyipp==0.11.0 From ace9592aa103ad59e80994d576c32e9f469404ff Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Fri, 23 Sep 2022 16:47:58 +0200 Subject: [PATCH 672/955] Enable strict typing for rfxtrx (#74927) * Additional typing * Enable strict typing * Avoid changes causing coverage change * Adjust comment on force update * Rename to replace_devices * Reduce typing scope * Adjust mypy --- .strict-typing | 1 + homeassistant/components/rfxtrx/__init__.py | 51 +++++---- .../components/rfxtrx/binary_sensor.py | 29 +++-- .../components/rfxtrx/config_flow.py | 104 ++++++++++++------ homeassistant/components/rfxtrx/cover.py | 15 ++- .../components/rfxtrx/device_action.py | 6 +- homeassistant/components/rfxtrx/helpers.py | 4 +- homeassistant/components/rfxtrx/light.py | 13 ++- homeassistant/components/rfxtrx/sensor.py | 44 +++++--- homeassistant/components/rfxtrx/siren.py | 5 +- homeassistant/components/rfxtrx/switch.py | 9 +- mypy.ini | 10 ++ 12 files changed, 190 insertions(+), 101 deletions(-) diff --git a/.strict-typing b/.strict-typing index 8938e694a7e..9df3d16dc43 100644 --- a/.strict-typing +++ b/.strict-typing @@ -213,6 +213,7 @@ homeassistant.components.recorder.* homeassistant.components.remote.* homeassistant.components.renault.* homeassistant.components.repairs.* +homeassistant.components.rfxtrx.* homeassistant.components.rhasspy.* homeassistant.components.ridwell.* homeassistant.components.rituals_perfume_genie.* diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index 3257a482c0c..31a0e4694ba 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -6,7 +6,7 @@ import binascii from collections.abc import Callable import copy import logging -from typing import NamedTuple, cast +from typing import Any, NamedTuple, cast import RFXtrx as rfxtrxmod import async_timeout @@ -61,7 +61,7 @@ class DeviceTuple(NamedTuple): id_string: str -def _bytearray_string(data): +def _bytearray_string(data: Any) -> bytearray: val = cv.string(data) try: return bytearray.fromhex(val) @@ -116,7 +116,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -def _create_rfx(config): +def _create_rfx(config: dict[str, Any]) -> rfxtrxmod.Connect: """Construct a rfx object based on config.""" modes = config.get(CONF_PROTOCOLS) @@ -144,7 +144,9 @@ def _create_rfx(config): return rfx -def _get_device_lookup(devices): +def _get_device_lookup( + devices: dict[str, dict[str, Any]] +) -> dict[DeviceTuple, dict[str, Any]]: """Get a lookup structure for devices.""" lookup = {} for event_code, event_config in devices.items(): @@ -157,7 +159,7 @@ def _get_device_lookup(devices): return lookup -async def async_setup_internal(hass, entry: ConfigEntry): +async def async_setup_internal(hass: HomeAssistant, entry: ConfigEntry) -> None: """Set up the RFXtrx component.""" config = entry.data @@ -173,7 +175,7 @@ async def async_setup_internal(hass, entry: ConfigEntry): # Declare the Handle event @callback - def async_handle_receive(event): + def async_handle_receive(event: rfxtrxmod.RFXtrxEvent) -> None: """Handle received messages from RFXtrx gateway.""" # Log RFXCOM event if not event.device.id_string: @@ -204,7 +206,7 @@ async def async_setup_internal(hass, entry: ConfigEntry): pt2262_devices.append(event.device.id_string) device_entry = device_registry.async_get_device( - identifiers={(DOMAIN, *device_id)}, + identifiers={(DOMAIN, *device_id)}, # type: ignore[arg-type] ) if device_entry: event_data[ATTR_DEVICE_ID] = device_entry.id @@ -216,7 +218,7 @@ async def async_setup_internal(hass, entry: ConfigEntry): hass.bus.async_fire(EVENT_RFXTRX_EVENT, event_data) @callback - def _add_device(event, device_id): + def _add_device(event: rfxtrxmod.RFXtrxEvent, device_id: DeviceTuple) -> None: """Add a device to config entry.""" config = {} config[CONF_DEVICE_ID] = device_id @@ -237,7 +239,7 @@ async def async_setup_internal(hass, entry: ConfigEntry): devices[device_id] = config @callback - def _remove_device(device_id: DeviceTuple): + def _remove_device(device_id: DeviceTuple) -> None: data = { **entry.data, CONF_DEVICES: { @@ -250,7 +252,7 @@ async def async_setup_internal(hass, entry: ConfigEntry): devices.pop(device_id) @callback - def _updated_device(event: Event): + def _updated_device(event: Event) -> None: if event.data["action"] != "remove": return device_entry = device_registry.deleted_devices[event.data["device_id"]] @@ -264,7 +266,7 @@ async def async_setup_internal(hass, entry: ConfigEntry): hass.bus.async_listen(dr.EVENT_DEVICE_REGISTRY_UPDATED, _updated_device) ) - def _shutdown_rfxtrx(event): + def _shutdown_rfxtrx(event: Event) -> None: """Close connection with RFXtrx.""" rfx_object.close_connection() @@ -288,10 +290,15 @@ async def async_setup_platform_entry( async_add_entities: AddEntitiesCallback, supported: Callable[[rfxtrxmod.RFXtrxEvent], bool], constructor: Callable[ - [rfxtrxmod.RFXtrxEvent, rfxtrxmod.RFXtrxEvent | None, DeviceTuple, dict], + [ + rfxtrxmod.RFXtrxEvent, + rfxtrxmod.RFXtrxEvent | None, + DeviceTuple, + dict[str, Any], + ], list[Entity], ], -): +) -> None: """Set up config entry.""" entry_data = config_entry.data device_ids: set[DeviceTuple] = set() @@ -320,7 +327,7 @@ async def async_setup_platform_entry( if entry_data[CONF_AUTOMATIC_ADD]: @callback - def _update(event: rfxtrxmod.RFXtrxEvent, device_id: DeviceTuple): + def _update(event: rfxtrxmod.RFXtrxEvent, device_id: DeviceTuple) -> None: """Handle light updates from the RFXtrx gateway.""" if not supported(event): return @@ -373,7 +380,7 @@ def get_pt2262_cmd(device_id: str, data_bits: int) -> str | None: def get_device_data_bits( - device: rfxtrxmod.RFXtrxDevice, devices: dict[DeviceTuple, dict] + device: rfxtrxmod.RFXtrxDevice, devices: dict[DeviceTuple, dict[str, Any]] ) -> int | None: """Deduce data bits for device based on a cache of device bits.""" data_bits = None @@ -488,9 +495,9 @@ class RfxtrxEntity(RestoreEntity): self._device_id = device_id # If id_string is 213c7f2:1, the group_id is 213c7f2, and the device will respond to # group events regardless of their group indices. - (self._group_id, _, _) = device.id_string.partition(":") + (self._group_id, _, _) = cast(str, device.id_string).partition(":") - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Restore RFXtrx device state (ON/OFF).""" if self._event: self._apply_event(self._event) @@ -500,13 +507,15 @@ class RfxtrxEntity(RestoreEntity): ) @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, str] | None: """Return the device state attributes.""" if not self._event: return None return {ATTR_EVENT: "".join(f"{x:02x}" for x in self._event.data)} - def _event_applies(self, event: rfxtrxmod.RFXtrxEvent, device_id: DeviceTuple): + def _event_applies( + self, event: rfxtrxmod.RFXtrxEvent, device_id: DeviceTuple + ) -> bool: """Check if event applies to me.""" if isinstance(event, rfxtrxmod.ControlEvent): if ( @@ -514,7 +523,7 @@ class RfxtrxEntity(RestoreEntity): and event.values["Command"] in COMMAND_GROUP_LIST ): device: rfxtrxmod.RFXtrxDevice = event.device - (group_id, _, _) = device.id_string.partition(":") + (group_id, _, _) = cast(str, device.id_string).partition(":") return group_id == self._group_id # Otherwise, the event only applies to the matching device. @@ -546,6 +555,6 @@ class RfxtrxCommandEntity(RfxtrxEntity): """Initialzie a switch or light device.""" super().__init__(device, device_id, event=event) - async def _async_send(self, fun, *args): + async def _async_send(self, fun: Callable[..., None], *args: Any) -> None: rfx_object = self.hass.data[DOMAIN][DATA_RFXOBJECT] await self.hass.async_add_executor_job(fun, rfx_object.transport, *args) diff --git a/homeassistant/components/rfxtrx/binary_sensor.py b/homeassistant/components/rfxtrx/binary_sensor.py index df79c17263c..03ad7ce071b 100644 --- a/homeassistant/components/rfxtrx/binary_sensor.py +++ b/homeassistant/components/rfxtrx/binary_sensor.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any import RFXtrx as rfxtrxmod @@ -14,6 +15,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_COMMAND_OFF, CONF_COMMAND_ON, STATE_ON from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers import event as evt +from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import DeviceTuple, RfxtrxEntity, async_setup_platform_entry, get_pt2262_cmd @@ -72,7 +74,7 @@ SENSOR_TYPES = ( SENSOR_TYPES_DICT = {desc.key: desc for desc in SENSOR_TYPES} -def supported(event: rfxtrxmod.RFXtrxEvent): +def supported(event: rfxtrxmod.RFXtrxEvent) -> bool: """Return whether an event supports binary_sensor.""" if isinstance(event, rfxtrxmod.ControlEvent): return True @@ -91,7 +93,7 @@ async def async_setup_entry( ) -> None: """Set up config entry.""" - def get_sensor_description(type_string: str): + def get_sensor_description(type_string: str) -> BinarySensorEntityDescription: if (description := SENSOR_TYPES_DICT.get(type_string)) is None: return BinarySensorEntityDescription(key=type_string) return description @@ -100,8 +102,8 @@ async def async_setup_entry( event: rfxtrxmod.RFXtrxEvent, auto: rfxtrxmod.RFXtrxEvent | None, device_id: DeviceTuple, - entity_info: dict, - ): + entity_info: dict[str, Any], + ) -> list[Entity]: return [ RfxtrxBinarySensor( @@ -122,10 +124,13 @@ async def async_setup_entry( class RfxtrxBinarySensor(RfxtrxEntity, BinarySensorEntity): - """A representation of a RFXtrx binary sensor.""" + """A representation of a RFXtrx binary sensor. + + Since all repeated events have meaning, these types of sensors + need to have force update enabled. + """ _attr_force_update = True - """We should force updates. Repeated states have meaning.""" def __init__( self, @@ -159,7 +164,7 @@ class RfxtrxBinarySensor(RfxtrxEntity, BinarySensorEntity): if self.is_on and self._off_delay is not None: self._attr_is_on = False - def _apply_event_lighting4(self, event: rfxtrxmod.RFXtrxEvent): + def _apply_event_lighting4(self, event: rfxtrxmod.RFXtrxEvent) -> None: """Apply event for a lighting 4 device.""" if self._data_bits is not None: cmdstr = get_pt2262_cmd(event.device.id_string, self._data_bits) @@ -172,7 +177,7 @@ class RfxtrxBinarySensor(RfxtrxEntity, BinarySensorEntity): else: self._attr_is_on = True - def _apply_event_standard(self, event: rfxtrxmod.RFXtrxEvent): + def _apply_event_standard(self, event: rfxtrxmod.RFXtrxEvent) -> None: assert isinstance(event, (rfxtrxmod.SensorEvent, rfxtrxmod.ControlEvent)) if event.values.get("Command") in COMMAND_ON_LIST: self._attr_is_on = True @@ -183,7 +188,7 @@ class RfxtrxBinarySensor(RfxtrxEntity, BinarySensorEntity): elif event.values.get("Sensor Status") in SENSOR_STATUS_OFF: self._attr_is_on = False - def _apply_event(self, event: rfxtrxmod.RFXtrxEvent): + def _apply_event(self, event: rfxtrxmod.RFXtrxEvent) -> None: """Apply command from rfxtrx.""" super()._apply_event(event) if event.device.packettype == DEVICE_PACKET_TYPE_LIGHTING4: @@ -192,7 +197,9 @@ class RfxtrxBinarySensor(RfxtrxEntity, BinarySensorEntity): self._apply_event_standard(event) @callback - def _handle_event(self, event: rfxtrxmod.RFXtrxEvent, device_id: DeviceTuple): + def _handle_event( + self, event: rfxtrxmod.RFXtrxEvent, device_id: DeviceTuple + ) -> None: """Check if event applies to me and update.""" if not self._event_applies(event, device_id): return @@ -215,7 +222,7 @@ class RfxtrxBinarySensor(RfxtrxEntity, BinarySensorEntity): if self.is_on and self._off_delay is not None: @callback - def off_delay_listener(now): + def off_delay_listener(now: Any) -> None: """Switch device off after a delay.""" self._delay_listener = None self._attr_is_on = False diff --git a/homeassistant/components/rfxtrx/config_flow.py b/homeassistant/components/rfxtrx/config_flow.py index 508ca9a7037..7b5fdc08261 100644 --- a/homeassistant/components/rfxtrx/config_flow.py +++ b/homeassistant/components/rfxtrx/config_flow.py @@ -4,14 +4,14 @@ from __future__ import annotations import copy import itertools import os -from typing import TypedDict, cast +from typing import Any, TypedDict, cast import RFXtrx as rfxtrxmod import serial import serial.tools.list_ports import voluptuous as vol -from homeassistant import config_entries, exceptions +from homeassistant import config_entries, data_entry_flow, exceptions from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_COMMAND_OFF, @@ -64,7 +64,7 @@ class DeviceData(TypedDict): device_id: DeviceTuple -def none_or_int(value, base): +def none_or_int(value: str | None, base: int) -> int | None: """Check if strin is one otherwise convert to int.""" if value is None: return None @@ -80,17 +80,21 @@ class OptionsFlow(config_entries.OptionsFlow): def __init__(self, config_entry: ConfigEntry) -> None: """Initialize rfxtrx options flow.""" self._config_entry = config_entry - self._global_options = None - self._selected_device = None + self._global_options: dict[str, Any] = {} + self._selected_device: dict[str, Any] = {} self._selected_device_entry_id: str | None = None self._selected_device_event_code: str | None = None self._selected_device_object: rfxtrxmod.RFXtrxEvent | None = None - async def async_step_init(self, user_input=None): + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> data_entry_flow.FlowResult: """Manage the options.""" return await self.async_step_prompt_options() - async def async_step_prompt_options(self, user_input=None): + async def async_step_prompt_options( + self, user_input: dict[str, Any] | None = None + ) -> data_entry_flow.FlowResult: """Prompt for options.""" errors = {} @@ -103,7 +107,8 @@ class OptionsFlow(config_entries.OptionsFlow): entry_id = user_input[CONF_DEVICE] device_data = self._get_device_data(entry_id) self._selected_device_entry_id = entry_id - event_code = device_data[CONF_EVENT_CODE] + event_code = device_data["event_code"] + assert event_code self._selected_device_event_code = event_code self._selected_device = self._config_entry.data[CONF_DEVICES][ event_code @@ -111,7 +116,9 @@ class OptionsFlow(config_entries.OptionsFlow): self._selected_device_object = get_rfx_object(event_code) return await self.async_step_set_device_options() if CONF_EVENT_CODE in user_input: - self._selected_device_event_code = user_input[CONF_EVENT_CODE] + self._selected_device_event_code = cast( + str, user_input[CONF_EVENT_CODE] + ) self._selected_device = {} selected_device_object = get_rfx_object( self._selected_device_event_code @@ -159,13 +166,17 @@ class OptionsFlow(config_entries.OptionsFlow): step_id="prompt_options", data_schema=vol.Schema(options), errors=errors ) - async def async_step_set_device_options(self, user_input=None): + async def async_step_set_device_options( + self, user_input: dict[str, Any] | None = None + ) -> data_entry_flow.FlowResult: """Manage device options.""" errors = {} + assert self._selected_device_object + assert self._selected_device_event_code if user_input is not None: - assert self._selected_device_object - assert self._selected_device_event_code + devices: dict[str, dict[str, Any] | None] = {} + device: dict[str, Any] device_id = get_device_id( self._selected_device_object.device, data_bits=user_input.get(CONF_DATA_BITS), @@ -278,16 +289,16 @@ class OptionsFlow(config_entries.OptionsFlow): ), } ) - devices = { + replace_devices = { entry.id: entry.name_by_user if entry.name_by_user else entry.name for entry in self._device_entries if self._can_replace_device(entry.id) } - if devices: + if replace_devices: data_schema.update( { - vol.Optional(CONF_REPLACE_DEVICE): vol.In(devices), + vol.Optional(CONF_REPLACE_DEVICE): vol.In(replace_devices), } ) @@ -297,11 +308,13 @@ class OptionsFlow(config_entries.OptionsFlow): errors=errors, ) - async def _async_replace_device(self, replace_device): + async def _async_replace_device(self, replace_device: str) -> None: """Migrate properties of a device into another.""" device_registry = self._device_registry old_device = self._selected_device_entry_id + assert old_device old_entry = device_registry.async_get(old_device) + assert old_entry device_registry.async_update_device( replace_device, area_id=old_entry.area_id, @@ -343,23 +356,29 @@ class OptionsFlow(config_entries.OptionsFlow): device_registry.async_remove_device(old_device) - def _can_add_device(self, new_rfx_obj): + def _can_add_device(self, new_rfx_obj: rfxtrxmod.RFXtrxEvent) -> bool: """Check if device does not already exist.""" new_device_id = get_device_id(new_rfx_obj.device) for packet_id, entity_info in self._config_entry.data[CONF_DEVICES].items(): rfx_obj = get_rfx_object(packet_id) + assert rfx_obj + device_id = get_device_id(rfx_obj.device, entity_info.get(CONF_DATA_BITS)) if new_device_id == device_id: return False return True - def _can_replace_device(self, entry_id): + def _can_replace_device(self, entry_id: str) -> bool: """Check if device can be replaced with selected device.""" + assert self._selected_device_object + device_data = self._get_device_data(entry_id) - if (event_code := device_data[CONF_EVENT_CODE]) is not None: + if (event_code := device_data["event_code"]) is not None: rfx_obj = get_rfx_object(event_code) + assert rfx_obj + if ( rfx_obj.device.packettype == self._selected_device_object.device.packettype @@ -371,12 +390,12 @@ class OptionsFlow(config_entries.OptionsFlow): return False - def _get_device_event_code(self, entry_id): + def _get_device_event_code(self, entry_id: str) -> str | None: data = self._get_device_data(entry_id) - return data[CONF_EVENT_CODE] + return data["event_code"] - def _get_device_data(self, entry_id) -> DeviceData: + def _get_device_data(self, entry_id: str) -> DeviceData: """Get event code based on device identifier.""" event_code: str | None = None entry = self._device_registry.async_get(entry_id) @@ -390,7 +409,11 @@ class OptionsFlow(config_entries.OptionsFlow): return DeviceData(event_code=event_code, device_id=device_id) @callback - def update_config_data(self, global_options=None, devices=None): + def update_config_data( + self, + global_options: dict[str, Any] | None = None, + devices: dict[str, Any] | None = None, + ) -> None: """Update data in ConfigEntry.""" entry_data = self._config_entry.data.copy() entry_data[CONF_DEVICES] = copy.deepcopy(self._config_entry.data[CONF_DEVICES]) @@ -413,12 +436,14 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> data_entry_flow.FlowResult: """Step when user initializes a integration.""" await self.async_set_unique_id(DOMAIN) self._abort_if_unique_id_configured() - errors = {} + errors: dict[str, str] = {} if user_input is not None: if user_input[CONF_TYPE] == "Serial": return await self.async_step_setup_serial() @@ -430,9 +455,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): schema = vol.Schema({vol.Required(CONF_TYPE): vol.In(list_of_types)}) return self.async_show_form(step_id="user", data_schema=schema, errors=errors) - async def async_step_setup_network(self, user_input=None): + async def async_step_setup_network( + self, user_input: dict[str, Any] | None = None + ) -> data_entry_flow.FlowResult: """Step when setting up network configuration.""" - errors = {} + errors: dict[str, str] = {} if user_input is not None: host = user_input[CONF_HOST] @@ -455,9 +482,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_setup_serial(self, user_input=None): + async def async_step_setup_serial( + self, user_input: dict[str, Any] | None = None + ) -> data_entry_flow.FlowResult: """Step when setting up serial configuration.""" - errors = {} + errors: dict[str, str] = {} if user_input is not None: user_selection = user_input[CONF_DEVICE] @@ -493,9 +522,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_setup_serial_manual_path(self, user_input=None): + async def async_step_setup_serial_manual_path( + self, user_input: dict[str, Any] | None = None + ) -> data_entry_flow.FlowResult: """Select path manually.""" - errors = {} + errors: dict[str, str] = {} if user_input is not None: device = user_input[CONF_DEVICE] @@ -514,7 +545,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_validate_rfx(self, host=None, port=None, device=None): + async def async_validate_rfx( + self, + host: str | None = None, + port: int | None = None, + device: str | None = None, + ) -> dict[str, Any]: """Create data for rfxtrx entry.""" success = await self.hass.async_add_executor_job( _test_transport, host, port, device @@ -522,7 +558,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if not success: raise CannotConnect - data = { + data: dict[str, Any] = { CONF_HOST: host, CONF_PORT: port, CONF_DEVICE: device, @@ -538,7 +574,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return OptionsFlow(config_entry) -def _test_transport(host, port, device): +def _test_transport(host: str | None, port: int | None, device: str | None) -> bool: """Construct a rfx object based on config.""" if port is not None: try: diff --git a/homeassistant/components/rfxtrx/cover.py b/homeassistant/components/rfxtrx/cover.py index 5e1788194f5..532e41ac50c 100644 --- a/homeassistant/components/rfxtrx/cover.py +++ b/homeassistant/components/rfxtrx/cover.py @@ -10,6 +10,7 @@ from homeassistant.components.cover import CoverEntity, CoverEntityFeature from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OPEN from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import DeviceTuple, RfxtrxCommandEntity, async_setup_platform_entry @@ -24,9 +25,9 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -def supported(event: rfxtrxmod.RFXtrxEvent): +def supported(event: rfxtrxmod.RFXtrxEvent) -> bool: """Return whether an event supports cover.""" - return event.device.known_to_be_rollershutter + return bool(event.device.known_to_be_rollershutter) async def async_setup_entry( @@ -40,8 +41,8 @@ async def async_setup_entry( event: rfxtrxmod.RFXtrxEvent, auto: rfxtrxmod.RFXtrxEvent | None, device_id: DeviceTuple, - entity_info: dict, - ): + entity_info: dict[str, Any], + ) -> list[Entity]: return [ RfxtrxCover( event.device, @@ -144,7 +145,7 @@ class RfxtrxCover(RfxtrxCommandEntity, CoverEntity): self._attr_is_closed = False self.async_write_ha_state() - def _apply_event(self, event: rfxtrxmod.RFXtrxEvent): + def _apply_event(self, event: rfxtrxmod.RFXtrxEvent) -> None: """Apply command from rfxtrx.""" assert isinstance(event, rfxtrxmod.ControlEvent) super()._apply_event(event) @@ -154,7 +155,9 @@ class RfxtrxCover(RfxtrxCommandEntity, CoverEntity): self._attr_is_closed = True @callback - def _handle_event(self, event: rfxtrxmod.RFXtrxEvent, device_id: DeviceTuple): + def _handle_event( + self, event: rfxtrxmod.RFXtrxEvent, device_id: DeviceTuple + ) -> None: """Check if event applies to me and update.""" if device_id != self._device_id: return diff --git a/homeassistant/components/rfxtrx/device_action.py b/homeassistant/components/rfxtrx/device_action.py index e8fc8c6707d..15595b88cd2 100644 --- a/homeassistant/components/rfxtrx/device_action.py +++ b/homeassistant/components/rfxtrx/device_action.py @@ -1,6 +1,8 @@ """Provides device automations for RFXCOM RFXtrx.""" from __future__ import annotations +from collections.abc import Callable + import voluptuous as vol from homeassistant.components.device_automation.exceptions import ( @@ -65,7 +67,9 @@ async def async_get_actions( return actions -def _get_commands(hass, device_id, action_type): +def _get_commands( + hass: HomeAssistant, device_id: str, action_type: str +) -> tuple[dict[str, str], Callable[..., None]]: device = async_get_device_object(hass, device_id) send_fun = getattr(device, action_type) commands = getattr(device, ACTION_SELECTION[action_type], {}) diff --git a/homeassistant/components/rfxtrx/helpers.py b/homeassistant/components/rfxtrx/helpers.py index 7e567cff1cd..2badc6d4ca5 100644 --- a/homeassistant/components/rfxtrx/helpers.py +++ b/homeassistant/components/rfxtrx/helpers.py @@ -1,14 +1,14 @@ """Provides helpers for RFXtrx.""" -from RFXtrx import get_device +from RFXtrx import RFXtrxDevice, get_device from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr @callback -def async_get_device_object(hass: HomeAssistant, device_id): +def async_get_device_object(hass: HomeAssistant, device_id: str) -> RFXtrxDevice: """Get a device for the given device registry id.""" device_registry = dr.async_get(hass) registry_device = device_registry.async_get(device_id) diff --git a/homeassistant/components/rfxtrx/light.py b/homeassistant/components/rfxtrx/light.py index 7f32ee0bc83..ad84515d41d 100644 --- a/homeassistant/components/rfxtrx/light.py +++ b/homeassistant/components/rfxtrx/light.py @@ -10,6 +10,7 @@ from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEnti from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_ON from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import DeviceTuple, RfxtrxCommandEntity, async_setup_platform_entry @@ -18,7 +19,7 @@ from .const import COMMAND_OFF_LIST, COMMAND_ON_LIST _LOGGER = logging.getLogger(__name__) -def supported(event: rfxtrxmod.RFXtrxEvent): +def supported(event: rfxtrxmod.RFXtrxEvent) -> bool: """Return whether an event supports light.""" return ( isinstance(event.device, rfxtrxmod.LightingDevice) @@ -37,8 +38,8 @@ async def async_setup_entry( event: rfxtrxmod.RFXtrxEvent, auto: rfxtrxmod.RFXtrxEvent | None, device_id: DeviceTuple, - entity_info: dict, - ): + entity_info: dict[str, Any], + ) -> list[Entity]: return [ RfxtrxLight( event.device, @@ -91,7 +92,7 @@ class RfxtrxLight(RfxtrxCommandEntity, LightEntity): self._attr_brightness = 0 self.async_write_ha_state() - def _apply_event(self, event: rfxtrxmod.RFXtrxEvent): + def _apply_event(self, event: rfxtrxmod.RFXtrxEvent) -> None: """Apply command from rfxtrx.""" assert isinstance(event, rfxtrxmod.ControlEvent) super()._apply_event(event) @@ -105,7 +106,9 @@ class RfxtrxLight(RfxtrxCommandEntity, LightEntity): self._attr_is_on = brightness > 0 @callback - def _handle_event(self, event, device_id): + def _handle_event( + self, event: rfxtrxmod.RFXtrxEvent, device_id: DeviceTuple + ) -> None: """Check if event applies to me and update.""" if device_id != self._device_id: return diff --git a/homeassistant/components/rfxtrx/sensor.py b/homeassistant/components/rfxtrx/sensor.py index c3524b022f8..b4d4d65295c 100644 --- a/homeassistant/components/rfxtrx/sensor.py +++ b/homeassistant/components/rfxtrx/sensor.py @@ -3,9 +3,12 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass +from datetime import date, datetime +from decimal import Decimal import logging +from typing import Any, cast -from RFXtrx import ControlEvent, RFXtrxEvent, SensorEvent +from RFXtrx import ControlEvent, RFXtrxDevice, RFXtrxEvent, SensorEvent from homeassistant.components.sensor import ( SensorDeviceClass, @@ -30,8 +33,9 @@ from homeassistant.const import ( UV_INDEX, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity import Entity, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType from . import DeviceTuple, RfxtrxEntity, async_setup_platform_entry, get_rfx_object from .const import ATTR_EVENT @@ -39,14 +43,14 @@ from .const import ATTR_EVENT _LOGGER = logging.getLogger(__name__) -def _battery_convert(value): +def _battery_convert(value: int | None) -> int | None: """Battery is given as a value between 0 and 9.""" if value is None: return None return (value + 1) * 10 -def _rssi_convert(value): +def _rssi_convert(value: int | None) -> str | None: """Rssi is given as dBm value.""" if value is None: return None @@ -57,7 +61,9 @@ def _rssi_convert(value): class RfxtrxSensorEntityDescription(SensorEntityDescription): """Description of sensor entities.""" - convert: Callable = lambda x: x + convert: Callable[[Any], StateType | date | datetime | Decimal] = lambda x: cast( + StateType, x + ) SENSOR_TYPES = ( @@ -243,16 +249,16 @@ async def async_setup_entry( ) -> None: """Set up config entry.""" - def _supported(event): + def _supported(event: RFXtrxEvent) -> bool: return isinstance(event, (ControlEvent, SensorEvent)) def _constructor( event: RFXtrxEvent, auto: RFXtrxEvent | None, device_id: DeviceTuple, - entity_info: dict, - ): - entities: list[RfxtrxSensor] = [] + entity_info: dict[str, Any], + ) -> list[Entity]: + entities: list[Entity] = [] for data_type in set(event.values) & set(SENSOR_TYPES_DICT): entities.append( RfxtrxSensor( @@ -271,14 +277,22 @@ async def async_setup_entry( class RfxtrxSensor(RfxtrxEntity, SensorEntity): - """Representation of a RFXtrx sensor.""" + """Representation of a RFXtrx sensor. + + Since all repeated events have meaning, these types of sensors + need to have force update enabled. + """ _attr_force_update = True - """We should force updates. Repeated states have meaning.""" - entity_description: RfxtrxSensorEntityDescription - def __init__(self, device, device_id, entity_description, event=None): + def __init__( + self, + device: RFXtrxDevice, + device_id: DeviceTuple, + entity_description: RfxtrxSensorEntityDescription, + event: RFXtrxEvent | None = None, + ) -> None: """Initialize the sensor.""" super().__init__(device, device_id, event=event) self.entity_description = entity_description @@ -296,7 +310,7 @@ class RfxtrxSensor(RfxtrxEntity, SensorEntity): self._apply_event(get_rfx_object(event)) @property - def native_value(self): + def native_value(self) -> StateType | date | datetime | Decimal: """Return the state of the sensor.""" if not self._event: return None @@ -304,7 +318,7 @@ class RfxtrxSensor(RfxtrxEntity, SensorEntity): return self.entity_description.convert(value) @callback - def _handle_event(self, event, device_id): + def _handle_event(self, event: RFXtrxEvent, device_id: DeviceTuple) -> None: """Check if event applies to me and update.""" if device_id != self._device_id: return diff --git a/homeassistant/components/rfxtrx/siren.py b/homeassistant/components/rfxtrx/siren.py index 0b49a7d4d8c..c9f10febb6b 100644 --- a/homeassistant/components/rfxtrx/siren.py +++ b/homeassistant/components/rfxtrx/siren.py @@ -58,8 +58,8 @@ async def async_setup_entry( event: rfxtrxmod.RFXtrxEvent, auto: rfxtrxmod.RFXtrxEvent | None, device_id: DeviceTuple, - entity_info: dict, - ): + entity_info: dict[str, Any], + ) -> list[Entity]: """Construct a entity from an event.""" device = event.device @@ -85,6 +85,7 @@ async def async_setup_entry( auto, ) ] + return [] await async_setup_platform_entry( hass, config_entry, async_add_entities, supported, _constructor diff --git a/homeassistant/components/rfxtrx/switch.py b/homeassistant/components/rfxtrx/switch.py index c73b0ba3b1d..edc34aeb80d 100644 --- a/homeassistant/components/rfxtrx/switch.py +++ b/homeassistant/components/rfxtrx/switch.py @@ -10,6 +10,7 @@ from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_COMMAND_OFF, CONF_COMMAND_ON, STATE_ON from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import ( @@ -31,7 +32,7 @@ DATA_SWITCH = f"{DOMAIN}_switch" _LOGGER = logging.getLogger(__name__) -def supported(event): +def supported(event: rfxtrxmod.RFXtrxEvent) -> bool: """Return whether an event supports switch.""" return ( isinstance(event.device, rfxtrxmod.LightingDevice) @@ -52,8 +53,8 @@ async def async_setup_entry( event: rfxtrxmod.RFXtrxEvent, auto: rfxtrxmod.RFXtrxEvent | None, device_id: DeviceTuple, - entity_info: dict, - ): + entity_info: dict[str, Any], + ) -> list[Entity]: return [ RfxtrxSwitch( event.device, @@ -97,7 +98,7 @@ class RfxtrxSwitch(RfxtrxCommandEntity, SwitchEntity): if old_state is not None: self._attr_is_on = old_state.state == STATE_ON - def _apply_event_lighting4(self, event: rfxtrxmod.RFXtrxEvent): + def _apply_event_lighting4(self, event: rfxtrxmod.RFXtrxEvent) -> None: """Apply event for a lighting 4 device.""" if self._data_bits is not None: cmdstr = get_pt2262_cmd(event.device.id_string, self._data_bits) diff --git a/mypy.ini b/mypy.ini index 827d47d12ba..c77934d958a 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1882,6 +1882,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.rfxtrx.*] +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.rhasspy.*] check_untyped_defs = true disallow_incomplete_defs = true From 5477ebdb134e6f1b637e23ed519d3d3ab6ad702d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 23 Sep 2022 05:06:24 -1000 Subject: [PATCH 673/955] Avoid creating iBeacon trackers when the device has no name (#78983) --- .../components/ibeacon/coordinator.py | 8 ++- tests/components/ibeacon/__init__.py | 9 +++ tests/components/ibeacon/test_coordinator.py | 56 ++++++++++++++++++- 3 files changed, 71 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/ibeacon/coordinator.py b/homeassistant/components/ibeacon/coordinator.py index 3a18c77678a..0b813eca933 100644 --- a/homeassistant/components/ibeacon/coordinator.py +++ b/homeassistant/components/ibeacon/coordinator.py @@ -67,7 +67,7 @@ def async_name( base_name = service_info.name if unique_address: short_address = make_short_address(service_info.address) - if not base_name.endswith(short_address): + if not base_name.upper().endswith(short_address): return f"{base_name} {short_address}" return base_name @@ -233,6 +233,12 @@ class IBeaconCoordinator: address = service_info.address unique_id = f"{group_id}_{address}" new = unique_id not in self._last_rssi_by_unique_id + # Reject creating new trackers if the name is not set + if new and ( + service_info.device.name is None + or service_info.device.name.replace("-", ":") == service_info.device.address + ): + return self._last_rssi_by_unique_id[unique_id] = service_info.rssi self._async_track_ibeacon_with_unique_address(address, group_id, unique_id) if address not in self._unavailable_trackers: diff --git a/tests/components/ibeacon/__init__.py b/tests/components/ibeacon/__init__.py index f9b2c1576ad..f1b8928f67b 100644 --- a/tests/components/ibeacon/__init__.py +++ b/tests/components/ibeacon/__init__.py @@ -28,6 +28,15 @@ BLUECHARM_BEACON_SERVICE_INFO_2 = BluetoothServiceInfo( service_uuids=["0000feaa-0000-1000-8000-00805f9b34fb"], source="local", ) +BLUECHARM_BEACON_SERVICE_INFO_DBUS = BluetoothServiceInfo( + name="BlueCharm_177999", + address="AA:BB:CC:DD:EE:FF", + rssi=-63, + service_data={}, + manufacturer_data={76: b"\x02\x15BlueCharmBeacons\x0e\xfe\x13U\xc5"}, + service_uuids=[], + source="local", +) NO_NAME_BEACON_SERVICE_INFO = BluetoothServiceInfo( name="61DE521B-F0BF-9F44-64D4-75BBE1738105", address="61DE521B-F0BF-9F44-64D4-75BBE1738105", diff --git a/tests/components/ibeacon/test_coordinator.py b/tests/components/ibeacon/test_coordinator.py index a732b8ec2d3..cb7e0bdefc8 100644 --- a/tests/components/ibeacon/test_coordinator.py +++ b/tests/components/ibeacon/test_coordinator.py @@ -8,7 +8,7 @@ import pytest from homeassistant.components.ibeacon.const import DOMAIN from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo -from . import BLUECHARM_BEACON_SERVICE_INFO +from . import BLUECHARM_BEACON_SERVICE_INFO, BLUECHARM_BEACON_SERVICE_INFO_DBUS from tests.common import MockConfigEntry from tests.components.bluetooth import inject_bluetooth_service_info @@ -73,3 +73,57 @@ async def test_ignore_not_ibeacons(hass): ) await hass.async_block_till_done() assert len(hass.states.async_entity_ids()) == before_entity_count + + +async def test_ignore_no_name_but_create_if_set_later(hass): + """Test we ignore devices with no name but create it if it set set later.""" + entry = MockConfigEntry( + domain=DOMAIN, + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + before_entity_count = len(hass.states.async_entity_ids()) + inject_bluetooth_service_info( + hass, + replace(BLUECHARM_BEACON_SERVICE_INFO, name=None), + ) + await hass.async_block_till_done() + assert len(hass.states.async_entity_ids()) == before_entity_count + + inject_bluetooth_service_info( + hass, + replace( + BLUECHARM_BEACON_SERVICE_INFO, + service_data={ + "00002080-0000-1000-8000-00805f9b34fb": b"j\x0c\x0e\xfe\x13U", + "0000feaa-0000-1000-8000-00805f9b34fb": b" \x00\x0c\x00\x1c\x00\x00\x00\x06h\x00\x008\x10", + }, + ), + ) + await hass.async_block_till_done() + assert len(hass.states.async_entity_ids()) > before_entity_count + + +async def test_ignore_default_name(hass): + """Test we ignore devices with default name.""" + entry = MockConfigEntry( + domain=DOMAIN, + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + before_entity_count = len(hass.states.async_entity_ids()) + inject_bluetooth_service_info( + hass, + replace( + BLUECHARM_BEACON_SERVICE_INFO_DBUS, + name=BLUECHARM_BEACON_SERVICE_INFO_DBUS.address, + ), + ) + await hass.async_block_till_done() + assert len(hass.states.async_entity_ids()) == before_entity_count From ffd88ab50ed765e2ff8defe4d11acb6ce91cc044 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Fri, 23 Sep 2022 16:09:10 +0100 Subject: [PATCH 674/955] Enable Thread transport in homekit_controller (#78994) --- 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 7ecd54e0a79..79dbca9109c 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==1.5.12"], + "requirements": ["aiohomekit==2.0.0"], "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 39ac68b68c0..fe15419d7db 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==1.5.12 +aiohomekit==2.0.0 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 50fd38c35e3..a38c27925e4 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==1.5.12 +aiohomekit==2.0.0 # homeassistant.components.emulated_hue # homeassistant.components.http From 62022a2657e5b3545e4c44c5d8f8617ba0961d52 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 23 Sep 2022 17:32:59 +0200 Subject: [PATCH 675/955] Increase code coverage for migrated utilities (#78990) Increase code coverage for migrated utilites --- tests/util/test_temperature.py | 17 +++++++++++++++++ tests/util/test_volume.py | 17 +++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/tests/util/test_temperature.py b/tests/util/test_temperature.py index 7730a89cbb8..71693683fa3 100644 --- a/tests/util/test_temperature.py +++ b/tests/util/test_temperature.py @@ -8,6 +8,23 @@ INVALID_SYMBOL = "bob" VALID_SYMBOL = TEMP_CELSIUS +@pytest.mark.parametrize( + "function_name, value, expected", + [ + ("fahrenheit_to_celsius", 75.2, 24), + ("kelvin_to_celsius", 297.65, 24.5), + ("celsius_to_fahrenheit", 23, 73.4), + ("celsius_to_kelvin", 23, 296.15), + ], +) +def test_deprecated_functions( + function_name: str, value: float, expected: float +) -> None: + """Test that deprecated function still work.""" + convert = getattr(temperature_util, function_name) + assert convert(value) == expected + + def test_convert_same_unit(): """Test conversion from any unit to same unit.""" assert temperature_util.convert(2, TEMP_CELSIUS, TEMP_CELSIUS) == 2 diff --git a/tests/util/test_volume.py b/tests/util/test_volume.py index f78d6c4ed18..5362f3099fb 100644 --- a/tests/util/test_volume.py +++ b/tests/util/test_volume.py @@ -16,6 +16,23 @@ INVALID_SYMBOL = "bob" VALID_SYMBOL = VOLUME_LITERS +@pytest.mark.parametrize( + "function_name, value, expected", + [ + ("liter_to_gallon", 2, pytest.approx(0.528344)), + ("gallon_to_liter", 2, 7.570823568), + ("cubic_meter_to_cubic_feet", 2, pytest.approx(70.629333)), + ("cubic_feet_to_cubic_meter", 2, pytest.approx(0.0566337)), + ], +) +def test_deprecated_functions( + function_name: str, value: float, expected: float +) -> None: + """Test that deprecated function still work.""" + convert = getattr(volume_util, function_name) + assert convert(value) == expected + + def test_convert_same_unit(): """Test conversion from any unit to same unit.""" assert volume_util.convert(2, VOLUME_LITERS, VOLUME_LITERS) == 2 From dd7a06b9dca8a04152f6c4ef4828c8e214260393 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 23 Sep 2022 17:33:32 +0200 Subject: [PATCH 676/955] Use unit_conversion in components (#78991) --- .../components/google_assistant/trait.py | 23 +++++++++++-------- homeassistant/components/homekit/util.py | 6 ++--- homeassistant/components/smhi/weather.py | 5 ++-- homeassistant/components/template/weather.py | 20 ++++++++-------- homeassistant/components/weather/__init__.py | 20 ++++++++-------- 5 files changed, 39 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index d7b6b45de87..a76b0a7d687 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -68,11 +68,12 @@ from homeassistant.const import ( ) from homeassistant.core import DOMAIN as HA_DOMAIN from homeassistant.helpers.network import get_url -from homeassistant.util import color as color_util, dt, temperature as temp_util +from homeassistant.util import color as color_util, dt from homeassistant.util.percentage import ( ordered_list_item_to_percentage, percentage_to_ordered_list_item, ) +from homeassistant.util.unit_conversion import TemperatureConverter from .const import ( CHALLENGE_ACK_NEEDED, @@ -843,7 +844,9 @@ class TemperatureControlTrait(_Trait): unit = self.hass.config.units.temperature_unit current_temp = self.state.state if current_temp not in (STATE_UNKNOWN, STATE_UNAVAILABLE): - temp = round(temp_util.convert(float(current_temp), unit, TEMP_CELSIUS), 1) + temp = round( + TemperatureConverter.convert(float(current_temp), unit, TEMP_CELSIUS), 1 + ) response["temperatureSetpointCelsius"] = temp response["temperatureAmbientCelsius"] = temp @@ -948,7 +951,7 @@ class TemperatureSettingTrait(_Trait): current_temp = attrs.get(climate.ATTR_CURRENT_TEMPERATURE) if current_temp is not None: response["thermostatTemperatureAmbient"] = round( - temp_util.convert(current_temp, unit, TEMP_CELSIUS), 1 + TemperatureConverter.convert(current_temp, unit, TEMP_CELSIUS), 1 ) current_humidity = attrs.get(climate.ATTR_CURRENT_HUMIDITY) @@ -958,13 +961,13 @@ class TemperatureSettingTrait(_Trait): if operation in (climate.HVACMode.AUTO, climate.HVACMode.HEAT_COOL): if supported & climate.SUPPORT_TARGET_TEMPERATURE_RANGE: response["thermostatTemperatureSetpointHigh"] = round( - temp_util.convert( + TemperatureConverter.convert( attrs[climate.ATTR_TARGET_TEMP_HIGH], unit, TEMP_CELSIUS ), 1, ) response["thermostatTemperatureSetpointLow"] = round( - temp_util.convert( + TemperatureConverter.convert( attrs[climate.ATTR_TARGET_TEMP_LOW], unit, TEMP_CELSIUS ), 1, @@ -972,14 +975,14 @@ class TemperatureSettingTrait(_Trait): else: if (target_temp := attrs.get(ATTR_TEMPERATURE)) is not None: target_temp = round( - temp_util.convert(target_temp, unit, TEMP_CELSIUS), 1 + TemperatureConverter.convert(target_temp, unit, TEMP_CELSIUS), 1 ) response["thermostatTemperatureSetpointHigh"] = target_temp response["thermostatTemperatureSetpointLow"] = target_temp else: if (target_temp := attrs.get(ATTR_TEMPERATURE)) is not None: response["thermostatTemperatureSetpoint"] = round( - temp_util.convert(target_temp, unit, TEMP_CELSIUS), 1 + TemperatureConverter.convert(target_temp, unit, TEMP_CELSIUS), 1 ) return response @@ -992,7 +995,7 @@ class TemperatureSettingTrait(_Trait): max_temp = self.state.attributes[climate.ATTR_MAX_TEMP] if command == COMMAND_THERMOSTAT_TEMPERATURE_SETPOINT: - temp = temp_util.convert( + temp = TemperatureConverter.convert( params["thermostatTemperatureSetpoint"], TEMP_CELSIUS, unit ) if unit == TEMP_FAHRENHEIT: @@ -1013,7 +1016,7 @@ class TemperatureSettingTrait(_Trait): ) elif command == COMMAND_THERMOSTAT_TEMPERATURE_SET_RANGE: - temp_high = temp_util.convert( + temp_high = TemperatureConverter.convert( params["thermostatTemperatureSetpointHigh"], TEMP_CELSIUS, unit ) if unit == TEMP_FAHRENHEIT: @@ -1028,7 +1031,7 @@ class TemperatureSettingTrait(_Trait): ), ) - temp_low = temp_util.convert( + temp_low = TemperatureConverter.convert( params["thermostatTemperatureSetpointLow"], TEMP_CELSIUS, unit ) if unit == TEMP_FAHRENHEIT: diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index b7af7d516dd..445b73cccbe 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -40,7 +40,7 @@ from homeassistant.const import ( from homeassistant.core import Event, HomeAssistant, State, callback, split_entity_id import homeassistant.helpers.config_validation as cv from homeassistant.helpers.storage import STORAGE_DIR -import homeassistant.util.temperature as temp_util +from homeassistant.util.unit_conversion import TemperatureConverter from .const import ( AUDIO_CODEC_COPY, @@ -391,12 +391,12 @@ def cleanup_name_for_homekit(name: str | None) -> str: def temperature_to_homekit(temperature: float | int, unit: str) -> float: """Convert temperature to Celsius for HomeKit.""" - return round(temp_util.convert(temperature, unit, TEMP_CELSIUS), 1) + return round(TemperatureConverter.convert(temperature, unit, TEMP_CELSIUS), 1) def temperature_to_states(temperature: float | int, unit: str) -> float: """Convert temperature back from Celsius to Home Assistant unit.""" - return round(temp_util.convert(temperature, TEMP_CELSIUS, unit) * 2) / 2 + return round(TemperatureConverter.convert(temperature, TEMP_CELSIUS, unit) * 2) / 2 def density_to_air_quality(density: float) -> int: diff --git a/homeassistant/components/smhi/weather.py b/homeassistant/components/smhi/weather.py index f8afcb7b59a..a02a627b7f2 100644 --- a/homeassistant/components/smhi/weather.py +++ b/homeassistant/components/smhi/weather.py @@ -57,7 +57,8 @@ from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_call_later -from homeassistant.util import Throttle, slugify, speed as speed_util +from homeassistant.util import Throttle, slugify +from homeassistant.util.unit_conversion import SpeedConverter from .const import ( ATTR_SMHI_CLOUDINESS, @@ -155,7 +156,7 @@ class SmhiWeather(WeatherEntity): def extra_state_attributes(self) -> Mapping[str, Any] | None: """Return additional attributes.""" if self._forecasts: - wind_gust = speed_util.convert( + wind_gust = SpeedConverter.convert( self._forecasts[0].wind_gust, SPEED_METERS_PER_SECOND, self._wind_speed_unit, diff --git a/homeassistant/components/template/weather.py b/homeassistant/components/template/weather.py index d65e03b9656..9c1680d1a5d 100644 --- a/homeassistant/components/template/weather.py +++ b/homeassistant/components/template/weather.py @@ -30,11 +30,11 @@ from homeassistant.helpers.config_validation import PLATFORM_SCHEMA from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.util import ( - distance as distance_util, - pressure as pressure_util, - speed as speed_util, - temperature as temp_util, +from homeassistant.util.unit_conversion import ( + DistanceConverter, + PressureConverter, + SpeedConverter, + TemperatureConverter, ) from .template_entity import TemplateEntity, rewrite_common_legacy_to_modern_conf @@ -87,11 +87,11 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_VISIBILITY_TEMPLATE): cv.template, vol.Optional(CONF_FORECAST_TEMPLATE): cv.template, vol.Optional(CONF_UNIQUE_ID): cv.string, - vol.Optional(CONF_TEMPERATURE_UNIT): vol.In(temp_util.VALID_UNITS), - vol.Optional(CONF_PRESSURE_UNIT): vol.In(pressure_util.VALID_UNITS), - vol.Optional(CONF_WIND_SPEED_UNIT): vol.In(speed_util.VALID_UNITS), - vol.Optional(CONF_VISIBILITY_UNIT): vol.In(distance_util.VALID_UNITS), - vol.Optional(CONF_PRECIPITATION_UNIT): vol.In(distance_util.VALID_UNITS), + vol.Optional(CONF_TEMPERATURE_UNIT): vol.In(TemperatureConverter.VALID_UNITS), + vol.Optional(CONF_PRESSURE_UNIT): vol.In(PressureConverter.VALID_UNITS), + vol.Optional(CONF_WIND_SPEED_UNIT): vol.In(SpeedConverter.VALID_UNITS), + vol.Optional(CONF_VISIBILITY_UNIT): vol.In(DistanceConverter.VALID_UNITS), + vol.Optional(CONF_PRECIPITATION_UNIT): vol.In(DistanceConverter.VALID_UNITS), } ) diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index b28cd143b20..bb358e8d980 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -38,11 +38,11 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType -from homeassistant.util import ( - distance as distance_util, - pressure as pressure_util, - speed as speed_util, - temperature as temperature_util, +from homeassistant.util.unit_conversion import ( + DistanceConverter, + PressureConverter, + SpeedConverter, + TemperatureConverter, ) _LOGGER = logging.getLogger(__name__) @@ -126,11 +126,11 @@ VALID_UNITS_WIND_SPEED: tuple[str, ...] = ( ) UNIT_CONVERSIONS: dict[str, Callable[[float, str, str], float]] = { - ATTR_WEATHER_PRESSURE_UNIT: pressure_util.convert, - ATTR_WEATHER_TEMPERATURE_UNIT: temperature_util.convert, - ATTR_WEATHER_VISIBILITY_UNIT: distance_util.convert, - ATTR_WEATHER_PRECIPITATION_UNIT: distance_util.convert, - ATTR_WEATHER_WIND_SPEED_UNIT: speed_util.convert, + ATTR_WEATHER_PRESSURE_UNIT: PressureConverter.convert, + ATTR_WEATHER_TEMPERATURE_UNIT: TemperatureConverter.convert, + ATTR_WEATHER_VISIBILITY_UNIT: DistanceConverter.convert, + ATTR_WEATHER_PRECIPITATION_UNIT: DistanceConverter.convert, + ATTR_WEATHER_WIND_SPEED_UNIT: SpeedConverter.convert, } VALID_UNITS: dict[str, tuple[str, ...]] = { From e6f567a751291ed97495a1d9d57da4939b255af6 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 23 Sep 2022 18:44:52 +0200 Subject: [PATCH 677/955] Use device class duration for relevant Xiaomi Miio sensors (#78974) --- homeassistant/components/xiaomi_miio/sensor.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index 6a84874e2e6..c94e0e371fb 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -235,6 +235,7 @@ SENSOR_TYPES = { name="Use time", native_unit_of_measurement=TIME_SECONDS, icon="mdi:progress-clock", + device_class=SensorDeviceClass.DURATION, state_class=SensorStateClass.TOTAL_INCREASING, entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, @@ -294,6 +295,7 @@ SENSOR_TYPES = { name="Filter use", native_unit_of_measurement=TIME_HOURS, icon="mdi:clock-outline", + device_class=SensorDeviceClass.DURATION, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, ), @@ -302,6 +304,7 @@ SENSOR_TYPES = { name="Filter time left", native_unit_of_measurement=TIME_DAYS, icon="mdi:clock-outline", + device_class=SensorDeviceClass.DURATION, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, ), @@ -319,6 +322,7 @@ SENSOR_TYPES = { name="Dust filter life remaining days", native_unit_of_measurement=TIME_DAYS, icon="mdi:clock-outline", + device_class=SensorDeviceClass.DURATION, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, ), @@ -336,6 +340,7 @@ SENSOR_TYPES = { name="Upper filter life remaining days", native_unit_of_measurement=TIME_DAYS, icon="mdi:clock-outline", + device_class=SensorDeviceClass.DURATION, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, ), @@ -572,6 +577,7 @@ VACUUM_SENSORS = { f"last_clean_{ATTR_LAST_CLEAN_TIME}": XiaomiMiioSensorDescription( native_unit_of_measurement=TIME_SECONDS, icon="mdi:timer-sand", + device_class=SensorDeviceClass.DURATION, key=ATTR_LAST_CLEAN_TIME, parent_key=VacuumCoordinatorDataAttributes.last_clean_details, name="Last clean duration", @@ -588,6 +594,7 @@ VACUUM_SENSORS = { f"current_{ATTR_STATUS_CLEAN_TIME}": XiaomiMiioSensorDescription( native_unit_of_measurement=TIME_SECONDS, icon="mdi:timer-sand", + device_class=SensorDeviceClass.DURATION, key=ATTR_STATUS_CLEAN_TIME, parent_key=VacuumCoordinatorDataAttributes.status, name="Current clean duration", @@ -603,6 +610,7 @@ VACUUM_SENSORS = { ), f"clean_history_{ATTR_CLEAN_HISTORY_TOTAL_DURATION}": XiaomiMiioSensorDescription( native_unit_of_measurement=TIME_SECONDS, + device_class=SensorDeviceClass.DURATION, icon="mdi:timer-sand", key=ATTR_CLEAN_HISTORY_TOTAL_DURATION, parent_key=VacuumCoordinatorDataAttributes.clean_history_status, @@ -642,6 +650,7 @@ VACUUM_SENSORS = { f"consumable_{ATTR_CONSUMABLE_STATUS_MAIN_BRUSH_LEFT}": XiaomiMiioSensorDescription( native_unit_of_measurement=TIME_SECONDS, icon="mdi:brush", + device_class=SensorDeviceClass.DURATION, key=ATTR_CONSUMABLE_STATUS_MAIN_BRUSH_LEFT, parent_key=VacuumCoordinatorDataAttributes.consumable_status, name="Main brush left", @@ -650,6 +659,7 @@ VACUUM_SENSORS = { f"consumable_{ATTR_CONSUMABLE_STATUS_SIDE_BRUSH_LEFT}": XiaomiMiioSensorDescription( native_unit_of_measurement=TIME_SECONDS, icon="mdi:brush", + device_class=SensorDeviceClass.DURATION, key=ATTR_CONSUMABLE_STATUS_SIDE_BRUSH_LEFT, parent_key=VacuumCoordinatorDataAttributes.consumable_status, name="Side brush left", @@ -658,6 +668,7 @@ VACUUM_SENSORS = { f"consumable_{ATTR_CONSUMABLE_STATUS_FILTER_LEFT}": XiaomiMiioSensorDescription( native_unit_of_measurement=TIME_SECONDS, icon="mdi:air-filter", + device_class=SensorDeviceClass.DURATION, key=ATTR_CONSUMABLE_STATUS_FILTER_LEFT, parent_key=VacuumCoordinatorDataAttributes.consumable_status, name="Filter left", @@ -666,6 +677,7 @@ VACUUM_SENSORS = { f"consumable_{ATTR_CONSUMABLE_STATUS_SENSOR_DIRTY_LEFT}": XiaomiMiioSensorDescription( native_unit_of_measurement=TIME_SECONDS, icon="mdi:eye-outline", + device_class=SensorDeviceClass.DURATION, key=ATTR_CONSUMABLE_STATUS_SENSOR_DIRTY_LEFT, parent_key=VacuumCoordinatorDataAttributes.consumable_status, name="Sensor dirty left", From d39ed0cde4d5e9d34849fb1df2d0ea1f32d4e1e2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 23 Sep 2022 13:03:28 -0400 Subject: [PATCH 678/955] Remove unused custom data in Google Assistant (#79003) --- homeassistant/components/google_assistant/helpers.py | 7 ++----- tests/components/google_assistant/test_diagnostics.py | 6 ------ tests/components/google_assistant/test_helpers.py | 5 +---- 3 files changed, 3 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index 0b9b12f2f4c..3351af94648 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -11,6 +11,7 @@ import pprint from aiohttp.web import json_response from awesomeversion import AwesomeVersion +from yarl import URL from homeassistant.components import webhook from homeassistant.const import ( @@ -610,12 +611,8 @@ class GoogleEntity: device["otherDeviceIds"] = [{"deviceId": self.entity_id}] device["customData"] = { "webhookId": self.config.get_local_webhook_id(agent_user_id), - "httpPort": self.hass.http.server_port, + "httpPort": URL(get_url(self.hass, allow_external=False)).port, "uuid": instance_uuid, - # Below can be removed in HA 2022.9 - "httpSSL": self.hass.config.api.use_ssl, - "baseUrl": get_url(self.hass, prefer_external=True), - "proxyDeviceId": agent_user_id, } # Add trait sync attributes diff --git a/tests/components/google_assistant/test_diagnostics.py b/tests/components/google_assistant/test_diagnostics.py index 99a091f0bdc..9bb6f6955be 100644 --- a/tests/components/google_assistant/test_diagnostics.py +++ b/tests/components/google_assistant/test_diagnostics.py @@ -53,10 +53,7 @@ async def test_diagnostics(hass: core.HomeAssistant, hass_client: Any): "type": "action.devices.types.SWITCH", "willReportState": False, "customData": { - "baseUrl": "**REDACTED**", "httpPort": 8123, - "httpSSL": False, - "proxyDeviceId": "**REDACTED**", "uuid": "**REDACTED**", "webhookId": None, }, @@ -70,10 +67,7 @@ async def test_diagnostics(hass: core.HomeAssistant, hass_client: Any): "type": "action.devices.types.OUTLET", "willReportState": False, "customData": { - "baseUrl": "**REDACTED**", "httpPort": 8123, - "httpSSL": False, - "proxyDeviceId": "**REDACTED**", "uuid": "**REDACTED**", "webhookId": None, }, diff --git a/tests/components/google_assistant/test_helpers.py b/tests/components/google_assistant/test_helpers.py index 8898fc7ef76..3a455802054 100644 --- a/tests/components/google_assistant/test_helpers.py +++ b/tests/components/google_assistant/test_helpers.py @@ -30,7 +30,7 @@ from tests.common import ( async def test_google_entity_sync_serialize_with_local_sdk(hass): """Test sync serialize attributes of a GoogleEntity.""" hass.states.async_set("light.ceiling_lights", "off") - hass.config.api = Mock(port=1234, use_ssl=False) + hass.config.api = Mock(port=1234, local_ip="192.168.123.123", use_ssl=False) await async_process_ha_core_config( hass, {"external_url": "https://hostname:1234"}, @@ -57,10 +57,7 @@ async def test_google_entity_sync_serialize_with_local_sdk(hass): assert serialized["otherDeviceIds"] == [{"deviceId": "light.ceiling_lights"}] assert serialized["customData"] == { "httpPort": 1234, - "httpSSL": False, - "proxyDeviceId": "mock-user-id", "webhookId": "mock-webhook-id", - "baseUrl": "https://hostname:1234", "uuid": "abcdef", } From 81514b0d1cb4c19f5eeef3b1e212f7339d3207a2 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 23 Sep 2022 20:55:29 +0200 Subject: [PATCH 679/955] Move MQTT debug_info to dataclass (#78788) * Add MQTT debug_info to dataclass * Remove total attr, assign factory * Rename typed dict to MqttDebugInfo and use helper * Split entity and trigger debug info * Refactor * More rework --- homeassistant/components/mqtt/__init__.py | 1 - homeassistant/components/mqtt/debug_info.py | 133 +++++++++++--------- homeassistant/components/mqtt/mixins.py | 1 + homeassistant/components/mqtt/models.py | 32 ++++- tests/components/mqtt/test_common.py | 6 +- 5 files changed, 109 insertions(+), 64 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 03e4093bb01..306132c4f36 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -170,7 +170,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: websocket_api.async_register_command(hass, websocket_subscribe) websocket_api.async_register_command(hass, websocket_mqtt_info) - debug_info.initialize(hass) if conf: conf = dict(conf) diff --git a/homeassistant/components/mqtt/debug_info.py b/homeassistant/components/mqtt/debug_info.py index 17dbc27f0c4..5fae98eaea5 100644 --- a/homeassistant/components/mqtt/debug_info.py +++ b/homeassistant/components/mqtt/debug_info.py @@ -11,29 +11,26 @@ import attr from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.typing import DiscoveryInfoType from homeassistant.util import dt as dt_util from .const import ATTR_DISCOVERY_PAYLOAD, ATTR_DISCOVERY_TOPIC from .models import MessageCallbackType, PublishPayloadType +from .util import get_mqtt_data -DATA_MQTT_DEBUG_INFO = "mqtt_debug_info" STORED_MESSAGES = 10 -def initialize(hass: HomeAssistant): - """Initialize MQTT debug info.""" - hass.data[DATA_MQTT_DEBUG_INFO] = {"entities": {}, "triggers": {}} - - def log_messages( hass: HomeAssistant, entity_id: str ) -> Callable[[MessageCallbackType], MessageCallbackType]: """Wrap an MQTT message callback to support message logging.""" + debug_info_entities = get_mqtt_data(hass).debug_info_entities + def _log_message(msg): """Log message.""" - debug_info = hass.data[DATA_MQTT_DEBUG_INFO] - messages = debug_info["entities"][entity_id]["subscriptions"][ + messages = debug_info_entities[entity_id]["subscriptions"][ msg.subscribed_topic ]["messages"] if msg not in messages: @@ -72,8 +69,7 @@ def log_message( retain: bool, ) -> None: """Log an outgoing MQTT message.""" - debug_info = hass.data[DATA_MQTT_DEBUG_INFO] - entity_info = debug_info["entities"].setdefault( + entity_info = get_mqtt_data(hass).debug_info_entities.setdefault( entity_id, {"subscriptions": {}, "discovery_data": {}, "transmitted": {}} ) if topic not in entity_info["transmitted"]: @@ -86,11 +82,14 @@ def log_message( entity_info["transmitted"][topic]["messages"].append(msg) -def add_subscription(hass, message_callback, subscription): +def add_subscription( + hass: HomeAssistant, + message_callback: MessageCallbackType, + subscription: str, +) -> None: """Prepare debug data for subscription.""" if entity_id := getattr(message_callback, "__entity_id", None): - debug_info = hass.data[DATA_MQTT_DEBUG_INFO] - entity_info = debug_info["entities"].setdefault( + entity_info = get_mqtt_data(hass).debug_info_entities.setdefault( entity_id, {"subscriptions": {}, "discovery_data": {}, "transmitted": {}} ) if subscription not in entity_info["subscriptions"]: @@ -101,65 +100,81 @@ def add_subscription(hass, message_callback, subscription): entity_info["subscriptions"][subscription]["count"] += 1 -def remove_subscription(hass, message_callback, subscription): +def remove_subscription( + hass: HomeAssistant, + message_callback: MessageCallbackType, + subscription: str, +) -> None: """Remove debug data for subscription if it exists.""" - entity_id = getattr(message_callback, "__entity_id", None) - if entity_id and entity_id in hass.data[DATA_MQTT_DEBUG_INFO]["entities"]: - hass.data[DATA_MQTT_DEBUG_INFO]["entities"][entity_id]["subscriptions"][ - subscription - ]["count"] -= 1 - if not hass.data[DATA_MQTT_DEBUG_INFO]["entities"][entity_id]["subscriptions"][ - subscription - ]["count"]: - hass.data[DATA_MQTT_DEBUG_INFO]["entities"][entity_id]["subscriptions"].pop( - subscription - ) + if (entity_id := getattr(message_callback, "__entity_id", None)) and entity_id in ( + debug_info_entities := get_mqtt_data(hass).debug_info_entities + ): + debug_info_entities[entity_id]["subscriptions"][subscription]["count"] -= 1 + if not debug_info_entities[entity_id]["subscriptions"][subscription]["count"]: + debug_info_entities[entity_id]["subscriptions"].pop(subscription) -def add_entity_discovery_data(hass, discovery_data, entity_id): +def add_entity_discovery_data( + hass: HomeAssistant, discovery_data: DiscoveryInfoType, entity_id: str +) -> None: """Add discovery data.""" - debug_info = hass.data[DATA_MQTT_DEBUG_INFO] - entity_info = debug_info["entities"].setdefault( + entity_info = get_mqtt_data(hass).debug_info_entities.setdefault( entity_id, {"subscriptions": {}, "discovery_data": {}, "transmitted": {}} ) entity_info["discovery_data"] = discovery_data -def update_entity_discovery_data(hass, discovery_payload, entity_id): +def update_entity_discovery_data( + hass: HomeAssistant, discovery_payload: DiscoveryInfoType, entity_id: str +) -> None: """Update discovery data.""" - entity_info = hass.data[DATA_MQTT_DEBUG_INFO]["entities"][entity_id] - entity_info["discovery_data"][ATTR_DISCOVERY_PAYLOAD] = discovery_payload + assert ( + discovery_data := get_mqtt_data(hass).debug_info_entities[entity_id][ + "discovery_data" + ] + ) is not None + discovery_data[ATTR_DISCOVERY_PAYLOAD] = discovery_payload -def remove_entity_data(hass, entity_id): +def remove_entity_data(hass: HomeAssistant, entity_id: str) -> None: """Remove discovery data.""" - if entity_id in hass.data[DATA_MQTT_DEBUG_INFO]["entities"]: - hass.data[DATA_MQTT_DEBUG_INFO]["entities"].pop(entity_id) + if entity_id in (debug_info_entities := get_mqtt_data(hass).debug_info_entities): + debug_info_entities.pop(entity_id) -def add_trigger_discovery_data(hass, discovery_hash, discovery_data, device_id): +def add_trigger_discovery_data( + hass: HomeAssistant, + discovery_hash: tuple[str, str], + discovery_data: DiscoveryInfoType, + device_id: str, +) -> None: """Add discovery data.""" - debug_info = hass.data[DATA_MQTT_DEBUG_INFO] - debug_info["triggers"][discovery_hash] = { + get_mqtt_data(hass).debug_info_triggers[discovery_hash] = { "device_id": device_id, "discovery_data": discovery_data, } -def update_trigger_discovery_data(hass, discovery_hash, discovery_payload): +def update_trigger_discovery_data( + hass: HomeAssistant, + discovery_hash: tuple[str, str], + discovery_payload: DiscoveryInfoType, +) -> None: """Update discovery data.""" - trigger_info = hass.data[DATA_MQTT_DEBUG_INFO]["triggers"][discovery_hash] - trigger_info["discovery_data"][ATTR_DISCOVERY_PAYLOAD] = discovery_payload + get_mqtt_data(hass).debug_info_triggers[discovery_hash]["discovery_data"][ + ATTR_DISCOVERY_PAYLOAD + ] = discovery_payload -def remove_trigger_discovery_data(hass, discovery_hash): +def remove_trigger_discovery_data( + hass: HomeAssistant, discovery_hash: tuple[str, str] +) -> None: """Remove discovery data.""" - hass.data[DATA_MQTT_DEBUG_INFO]["triggers"].pop(discovery_hash) + get_mqtt_data(hass).debug_info_triggers.pop(discovery_hash) def _info_for_entity(hass: HomeAssistant, entity_id: str) -> dict[str, Any]: - mqtt_debug_info = hass.data[DATA_MQTT_DEBUG_INFO] - entity_info = mqtt_debug_info["entities"][entity_id] + entity_info = get_mqtt_data(hass).debug_info_entities[entity_id] subscriptions = [ { "topic": topic, @@ -205,9 +220,10 @@ def _info_for_entity(hass: HomeAssistant, entity_id: str) -> dict[str, Any]: } -def _info_for_trigger(hass: HomeAssistant, trigger_key: str) -> dict[str, Any]: - mqtt_debug_info = hass.data[DATA_MQTT_DEBUG_INFO] - trigger = mqtt_debug_info["triggers"][trigger_key] +def _info_for_trigger( + hass: HomeAssistant, trigger_key: tuple[str, str] +) -> dict[str, Any]: + trigger = get_mqtt_data(hass).debug_info_triggers[trigger_key] discovery_data = None if trigger["discovery_data"] is not None: discovery_data = { @@ -217,36 +233,39 @@ def _info_for_trigger(hass: HomeAssistant, trigger_key: str) -> dict[str, Any]: return {"discovery_data": discovery_data, "trigger_key": trigger_key} -def info_for_config_entry(hass): +def info_for_config_entry(hass: HomeAssistant) -> dict[str, list[Any]]: """Get debug info for all entities and triggers.""" - mqtt_info = {"entities": [], "triggers": []} - mqtt_debug_info = hass.data[DATA_MQTT_DEBUG_INFO] - for entity_id in mqtt_debug_info["entities"]: + mqtt_data = get_mqtt_data(hass) + mqtt_info: dict[str, list[Any]] = {"entities": [], "triggers": []} + + for entity_id in mqtt_data.debug_info_entities: mqtt_info["entities"].append(_info_for_entity(hass, entity_id)) - for trigger_key in mqtt_debug_info["triggers"]: + for trigger_key in mqtt_data.debug_info_triggers: mqtt_info["triggers"].append(_info_for_trigger(hass, trigger_key)) return mqtt_info -def info_for_device(hass, device_id): +def info_for_device(hass: HomeAssistant, device_id: str) -> dict[str, list[Any]]: """Get debug info for a device.""" - mqtt_info = {"entities": [], "triggers": []} + + mqtt_data = get_mqtt_data(hass) + + mqtt_info: dict[str, list[Any]] = {"entities": [], "triggers": []} entity_registry = er.async_get(hass) entries = er.async_entries_for_device( entity_registry, device_id, include_disabled_entities=True ) - mqtt_debug_info = hass.data[DATA_MQTT_DEBUG_INFO] for entry in entries: - if entry.entity_id not in mqtt_debug_info["entities"]: + if entry.entity_id not in mqtt_data.debug_info_entities: continue mqtt_info["entities"].append(_info_for_entity(hass, entry.entity_id)) - for trigger_key, trigger in mqtt_debug_info["triggers"].items(): + for trigger_key, trigger in mqtt_data.debug_info_triggers.items(): if trigger["device_id"] != device_id: continue diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 141d93666c5..8022a6e91ae 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -865,6 +865,7 @@ class MqttDiscoveryUpdate(Entity): send_discovery_done(self.hass, self._discovery_data) if discovery_hash: + assert self._discovery_data is not None debug_info.add_entity_discovery_data( self.hass, self._discovery_data, self.entity_id ) diff --git a/homeassistant/components/mqtt/models.py b/homeassistant/components/mqtt/models.py index 2cff89f93a1..566f18bc791 100644 --- a/homeassistant/components/mqtt/models.py +++ b/homeassistant/components/mqtt/models.py @@ -2,10 +2,11 @@ from __future__ import annotations from ast import literal_eval +from collections import deque from collections.abc import Callable, Coroutine from dataclasses import dataclass, field import datetime as dt -from typing import TYPE_CHECKING, Any, Union +from typing import TYPE_CHECKING, Any, TypedDict, Union import attr @@ -14,10 +15,11 @@ from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers import template from homeassistant.helpers.entity import Entity from homeassistant.helpers.service_info.mqtt import ReceivePayloadType -from homeassistant.helpers.typing import ConfigType, TemplateVarsType +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, TemplateVarsType if TYPE_CHECKING: from .client import MQTT, Subscription + from .debug_info import TimestampedPublishMessage from .device_trigger import Trigger _SENTINEL = object() @@ -53,6 +55,28 @@ AsyncMessageCallbackType = Callable[[ReceiveMessage], Coroutine[Any, Any, None]] MessageCallbackType = Callable[[ReceiveMessage], None] +class SubscriptionDebugInfo(TypedDict): + """Class for holding subscription debug info.""" + + messages: deque[ReceiveMessage] + count: int + + +class EntityDebugInfo(TypedDict): + """Class for holding entity based debug info.""" + + subscriptions: dict[str, SubscriptionDebugInfo] + discovery_data: DiscoveryInfoType + transmitted: dict[str, dict[str, deque[TimestampedPublishMessage]]] + + +class TriggerDebugInfo(TypedDict): + """Class for holding trigger based debug info.""" + + device_id: str + discovery_data: DiscoveryInfoType + + class MqttCommandTemplate: """Class for rendering MQTT payload with command templates.""" @@ -187,6 +211,10 @@ class MqttData: client: MQTT | None = None config: ConfigType | None = None + debug_info_entities: dict[str, EntityDebugInfo] = field(default_factory=dict) + debug_info_triggers: dict[tuple[str, str], TriggerDebugInfo] = field( + default_factory=dict + ) device_triggers: dict[str, Trigger] = field(default_factory=dict) discovery_registry_hooks: dict[tuple[str, str], CALLBACK_TYPE] = field( default_factory=dict diff --git a/tests/components/mqtt/test_common.py b/tests/components/mqtt/test_common.py index e2411f9fc6c..0ac7e64d1bb 100644 --- a/tests/components/mqtt/test_common.py +++ b/tests/components/mqtt/test_common.py @@ -1391,7 +1391,7 @@ async def help_test_entity_debug_info_remove( debug_info_data = debug_info.info_for_device(hass, device.id) assert len(debug_info_data["entities"]) == 0 assert len(debug_info_data["triggers"]) == 0 - assert entity_id not in hass.data[debug_info.DATA_MQTT_DEBUG_INFO]["entities"] + assert entity_id not in hass.data["mqtt"].debug_info_entities async def help_test_entity_debug_info_update_entity_id( @@ -1449,9 +1449,7 @@ async def help_test_entity_debug_info_update_entity_id( "subscriptions" ] assert len(debug_info_data["triggers"]) == 0 - assert ( - f"{domain}.test" not in hass.data[debug_info.DATA_MQTT_DEBUG_INFO]["entities"] - ) + assert f"{domain}.test" not in hass.data["mqtt"].debug_info_entities async def help_test_entity_disabled_by_default( From 21b91f75ba2037b7f9f41de13fd7f1f7f6cd284c Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 23 Sep 2022 13:46:25 -0600 Subject: [PATCH 680/955] Bump `regenmaschine` to 2022.09.2 (#79010) * Bump `regenmaschine` to 2022.09.2 * Fix tests * Restore incorrectly-deleted test --- homeassistant/components/rainmachine/__init__.py | 2 +- homeassistant/components/rainmachine/config_flow.py | 2 +- homeassistant/components/rainmachine/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/rainmachine/conftest.py | 6 ++---- .../rainmachine/fixtures/api_versions_data.json | 2 +- tests/components/rainmachine/test_diagnostics.py | 12 ++++++++++-- 8 files changed, 18 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index ff52b74ab16..8cc3b3d5e80 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -497,7 +497,7 @@ class RainMachineEntity(CoordinatorEntity): f"{self._entry.data[CONF_PORT]}" ), connections={(dr.CONNECTION_NETWORK_MAC, self._data.controller.mac)}, - name=str(self._data.controller.name).capitalize(), + name=self._data.controller.name.capitalize(), manufacturer="RainMachine", model=( f"Version {self._version_coordinator.data['hwVer']} " diff --git a/homeassistant/components/rainmachine/config_flow.py b/homeassistant/components/rainmachine/config_flow.py index c12362591e7..eed80b9c145 100644 --- a/homeassistant/components/rainmachine/config_flow.py +++ b/homeassistant/components/rainmachine/config_flow.py @@ -132,7 +132,7 @@ class RainMachineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # access token without using the IP address and password, so we have to # store it: return self.async_create_entry( - title=str(controller.name), + title=controller.name.capitalize(), data={ CONF_IP_ADDRESS: user_input[CONF_IP_ADDRESS], CONF_PASSWORD: user_input[CONF_PASSWORD], diff --git a/homeassistant/components/rainmachine/manifest.json b/homeassistant/components/rainmachine/manifest.json index 6a87110f6f6..ca7543bfb38 100644 --- a/homeassistant/components/rainmachine/manifest.json +++ b/homeassistant/components/rainmachine/manifest.json @@ -3,7 +3,7 @@ "name": "RainMachine", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/rainmachine", - "requirements": ["regenmaschine==2022.09.1"], + "requirements": ["regenmaschine==2022.09.2"], "codeowners": ["@bachya"], "iot_class": "local_polling", "homekit": { diff --git a/requirements_all.txt b/requirements_all.txt index fe15419d7db..8f5bfd7ec5b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2138,7 +2138,7 @@ raincloudy==0.0.7 raspyrfm-client==1.2.8 # homeassistant.components.rainmachine -regenmaschine==2022.09.1 +regenmaschine==2022.09.2 # homeassistant.components.renault renault-api==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a38c27925e4..f877e9a5890 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1468,7 +1468,7 @@ radios==0.1.1 radiotherm==2.1.0 # homeassistant.components.rainmachine -regenmaschine==2022.09.1 +regenmaschine==2022.09.2 # homeassistant.components.renault renault-api==0.1.11 diff --git a/tests/components/rainmachine/conftest.py b/tests/components/rainmachine/conftest.py index 1dfef7e399c..af00a1013e0 100644 --- a/tests/components/rainmachine/conftest.py +++ b/tests/components/rainmachine/conftest.py @@ -51,10 +51,8 @@ def controller_fixture( """Define a regenmaschine controller.""" controller = AsyncMock() controller.api_version = "4.5.0" - controller.hardware_version = 3 - # The api returns a controller with all numbers as numeric - # instead of a string - controller.name = 12345 + controller.hardware_version = "3" + controller.name = "12345" controller.mac = controller_mac controller.software_version = "4.0.925" diff --git a/tests/components/rainmachine/fixtures/api_versions_data.json b/tests/components/rainmachine/fixtures/api_versions_data.json index d4ec1fb80e9..67cfa4ce033 100644 --- a/tests/components/rainmachine/fixtures/api_versions_data.json +++ b/tests/components/rainmachine/fixtures/api_versions_data.json @@ -1,5 +1,5 @@ { "apiVer": "4.6.1", - "hwVer": 3, + "hwVer": "3", "swVer": "4.0.1144" } diff --git a/tests/components/rainmachine/test_diagnostics.py b/tests/components/rainmachine/test_diagnostics.py index a600c5f7c34..7cf8406d2ae 100644 --- a/tests/components/rainmachine/test_diagnostics.py +++ b/tests/components/rainmachine/test_diagnostics.py @@ -21,7 +21,11 @@ async def test_entry_diagnostics(hass, config_entry, hass_client, setup_rainmach }, "data": { "coordinator": { - "api.versions": {"apiVer": "4.6.1", "hwVer": 3, "swVer": "4.0.1144"}, + "api.versions": { + "apiVer": "4.6.1", + "hwVer": "3", + "swVer": "4.0.1144", + }, "machine.firmware_update_status": { "lastUpdateCheckTimestamp": 1657825288, "packageDetails": [], @@ -635,7 +639,11 @@ async def test_entry_diagnostics_failed_controller_diagnostics( }, "data": { "coordinator": { - "api.versions": {"apiVer": "4.6.1", "hwVer": 3, "swVer": "4.0.1144"}, + "api.versions": { + "apiVer": "4.6.1", + "hwVer": "3", + "swVer": "4.0.1144", + }, "machine.firmware_update_status": { "lastUpdateCheckTimestamp": 1657825288, "packageDetails": [], From 26d9962fe551c7163cf4b1e3220c7c0c9507cf44 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 23 Sep 2022 22:34:49 +0200 Subject: [PATCH 681/955] Add base entity to switchbee (#78987) * Add base entity to switchbee * Adjust coverage * Add SwitchBeeDeviceEntity * Don't move available * Update homeassistant/components/switchbee/entity.py Co-authored-by: Joakim Plate * Update homeassistant/components/switchbee/entity.py Co-authored-by: Joakim Plate Co-authored-by: Joakim Plate --- .coveragerc | 2 +- homeassistant/components/switchbee/button.py | 19 ++----- homeassistant/components/switchbee/entity.py | 54 ++++++++++++++++++++ homeassistant/components/switchbee/switch.py | 39 +++----------- 4 files changed, 67 insertions(+), 47 deletions(-) create mode 100644 homeassistant/components/switchbee/entity.py diff --git a/.coveragerc b/.coveragerc index 71a4b652ce1..9d8d87b1312 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1222,8 +1222,8 @@ omit = homeassistant/components/swisscom/device_tracker.py homeassistant/components/switchbee/__init__.py homeassistant/components/switchbee/button.py - homeassistant/components/switchbee/const.py homeassistant/components/switchbee/coordinator.py + homeassistant/components/switchbee/entity.py homeassistant/components/switchbee/switch.py homeassistant/components/switchbot/__init__.py homeassistant/components/switchbot/binary_sensor.py diff --git a/homeassistant/components/switchbee/button.py b/homeassistant/components/switchbee/button.py index fc2c7373b7e..175aa3af26e 100644 --- a/homeassistant/components/switchbee/button.py +++ b/homeassistant/components/switchbee/button.py @@ -1,17 +1,17 @@ """Support for SwitchBee scenario button.""" from switchbee.api import SwitchBeeError -from switchbee.device import ApiStateCommand, DeviceType, SwitchBeeBaseDevice +from switchbee.device import ApiStateCommand, DeviceType from homeassistant.components.button import ButtonEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN from .coordinator import SwitchBeeCoordinator +from .entity import SwitchBeeEntity async def async_setup_entry( @@ -26,24 +26,13 @@ async def async_setup_entry( ) -class SwitchBeeButton(CoordinatorEntity[SwitchBeeCoordinator], ButtonEntity): +class SwitchBeeButton(SwitchBeeEntity, ButtonEntity): """Representation of an Switchbee button.""" - def __init__( - self, - device: SwitchBeeBaseDevice, - coordinator: SwitchBeeCoordinator, - ) -> None: - """Initialize the Switchbee switch.""" - super().__init__(coordinator) - self._attr_name = device.name - self._device_id = device.id - self._attr_unique_id = f"{coordinator.mac_formated}-{device.id}" - async def async_press(self) -> None: """Fire the scenario in the SwitchBee hub.""" try: - await self.coordinator.api.set_state(self._device_id, ApiStateCommand.ON) + await self.coordinator.api.set_state(self._device.id, ApiStateCommand.ON) except SwitchBeeError as exp: raise HomeAssistantError( f"Failed to fire scenario {self.name}, {str(exp)}" diff --git a/homeassistant/components/switchbee/entity.py b/homeassistant/components/switchbee/entity.py new file mode 100644 index 00000000000..fa2fb93c4f2 --- /dev/null +++ b/homeassistant/components/switchbee/entity.py @@ -0,0 +1,54 @@ +"""Support for SwitchBee entity.""" +from switchbee import SWITCHBEE_BRAND +from switchbee.device import SwitchBeeBaseDevice + +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import SwitchBeeCoordinator + + +class SwitchBeeEntity(CoordinatorEntity[SwitchBeeCoordinator]): + """Representation of a Switchbee entity.""" + + _attr_has_entity_name = True + + def __init__( + self, + device: SwitchBeeBaseDevice, + coordinator: SwitchBeeCoordinator, + ) -> None: + """Initialize the Switchbee entity.""" + super().__init__(coordinator) + self._device = device + self._attr_name = device.name + self._attr_unique_id = f"{coordinator.mac_formated}-{device.id}" + + +class SwitchBeeDeviceEntity(SwitchBeeEntity): + """Representation of a Switchbee device entity.""" + + def __init__( + self, + device: SwitchBeeBaseDevice, + coordinator: SwitchBeeCoordinator, + ) -> None: + """Initialize the Switchbee device.""" + super().__init__(device, coordinator) + self._attr_device_info = DeviceInfo( + name=f"SwitchBee {device.unit_id}", + identifiers={ + ( + DOMAIN, + f"{device.unit_id}-{coordinator.mac_formated}", + ) + }, + manufacturer=SWITCHBEE_BRAND, + model=coordinator.api.module_display(device.unit_id), + suggested_area=device.zone, + via_device=( + DOMAIN, + f"{coordinator.api.name} ({coordinator.api.mac})", + ), + ) diff --git a/homeassistant/components/switchbee/switch.py b/homeassistant/components/switchbee/switch.py index 298c30fa7b8..e05339ee68e 100644 --- a/homeassistant/components/switchbee/switch.py +++ b/homeassistant/components/switchbee/switch.py @@ -2,7 +2,6 @@ import logging from typing import Any -from switchbee import SWITCHBEE_BRAND from switchbee.api import SwitchBeeDeviceOfflineError, SwitchBeeError from switchbee.device import ApiStateCommand, DeviceType, SwitchBeeBaseDevice @@ -10,12 +9,11 @@ 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.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN from .coordinator import SwitchBeeCoordinator +from .entity import SwitchBeeDeviceEntity _LOGGER = logging.getLogger(__name__) @@ -39,8 +37,8 @@ async def async_setup_entry( ) -class SwitchBeeSwitchEntity(CoordinatorEntity[SwitchBeeCoordinator], SwitchEntity): - """Representation of an Switchbee switch.""" +class SwitchBeeSwitchEntity(SwitchBeeDeviceEntity, SwitchEntity): + """Representation of a Switchbee switch.""" def __init__( self, @@ -48,30 +46,9 @@ class SwitchBeeSwitchEntity(CoordinatorEntity[SwitchBeeCoordinator], SwitchEntit coordinator: SwitchBeeCoordinator, ) -> None: """Initialize the Switchbee switch.""" - super().__init__(coordinator) - self._attr_name = f"{device.name}" - self._device_id = device.id - self._attr_unique_id = f"{coordinator.mac_formated}-{device.id}" + super().__init__(device, coordinator) self._attr_is_on = False self._is_online = True - self._attr_has_entity_name = True - self._device = device - self._attr_device_info = DeviceInfo( - name=f"SwitchBee_{str(device.unit_id)}", - identifiers={ - ( - DOMAIN, - f"{str(device.unit_id)}-{coordinator.mac_formated}", - ) - }, - manufacturer=SWITCHBEE_BRAND, - model=coordinator.api.module_display(device.unit_id), - suggested_area=device.zone, - via_device=( - DOMAIN, - f"{coordinator.api.name} ({coordinator.api.mac})", - ), - ) @property def available(self) -> bool: @@ -101,13 +78,13 @@ class SwitchBeeSwitchEntity(CoordinatorEntity[SwitchBeeCoordinator], SwitchEntit """ try: - await self.coordinator.api.set_state(self._device_id, "dummy") + await self.coordinator.api.set_state(self._device.id, "dummy") except SwitchBeeDeviceOfflineError: return except SwitchBeeError: return - if self.coordinator.data[self._device_id].state == -1: + if self.coordinator.data[self._device.id].state == -1: # This specific call will refresh the state of the device in the CU self.hass.async_create_task(async_refresh_state()) @@ -132,7 +109,7 @@ class SwitchBeeSwitchEntity(CoordinatorEntity[SwitchBeeCoordinator], SwitchEntit # timed power switch state is an integer representing the number of minutes left until it goes off # regulare switches state is ON/OFF (1/0 respectively) self._attr_is_on = ( - self.coordinator.data[self._device_id].state != ApiStateCommand.OFF + self.coordinator.data[self._device.id].state != ApiStateCommand.OFF ) async def async_turn_on(self, **kwargs: Any) -> None: @@ -145,7 +122,7 @@ class SwitchBeeSwitchEntity(CoordinatorEntity[SwitchBeeCoordinator], SwitchEntit async def _async_set_state(self, state: ApiStateCommand) -> None: try: - await self.coordinator.api.set_state(self._device_id, state) + await self.coordinator.api.set_state(self._device.id, state) except (SwitchBeeError, SwitchBeeDeviceOfflineError) as exp: await self.coordinator.async_refresh() raise HomeAssistantError( From a495df9759ee973620690ee8083d73181547934c Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Fri, 23 Sep 2022 23:11:06 +0200 Subject: [PATCH 682/955] Fix velbus matching ignored entries in config flow (#78999) * Fix bug #fix78826 * start using async_abort_entries_match * fix/rewrite tests --- .../components/velbus/config_flow.py | 24 +--- tests/components/velbus/test_config_flow.py | 110 ++++++++++-------- 2 files changed, 67 insertions(+), 67 deletions(-) diff --git a/homeassistant/components/velbus/config_flow.py b/homeassistant/components/velbus/config_flow.py index 993146d375c..32f1f3a500d 100644 --- a/homeassistant/components/velbus/config_flow.py +++ b/homeassistant/components/velbus/config_flow.py @@ -10,21 +10,12 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components import usb from homeassistant.const import CONF_NAME, CONF_PORT -from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult from homeassistant.util import slugify from .const import DOMAIN -@callback -def velbus_entries(hass: HomeAssistant) -> set[str]: - """Return connections for Velbus domain.""" - return { - entry.data[CONF_PORT] for entry in hass.config_entries.async_entries(DOMAIN) - } - - class VelbusConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow.""" @@ -51,10 +42,6 @@ class VelbusConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return False return True - def _prt_in_configuration_exists(self, prt: str) -> bool: - """Return True if port exists in configuration.""" - return prt in velbus_entries(self.hass) - async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: @@ -63,11 +50,9 @@ class VelbusConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: name = slugify(user_input[CONF_NAME]) prt = user_input[CONF_PORT] - if not self._prt_in_configuration_exists(prt): - if await self._test_connection(prt): - return self._create_device(name, prt) - else: - self._errors[CONF_PORT] = "already_configured" + self._async_abort_entries_match({CONF_PORT: prt}) + if await self._test_connection(prt): + return self._create_device(name, prt) else: user_input = {} user_input[CONF_NAME] = "" @@ -93,8 +78,7 @@ class VelbusConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): usb.get_serial_by_id, discovery_info.device ) # check if this device is not already configured - if self._prt_in_configuration_exists(dev_path): - return self.async_abort(reason="already_configured") + self._async_abort_entries_match({CONF_PORT: dev_path}) # check if we can make a valid velbus connection if not await self._test_connection(dev_path): return self.async_abort(reason="cannot_connect") diff --git a/tests/components/velbus/test_config_flow.py b/tests/components/velbus/test_config_flow.py index 207f745e495..454290b3581 100644 --- a/tests/components/velbus/test_config_flow.py +++ b/tests/components/velbus/test_config_flow.py @@ -7,9 +7,8 @@ from velbusaio.exceptions import VelbusConnectionFailed from homeassistant import data_entry_flow from homeassistant.components import usb -from homeassistant.components.velbus import config_flow from homeassistant.components.velbus.const import DOMAIN -from homeassistant.config_entries import SOURCE_USB +from homeassistant.config_entries import SOURCE_USB, SOURCE_USER from homeassistant.const import CONF_NAME, CONF_PORT, CONF_SOURCE from homeassistant.core import HomeAssistant @@ -53,63 +52,76 @@ def mock_controller_connection_failed(): yield -def init_config_flow(hass: HomeAssistant): - """Init a configuration flow.""" - flow = config_flow.VelbusConfigFlow() - flow.hass = hass - return flow - - @pytest.mark.usefixtures("controller") async def test_user(hass: HomeAssistant): """Test user config.""" - flow = init_config_flow(hass) - - result = await flow.async_step_user() - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "user" - - result = await flow.async_step_user( - {CONF_NAME: "Velbus Test Serial", CONF_PORT: PORT_SERIAL} + # simple user form + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert result["title"] == "velbus_test_serial" - assert result["data"][CONF_PORT] == PORT_SERIAL + assert result + assert result.get("flow_id") + assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("step_id") == "user" - result = await flow.async_step_user( - {CONF_NAME: "Velbus Test TCP", CONF_PORT: PORT_TCP} + # try with a serial port + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_NAME: "Velbus Test Serial", CONF_PORT: PORT_SERIAL}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert result["title"] == "velbus_test_tcp" - assert result["data"][CONF_PORT] == PORT_TCP + assert result + assert result.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result.get("title") == "velbus_test_serial" + data = result.get("data") + assert data[CONF_PORT] == PORT_SERIAL + + # try with a ip:port combination + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_NAME: "Velbus Test TCP", CONF_PORT: PORT_TCP}, + ) + assert result + assert result.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result.get("title") == "velbus_test_tcp" + data = result.get("data") + assert data[CONF_PORT] == PORT_TCP @pytest.mark.usefixtures("controller_connection_failed") async def test_user_fail(hass: HomeAssistant): """Test user config.""" - flow = init_config_flow(hass) - - result = await flow.async_step_user( - {CONF_NAME: "Velbus Test Serial", CONF_PORT: PORT_SERIAL} + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_NAME: "Velbus Test Serial", CONF_PORT: PORT_SERIAL}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["errors"] == {CONF_PORT: "cannot_connect"} + assert result + assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("errors") == {CONF_PORT: "cannot_connect"} - result = await flow.async_step_user( - {CONF_NAME: "Velbus Test TCP", CONF_PORT: PORT_TCP} + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_NAME: "Velbus Test TCP", CONF_PORT: PORT_TCP}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["errors"] == {CONF_PORT: "cannot_connect"} + assert result + assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("errors") == {CONF_PORT: "cannot_connect"} @pytest.mark.usefixtures("config_entry") async def test_abort_if_already_setup(hass: HomeAssistant): """Test we abort if Velbus is already setup.""" - flow = init_config_flow(hass) - - result = await flow.async_step_user({CONF_PORT: PORT_TCP, CONF_NAME: "velbus test"}) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["errors"] == {"port": "already_configured"} + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_PORT: PORT_TCP, CONF_NAME: "velbus test"}, + ) + assert result + assert result.get("type") == data_entry_flow.FlowResultType.ABORT + assert result.get("reason") == "already_configured" @pytest.mark.usefixtures("controller") @@ -121,14 +133,16 @@ async def test_flow_usb(hass: HomeAssistant): context={CONF_SOURCE: SOURCE_USB}, data=DISCOVERY_INFO, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "discovery_confirm" + assert result + assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("step_id") == "discovery_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result + assert result.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY # test an already configured discovery entry = MockConfigEntry( @@ -141,8 +155,9 @@ async def test_flow_usb(hass: HomeAssistant): context={CONF_SOURCE: SOURCE_USB}, data=DISCOVERY_INFO, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT - assert result["reason"] == "already_configured" + assert result + assert result.get("type") == data_entry_flow.FlowResultType.ABORT + assert result.get("reason") == "already_configured" @pytest.mark.usefixtures("controller_connection_failed") @@ -154,5 +169,6 @@ async def test_flow_usb_failed(hass: HomeAssistant): context={CONF_SOURCE: SOURCE_USB}, data=DISCOVERY_INFO, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT - assert result["reason"] == "cannot_connect" + assert result + assert result.get("type") == data_entry_flow.FlowResultType.ABORT + assert result.get("reason") == "cannot_connect" From 55b214c911bd7363ff7362acf73239e7a4850323 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 23 Sep 2022 17:05:07 -0600 Subject: [PATCH 683/955] Replace two RainMachine binary sensors with config switches (#76478) --- .../components/rainmachine/binary_sensor.py | 28 +++++- .../components/rainmachine/switch.py | 93 +++++++++++++++++-- 2 files changed, 112 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/rainmachine/binary_sensor.py b/homeassistant/components/rainmachine/binary_sensor.py index 48f11f598c9..212d87f2982 100644 --- a/homeassistant/components/rainmachine/binary_sensor.py +++ b/homeassistant/components/rainmachine/binary_sensor.py @@ -2,6 +2,7 @@ from dataclasses import dataclass from homeassistant.components.binary_sensor import ( + DOMAIN as BINARY_SENSOR_DOMAIN, BinarySensorEntity, BinarySensorEntityDescription, ) @@ -21,7 +22,11 @@ from .model import ( RainMachineEntityDescription, RainMachineEntityDescriptionMixinDataKey, ) -from .util import key_exists +from .util import ( + EntityDomainReplacementStrategy, + async_finish_entity_domain_replacements, + key_exists, +) TYPE_FLOW_SENSOR = "flow_sensor" TYPE_FREEZE = "freeze" @@ -125,6 +130,27 @@ async def async_setup_entry( """Set up RainMachine binary sensors based on a config entry.""" data: RainMachineData = hass.data[DOMAIN][entry.entry_id] + async_finish_entity_domain_replacements( + hass, + entry, + ( + EntityDomainReplacementStrategy( + BINARY_SENSOR_DOMAIN, + f"{data.controller.mac}_freeze_protection", + f"switch.{data.controller.name.lower()}_freeze_protect_enabled", + breaks_in_ha_version="2022.12.0", + remove_old_entity=False, + ), + EntityDomainReplacementStrategy( + BINARY_SENSOR_DOMAIN, + f"{data.controller.mac}_extra_water_on_hot_days", + f"switch.{data.controller.name.lower()}_hot_days_extra_watering", + breaks_in_ha_version="2022.12.0", + remove_old_entity=False, + ), + ), + ) + api_category_sensor_map = { DATA_PROVISION_SETTINGS: ProvisionSettingsBinarySensor, DATA_RESTRICTIONS_CURRENT: CurrentRestrictionsBinarySensor, diff --git a/homeassistant/components/rainmachine/switch.py b/homeassistant/components/rainmachine/switch.py index 029c8c06771..56ac814e2eb 100644 --- a/homeassistant/components/rainmachine/switch.py +++ b/homeassistant/components/rainmachine/switch.py @@ -24,11 +24,16 @@ from . import RainMachineData, RainMachineEntity, async_update_programs_and_zone from .const import ( CONF_ZONE_RUN_TIME, DATA_PROGRAMS, + DATA_RESTRICTIONS_UNIVERSAL, DATA_ZONES, DEFAULT_ZONE_RUN, DOMAIN, ) -from .model import RainMachineEntityDescription, RainMachineEntityDescriptionMixinUid +from .model import ( + RainMachineEntityDescription, + RainMachineEntityDescriptionMixinDataKey, + RainMachineEntityDescriptionMixinUid, +) from .util import RUN_STATE_MAP ATTR_AREA = "area" @@ -130,11 +135,45 @@ def raise_on_request_error( class RainMachineSwitchDescription( SwitchEntityDescription, RainMachineEntityDescription, - RainMachineEntityDescriptionMixinUid, ): """Describe a RainMachine switch.""" +@dataclass +class RainMachineActivitySwitchDescription( + RainMachineSwitchDescription, RainMachineEntityDescriptionMixinUid +): + """Describe a RainMachine activity (program/zone) switch.""" + + +@dataclass +class RainMachineRestrictionSwitchDescription( + RainMachineSwitchDescription, RainMachineEntityDescriptionMixinDataKey +): + """Describe a RainMachine restriction switch.""" + + +TYPE_RESTRICTIONS_FREEZE_PROTECT_ENABLED = "freeze_protect_enabled" +TYPE_RESTRICTIONS_HOT_DAYS_EXTRA_WATERING = "hot_days_extra_watering" + +RESTRICTIONS_SWITCH_DESCRIPTIONS = ( + RainMachineRestrictionSwitchDescription( + key=TYPE_RESTRICTIONS_FREEZE_PROTECT_ENABLED, + name="Freeze protection", + icon="mdi:snowflake-alert", + api_category=DATA_RESTRICTIONS_UNIVERSAL, + data_key="freezeProtectEnabled", + ), + RainMachineRestrictionSwitchDescription( + key=TYPE_RESTRICTIONS_HOT_DAYS_EXTRA_WATERING, + name="Extra water on hot days", + icon="mdi:heat-wave", + api_category=DATA_RESTRICTIONS_UNIVERSAL, + data_key="hotDaysExtraWatering", + ), +) + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: @@ -158,8 +197,8 @@ async def async_setup_entry( platform.async_register_entity_service(service_name, schema, method) data: RainMachineData = hass.data[DOMAIN][entry.entry_id] + entities: list[RainMachineBaseSwitch] = [] - entities: list[RainMachineActivitySwitch | RainMachineEnabledSwitch] = [] for kind, api_category, switch_class, switch_enabled_class in ( ("program", DATA_PROGRAMS, RainMachineProgram, RainMachineProgramEnabled), ("zone", DATA_ZONES, RainMachineZone, RainMachineZoneEnabled), @@ -173,10 +212,9 @@ async def async_setup_entry( switch_class( entry, data, - RainMachineSwitchDescription( + RainMachineActivitySwitchDescription( key=f"{kind}_{uid}", name=name, - icon="mdi:water", api_category=api_category, uid=uid, ), @@ -188,17 +226,19 @@ async def async_setup_entry( switch_enabled_class( entry, data, - RainMachineSwitchDescription( + RainMachineActivitySwitchDescription( key=f"{kind}_{uid}_enabled", name=f"{name} enabled", - entity_category=EntityCategory.CONFIG, - icon="mdi:cog", api_category=api_category, uid=uid, ), ) ) + # Add switches to control restrictions: + for description in RESTRICTIONS_SWITCH_DESCRIPTIONS: + entities.append(RainMachineRestrictionSwitch(entry, data, description)) + async_add_entities(entities) @@ -246,6 +286,9 @@ class RainMachineBaseSwitch(RainMachineEntity, SwitchEntity): class RainMachineActivitySwitch(RainMachineBaseSwitch): """Define a RainMachine switch to start/stop an activity (program or zone).""" + _attr_icon = "mdi:water" + entity_description: RainMachineActivitySwitchDescription + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off. @@ -284,6 +327,10 @@ class RainMachineActivitySwitch(RainMachineBaseSwitch): class RainMachineEnabledSwitch(RainMachineBaseSwitch): """Define a RainMachine switch to enable/disable an activity (program or zone).""" + _attr_entity_category = EntityCategory.CONFIG + _attr_icon = "mdi:cog" + entity_description: RainMachineActivitySwitchDescription + @callback def update_from_latest_data(self) -> None: """Update the entity when new data is received.""" @@ -360,6 +407,36 @@ class RainMachineProgramEnabled(RainMachineEnabledSwitch): self._update_activities() +class RainMachineRestrictionSwitch(RainMachineBaseSwitch): + """Define a RainMachine restriction setting.""" + + _attr_entity_category = EntityCategory.CONFIG + entity_description: RainMachineRestrictionSwitchDescription + + @raise_on_request_error + async def async_turn_off(self, **kwargs: Any) -> None: + """Disable the restriction.""" + await self._data.controller.restrictions.set_universal( + {self.entity_description.data_key: False} + ) + self._attr_is_on = False + self.async_write_ha_state() + + @raise_on_request_error + async def async_turn_on(self, **kwargs: Any) -> None: + """Enable the restriction.""" + await self._data.controller.restrictions.set_universal( + {self.entity_description.data_key: True} + ) + self._attr_is_on = True + self.async_write_ha_state() + + @callback + def update_from_latest_data(self) -> None: + """Update the entity when new data is received.""" + self._attr_is_on = self.coordinator.data[self.entity_description.data_key] + + class RainMachineZone(RainMachineActivitySwitch): """Define a RainMachine zone.""" From 3875ce6c9e27e5497e0ecdb6f44893bfd2885222 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 24 Sep 2022 00:31:54 +0000 Subject: [PATCH 684/955] [ci skip] Translation update --- .../components/anthemav/translations/bg.json | 18 +++++++ .../automation/translations/bg.json | 12 +++++ .../components/bluetooth/translations/it.json | 6 +++ .../components/bluetooth/translations/no.json | 6 +++ .../components/bluetooth/translations/ru.json | 6 +++ .../components/braviatv/translations/de.json | 12 +++-- .../components/braviatv/translations/es.json | 12 +++-- .../components/braviatv/translations/fr.json | 10 +++- .../components/braviatv/translations/id.json | 12 +++-- .../components/braviatv/translations/it.json | 12 +++-- .../components/braviatv/translations/ru.json | 12 +++-- .../braviatv/translations/zh-Hant.json | 12 +++-- .../components/climacell/translations/af.json | 9 ++++ .../components/climacell/translations/ca.json | 13 +++++ .../components/climacell/translations/de.json | 13 +++++ .../components/climacell/translations/el.json | 13 +++++ .../components/climacell/translations/en.json | 13 +++++ .../climacell/translations/es-419.json | 13 +++++ .../components/climacell/translations/es.json | 13 +++++ .../components/climacell/translations/et.json | 13 +++++ .../components/climacell/translations/fr.json | 13 +++++ .../components/climacell/translations/hu.json | 13 +++++ .../components/climacell/translations/id.json | 13 +++++ .../components/climacell/translations/ja.json | 13 +++++ .../components/climacell/translations/ko.json | 13 +++++ .../components/climacell/translations/nl.json | 13 +++++ .../components/climacell/translations/no.json | 13 +++++ .../components/climacell/translations/pl.json | 13 +++++ .../climacell/translations/pt-BR.json | 13 +++++ .../components/climacell/translations/ru.json | 13 +++++ .../climacell/translations/sensor.bg.json | 8 ++++ .../climacell/translations/sensor.ca.json | 27 +++++++++++ .../climacell/translations/sensor.de.json | 27 +++++++++++ .../climacell/translations/sensor.el.json | 27 +++++++++++ .../climacell/translations/sensor.en.json | 27 +++++++++++ .../climacell/translations/sensor.es-419.json | 27 +++++++++++ .../climacell/translations/sensor.es.json | 27 +++++++++++ .../climacell/translations/sensor.et.json | 27 +++++++++++ .../climacell/translations/sensor.fr.json | 27 +++++++++++ .../climacell/translations/sensor.he.json | 7 +++ .../climacell/translations/sensor.hu.json | 27 +++++++++++ .../climacell/translations/sensor.id.json | 27 +++++++++++ .../climacell/translations/sensor.is.json | 12 +++++ .../climacell/translations/sensor.ja.json | 27 +++++++++++ .../climacell/translations/sensor.ko.json | 7 +++ .../climacell/translations/sensor.lv.json | 27 +++++++++++ .../climacell/translations/sensor.nl.json | 27 +++++++++++ .../climacell/translations/sensor.no.json | 27 +++++++++++ .../climacell/translations/sensor.pl.json | 27 +++++++++++ .../climacell/translations/sensor.pt-BR.json | 27 +++++++++++ .../climacell/translations/sensor.pt.json | 7 +++ .../climacell/translations/sensor.ru.json | 27 +++++++++++ .../climacell/translations/sensor.sk.json | 7 +++ .../climacell/translations/sensor.sv.json | 27 +++++++++++ .../climacell/translations/sensor.tr.json | 27 +++++++++++ .../translations/sensor.zh-Hant.json | 27 +++++++++++ .../components/climacell/translations/sv.json | 13 +++++ .../components/climacell/translations/tr.json | 13 +++++ .../climacell/translations/zh-Hant.json | 13 +++++ .../components/dlna_dms/translations/bg.json | 3 +- .../eight_sleep/translations/bg.json | 15 ++++++ .../components/google/translations/bg.json | 5 ++ .../components/group/translations/bg.json | 6 ++- .../components/guardian/translations/it.json | 13 ++++- .../components/guardian/translations/no.json | 17 +++++-- .../components/guardian/translations/ru.json | 2 +- .../here_travel_time/translations/bg.json | 26 ++++++++++ .../homeassistant/translations/bg.json | 1 + .../components/ibeacon/translations/en.json | 10 ++++ .../components/ibeacon/translations/it.json | 23 +++++++++ .../components/ibeacon/translations/no.json | 23 +++++++++ .../components/ibeacon/translations/ru.json | 23 +++++++++ .../components/kegtron/translations/it.json | 22 +++++++++ .../components/kegtron/translations/no.json | 22 +++++++++ .../components/kegtron/translations/ru.json | 22 +++++++++ .../keymitt_ble/translations/it.json | 27 +++++++++++ .../keymitt_ble/translations/no.json | 27 +++++++++++ .../keymitt_ble/translations/pt-BR.json | 27 +++++++++++ .../keymitt_ble/translations/ru.json | 27 +++++++++++ .../components/lametric/translations/no.json | 3 +- .../lg_soundbar/translations/bg.json | 17 +++++++ .../components/lidarr/translations/bg.json | 9 ++++ .../components/lidarr/translations/it.json | 42 ++++++++++++++++ .../components/lidarr/translations/no.json | 42 ++++++++++++++++ .../components/lidarr/translations/ru.json | 42 ++++++++++++++++ .../components/life360/translations/bg.json | 22 ++++++++- .../litterrobot/translations/sensor.id.json | 14 ++++-- .../litterrobot/translations/sensor.it.json | 3 ++ .../litterrobot/translations/sensor.no.json | 3 ++ .../components/nest/translations/bg.json | 6 +++ .../components/nextdns/translations/bg.json | 24 ++++++++++ .../components/nextdns/translations/id.json | 5 ++ .../nibe_heatpump/translations/hu.json | 14 ++++++ .../nibe_heatpump/translations/it.json | 25 ++++++++++ .../nibe_heatpump/translations/no.json | 25 ++++++++++ .../nibe_heatpump/translations/pt-BR.json | 25 ++++++++++ .../nibe_heatpump/translations/ru.json | 25 ++++++++++ .../components/nina/translations/bg.json | 7 +++ .../components/openuv/translations/it.json | 10 ++++ .../components/openuv/translations/no.json | 10 ++++ .../components/radarr/translations/bg.json | 24 ++++++++++ .../components/radarr/translations/de.json | 48 +++++++++++++++++++ .../components/radarr/translations/el.json | 48 +++++++++++++++++++ .../components/radarr/translations/en.json | 34 ++++++------- .../components/radarr/translations/es.json | 48 +++++++++++++++++++ .../components/radarr/translations/fr.json | 45 +++++++++++++++++ .../components/radarr/translations/id.json | 47 ++++++++++++++++++ .../components/radarr/translations/it.json | 48 +++++++++++++++++++ .../components/radarr/translations/no.json | 48 +++++++++++++++++++ .../components/radarr/translations/pt-BR.json | 48 +++++++++++++++++++ .../components/radarr/translations/ru.json | 48 +++++++++++++++++++ .../radarr/translations/zh-Hant.json | 48 +++++++++++++++++++ .../rainmachine/translations/bg.json | 12 +++++ .../rainmachine/translations/de.json | 13 +++++ .../rainmachine/translations/es.json | 13 +++++ .../rainmachine/translations/it.json | 13 +++++ .../rainmachine/translations/no.json | 13 +++++ .../rainmachine/translations/pt-BR.json | 13 +++++ .../rainmachine/translations/ru.json | 13 +++++ .../rainmachine/translations/zh-Hant.json | 13 +++++ .../components/rhasspy/translations/bg.json | 7 +++ .../components/scrape/translations/bg.json | 5 +- .../components/select/translations/bg.json | 3 ++ .../components/shelly/translations/bg.json | 3 +- .../simplisafe/translations/it.json | 6 +++ .../simplisafe/translations/no.json | 6 +++ .../soundtouch/translations/bg.json | 10 ++++ .../components/spotify/translations/bg.json | 5 ++ .../components/steamist/translations/bg.json | 3 +- .../components/tasmota/translations/it.json | 10 ++++ .../components/tasmota/translations/no.json | 10 ++++ .../transmission/translations/bg.json | 10 +++- .../tuya/translations/select.bg.json | 3 +- .../ukraine_alarm/translations/bg.json | 1 + .../components/verisure/translations/bg.json | 12 +++++ .../volvooncall/translations/it.json | 1 + .../volvooncall/translations/no.json | 1 + .../volvooncall/translations/ru.json | 1 + .../components/wiz/translations/bg.json | 3 +- .../components/zwave_js/translations/it.json | 6 +++ .../components/zwave_js/translations/no.json | 6 +++ 141 files changed, 2393 insertions(+), 57 deletions(-) create mode 100644 homeassistant/components/anthemav/translations/bg.json create mode 100644 homeassistant/components/climacell/translations/af.json create mode 100644 homeassistant/components/climacell/translations/ca.json create mode 100644 homeassistant/components/climacell/translations/de.json create mode 100644 homeassistant/components/climacell/translations/el.json create mode 100644 homeassistant/components/climacell/translations/en.json create mode 100644 homeassistant/components/climacell/translations/es-419.json create mode 100644 homeassistant/components/climacell/translations/es.json create mode 100644 homeassistant/components/climacell/translations/et.json create mode 100644 homeassistant/components/climacell/translations/fr.json create mode 100644 homeassistant/components/climacell/translations/hu.json create mode 100644 homeassistant/components/climacell/translations/id.json create mode 100644 homeassistant/components/climacell/translations/ja.json create mode 100644 homeassistant/components/climacell/translations/ko.json create mode 100644 homeassistant/components/climacell/translations/nl.json create mode 100644 homeassistant/components/climacell/translations/no.json create mode 100644 homeassistant/components/climacell/translations/pl.json create mode 100644 homeassistant/components/climacell/translations/pt-BR.json create mode 100644 homeassistant/components/climacell/translations/ru.json create mode 100644 homeassistant/components/climacell/translations/sensor.bg.json create mode 100644 homeassistant/components/climacell/translations/sensor.ca.json create mode 100644 homeassistant/components/climacell/translations/sensor.de.json create mode 100644 homeassistant/components/climacell/translations/sensor.el.json create mode 100644 homeassistant/components/climacell/translations/sensor.en.json create mode 100644 homeassistant/components/climacell/translations/sensor.es-419.json create mode 100644 homeassistant/components/climacell/translations/sensor.es.json create mode 100644 homeassistant/components/climacell/translations/sensor.et.json create mode 100644 homeassistant/components/climacell/translations/sensor.fr.json create mode 100644 homeassistant/components/climacell/translations/sensor.he.json create mode 100644 homeassistant/components/climacell/translations/sensor.hu.json create mode 100644 homeassistant/components/climacell/translations/sensor.id.json create mode 100644 homeassistant/components/climacell/translations/sensor.is.json create mode 100644 homeassistant/components/climacell/translations/sensor.ja.json create mode 100644 homeassistant/components/climacell/translations/sensor.ko.json create mode 100644 homeassistant/components/climacell/translations/sensor.lv.json create mode 100644 homeassistant/components/climacell/translations/sensor.nl.json create mode 100644 homeassistant/components/climacell/translations/sensor.no.json create mode 100644 homeassistant/components/climacell/translations/sensor.pl.json create mode 100644 homeassistant/components/climacell/translations/sensor.pt-BR.json create mode 100644 homeassistant/components/climacell/translations/sensor.pt.json create mode 100644 homeassistant/components/climacell/translations/sensor.ru.json create mode 100644 homeassistant/components/climacell/translations/sensor.sk.json create mode 100644 homeassistant/components/climacell/translations/sensor.sv.json create mode 100644 homeassistant/components/climacell/translations/sensor.tr.json create mode 100644 homeassistant/components/climacell/translations/sensor.zh-Hant.json create mode 100644 homeassistant/components/climacell/translations/sv.json create mode 100644 homeassistant/components/climacell/translations/tr.json create mode 100644 homeassistant/components/climacell/translations/zh-Hant.json create mode 100644 homeassistant/components/eight_sleep/translations/bg.json create mode 100644 homeassistant/components/ibeacon/translations/it.json create mode 100644 homeassistant/components/ibeacon/translations/no.json create mode 100644 homeassistant/components/ibeacon/translations/ru.json create mode 100644 homeassistant/components/kegtron/translations/it.json create mode 100644 homeassistant/components/kegtron/translations/no.json create mode 100644 homeassistant/components/kegtron/translations/ru.json create mode 100644 homeassistant/components/keymitt_ble/translations/it.json create mode 100644 homeassistant/components/keymitt_ble/translations/no.json create mode 100644 homeassistant/components/keymitt_ble/translations/pt-BR.json create mode 100644 homeassistant/components/keymitt_ble/translations/ru.json create mode 100644 homeassistant/components/lg_soundbar/translations/bg.json create mode 100644 homeassistant/components/lidarr/translations/it.json create mode 100644 homeassistant/components/lidarr/translations/no.json create mode 100644 homeassistant/components/lidarr/translations/ru.json create mode 100644 homeassistant/components/nextdns/translations/bg.json create mode 100644 homeassistant/components/nibe_heatpump/translations/hu.json create mode 100644 homeassistant/components/nibe_heatpump/translations/it.json create mode 100644 homeassistant/components/nibe_heatpump/translations/no.json create mode 100644 homeassistant/components/nibe_heatpump/translations/pt-BR.json create mode 100644 homeassistant/components/nibe_heatpump/translations/ru.json create mode 100644 homeassistant/components/radarr/translations/bg.json create mode 100644 homeassistant/components/radarr/translations/de.json create mode 100644 homeassistant/components/radarr/translations/el.json create mode 100644 homeassistant/components/radarr/translations/es.json create mode 100644 homeassistant/components/radarr/translations/fr.json create mode 100644 homeassistant/components/radarr/translations/id.json create mode 100644 homeassistant/components/radarr/translations/it.json create mode 100644 homeassistant/components/radarr/translations/no.json create mode 100644 homeassistant/components/radarr/translations/pt-BR.json create mode 100644 homeassistant/components/radarr/translations/ru.json create mode 100644 homeassistant/components/radarr/translations/zh-Hant.json create mode 100644 homeassistant/components/rhasspy/translations/bg.json create mode 100644 homeassistant/components/select/translations/bg.json create mode 100644 homeassistant/components/soundtouch/translations/bg.json diff --git a/homeassistant/components/anthemav/translations/bg.json b/homeassistant/components/anthemav/translations/bg.json new file mode 100644 index 00000000000..cc5f200ef95 --- /dev/null +++ b/homeassistant/components/anthemav/translations/bg.json @@ -0,0 +1,18 @@ +{ + "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" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/automation/translations/bg.json b/homeassistant/components/automation/translations/bg.json index 1e294bff9a7..2765eab3ce3 100644 --- a/homeassistant/components/automation/translations/bg.json +++ b/homeassistant/components/automation/translations/bg.json @@ -1,4 +1,16 @@ { + "issues": { + "service_not_found": { + "fix_flow": { + "step": { + "confirm": { + "title": "{name} \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430 \u043d\u0435\u043f\u043e\u0437\u043d\u0430\u0442\u0430 \u0443\u0441\u043b\u0443\u0433\u0430" + } + } + }, + "title": "{name} \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430 \u043d\u0435\u043f\u043e\u0437\u043d\u0430\u0442\u0430 \u0443\u0441\u043b\u0443\u0433\u0430" + } + }, "state": { "_": { "off": "\u0418\u0437\u043a\u043b\u044e\u0447\u0435\u043d", diff --git a/homeassistant/components/bluetooth/translations/it.json b/homeassistant/components/bluetooth/translations/it.json index fc9ea431d29..6d3adac2f76 100644 --- a/homeassistant/components/bluetooth/translations/it.json +++ b/homeassistant/components/bluetooth/translations/it.json @@ -29,6 +29,12 @@ } } }, + "issues": { + "haos_outdated": { + "description": "Per migliorare l'affidabilit\u00e0 e le prestazioni del Bluetooth, si consiglia vivamente di aggiornare alla versione 9.0 o successiva del sistema operativo Home Assistant.", + "title": "Aggiorna al Home Assistant OS 9.0 o successivo" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/bluetooth/translations/no.json b/homeassistant/components/bluetooth/translations/no.json index 5ab1050a849..687b651eaca 100644 --- a/homeassistant/components/bluetooth/translations/no.json +++ b/homeassistant/components/bluetooth/translations/no.json @@ -29,6 +29,12 @@ } } }, + "issues": { + "haos_outdated": { + "description": "For \u00e5 forbedre Bluetooth-p\u00e5litelighet og ytelse, anbefaler vi p\u00e5 det sterkeste at du oppdaterer til versjon 9.0 eller nyere av Home Assistant-operativsystemet.", + "title": "Oppdater til Home Assistant-operativsystem 9.0 eller nyere" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/bluetooth/translations/ru.json b/homeassistant/components/bluetooth/translations/ru.json index 5b2029bb8b0..1be63589bb4 100644 --- a/homeassistant/components/bluetooth/translations/ru.json +++ b/homeassistant/components/bluetooth/translations/ru.json @@ -29,6 +29,12 @@ } } }, + "issues": { + "haos_outdated": { + "description": "\u0427\u0442\u043e\u0431\u044b \u043f\u043e\u0432\u044b\u0441\u0438\u0442\u044c \u043d\u0430\u0434\u0435\u0436\u043d\u043e\u0441\u0442\u044c \u0438 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u044c Bluetooth, \u043c\u044b \u043d\u0430\u0441\u0442\u043e\u044f\u0442\u0435\u043b\u044c\u043d\u043e \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u043c \u0412\u0430\u043c \u043e\u0431\u043d\u043e\u0432\u0438\u0442\u044c Home Assistant Operating System \u0434\u043e \u0432\u0435\u0440\u0441\u0438\u0438 9.0 \u0438\u043b\u0438 \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0437\u0434\u043d\u0435\u0439.", + "title": "\u041e\u0431\u043d\u043e\u0432\u0438\u0442\u0435 Home Assistant Operating System \u0434\u043e \u0432\u0435\u0440\u0441\u0438\u0438 9.0 \u0438\u043b\u0438 \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0437\u0434\u043d\u0435\u0439" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/braviatv/translations/de.json b/homeassistant/components/braviatv/translations/de.json index 035de7bc060..18863481bc0 100644 --- a/homeassistant/components/braviatv/translations/de.json +++ b/homeassistant/components/braviatv/translations/de.json @@ -2,21 +2,27 @@ "config": { "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", - "no_ip_control": "IP-Steuerung ist auf deinen Fernseher deaktiviert oder der Fernseher wird nicht unterst\u00fctzt." + "no_ip_control": "IP-Steuerung ist auf deinen Fernseher deaktiviert oder der Fernseher wird nicht unterst\u00fctzt.", + "not_bravia_device": "Das Ger\u00e4t ist kein Bravia-Fernseher." }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", "invalid_host": "Ung\u00fcltiger Hostname oder IP-Adresse", "unsupported_model": "Dein TV-Modell wird nicht unterst\u00fctzt." }, "step": { "authorize": { "data": { - "pin": "PIN-Code" + "pin": "PIN-Code", + "use_psk": "PSK-Authentifizierung verwenden" }, - "description": "Gib den auf dem Sony Bravia-Fernseher angezeigten PIN-Code ein. \n\nWenn der PIN-Code nicht angezeigt wird, musst du die Registrierung von Home Assistant auf deinem Fernseher aufheben, gehe daf\u00fcr zu: Einstellungen \u2192 Netzwerk \u2192 Remote - Ger\u00e4teeinstellungen \u2192 Registrierung des entfernten Ger\u00e4ts aufheben.", + "description": "Gib den auf dem Sony Bravia-Fernseher angezeigten PIN-Code ein. \n\nWenn der PIN-Code nicht angezeigt wird, musst du die Registrierung von Home Assistant auf Ihrem Fernseher aufheben, gehe zu: Einstellungen - > Netzwerk - > Remote-Ger\u00e4teeinstellungen - > Remote-Ger\u00e4t abmelden. \n\nDu kannst PSK (Pre-Shared-Key) anstelle der PIN verwenden. PSK ist ein benutzerdefinierter geheimer Schl\u00fcssel, der f\u00fcr die Zugriffskontrolle verwendet wird. Diese Authentifizierungsmethode wird als stabiler empfohlen. Um PSK auf deinem Fernseher zu aktivieren, gehe zu: Einstellungen - > Netzwerk - > Heimnetzwerk-Setup - > IP-Steuerung. Aktiviere dann das Kontrollk\u00e4stchen \u00abPSK-Authentifizierung verwenden\u00bb und gib deinen PSK anstelle der PIN ein.", "title": "Autorisiere Sony Bravia TV" }, + "confirm": { + "description": "M\u00f6chtest Du mit der Einrichtung beginnen?" + }, "user": { "data": { "host": "Host" diff --git a/homeassistant/components/braviatv/translations/es.json b/homeassistant/components/braviatv/translations/es.json index 86436249717..325ccb4c535 100644 --- a/homeassistant/components/braviatv/translations/es.json +++ b/homeassistant/components/braviatv/translations/es.json @@ -2,21 +2,27 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "no_ip_control": "El Control IP est\u00e1 desactivado en tu TV o la TV no es compatible." + "no_ip_control": "El Control IP est\u00e1 desactivado en tu TV o la TV no es compatible.", + "not_bravia_device": "El dispositivo no es una TV Bravia." }, "error": { "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "invalid_host": "Nombre de host o direcci\u00f3n IP no v\u00e1lidos", "unsupported_model": "Tu modelo de TV no es compatible." }, "step": { "authorize": { "data": { - "pin": "C\u00f3digo PIN" + "pin": "C\u00f3digo PIN", + "use_psk": "Usar autenticaci\u00f3n PSK" }, - "description": "Introduce el c\u00f3digo PIN que se muestra en Sony Bravia TV. \n\nSi no se muestra el c\u00f3digo PIN, debes cancelar el registro de Home Assistant en tu TV, ve a: Configuraci\u00f3n - > Red - > Configuraci\u00f3n del dispositivo remoto - > Cancelar el registro del dispositivo remoto.", + "description": "Introduce el c\u00f3digo PIN que se muestra en la TV Sony Bravia. \n\nSi no se muestra el c\u00f3digo PIN, debes cancelar el registro de Home Assistant en tu TV, ve a: Configuraci\u00f3n - > Red - > Configuraci\u00f3n del dispositivo remoto - > Cancelar el registro del dispositivo remoto. \n\nPuedes usar PSK (clave precompartida) en lugar de PIN. PSK es una clave secreta definida por el usuario que se utiliza para el control de acceso. Este m\u00e9todo de autenticaci\u00f3n se recomienda como m\u00e1s estable. Para habilitar PSK en tu TV, ve a: Configuraci\u00f3n - > Red - > Configuraci\u00f3n de red dom\u00e9stica - > Control de IP. Luego marca la casilla \u00abUsar autenticaci\u00f3n PSK\u00bb e introduce tu PSK en lugar de PIN.", "title": "Autorizar Sony Bravia TV" }, + "confirm": { + "description": "\u00bfQuieres iniciar la configuraci\u00f3n?" + }, "user": { "data": { "host": "Host" diff --git a/homeassistant/components/braviatv/translations/fr.json b/homeassistant/components/braviatv/translations/fr.json index 0290dad6857..73a32d0a06a 100644 --- a/homeassistant/components/braviatv/translations/fr.json +++ b/homeassistant/components/braviatv/translations/fr.json @@ -2,21 +2,27 @@ "config": { "abort": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", - "no_ip_control": "Le contr\u00f4le IP est d\u00e9sactiv\u00e9 sur votre t\u00e9l\u00e9viseur ou le t\u00e9l\u00e9viseur n'est pas pris en charge." + "no_ip_control": "Le contr\u00f4le IP est d\u00e9sactiv\u00e9 sur votre t\u00e9l\u00e9viseur ou le t\u00e9l\u00e9viseur n'est pas pris en charge.", + "not_bravia_device": "L'appareil n'est pas un t\u00e9l\u00e9viseur Bravia." }, "error": { "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification non valide", "invalid_host": "Nom d'h\u00f4te ou adresse IP non valide", "unsupported_model": "Votre mod\u00e8le de t\u00e9l\u00e9viseur n'est pas pris en charge." }, "step": { "authorize": { "data": { - "pin": "Code PIN" + "pin": "Code PIN", + "use_psk": "Utiliser l'authentification PSK" }, "description": "Saisissez le code PIN affich\u00e9 sur le t\u00e9l\u00e9viseur Sony Bravia. \n\nSi le code PIN n'est pas affich\u00e9, vous devez d\u00e9senregistrer Home Assistant de votre t\u00e9l\u00e9viseur, allez dans: Param\u00e8tres - > R\u00e9seau - > Param\u00e8tres de l'appareil distant - > Annuler l'enregistrement de l'appareil distant.", "title": "Autoriser Sony Bravia TV" }, + "confirm": { + "description": "Voulez-vous commencer la configuration\u00a0?" + }, "user": { "data": { "host": "H\u00f4te" diff --git a/homeassistant/components/braviatv/translations/id.json b/homeassistant/components/braviatv/translations/id.json index e387a2113b0..63b4353aefc 100644 --- a/homeassistant/components/braviatv/translations/id.json +++ b/homeassistant/components/braviatv/translations/id.json @@ -2,21 +2,27 @@ "config": { "abort": { "already_configured": "Perangkat sudah dikonfigurasi", - "no_ip_control": "Kontrol IP dinonaktifkan di TV Anda atau TV tidak didukung." + "no_ip_control": "Kontrol IP dinonaktifkan di TV Anda atau TV tidak didukung.", + "not_bravia_device": "Perangkat ini bukan TV Bravia." }, "error": { "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", "invalid_host": "Nama host atau alamat IP tidak valid", "unsupported_model": "Model TV Anda tidak didukung." }, "step": { "authorize": { "data": { - "pin": "Kode PIN" + "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.", + "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": { + "description": "Ingin memulai penyiapan?" + }, "user": { "data": { "host": "Host" diff --git a/homeassistant/components/braviatv/translations/it.json b/homeassistant/components/braviatv/translations/it.json index 3dd57d1359a..8ac0b2d7df9 100644 --- a/homeassistant/components/braviatv/translations/it.json +++ b/homeassistant/components/braviatv/translations/it.json @@ -2,21 +2,27 @@ "config": { "abort": { "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", - "no_ip_control": "Il controllo IP \u00e8 disabilitato sulla TV o la TV non \u00e8 supportata." + "no_ip_control": "Il controllo IP \u00e8 disabilitato sulla TV o la TV non \u00e8 supportata.", + "not_bravia_device": "Il dispositivo non \u00e8 una TV Bravia." }, "error": { "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", "invalid_host": "Nome host o indirizzo IP non valido", "unsupported_model": "Il tuo modello TV non \u00e8 supportato." }, "step": { "authorize": { "data": { - "pin": "Codice PIN" + "pin": "Codice PIN", + "use_psk": "Usa l'autenticazione PSK" }, - "description": "Immetti il codice PIN visualizzato sul Sony Bravia TV. \n\nSe il codice PIN non viene visualizzato, devi annullare la registrazione di Home Assistant sul televisore, vai su: Impostazioni - > Rete - > Impostazioni dispositivo remoto - > Annulla registrazione dispositivo remoto.", + "description": "Inserisci il codice PIN mostrato sul Sony Bravia TV. \n\nSe il codice PIN non viene visualizzato, devi annullare la registrazione di Home Assistant sulla TV, vai su: Impostazioni -> Rete -> Impostazioni dispositivo remoto -> Annulla registrazione dispositivo remoto. \n\nPuoi usare PSK (Pre-Shared-Key) invece del PIN. PSK \u00e8 una chiave segreta definita dall'utente utilizzata per il controllo degli accessi. Questo metodo di autenticazione \u00e8 consigliato come pi\u00f9 stabile. Per abilitare PSK sulla tua TV, vai su: Impostazioni -> Rete -> Configurazione rete domestica -> Controllo IP. Quindi seleziona la casella \u00abUtilizza l'autenticazione PSK\u00bb e inserisci la tua PSK anzich\u00e9 il PIN.", "title": "Autorizza Sony Bravia TV" }, + "confirm": { + "description": "Vuoi iniziare la configurazione?" + }, "user": { "data": { "host": "Host" diff --git a/homeassistant/components/braviatv/translations/ru.json b/homeassistant/components/braviatv/translations/ru.json index 046a46c5ae4..f191e3607cc 100644 --- a/homeassistant/components/braviatv/translations/ru.json +++ b/homeassistant/components/braviatv/translations/ru.json @@ -2,21 +2,27 @@ "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.", - "no_ip_control": "\u041d\u0430 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0435 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u043f\u043e IP, \u043b\u0438\u0431\u043e \u044d\u0442\u0430 \u043c\u043e\u0434\u0435\u043b\u044c \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f." + "no_ip_control": "\u041d\u0430 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0435 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u043f\u043e IP, \u043b\u0438\u0431\u043e \u044d\u0442\u0430 \u043c\u043e\u0434\u0435\u043b\u044c \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f.", + "not_bravia_device": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 Bravia." }, "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.", "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": { "authorize": { "data": { - "pin": "PIN-\u043a\u043e\u0434" + "pin": "PIN-\u043a\u043e\u0434", + "use_psk": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c PSK-\u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e" }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 PIN-\u043a\u043e\u0434, \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u044e\u0449\u0438\u0439\u0441\u044f \u043d\u0430 \u044d\u043a\u0440\u0430\u043d\u0435 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430 Sony Bravia. \n\n\u0415\u0441\u043b\u0438 \u0412\u044b \u043d\u0435 \u0432\u0438\u0434\u0438\u0442\u0435 PIN-\u043a\u043e\u0434, \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u043e\u0442\u043c\u0435\u043d\u0438\u0442\u044c \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u044e Home Assistant \u043d\u0430 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0435. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u0432 \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 -> \u0421\u0435\u0442\u044c -> \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0434\u0430\u043b\u0435\u043d\u043d\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 -> \u041e\u0442\u043c\u0435\u043d\u0438\u0442\u044c \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u044e \u0443\u0434\u0430\u043b\u0435\u043d\u043d\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430.", + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 PIN-\u043a\u043e\u0434, \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u044e\u0449\u0438\u0439\u0441\u044f \u043d\u0430 \u044d\u043a\u0440\u0430\u043d\u0435 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430 Sony Bravia. \n\n\u0415\u0441\u043b\u0438 \u0412\u044b \u043d\u0435 \u0432\u0438\u0434\u0438\u0442\u0435 PIN-\u043a\u043e\u0434, \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u043e\u0442\u043c\u0435\u043d\u0438\u0442\u044c \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u044e Home Assistant \u043d\u0430 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0435. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u0432 \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 -> \u0421\u0435\u0442\u044c -> \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0434\u0430\u043b\u0435\u043d\u043d\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 -> \u041e\u0442\u043c\u0435\u043d\u0438\u0442\u044c \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u044e \u0443\u0434\u0430\u043b\u0435\u043d\u043d\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430.\n\n\u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c PSK (Pre-Shared-Key) \u0432\u043c\u0435\u0441\u0442\u043e PIN-\u043a\u043e\u0434\u0430. PSK \u2014 \u044d\u0442\u043e \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u043c\u044b\u0439 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u043c \u0441\u0435\u043a\u0440\u0435\u0442\u043d\u044b\u0439 \u043a\u043b\u044e\u0447, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u044b\u0439 \u0434\u043b\u044f \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u0434\u043e\u0441\u0442\u0443\u043f\u043e\u043c. \u042d\u0442\u043e\u0442 \u043c\u0435\u0442\u043e\u0434 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u0442\u0441\u044f \u043a\u0430\u043a \u0431\u043e\u043b\u0435\u0435 \u0441\u0442\u0430\u0431\u0438\u043b\u044c\u043d\u044b\u0439. \u0427\u0442\u043e\u0431\u044b \u0432\u043a\u043b\u044e\u0447\u0438\u0442\u044c PSK \u043d\u0430 \u0412\u0430\u0448\u0435\u043c \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0435, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u0432 \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 - > \u0421\u0435\u0442\u044c - > \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043e\u043c\u0430\u0448\u043d\u0435\u0439 \u0441\u0435\u0442\u0438 - > \u0423\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435 IP. \u0417\u0430\u0442\u0435\u043c \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u0435 \u0444\u043b\u0430\u0436\u043e\u043a \u00ab\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e PSK\u00bb \u0438 \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u0441\u0432\u043e\u0439 PSK \u0432\u043c\u0435\u0441\u0442\u043e PIN-\u043a\u043e\u0434\u0430.", "title": "\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430 Sony Bravia" }, + "confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0447\u0430\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443?" + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442" diff --git a/homeassistant/components/braviatv/translations/zh-Hant.json b/homeassistant/components/braviatv/translations/zh-Hant.json index 1fb0931d4b6..9753715eec1 100644 --- a/homeassistant/components/braviatv/translations/zh-Hant.json +++ b/homeassistant/components/braviatv/translations/zh-Hant.json @@ -2,21 +2,27 @@ "config": { "abort": { "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "no_ip_control": "\u96fb\u8996\u4e0a\u7684 IP \u5df2\u95dc\u9589\u6216\u4e0d\u652f\u63f4\u6b64\u6b3e\u96fb\u8996\u3002" + "no_ip_control": "\u96fb\u8996\u4e0a\u7684 IP \u5df2\u95dc\u9589\u6216\u4e0d\u652f\u63f4\u6b64\u6b3e\u96fb\u8996\u3002", + "not_bravia_device": "\u88dd\u7f6e\u4e26\u975e Bravia TV\u3002" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "invalid_host": "\u7121\u6548\u4e3b\u6a5f\u540d\u7a31\u6216 IP \u4f4d\u5740", "unsupported_model": "\u4e0d\u652f\u63f4\u6b64\u6b3e\u96fb\u8996\u578b\u865f\u3002" }, "step": { "authorize": { "data": { - "pin": "PIN \u78bc" + "pin": "PIN \u78bc", + "use_psk": "\u4f7f\u7528 PSK \u9a57\u8b49" }, - "description": "\u8f38\u5165 Sony Bravia \u96fb\u8996\u6240\u986f\u793a\u4e4b PIN \u78bc\u3002\n\n\u5047\u5982 PIN \u78bc\u672a\u986f\u793a\uff0c\u5fc5\u9808\u5148\u65bc\u96fb\u8996\u89e3\u9664 Home Assistant \u8a3b\u518a\uff0c\u6b65\u9a5f\u70ba\uff1a\u8a2d\u5b9a -> \u7db2\u8def -> \u9060\u7aef\u88dd\u7f6e\u8a2d\u5b9a -> \u89e3\u9664\u9060\u7aef\u88dd\u7f6e\u8a3b\u518a\u3002", + "description": "\u8f38\u5165 Sony Bravia \u96fb\u8996\u6240\u986f\u793a\u4e4b PIN \u78bc\u3002\n\n\u5047\u5982 PIN \u78bc\u672a\u986f\u793a\uff0c\u5fc5\u9808\u5148\u65bc\u96fb\u8996\u89e3\u9664 Home Assistant \u8a3b\u518a\uff0c\u6b65\u9a5f\u70ba\uff1a\u8a2d\u5b9a -> \u7db2\u8def -> \u9060\u7aef\u88dd\u7f6e\u8a2d\u5b9a -> \u89e3\u9664\u9060\u7aef\u88dd\u7f6e\u8a3b\u518a\u3002\n\n\u53ef\u4f7f\u7528 PSK (Pre-Shared-Key) \u53d6\u4ee3 PIN \u78bc\u3002PSK \u70ba\u4f7f\u7528\u8005\u81ea\u5b9a\u5bc6\u9470\u7528\u4ee5\u5b58\u53d6\u63a7\u5236\u3002\u5efa\u8b70\u63a1\u7528\u6b64\u8a8d\u8b49\u65b9\u5f0f\u66f4\u70ba\u7a69\u5b9a\u3002\u6b32\u65bc\u96fb\u8996\u555f\u7528 PSK\u3002\u6b65\u9a5f\u70ba\uff1a\u8a2d\u5b9a -> \u7db2\u8def -> \u5bb6\u5ead\u7db2\u8def\u8a2d\u5b9a -> IP \u63a7\u5236\u3002\u7136\u5f8c\u52fe\u9078 \u00ab\u4f7f\u7528 PSK \u8a8d\u8b49\u00bb \u4e26\u8f38\u5165 PSK \u78bc\u3002", "title": "\u8a8d\u8b49 Sony Bravia \u96fb\u8996" }, + "confirm": { + "description": "\u662f\u5426\u8981\u958b\u59cb\u8a2d\u5b9a\uff1f" + }, "user": { "data": { "host": "\u4e3b\u6a5f\u7aef" diff --git a/homeassistant/components/climacell/translations/af.json b/homeassistant/components/climacell/translations/af.json new file mode 100644 index 00000000000..d05e07e4eff --- /dev/null +++ b/homeassistant/components/climacell/translations/af.json @@ -0,0 +1,9 @@ +{ + "options": { + "step": { + "init": { + "title": "Update [%key:component::climacell::title%] opties" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/ca.json b/homeassistant/components/climacell/translations/ca.json new file mode 100644 index 00000000000..2b6abb46737 --- /dev/null +++ b/homeassistant/components/climacell/translations/ca.json @@ -0,0 +1,13 @@ +{ + "options": { + "step": { + "init": { + "data": { + "timestep": "Minuts entre previsions NowCast" + }, + "description": "Si decideixes activar l'entitat de previsi\u00f3 `nowcast`, podr\u00e0s configurar l'interval en minuts entre cada previsi\u00f3. El nombre de previsions proporcionades dep\u00e8n d'aquest interval de minuts.", + "title": "Actualitzaci\u00f3 d'opcions de ClimaCell" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/de.json b/homeassistant/components/climacell/translations/de.json new file mode 100644 index 00000000000..7c3e929dde2 --- /dev/null +++ b/homeassistant/components/climacell/translations/de.json @@ -0,0 +1,13 @@ +{ + "options": { + "step": { + "init": { + "data": { + "timestep": "Minuten zwischen den NowCast Kurzvorhersagen" + }, + "description": "Wenn du die Vorhersage-Entitit\u00e4t \"Kurzvorhersage\" aktivierst, kannst du die Anzahl der Minuten zwischen den einzelnen Vorhersagen konfigurieren. Die Anzahl der bereitgestellten Vorhersagen h\u00e4ngt von der Anzahl der zwischen den Vorhersagen gew\u00e4hlten Minuten ab.", + "title": "ClimaCell-Optionen aktualisieren" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/el.json b/homeassistant/components/climacell/translations/el.json new file mode 100644 index 00000000000..392573f693c --- /dev/null +++ b/homeassistant/components/climacell/translations/el.json @@ -0,0 +1,13 @@ +{ + "options": { + "step": { + "init": { + "data": { + "timestep": "\u039b\u03b5\u03c0\u03c4\u03ac \u03bc\u03b5\u03c4\u03b1\u03be\u03cd \u03c4\u03c9\u03bd \u03b4\u03b5\u03bb\u03c4\u03af\u03c9\u03bd NowCast" + }, + "description": "\u0395\u03ac\u03bd \u03b5\u03c0\u03b9\u03bb\u03ad\u03be\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 \u03b4\u03b5\u03bb\u03c4\u03af\u03c9\u03bd 'nowcast', \u03bc\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc \u03c4\u03c9\u03bd \u03bb\u03b5\u03c0\u03c4\u03ce\u03bd \u03bc\u03b5\u03c4\u03b1\u03be\u03cd \u03ba\u03ac\u03b8\u03b5 \u03b4\u03b5\u03bb\u03c4\u03af\u03bf\u03c5. \u039f \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03c4\u03c9\u03bd \u03b4\u03b5\u03bb\u03c4\u03af\u03c9\u03bd \u03c0\u03bf\u03c5 \u03c0\u03b1\u03c1\u03ad\u03c7\u03bf\u03bd\u03c4\u03b1\u03b9 \u03b5\u03be\u03b1\u03c1\u03c4\u03ac\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf\u03bd \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc \u03c4\u03c9\u03bd \u03bb\u03b5\u03c0\u03c4\u03ce\u03bd \u03c0\u03bf\u03c5 \u03b5\u03c0\u03b9\u03bb\u03ad\u03b3\u03bf\u03bd\u03c4\u03b1\u03b9 \u03bc\u03b5\u03c4\u03b1\u03be\u03cd \u03c4\u03c9\u03bd \u03b4\u03b5\u03bb\u03c4\u03af\u03c9\u03bd.", + "title": "\u0395\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ce\u03bd ClimaCell" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/en.json b/homeassistant/components/climacell/translations/en.json new file mode 100644 index 00000000000..a35be85d5b2 --- /dev/null +++ b/homeassistant/components/climacell/translations/en.json @@ -0,0 +1,13 @@ +{ + "options": { + "step": { + "init": { + "data": { + "timestep": "Min. Between NowCast Forecasts" + }, + "description": "If you choose to enable the `nowcast` forecast entity, you can configure the number of minutes between each forecast. The number of forecasts provided depends on the number of minutes chosen between forecasts.", + "title": "Update ClimaCell Options" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/es-419.json b/homeassistant/components/climacell/translations/es-419.json new file mode 100644 index 00000000000..449ad1ba367 --- /dev/null +++ b/homeassistant/components/climacell/translations/es-419.json @@ -0,0 +1,13 @@ +{ + "options": { + "step": { + "init": { + "data": { + "timestep": "Min. entre pron\u00f3sticos de NowCast" + }, + "description": "Si elige habilitar la entidad de pron\u00f3stico \"nowcast\", puede configurar el n\u00famero de minutos entre cada pron\u00f3stico. El n\u00famero de pron\u00f3sticos proporcionados depende del n\u00famero de minutos elegidos entre los pron\u00f3sticos.", + "title": "Actualizar opciones de ClimaCell" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/es.json b/homeassistant/components/climacell/translations/es.json new file mode 100644 index 00000000000..270d72bd58c --- /dev/null +++ b/homeassistant/components/climacell/translations/es.json @@ -0,0 +1,13 @@ +{ + "options": { + "step": { + "init": { + "data": { + "timestep": "Min. entre pron\u00f3sticos de NowCast" + }, + "description": "Si eliges habilitar la entidad de pron\u00f3stico `nowcast`, puedes configurar la cantidad de minutos entre cada pron\u00f3stico. La cantidad de pron\u00f3sticos proporcionados depende de la cantidad de minutos elegidos entre los pron\u00f3sticos.", + "title": "Actualizar opciones de ClimaCell" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/et.json b/homeassistant/components/climacell/translations/et.json new file mode 100644 index 00000000000..5d915a87d80 --- /dev/null +++ b/homeassistant/components/climacell/translations/et.json @@ -0,0 +1,13 @@ +{ + "options": { + "step": { + "init": { + "data": { + "timestep": "Minuteid NowCasti prognooside vahel" + }, + "description": "Kui otsustad lubada \"nowcast\" prognoosi\u00fcksuse, saad seadistada minutite arvu iga prognoosi vahel. Esitatavate prognooside arv s\u00f5ltub prognooside vahel valitud minutite arvust.", + "title": "V\u00e4rskenda [%key:component::climacell::title%] suvandeid" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/fr.json b/homeassistant/components/climacell/translations/fr.json new file mode 100644 index 00000000000..b2c1285ecc9 --- /dev/null +++ b/homeassistant/components/climacell/translations/fr.json @@ -0,0 +1,13 @@ +{ + "options": { + "step": { + "init": { + "data": { + "timestep": "Min. entre les pr\u00e9visions NowCast" + }, + "description": "Si vous choisissez d'activer l'entit\u00e9 de pr\u00e9vision \u00ab\u00a0nowcast\u00a0\u00bb, vous pouvez configurer le nombre de minutes entre chaque pr\u00e9vision. Le nombre de pr\u00e9visions fournies d\u00e9pend du nombre de minutes choisies entre les pr\u00e9visions.", + "title": "Mettre \u00e0 jour les options ClimaCell" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/hu.json b/homeassistant/components/climacell/translations/hu.json new file mode 100644 index 00000000000..4cad1eaaa0f --- /dev/null +++ b/homeassistant/components/climacell/translations/hu.json @@ -0,0 +1,13 @@ +{ + "options": { + "step": { + "init": { + "data": { + "timestep": "Min. A NowCast el\u0151rejelz\u00e9sek k\u00f6z\u00f6tt" + }, + "description": "Ha a `nowcast` el\u0151rejelz\u00e9si entit\u00e1s enged\u00e9lyez\u00e9s\u00e9t v\u00e1lasztja, be\u00e1ll\u00edthatja az egyes el\u0151rejelz\u00e9sek k\u00f6z\u00f6tti percek sz\u00e1m\u00e1t. A megadott el\u0151rejelz\u00e9sek sz\u00e1ma az el\u0151rejelz\u00e9sek k\u00f6z\u00f6tt kiv\u00e1lasztott percek sz\u00e1m\u00e1t\u00f3l f\u00fcgg.", + "title": "ClimaCell be\u00e1ll\u00edt\u00e1sok friss\u00edt\u00e9se" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/id.json b/homeassistant/components/climacell/translations/id.json new file mode 100644 index 00000000000..4d020351665 --- /dev/null +++ b/homeassistant/components/climacell/translations/id.json @@ -0,0 +1,13 @@ +{ + "options": { + "step": { + "init": { + "data": { + "timestep": "Jarak Interval Prakiraan NowCast dalam Menit" + }, + "description": "Jika Anda memilih untuk mengaktifkan entitas prakiraan `nowcast`, Anda dapat mengonfigurasi jarak interval prakiraan dalam menit. Jumlah prakiraan yang diberikan tergantung pada nilai interval yang dipilih.", + "title": "Perbarui Opsi ClimaCell" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/ja.json b/homeassistant/components/climacell/translations/ja.json new file mode 100644 index 00000000000..e2742d11435 --- /dev/null +++ b/homeassistant/components/climacell/translations/ja.json @@ -0,0 +1,13 @@ +{ + "options": { + "step": { + "init": { + "data": { + "timestep": "\u6700\u5c0f\u3002 NowCast \u4e88\u6e2c\u306e\u9593" + }, + "description": "`nowcast` forecast(\u4e88\u6e2c) \u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u3092\u6709\u52b9\u306b\u3059\u308b\u3053\u3068\u3092\u9078\u629e\u3057\u305f\u5834\u5408\u3001\u5404\u4e88\u6e2c\u9593\u306e\u5206\u6570\u3092\u8a2d\u5b9a\u3059\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u3059\u3002\u63d0\u4f9b\u3055\u308c\u308bforecast(\u4e88\u6e2c)\u306e\u6570\u306f\u3001forecast(\u4e88\u6e2c)\u306e\u9593\u306b\u9078\u629e\u3057\u305f\u5206\u6570\u306b\u4f9d\u5b58\u3057\u307e\u3059\u3002", + "title": "ClimaCell \u30aa\u30d7\u30b7\u30e7\u30f3\u306e\u66f4\u65b0" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/ko.json b/homeassistant/components/climacell/translations/ko.json new file mode 100644 index 00000000000..8accc07410d --- /dev/null +++ b/homeassistant/components/climacell/translations/ko.json @@ -0,0 +1,13 @@ +{ + "options": { + "step": { + "init": { + "data": { + "timestep": "\ub2e8\uae30\uc608\uce21 \uc77c\uae30\uc608\ubcf4 \uac04 \ucd5c\uc18c \uc2dc\uac04" + }, + "description": "`nowcast` \uc77c\uae30\uc608\ubcf4 \uad6c\uc131\uc694\uc18c\ub97c \uc0ac\uc6a9\ud558\ub3c4\ub85d \uc120\ud0dd\ud55c \uacbd\uc6b0 \uac01 \uc77c\uae30\uc608\ubcf4 \uc0ac\uc774\uc758 \uc2dc\uac04(\ubd84)\uc744 \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uc81c\uacf5\ub41c \uc77c\uae30\uc608\ubcf4 \ud69f\uc218\ub294 \uc608\uce21 \uac04 \uc120\ud0dd\ud55c \uc2dc\uac04(\ubd84)\uc5d0 \ub530\ub77c \ub2ec\ub77c\uc9d1\ub2c8\ub2e4.", + "title": "[%key:component::climacell::title%] \uc635\uc158 \uc5c5\ub370\uc774\ud2b8\ud558\uae30" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/nl.json b/homeassistant/components/climacell/translations/nl.json new file mode 100644 index 00000000000..a895fa8234d --- /dev/null +++ b/homeassistant/components/climacell/translations/nl.json @@ -0,0 +1,13 @@ +{ + "options": { + "step": { + "init": { + "data": { + "timestep": "Min. Tussen NowCast-voorspellingen" + }, + "description": "Als u ervoor kiest om de `nowcast` voorspellingsentiteit in te schakelen, kan u het aantal minuten tussen elke voorspelling configureren. Het aantal voorspellingen hangt af van het aantal gekozen minuten tussen de voorspellingen.", + "title": "Update ClimaCell Opties" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/no.json b/homeassistant/components/climacell/translations/no.json new file mode 100644 index 00000000000..9f050624967 --- /dev/null +++ b/homeassistant/components/climacell/translations/no.json @@ -0,0 +1,13 @@ +{ + "options": { + "step": { + "init": { + "data": { + "timestep": "Min. mellom NowCast prognoser" + }, + "description": "Hvis du velger \u00e5 aktivere \u00abnowcast\u00bb -varselentiteten, kan du konfigurere antall minutter mellom hver prognose. Antall angitte prognoser avhenger av antall minutter som er valgt mellom prognosene.", + "title": "Oppdater ClimaCell-alternativer" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/pl.json b/homeassistant/components/climacell/translations/pl.json new file mode 100644 index 00000000000..5f69764ffab --- /dev/null +++ b/homeassistant/components/climacell/translations/pl.json @@ -0,0 +1,13 @@ +{ + "options": { + "step": { + "init": { + "data": { + "timestep": "Czas (min) mi\u0119dzy prognozami NowCast" + }, + "description": "Je\u015bli zdecydujesz si\u0119 w\u0142\u0105czy\u0107 encj\u0119 prognozy \u201enowcast\u201d, mo\u017cesz skonfigurowa\u0107 liczb\u0119 minut mi\u0119dzy ka\u017cd\u0105 prognoz\u0105. Liczba dostarczonych prognoz zale\u017cy od liczby minut wybranych mi\u0119dzy prognozami.", + "title": "Opcje aktualizacji ClimaCell" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/pt-BR.json b/homeassistant/components/climacell/translations/pt-BR.json new file mode 100644 index 00000000000..b7e71d45971 --- /dev/null +++ b/homeassistant/components/climacell/translations/pt-BR.json @@ -0,0 +1,13 @@ +{ + "options": { + "step": { + "init": { + "data": { + "timestep": "M\u00ednimo entre previs\u00f5es NowCast" + }, + "description": "Se voc\u00ea optar por ativar a entidade de previs\u00e3o `nowcast`, poder\u00e1 configurar o n\u00famero de minutos entre cada previs\u00e3o. O n\u00famero de previs\u00f5es fornecidas depende do n\u00famero de minutos escolhidos entre as previs\u00f5es.", + "title": "Atualizar as op\u00e7\u00f5es do ClimaCell" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/ru.json b/homeassistant/components/climacell/translations/ru.json new file mode 100644 index 00000000000..9f3219ce4d6 --- /dev/null +++ b/homeassistant/components/climacell/translations/ru.json @@ -0,0 +1,13 @@ +{ + "options": { + "step": { + "init": { + "data": { + "timestep": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f (\u0432 \u043c\u0438\u043d\u0443\u0442\u0430\u0445)" + }, + "description": "\u0415\u0441\u043b\u0438 \u0412\u044b \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0443\u0435\u0442\u0435 \u043e\u0431\u044a\u0435\u043a\u0442 \u043f\u0440\u043e\u0433\u043d\u043e\u0437\u0430 'nowcast', \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0438\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f \u043f\u0440\u043e\u0433\u043d\u043e\u0437\u0430.", + "title": "\u041e\u0431\u043d\u043e\u0432\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 ClimaCell" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.bg.json b/homeassistant/components/climacell/translations/sensor.bg.json new file mode 100644 index 00000000000..04f393f1d99 --- /dev/null +++ b/homeassistant/components/climacell/translations/sensor.bg.json @@ -0,0 +1,8 @@ +{ + "state": { + "climacell__precipitation_type": { + "rain": "\u0414\u044a\u0436\u0434", + "snow": "\u0421\u043d\u044f\u0433" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.ca.json b/homeassistant/components/climacell/translations/sensor.ca.json new file mode 100644 index 00000000000..359857925da --- /dev/null +++ b/homeassistant/components/climacell/translations/sensor.ca.json @@ -0,0 +1,27 @@ +{ + "state": { + "climacell__health_concern": { + "good": "Bo", + "hazardous": "Perill\u00f3s", + "moderate": "Moderat", + "unhealthy": "Poc saludable", + "unhealthy_for_sensitive_groups": "No saludable per a grups sensibles", + "very_unhealthy": "Gens saludable" + }, + "climacell__pollen_index": { + "high": "Alt", + "low": "Baix", + "medium": "Mitj\u00e0", + "none": "Cap", + "very_high": "Molt alt", + "very_low": "Molt baix" + }, + "climacell__precipitation_type": { + "freezing_rain": "Pluja congelada", + "ice_pellets": "Gran\u00eds", + "none": "Cap", + "rain": "Pluja", + "snow": "Neu" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.de.json b/homeassistant/components/climacell/translations/sensor.de.json new file mode 100644 index 00000000000..93a1e5e8e98 --- /dev/null +++ b/homeassistant/components/climacell/translations/sensor.de.json @@ -0,0 +1,27 @@ +{ + "state": { + "climacell__health_concern": { + "good": "Gut", + "hazardous": "Gef\u00e4hrlich", + "moderate": "M\u00e4\u00dfig", + "unhealthy": "Ungesund", + "unhealthy_for_sensitive_groups": "Ungesund f\u00fcr sensible Gruppen", + "very_unhealthy": "Sehr ungesund" + }, + "climacell__pollen_index": { + "high": "Hoch", + "low": "Niedrig", + "medium": "Mittel", + "none": "Keine", + "very_high": "Sehr hoch", + "very_low": "Sehr niedrig" + }, + "climacell__precipitation_type": { + "freezing_rain": "Gefrierender Regen", + "ice_pellets": "Graupel", + "none": "Keine", + "rain": "Regen", + "snow": "Schnee" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.el.json b/homeassistant/components/climacell/translations/sensor.el.json new file mode 100644 index 00000000000..facd86ed7c6 --- /dev/null +++ b/homeassistant/components/climacell/translations/sensor.el.json @@ -0,0 +1,27 @@ +{ + "state": { + "climacell__health_concern": { + "good": "\u039a\u03b1\u03bb\u03cc", + "hazardous": "\u0395\u03c0\u03b9\u03ba\u03af\u03bd\u03b4\u03c5\u03bd\u03bf", + "moderate": "\u039c\u03ad\u03c4\u03c1\u03b9\u03bf", + "unhealthy": "\u0391\u03bd\u03b8\u03c5\u03b3\u03b9\u03b5\u03b9\u03bd\u03cc", + "unhealthy_for_sensitive_groups": "\u0391\u03bd\u03b8\u03c5\u03b3\u03b9\u03b5\u03b9\u03bd\u03cc \u03b3\u03b9\u03b1 \u03b5\u03c5\u03b1\u03af\u03c3\u03b8\u03b7\u03c4\u03b5\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b5\u03c2", + "very_unhealthy": "\u03a0\u03bf\u03bb\u03cd \u0391\u03bd\u03b8\u03c5\u03b3\u03b9\u03b5\u03b9\u03bd\u03cc" + }, + "climacell__pollen_index": { + "high": "\u03a5\u03c8\u03b7\u03bb\u03cc", + "low": "\u03a7\u03b1\u03bc\u03b7\u03bb\u03cc", + "medium": "\u039c\u03b5\u03c3\u03b1\u03af\u03bf", + "none": "\u03a4\u03af\u03c0\u03bf\u03c4\u03b1", + "very_high": "\u03a0\u03bf\u03bb\u03cd \u03c5\u03c8\u03b7\u03bb\u03cc", + "very_low": "\u03a0\u03bf\u03bb\u03cd \u03c7\u03b1\u03bc\u03b7\u03bb\u03cc" + }, + "climacell__precipitation_type": { + "freezing_rain": "\u03a0\u03b1\u03b3\u03c9\u03bc\u03ad\u03bd\u03b7 \u03b2\u03c1\u03bf\u03c7\u03ae", + "ice_pellets": "\u03a0\u03ad\u03bb\u03bb\u03b5\u03c4 \u03c0\u03ac\u03b3\u03bf\u03c5", + "none": "\u03a4\u03af\u03c0\u03bf\u03c4\u03b1", + "rain": "\u0392\u03c1\u03bf\u03c7\u03ae", + "snow": "\u03a7\u03b9\u03cc\u03bd\u03b9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.en.json b/homeassistant/components/climacell/translations/sensor.en.json new file mode 100644 index 00000000000..0cb1d27aaec --- /dev/null +++ b/homeassistant/components/climacell/translations/sensor.en.json @@ -0,0 +1,27 @@ +{ + "state": { + "climacell__health_concern": { + "good": "Good", + "hazardous": "Hazardous", + "moderate": "Moderate", + "unhealthy": "Unhealthy", + "unhealthy_for_sensitive_groups": "Unhealthy for Sensitive Groups", + "very_unhealthy": "Very Unhealthy" + }, + "climacell__pollen_index": { + "high": "High", + "low": "Low", + "medium": "Medium", + "none": "None", + "very_high": "Very High", + "very_low": "Very Low" + }, + "climacell__precipitation_type": { + "freezing_rain": "Freezing Rain", + "ice_pellets": "Ice Pellets", + "none": "None", + "rain": "Rain", + "snow": "Snow" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.es-419.json b/homeassistant/components/climacell/translations/sensor.es-419.json new file mode 100644 index 00000000000..127177e84b4 --- /dev/null +++ b/homeassistant/components/climacell/translations/sensor.es-419.json @@ -0,0 +1,27 @@ +{ + "state": { + "climacell__health_concern": { + "good": "Bueno", + "hazardous": "Peligroso", + "moderate": "Moderado", + "unhealthy": "Insalubre", + "unhealthy_for_sensitive_groups": "Insalubre para grupos sensibles", + "very_unhealthy": "Muy poco saludable" + }, + "climacell__pollen_index": { + "high": "Alto", + "low": "Bajo", + "medium": "Medio", + "none": "Ninguno", + "very_high": "Muy alto", + "very_low": "Muy bajo" + }, + "climacell__precipitation_type": { + "freezing_rain": "Lluvia helada", + "ice_pellets": "Gr\u00e1nulos de hielo", + "none": "Ninguno", + "rain": "Lluvia", + "snow": "Nieve" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.es.json b/homeassistant/components/climacell/translations/sensor.es.json new file mode 100644 index 00000000000..4cb1b34eb21 --- /dev/null +++ b/homeassistant/components/climacell/translations/sensor.es.json @@ -0,0 +1,27 @@ +{ + "state": { + "climacell__health_concern": { + "good": "Bueno", + "hazardous": "Peligroso", + "moderate": "Moderado", + "unhealthy": "No saludable", + "unhealthy_for_sensitive_groups": "No es saludable para grupos sensibles", + "very_unhealthy": "Muy poco saludable" + }, + "climacell__pollen_index": { + "high": "Alto", + "low": "Bajo", + "medium": "Medio", + "none": "Ninguno", + "very_high": "Muy alto", + "very_low": "Muy bajo" + }, + "climacell__precipitation_type": { + "freezing_rain": "Lluvia helada", + "ice_pellets": "Granizo", + "none": "Ninguna", + "rain": "Lluvia", + "snow": "Nieve" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.et.json b/homeassistant/components/climacell/translations/sensor.et.json new file mode 100644 index 00000000000..a0b7ac0562b --- /dev/null +++ b/homeassistant/components/climacell/translations/sensor.et.json @@ -0,0 +1,27 @@ +{ + "state": { + "climacell__health_concern": { + "good": "Normaalne", + "hazardous": "Ohtlik", + "moderate": "M\u00f5\u00f5dukas", + "unhealthy": "Ebatervislik", + "unhealthy_for_sensitive_groups": "Ebatervislik riskir\u00fchmale", + "very_unhealthy": "V\u00e4ga ebatervislik" + }, + "climacell__pollen_index": { + "high": "K\u00f5rge", + "low": "Madal", + "medium": "Keskmine", + "none": "Puudub", + "very_high": "V\u00e4ga k\u00f5rge", + "very_low": "V\u00e4ga madal" + }, + "climacell__precipitation_type": { + "freezing_rain": "J\u00e4\u00e4vihm", + "ice_pellets": "J\u00e4\u00e4kruubid", + "none": "Sademeid pole", + "rain": "Vihm", + "snow": "Lumi" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.fr.json b/homeassistant/components/climacell/translations/sensor.fr.json new file mode 100644 index 00000000000..acff91fc570 --- /dev/null +++ b/homeassistant/components/climacell/translations/sensor.fr.json @@ -0,0 +1,27 @@ +{ + "state": { + "climacell__health_concern": { + "good": "Bon", + "hazardous": "Dangereux", + "moderate": "Mod\u00e9r\u00e9", + "unhealthy": "Mauvais pour la sant\u00e9", + "unhealthy_for_sensitive_groups": "Mauvaise qualit\u00e9 pour les groupes sensibles", + "very_unhealthy": "Tr\u00e8s mauvais pour la sant\u00e9" + }, + "climacell__pollen_index": { + "high": "Haut", + "low": "Faible", + "medium": "Moyen", + "none": "Aucun", + "very_high": "Tr\u00e8s \u00e9lev\u00e9", + "very_low": "Tr\u00e8s faible" + }, + "climacell__precipitation_type": { + "freezing_rain": "Pluie vergla\u00e7ante", + "ice_pellets": "Gr\u00e9sil", + "none": "Aucun", + "rain": "Pluie", + "snow": "Neige" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.he.json b/homeassistant/components/climacell/translations/sensor.he.json new file mode 100644 index 00000000000..2a509464928 --- /dev/null +++ b/homeassistant/components/climacell/translations/sensor.he.json @@ -0,0 +1,7 @@ +{ + "state": { + "climacell__health_concern": { + "unhealthy_for_sensitive_groups": "\u05dc\u05d0 \u05d1\u05e8\u05d9\u05d0 \u05dc\u05e7\u05d1\u05d5\u05e6\u05d5\u05ea \u05e8\u05d2\u05d9\u05e9\u05d5\u05ea" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.hu.json b/homeassistant/components/climacell/translations/sensor.hu.json new file mode 100644 index 00000000000..656a460f429 --- /dev/null +++ b/homeassistant/components/climacell/translations/sensor.hu.json @@ -0,0 +1,27 @@ +{ + "state": { + "climacell__health_concern": { + "good": "J\u00f3", + "hazardous": "Vesz\u00e9lyes", + "moderate": "M\u00e9rs\u00e9kelt", + "unhealthy": "Eg\u00e9szs\u00e9gtelen", + "unhealthy_for_sensitive_groups": "Eg\u00e9szs\u00e9gtelen \u00e9rz\u00e9keny csoportok sz\u00e1m\u00e1ra", + "very_unhealthy": "Nagyon eg\u00e9szs\u00e9gtelen" + }, + "climacell__pollen_index": { + "high": "Magas", + "low": "Alacsony", + "medium": "K\u00f6zepes", + "none": "Nincs", + "very_high": "Nagyon magas", + "very_low": "Nagyon alacsony" + }, + "climacell__precipitation_type": { + "freezing_rain": "Havas es\u0151", + "ice_pellets": "\u00d3nos es\u0151", + "none": "Nincs", + "rain": "Es\u0151", + "snow": "Havaz\u00e1s" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.id.json b/homeassistant/components/climacell/translations/sensor.id.json new file mode 100644 index 00000000000..37ac0f7d876 --- /dev/null +++ b/homeassistant/components/climacell/translations/sensor.id.json @@ -0,0 +1,27 @@ +{ + "state": { + "climacell__health_concern": { + "good": "Bagus", + "hazardous": "Berbahaya", + "moderate": "Sedang", + "unhealthy": "Tidak Sehat", + "unhealthy_for_sensitive_groups": "Tidak Sehat untuk Kelompok Sensitif", + "very_unhealthy": "Sangat Tidak Sehat" + }, + "climacell__pollen_index": { + "high": "Tinggi", + "low": "Rendah", + "medium": "Sedang", + "none": "Tidak Ada", + "very_high": "Sangat Tinggi", + "very_low": "Sangat Rendah" + }, + "climacell__precipitation_type": { + "freezing_rain": "Hujan Beku", + "ice_pellets": "Hujan Es", + "none": "Tidak Ada", + "rain": "Hujan", + "snow": "Salju" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.is.json b/homeassistant/components/climacell/translations/sensor.is.json new file mode 100644 index 00000000000..bc22f9c67a9 --- /dev/null +++ b/homeassistant/components/climacell/translations/sensor.is.json @@ -0,0 +1,12 @@ +{ + "state": { + "climacell__health_concern": { + "hazardous": "H\u00e6ttulegt", + "unhealthy": "\u00d3hollt" + }, + "climacell__precipitation_type": { + "rain": "Rigning", + "snow": "Snj\u00f3r" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.ja.json b/homeassistant/components/climacell/translations/sensor.ja.json new file mode 100644 index 00000000000..6d8df99ca70 --- /dev/null +++ b/homeassistant/components/climacell/translations/sensor.ja.json @@ -0,0 +1,27 @@ +{ + "state": { + "climacell__health_concern": { + "good": "\u826f\u597d", + "hazardous": "\u5371\u967a", + "moderate": "\u7a4f\u3084\u304b\u306a", + "unhealthy": "\u4e0d\u5065\u5eb7", + "unhealthy_for_sensitive_groups": "\u654f\u611f\u306a\u30b0\u30eb\u30fc\u30d7\u306b\u3068\u3063\u3066\u306f\u4e0d\u5065\u5eb7", + "very_unhealthy": "\u975e\u5e38\u306b\u4e0d\u5065\u5eb7" + }, + "climacell__pollen_index": { + "high": "\u9ad8\u3044", + "low": "\u4f4e\u3044", + "medium": "\u4e2d", + "none": "\u306a\u3057", + "very_high": "\u975e\u5e38\u306b\u9ad8\u3044", + "very_low": "\u3068\u3066\u3082\u4f4e\u3044" + }, + "climacell__precipitation_type": { + "freezing_rain": "\u51cd\u3066\u3064\u304f\u96e8", + "ice_pellets": "\u51cd\u96e8", + "none": "\u306a\u3057", + "rain": "\u96e8", + "snow": "\u96ea" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.ko.json b/homeassistant/components/climacell/translations/sensor.ko.json new file mode 100644 index 00000000000..e5ec616959e --- /dev/null +++ b/homeassistant/components/climacell/translations/sensor.ko.json @@ -0,0 +1,7 @@ +{ + "state": { + "climacell__precipitation_type": { + "snow": "\ub208" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.lv.json b/homeassistant/components/climacell/translations/sensor.lv.json new file mode 100644 index 00000000000..a0010b4e4a8 --- /dev/null +++ b/homeassistant/components/climacell/translations/sensor.lv.json @@ -0,0 +1,27 @@ +{ + "state": { + "climacell__health_concern": { + "good": "Labs", + "hazardous": "B\u012bstams", + "moderate": "M\u0113rens", + "unhealthy": "Nevesel\u012bgs", + "unhealthy_for_sensitive_groups": "Nevesel\u012bgs jut\u012bg\u0101m grup\u0101m", + "very_unhealthy": "\u013boti nevesel\u012bgs" + }, + "climacell__pollen_index": { + "high": "Augsts", + "low": "Zems", + "medium": "Vid\u0113js", + "none": "Nav", + "very_high": "\u013boti augsts", + "very_low": "\u013boti zems" + }, + "climacell__precipitation_type": { + "freezing_rain": "Sasalsto\u0161s lietus", + "ice_pellets": "Krusa", + "none": "Nav", + "rain": "Lietus", + "snow": "Sniegs" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.nl.json b/homeassistant/components/climacell/translations/sensor.nl.json new file mode 100644 index 00000000000..710198156d1 --- /dev/null +++ b/homeassistant/components/climacell/translations/sensor.nl.json @@ -0,0 +1,27 @@ +{ + "state": { + "climacell__health_concern": { + "good": "Goed", + "hazardous": "Gevaarlijk", + "moderate": "Gematigd", + "unhealthy": "Ongezond", + "unhealthy_for_sensitive_groups": "Ongezond voor gevoelige groepen", + "very_unhealthy": "Zeer ongezond" + }, + "climacell__pollen_index": { + "high": "Hoog", + "low": "Laag", + "medium": "Medium", + "none": "Geen", + "very_high": "Zeer Hoog", + "very_low": "Zeer Laag" + }, + "climacell__precipitation_type": { + "freezing_rain": "IJzel", + "ice_pellets": "IJskorrels", + "none": "Geen", + "rain": "Regen", + "snow": "Sneeuw" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.no.json b/homeassistant/components/climacell/translations/sensor.no.json new file mode 100644 index 00000000000..10f2a02db72 --- /dev/null +++ b/homeassistant/components/climacell/translations/sensor.no.json @@ -0,0 +1,27 @@ +{ + "state": { + "climacell__health_concern": { + "good": "Bra", + "hazardous": "Farlig", + "moderate": "Moderat", + "unhealthy": "Usunt", + "unhealthy_for_sensitive_groups": "Usunt for sensitive grupper", + "very_unhealthy": "Veldig usunt" + }, + "climacell__pollen_index": { + "high": "H\u00f8y", + "low": "Lav", + "medium": "Medium", + "none": "Ingen", + "very_high": "Veldig h\u00f8y", + "very_low": "Veldig lav" + }, + "climacell__precipitation_type": { + "freezing_rain": "Underkj\u00f8lt regn", + "ice_pellets": "Is tapper", + "none": "Ingen", + "rain": "Regn", + "snow": "Sn\u00f8" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.pl.json b/homeassistant/components/climacell/translations/sensor.pl.json new file mode 100644 index 00000000000..67a0217a7ea --- /dev/null +++ b/homeassistant/components/climacell/translations/sensor.pl.json @@ -0,0 +1,27 @@ +{ + "state": { + "climacell__health_concern": { + "good": "dobre", + "hazardous": "niebezpieczne", + "moderate": "umiarkowane", + "unhealthy": "niezdrowe", + "unhealthy_for_sensitive_groups": "niezdrowe dla grup wra\u017cliwych", + "very_unhealthy": "bardzo niezdrowe" + }, + "climacell__pollen_index": { + "high": "wysokie", + "low": "niskie", + "medium": "\u015brednie", + "none": "brak", + "very_high": "bardzo wysokie", + "very_low": "bardzo niskie" + }, + "climacell__precipitation_type": { + "freezing_rain": "marzn\u0105cy deszcz", + "ice_pellets": "granulki lodu", + "none": "brak", + "rain": "deszcz", + "snow": "\u015bnieg" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.pt-BR.json b/homeassistant/components/climacell/translations/sensor.pt-BR.json new file mode 100644 index 00000000000..eb3814331b9 --- /dev/null +++ b/homeassistant/components/climacell/translations/sensor.pt-BR.json @@ -0,0 +1,27 @@ +{ + "state": { + "climacell__health_concern": { + "good": "Bom", + "hazardous": "Perigosos", + "moderate": "Moderado", + "unhealthy": "Pouco saud\u00e1vel", + "unhealthy_for_sensitive_groups": "Insalubre para grupos sens\u00edveis", + "very_unhealthy": "Muito prejudicial \u00e0 sa\u00fade" + }, + "climacell__pollen_index": { + "high": "Alto", + "low": "Baixo", + "medium": "M\u00e9dio", + "none": "Nenhum", + "very_high": "Muito alto", + "very_low": "Muito baixo" + }, + "climacell__precipitation_type": { + "freezing_rain": "Chuva congelante", + "ice_pellets": "Granizo", + "none": "Nenhum", + "rain": "Chuva", + "snow": "Neve" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.pt.json b/homeassistant/components/climacell/translations/sensor.pt.json new file mode 100644 index 00000000000..30ba0f75808 --- /dev/null +++ b/homeassistant/components/climacell/translations/sensor.pt.json @@ -0,0 +1,7 @@ +{ + "state": { + "climacell__health_concern": { + "unhealthy_for_sensitive_groups": "Pouco saud\u00e1vel para grupos sens\u00edveis" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.ru.json b/homeassistant/components/climacell/translations/sensor.ru.json new file mode 100644 index 00000000000..3a5d1a07a7e --- /dev/null +++ b/homeassistant/components/climacell/translations/sensor.ru.json @@ -0,0 +1,27 @@ +{ + "state": { + "climacell__health_concern": { + "good": "\u0425\u043e\u0440\u043e\u0448\u043e", + "hazardous": "\u041e\u043f\u0430\u0441\u043d\u043e", + "moderate": "\u0421\u0440\u0435\u0434\u043d\u0435", + "unhealthy": "\u0412\u0440\u0435\u0434\u043d\u043e", + "unhealthy_for_sensitive_groups": "\u0412\u0440\u0435\u0434\u043d\u043e \u0434\u043b\u044f \u0443\u044f\u0437\u0432\u0438\u043c\u044b\u0445 \u0433\u0440\u0443\u043f\u043f", + "very_unhealthy": "\u041e\u0447\u0435\u043d\u044c \u0432\u0440\u0435\u0434\u043d\u043e" + }, + "climacell__pollen_index": { + "high": "\u0412\u044b\u0441\u043e\u043a\u0438\u0439", + "low": "\u041d\u0438\u0437\u043a\u0438\u0439", + "medium": "\u0421\u0440\u0435\u0434\u043d\u0438\u0439", + "none": "\u041e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442", + "very_high": "\u041e\u0447\u0435\u043d\u044c \u0432\u044b\u0441\u043e\u043a\u0438\u0439", + "very_low": "\u041e\u0447\u0435\u043d\u044c \u043d\u0438\u0437\u043a\u0438\u0439" + }, + "climacell__precipitation_type": { + "freezing_rain": "\u041b\u0435\u0434\u044f\u043d\u043e\u0439 \u0434\u043e\u0436\u0434\u044c", + "ice_pellets": "\u041b\u0435\u0434\u044f\u043d\u0430\u044f \u043a\u0440\u0443\u043f\u0430", + "none": "\u041e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442", + "rain": "\u0414\u043e\u0436\u0434\u044c", + "snow": "\u0421\u043d\u0435\u0433" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.sk.json b/homeassistant/components/climacell/translations/sensor.sk.json new file mode 100644 index 00000000000..843169b2f3b --- /dev/null +++ b/homeassistant/components/climacell/translations/sensor.sk.json @@ -0,0 +1,7 @@ +{ + "state": { + "climacell__health_concern": { + "unhealthy": "Nezdrav\u00e9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.sv.json b/homeassistant/components/climacell/translations/sensor.sv.json new file mode 100644 index 00000000000..d6172566c7a --- /dev/null +++ b/homeassistant/components/climacell/translations/sensor.sv.json @@ -0,0 +1,27 @@ +{ + "state": { + "climacell__health_concern": { + "good": "Bra", + "hazardous": "Farligt", + "moderate": "M\u00e5ttligt", + "unhealthy": "Oh\u00e4lsosamt", + "unhealthy_for_sensitive_groups": "Oh\u00e4lsosamt f\u00f6r k\u00e4nsliga grupper", + "very_unhealthy": "Mycket oh\u00e4lsosamt" + }, + "climacell__pollen_index": { + "high": "H\u00f6gt", + "low": "L\u00e5gt", + "medium": "Medium", + "none": "Inget", + "very_high": "V\u00e4ldigt h\u00f6gt", + "very_low": "V\u00e4ldigt l\u00e5gt" + }, + "climacell__precipitation_type": { + "freezing_rain": "Underkylt regn", + "ice_pellets": "Hagel", + "none": "Ingen", + "rain": "Regn", + "snow": "Sn\u00f6" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.tr.json b/homeassistant/components/climacell/translations/sensor.tr.json new file mode 100644 index 00000000000..6c58f82bb94 --- /dev/null +++ b/homeassistant/components/climacell/translations/sensor.tr.json @@ -0,0 +1,27 @@ +{ + "state": { + "climacell__health_concern": { + "good": "\u0130yi", + "hazardous": "Tehlikeli", + "moderate": "Il\u0131ml\u0131", + "unhealthy": "Sa\u011fl\u0131ks\u0131z", + "unhealthy_for_sensitive_groups": "Hassas Gruplar \u0130\u00e7in Sa\u011fl\u0131ks\u0131z", + "very_unhealthy": "\u00c7ok Sa\u011fl\u0131ks\u0131z" + }, + "climacell__pollen_index": { + "high": "Y\u00fcksek", + "low": "D\u00fc\u015f\u00fck", + "medium": "Orta", + "none": "Hi\u00e7biri", + "very_high": "\u00c7ok Y\u00fcksek", + "very_low": "\u00c7ok D\u00fc\u015f\u00fck" + }, + "climacell__precipitation_type": { + "freezing_rain": "Dondurucu Ya\u011fmur", + "ice_pellets": "Buz Peletleri", + "none": "Hi\u00e7biri", + "rain": "Ya\u011fmur", + "snow": "Kar" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.zh-Hant.json b/homeassistant/components/climacell/translations/sensor.zh-Hant.json new file mode 100644 index 00000000000..c9898fcfe4d --- /dev/null +++ b/homeassistant/components/climacell/translations/sensor.zh-Hant.json @@ -0,0 +1,27 @@ +{ + "state": { + "climacell__health_concern": { + "good": "\u826f\u597d", + "hazardous": "\u5371\u96aa", + "moderate": "\u4e2d\u7b49", + "unhealthy": "\u4e0d\u5065\u5eb7", + "unhealthy_for_sensitive_groups": "\u5c0d\u654f\u611f\u65cf\u7fa4\u4e0d\u5065\u5eb7", + "very_unhealthy": "\u975e\u5e38\u4e0d\u5065\u5eb7" + }, + "climacell__pollen_index": { + "high": "\u9ad8", + "low": "\u4f4e", + "medium": "\u4e2d", + "none": "\u7121", + "very_high": "\u6975\u9ad8", + "very_low": "\u6975\u4f4e" + }, + "climacell__precipitation_type": { + "freezing_rain": "\u51cd\u96e8", + "ice_pellets": "\u51b0\u73e0", + "none": "\u7121", + "rain": "\u4e0b\u96e8", + "snow": "\u4e0b\u96ea" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sv.json b/homeassistant/components/climacell/translations/sv.json new file mode 100644 index 00000000000..2382ec64324 --- /dev/null +++ b/homeassistant/components/climacell/translations/sv.json @@ -0,0 +1,13 @@ +{ + "options": { + "step": { + "init": { + "data": { + "timestep": "Min. Mellan NowCast-prognoser" + }, + "description": "Om du v\u00e4ljer att aktivera \"nowcast\"-prognosentiteten kan du konfigurera antalet minuter mellan varje prognos. Antalet prognoser som tillhandah\u00e5lls beror p\u00e5 antalet minuter som v\u00e4ljs mellan prognoserna.", + "title": "Uppdatera ClimaCell-alternativ" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/tr.json b/homeassistant/components/climacell/translations/tr.json new file mode 100644 index 00000000000..54e24f813e4 --- /dev/null +++ b/homeassistant/components/climacell/translations/tr.json @@ -0,0 +1,13 @@ +{ + "options": { + "step": { + "init": { + "data": { + "timestep": "Min. NowCast Tahminleri Aras\u0131nda" + }, + "description": "'Nowcast' tahmin varl\u0131\u011f\u0131n\u0131 etkinle\u015ftirmeyi se\u00e7erseniz, her tahmin aras\u0131ndaki dakika say\u0131s\u0131n\u0131 yap\u0131land\u0131rabilirsiniz. Sa\u011flanan tahmin say\u0131s\u0131, tahminler aras\u0131nda se\u00e7ilen dakika say\u0131s\u0131na ba\u011fl\u0131d\u0131r.", + "title": "ClimaCell Se\u00e7eneklerini G\u00fcncelleyin" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/zh-Hant.json b/homeassistant/components/climacell/translations/zh-Hant.json new file mode 100644 index 00000000000..309b39ab242 --- /dev/null +++ b/homeassistant/components/climacell/translations/zh-Hant.json @@ -0,0 +1,13 @@ +{ + "options": { + "step": { + "init": { + "data": { + "timestep": "NowCast \u9810\u5831\u9593\u9694\u5206\u9418" + }, + "description": "\u5047\u5982\u9078\u64c7\u958b\u555f `nowcast` \u9810\u5831\u5be6\u9ad4\u3001\u5c07\u53ef\u4ee5\u8a2d\u5b9a\u9810\u5831\u983b\u7387\u9593\u9694\u5206\u9418\u6578\u3002\u6839\u64da\u6240\u8f38\u5165\u7684\u9593\u9694\u6642\u9593\u5c07\u6c7a\u5b9a\u9810\u5831\u7684\u6578\u76ee\u3002", + "title": "\u66f4\u65b0 ClimaCell \u9078\u9805" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dms/translations/bg.json b/homeassistant/components/dlna_dms/translations/bg.json index 4356e0973c1..da5fbf4c01c 100644 --- a/homeassistant/components/dlna_dms/translations/bg.json +++ b/homeassistant/components/dlna_dms/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", + "no_devices_found": "\u041d\u044f\u043c\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\u0430" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/eight_sleep/translations/bg.json b/homeassistant/components/eight_sleep/translations/bg.json new file mode 100644 index 00000000000..31421ee3089 --- /dev/null +++ b/homeassistant/components/eight_sleep/translations/bg.json @@ -0,0 +1,15 @@ +{ + "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" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google/translations/bg.json b/homeassistant/components/google/translations/bg.json index cd81f011d5a..2fa49447827 100644 --- a/homeassistant/components/google/translations/bg.json +++ b/homeassistant/components/google/translations/bg.json @@ -18,6 +18,11 @@ } } }, + "issues": { + "deprecated_yaml": { + "title": "YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 Google Calendar \u0441\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u0432\u0430" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/group/translations/bg.json b/homeassistant/components/group/translations/bg.json index dea9870e1b7..f39166c65d8 100644 --- a/homeassistant/components/group/translations/bg.json +++ b/homeassistant/components/group/translations/bg.json @@ -8,6 +8,9 @@ }, "title": "\u041d\u043e\u0432\u0430 \u0433\u0440\u0443\u043f\u0430" }, + "cover": { + "title": "\u0414\u043e\u0431\u0430\u0432\u044f\u043d\u0435 \u043d\u0430 \u0433\u0440\u0443\u043f\u0430" + }, "fan": { "data": { "name": "\u0418\u043c\u0435" @@ -30,7 +33,8 @@ "media_player": { "data": { "name": "\u0418\u043c\u0435 \u043d\u0430 \u0433\u0440\u0443\u043f\u0430" - } + }, + "title": "\u0414\u043e\u0431\u0430\u0432\u044f\u043d\u0435 \u043d\u0430 \u0433\u0440\u0443\u043f\u0430" }, "switch": { "data": { diff --git a/homeassistant/components/guardian/translations/it.json b/homeassistant/components/guardian/translations/it.json index 4f43ace1b71..29c1d90ed87 100644 --- a/homeassistant/components/guardian/translations/it.json +++ b/homeassistant/components/guardian/translations/it.json @@ -23,12 +23,23 @@ "fix_flow": { "step": { "confirm": { - "description": "Aggiorna tutte le automazioni o gli script che utilizzano questo servizio e utilizzare invece il servizio `{alternate_service}` con un ID entit\u00e0 di destinazione di `{alternate_target}`. Quindi, fai clic su INVIA di seguito per contrassegnare questo problema come risolto.", + "description": "Aggiorna tutte le automazioni o gli script che utilizzano questo servizio per utilizzare invece il servizio `{alternate_service}` con un ID entit\u00e0 di destinazione di `{alternate_target}`.", "title": "Il servizio {deprecated_service} verr\u00e0 rimosso" } } }, "title": "Il servizio {deprecated_service} verr\u00e0 rimosso" + }, + "replaced_old_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "Aggiorna tutte le automazioni o gli script che utilizzano questa entit\u00e0 in modo che utilizzino invece `{replacement_entity_id}`.", + "title": "L'entit\u00e0 {old_entity_id} verr\u00e0 rimossa" + } + } + }, + "title": "L'entit\u00e0 {old_entity_id} verr\u00e0 rimossa" } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/no.json b/homeassistant/components/guardian/translations/no.json index 9c2669fdeb2..b550bf9c43d 100644 --- a/homeassistant/components/guardian/translations/no.json +++ b/homeassistant/components/guardian/translations/no.json @@ -23,12 +23,23 @@ "fix_flow": { "step": { "confirm": { - "description": "Oppdater eventuelle automatiseringer eller skript som bruker denne tjenesten til i stedet \u00e5 bruke ` {alternate_service} `-tjenesten med en m\u00e5lenhets-ID p\u00e5 ` {alternate_target} `. Klikk deretter SEND nedenfor for \u00e5 merke dette problemet som l\u00f8st.", - "title": "{deprecated_service} -tjenesten blir fjernet" + "description": "Oppdater eventuelle automatiseringer eller skript som bruker denne tjenesten for i stedet \u00e5 bruke ` {alternate_service} `-tjenesten med en m\u00e5lenhets-ID p\u00e5 ` {alternate_target} `.", + "title": "{deprecated_service} -tjenesten vil bli fjernet" } } }, - "title": "{deprecated_service} -tjenesten blir fjernet" + "title": "{deprecated_service} -tjenesten vil bli fjernet" + }, + "replaced_old_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "Oppdater eventuelle automatiseringer eller skript som bruker denne enheten til i stedet \u00e5 bruke ` {replacement_entity_id} `.", + "title": "{old_entity_id} vil bli fjernet" + } + } + }, + "title": "{old_entity_id} vil bli fjernet" } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/ru.json b/homeassistant/components/guardian/translations/ru.json index 4d256258e71..6cfc857f0de 100644 --- a/homeassistant/components/guardian/translations/ru.json +++ b/homeassistant/components/guardian/translations/ru.json @@ -34,7 +34,7 @@ "fix_flow": { "step": { "confirm": { - "description": "\u042d\u0442\u043e\u0442 \u043e\u0431\u044a\u0435\u043a\u0442 \u0431\u044b\u043b \u0437\u0430\u043c\u0435\u043d\u0435\u043d \u043d\u0430 `{replacement_entity_id}`.", + "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\u043e\u0442 \u043e\u0431\u044a\u0435\u043a\u0442, \u0442\u0435\u043f\u0435\u0440\u044c \u0441\u043b\u0435\u0434\u0443\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043e\u0431\u044a\u0435\u043a\u0442 `{replacement_entity_id}`.", "title": "\u041e\u0431\u044a\u0435\u043a\u0442 {old_entity_id} \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d" } } diff --git a/homeassistant/components/here_travel_time/translations/bg.json b/homeassistant/components/here_travel_time/translations/bg.json index 75bb03c2a1f..8202e295a74 100644 --- a/homeassistant/components/here_travel_time/translations/bg.json +++ b/homeassistant/components/here_travel_time/translations/bg.json @@ -14,12 +14,38 @@ "destination_entity_id": { "title": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0434\u0435\u0441\u0442\u0438\u043d\u0430\u0446\u0438\u044f" }, + "destination_menu": { + "title": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0434\u0435\u0441\u0442\u0438\u043d\u0430\u0446\u0438\u044f" + }, "user": { "data": { "api_key": "API \u043a\u043b\u044e\u0447", + "mode": "\u0420\u0435\u0436\u0438\u043c \u043d\u0430 \u043f\u044a\u0442\u0443\u0432\u0430\u043d\u0435", "name": "\u0418\u043c\u0435" } } } + }, + "options": { + "step": { + "arrival_time": { + "data": { + "arrival_time": "\u0427\u0430\u0441 \u043d\u0430 \u043f\u0440\u0438\u0441\u0442\u0438\u0433\u0430\u043d\u0435" + }, + "title": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0447\u0430\u0441 \u043d\u0430 \u043f\u0440\u0438\u0441\u0442\u0438\u0433\u0430\u043d\u0435" + }, + "departure_time": { + "data": { + "departure_time": "\u0427\u0430\u0441 \u043d\u0430 \u0437\u0430\u043c\u0438\u043d\u0430\u0432\u0430\u043d\u0435" + }, + "title": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0447\u0430\u0441 \u043d\u0430 \u0437\u0430\u043c\u0438\u043d\u0430\u0432\u0430\u043d\u0435" + }, + "init": { + "data": { + "route_mode": "\u0420\u0435\u0436\u0438\u043c \u043d\u0430 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0430", + "traffic_mode": "\u0420\u0435\u0436\u0438\u043c \u043d\u0430 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/homeassistant/translations/bg.json b/homeassistant/components/homeassistant/translations/bg.json index 822b2534c40..260c7bcb57c 100644 --- a/homeassistant/components/homeassistant/translations/bg.json +++ b/homeassistant/components/homeassistant/translations/bg.json @@ -2,6 +2,7 @@ "system_health": { "info": { "arch": "\u0410\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u0430 \u043d\u0430 CPU", + "config_dir": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u0430 \u0434\u0438\u0440\u0435\u043a\u0442\u043e\u0440\u0438\u044f", "docker": "Docker", "hassio": "Supervisor", "installation_type": "\u0422\u0438\u043f \u0438\u043d\u0441\u0442\u0430\u043b\u0430\u0446\u0438\u044f", diff --git a/homeassistant/components/ibeacon/translations/en.json b/homeassistant/components/ibeacon/translations/en.json index c1e64281104..1125e778b19 100644 --- a/homeassistant/components/ibeacon/translations/en.json +++ b/homeassistant/components/ibeacon/translations/en.json @@ -9,5 +9,15 @@ "description": "Do you want to setup iBeacon Tracker?" } } + }, + "options": { + "step": { + "init": { + "data": { + "min_rssi": "Minimum RSSI" + }, + "description": "iBeacons with an RSSI value lower than the Minimum RSSI will be ignored. If the integration is seeing neighboring iBeacons, increasing this value may help." + } + } } } \ No newline at end of file diff --git a/homeassistant/components/ibeacon/translations/it.json b/homeassistant/components/ibeacon/translations/it.json new file mode 100644 index 00000000000..41b6f30a401 --- /dev/null +++ b/homeassistant/components/ibeacon/translations/it.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "bluetooth_not_available": "Per utilizzare iBeacon Tracker \u00e8 necessario configurare almeno un adattatore o un telecomando Bluetooth.", + "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." + }, + "step": { + "user": { + "description": "Vuoi configurare iBeacon Tracker?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "min_rssi": "RSSI minimo" + }, + "description": "Gli iBeacon con un valore RSSI inferiore all'RSSI minimo verranno ignorati. Se l'integrazione vede iBeacon vicini, aumentare questo valore pu\u00f2 essere d'aiuto." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ibeacon/translations/no.json b/homeassistant/components/ibeacon/translations/no.json new file mode 100644 index 00000000000..f198120c2c4 --- /dev/null +++ b/homeassistant/components/ibeacon/translations/no.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "bluetooth_not_available": "Minst \u00e9n Bluetooth-adapter eller fjernkontroll m\u00e5 konfigureres for \u00e5 bruke iBeacon Tracker.", + "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." + }, + "step": { + "user": { + "description": "Vil du sette opp iBeacon Tracker?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "min_rssi": "Minimum RSSI" + }, + "description": "iBeacons med en RSSI-verdi lavere enn Minimum RSSI vil bli ignorert. Hvis integrasjonen ser n\u00e6rliggende iBeacons, kan det hjelpe \u00e5 \u00f8ke denne verdien." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ibeacon/translations/ru.json b/homeassistant/components/ibeacon/translations/ru.json new file mode 100644 index 00000000000..a61ea54ceb6 --- /dev/null +++ b/homeassistant/components/ibeacon/translations/ru.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "bluetooth_not_available": "\u0414\u043b\u044f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f iBeacon Tracker \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d \u0445\u043e\u0442\u044f \u0431\u044b \u043e\u0434\u0438\u043d \u0430\u0434\u0430\u043f\u0442\u0435\u0440 Bluetooth.", + "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." + }, + "step": { + "user": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c iBeacon Tracker?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "min_rssi": "\u041c\u0438\u043d\u0438\u043c\u0430\u043b\u044c\u043d\u044b\u0439 RSSI" + }, + "description": "iBeacons \u0441\u043e \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435\u043c RSSI \u043d\u0438\u0436\u0435 \u043c\u0438\u043d\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0433\u043e RSSI \u0431\u0443\u0434\u0443\u0442 \u0438\u0433\u043d\u043e\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f. \u0415\u0441\u043b\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0432\u0438\u0434\u0438\u0442 \u0441\u043e\u0441\u0435\u0434\u043d\u0438\u0435 iBeacons, \u0443\u0432\u0435\u043b\u0438\u0447\u0435\u043d\u0438\u0435 \u044d\u0442\u043e\u0433\u043e \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u043c\u043e\u0436\u0435\u0442 \u043f\u043e\u043c\u043e\u0447\u044c." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kegtron/translations/it.json b/homeassistant/components/kegtron/translations/it.json new file mode 100644 index 00000000000..7784ed3a240 --- /dev/null +++ b/homeassistant/components/kegtron/translations/it.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "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": "{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/kegtron/translations/no.json b/homeassistant/components/kegtron/translations/no.json new file mode 100644 index 00000000000..0bf8b1695ec --- /dev/null +++ b/homeassistant/components/kegtron/translations/no.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", + "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket", + "not_supported": "Enheten st\u00f8ttes ikke" + }, + "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/kegtron/translations/ru.json b/homeassistant/components/kegtron/translations/ru.json new file mode 100644 index 00000000000..887499e5f2e --- /dev/null +++ b/homeassistant/components/kegtron/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.", + "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.", + "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.", + "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." + }, + "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/keymitt_ble/translations/it.json b/homeassistant/components/keymitt_ble/translations/it.json new file mode 100644 index 00000000000..583f4d3940b --- /dev/null +++ b/homeassistant/components/keymitt_ble/translations/it.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured_device": "Il dispositivo \u00e8 gi\u00e0 configurato", + "cannot_connect": "Impossibile connettersi", + "no_unconfigured_devices": "Non sono stati trovati dispositivi non configurati.", + "unknown": "Errore imprevisto" + }, + "error": { + "linking": "Impossibile eseguire l'associazione, riprovare. Il MicroBot \u00e8 in modalit\u00e0 di associazione?" + }, + "flow_title": "{name}", + "step": { + "init": { + "data": { + "address": "Indirizzo del dispositivo", + "name": "Nome" + }, + "title": "Configura il dispositivo MicroBot" + }, + "link": { + "description": "Premere il pulsante sul MicroBot Push quando il LED \u00e8 rosa o verde fisso per registrarsi con Home Assistant.", + "title": "Abbinamento" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/keymitt_ble/translations/no.json b/homeassistant/components/keymitt_ble/translations/no.json new file mode 100644 index 00000000000..6aa5ca36d43 --- /dev/null +++ b/homeassistant/components/keymitt_ble/translations/no.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured_device": "Enheten er allerede konfigurert", + "cannot_connect": "Tilkobling mislyktes", + "no_unconfigured_devices": "Fant ingen ukonfigurerte enheter.", + "unknown": "Uventet feil" + }, + "error": { + "linking": "Kunne ikke pare. Pr\u00f8v igjen. Er MicroBot i sammenkoblingsmodus?" + }, + "flow_title": "{name}", + "step": { + "init": { + "data": { + "address": "Enhetsadresse", + "name": "Navn" + }, + "title": "Sett opp MicroBot-enhet" + }, + "link": { + "description": "Trykk p\u00e5 knappen p\u00e5 MicroBot Push n\u00e5r lysdioden lyser rosa eller gr\u00f8nt for \u00e5 registrere deg med Home Assistant.", + "title": "Sammenkobling" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/keymitt_ble/translations/pt-BR.json b/homeassistant/components/keymitt_ble/translations/pt-BR.json new file mode 100644 index 00000000000..66e44612afe --- /dev/null +++ b/homeassistant/components/keymitt_ble/translations/pt-BR.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured_device": "O dispositivo j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falhou ao conectar", + "no_unconfigured_devices": "Nenhum dispositivo n\u00e3o configurado encontrado.", + "unknown": "Erro inesperado" + }, + "error": { + "linking": "Falha ao emparelhar. Tente novamente. O MicroBot est\u00e1 no modo de emparelhamento?" + }, + "flow_title": "{name}", + "step": { + "init": { + "data": { + "address": "Endere\u00e7o do dispositivo", + "name": "Nome" + }, + "title": "Configurar dispositivo MicroBot" + }, + "link": { + "description": "Pressione o bot\u00e3o no MicroBot Push quando o LED estiver rosa ou verde s\u00f3lido para se registrar no Home Assistant.", + "title": "Emparelhamento" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/keymitt_ble/translations/ru.json b/homeassistant/components/keymitt_ble/translations/ru.json new file mode 100644 index 00000000000..200bd4d798c --- /dev/null +++ b/homeassistant/components/keymitt_ble/translations/ru.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured_device": "\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.", + "no_unconfigured_devices": "\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.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "error": { + "linking": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435. \u041f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443. \u041d\u0430\u0445\u043e\u0434\u0438\u0442\u0441\u044f \u043b\u0438 MicroBot \u0432 \u0440\u0435\u0436\u0438\u043c\u0435 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f?" + }, + "flow_title": "{name}", + "step": { + "init": { + "data": { + "address": "\u0410\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" + }, + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 MicroBot" + }, + "link": { + "description": "\u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 \u043d\u0430 MicroBot Push, \u043a\u043e\u0433\u0434\u0430 \u0438\u043d\u0434\u0438\u043a\u0430\u0442\u043e\u0440 \u0433\u043e\u0440\u0438\u0442 \u0440\u043e\u0437\u043e\u0432\u044b\u043c \u0438\u043b\u0438 \u0437\u0435\u043b\u0435\u043d\u044b\u043c \u0446\u0432\u0435\u0442\u043e\u043c, \u0447\u0442\u043e\u0431\u044b \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0432 Home Assistant.", + "title": "\u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lametric/translations/no.json b/homeassistant/components/lametric/translations/no.json index 79d3591aec1..4984e190241 100644 --- a/homeassistant/components/lametric/translations/no.json +++ b/homeassistant/components/lametric/translations/no.json @@ -7,7 +7,8 @@ "link_local_address": "Lokale koblingsadresser st\u00f8ttes ikke", "missing_configuration": "LaMetric-integrasjonen er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen.", "no_devices": "Den autoriserte brukeren har ingen LaMetric-enheter", - "no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, [sjekk hjelpseksjonen]({docs_url})" + "no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, [sjekk hjelpseksjonen]({docs_url})", + "unknown": "Uventet feil" }, "error": { "cannot_connect": "Tilkobling mislyktes", diff --git a/homeassistant/components/lg_soundbar/translations/bg.json b/homeassistant/components/lg_soundbar/translations/bg.json new file mode 100644 index 00000000000..5d235f77133 --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/bg.json @@ -0,0 +1,17 @@ +{ + "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" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lidarr/translations/bg.json b/homeassistant/components/lidarr/translations/bg.json index 682e8687712..4e22178a11d 100644 --- a/homeassistant/components/lidarr/translations/bg.json +++ b/homeassistant/components/lidarr/translations/bg.json @@ -1,5 +1,14 @@ { "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", + "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", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, "step": { "reauth_confirm": { "data": { diff --git a/homeassistant/components/lidarr/translations/it.json b/homeassistant/components/lidarr/translations/it.json new file mode 100644 index 00000000000..b040d3eeb00 --- /dev/null +++ b/homeassistant/components/lidarr/translations/it.json @@ -0,0 +1,42 @@ +{ + "config": { + "abort": { + "already_configured": "Il servizio \u00e8 gi\u00e0 configurato", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto", + "wrong_app": "Applicazione errata raggiunta. Per favore riprova", + "zeroconf_failed": "Chiave API non trovata. Si prega di inserirla manualmente" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Chiave API" + }, + "description": "L'integrazione Lidarr deve essere riautenticata manualmente con l'API Lidarr", + "title": "Autentica nuovamente l'integrazione" + }, + "user": { + "data": { + "api_key": "Chiave API", + "url": "URL", + "verify_ssl": "Verifica il certificato SSL" + }, + "description": "La chiave API pu\u00f2 essere recuperata automaticamente se le credenziali di accesso non sono state impostate nell'applicazione.\nLa tua chiave API pu\u00f2 essere trovata in Impostazioni > Generali nell'interfaccia utente web di Lidarr." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "max_records": "Numero massimo di record da visualizzare su ricercato e coda", + "upcoming_days": "Numero di giorni successivi da visualizzare sul calendario" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lidarr/translations/no.json b/homeassistant/components/lidarr/translations/no.json new file mode 100644 index 00000000000..74514056485 --- /dev/null +++ b/homeassistant/components/lidarr/translations/no.json @@ -0,0 +1,42 @@ +{ + "config": { + "abort": { + "already_configured": "Tjenesten er allerede konfigurert", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil", + "wrong_app": "Feil s\u00f8knad er n\u00e5dd. V\u00e6r s\u00e5 snill, pr\u00f8v p\u00e5 nytt", + "zeroconf_failed": "Finner ikke API-n\u00f8kkel. Vennligst skriv det inn manuelt" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API-n\u00f8kkel" + }, + "description": "Lidarr-integrasjonen m\u00e5 re-autentiseres manuelt med Lidarr API", + "title": "Godkjenne integrering p\u00e5 nytt" + }, + "user": { + "data": { + "api_key": "API-n\u00f8kkel", + "url": "URL", + "verify_ssl": "Verifisere SSL-sertifikat" + }, + "description": "API-n\u00f8kkel kan hentes automatisk hvis p\u00e5loggingsinformasjon ikke ble angitt i applikasjonen.\n API-n\u00f8kkelen din finner du i Innstillinger > Generelt i Lidarr Web UI." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "max_records": "Antall maksimale poster \u00e5 vise p\u00e5 \u00f8nsket og k\u00f8", + "upcoming_days": "Antall kommende dager som skal vises i kalenderen" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lidarr/translations/ru.json b/homeassistant/components/lidarr/translations/ru.json new file mode 100644 index 00000000000..afda2835228 --- /dev/null +++ b/homeassistant/components/lidarr/translations/ru.json @@ -0,0 +1,42 @@ +{ + "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.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430.", + "wrong_app": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435. \u041f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0451 \u0440\u0430\u0437.", + "zeroconf_failed": "\u041a\u043b\u044e\u0447 API \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u0435\u0433\u043e \u0432\u0440\u0443\u0447\u043d\u0443\u044e." + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API" + }, + "description": "\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e API lidarr", + "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", + "url": "URL-\u0430\u0434\u0440\u0435\u0441", + "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL" + }, + "description": "\u041a\u043b\u044e\u0447 API \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u043f\u043e\u043b\u0443\u0447\u0435\u043d \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438, \u0435\u0441\u043b\u0438 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0434\u043b\u044f \u0432\u0445\u043e\u0434\u0430 \u043d\u0435 \u0431\u044b\u043b\u0438 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u044b \u0432 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0438.\n\u0412\u0430\u0448 \u043a\u043b\u044e\u0447 API \u043c\u043e\u0436\u043d\u043e \u043d\u0430\u0439\u0442\u0438 \u0432 \u0440\u0430\u0437\u0434\u0435\u043b\u0435 \u00ab\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 > \u00ab\u041e\u0441\u043d\u043e\u0432\u043d\u044b\u0435\u00bb \u0432 \u0432\u0435\u0431-\u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0435 Lidarr." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "max_records": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0435 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0437\u0430\u043f\u0438\u0441\u0435\u0439 \u0434\u043b\u044f \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f \u0432 \u043f\u043e\u0438\u0441\u043a\u0435 \u0438 \u0432 \u043e\u0447\u0435\u0440\u0435\u0434\u0438", + "upcoming_days": "\u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043f\u0440\u0435\u0434\u0441\u0442\u043e\u044f\u0449\u0438\u0445 \u0434\u043d\u0435\u0439 \u0434\u043b\u044f \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f \u0432 \u043a\u0430\u043b\u0435\u043d\u0434\u0430\u0440\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/translations/bg.json b/homeassistant/components/life360/translations/bg.json index d206a606b89..22fad245c5d 100644 --- a/homeassistant/components/life360/translations/bg.json +++ b/homeassistant/components/life360/translations/bg.json @@ -1,17 +1,26 @@ { "config": { "abort": { - "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" + "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", + "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", + "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" }, "create_entry": { "default": "\u0417\u0430 \u0434\u0430 \u0437\u0430\u0434\u0430\u0434\u0435\u0442\u0435 \u0440\u0430\u0437\u0448\u0438\u0440\u0435\u043d\u0438 \u043e\u043f\u0446\u0438\u0438, \u0432\u0438\u0436\u0442\u0435 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f \u043d\u0430 Life360]({docs_url})." }, "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_username": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + }, + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" + }, "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u0430", @@ -21,5 +30,16 @@ "title": "\u0418\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u0437\u0430 Life360 \u043f\u0440\u043e\u0444\u0438\u043b" } } + }, + "options": { + "step": { + "init": { + "data": { + "driving_speed": "\u0421\u043a\u043e\u0440\u043e\u0441\u0442 \u043d\u0430 \u0448\u043e\u0444\u0438\u0440\u0430\u043d\u0435", + "max_gps_accuracy": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u043d\u0430 GPS \u0442\u043e\u0447\u043d\u043e\u0441\u0442 (\u043c\u0435\u0442\u0440\u0438)" + }, + "title": "\u041e\u043f\u0446\u0438\u0438 \u043d\u0430 \u0430\u043a\u0430\u0443\u043d\u0442\u0430" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/sensor.id.json b/homeassistant/components/litterrobot/translations/sensor.id.json index 003bb338a02..7a2382a4aef 100644 --- a/homeassistant/components/litterrobot/translations/sensor.id.json +++ b/homeassistant/components/litterrobot/translations/sensor.id.json @@ -2,8 +2,8 @@ "state": { "litterrobot__status_code": { "br": "Bonnet Dihapus", - "ccc": "Siklus Bersih Selesai", - "ccp": "Siklus Bersih Sedang Berlangsung", + "ccc": "Siklus Pembersihan Selesai", + "ccp": "Siklus Pembersihan Sedang Berlangsung", "cd": "Kucing Terdeteksi", "csf": "Kesalahan Sensor Kucing", "csi": "Sensor Kucing Terganggu", @@ -11,17 +11,21 @@ "df1": "Laci Hampir Penuh - Tersisa 2 Siklus", "df2": "Laci Hampir Penuh - Tersisa 1 Siklus", "dfs": "Laci Penuh", - "ec": "Siklus Kosong", - "hpf": "Kesalahan Posisi Rumah", + "dhf": "Kesalahan Posisi Dump + Home", + "dpf": "Kesalahan Posisi Dump", + "ec": "Siklus Pengosongan", + "hpf": "Kesalahan Posisi Home", "off": "Mati", "offline": "Luring", "otf": "Kesalahan Torsi Berlebih", "p": "Jeda", + "pd": "Deteksi Pinch", "pwrd": "Mematikan Daya", "pwru": "Menyalakan", "rdy": "Siap", "scf": "Kesalahan Sensor Kucing Saat Mulai", - "sdf": "Laci Penuh Saat Memulai" + "sdf": "Laci Penuh Saat Memulai", + "spf": "Deteksi Pinch Saat Memulai" } } } \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/sensor.it.json b/homeassistant/components/litterrobot/translations/sensor.it.json index 926cb5d68d7..c578c2aa9cd 100644 --- a/homeassistant/components/litterrobot/translations/sensor.it.json +++ b/homeassistant/components/litterrobot/translations/sensor.it.json @@ -4,6 +4,7 @@ "br": "Coperchio rimosso", "ccc": "Ciclo di pulizia completato", "ccp": "Ciclo di pulizia in corso", + "cd": "Gatto rilevato", "csf": "Errore del sensore Gatto", "csi": "Sensore Gatto interrotto", "cst": "Temporizzazione del sensore Gatto", @@ -19,6 +20,8 @@ "otf": "Errore di sovracoppia", "p": "In pausa", "pd": "Antipresa", + "pwrd": "Spegnimento", + "pwru": "Accensione", "rdy": "Pronto", "scf": "Errore del sensore Gatto all'avvio", "sdf": "Cassetto pieno all'avvio", diff --git a/homeassistant/components/litterrobot/translations/sensor.no.json b/homeassistant/components/litterrobot/translations/sensor.no.json index 2997930e53b..f1a75e6902b 100644 --- a/homeassistant/components/litterrobot/translations/sensor.no.json +++ b/homeassistant/components/litterrobot/translations/sensor.no.json @@ -4,6 +4,7 @@ "br": "Panser fjernet", "ccc": "Rengj\u00f8ringssyklus fullf\u00f8rt", "ccp": "Rengj\u00f8ringssyklus p\u00e5g\u00e5r", + "cd": "Katt oppdaget", "csf": "Feil p\u00e5 kattesensor", "csi": "Kattesensor avbrutt", "cst": "Tidsberegning for kattesensor", @@ -19,6 +20,8 @@ "otf": "Over dreiemomentfeil", "p": "Pauset", "pd": "Knip gjenkjenning", + "pwrd": "Sl\u00e5r av", + "pwru": "Sl\u00e5r p\u00e5", "rdy": "Klar", "scf": "Kattesensorfeil ved oppstart", "sdf": "Skuff full ved oppstart", diff --git a/homeassistant/components/nest/translations/bg.json b/homeassistant/components/nest/translations/bg.json index efa1378b81e..41f20ca4a5f 100644 --- a/homeassistant/components/nest/translations/bg.json +++ b/homeassistant/components/nest/translations/bg.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", "authorize_url_timeout": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0430\u0434\u0440\u0435\u0441 \u0437\u0430 \u043e\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f \u0432 \u0441\u0440\u043e\u043a." }, "create_entry": { @@ -30,5 +31,10 @@ "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" } } + }, + "issues": { + "deprecated_yaml": { + "title": "YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 Nest \u0441\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u0432\u0430" + } } } \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/bg.json b/homeassistant/components/nextdns/translations/bg.json new file mode 100644 index 00000000000..e476a498ed6 --- /dev/null +++ b/homeassistant/components/nextdns/translations/bg.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\u0422\u043e\u0437\u0438 NextDNS \u043f\u0440\u043e\u0444\u0438\u043b \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d." + }, + "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", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "profiles": { + "data": { + "profile": "\u041f\u0440\u043e\u0444\u0438\u043b" + } + }, + "user": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/id.json b/homeassistant/components/nextdns/translations/id.json index 39f87faf813..39b471b4053 100644 --- a/homeassistant/components/nextdns/translations/id.json +++ b/homeassistant/components/nextdns/translations/id.json @@ -20,5 +20,10 @@ } } } + }, + "system_health": { + "info": { + "can_reach_server": "Keterjangkauan server" + } } } \ No newline at end of file diff --git a/homeassistant/components/nibe_heatpump/translations/hu.json b/homeassistant/components/nibe_heatpump/translations/hu.json new file mode 100644 index 00000000000..16c26cdba0e --- /dev/null +++ b/homeassistant/components/nibe_heatpump/translations/hu.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "address": "\u00c9rv\u00e9nytelen t\u00e1voli IP-c\u00edm van megadva. A c\u00edmnek IPV4-c\u00edmnek kell lennie.", + "address_in_use": "A kiv\u00e1lasztott port m\u00e1r haszn\u00e1latban van ezen a rendszeren.", + "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" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nibe_heatpump/translations/it.json b/homeassistant/components/nibe_heatpump/translations/it.json new file mode 100644 index 00000000000..9de61113160 --- /dev/null +++ b/homeassistant/components/nibe_heatpump/translations/it.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "address": "Indirizzo IP remoto specificato non valido. L'indirizzo deve essere un indirizzo IPV4.", + "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\".", + "unknown": "Errore imprevisto", + "write": "Errore nella richiesta di scrittura alla pompa. Verifica la tua \"Porta di scrittura remota\" o \"Indirizzo IP remoto\"." + }, + "step": { + "user": { + "data": { + "ip_address": "Indirizzo IP remoto", + "listening_port": "Porta di ascolto locale", + "remote_read_port": "Porta di lettura remota", + "remote_write_port": "Porta di scrittura remota" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nibe_heatpump/translations/no.json b/homeassistant/components/nibe_heatpump/translations/no.json new file mode 100644 index 00000000000..a9c4c41993d --- /dev/null +++ b/homeassistant/components/nibe_heatpump/translations/no.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "address": "Ugyldig ekstern IP-adresse er angitt. Adressen m\u00e5 v\u00e6re en IPV4-adresse.", + "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\".", + "unknown": "Uventet feil", + "write": "Feil ved skriveforesp\u00f8rsel til pumpen. Bekreft din \"Ekstern skriveport\" eller \"Ekstern IP-adresse\"." + }, + "step": { + "user": { + "data": { + "ip_address": "Ekstern IP-adresse", + "listening_port": "Lokal lytteport", + "remote_read_port": "Ekstern leseport", + "remote_write_port": "Ekstern skriveport" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nibe_heatpump/translations/pt-BR.json b/homeassistant/components/nibe_heatpump/translations/pt-BR.json new file mode 100644 index 00000000000..127f6c6010b --- /dev/null +++ b/homeassistant/components/nibe_heatpump/translations/pt-BR.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "address": "Endere\u00e7o IP remoto inv\u00e1lido especificado. O endere\u00e7o deve ser um endere\u00e7o IPV4.", + "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`.", + "unknown": "Erro inesperado", + "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": { + "user": { + "data": { + "ip_address": "Endere\u00e7o IP remoto", + "listening_port": "Porta de escuta local", + "remote_read_port": "Porta de leitura remota", + "remote_write_port": "Porta de grava\u00e7\u00e3o remota" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nibe_heatpump/translations/ru.json b/homeassistant/components/nibe_heatpump/translations/ru.json new file mode 100644 index 00000000000..e1192c7e08c --- /dev/null +++ b/homeassistant/components/nibe_heatpump/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." + }, + "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 IP-\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 IPV4.", + "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`.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430.", + "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": { + "data": { + "ip_address": "\u0423\u0434\u0430\u043b\u0435\u043d\u043d\u044b\u0439 IP-\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" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nina/translations/bg.json b/homeassistant/components/nina/translations/bg.json index 7520f3a8624..be3ffecd284 100644 --- a/homeassistant/components/nina/translations/bg.json +++ b/homeassistant/components/nina/translations/bg.json @@ -7,5 +7,12 @@ "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" } + }, + "options": { + "step": { + "init": { + "title": "\u041e\u043f\u0446\u0438\u0438" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/openuv/translations/it.json b/homeassistant/components/openuv/translations/it.json index 241e118a800..4e51b09aec2 100644 --- a/homeassistant/components/openuv/translations/it.json +++ b/homeassistant/components/openuv/translations/it.json @@ -18,6 +18,16 @@ } } }, + "issues": { + "deprecated_service_multiple_alternate_targets": { + "description": "Aggiorna tutte le automazioni o gli script che utilizzano questo servizio per utilizzare invece il servizio `{alternate_service}` con uno di questi ID entit\u00e0 come destinazione: `{alternate_targets}`.", + "title": "Il servizio {deprecated_service} \u00e8 stato rimosso" + }, + "deprecated_service_single_alternate_target": { + "description": "Aggiorna tutte le automazioni o gli script che utilizzano questo servizio per utilizzare invece il servizio `{alternate_service}` con `{alternate_targets}` come destinazione.", + "title": "Il servizio {deprecated_service} \u00e8 stato rimosso" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/openuv/translations/no.json b/homeassistant/components/openuv/translations/no.json index f76787b4e4d..1fcba27dc9f 100644 --- a/homeassistant/components/openuv/translations/no.json +++ b/homeassistant/components/openuv/translations/no.json @@ -18,6 +18,16 @@ } } }, + "issues": { + "deprecated_service_multiple_alternate_targets": { + "description": "Oppdater eventuelle automatiseringer eller skript som bruker denne tjenesten til i stedet \u00e5 bruke ` {alternate_service} `-tjenesten med en av disse enhets-ID-ene som m\u00e5l: ` {alternate_targets} `.", + "title": "{deprecated_service} -tjenesten blir fjernet" + }, + "deprecated_service_single_alternate_target": { + "description": "Oppdater eventuelle automatiseringer eller skript som bruker denne tjenesten til i stedet \u00e5 bruke ` {alternate_service} `-tjenesten med ` {alternate_targets} ` som m\u00e5l.", + "title": "{deprecated_service} -tjenesten blir fjernet" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/radarr/translations/bg.json b/homeassistant/components/radarr/translations/bg.json new file mode 100644 index 00000000000..42d6e3511de --- /dev/null +++ b/homeassistant/components/radarr/translations/bg.json @@ -0,0 +1,24 @@ +{ + "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", + "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", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "reauth_confirm": { + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" + }, + "user": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447", + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radarr/translations/de.json b/homeassistant/components/radarr/translations/de.json new file mode 100644 index 00000000000..81aafd0a351 --- /dev/null +++ b/homeassistant/components/radarr/translations/de.json @@ -0,0 +1,48 @@ +{ + "config": { + "abort": { + "already_configured": "Der Dienst ist bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler", + "wrong_app": "Falsche Anwendung erreicht. Bitte versuche es erneut", + "zeroconf_failed": "API-Schl\u00fcssel nicht gefunden. Bitte gib ihn manuell ein" + }, + "step": { + "reauth_confirm": { + "description": "Die Radarr-Integration muss manuell erneut mit der Radarr-API authentifiziert werden", + "title": "Integration erneut authentifizieren" + }, + "user": { + "data": { + "api_key": "API-Schl\u00fcssel", + "url": "URL", + "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" + }, + "description": "Der API-Schl\u00fcssel kann automatisch abgerufen werden, wenn in der Anwendung keine Anmeldeinformationen festgelegt wurden.\nDeinen API-Schl\u00fcssel findest unter Einstellungen > Allgemein in der Radarr-Web-Benutzeroberfl\u00e4che." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Die Konfiguration von Radarr mit YAML wird entfernt. \n\nDeine vorhandene YAML-Konfiguration wurde automatisch in die Benutzeroberfl\u00e4che importiert. \n\nEntferne die Radarr-YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die Radarr-YAML-Konfiguration wird entfernt" + }, + "removed_attributes": { + "description": "Es wurden einige \u00c4nderungen vorgenommen, um den Filmz\u00e4hlsensor aus Vorsicht zu deaktivieren.\n\nDieser Sensor kann bei gro\u00dfen Datenbanken Probleme verursachen. Wenn du ihn dennoch verwenden m\u00f6chtest, kannst du dies tun.\n\nFilmnamen werden nicht mehr als Attribute in den Filmsensor aufgenommen.\n\nUpcoming wurde entfernt. Er wird modernisiert, so wie es bei Kalenderelementen sein sollte. Der Speicherplatz ist jetzt in verschiedene Sensoren aufgeteilt, einen f\u00fcr jeden Ordner.\n\nStatus und Befehle wurden entfernt, da sie f\u00fcr Automatisierungen keinen wirklichen Wert zu haben scheinen.", + "title": "\u00c4nderungen an der Radarr-Integration" + } + }, + "options": { + "step": { + "init": { + "data": { + "upcoming_days": "Anzahl der anzuzeigenden Tage" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radarr/translations/el.json b/homeassistant/components/radarr/translations/el.json new file mode 100644 index 00000000000..f7eb031cba5 --- /dev/null +++ b/homeassistant/components/radarr/translations/el.json @@ -0,0 +1,48 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af", + "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2" + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1", + "wrong_app": "\u0395\u03c0\u03af\u03c4\u03b5\u03c5\u03be\u03b7 \u03bb\u03b1\u03bd\u03b8\u03b1\u03c3\u03bc\u03ad\u03bd\u03b7\u03c2 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2. \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03ce \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac", + "zeroconf_failed": "\u03a4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API \u03b4\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b5. \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03ce \u03b5\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03c7\u03b5\u03b9\u03c1\u03bf\u03ba\u03af\u03bd\u03b7\u03c4\u03b1" + }, + "step": { + "reauth_confirm": { + "description": "\u0397 \u03b5\u03bd\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03c4\u03bf\u03c5 Radarr \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03c5\u03c4\u03b5\u03af \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03bc\u03b5 \u03bc\u03b7 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03bf \u03c4\u03c1\u03cc\u03c0\u03bf \u03bc\u03b5 \u03c4\u03bf Radarr API", + "title": "\u0395\u03c0\u03b1\u03bd\u03b1\u03bb\u03b7\u03c0\u03c4\u03b9\u03ba\u03cc\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2" + }, + "user": { + "data": { + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", + "url": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL", + "verify_ssl": "\u0395\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03cd\u03c3\u03c4\u03b5 \u03c4\u03bf \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03c4\u03b9\u03ba\u03cc SSL" + }, + "description": "\u03a4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03b1\u03bd\u03b1\u03ba\u03c4\u03b7\u03b8\u03b5\u03af \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1 \u03b5\u03ac\u03bd \u03c4\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03b4\u03b5\u03bd \u03b5\u03af\u03c7\u03b1\u03bd \u03bf\u03c1\u03b9\u03c3\u03c4\u03b5\u03af \u03c3\u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae.\n \u039c\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03b2\u03c1\u03b5\u03af\u03c4\u03b5 \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API \u03c3\u03b1\u03c2 \u03c3\u03c4\u03b9\u03c2 \u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 > \u0393\u03b5\u03bd\u03b9\u03ba\u03ac \u03c3\u03c4\u03bf \u03c0\u03b5\u03c1\u03b9\u03b2\u03ac\u03bb\u03bb\u03bf\u03bd \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03c4\u03bf\u03c5 Radarr Web." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Radarr \u03bc\u03b5 \u03c7\u03c1\u03ae\u03c3\u03b7 YAML \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9. \n\n \u0397 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03ae \u03c3\u03b1\u03c2 YAML \u03ad\u03c7\u03b5\u03b9 \u03b5\u03b9\u03c3\u03b1\u03c7\u03b8\u03b5\u03af \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03c0\u03b1\u03c6\u03ae \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7. \n\n \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 Radarr YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 Radarr YAML \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + }, + "removed_attributes": { + "description": "\u0388\u03b3\u03b9\u03bd\u03b1\u03bd \u03bf\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03b5\u03c2 \u03c3\u03b7\u03bc\u03b1\u03bd\u03c4\u03b9\u03ba\u03ad\u03c2 \u03b1\u03bb\u03bb\u03b1\u03b3\u03ad\u03c2 \u03c3\u03c4\u03b7\u03bd \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03bc\u03ad\u03c4\u03c1\u03b7\u03c3\u03b7\u03c2 \u03c4\u03b1\u03b9\u03bd\u03b9\u03ce\u03bd \u03c7\u03c9\u03c1\u03af\u03c2 \u03c0\u03c1\u03bf\u03c3\u03bf\u03c7\u03ae. \n\n \u0391\u03c5\u03c4\u03cc\u03c2 \u03bf \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2 \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03c0\u03c1\u03bf\u03ba\u03b1\u03bb\u03ad\u03c3\u03b5\u03b9 \u03c0\u03c1\u03bf\u03b2\u03bb\u03ae\u03bc\u03b1\u03c4\u03b1 \u03bc\u03b5 \u03c4\u03b5\u03c1\u03ac\u03c3\u03c4\u03b9\u03b5\u03c2 \u03b2\u03ac\u03c3\u03b5\u03b9\u03c2 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03c9\u03bd. \u0395\u03ac\u03bd \u03b5\u03be\u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c4\u03bf \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5, \u03bc\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03c4\u03bf \u03ba\u03ac\u03bd\u03b5\u03c4\u03b5. \n\n \u03a4\u03b1 \u03bf\u03bd\u03cc\u03bc\u03b1\u03c4\u03b1 \u03c4\u03b1\u03b9\u03bd\u03b9\u03ce\u03bd \u03b4\u03b5\u03bd \u03c0\u03b5\u03c1\u03b9\u03bb\u03b1\u03bc\u03b2\u03ac\u03bd\u03bf\u03bd\u03c4\u03b1\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd \u03c9\u03c2 \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03b7\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03ac \u03c3\u03c4\u03bf\u03bd \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03c4\u03b1\u03b9\u03bd\u03b9\u03ce\u03bd. \n\n \u03a4\u03bf \u03b5\u03c0\u03b5\u03c1\u03c7\u03cc\u03bc\u03b5\u03bd\u03bf \u03ad\u03c7\u03b5\u03b9 \u03b1\u03c6\u03b1\u03b9\u03c1\u03b5\u03b8\u03b5\u03af. \u0395\u03ba\u03c3\u03c5\u03b3\u03c7\u03c1\u03bf\u03bd\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03cc\u03c0\u03c9\u03c2 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c4\u03b1 \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03b1 \u03c4\u03bf\u03c5 \u03b7\u03bc\u03b5\u03c1\u03bf\u03bb\u03bf\u03b3\u03af\u03bf\u03c5. \u039f \u03c7\u03ce\u03c1\u03bf\u03c2 \u03c3\u03c4\u03bf \u03b4\u03af\u03c3\u03ba\u03bf \u03c7\u03c9\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd \u03c3\u03b5 \u03b4\u03b9\u03b1\u03c6\u03bf\u03c1\u03b5\u03c4\u03b9\u03ba\u03bf\u03cd\u03c2 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b5\u03c2, \u03ad\u03bd\u03b1\u03bd \u03b3\u03b9\u03b1 \u03ba\u03ac\u03b8\u03b5 \u03c6\u03ac\u03ba\u03b5\u03bb\u03bf. \n\n \u0397 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03ba\u03b1\u03b9 \u03bf\u03b9 \u03b5\u03bd\u03c4\u03bf\u03bb\u03ad\u03c2 \u03ad\u03c7\u03bf\u03c5\u03bd \u03b1\u03c6\u03b1\u03b9\u03c1\u03b5\u03b8\u03b5\u03af \u03ba\u03b1\u03b8\u03ce\u03c2 \u03b4\u03b5\u03bd \u03c6\u03b1\u03af\u03bd\u03b5\u03c4\u03b1\u03b9 \u03bd\u03b1 \u03ad\u03c7\u03bf\u03c5\u03bd \u03c0\u03c1\u03b1\u03b3\u03bc\u03b1\u03c4\u03b9\u03ba\u03ae \u03b1\u03be\u03af\u03b1 \u03b3\u03b9\u03b1 \u03c4\u03bf\u03c5\u03c2 \u03b1\u03c5\u03c4\u03bf\u03bc\u03b1\u03c4\u03b9\u03c3\u03bc\u03bf\u03cd\u03c2.", + "title": "\u0391\u03bb\u03bb\u03b1\u03b3\u03ad\u03c2 \u03c3\u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Radarr" + } + }, + "options": { + "step": { + "init": { + "data": { + "upcoming_days": "\u0391\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03c0\u03c1\u03bf\u03c3\u03b5\u03c7\u03ce\u03bd \u03b7\u03bc\u03b5\u03c1\u03ce\u03bd \u03c0\u03c1\u03bf\u03c2 \u03b5\u03bc\u03c6\u03ac\u03bd\u03b9\u03c3\u03b7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radarr/translations/en.json b/homeassistant/components/radarr/translations/en.json index 7172eed0021..168c3cc2fe2 100644 --- a/homeassistant/components/radarr/translations/en.json +++ b/homeassistant/components/radarr/translations/en.json @@ -7,25 +7,35 @@ "error": { "cannot_connect": "Failed to connect", "invalid_auth": "Invalid authentication", - "zeroconf_failed": "API key not found. Please enter it manually", + "unknown": "Unexpected error", "wrong_app": "Incorrect application reached. Please try again", - "unknown": "Unexpected error" + "zeroconf_failed": "API key not found. Please enter it manually" }, "step": { "reauth_confirm": { - "title": "Reauthenticate Integration", - "description": "The Radarr integration needs to be manually re-authenticated with the Radarr API" + "description": "The Radarr integration needs to be manually re-authenticated with the Radarr API", + "title": "Reauthenticate Integration" }, "user": { - "description": "API key can be retrieved automatically if login credentials were not set in application.\nYour API key can be found in Settings > General in the Radarr Web UI.", "data": { "api_key": "API Key", "url": "URL", "verify_ssl": "Verify SSL certificate" - } + }, + "description": "API key can be retrieved automatically if login credentials were not set in application.\nYour API key can be found in Settings > General in the Radarr Web UI." } } }, + "issues": { + "deprecated_yaml": { + "description": "Configuring Radarr using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Radarr YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Radarr YAML configuration is being removed" + }, + "removed_attributes": { + "description": "Some breaking changes has been made in disabling the Movies count sensor out of caution.\n\nThis sensor can cause problems with massive databases. If you still wish to use it, you may do so.\n\nMovie names are no longer included as attributes in the movies sensor.\n\nUpcoming has been removed. It is being modernized as calendar items should be. Disk space is now split into different sensors, one for each folder.\n\nStatus and commands have been removed as they don't appear to have real value for automations.", + "title": "Changes to the Radarr integration" + } + }, "options": { "step": { "init": { @@ -34,15 +44,5 @@ } } } - }, - "issues": { - "deprecated_yaml": { - "title": "The Radarr YAML configuration is being removed", - "description": "Configuring Radarr using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Radarr YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." - }, - "removed_attributes": { - "title": "Changes to the Radarr integration", - "description": "Some breaking changes has been made in disabling the Movies count sensor out of caution.\n\nThis sensor can cause problems with massive databases. If you still wish to use it, you may do so.\n\nMovie names are no longer included as attributes in the movies sensor.\n\nUpcoming has been removed. It is being modernized as calendar items should be. Disk space is now split into different sensors, one for each folder.\n\nStatus and commands have been removed as they don't appear to have real value for automations." - } } -} +} \ No newline at end of file diff --git a/homeassistant/components/radarr/translations/es.json b/homeassistant/components/radarr/translations/es.json new file mode 100644 index 00000000000..bb8c8888654 --- /dev/null +++ b/homeassistant/components/radarr/translations/es.json @@ -0,0 +1,48 @@ +{ + "config": { + "abort": { + "already_configured": "El servicio ya est\u00e1 configurado", + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "unknown": "Error inesperado", + "wrong_app": "Se ha alcanzado una aplicaci\u00f3n incorrecta. Por favor, int\u00e9ntalo de nuevo", + "zeroconf_failed": "Clave API no encontrada. Por favor, introd\u00facela manualmente" + }, + "step": { + "reauth_confirm": { + "description": "La integraci\u00f3n Radarr debe volver a autenticarse manualmente con la API de Radarr", + "title": "Volver a autenticar la integraci\u00f3n" + }, + "user": { + "data": { + "api_key": "Clave API", + "url": "URL", + "verify_ssl": "Verificar el certificado SSL" + }, + "description": "La clave API se puede recuperar autom\u00e1ticamente si las credenciales de inicio de sesi\u00f3n no se configuraron en la aplicaci\u00f3n.\nPuedes encontrar tu clave API en Configuraci\u00f3n > General en la IU web de Radarr." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Se eliminar\u00e1 la configuraci\u00f3n de Radarr mediante YAML. \n\nTu configuraci\u00f3n YAML existente se ha importado a la IU autom\u00e1ticamente. \n\nElimina la configuraci\u00f3n YAML de Radarr de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "Se va a eliminar la configuraci\u00f3n YAML de Radarr" + }, + "removed_attributes": { + "description": "Se han realizado algunos cambios importantes al deshabilitar el sensor de conteo de pel\u00edculas por precauci\u00f3n. \n\nEste sensor puede causar problemas con bases de datos enormes. Si a\u00fan deseas utilizarlo, puedes hacerlo. \n\nLos nombres de las pel\u00edculas ya no se incluyen como atributos en el sensor de pel\u00edculas. \n\nPr\u00f3ximamente ha sido eliminado. Se est\u00e1 modernizando como deber\u00edan ser los elementos del calendario. El espacio en disco ahora se divide en diferentes sensores, uno para cada carpeta. \n\nEl estado y los comandos se han eliminado porque no parecen tener un valor real para las automatizaciones.", + "title": "Cambios en la integraci\u00f3n de Radarr" + } + }, + "options": { + "step": { + "init": { + "data": { + "upcoming_days": "N\u00famero de d\u00edas pr\u00f3ximos a mostrar" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radarr/translations/fr.json b/homeassistant/components/radarr/translations/fr.json new file mode 100644 index 00000000000..5f8d2a9f78d --- /dev/null +++ b/homeassistant/components/radarr/translations/fr.json @@ -0,0 +1,45 @@ +{ + "config": { + "abort": { + "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification non valide", + "unknown": "Erreur inattendue", + "wrong_app": "Une application incorrecte a \u00e9t\u00e9 atteinte. Veuillez r\u00e9essayer", + "zeroconf_failed": "La cl\u00e9 d'API n'a pas \u00e9t\u00e9 trouv\u00e9e. Veuillez la saisir manuellement" + }, + "step": { + "reauth_confirm": { + "description": "L'int\u00e9gration Radarr doit \u00eatre r\u00e9-authentifi\u00e9e manuellement avec l'API Radarr", + "title": "R\u00e9-authentifier l'int\u00e9gration" + }, + "user": { + "data": { + "api_key": "Cl\u00e9 d'API", + "url": "URL", + "verify_ssl": "V\u00e9rifier le certificat SSL" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "title": "La configuration YAML pour Radarr sera bient\u00f4t supprim\u00e9e" + }, + "removed_attributes": { + "title": "Modifications apport\u00e9es \u00e0 l'int\u00e9gration Radarr" + } + }, + "options": { + "step": { + "init": { + "data": { + "upcoming_days": "Nombre de jours \u00e0 venir \u00e0 afficher" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radarr/translations/id.json b/homeassistant/components/radarr/translations/id.json new file mode 100644 index 00000000000..12b85c3e43a --- /dev/null +++ b/homeassistant/components/radarr/translations/id.json @@ -0,0 +1,47 @@ +{ + "config": { + "abort": { + "already_configured": "Layanan sudah dikonfigurasi", + "reauth_successful": "Autentikasi ulang berhasil" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan", + "wrong_app": "Aplikasi yang salah tercapai. Silakan coba lagi", + "zeroconf_failed": "Kunci API tidak ditemukan. Silakan masukkan secara manual" + }, + "step": { + "reauth_confirm": { + "description": "Integrasi Radarr perlu diautentikasi ulang secara manual dengan Radarr API", + "title": "Autentikasi Ulang Integrasi" + }, + "user": { + "data": { + "api_key": "Kunci API", + "url": "URL", + "verify_ssl": "Verifikasi sertifikat SSL" + }, + "description": "Kunci API dapat diambil secara otomatis jika kredensial login tidak diatur dalam aplikasi.\nKunci API Anda dapat ditemukan di Settings > General di antarmuka web Radarr." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Proses konfigurasi Radarr lewat YAML dalam proses penghapusan.\n\nKonfigurasi YAML yang ada telah diimpor ke antarmuka secara otomatis.\n\nHapus konfigurasi YAML Radarr dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Konfigurasi YAML Radarr dalam proses penghapusan" + }, + "removed_attributes": { + "title": "Perubahan pada integrasi Radarr" + } + }, + "options": { + "step": { + "init": { + "data": { + "upcoming_days": "Jumlah hari mendatang untuk ditampilkan" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radarr/translations/it.json b/homeassistant/components/radarr/translations/it.json new file mode 100644 index 00000000000..75514420617 --- /dev/null +++ b/homeassistant/components/radarr/translations/it.json @@ -0,0 +1,48 @@ +{ + "config": { + "abort": { + "already_configured": "Il servizio \u00e8 gi\u00e0 configurato", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto", + "wrong_app": "Applicazione errata raggiunta. Per favore riprova", + "zeroconf_failed": "Chiave API non trovata. Si prega di inserirla manualmente" + }, + "step": { + "reauth_confirm": { + "description": "L'integrazione Radarr deve essere riautenticata manualmente con l'API Radarr.", + "title": "Autentica nuovamente l'integrazione" + }, + "user": { + "data": { + "api_key": "Chiave API", + "url": "URL", + "verify_ssl": "Verifica il certificato SSL" + }, + "description": "La chiave API pu\u00f2 essere recuperata automaticamente se le credenziali di accesso non sono state impostate nell'applicazione.\nLa chiave API pu\u00f2 essere trovata in Impostazioni > Generali nell'interfaccia Web di Radarr." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "La configurazione di Radarr tramite YAML \u00e8 stata rimossa.\n\nLa configurazione YAML esistente \u00e8 stata importata automaticamente nell'interfaccia utente.\n\nRimuovere la configurazione YAML di Radarr dal file configuration.yaml e riavviare Home Assistant per risolvere il problema.", + "title": "La configurazione YAML di Radarr \u00e8 stata rimossa" + }, + "removed_attributes": { + "description": "Sono state apportate alcune modifiche alla disabilitazione del sensore di conteggio dei filmati per prudenza.\n\nQuesto sensore pu\u00f2 causare problemi con database di grandi dimensioni. Se si desidera ancora utilizzarlo, \u00e8 possibile farlo.\n\nI nomi dei film non sono pi\u00f9 inclusi come attributi nel sensore dei film.\n\nLa voce Upcoming \u00e8 stata rimossa. \u00c8 stato modernizzato come dovrebbero essere gli elementi del calendario. Lo spazio su disco \u00e8 ora suddiviso in diversi sensori, uno per ogni cartella.\n\nStato e comandi sono stati rimossi perch\u00e9 non sembrano avere un valore reale per le automazioni.", + "title": "Modifiche all'integrazione Radarr" + } + }, + "options": { + "step": { + "init": { + "data": { + "upcoming_days": "Numero dei prossimi giorni da visualizzare" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radarr/translations/no.json b/homeassistant/components/radarr/translations/no.json new file mode 100644 index 00000000000..4b6b2adb523 --- /dev/null +++ b/homeassistant/components/radarr/translations/no.json @@ -0,0 +1,48 @@ +{ + "config": { + "abort": { + "already_configured": "Tjenesten er allerede konfigurert", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil", + "wrong_app": "Feil s\u00f8knad er n\u00e5dd. V\u00e6r s\u00e5 snill, pr\u00f8v p\u00e5 nytt", + "zeroconf_failed": "Finner ikke API-n\u00f8kkel. Vennligst skriv det inn manuelt" + }, + "step": { + "reauth_confirm": { + "description": "Radarr-integrasjonen m\u00e5 re-autentiseres manuelt med Radarr API", + "title": "Godkjenne integrering p\u00e5 nytt" + }, + "user": { + "data": { + "api_key": "API-n\u00f8kkel", + "url": "URL", + "verify_ssl": "Verifisere SSL-sertifikat" + }, + "description": "API-n\u00f8kkel kan hentes automatisk hvis p\u00e5loggingsinformasjon ikke ble angitt i applikasjonen.\n API-n\u00f8kkelen din finner du i Innstillinger > Generelt i Radarr Web UI." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfigurering av Radarr med YAML blir fjernet. \n\n Din eksisterende YAML-konfigurasjon har blitt importert til brukergrensesnittet automatisk. \n\n Fjern Radarr YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "Radarr YAML-konfigurasjonen blir fjernet" + }, + "removed_attributes": { + "description": "Noen bruddendringer er gjort for \u00e5 deaktivere filmtellingssensoren ut av forsiktighet. \n\n Denne sensoren kan for\u00e5rsake problemer med massive databaser. Hvis du fortsatt \u00f8nsker \u00e5 bruke den, kan du gj\u00f8re det. \n\n Filmnavn er ikke lenger inkludert som attributter i filmsensoren. \n\n Kommende er fjernet. Den moderniseres slik kalenderposter skal v\u00e6re. Diskplass er n\u00e5 delt inn i forskjellige sensorer, en for hver mappe. \n\n Status og kommandoer er fjernet da de ikke ser ut til \u00e5 ha reell verdi for automatisering.", + "title": "Endringer i Radarr-integrasjonen" + } + }, + "options": { + "step": { + "init": { + "data": { + "upcoming_days": "Antall kommende dager \u00e5 vise" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radarr/translations/pt-BR.json b/homeassistant/components/radarr/translations/pt-BR.json new file mode 100644 index 00000000000..74d33fa6136 --- /dev/null +++ b/homeassistant/components/radarr/translations/pt-BR.json @@ -0,0 +1,48 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" + }, + "error": { + "cannot_connect": "Falhou ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado", + "wrong_app": "Aplica\u00e7\u00e3o incorreta alcan\u00e7ada. Por favor, tente novamente", + "zeroconf_failed": "Chave de API n\u00e3o encontrada. Por favor, insira-o manualmente" + }, + "step": { + "reauth_confirm": { + "description": "A integra\u00e7\u00e3o do Radarr precisa ser autenticada manualmente com a API do Radarr", + "title": "Reautenticar Integra\u00e7\u00e3o" + }, + "user": { + "data": { + "api_key": "Chave de API", + "url": "URL", + "verify_ssl": "Verificar certificado SSL" + }, + "description": "A chave de API pode ser recuperada automaticamente se as credenciais de login n\u00e3o tiverem sido definidas no aplicativo.\n Sua chave de API pode ser encontrada em Configura\u00e7\u00f5es > Geral na interface da Web do Radarr." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "A configura\u00e7\u00e3o do Radarr 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 Radarr do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o YAML do Radarr est\u00e1 sendo removida" + }, + "removed_attributes": { + "description": "Algumas mudan\u00e7as importantes foram feitas na desativa\u00e7\u00e3o do sensor de contagem de filmes por precau\u00e7\u00e3o. \n\n Este sensor pode causar problemas com bancos de dados massivos. Se voc\u00ea ainda deseja us\u00e1-lo, voc\u00ea pode faz\u00ea-lo. \n\n Os nomes dos filmes n\u00e3o s\u00e3o mais inclu\u00eddos como atributos no sensor de filmes. \n\n O pr\u00f3ximo foi removido. Ele est\u00e1 sendo modernizado como os itens do calend\u00e1rio devem ser. O espa\u00e7o em disco agora \u00e9 dividido em diferentes sensores, um para cada pasta. \n\n Status e comandos foram removidos, pois n\u00e3o parecem ter valor real para automa\u00e7\u00f5es.", + "title": "Mudan\u00e7as na integra\u00e7\u00e3o do Radarr" + } + }, + "options": { + "step": { + "init": { + "data": { + "upcoming_days": "N\u00famero de pr\u00f3ximos dias a serem exibidos" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radarr/translations/ru.json b/homeassistant/components/radarr/translations/ru.json new file mode 100644 index 00000000000..46fd877aa3a --- /dev/null +++ b/homeassistant/components/radarr/translations/ru.json @@ -0,0 +1,48 @@ +{ + "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.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430.", + "wrong_app": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435. \u041f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0451 \u0440\u0430\u0437.", + "zeroconf_failed": "\u041a\u043b\u044e\u0447 API \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u0435\u0433\u043e \u0432\u0440\u0443\u0447\u043d\u0443\u044e." + }, + "step": { + "reauth_confirm": { + "description": "\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e API Radarr", + "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", + "url": "URL-\u0430\u0434\u0440\u0435\u0441", + "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL" + }, + "description": "\u041a\u043b\u044e\u0447 API \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u043f\u043e\u043b\u0443\u0447\u0435\u043d \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438, \u0435\u0441\u043b\u0438 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0434\u043b\u044f \u0432\u0445\u043e\u0434\u0430 \u043d\u0435 \u0431\u044b\u043b\u0438 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u044b \u0432 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0438.\n\u0412\u0430\u0448 \u043a\u043b\u044e\u0447 API \u043c\u043e\u0436\u043d\u043e \u043d\u0430\u0439\u0442\u0438 \u0432 \u0440\u0430\u0437\u0434\u0435\u043b\u0435 \u00ab\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 > \u00ab\u041e\u0441\u043d\u043e\u0432\u043d\u044b\u0435\u00bb \u0432 \u0432\u0435\u0431-\u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0435 Radarr." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Radarr \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 Radarr \u0447\u0435\u0440\u0435\u0437 YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + }, + "removed_attributes": { + "description": "\u0412 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u043c\u0435\u0440\u044b \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u043e\u0440\u043e\u0436\u043d\u043e\u0441\u0442\u0438 \u0431\u044b\u043b \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d \u0441\u0435\u043d\u0441\u043e\u0440 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u0430 \u0444\u0438\u043b\u044c\u043c\u043e\u0432. \u042d\u0442\u043e\u0442 \u0441\u0435\u043d\u0441\u043e\u0440 \u043c\u043e\u0436\u0435\u0442 \u0432\u044b\u0437\u0432\u0430\u0442\u044c \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u0441 \u043c\u0430\u0441\u0441\u0438\u0432\u043d\u044b\u043c\u0438 \u0431\u0430\u0437\u0430\u043c\u0438 \u0434\u0430\u043d\u043d\u044b\u0445, \u043d\u043e \u043f\u0440\u0438 \u0436\u0435\u043b\u0430\u043d\u0438\u0438 \u0412\u044b \u0432\u0441\u0435 \u0435\u0449\u0451 \u043c\u043e\u0436\u0435\u0442\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0435\u0433\u043e.\n\n\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u044f \u0444\u0438\u043b\u044c\u043c\u043e\u0432 \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0432\u043a\u043b\u044e\u0447\u0430\u044e\u0442\u0441\u044f \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0430\u0442\u0440\u0438\u0431\u0443\u0442\u043e\u0432 \u0432 \u0441\u0435\u043d\u0441\u043e\u0440 \u0444\u0438\u043b\u044c\u043c\u043e\u0432.\n\n\u041f\u0440\u0435\u0434\u0441\u0442\u043e\u044f\u0449\u0435\u0435 \u0431\u044b\u043b\u043e \u0443\u0434\u0430\u043b\u0435\u043d\u043e. \u041e\u043d\u043e \u043c\u043e\u0434\u0435\u0440\u043d\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u043d\u043e, \u043a\u0430\u043a \u0438 \u0434\u0440\u0443\u0433\u0438\u0435 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u044b \u043a\u0430\u043b\u0435\u043d\u0434\u0430\u0440\u044f. \u0414\u0438\u0441\u043a\u043e\u0432\u043e\u0435 \u043f\u0440\u043e\u0441\u0442\u0440\u0430\u043d\u0441\u0442\u0432\u043e \u0442\u0435\u043f\u0435\u0440\u044c \u0440\u0430\u0437\u0434\u0435\u043b\u0435\u043d\u043e \u043d\u0430 \u0440\u0430\u0437\u043d\u044b\u0435 \u0441\u0435\u043d\u0441\u043e\u0440\u044b, \u043f\u043e \u043e\u0434\u043d\u043e\u043c\u0443 \u0434\u043b\u044f \u043a\u0430\u0436\u0434\u043e\u0439 \u043f\u0430\u043f\u043a\u0438.\n\n\u0421\u0442\u0430\u0442\u0443\u0441 \u0438 \u043a\u043e\u043c\u0430\u043d\u0434\u044b \u0431\u044b\u043b\u0438 \u0443\u0434\u0430\u043b\u0435\u043d\u044b, \u0442\u0430\u043a \u043a\u0430\u043a \u043e\u043d\u0438 \u043d\u0435 \u0438\u043c\u0435\u044e\u0442 \u0440\u0435\u0430\u043b\u044c\u043d\u043e\u0439 \u0446\u0435\u043d\u043d\u043e\u0441\u0442\u0438 \u0434\u043b\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0438.", + "title": "\u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f \u0432 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 Radarr" + } + }, + "options": { + "step": { + "init": { + "data": { + "upcoming_days": "\u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043f\u0440\u0435\u0434\u0441\u0442\u043e\u044f\u0449\u0438\u0445 \u0434\u043d\u0435\u0439 \u0434\u043b\u044f \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radarr/translations/zh-Hant.json b/homeassistant/components/radarr/translations/zh-Hant.json new file mode 100644 index 00000000000..6b381643c1a --- /dev/null +++ b/homeassistant/components/radarr/translations/zh-Hant.json @@ -0,0 +1,48 @@ +{ + "config": { + "abort": { + "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4", + "wrong_app": "\u5b58\u53d6\u61c9\u7528\u7a0b\u5f0f\u4e0d\u6b63\u78ba\uff0c\u8acb\u518d\u8a66\u4e00\u6b21", + "zeroconf_failed": "\u627e\u4e0d\u5230 API \u91d1\u9470\u3001\u8acb\u624b\u52d5\u8f38\u5165\u3002" + }, + "step": { + "reauth_confirm": { + "description": "Radarr \u6574\u5408\u9700\u8981\u624b\u52d5\u91cd\u65b0\u8a8d\u8b49 Radarr API", + "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" + }, + "user": { + "data": { + "api_key": "API \u91d1\u9470", + "url": "\u7db2\u5740", + "verify_ssl": "\u78ba\u8a8d SSL \u8a8d\u8b49" + }, + "description": "\u5047\u5982\u6c92\u6709\u65bc\u61c9\u7528\u7a0b\u5f0f\u4e2d\u8a2d\u5b9a\u767b\u5165\u6191\u8b49\uff0c\u5247\u53ef\u4ee5\u81ea\u52d5\u53d6\u5f97 API \u91d1\u9470\u3002\n\u91d1\u9470\u53ef\u4ee5\u65bc Radarr Web \u4ecb\u9762\u4e2d\u8a2d\u5b9a\uff08Settings\uff09 > \u4e00\u822c\uff08General\uff09\u4e2d\u53d6\u5f97\u3002" + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a\u7684 Radarr \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 Radarr YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Radarr YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" + }, + "removed_attributes": { + "description": "\u5728\u8b39\u614e\u8003\u91cf\u5f8c\u3001\u95dc\u9589\u96fb\u5f71\u6578\u611f\u6e2c\u5668\u4e0a\u505a\u4e86\u4e00\u4e9b\u91cd\u5927\u8b8a\u66f4\u3002\n\n\u7531\u65bc\u5de8\u5927\u7684\u8cc7\u6599\u5eab\u53ef\u80fd\u6703\u5c0e\u81f4\u611f\u6e2c\u5668\u51fa\u73fe\u554f\u984c\u3002\u5047\u5982\u60a8\u4ecd\u60f3\u7e7c\u7e8c\u4f7f\u7528\u3001\u8acb\u6ce8\u610f\u76f8\u95dc\u554f\u984c\u3002\n\n\u96fb\u5f71\u611f\u6e2c\u5668\u5c6c\u6027\u4e2d\u5c07\u4e0d\u518d\u5305\u542b\u96fb\u5f71\u540d\u7a31\u3002\n\n\u5373\u5c07\u4e0a\u6620\u90e8\u5206\u5df2\u7d93\u79fb\u9664\u3002\u6b63\u8ddf\u8457\u884c\u4e8b\u66c6\u529f\u80fd\u9032\u884c\u66f4\u65b0\uff0c\u78c1\u789f\u7a7a\u9593\u5c07\u6703\u4f9d\u64da\u4e0d\u540c\u611f\u6e2c\u5668\u9032\u884c\u5206\u9694\u65bc\u5404\u81ea\u7684\u8cc7\u6599\u593e\u3002\n\n\u7531\u65bc\u5c0d\u65bc\u81ea\u52d5\u5316\u7684\u7528\u9014\u4e0d\u9ad8\uff0c\u72c0\u614b\u8207\u547d\u4ee4\u4e5f\u5df2\u7d93\u79fb\u9664\u3002", + "title": "\u8b8a\u66f4\u81f3 Radarr \u6574\u5408" + } + }, + "options": { + "step": { + "init": { + "data": { + "upcoming_days": "\u5373\u5c07\u5230\u4f86\u986f\u793a\u5929\u6578" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainmachine/translations/bg.json b/homeassistant/components/rainmachine/translations/bg.json index 1239915231b..4ba51cf991e 100644 --- a/homeassistant/components/rainmachine/translations/bg.json +++ b/homeassistant/components/rainmachine/translations/bg.json @@ -11,5 +11,17 @@ "title": "\u041f\u043e\u043f\u044a\u043b\u043d\u0435\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f\u0442\u0430 \u0441\u0438" } } + }, + "issues": { + "replaced_old_entity": { + "fix_flow": { + "step": { + "confirm": { + "title": "\u041e\u0431\u0435\u043a\u0442\u044a\u0442 {old_entity_id} \u0449\u0435 \u0431\u044a\u0434\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0430\u0442" + } + } + }, + "title": "\u041e\u0431\u0435\u043a\u0442\u044a\u0442 {old_entity_id} \u0449\u0435 \u0431\u044a\u0434\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0430\u0442" + } } } \ No newline at end of file diff --git a/homeassistant/components/rainmachine/translations/de.json b/homeassistant/components/rainmachine/translations/de.json index 20c49ea30f4..51b6f0814af 100644 --- a/homeassistant/components/rainmachine/translations/de.json +++ b/homeassistant/components/rainmachine/translations/de.json @@ -18,6 +18,19 @@ } } }, + "issues": { + "replaced_old_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "Aktualisiere alle Automatisierungen oder Skripte, die diese Entit\u00e4t verwenden, um stattdessen `{replacement_entity_id}` zu verwenden.", + "title": "Die Entit\u00e4t {old_entity_id} wird entfernt" + } + } + }, + "title": "Die Entit\u00e4t {old_entity_id} wird entfernt" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/rainmachine/translations/es.json b/homeassistant/components/rainmachine/translations/es.json index 3e13d925b34..a8bcc7cbd0d 100644 --- a/homeassistant/components/rainmachine/translations/es.json +++ b/homeassistant/components/rainmachine/translations/es.json @@ -18,6 +18,19 @@ } } }, + "issues": { + "replaced_old_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "Actualiza cualquier automatizaci\u00f3n o script que use esta entidad para usar `{replacement_entity_id}`.", + "title": "Se eliminar\u00e1 la entidad {old_entity_id}" + } + } + }, + "title": "Se eliminar\u00e1 la entidad {old_entity_id}" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/rainmachine/translations/it.json b/homeassistant/components/rainmachine/translations/it.json index c63bbd7db11..9cca839ea00 100644 --- a/homeassistant/components/rainmachine/translations/it.json +++ b/homeassistant/components/rainmachine/translations/it.json @@ -18,6 +18,19 @@ } } }, + "issues": { + "replaced_old_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "Aggiorna tutte le automazioni o gli script che utilizzano questa entit\u00e0 in modo che utilizzino invece `{replacement_entity_id}`.", + "title": "L'entit\u00e0 {old_entity_id} verr\u00e0 rimossa" + } + } + }, + "title": "L'entit\u00e0 {old_entity_id} verr\u00e0 rimossa" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/rainmachine/translations/no.json b/homeassistant/components/rainmachine/translations/no.json index 7fbeda38374..f7e0a758ee0 100644 --- a/homeassistant/components/rainmachine/translations/no.json +++ b/homeassistant/components/rainmachine/translations/no.json @@ -18,6 +18,19 @@ } } }, + "issues": { + "replaced_old_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "Oppdater eventuelle automatiseringer eller skript som bruker denne enheten til i stedet \u00e5 bruke ` {replacement_entity_id} `.", + "title": "{old_entity_id} vil bli fjernet" + } + } + }, + "title": "{old_entity_id} vil bli fjernet" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/rainmachine/translations/pt-BR.json b/homeassistant/components/rainmachine/translations/pt-BR.json index 6359b1b6ae9..7128423202e 100644 --- a/homeassistant/components/rainmachine/translations/pt-BR.json +++ b/homeassistant/components/rainmachine/translations/pt-BR.json @@ -18,6 +18,19 @@ } } }, + "issues": { + "replaced_old_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "Atualize quaisquer automa\u00e7\u00f5es ou scripts que usam essa entidade para usar `{replacement_entity_id}`.", + "title": "A entidade {old_entity_id} ser\u00e1 removida" + } + } + }, + "title": "A entidade {old_entity_id} ser\u00e1 removida" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/rainmachine/translations/ru.json b/homeassistant/components/rainmachine/translations/ru.json index 8dbe804ecab..2ed8c6df530 100644 --- a/homeassistant/components/rainmachine/translations/ru.json +++ b/homeassistant/components/rainmachine/translations/ru.json @@ -18,6 +18,19 @@ } } }, + "issues": { + "replaced_old_entity": { + "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\u043e\u0442 \u043e\u0431\u044a\u0435\u043a\u0442, \u0442\u0435\u043f\u0435\u0440\u044c \u0441\u043b\u0435\u0434\u0443\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043e\u0431\u044a\u0435\u043a\u0442 `{replacement_entity_id}`.", + "title": "\u041e\u0431\u044a\u0435\u043a\u0442 {old_entity_id} \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d" + } + } + }, + "title": "\u041e\u0431\u044a\u0435\u043a\u0442 {old_entity_id} \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/rainmachine/translations/zh-Hant.json b/homeassistant/components/rainmachine/translations/zh-Hant.json index d37ae79541f..c0ead98a13e 100644 --- a/homeassistant/components/rainmachine/translations/zh-Hant.json +++ b/homeassistant/components/rainmachine/translations/zh-Hant.json @@ -18,6 +18,19 @@ } } }, + "issues": { + "replaced_old_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u4f7f\u7528\u6b64\u5be6\u9ad4\u4ee5\u66f4\u65b0\u4efb\u4f55\u81ea\u52d5\u5316\u6216\u8173\u672c\uff0c\u4ee5\u53d6\u4ee3 `{replacement_entity_id}`\u3002", + "title": "{old_entity_id} \u5be6\u9ad4\u5c07\u9032\u884c\u79fb\u9664" + } + } + }, + "title": "{old_entity_id} \u5be6\u9ad4\u5c07\u9032\u884c\u79fb\u9664" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/rhasspy/translations/bg.json b/homeassistant/components/rhasspy/translations/bg.json new file mode 100644 index 00000000000..1c6120581b0 --- /dev/null +++ b/homeassistant/components/rhasspy/translations/bg.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "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." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/scrape/translations/bg.json b/homeassistant/components/scrape/translations/bg.json index f22c3fd3b26..89c2ffc7880 100644 --- a/homeassistant/components/scrape/translations/bg.json +++ b/homeassistant/components/scrape/translations/bg.json @@ -11,6 +11,7 @@ "index": "\u0418\u043d\u0434\u0435\u043a\u0441", "name": "\u0418\u043c\u0435", "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "select": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435", "unit_of_measurement": "\u0415\u0434\u0438\u043d\u0438\u0446\u0430 \u0437\u0430 \u0438\u0437\u043c\u0435\u0440\u0432\u0430\u043d\u0435", "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" } @@ -26,7 +27,9 @@ "index": "\u0418\u043d\u0434\u0435\u043a\u0441", "name": "\u0418\u043c\u0435", "password": "\u041f\u0430\u0440\u043e\u043b\u0430", - "unit_of_measurement": "\u0415\u0434\u0438\u043d\u0438\u0446\u0430 \u0437\u0430 \u0438\u0437\u043c\u0435\u0440\u0432\u0430\u043d\u0435" + "select": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435", + "unit_of_measurement": "\u0415\u0434\u0438\u043d\u0438\u0446\u0430 \u0437\u0430 \u0438\u0437\u043c\u0435\u0440\u0432\u0430\u043d\u0435", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" } } } diff --git a/homeassistant/components/select/translations/bg.json b/homeassistant/components/select/translations/bg.json new file mode 100644 index 00000000000..bcee96446e9 --- /dev/null +++ b/homeassistant/components/select/translations/bg.json @@ -0,0 +1,3 @@ +{ + "title": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435" +} \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/bg.json b/homeassistant/components/shelly/translations/bg.json index f7cb7f9c48e..a000ef9933d 100644 --- a/homeassistant/components/shelly/translations/bg.json +++ b/homeassistant/components/shelly/translations/bg.json @@ -29,7 +29,8 @@ "button": "\u0411\u0443\u0442\u043e\u043d", "button1": "\u041f\u044a\u0440\u0432\u0438 \u0431\u0443\u0442\u043e\u043d", "button2": "\u0412\u0442\u043e\u0440\u0438 \u0431\u0443\u0442\u043e\u043d", - "button3": "\u0422\u0440\u0435\u0442\u0438 \u0431\u0443\u0442\u043e\u043d" + "button3": "\u0422\u0440\u0435\u0442\u0438 \u0431\u0443\u0442\u043e\u043d", + "button4": "\u0427\u0435\u0442\u0432\u044a\u0440\u0442\u0438 \u0431\u0443\u0442\u043e\u043d" } } } \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/it.json b/homeassistant/components/simplisafe/translations/it.json index 997f376916d..dda07835a81 100644 --- a/homeassistant/components/simplisafe/translations/it.json +++ b/homeassistant/components/simplisafe/translations/it.json @@ -39,6 +39,12 @@ } } }, + "issues": { + "deprecated_service": { + "description": "Aggiorna tutte le automazioni o gli script che utilizzano questo servizio per utilizzare invece il servizio `{alternate_service}` con un ID entit\u00e0 di destinazione di `{alternate_target}`. Quindi, fai clic su INVIA di seguito per contrassegnare questo problema come risolto.", + "title": "Il servizio {deprecated_service} \u00e8 stato rimosso" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/simplisafe/translations/no.json b/homeassistant/components/simplisafe/translations/no.json index 2997ca5dba5..e2c369e9dd8 100644 --- a/homeassistant/components/simplisafe/translations/no.json +++ b/homeassistant/components/simplisafe/translations/no.json @@ -39,6 +39,12 @@ } } }, + "issues": { + "deprecated_service": { + "description": "Oppdater eventuelle automatiseringer eller skript som bruker denne tjenesten for i stedet \u00e5 bruke ` {alternate_service} `-tjenesten med en m\u00e5lenhets-ID p\u00e5 ` {alternate_target} `. Klikk deretter SEND nedenfor for \u00e5 merke dette problemet som l\u00f8st.", + "title": "{deprecated_service} -tjenesten blir fjernet" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/soundtouch/translations/bg.json b/homeassistant/components/soundtouch/translations/bg.json new file mode 100644 index 00000000000..ab665e1e59b --- /dev/null +++ b/homeassistant/components/soundtouch/translations/bg.json @@ -0,0 +1,10 @@ +{ + "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" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/spotify/translations/bg.json b/homeassistant/components/spotify/translations/bg.json index 982756c0d85..35e3428d677 100644 --- a/homeassistant/components/spotify/translations/bg.json +++ b/homeassistant/components/spotify/translations/bg.json @@ -5,5 +5,10 @@ "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" } } + }, + "issues": { + "removed_yaml": { + "title": "YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 Spotify \u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0430\u0442\u0430" + } } } \ No newline at end of file diff --git a/homeassistant/components/steamist/translations/bg.json b/homeassistant/components/steamist/translations/bg.json index 10f6abeb604..db9a655fd52 100644 --- a/homeassistant/components/steamist/translations/bg.json +++ b/homeassistant/components/steamist/translations/bg.json @@ -2,7 +2,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", - "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", + "no_devices_found": "\u041d\u044f\u043c\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\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/tasmota/translations/it.json b/homeassistant/components/tasmota/translations/it.json index 22269eafd9f..f4931120402 100644 --- a/homeassistant/components/tasmota/translations/it.json +++ b/homeassistant/components/tasmota/translations/it.json @@ -16,5 +16,15 @@ "description": "Vuoi configurare Tasmota?" } } + }, + "issues": { + "topic_duplicated": { + "description": "Diversi dispositivi Tasmota stanno condividendo l'argomento {topic}. \n\nDispositivi Tasmota con questo problema: {offenders}.", + "title": "Diversi dispositivi Tasmota condividono lo stesso argomento" + }, + "topic_no_prefix": { + "description": "Il dispositivo Tasmota {name} con IP {ip} non include `%prefix%` nel suo argomento completo. \nLe entit\u00e0 per questi dispositivi sono disabilitate fino a quando la configurazione non \u00e8 stata corretta.", + "title": "Il dispositivo Tasmota {name} ha un argomento MQTT non valido" + } } } \ No newline at end of file diff --git a/homeassistant/components/tasmota/translations/no.json b/homeassistant/components/tasmota/translations/no.json index 7f0a67e7c9f..dfb7983d973 100644 --- a/homeassistant/components/tasmota/translations/no.json +++ b/homeassistant/components/tasmota/translations/no.json @@ -16,5 +16,15 @@ "description": "Vil du sette opp Tasmota?" } } + }, + "issues": { + "topic_duplicated": { + "description": "Flere Tasmota-enheter deler emnet {topic} . \n\n Tasmota-enheter med dette problemet: {offenders} .", + "title": "Flere Tasmota-enheter deler samme emne" + }, + "topic_no_prefix": { + "description": "Tasmota-enhet {name} med IP {ip} inkluderer ikke ` %prefix% ` i hele emnet. \n\n Entiteter for denne enheten er deaktivert til konfigurasjonen er korrigert.", + "title": "Tasmota-enheten {name} har et ugyldig MQTT-emne" + } } } \ No newline at end of file diff --git a/homeassistant/components/transmission/translations/bg.json b/homeassistant/components/transmission/translations/bg.json index 2e348d59f24..23c75c464b7 100644 --- a/homeassistant/components/transmission/translations/bg.json +++ b/homeassistant/components/transmission/translations/bg.json @@ -1,13 +1,21 @@ { "config": { "abort": { - "already_configured": "\u0410\u0434\u0440\u0435\u0441\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d." + "already_configured": "\u0410\u0434\u0440\u0435\u0441\u044a\u0442 \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": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 \u0430\u0434\u0440\u0435\u0441\u0430", "name_exists": "\u0418\u043c\u0435\u0442\u043e \u0432\u0435\u0447\u0435 \u0441\u044a\u0449\u0435\u0441\u0442\u0432\u0443\u0432\u0430" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + }, + "description": "\u041f\u0430\u0440\u043e\u043b\u0430\u0442\u0430 \u0437\u0430 {username} \u0435 \u043d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u0430.", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" + }, "user": { "data": { "host": "\u0410\u0434\u0440\u0435\u0441", diff --git a/homeassistant/components/tuya/translations/select.bg.json b/homeassistant/components/tuya/translations/select.bg.json index 4e46bd55033..5f122c09d0c 100644 --- a/homeassistant/components/tuya/translations/select.bg.json +++ b/homeassistant/components/tuya/translations/select.bg.json @@ -48,7 +48,8 @@ "level_9": "\u041d\u0438\u0432\u043e 9" }, "tuya__humidifier_spray_mode": { - "auto": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e" + "auto": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e", + "humidity": "\u0412\u043b\u0430\u0436\u043d\u043e\u0441\u0442" }, "tuya__led_type": { "halogen": "\u0425\u0430\u043b\u043e\u0433\u0435\u043d\u043d\u0438", diff --git a/homeassistant/components/ukraine_alarm/translations/bg.json b/homeassistant/components/ukraine_alarm/translations/bg.json index ed64f99c6b2..874d7b9dd29 100644 --- a/homeassistant/components/ukraine_alarm/translations/bg.json +++ b/homeassistant/components/ukraine_alarm/translations/bg.json @@ -4,6 +4,7 @@ "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", "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "max_regions": "\u041c\u043e\u0433\u0430\u0442 \u0434\u0430 \u0441\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0442 \u043c\u0430\u043a\u0441\u0438\u043c\u0443\u043c 5 \u0440\u0435\u0433\u0438\u043e\u043d\u0430", + "rate_limit": "\u0422\u0432\u044a\u0440\u0434\u0435 \u043c\u043d\u043e\u0433\u043e \u0437\u0430\u044f\u0432\u043a\u0438", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "step": { diff --git a/homeassistant/components/verisure/translations/bg.json b/homeassistant/components/verisure/translations/bg.json index cf602238bf3..f5447e1d865 100644 --- a/homeassistant/components/verisure/translations/bg.json +++ b/homeassistant/components/verisure/translations/bg.json @@ -6,6 +6,18 @@ }, "error": { "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "mfa": { + "data": { + "code": "\u041a\u043e\u0434 \u0437\u0430 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0430" + } + }, + "reauth_mfa": { + "data": { + "code": "\u041a\u043e\u0434 \u0437\u0430 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0430" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/volvooncall/translations/it.json b/homeassistant/components/volvooncall/translations/it.json index dedf72573b6..d96646b7873 100644 --- a/homeassistant/components/volvooncall/translations/it.json +++ b/homeassistant/components/volvooncall/translations/it.json @@ -15,6 +15,7 @@ "password": "Password", "region": "Regione", "scandinavian_miles": "Usa le miglia scandinave", + "unit_system": "Unit\u00e0 di misura", "username": "Nome utente" } } diff --git a/homeassistant/components/volvooncall/translations/no.json b/homeassistant/components/volvooncall/translations/no.json index c1c56e7fe23..2d60c5983fc 100644 --- a/homeassistant/components/volvooncall/translations/no.json +++ b/homeassistant/components/volvooncall/translations/no.json @@ -15,6 +15,7 @@ "password": "Passord", "region": "Region", "scandinavian_miles": "Bruk Skandinaviske Miles", + "unit_system": "Enhetssystem", "username": "Brukernavn" } } diff --git a/homeassistant/components/volvooncall/translations/ru.json b/homeassistant/components/volvooncall/translations/ru.json index 8279196a31d..3a125859e72 100644 --- a/homeassistant/components/volvooncall/translations/ru.json +++ b/homeassistant/components/volvooncall/translations/ru.json @@ -15,6 +15,7 @@ "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "region": "\u041e\u0431\u043b\u0430\u0441\u0442\u044c", "scandinavian_miles": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0441\u043a\u0430\u043d\u0434\u0438\u043d\u0430\u0432\u0441\u043a\u0438\u0435 \u043c\u0438\u043b\u0438", + "unit_system": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u043c\u0435\u0440", "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" } } diff --git a/homeassistant/components/wiz/translations/bg.json b/homeassistant/components/wiz/translations/bg.json index 059a8fb9358..9dabef7c985 100644 --- a/homeassistant/components/wiz/translations/bg.json +++ b/homeassistant/components/wiz/translations/bg.json @@ -2,7 +2,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", - "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", + "no_devices_found": "\u041d\u044f\u043c\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\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/zwave_js/translations/it.json b/homeassistant/components/zwave_js/translations/it.json index eb1e829a52c..34977cdd48c 100644 --- a/homeassistant/components/zwave_js/translations/it.json +++ b/homeassistant/components/zwave_js/translations/it.json @@ -91,6 +91,12 @@ "zwave_js.value_updated.value": "Variazione su un valore Z-Wave JS" } }, + "issues": { + "invalid_server_version": { + "description": "La versione di Z-Wave JS Server attualmente in esecuzione \u00e8 troppo vecchia per questa versione di Home Assistant. Aggiorna Z-Wave JS Server all'ultima versione per risolvere questo problema.", + "title": "\u00c8 necessaria una versione pi\u00f9 recente di Z-Wave JS Server" + } + }, "options": { "abort": { "addon_get_discovery_info_failed": "Impossibile ottenere le informazioni sul rilevamento del componente aggiuntivo Z-Wave JS.", diff --git a/homeassistant/components/zwave_js/translations/no.json b/homeassistant/components/zwave_js/translations/no.json index c89e5582677..6e9ae85cd74 100644 --- a/homeassistant/components/zwave_js/translations/no.json +++ b/homeassistant/components/zwave_js/translations/no.json @@ -91,6 +91,12 @@ "zwave_js.value_updated.value": "Verdiendring p\u00e5 en Z-Wave JS-verdi" } }, + "issues": { + "invalid_server_version": { + "description": "Versjonen av Z-Wave JS Server du kj\u00f8rer er for gammel for denne versjonen av Home Assistant. Oppdater Z-Wave JS Server til den nyeste versjonen for \u00e5 fikse dette problemet.", + "title": "Nyere versjon av Z-Wave JS Server er n\u00f8dvendig" + } + }, "options": { "abort": { "addon_get_discovery_info_failed": "Kunne ikke hente oppdagelsesinformasjon om Z-Wave JS-tillegg", From fc58d887705ed405bc10d0706855d5c25a49c744 Mon Sep 17 00:00:00 2001 From: Thomas Schamm Date: Sat, 24 Sep 2022 02:43:03 +0200 Subject: [PATCH 685/955] Bumped boschshcpy 0.2.30 to 0.2.35 (#79017) Bumped to boschshcpy==0.2.35 --- homeassistant/components/bosch_shc/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bosch_shc/manifest.json b/homeassistant/components/bosch_shc/manifest.json index a5927162e50..df902601e75 100644 --- a/homeassistant/components/bosch_shc/manifest.json +++ b/homeassistant/components/bosch_shc/manifest.json @@ -3,7 +3,7 @@ "name": "Bosch SHC", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/bosch_shc", - "requirements": ["boschshcpy==0.2.30"], + "requirements": ["boschshcpy==0.2.35"], "zeroconf": [{ "type": "_http._tcp.local.", "name": "bosch shc*" }], "iot_class": "local_push", "codeowners": ["@tschamm"], diff --git a/requirements_all.txt b/requirements_all.txt index 8f5bfd7ec5b..d1ebd3a8d8e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -444,7 +444,7 @@ bluetooth-auto-recovery==0.3.3 bond-async==0.1.22 # homeassistant.components.bosch_shc -boschshcpy==0.2.30 +boschshcpy==0.2.35 # homeassistant.components.amazon_polly # homeassistant.components.route53 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f877e9a5890..ea866c985fd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -355,7 +355,7 @@ bluetooth-auto-recovery==0.3.3 bond-async==0.1.22 # homeassistant.components.bosch_shc -boschshcpy==0.2.30 +boschshcpy==0.2.35 # homeassistant.components.broadlink broadlink==0.18.2 From 02731efc4cb3f7ee94b0c08aecc10e3a5209dbf4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 23 Sep 2022 14:45:09 -1000 Subject: [PATCH 686/955] Handle iBeacons that broadcast multiple different uuids (#79011) * Handle iBeacons that broadcast multiple different uuids * fix flip-flopping between uuids * naming --- .../components/ibeacon/coordinator.py | 74 +++++++++++-------- .../components/ibeacon/device_tracker.py | 14 ++-- homeassistant/components/ibeacon/entity.py | 16 ++-- .../components/ibeacon/manifest.json | 2 +- homeassistant/components/ibeacon/sensor.py | 22 +++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/ibeacon/__init__.py | 43 +++++++++++ tests/components/ibeacon/test_sensor.py | 62 ++++++++++++++++ 9 files changed, 180 insertions(+), 57 deletions(-) diff --git a/homeassistant/components/ibeacon/coordinator.py b/homeassistant/components/ibeacon/coordinator.py index 0b813eca933..e915619118c 100644 --- a/homeassistant/components/ibeacon/coordinator.py +++ b/homeassistant/components/ibeacon/coordinator.py @@ -54,7 +54,7 @@ def make_short_address(address: str) -> str: @callback def async_name( service_info: bluetooth.BluetoothServiceInfoBleak, - parsed: iBeaconAdvertisement, + ibeacon_advertisement: iBeaconAdvertisement, unique_address: bool = False, ) -> str: """Return a name for the device.""" @@ -62,7 +62,7 @@ def async_name( service_info.name, service_info.name.replace("_", ":"), ): - base_name = f"{parsed.uuid} {parsed.major}.{parsed.minor}" + base_name = f"{ibeacon_advertisement.uuid} {ibeacon_advertisement.major}.{ibeacon_advertisement.minor}" else: base_name = service_info.name if unique_address: @@ -77,7 +77,7 @@ def _async_dispatch_update( hass: HomeAssistant, device_id: str, service_info: bluetooth.BluetoothServiceInfoBleak, - parsed: iBeaconAdvertisement, + ibeacon_advertisement: iBeaconAdvertisement, new: bool, unique_address: bool, ) -> None: @@ -87,15 +87,15 @@ def _async_dispatch_update( hass, SIGNAL_IBEACON_DEVICE_NEW, device_id, - async_name(service_info, parsed, unique_address), - parsed, + async_name(service_info, ibeacon_advertisement, unique_address), + ibeacon_advertisement, ) return async_dispatcher_send( hass, signal_seen(device_id), - parsed, + ibeacon_advertisement, ) @@ -117,7 +117,9 @@ class IBeaconCoordinator: ) # iBeacons with fixed MAC addresses - self._last_rssi_by_unique_id: dict[str, int] = {} + self._last_ibeacon_advertisement_by_unique_id: dict[ + str, iBeaconAdvertisement + ] = {} self._group_ids_by_address: dict[str, set[str]] = {} self._unique_ids_by_address: dict[str, set[str]] = {} self._unique_ids_by_group_id: dict[str, set[str]] = {} @@ -162,21 +164,23 @@ class IBeaconCoordinator: for unique_id in unique_ids: if device := self._dev_reg.async_get_device({(DOMAIN, unique_id)}): self._dev_reg.async_remove_device(device.id) - self._last_rssi_by_unique_id.pop(unique_id, None) + self._last_ibeacon_advertisement_by_unique_id.pop(unique_id, None) @callback def _async_convert_random_mac_tracking( self, group_id: str, service_info: bluetooth.BluetoothServiceInfoBleak, - parsed: iBeaconAdvertisement, + ibeacon_advertisement: iBeaconAdvertisement, ) -> None: """Switch to random mac tracking method when a group is using rotating mac addresses.""" self._group_ids_random_macs.add(group_id) self._async_purge_untrackable_entities(self._unique_ids_by_group_id[group_id]) self._unique_ids_by_group_id.pop(group_id) self._addresses_by_group_id.pop(group_id) - self._async_update_ibeacon_with_random_mac(group_id, service_info, parsed) + self._async_update_ibeacon_with_random_mac( + group_id, service_info, ibeacon_advertisement + ) def _async_track_ibeacon_with_unique_address( self, address: str, group_id: str, unique_id: str @@ -197,49 +201,55 @@ class IBeaconCoordinator: """Update from a bluetooth callback.""" if service_info.address in self._ignore_addresses: return - if not (parsed := parse(service_info)): + if not (ibeacon_advertisement := parse(service_info)): return - group_id = f"{parsed.uuid}_{parsed.major}_{parsed.minor}" + group_id = f"{ibeacon_advertisement.uuid}_{ibeacon_advertisement.major}_{ibeacon_advertisement.minor}" if group_id in self._group_ids_random_macs: - self._async_update_ibeacon_with_random_mac(group_id, service_info, parsed) + self._async_update_ibeacon_with_random_mac( + group_id, service_info, ibeacon_advertisement + ) return - self._async_update_ibeacon_with_unique_address(group_id, service_info, parsed) + self._async_update_ibeacon_with_unique_address( + group_id, service_info, ibeacon_advertisement + ) @callback def _async_update_ibeacon_with_random_mac( self, group_id: str, service_info: bluetooth.BluetoothServiceInfoBleak, - parsed: iBeaconAdvertisement, + ibeacon_advertisement: iBeaconAdvertisement, ) -> None: """Update iBeacons with random mac addresses.""" new = group_id not in self._last_seen_by_group_id self._last_seen_by_group_id[group_id] = service_info self._unavailable_group_ids.discard(group_id) - _async_dispatch_update(self.hass, group_id, service_info, parsed, new, False) + _async_dispatch_update( + self.hass, group_id, service_info, ibeacon_advertisement, new, False + ) @callback def _async_update_ibeacon_with_unique_address( self, group_id: str, service_info: bluetooth.BluetoothServiceInfoBleak, - parsed: iBeaconAdvertisement, + ibeacon_advertisement: iBeaconAdvertisement, ) -> None: # Handle iBeacon with a fixed mac address # and or detect if the iBeacon is using a rotating mac address # and switch to random mac tracking method address = service_info.address unique_id = f"{group_id}_{address}" - new = unique_id not in self._last_rssi_by_unique_id + new = unique_id not in self._last_ibeacon_advertisement_by_unique_id # Reject creating new trackers if the name is not set if new and ( service_info.device.name is None or service_info.device.name.replace("-", ":") == service_info.device.address ): return - self._last_rssi_by_unique_id[unique_id] = service_info.rssi + self._last_ibeacon_advertisement_by_unique_id[unique_id] = ibeacon_advertisement self._async_track_ibeacon_with_unique_address(address, group_id, unique_id) if address not in self._unavailable_trackers: self._unavailable_trackers[address] = bluetooth.async_track_unavailable( @@ -259,10 +269,14 @@ class IBeaconCoordinator: # group_id we remove all the trackers for that group_id # as it means the addresses are being rotated. if len(self._addresses_by_group_id[group_id]) >= MAX_IDS: - self._async_convert_random_mac_tracking(group_id, service_info, parsed) + self._async_convert_random_mac_tracking( + group_id, service_info, ibeacon_advertisement + ) return - _async_dispatch_update(self.hass, unique_id, service_info, parsed, new, True) + _async_dispatch_update( + self.hass, unique_id, service_info, ibeacon_advertisement, new, True + ) @callback def _async_stop(self) -> None: @@ -294,21 +308,21 @@ class IBeaconCoordinator: here and send them over the dispatcher periodically to ensure the distance calculation is update. """ - for unique_id, rssi in self._last_rssi_by_unique_id.items(): + for ( + unique_id, + ibeacon_advertisement, + ) in self._last_ibeacon_advertisement_by_unique_id.items(): address = unique_id.split("_")[-1] if ( - ( - service_info := bluetooth.async_last_service_info( - self.hass, address, connectable=False - ) + service_info := bluetooth.async_last_service_info( + self.hass, address, connectable=False ) - and service_info.rssi != rssi - and (parsed := parse(service_info)) - ): + ) and service_info.rssi != ibeacon_advertisement.rssi: + ibeacon_advertisement.update_rssi(service_info.rssi) async_dispatcher_send( self.hass, signal_seen(unique_id), - parsed, + ibeacon_advertisement, ) @callback diff --git a/homeassistant/components/ibeacon/device_tracker.py b/homeassistant/components/ibeacon/device_tracker.py index e2db3bd291f..4c9337e54ce 100644 --- a/homeassistant/components/ibeacon/device_tracker.py +++ b/homeassistant/components/ibeacon/device_tracker.py @@ -26,7 +26,7 @@ async def async_setup_entry( def _async_device_new( unique_id: str, identifier: str, - parsed: iBeaconAdvertisement, + ibeacon_advertisement: iBeaconAdvertisement, ) -> None: """Signal a new device.""" async_add_entities( @@ -35,7 +35,7 @@ async def async_setup_entry( coordinator, identifier, unique_id, - parsed, + ibeacon_advertisement, ) ] ) @@ -53,10 +53,12 @@ class IBeaconTrackerEntity(IBeaconEntity, BaseTrackerEntity): coordinator: IBeaconCoordinator, identifier: str, device_unique_id: str, - parsed: iBeaconAdvertisement, + ibeacon_advertisement: iBeaconAdvertisement, ) -> None: """Initialize an iBeacon tracker entity.""" - super().__init__(coordinator, identifier, device_unique_id, parsed) + super().__init__( + coordinator, identifier, device_unique_id, ibeacon_advertisement + ) self._attr_unique_id = device_unique_id self._active = True @@ -78,11 +80,11 @@ class IBeaconTrackerEntity(IBeaconEntity, BaseTrackerEntity): @callback def _async_seen( self, - parsed: iBeaconAdvertisement, + ibeacon_advertisement: iBeaconAdvertisement, ) -> None: """Update state.""" self._active = True - self._parsed = parsed + self._ibeacon_advertisement = ibeacon_advertisement self.async_write_ha_state() @callback diff --git a/homeassistant/components/ibeacon/entity.py b/homeassistant/components/ibeacon/entity.py index 3ce64fb8535..4baa06dd617 100644 --- a/homeassistant/components/ibeacon/entity.py +++ b/homeassistant/components/ibeacon/entity.py @@ -24,12 +24,12 @@ class IBeaconEntity(Entity): coordinator: IBeaconCoordinator, identifier: str, device_unique_id: str, - parsed: iBeaconAdvertisement, + ibeacon_advertisement: iBeaconAdvertisement, ) -> None: """Initialize an iBeacon sensor entity.""" self._device_unique_id = device_unique_id self._coordinator = coordinator - self._parsed = parsed + self._ibeacon_advertisement = ibeacon_advertisement self._attr_device_info = DeviceInfo( name=identifier, identifiers={(DOMAIN, device_unique_id)}, @@ -40,19 +40,19 @@ class IBeaconEntity(Entity): self, ) -> dict[str, str | int]: """Return the device state attributes.""" - parsed = self._parsed + ibeacon_advertisement = self._ibeacon_advertisement return { - ATTR_UUID: str(parsed.uuid), - ATTR_MAJOR: parsed.major, - ATTR_MINOR: parsed.minor, - ATTR_SOURCE: parsed.source, + ATTR_UUID: str(ibeacon_advertisement.uuid), + ATTR_MAJOR: ibeacon_advertisement.major, + ATTR_MINOR: ibeacon_advertisement.minor, + ATTR_SOURCE: ibeacon_advertisement.source, } @abstractmethod @callback def _async_seen( self, - parsed: iBeaconAdvertisement, + ibeacon_advertisement: iBeaconAdvertisement, ) -> None: """Update state.""" diff --git a/homeassistant/components/ibeacon/manifest.json b/homeassistant/components/ibeacon/manifest.json index 531daed00f8..273afdaa07f 100644 --- a/homeassistant/components/ibeacon/manifest.json +++ b/homeassistant/components/ibeacon/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/ibeacon", "dependencies": ["bluetooth"], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [2, 21] }], - "requirements": ["ibeacon_ble==0.6.4"], + "requirements": ["ibeacon_ble==0.7.0"], "codeowners": ["@bdraco"], "iot_class": "local_push", "loggers": ["bleak"], diff --git a/homeassistant/components/ibeacon/sensor.py b/homeassistant/components/ibeacon/sensor.py index d3468fbc3dc..36a7917a9e6 100644 --- a/homeassistant/components/ibeacon/sensor.py +++ b/homeassistant/components/ibeacon/sensor.py @@ -42,7 +42,7 @@ SENSOR_DESCRIPTIONS = ( device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, entity_registry_enabled_default=False, - value_fn=lambda parsed: parsed.rssi, + value_fn=lambda ibeacon_advertisement: ibeacon_advertisement.rssi, state_class=SensorStateClass.MEASUREMENT, ), IBeaconSensorEntityDescription( @@ -51,7 +51,7 @@ SENSOR_DESCRIPTIONS = ( device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, entity_registry_enabled_default=False, - value_fn=lambda parsed: parsed.power, + value_fn=lambda ibeacon_advertisement: ibeacon_advertisement.power, state_class=SensorStateClass.MEASUREMENT, ), IBeaconSensorEntityDescription( @@ -59,7 +59,7 @@ SENSOR_DESCRIPTIONS = ( name="Estimated Distance", icon="mdi:signal-distance-variant", native_unit_of_measurement=LENGTH_METERS, - value_fn=lambda parsed: parsed.distance, + value_fn=lambda ibeacon_advertisement: ibeacon_advertisement.distance, state_class=SensorStateClass.MEASUREMENT, ), ) @@ -75,7 +75,7 @@ async def async_setup_entry( def _async_device_new( unique_id: str, identifier: str, - parsed: iBeaconAdvertisement, + ibeacon_advertisement: iBeaconAdvertisement, ) -> None: """Signal a new device.""" async_add_entities( @@ -84,7 +84,7 @@ async def async_setup_entry( description, identifier, unique_id, - parsed, + ibeacon_advertisement, ) for description in SENSOR_DESCRIPTIONS ) @@ -105,21 +105,23 @@ class IBeaconSensorEntity(IBeaconEntity, SensorEntity): description: IBeaconSensorEntityDescription, identifier: str, device_unique_id: str, - parsed: iBeaconAdvertisement, + ibeacon_advertisement: iBeaconAdvertisement, ) -> None: """Initialize an iBeacon sensor entity.""" - super().__init__(coordinator, identifier, device_unique_id, parsed) + super().__init__( + coordinator, identifier, device_unique_id, ibeacon_advertisement + ) self._attr_unique_id = f"{device_unique_id}_{description.key}" self.entity_description = description @callback def _async_seen( self, - parsed: iBeaconAdvertisement, + ibeacon_advertisement: iBeaconAdvertisement, ) -> None: """Update state.""" self._attr_available = True - self._parsed = parsed + self._ibeacon_advertisement = ibeacon_advertisement self.async_write_ha_state() @callback @@ -131,4 +133,4 @@ class IBeaconSensorEntity(IBeaconEntity, SensorEntity): @property def native_value(self) -> int | None: """Return the state of the sensor.""" - return self.entity_description.value_fn(self._parsed) + return self.entity_description.value_fn(self._ibeacon_advertisement) diff --git a/requirements_all.txt b/requirements_all.txt index d1ebd3a8d8e..6b05a4d21f8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -898,7 +898,7 @@ iammeter==0.1.7 iaqualink==0.4.1 # homeassistant.components.ibeacon -ibeacon_ble==0.6.4 +ibeacon_ble==0.7.0 # homeassistant.components.watson_tts ibm-watson==5.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ea866c985fd..679756d3400 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -663,7 +663,7 @@ hyperion-py==0.7.5 iaqualink==0.4.1 # homeassistant.components.ibeacon -ibeacon_ble==0.6.4 +ibeacon_ble==0.7.0 # homeassistant.components.ping icmplib==3.0 diff --git a/tests/components/ibeacon/__init__.py b/tests/components/ibeacon/__init__.py index f1b8928f67b..f10bc65ed33 100644 --- a/tests/components/ibeacon/__init__.py +++ b/tests/components/ibeacon/__init__.py @@ -58,3 +58,46 @@ BEACON_RANDOM_ADDRESS_SERVICE_INFO = BluetoothServiceInfo( service_uuids=[], source="local", ) + + +FEASY_BEACON_BLE_DEVICE = BLEDevice( + address="AA:BB:CC:DD:EE:FF", + name="FSC-BP108", +) + +FEASY_BEACON_SERVICE_INFO_1 = BluetoothServiceInfo( + name="FSC-BP108", + address="AA:BB:CC:DD:EE:FF", + rssi=-63, + manufacturer_data={ + 76: b"\x02\x15\xfd\xa5\x06\x93\xa4\xe2O\xb1\xaf\xcf\xc6\xeb\x07dx%'Qe\xc1\xfd" + }, + service_data={ + "0000feaa-0000-1000-8000-00805f9b34fb": b' \x00\x0c\x86\x80\x00\x00\x00\x93f\x0b\x7f\x93"', + "0000fff0-0000-1000-8000-00805f9b34fb": b"'\x02\x17\x92\xdc\r0\x0e \xbad", + }, + service_uuids=[ + "0000feaa-0000-1000-8000-00805f9b34fb", + "0000fef5-0000-1000-8000-00805f9b34fb", + ], + source="local", +) + + +FEASY_BEACON_SERVICE_INFO_2 = BluetoothServiceInfo( + name="FSC-BP108", + address="AA:BB:CC:DD:EE:FF", + rssi=-63, + manufacturer_data={ + 76: b"\x02\x15\xd5F\xdf\x97GWG\xef\xbe\t>-\xcb\xdd\x0cw\xed\xd1;\xd2\xb5" + }, + service_data={ + "0000feaa-0000-1000-8000-00805f9b34fb": b' \x00\x0c\x86\x80\x00\x00\x00\x93f\x0b\x7f\x93"', + "0000fff0-0000-1000-8000-00805f9b34fb": b"'\x02\x17\x92\xdc\r0\x0e \xbad", + }, + service_uuids=[ + "0000feaa-0000-1000-8000-00805f9b34fb", + "0000fef5-0000-1000-8000-00805f9b34fb", + ], + source="local", +) diff --git a/tests/components/ibeacon/test_sensor.py b/tests/components/ibeacon/test_sensor.py index 38c03b0be5d..671172efe93 100644 --- a/tests/components/ibeacon/test_sensor.py +++ b/tests/components/ibeacon/test_sensor.py @@ -20,6 +20,9 @@ from . import ( BLUECHARM_BEACON_SERVICE_INFO, BLUECHARM_BEACON_SERVICE_INFO_2, BLUECHARM_BLE_DEVICE, + FEASY_BEACON_BLE_DEVICE, + FEASY_BEACON_SERVICE_INFO_1, + FEASY_BEACON_SERVICE_INFO_2, NO_NAME_BEACON_SERVICE_INFO, ) @@ -182,3 +185,62 @@ async def test_can_unload_and_reload(hass): assert ( hass.states.get("sensor.bluecharm_177999_8105_estimated_distance").state == "2" ) + + +async def test_multiple_uuids_same_beacon(hass): + """Test a beacon that broadcasts multiple uuids.""" + entry = MockConfigEntry( + domain=DOMAIN, + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + with patch_all_discovered_devices([FEASY_BEACON_BLE_DEVICE]): + inject_bluetooth_service_info(hass, FEASY_BEACON_SERVICE_INFO_1) + await hass.async_block_till_done() + + distance_sensor = hass.states.get("sensor.fsc_bp108_eeff_estimated_distance") + distance_attributes = distance_sensor.attributes + assert distance_sensor.state == "400" + assert ( + distance_attributes[ATTR_FRIENDLY_NAME] == "FSC-BP108 EEFF Estimated Distance" + ) + assert distance_attributes[ATTR_UNIT_OF_MEASUREMENT] == "m" + assert distance_attributes[ATTR_STATE_CLASS] == "measurement" + + with patch_all_discovered_devices([FEASY_BEACON_BLE_DEVICE]): + inject_bluetooth_service_info(hass, FEASY_BEACON_SERVICE_INFO_2) + await hass.async_block_till_done() + + distance_sensor = hass.states.get("sensor.fsc_bp108_eeff_estimated_distance_2") + distance_attributes = distance_sensor.attributes + assert distance_sensor.state == "0" + assert ( + distance_attributes[ATTR_FRIENDLY_NAME] == "FSC-BP108 EEFF Estimated Distance" + ) + assert distance_attributes[ATTR_UNIT_OF_MEASUREMENT] == "m" + assert distance_attributes[ATTR_STATE_CLASS] == "measurement" + + with patch_all_discovered_devices([FEASY_BEACON_BLE_DEVICE]): + inject_bluetooth_service_info(hass, FEASY_BEACON_SERVICE_INFO_1) + await hass.async_block_till_done() + + distance_sensor = hass.states.get("sensor.fsc_bp108_eeff_estimated_distance") + distance_attributes = distance_sensor.attributes + assert distance_sensor.state == "400" + assert ( + distance_attributes[ATTR_FRIENDLY_NAME] == "FSC-BP108 EEFF Estimated Distance" + ) + assert distance_attributes[ATTR_UNIT_OF_MEASUREMENT] == "m" + assert distance_attributes[ATTR_STATE_CLASS] == "measurement" + + distance_sensor = hass.states.get("sensor.fsc_bp108_eeff_estimated_distance_2") + distance_attributes = distance_sensor.attributes + assert distance_sensor.state == "0" + assert ( + distance_attributes[ATTR_FRIENDLY_NAME] == "FSC-BP108 EEFF Estimated Distance" + ) + assert distance_attributes[ATTR_UNIT_OF_MEASUREMENT] == "m" + assert distance_attributes[ATTR_STATE_CLASS] == "measurement" From 1b144c0e4dd683e3b47668a89da5eb6da4ae5e08 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 23 Sep 2022 15:09:28 -1000 Subject: [PATCH 687/955] Update to bleak 0.18.0 (#79008) --- .../components/bluetooth/__init__.py | 4 + homeassistant/components/bluetooth/const.py | 3 + homeassistant/components/bluetooth/manager.py | 3 +- .../components/bluetooth/manifest.json | 6 +- homeassistant/components/bluetooth/models.py | 149 ++++++++++-- homeassistant/components/bluetooth/usage.py | 2 +- homeassistant/package_constraints.txt | 6 +- requirements_all.txt | 6 +- requirements_test_all.txt | 6 +- tests/components/bluetooth/conftest.py | 2 +- tests/components/bluetooth/test_models.py | 212 ++++++++++++++++++ tests/components/bluetooth/test_usage.py | 4 - 12 files changed, 371 insertions(+), 32 deletions(-) create mode 100644 tests/components/bluetooth/test_models.py diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 24eab5c9a5c..2afb638b230 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -52,6 +52,7 @@ from .models import ( BluetoothServiceInfo, BluetoothServiceInfoBleak, HaBleakScannerWrapper, + HaBluetoothConnector, ProcessAdvertisementCallback, ) from .scanner import HaScanner, ScannerStartError, create_bleak_scanner @@ -66,9 +67,11 @@ __all__ = [ "async_ble_device_from_address", "async_discovered_service_info", "async_get_scanner", + "async_last_service_info", "async_process_advertisements", "async_rediscover_address", "async_register_callback", + "async_register_scanner", "async_track_unavailable", "async_scanner_count", "BaseHaScanner", @@ -76,6 +79,7 @@ __all__ = [ "BluetoothServiceInfoBleak", "BluetoothScanningMode", "BluetoothCallback", + "HaBluetoothConnector", "SOURCE_LOCAL", ] diff --git a/homeassistant/components/bluetooth/const.py b/homeassistant/components/bluetooth/const.py index 891e6d8be82..4d4a096bb66 100644 --- a/homeassistant/components/bluetooth/const.py +++ b/homeassistant/components/bluetooth/const.py @@ -66,3 +66,6 @@ ADAPTER_ADDRESS: Final = "address" ADAPTER_SW_VERSION: Final = "sw_version" ADAPTER_HW_VERSION: Final = "hw_version" ADAPTER_PASSIVE_SCAN: Final = "passive_scan" + + +NO_RSSI_VALUE: Final = -1000 diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index 06eb71b5a71..a7d1e141b3d 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -24,6 +24,7 @@ from homeassistant.helpers.event import async_track_time_interval from .const import ( ADAPTER_ADDRESS, ADAPTER_PASSIVE_SCAN, + NO_RSSI_VALUE, STALE_ADVERTISEMENT_SECONDS, UNAVAILABLE_TRACK_SECONDS, AdapterDetails, @@ -65,7 +66,6 @@ APPLE_START_BYTES_WANTED: Final = { } RSSI_SWITCH_THRESHOLD = 6 -NO_RSSI_VALUE = -1000 _LOGGER = logging.getLogger(__name__) @@ -340,6 +340,7 @@ class BluetoothManager: service_info.manufacturer_data != old_service_info.manufacturer_data or service_info.service_data != old_service_info.service_data or service_info.service_uuids != old_service_info.service_uuids + or service_info.name != old_service_info.name ): return diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 7a7cfbae007..dc4e1f05656 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -6,11 +6,11 @@ "after_dependencies": ["hassio"], "quality_scale": "internal", "requirements": [ - "bleak==0.17.0", - "bleak-retry-connector==1.17.1", + "bleak==0.18.0", + "bleak-retry-connector==1.17.3", "bluetooth-adapters==0.5.1", "bluetooth-auto-recovery==0.3.3", - "dbus-fast==1.5.1" + "dbus-fast==1.7.0" ], "codeowners": ["@bdraco"], "config_flow": true, diff --git a/homeassistant/components/bluetooth/models.py b/homeassistant/components/bluetooth/models.py index 6c70633f597..100d9f69c03 100644 --- a/homeassistant/components/bluetooth/models.py +++ b/homeassistant/components/bluetooth/models.py @@ -11,17 +11,21 @@ import logging from typing import TYPE_CHECKING, Any, Final from bleak import BleakClient, BleakError +from bleak.backends.client import BaseBleakClient, get_platform_client_backend_type from bleak.backends.device import BLEDevice from bleak.backends.scanner import ( AdvertisementData, AdvertisementDataCallback, BaseBleakScanner, ) +from bleak_retry_connector import freshen_ble_device -from homeassistant.core import CALLBACK_TYPE +from homeassistant.core import CALLBACK_TYPE, callback as hass_callback from homeassistant.helpers.frame import report from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo +from .const import NO_RSSI_VALUE + if TYPE_CHECKING: from .manager import BluetoothManager @@ -62,6 +66,23 @@ BluetoothCallback = Callable[[BluetoothServiceInfoBleak, BluetoothChange], None] ProcessAdvertisementCallback = Callable[[BluetoothServiceInfoBleak], bool] +@dataclass +class HaBluetoothConnector: + """Data for how to connect a BLEDevice from a given scanner.""" + + client: type[BaseBleakClient] + source: str + can_connect: Callable[[], bool] + + +@dataclass +class _HaWrappedBleakBackend: + """Wrap bleak backend to make it usable by Home Assistant.""" + + device: BLEDevice + client: type[BaseBleakClient] + + class BaseHaScanner: """Base class for Ha Scanners.""" @@ -109,6 +130,12 @@ class HaBleakScannerWrapper(BaseBleakScanner): detection_callback=detection_callback, service_uuids=service_uuids or [] ) + @classmethod + async def discover(cls, timeout: float = 5.0, **kwargs: Any) -> list[BLEDevice]: + """Discover devices.""" + assert MANAGER is not None + return list(MANAGER.async_discovered_devices(True)) + async def stop(self, *args: Any, **kwargs: Any) -> None: """Stop scanning for devices.""" @@ -189,20 +216,116 @@ class HaBleakClientWrapper(BleakClient): when an integration does this. """ - def __init__( - self, address_or_ble_device: str | BLEDevice, *args: Any, **kwargs: Any + def __init__( # pylint: disable=super-init-not-called, keyword-arg-before-vararg + self, + address_or_ble_device: str | BLEDevice, + disconnected_callback: Callable[[BleakClient], None] | None = None, + *args: Any, + timeout: float = 10.0, + **kwargs: Any, ) -> None: """Initialize the BleakClient.""" if isinstance(address_or_ble_device, BLEDevice): - super().__init__(address_or_ble_device, *args, **kwargs) - return - report( - "attempted to call BleakClient with an address instead of a BLEDevice", - exclude_integrations={"bluetooth"}, - error_if_core=False, - ) + self.__address = address_or_ble_device.address + else: + report( + "attempted to call BleakClient with an address instead of a BLEDevice", + exclude_integrations={"bluetooth"}, + error_if_core=False, + ) + self.__address = address_or_ble_device + self.__disconnected_callback = disconnected_callback + self.__timeout = timeout + self._backend: BaseBleakClient | None = None # type: ignore[assignment] + + @property + def is_connected(self) -> bool: + """Return True if the client is connected to a device.""" + return self._backend is not None and self._backend.is_connected + + def set_disconnected_callback( + self, + callback: Callable[[BleakClient], None] | None, + **kwargs: Any, + ) -> None: + """Set the disconnect callback.""" + self.__disconnected_callback = callback + if self._backend: + self._backend.set_disconnected_callback(callback, **kwargs) # type: ignore[arg-type] + + async def connect(self, **kwargs: Any) -> bool: + """Connect to the specified GATT server.""" + if not self._backend: + wrapped_backend = self._async_get_backend() + self._backend = wrapped_backend.client( + await freshen_ble_device(wrapped_backend.device) + or wrapped_backend.device, + disconnected_callback=self.__disconnected_callback, + timeout=self.__timeout, + ) + return await super().connect(**kwargs) + + @hass_callback + def _async_get_backend_for_ble_device( + self, ble_device: BLEDevice + ) -> _HaWrappedBleakBackend | None: + """Get the backend for a BLEDevice.""" + details = ble_device.details + if not isinstance(details, dict) or "connector" not in details: + # If client is not defined in details + # its the client for this platform + cls = get_platform_client_backend_type() + return _HaWrappedBleakBackend(ble_device, cls) + + connector: HaBluetoothConnector = details["connector"] + # Make sure the backend can connect to the device + # as some backends have connection limits + if not connector.can_connect(): + return None + + return _HaWrappedBleakBackend(ble_device, connector.client) + + @hass_callback + def _async_get_backend(self) -> _HaWrappedBleakBackend: + """Get the bleak backend for the given address.""" assert MANAGER is not None - ble_device = MANAGER.async_ble_device_from_address(address_or_ble_device, True) + address = self.__address + ble_device = MANAGER.async_ble_device_from_address(address, True) if ble_device is None: - raise BleakError(f"No device found for address {address_or_ble_device}") - super().__init__(ble_device, *args, **kwargs) + raise BleakError(f"No device found for address {address}") + + if backend := self._async_get_backend_for_ble_device(ble_device): + return backend + + # + # The preferred backend cannot currently connect the device + # because it is likely out of connection slots. + # + # We need to try all backends to find one that can + # connect to the device. + # + # Currently we have to search all the discovered devices + # because the bleak API does not allow us to get the + # details for a specific device. + # + for ble_device in sorted( + ( + ble_device + for ble_device in MANAGER.async_all_discovered_devices(True) + if ble_device.address == address + ), + key=lambda ble_device: ble_device.rssi or NO_RSSI_VALUE, + reverse=True, + ): + if backend := self._async_get_backend_for_ble_device(ble_device): + return backend + + raise BleakError( + f"No backend with an available connection slot that can reach address {address} was found" + ) + + async def disconnect(self) -> bool: + """Disconnect from the device.""" + if self._backend is None: + return True + return await self._backend.disconnect() diff --git a/homeassistant/components/bluetooth/usage.py b/homeassistant/components/bluetooth/usage.py index d282ca7415b..23388c84302 100644 --- a/homeassistant/components/bluetooth/usage.py +++ b/homeassistant/components/bluetooth/usage.py @@ -13,7 +13,7 @@ ORIGINAL_BLEAK_CLIENT = bleak.BleakClient def install_multiple_bleak_catcher() -> None: """Wrap the bleak classes to return the shared instance if multiple instances are detected.""" bleak.BleakScanner = HaBleakScannerWrapper # type: ignore[misc, assignment] - bleak.BleakClient = HaBleakClientWrapper # type: ignore[misc] + bleak.BleakClient = HaBleakClientWrapper # type: ignore[misc, assignment] def uninstall_multiple_bleak_catcher() -> None: diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 48eb026d30b..dcf8f415c3b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,14 +10,14 @@ atomicwrites-homeassistant==1.4.1 attrs==21.2.0 awesomeversion==22.9.0 bcrypt==3.1.7 -bleak-retry-connector==1.17.1 -bleak==0.17.0 +bleak-retry-connector==1.17.3 +bleak==0.18.0 bluetooth-adapters==0.5.1 bluetooth-auto-recovery==0.3.3 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==37.0.4 -dbus-fast==1.5.1 +dbus-fast==1.7.0 fnvhash==0.1.0 hass-nabucasa==0.55.0 home-assistant-bluetooth==1.3.0 diff --git a/requirements_all.txt b/requirements_all.txt index 6b05a4d21f8..f3cb09602d6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -410,10 +410,10 @@ bimmer_connected==0.10.4 bizkaibus==0.1.1 # homeassistant.components.bluetooth -bleak-retry-connector==1.17.1 +bleak-retry-connector==1.17.3 # homeassistant.components.bluetooth -bleak==0.17.0 +bleak==0.18.0 # homeassistant.components.blebox blebox_uniapi==2.0.2 @@ -540,7 +540,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.5.1 +dbus-fast==1.7.0 # homeassistant.components.debugpy debugpy==1.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 679756d3400..881de4a5650 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -331,10 +331,10 @@ bellows==0.33.1 bimmer_connected==0.10.4 # homeassistant.components.bluetooth -bleak-retry-connector==1.17.1 +bleak-retry-connector==1.17.3 # homeassistant.components.bluetooth -bleak==0.17.0 +bleak==0.18.0 # homeassistant.components.blebox blebox_uniapi==2.0.2 @@ -417,7 +417,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.5.1 +dbus-fast==1.7.0 # homeassistant.components.debugpy debugpy==1.6.3 diff --git a/tests/components/bluetooth/conftest.py b/tests/components/bluetooth/conftest.py index 4c78f063780..3d29b4cbbe1 100644 --- a/tests/components/bluetooth/conftest.py +++ b/tests/components/bluetooth/conftest.py @@ -56,7 +56,7 @@ def bluez_dbus_mock(): @pytest.fixture(name="macos_adapter") def macos_adapter(): """Fixture that mocks the macos adapter.""" - with patch( + with patch("bleak.get_platform_scanner_backend_type"), patch( "homeassistant.components.bluetooth.platform.system", return_value="Darwin" ), patch( "homeassistant.components.bluetooth.scanner.platform.system", diff --git a/tests/components/bluetooth/test_models.py b/tests/components/bluetooth/test_models.py new file mode 100644 index 00000000000..2321a64e1e3 --- /dev/null +++ b/tests/components/bluetooth/test_models.py @@ -0,0 +1,212 @@ +"""Tests for the Bluetooth integration models.""" + +from unittest.mock import patch + +import bleak +from bleak import BleakClient, BleakError +from bleak.backends.device import BLEDevice +from bleak.backends.scanner import AdvertisementData +import pytest + +from homeassistant.components.bluetooth.models import ( + BaseHaScanner, + HaBleakClientWrapper, + HaBleakScannerWrapper, + HaBluetoothConnector, +) + +from . import _get_manager, inject_advertisement, inject_advertisement_with_source + + +class MockBleakClient(BleakClient): + """Mock bleak client.""" + + def __init__(self, *args, **kwargs): + """Mock init.""" + super().__init__(*args, **kwargs) + self._device_path = "/dev/test" + + @property + def is_connected(self) -> bool: + """Mock connected.""" + return True + + async def connect(self, *args, **kwargs): + """Mock connect.""" + return True + + async def disconnect(self, *args, **kwargs): + """Mock disconnect.""" + pass + + async def get_services(self, *args, **kwargs): + """Mock get_services.""" + return [] + + +async def test_wrapped_bleak_scanner(hass, enable_bluetooth): + """Test wrapped bleak scanner dispatches calls as expected.""" + scanner = HaBleakScannerWrapper() + switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") + switchbot_adv = AdvertisementData( + local_name="wohand", service_uuids=[], manufacturer_data={1: b"\x01"} + ) + inject_advertisement(hass, switchbot_device, switchbot_adv) + assert scanner.discovered_devices == [switchbot_device] + assert await scanner.discover() == [switchbot_device] + + +async def test_wrapped_bleak_client_raises_device_missing(hass, enable_bluetooth): + """Test wrapped bleak client dispatches calls as expected.""" + switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") + client = HaBleakClientWrapper(switchbot_device) + assert client.is_connected is False + with pytest.raises(bleak.BleakError): + await client.connect() + assert client.is_connected is False + await client.disconnect() + + +async def test_wrapped_bleak_client_set_disconnected_callback_before_connected( + hass, enable_bluetooth +): + """Test wrapped bleak client can set a disconnected callback before connected.""" + switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") + client = HaBleakClientWrapper(switchbot_device) + client.set_disconnected_callback(lambda client: None) + + +async def test_wrapped_bleak_client_set_disconnected_callback_after_connected( + hass, enable_bluetooth, one_adapter +): + """Test wrapped bleak client can set a disconnected callback after connected.""" + switchbot_device = BLEDevice( + "44:44:33:11:23:45", "wohand", {"path": "/org/bluez/hci0/dev_44_44_33_11_23_45"} + ) + switchbot_adv = AdvertisementData( + local_name="wohand", service_uuids=[], manufacturer_data={1: b"\x01"} + ) + inject_advertisement(hass, switchbot_device, switchbot_adv) + client = HaBleakClientWrapper(switchbot_device) + with patch( + "bleak.backends.bluezdbus.client.BleakClientBlueZDBus.connect" + ) as connect: + await client.connect() + assert len(connect.mock_calls) == 1 + assert client._backend is not None + client.set_disconnected_callback(lambda client: None) + await client.disconnect() + + +async def test_ble_device_with_proxy_client_out_of_connections( + hass, enable_bluetooth, one_adapter +): + """Test we switch to the next available proxy when one runs out of connections.""" + manager = _get_manager() + + switchbot_proxy_device_no_connection_slot = BLEDevice( + "44:44:33:11:23:45", + "wohand", + { + "connector": HaBluetoothConnector( + MockBleakClient, "mock_bleak_client", lambda: False + ), + "path": "/org/bluez/hci0/dev_44_44_33_11_23_45", + }, + rssi=-30, + ) + switchbot_adv = AdvertisementData( + local_name="wohand", service_uuids=[], manufacturer_data={1: b"\x01"} + ) + + inject_advertisement_with_source( + hass, switchbot_proxy_device_no_connection_slot, switchbot_adv, "esp32" + ) + + assert manager.async_discovered_devices(True) == [ + switchbot_proxy_device_no_connection_slot + ] + + client = HaBleakClientWrapper(switchbot_proxy_device_no_connection_slot) + with patch( + "bleak.backends.bluezdbus.client.BleakClientBlueZDBus.connect" + ), pytest.raises(BleakError): + await client.connect() + assert client.is_connected is False + client.set_disconnected_callback(lambda client: None) + await client.disconnect() + + +async def test_ble_device_with_proxy_client_out_of_connections_uses_best_available( + hass, enable_bluetooth, one_adapter +): + """Test we switch to the next available proxy when one runs out of connections.""" + manager = _get_manager() + + switchbot_proxy_device_no_connection_slot = BLEDevice( + "44:44:33:11:23:45", + "wohand", + { + "connector": HaBluetoothConnector( + MockBleakClient, "mock_bleak_client", lambda: False + ), + "path": "/org/bluez/hci0/dev_44_44_33_11_23_45", + }, + rssi=-30, + ) + switchbot_proxy_device_has_connection_slot = BLEDevice( + "44:44:33:11:23:45", + "wohand", + { + "connector": HaBluetoothConnector( + MockBleakClient, "mock_bleak_client", lambda: True + ), + "path": "/org/bluez/hci0/dev_44_44_33_11_23_45", + }, + rssi=-40, + ) + switchbot_device = BLEDevice( + "44:44:33:11:23:45", + "wohand", + {"path": "/org/bluez/hci0/dev_44_44_33_11_23_45"}, + rssi=-100, + ) + switchbot_adv = AdvertisementData( + local_name="wohand", service_uuids=[], manufacturer_data={1: b"\x01"} + ) + + inject_advertisement_with_source( + hass, switchbot_device, switchbot_adv, "00:00:00:00:00:01" + ) + inject_advertisement_with_source( + hass, + switchbot_proxy_device_has_connection_slot, + switchbot_adv, + "esp32_has_connection_slot", + ) + inject_advertisement_with_source( + hass, + switchbot_proxy_device_no_connection_slot, + switchbot_adv, + "esp32_no_connection_slot", + ) + + class FakeScanner(BaseHaScanner): + @property + def discovered_devices(self) -> list[BLEDevice]: + """Return a list of discovered devices.""" + return [switchbot_proxy_device_has_connection_slot] + + scanner = FakeScanner() + cancel = manager.async_register_scanner(scanner, True) + assert manager.async_discovered_devices(True) == [ + switchbot_proxy_device_no_connection_slot + ] + + client = HaBleakClientWrapper(switchbot_proxy_device_no_connection_slot) + with patch("bleak.backends.bluezdbus.client.BleakClientBlueZDBus.connect"): + await client.connect() + assert client.is_connected is True + client.set_disconnected_callback(lambda client: None) + await client.disconnect() + cancel() diff --git a/tests/components/bluetooth/test_usage.py b/tests/components/bluetooth/test_usage.py index 3e35547d2f2..2c6bacfd4cb 100644 --- a/tests/components/bluetooth/test_usage.py +++ b/tests/components/bluetooth/test_usage.py @@ -5,7 +5,6 @@ from unittest.mock import patch import bleak from bleak.backends.device import BLEDevice -import pytest from homeassistant.components.bluetooth.models import ( HaBleakClientWrapper, @@ -57,9 +56,6 @@ async def test_bleak_client_reports_with_address(hass, enable_bluetooth, caplog) """Test we report when we pass an address to BleakClient.""" install_multiple_bleak_catcher() - with pytest.raises(bleak.BleakError): - instance = bleak.BleakClient("00:00:00:00:00:00") - with patch.object( _get_manager(), "async_ble_device_from_address", From 57746642349a3ca62959de4447a4eb5963a84ae1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 24 Sep 2022 03:58:01 -0400 Subject: [PATCH 688/955] Clean up Speech-to-text integration and add tests (#79012) --- homeassistant/components/stt/__init__.py | 116 +++++++++++------ tests/components/stt/test_init.py | 153 ++++++++++++++++++++--- 2 files changed, 215 insertions(+), 54 deletions(-) diff --git a/homeassistant/components/stt/__init__.py b/homeassistant/components/stt/__init__.py index 94acf155968..1d68b0a954b 100644 --- a/homeassistant/components/stt/__init__.py +++ b/homeassistant/components/stt/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations from abc import ABC, abstractmethod import asyncio +from dataclasses import asdict, dataclass import logging from typing import Any @@ -13,7 +14,6 @@ from aiohttp.web_exceptions import ( HTTPNotFound, HTTPUnsupportedMediaType, ) -import attr from homeassistant.components.http import HomeAssistantView from homeassistant.core import HomeAssistant, callback @@ -34,9 +34,18 @@ from .const import ( _LOGGER = logging.getLogger(__name__) +@callback +def async_get_provider(hass: HomeAssistant, domain: str | None = None) -> Provider: + """Return provider.""" + if domain is None: + domain = next(iter(hass.data[DOMAIN])) + + return hass.data[DOMAIN][domain] + + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up STT.""" - providers = {} + providers = hass.data[DOMAIN] = {} async def async_setup_platform(p_type, p_config=None, discovery_info=None): """Set up a TTS platform.""" @@ -80,24 +89,30 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -@attr.s +@dataclass class SpeechMetadata: """Metadata of audio stream.""" - language: str = attr.ib() - format: AudioFormats = attr.ib() - codec: AudioCodecs = attr.ib() - bit_rate: AudioBitRates = attr.ib(converter=int) - sample_rate: AudioSampleRates = attr.ib(converter=int) - channel: AudioChannels = attr.ib(converter=int) + language: str + format: AudioFormats + codec: AudioCodecs + bit_rate: AudioBitRates + sample_rate: AudioSampleRates + channel: AudioChannels + + def __post_init__(self) -> None: + """Finish initializing the metadata.""" + self.bit_rate = AudioBitRates(int(self.bit_rate)) + self.sample_rate = AudioSampleRates(int(self.sample_rate)) + self.channel = AudioChannels(int(self.channel)) -@attr.s +@dataclass class SpeechResult: """Result of audio Speech.""" - text: str | None = attr.ib() - result: SpeechResultState = attr.ib() + text: str | None + result: SpeechResultState class Provider(ABC): @@ -171,30 +186,6 @@ class SpeechToTextView(HomeAssistantView): """Initialize a tts view.""" self.providers = providers - @staticmethod - def _metadata_from_header(request: web.Request) -> SpeechMetadata | None: - """Extract metadata from header. - - X-Speech-Content: format=wav; codec=pcm; sample_rate=16000; bit_rate=16; channel=1; language=de_de - """ - try: - data = request.headers[istr("X-Speech-Content")].split(";") - except KeyError: - _LOGGER.warning("Missing X-Speech-Content") - return None - - # Convert Header data - args: dict[str, Any] = {} - for value in data: - value = value.strip() - args[value.partition("=")[0]] = value.partition("=")[2] - - try: - return SpeechMetadata(**args) - except TypeError as err: - _LOGGER.warning("Wrong format of X-Speech-Content: %s", err) - return None - async def post(self, request: web.Request, provider: str) -> web.Response: """Convert Speech (audio) to text.""" if provider not in self.providers: @@ -202,9 +193,10 @@ class SpeechToTextView(HomeAssistantView): stt_provider: Provider = self.providers[provider] # Get metadata - metadata = self._metadata_from_header(request) - if not metadata: - raise HTTPBadRequest() + try: + metadata = metadata_from_header(request) + except ValueError as err: + raise HTTPBadRequest(text=str(err)) from err # Check format if not stt_provider.check_metadata(metadata): @@ -216,7 +208,7 @@ class SpeechToTextView(HomeAssistantView): ) # Return result - return self.json(attr.asdict(result)) + return self.json(asdict(result)) async def get(self, request: web.Request, provider: str) -> web.Response: """Return provider specific audio information.""" @@ -234,3 +226,47 @@ class SpeechToTextView(HomeAssistantView): "channels": stt_provider.supported_channels, } ) + + +def metadata_from_header(request: web.Request) -> SpeechMetadata: + """Extract STT metadata from header. + + X-Speech-Content: format=wav; codec=pcm; sample_rate=16000; bit_rate=16; channel=1; language=de_de + """ + try: + data = request.headers[istr("X-Speech-Content")].split(";") + except KeyError as err: + raise ValueError("Missing X-Speech-Content header") from err + + fields = ( + "language", + "format", + "codec", + "bit_rate", + "sample_rate", + "channel", + ) + + # Convert Header data + args: dict[str, Any] = {} + for entry in data: + key, _, value = entry.strip().partition("=") + if key not in fields: + raise ValueError(f"Invalid field {key}") + args[key] = value + + for field in fields: + if field not in args: + raise ValueError(f"Missing {field} in X-Speech-Content header") + + try: + return SpeechMetadata( + language=args["language"], + format=args["format"], + codec=args["codec"], + bit_rate=args["bit_rate"], + sample_rate=args["sample_rate"], + channel=args["channel"], + ) + except TypeError as err: + raise ValueError(f"Wrong format of X-Speech-Content: {err}") from err diff --git a/tests/components/stt/test_init.py b/tests/components/stt/test_init.py index 3b207fae01a..33242180f77 100644 --- a/tests/components/stt/test_init.py +++ b/tests/components/stt/test_init.py @@ -1,30 +1,155 @@ """Test STT component setup.""" +from asyncio import StreamReader from http import HTTPStatus +from unittest.mock import AsyncMock, Mock -from homeassistant.components import stt +import pytest + +from homeassistant.components.stt import ( + AudioBitRates, + AudioChannels, + AudioCodecs, + AudioFormats, + AudioSampleRates, + Provider, + SpeechMetadata, + SpeechResult, + SpeechResultState, + async_get_provider, +) from homeassistant.setup import async_setup_component - -async def test_setup_comp(hass): - """Set up demo component.""" - assert await async_setup_component(hass, stt.DOMAIN, {"stt": {}}) +from tests.common import mock_platform -async def test_demo_settings_not_exists(hass, hass_client): - """Test retrieve settings from demo provider.""" - assert await async_setup_component(hass, stt.DOMAIN, {"stt": {}}) +class TestProvider(Provider): + """Test provider.""" + + fail_process_audio = False + + def __init__(self) -> None: + """Init test provider.""" + self.calls = [] + + @property + def supported_languages(self): + """Return a list of supported languages.""" + return ["en"] + + @property + def supported_formats(self) -> list[AudioFormats]: + """Return a list of supported formats.""" + return [AudioFormats.WAV, AudioFormats.OGG] + + @property + def supported_codecs(self) -> list[AudioCodecs]: + """Return a list of supported codecs.""" + return [AudioCodecs.PCM, AudioCodecs.OPUS] + + @property + def supported_bit_rates(self) -> list[AudioBitRates]: + """Return a list of supported bitrates.""" + return [AudioBitRates.BITRATE_16] + + @property + def supported_sample_rates(self) -> list[AudioSampleRates]: + """Return a list of supported samplerates.""" + return [AudioSampleRates.SAMPLERATE_16000] + + @property + def supported_channels(self) -> list[AudioChannels]: + """Return a list of supported channels.""" + return [AudioChannels.CHANNEL_MONO] + + async def async_process_audio_stream( + self, metadata: SpeechMetadata, stream: StreamReader + ) -> SpeechResult: + """Process an audio stream.""" + self.calls.append((metadata, stream)) + if self.fail_process_audio: + return SpeechResult(None, SpeechResultState.ERROR) + + return SpeechResult("test", SpeechResultState.SUCCESS) + + +@pytest.fixture +def test_provider(): + """Test provider fixture.""" + return TestProvider() + + +@pytest.fixture(autouse=True) +async def mock_setup(hass, test_provider): + """Set up a test provider.""" + mock_platform( + hass, "test.stt", Mock(async_get_engine=AsyncMock(return_value=test_provider)) + ) + assert await async_setup_component(hass, "stt", {"stt": {"platform": "test"}}) + + +async def test_get_provider_info(hass, hass_client): + """Test engine that doesn't exist.""" client = await hass_client() + response = await client.get("/api/stt/test") + assert response.status == HTTPStatus.OK + assert await response.json() == { + "languages": ["en"], + "formats": ["wav", "ogg"], + "codecs": ["pcm", "opus"], + "sample_rates": [16000], + "bit_rates": [16], + "channels": [1], + } - response = await client.get("/api/stt/beer") +async def test_get_non_existing_provider_info(hass, hass_client): + """Test streaming to engine that doesn't exist.""" + client = await hass_client() + response = await client.get("/api/stt/not_exist") assert response.status == HTTPStatus.NOT_FOUND -async def test_demo_speech_not_exists(hass, hass_client): - """Test retrieve settings from demo provider.""" - assert await async_setup_component(hass, stt.DOMAIN, {"stt": {}}) +async def test_stream_audio(hass, hass_client): + """Test streaming audio and getting response.""" client = await hass_client() + response = await client.post( + "/api/stt/test", + headers={ + "X-Speech-Content": "format=wav; codec=pcm; sample_rate=16000; bit_rate=16; channel=1; language=en" + }, + ) + assert response.status == HTTPStatus.OK + assert await response.json() == {"text": "test", "result": "success"} - response = await client.post("/api/stt/beer", data=b"test") - assert response.status == HTTPStatus.NOT_FOUND +@pytest.mark.parametrize( + "header,status,error", + ( + (None, 400, "Missing X-Speech-Content header"), + ( + "format=wav; codec=pcm; sample_rate=16000; bit_rate=16; channel=100; language=en", + 400, + "100 is not a valid AudioChannels", + ), + ( + "format=wav; codec=pcm; sample_rate=16000", + 400, + "Missing language in X-Speech-Content header", + ), + ), +) +async def test_metadata_errors(hass, hass_client, header, status, error): + """Test metadata errors.""" + client = await hass_client() + headers = {} + if header: + headers["X-Speech-Content"] = header + + response = await client.post("/api/stt/test", headers=headers) + assert response.status == status + assert await response.text() == error + + +async def test_get_provider(hass, test_provider): + """Test we can get STT providers.""" + assert test_provider == async_get_provider(hass, "test") From 38b6377a31db2e323ec711a3d01b4a7b9977a004 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sat, 24 Sep 2022 10:30:16 +0000 Subject: [PATCH 689/955] Bump shelly backend library to version 2.0.2 (#79026) Bump aioshelly library to version 2.0.2 --- 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 48e48b618e4..d9a0e21764a 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==2.0.1"], + "requirements": ["aioshelly==2.0.2"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index f3cb09602d6..63a71f0335f 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==2.0.1 +aioshelly==2.0.2 # homeassistant.components.skybell aioskybell==22.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 881de4a5650..9f56911851d 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==2.0.1 +aioshelly==2.0.2 # homeassistant.components.skybell aioskybell==22.7.0 From a3cb38e11449c806a693202cc7f4540450d097d5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 24 Sep 2022 05:04:52 -1000 Subject: [PATCH 690/955] Bump dbus-fast to 1.9.0 (#79024) --- 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 dc4e1f05656..39631e4cc4c 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -10,7 +10,7 @@ "bleak-retry-connector==1.17.3", "bluetooth-adapters==0.5.1", "bluetooth-auto-recovery==0.3.3", - "dbus-fast==1.7.0" + "dbus-fast==1.9.0" ], "codeowners": ["@bdraco"], "config_flow": true, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index dcf8f415c3b..b3d8fb602a8 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ bluetooth-auto-recovery==0.3.3 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==37.0.4 -dbus-fast==1.7.0 +dbus-fast==1.9.0 fnvhash==0.1.0 hass-nabucasa==0.55.0 home-assistant-bluetooth==1.3.0 diff --git a/requirements_all.txt b/requirements_all.txt index 63a71f0335f..e31dffb6efc 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.7.0 +dbus-fast==1.9.0 # homeassistant.components.debugpy debugpy==1.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9f56911851d..359d85bf571 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -417,7 +417,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.7.0 +dbus-fast==1.9.0 # homeassistant.components.debugpy debugpy==1.6.3 From 3e9c0f18f96143d09420520b1812693b925b55d1 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 25 Sep 2022 00:28:01 +0000 Subject: [PATCH 691/955] [ci skip] Translation update --- .../components/braviatv/translations/ca.json | 10 +++- .../components/braviatv/translations/he.json | 4 ++ .../braviatv/translations/pt-BR.json | 12 +++-- .../components/climacell/translations/it.json | 13 +++++ .../climacell/translations/sensor.it.json | 27 +++++++++++ .../google_sheets/translations/he.json | 20 ++++++++ .../components/ibeacon/translations/he.json | 7 +++ .../components/kegtron/translations/he.json | 21 ++++++++ .../keymitt_ble/translations/he.json | 9 ++++ .../components/lametric/translations/he.json | 7 +++ .../components/lidarr/translations/ca.json | 4 +- .../components/lidarr/translations/he.json | 28 +++++++++++ .../nibe_heatpump/translations/he.json | 10 ++++ .../pvpc_hourly_pricing/translations/bg.json | 7 +++ .../components/radarr/translations/ca.json | 48 +++++++++++++++++++ .../components/radarr/translations/he.json | 25 ++++++++++ .../components/radarr/translations/it.json | 2 +- .../rainmachine/translations/ca.json | 13 +++++ .../components/switchbee/translations/he.json | 21 ++++++++ .../components/zone/translations/gl.json | 11 +++++ 20 files changed, 291 insertions(+), 8 deletions(-) create mode 100644 homeassistant/components/climacell/translations/it.json create mode 100644 homeassistant/components/climacell/translations/sensor.it.json create mode 100644 homeassistant/components/google_sheets/translations/he.json create mode 100644 homeassistant/components/ibeacon/translations/he.json create mode 100644 homeassistant/components/kegtron/translations/he.json create mode 100644 homeassistant/components/keymitt_ble/translations/he.json create mode 100644 homeassistant/components/lametric/translations/he.json create mode 100644 homeassistant/components/lidarr/translations/he.json create mode 100644 homeassistant/components/nibe_heatpump/translations/he.json create mode 100644 homeassistant/components/pvpc_hourly_pricing/translations/bg.json create mode 100644 homeassistant/components/radarr/translations/ca.json create mode 100644 homeassistant/components/radarr/translations/he.json create mode 100644 homeassistant/components/switchbee/translations/he.json create mode 100644 homeassistant/components/zone/translations/gl.json diff --git a/homeassistant/components/braviatv/translations/ca.json b/homeassistant/components/braviatv/translations/ca.json index 2f35c77caa1..6c8c9736750 100644 --- a/homeassistant/components/braviatv/translations/ca.json +++ b/homeassistant/components/braviatv/translations/ca.json @@ -2,21 +2,27 @@ "config": { "abort": { "already_configured": "El dispositiu ja est\u00e0 configurat", - "no_ip_control": "El control IP del teu televisor est\u00e0 desactivat o aquest no \u00e9s compatible." + "no_ip_control": "El control IP del teu televisor est\u00e0 desactivat o aquest no \u00e9s compatible.", + "not_bravia_device": "El dispositiu no \u00e9s un televisor Bravia." }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", "invalid_host": "Nom de l'amfitri\u00f3 o adre\u00e7a IP inv\u00e0lids", "unsupported_model": "Aquest model de televisor no \u00e9s compatible." }, "step": { "authorize": { "data": { - "pin": "Codi PIN" + "pin": "Codi PIN", + "use_psk": "Utilitza autenticaci\u00f3 PSK" }, "description": "Introdueix el codi PIN que es mostra a la pantalla del televisor.\n\nSi no es mostra el codi, has d'eliminar Home Assistant del teu televisor. V\u00e9s a Configuraci\u00f3 > Xarxa > Configuraci\u00f3 de dispositiu remot > Elimina dispositiu remot.", "title": "Autoritzaci\u00f3 del televisor Sony Bravia" }, + "confirm": { + "description": "Vols comen\u00e7ar la configuraci\u00f3?" + }, "user": { "data": { "host": "Amfitri\u00f3" diff --git a/homeassistant/components/braviatv/translations/he.json b/homeassistant/components/braviatv/translations/he.json index ab9d638a8ac..b717638aa2f 100644 --- a/homeassistant/components/braviatv/translations/he.json +++ b/homeassistant/components/braviatv/translations/he.json @@ -5,6 +5,7 @@ }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", "invalid_host": "\u05e9\u05dd \u05de\u05d0\u05e8\u05d7 \u05d0\u05d5 \u05db\u05ea\u05d5\u05d1\u05ea IP \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9\u05d9\u05dd" }, "step": { @@ -13,6 +14,9 @@ "pin": "\u05e7\u05d5\u05d3 PIN" } }, + "confirm": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05ea\u05d7\u05d9\u05dc \u05d1\u05d4\u05d2\u05d3\u05e8\u05d4?" + }, "user": { "data": { "host": "\u05de\u05d0\u05e8\u05d7" diff --git a/homeassistant/components/braviatv/translations/pt-BR.json b/homeassistant/components/braviatv/translations/pt-BR.json index 3931935ff38..a6aef38d17f 100644 --- a/homeassistant/components/braviatv/translations/pt-BR.json +++ b/homeassistant/components/braviatv/translations/pt-BR.json @@ -2,21 +2,27 @@ "config": { "abort": { "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", - "no_ip_control": "O Controle de IP est\u00e1 desativado em sua TV ou a TV n\u00e3o \u00e9 compat\u00edvel." + "no_ip_control": "O Controle de IP est\u00e1 desativado em sua TV ou a TV n\u00e3o \u00e9 compat\u00edvel.", + "not_bravia_device": "O dispositivo n\u00e3o \u00e9 uma TV Bravia." }, "error": { "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "invalid_host": "Nome de host ou endere\u00e7o IP inv\u00e1lido", "unsupported_model": "Seu modelo de TV n\u00e3o \u00e9 suportado." }, "step": { "authorize": { "data": { - "pin": "C\u00f3digo PIN" + "pin": "C\u00f3digo PIN", + "use_psk": "Usar autentica\u00e7\u00e3o PSK" }, - "description": "Digite o c\u00f3digo PIN mostrado na TV Sony Bravia.\n\nSe o c\u00f3digo PIN n\u00e3o for mostrado, voc\u00ea deve cancelar o registro do Home Assistant na sua TV, v\u00e1 para: Configura\u00e7\u00f5es -> Rede -> Configura\u00e7\u00f5es do dispositivo remoto -> Cancelar registro do dispositivo remoto.", + "description": "Digite o c\u00f3digo PIN mostrado na TV Sony Bravia. \n\n Se o c\u00f3digo PIN n\u00e3o for exibido, voc\u00ea deve cancelar o registro do Home Assistant na sua TV, v\u00e1 para: Configura\u00e7\u00f5es - > Rede - > Configura\u00e7\u00f5es do dispositivo remoto - > Cancelar o registro do dispositivo remoto. \n\n Voc\u00ea pode usar PSK (Pre-Shared-Key) em vez de PIN. PSK \u00e9 uma chave secreta definida pelo usu\u00e1rio usada para controle de acesso. Este m\u00e9todo de autentica\u00e7\u00e3o \u00e9 recomendado como mais est\u00e1vel. Para ativar o PSK em sua TV, v\u00e1 para: Configura\u00e7\u00f5es - > Rede - > Configura\u00e7\u00e3o de rede dom\u00e9stica - > Controle de IP. Em seguida, marque a caixa \u00abUsar autentica\u00e7\u00e3o PSK\u00bb e digite seu PSK em vez do PIN.", "title": "Autorizar a TV Sony Bravia" }, + "confirm": { + "description": "Deseja iniciar a configura\u00e7\u00e3o?" + }, "user": { "data": { "host": "Nome do host" diff --git a/homeassistant/components/climacell/translations/it.json b/homeassistant/components/climacell/translations/it.json new file mode 100644 index 00000000000..b9667d6bfb1 --- /dev/null +++ b/homeassistant/components/climacell/translations/it.json @@ -0,0 +1,13 @@ +{ + "options": { + "step": { + "init": { + "data": { + "timestep": "Minuti tra le previsioni di NowCast" + }, + "description": "Se scegli di abilitare l'entit\u00e0 di previsione `nowcast`, puoi configurare il numero di minuti tra ogni previsione. Il numero di previsioni fornite dipende dal numero di minuti scelti tra le previsioni.", + "title": "Aggiorna le opzioni di ClimaCell" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.it.json b/homeassistant/components/climacell/translations/sensor.it.json new file mode 100644 index 00000000000..b9326be886e --- /dev/null +++ b/homeassistant/components/climacell/translations/sensor.it.json @@ -0,0 +1,27 @@ +{ + "state": { + "climacell__health_concern": { + "good": "Buono", + "hazardous": "Pericoloso", + "moderate": "Moderato", + "unhealthy": "Malsano", + "unhealthy_for_sensitive_groups": "Malsano per gruppi sensibili", + "very_unhealthy": "Molto malsano" + }, + "climacell__pollen_index": { + "high": "Alto", + "low": "Basso", + "medium": "Medio", + "none": "Nessuno", + "very_high": "Molto alto", + "very_low": "Molto basso" + }, + "climacell__precipitation_type": { + "freezing_rain": "Grandine", + "ice_pellets": "Pioggia gelata", + "none": "Nessuno", + "rain": "Pioggia", + "snow": "Neve" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_sheets/translations/he.json b/homeassistant/components/google_sheets/translations/he.json new file mode 100644 index 00000000000..412a09eb52a --- /dev/null +++ b/homeassistant/components/google_sheets/translations/he.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_access_token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "missing_configuration": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05e8\u05db\u05d9\u05d1 \u05dc\u05d0 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e0\u05d0 \u05e2\u05e7\u05d5\u05d1 \u05d0\u05d7\u05e8 \u05d4\u05ea\u05d9\u05e2\u05d5\u05d3.", + "oauth_error": "\u05d4\u05ea\u05e7\u05d1\u05dc\u05d5 \u05e0\u05ea\u05d5\u05e0\u05d9 \u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9\u05d9\u05dd.", + "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", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "pick_implementation": { + "title": "\u05d1\u05d7\u05e8 \u05e9\u05d9\u05d8\u05ea \u05d0\u05d9\u05de\u05d5\u05ea" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ibeacon/translations/he.json b/homeassistant/components/ibeacon/translations/he.json new file mode 100644 index 00000000000..d0c3523da94 --- /dev/null +++ b/homeassistant/components/ibeacon/translations/he.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kegtron/translations/he.json b/homeassistant/components/kegtron/translations/he.json new file mode 100644 index 00000000000..de780eb221a --- /dev/null +++ b/homeassistant/components/kegtron/translations/he.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + }, + "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/keymitt_ble/translations/he.json b/homeassistant/components/keymitt_ble/translations/he.json new file mode 100644 index 00000000000..934eb6df0c4 --- /dev/null +++ b/homeassistant/components/keymitt_ble/translations/he.json @@ -0,0 +1,9 @@ +{ + "config": { + "abort": { + "already_configured_device": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lametric/translations/he.json b/homeassistant/components/lametric/translations/he.json new file mode 100644 index 00000000000..53f74430ae1 --- /dev/null +++ b/homeassistant/components/lametric/translations/he.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lidarr/translations/ca.json b/homeassistant/components/lidarr/translations/ca.json index 1610465f760..78d0904b50a 100644 --- a/homeassistant/components/lidarr/translations/ca.json +++ b/homeassistant/components/lidarr/translations/ca.json @@ -9,7 +9,7 @@ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", "unknown": "Error inesperat", "wrong_app": "No s'ha trobat l'aplicaci\u00f3 correcta. Torna-ho a intentar", - "zeroconf_failed": "No s'ha trobat la clau API. Introdu\u00efu-la manualment" + "zeroconf_failed": "No s'ha trobat la clau API. Introdueix-la manualment" }, "step": { "reauth_confirm": { @@ -25,7 +25,7 @@ "url": "URL", "verify_ssl": "Verifica el certificat SSL" }, - "description": "La clau API es pot recuperar autom\u00e0ticament si les credencials d'inici de sessi\u00f3 no s'han establert a l'aplicaci\u00f3.\nLa teva clau API es pot trobar a Configuraci\u00f3 ('Settings') > General a la interf\u00edcie web de Lidarr." + "description": "La clau API es pot recuperar autom\u00e0ticament si les credencials d'inici de sessi\u00f3 no s'han establert a l'aplicaci\u00f3.\nLa teva clau API es pot trobar a Configuraci\u00f3 ('Settings') > General, a la interf\u00edcie web de Lidarr." } } }, diff --git a/homeassistant/components/lidarr/translations/he.json b/homeassistant/components/lidarr/translations/he.json new file mode 100644 index 00000000000..7f63149230a --- /dev/null +++ b/homeassistant/components/lidarr/translations/he.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "\u05de\u05e4\u05ea\u05d7 API" + }, + "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1" + }, + "user": { + "data": { + "api_key": "\u05de\u05e4\u05ea\u05d7 API", + "url": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8", + "verify_ssl": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8 SSL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nibe_heatpump/translations/he.json b/homeassistant/components/nibe_heatpump/translations/he.json new file mode 100644 index 00000000000..ea40181bd9a --- /dev/null +++ b/homeassistant/components/nibe_heatpump/translations/he.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/bg.json b/homeassistant/components/pvpc_hourly_pricing/translations/bg.json new file mode 100644 index 00000000000..80a7cc489a9 --- /dev/null +++ b/homeassistant/components/pvpc_hourly_pricing/translations/bg.json @@ -0,0 +1,7 @@ +{ + "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" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radarr/translations/ca.json b/homeassistant/components/radarr/translations/ca.json new file mode 100644 index 00000000000..c94cd1cd901 --- /dev/null +++ b/homeassistant/components/radarr/translations/ca.json @@ -0,0 +1,48 @@ +{ + "config": { + "abort": { + "already_configured": "El servei ja est\u00e0 configurat", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat", + "wrong_app": "No s'ha trobat l'aplicaci\u00f3 correcta. Torna-ho a intentar", + "zeroconf_failed": "No s'ha trobat la clau API. Introdueix-la manualment" + }, + "step": { + "reauth_confirm": { + "description": "La integraci\u00f3 Radarr ha de tornar a autenticar-se manualment amb l'API de Radarr", + "title": "Reautenticaci\u00f3 de la integraci\u00f3" + }, + "user": { + "data": { + "api_key": "Clau API", + "url": "URL", + "verify_ssl": "Verifica el certificat SSL" + }, + "description": "La clau API es pot obtenir autom\u00e0ticament si les credencials d'inici de sessi\u00f3 no s'han establert a l'aplicaci\u00f3.\nLa teva clau API es pot trobar a Configuraci\u00f3 ('Settings') > General, a la interf\u00edcie web de Radarr." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "La configuraci\u00f3 de Radarr 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 Radarr del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", + "title": "La configuraci\u00f3 YAML de Radarr est\u00e0 sent eliminada" + }, + "removed_attributes": { + "description": "Per precauci\u00f3, s'han fet alguns canvis importants en el sensor recompte de pel\u00b7l\u00edcules.\n\nAquest sensor pot causar problemes amb bases de dades molt grans. Si encara vols utilitzar-lo, pots fer-ho. \n\nEls noms de pel\u00b7l\u00edcules ja no s'inclouen com a atributs al sensor pel\u00b7l\u00edcules. \n\nPropers s'ha eliminat. S'ha modernitzat com a elements de calendari. L'espai del disc ara es divideix en diferents sensors, un per a cada carpeta. \n\nL'estat i les comandes s'han eliminat perqu\u00e8 no sembla que tinguin un valor real per a les automatitzacions.", + "title": "Canvis a la integraci\u00f3 Radarr" + } + }, + "options": { + "step": { + "init": { + "data": { + "upcoming_days": "Nombre de dies propers a mostrar" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radarr/translations/he.json b/homeassistant/components/radarr/translations/he.json new file mode 100644 index 00000000000..44273d609c2 --- /dev/null +++ b/homeassistant/components/radarr/translations/he.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "reauth_confirm": { + "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1" + }, + "user": { + "data": { + "api_key": "\u05de\u05e4\u05ea\u05d7 API", + "url": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8", + "verify_ssl": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8 SSL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radarr/translations/it.json b/homeassistant/components/radarr/translations/it.json index 75514420617..afc64b6bc8e 100644 --- a/homeassistant/components/radarr/translations/it.json +++ b/homeassistant/components/radarr/translations/it.json @@ -13,7 +13,7 @@ }, "step": { "reauth_confirm": { - "description": "L'integrazione Radarr deve essere riautenticata manualmente con l'API Radarr.", + "description": "L'integrazione Radarr deve essere di nuovo autenticata manualmente con l'API Radarr.", "title": "Autentica nuovamente l'integrazione" }, "user": { diff --git a/homeassistant/components/rainmachine/translations/ca.json b/homeassistant/components/rainmachine/translations/ca.json index d441654ce08..e776a0bcdf3 100644 --- a/homeassistant/components/rainmachine/translations/ca.json +++ b/homeassistant/components/rainmachine/translations/ca.json @@ -18,6 +18,19 @@ } } }, + "issues": { + "replaced_old_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "Actualitza totes les automatitzacions o 'scripts' que utilitzin aquesta entitat perqu\u00e8 passin a utilitzar l'entitat `{replacement_entity_id}`.", + "title": "L'entitat {old_entity_id} s'eliminar\u00e0" + } + } + }, + "title": "L'entitat {old_entity_id} s'eliminar\u00e0" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/switchbee/translations/he.json b/homeassistant/components/switchbee/translations/he.json new file mode 100644 index 00000000000..479d2f2f5e8 --- /dev/null +++ b/homeassistant/components/switchbee/translations/he.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/translations/gl.json b/homeassistant/components/zone/translations/gl.json new file mode 100644 index 00000000000..24c2e1cae6d --- /dev/null +++ b/homeassistant/components/zone/translations/gl.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "init": { + "data": { + "longitude": "Lonxitude" + } + } + } + } +} \ No newline at end of file From 26b8e4e0fb8e4d266e8b1300668702bf4aba788b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 24 Sep 2022 15:44:59 -1000 Subject: [PATCH 692/955] Bump bluetooth dependencies (#79035) * Bump dbus-fast to 1.10.0 changelog: https://github.com/Bluetooth-Devices/dbus-fast/compare/v1.9.0...v1.10.0 * bump again * bump again * to 13 * bump * try again --- 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 39631e4cc4c..acf26c59e2e 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -7,10 +7,10 @@ "quality_scale": "internal", "requirements": [ "bleak==0.18.0", - "bleak-retry-connector==1.17.3", + "bleak-retry-connector==2.0.2", "bluetooth-adapters==0.5.1", "bluetooth-auto-recovery==0.3.3", - "dbus-fast==1.9.0" + "dbus-fast==1.13.0" ], "codeowners": ["@bdraco"], "config_flow": true, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b3d8fb602a8..aac5df14883 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,14 +10,14 @@ atomicwrites-homeassistant==1.4.1 attrs==21.2.0 awesomeversion==22.9.0 bcrypt==3.1.7 -bleak-retry-connector==1.17.3 +bleak-retry-connector==2.0.2 bleak==0.18.0 bluetooth-adapters==0.5.1 bluetooth-auto-recovery==0.3.3 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==37.0.4 -dbus-fast==1.9.0 +dbus-fast==1.13.0 fnvhash==0.1.0 hass-nabucasa==0.55.0 home-assistant-bluetooth==1.3.0 diff --git a/requirements_all.txt b/requirements_all.txt index e31dffb6efc..1e37dd8ee0b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -410,7 +410,7 @@ bimmer_connected==0.10.4 bizkaibus==0.1.1 # homeassistant.components.bluetooth -bleak-retry-connector==1.17.3 +bleak-retry-connector==2.0.2 # homeassistant.components.bluetooth bleak==0.18.0 @@ -540,7 +540,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.9.0 +dbus-fast==1.13.0 # homeassistant.components.debugpy debugpy==1.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 359d85bf571..bf8233fc76e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -331,7 +331,7 @@ bellows==0.33.1 bimmer_connected==0.10.4 # homeassistant.components.bluetooth -bleak-retry-connector==1.17.3 +bleak-retry-connector==2.0.2 # homeassistant.components.bluetooth bleak==0.18.0 @@ -417,7 +417,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.9.0 +dbus-fast==1.13.0 # homeassistant.components.debugpy debugpy==1.6.3 From c46f55caa869c4edf5660c433261454481f9e95d Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 25 Sep 2022 02:01:49 +0000 Subject: [PATCH 693/955] Add reauth flow to Shelly integration (#78786) --- homeassistant/components/shelly/__init__.py | 13 ++- homeassistant/components/shelly/climate.py | 20 ++-- .../components/shelly/config_flow.py | 49 ++++++++ homeassistant/components/shelly/strings.json | 10 +- tests/components/shelly/test_config_flow.py | 105 ++++++++++++++++++ 5 files changed, 187 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 125e63449ef..dcb2f518144 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -4,10 +4,13 @@ from __future__ import annotations import asyncio from collections.abc import Coroutine from datetime import timedelta +from http import HTTPStatus from typing import Any, Final, cast +from aiohttp import ClientResponseError import aioshelly from aioshelly.block_device import BlockDevice +from aioshelly.exceptions import AuthRequired, InvalidAuthError from aioshelly.rpc_device import RpcDevice import async_timeout import voluptuous as vol @@ -22,7 +25,7 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import Event, HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, device_registry, update_coordinator import homeassistant.helpers.config_validation as cv from homeassistant.helpers.debounce import Debouncer @@ -191,12 +194,18 @@ async def async_setup_block_entry(hass: HomeAssistant, entry: ConfigEntry) -> bo try: async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): await device.initialize() + await device.update_status() except asyncio.TimeoutError as err: raise ConfigEntryNotReady( str(err) or "Timeout during device setup" ) from err except OSError as err: raise ConfigEntryNotReady(str(err) or "Error during device setup") from err + except AuthRequired as err: + raise ConfigEntryAuthFailed from err + except ClientResponseError as err: + if err.status == HTTPStatus.UNAUTHORIZED: + raise ConfigEntryAuthFailed from err async_block_device_setup(hass, entry, device) elif sleep_period is None or device_entry is None: @@ -253,6 +262,8 @@ async def async_setup_rpc_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool raise ConfigEntryNotReady(str(err) or "Timeout during device setup") from err except OSError as err: raise ConfigEntryNotReady(str(err) or "Error during device setup") from err + except (AuthRequired, InvalidAuthError) as err: + raise ConfigEntryAuthFailed from err device_wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][ RPC diff --git a/homeassistant/components/shelly/climate.py b/homeassistant/components/shelly/climate.py index 0bdcb3a9ad9..f98c048d569 100644 --- a/homeassistant/components/shelly/climate.py +++ b/homeassistant/components/shelly/climate.py @@ -6,6 +6,7 @@ from collections.abc import Mapping from typing import Any, cast from aioshelly.block_device import Block +from aioshelly.exceptions import AuthRequired import async_timeout from homeassistant.components.climate import ( @@ -318,11 +319,14 @@ class BlockSleepingClimate( assert self.block.channel - self._preset_modes = [ - PRESET_NONE, - *self.wrapper.device.settings["thermostats"][int(self.block.channel)][ - "schedule_profile_names" - ], - ] - - self.async_write_ha_state() + try: + self._preset_modes = [ + PRESET_NONE, + *self.wrapper.device.settings["thermostats"][ + int(self.block.channel) + ]["schedule_profile_names"], + ] + except AuthRequired: + self.wrapper.entry.async_start_reauth(self.hass) + else: + self.async_write_ha_state() diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index 41e0bd3031a..38d30fd0b62 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from collections.abc import Mapping from http import HTTPStatus from typing import Any, Final @@ -91,6 +92,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): host: str = "" info: dict[str, Any] = {} device_info: dict[str, Any] = {} + entry: config_entries.ConfigEntry | None = None async def async_step_user( self, user_input: dict[str, Any] | None = None @@ -262,6 +264,53 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + """Handle configuration by re-auth.""" + self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Dialog that informs the user that reauth is required.""" + errors: dict[str, str] = {} + assert self.entry is not None + host = self.entry.data[CONF_HOST] + + if user_input is not None: + info = await self._async_get_info(host) + if self.entry.data.get("gen", 1) != 1: + user_input[CONF_USERNAME] = "admin" + try: + await validate_input(self.hass, host, info, user_input) + except ( + aiohttp.ClientResponseError, + aioshelly.exceptions.InvalidAuthError, + asyncio.TimeoutError, + aiohttp.ClientError, + ): + return self.async_abort(reason="reauth_unsuccessful") + else: + self.hass.config_entries.async_update_entry( + self.entry, data={**self.entry.data, **user_input} + ) + await self.hass.config_entries.async_reload(self.entry.entry_id) + return self.async_abort(reason="reauth_successful") + + if self.entry.data.get("gen", 1) == 1: + schema = { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + } + else: + schema = {vol.Required(CONF_PASSWORD): str} + + return self.async_show_form( + step_id="reauth_confirm", + data_schema=vol.Schema(schema), + errors=errors, + ) + async def _async_get_info(self, host: str) -> dict[str, Any]: """Get info from shelly device.""" async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): diff --git a/homeassistant/components/shelly/strings.json b/homeassistant/components/shelly/strings.json index db1c6043187..d3684f85be2 100644 --- a/homeassistant/components/shelly/strings.json +++ b/homeassistant/components/shelly/strings.json @@ -14,6 +14,12 @@ "password": "[%key:common::config_flow::data::password%]" } }, + "reauth_confirm": { + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + }, "confirm_discovery": { "description": "Do you want to set up the {model} at {host}?\n\nBattery-powered devices that are password protected must be woken up before continuing with setting up.\nBattery-powered devices that are not password protected will be added when the device wakes up, you can now manually wake the device up using a button on it or wait for the next data update from the device." } @@ -26,7 +32,9 @@ }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "unsupported_firmware": "The device is using an unsupported firmware version." + "unsupported_firmware": "The device is using an unsupported firmware version.", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "reauth_unsuccessful": "Re-authentication was unsuccessful, please remove the integration and set it up again." } }, "device_automation": { diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py index f47fdef0994..b7083cb6805 100644 --- a/tests/components/shelly/test_config_flow.py +++ b/tests/components/shelly/test_config_flow.py @@ -10,6 +10,7 @@ import pytest from homeassistant import config_entries, data_entry_flow from homeassistant.components import zeroconf from homeassistant.components.shelly.const import DOMAIN +from homeassistant.config_entries import SOURCE_REAUTH from tests.common import MockConfigEntry @@ -780,3 +781,107 @@ async def test_zeroconf_require_auth(hass): } assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + "test_data", + [ + (1, {"username": "test user", "password": "test1 password"}), + (2, {"password": "test2 password"}), + ], +) +async def test_reauth_successful(hass, test_data): + """Test starting a reauthentication flow.""" + gen, user_input = test_data + entry = MockConfigEntry( + domain="shelly", unique_id="test-mac", data={"host": "0.0.0.0", "gen": gen} + ) + entry.add_to_hass(hass) + + with patch( + "aioshelly.common.get_info", + return_value={"mac": "test-mac", "type": "SHSW-1", "auth": True, "gen": gen}, + ), patch( + "aioshelly.block_device.BlockDevice.create", + new=AsyncMock( + return_value=Mock( + model="SHSW-1", + settings=MOCK_SETTINGS, + ) + ), + ), patch( + "aioshelly.rpc_device.RpcDevice.create", + new=AsyncMock( + return_value=Mock( + shelly={"model": "SHSW-1", "gen": gen}, + config=MOCK_CONFIG, + shutdown=AsyncMock(), + ) + ), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_REAUTH, "entry_id": entry.entry_id}, + data=entry.data, + ) + + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=user_input, + ) + + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + + +@pytest.mark.parametrize( + "test_data", + [ + ( + 1, + {"username": "test user", "password": "test1 password"}, + aioshelly.exceptions.InvalidAuthError(code=HTTPStatus.UNAUTHORIZED.value), + ), + ( + 2, + {"password": "test2 password"}, + aiohttp.ClientResponseError(Mock(), (), status=HTTPStatus.UNAUTHORIZED), + ), + ], +) +async def test_reauth_unsuccessful(hass, test_data): + """Test reauthentication flow failed.""" + gen, user_input, exc = test_data + entry = MockConfigEntry( + domain="shelly", unique_id="test-mac", data={"host": "0.0.0.0", "gen": gen} + ) + entry.add_to_hass(hass) + + with patch( + "aioshelly.common.get_info", + return_value={"mac": "test-mac", "type": "SHSW-1", "auth": True, "gen": gen}, + ), patch( + "aioshelly.block_device.BlockDevice.create", + new=AsyncMock(side_effect=exc), + ), patch( + "aioshelly.rpc_device.RpcDevice.create", new=AsyncMock(side_effect=exc) + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_REAUTH, "entry_id": entry.entry_id}, + data=entry.data, + ) + + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=user_input, + ) + + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "reauth_unsuccessful" From 62490f18382f0d480d39084d209a4fb3ec22a488 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 24 Sep 2022 16:02:17 -1000 Subject: [PATCH 694/955] Bump govee-ble to 0.19.0 (#79038) --- homeassistant/components/govee_ble/manifest.json | 12 +++++++++++- homeassistant/generated/bluetooth.py | 12 ++++++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/govee_ble/manifest.json b/homeassistant/components/govee_ble/manifest.json index 537ae9c7ed5..29af7502ded 100644 --- a/homeassistant/components/govee_ble/manifest.json +++ b/homeassistant/components/govee_ble/manifest.json @@ -12,6 +12,11 @@ "service_uuid": "00008451-0000-1000-8000-00805f9b34fb", "connectable": false }, + { + "manufacturer_id": 63391, + "service_uuid": "00008351-0000-1000-8000-00805f9b34fb", + "connectable": false + }, { "manufacturer_id": 26589, "service_uuid": "00008351-0000-1000-8000-00805f9b34fb", @@ -32,6 +37,11 @@ "service_uuid": "00008551-0000-1000-8000-00805f9b34fb", "connectable": false }, + { + "manufacturer_id": 43682, + "service_uuid": "00008151-0000-1000-8000-00805f9b34fb", + "connectable": false + }, { "manufacturer_id": 59970, "service_uuid": "00008151-0000-1000-8000-00805f9b34fb", @@ -58,7 +68,7 @@ "connectable": false } ], - "requirements": ["govee-ble==0.17.3"], + "requirements": ["govee-ble==0.19.0"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 861960b1afd..8a9594f83ff 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -54,6 +54,12 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ "service_uuid": "00008451-0000-1000-8000-00805f9b34fb", "connectable": False, }, + { + "domain": "govee_ble", + "manufacturer_id": 63391, + "service_uuid": "00008351-0000-1000-8000-00805f9b34fb", + "connectable": False, + }, { "domain": "govee_ble", "manufacturer_id": 26589, @@ -78,6 +84,12 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ "service_uuid": "00008551-0000-1000-8000-00805f9b34fb", "connectable": False, }, + { + "domain": "govee_ble", + "manufacturer_id": 43682, + "service_uuid": "00008151-0000-1000-8000-00805f9b34fb", + "connectable": False, + }, { "domain": "govee_ble", "manufacturer_id": 59970, diff --git a/requirements_all.txt b/requirements_all.txt index 1e37dd8ee0b..9ce72ad763f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -780,7 +780,7 @@ googlemaps==2.5.1 goslide-api==0.5.1 # homeassistant.components.govee_ble -govee-ble==0.17.3 +govee-ble==0.19.0 # homeassistant.components.remote_rpi_gpio gpiozero==1.6.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bf8233fc76e..9850aab5c12 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -581,7 +581,7 @@ google-nest-sdm==2.0.0 googlemaps==2.5.1 # homeassistant.components.govee_ble -govee-ble==0.17.3 +govee-ble==0.19.0 # homeassistant.components.gree greeclimate==1.3.0 From bdcece4904fa607e955692fd2ba8462902da910e Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sun, 25 Sep 2022 04:03:03 +0200 Subject: [PATCH 695/955] Fix MQTT device_tracker generating unique id-s - regression on #78547 (#79033) --- .../mqtt/device_tracker/schema_discovery.py | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/homeassistant/components/mqtt/device_tracker/schema_discovery.py b/homeassistant/components/mqtt/device_tracker/schema_discovery.py index 105442b176e..907d424e8a4 100644 --- a/homeassistant/components/mqtt/device_tracker/schema_discovery.py +++ b/homeassistant/components/mqtt/device_tracker/schema_discovery.py @@ -1,7 +1,6 @@ """Support for tracking MQTT enabled devices identified through discovery.""" from __future__ import annotations -import asyncio import functools import voluptuous as vol @@ -28,12 +27,7 @@ from .. import subscription from ..config import MQTT_RO_SCHEMA from ..const import CONF_QOS, CONF_STATE_TOPIC from ..debug_info import log_messages -from ..mixins import ( - MQTT_ENTITY_COMMON_SCHEMA, - MqttEntity, - async_get_platform_config_from_yaml, - async_setup_entry_helper, -) +from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper from ..models import MqttValueTemplate CONF_PAYLOAD_HOME = "payload_home" @@ -58,16 +52,6 @@ async def async_setup_entry_from_discovery( async_add_entities: AddEntitiesCallback, ) -> None: """Set up MQTT device tracker configuration.yaml and dynamically through MQTT discovery.""" - # load and initialize platform config from configuration.yaml - await asyncio.gather( - *( - _async_setup_entity(hass, async_add_entities, config, config_entry) - for config in await async_get_platform_config_from_yaml( - hass, device_tracker.DOMAIN - ) - ) - ) - # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) From 5d378f6fbd8b6c90eb9cfe11a54833b3090be8cf Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 25 Sep 2022 04:03:21 +0200 Subject: [PATCH 696/955] Fix failing LaMetric pairing message during config flow (#79031) --- homeassistant/components/lametric/config_flow.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/lametric/config_flow.py b/homeassistant/components/lametric/config_flow.py index b91fff6cf37..a317d835413 100644 --- a/homeassistant/components/lametric/config_flow.py +++ b/homeassistant/components/lametric/config_flow.py @@ -13,6 +13,7 @@ from demetriek import ( Model, Notification, NotificationIconType, + NotificationPriority, NotificationSound, Simple, Sound, @@ -229,6 +230,7 @@ class LaMetricFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN): await lametric.notify( notification=Notification( + priority=NotificationPriority.CRITICAL, icon_type=NotificationIconType.INFO, model=Model( cycles=2, From fffa70a13666cd2555cf437824ea31fab7107bc7 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sun, 25 Sep 2022 04:03:54 +0200 Subject: [PATCH 697/955] Set OWM default mode to hourly legacy API (#78951) --- homeassistant/components/openweathermap/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/openweathermap/const.py b/homeassistant/components/openweathermap/const.py index 836a56c70b2..5a370604e1f 100644 --- a/homeassistant/components/openweathermap/const.py +++ b/homeassistant/components/openweathermap/const.py @@ -86,7 +86,7 @@ FORECAST_MODES = [ FORECAST_MODE_ONECALL_HOURLY, FORECAST_MODE_ONECALL_DAILY, ] -DEFAULT_FORECAST_MODE = FORECAST_MODE_ONECALL_DAILY +DEFAULT_FORECAST_MODE = FORECAST_MODE_HOURLY LANGUAGES = [ "af", From b4b892a3b38f5b64b42a5171d345018b60543979 Mon Sep 17 00:00:00 2001 From: Oscar Calvo <2091582+ocalvo@users.noreply.github.com> Date: Sun, 25 Sep 2022 00:43:46 -0600 Subject: [PATCH 698/955] Fix a bug where SMS will not be sent as GSM-alphabet (#78800) * Fix #76283 Fix #76283 * Update notify.py * Fixes a bug where unicode parameter is not parssed correctly * Apply PR feedback * Apply PR feedback --- homeassistant/components/sms/notify.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sms/notify.py b/homeassistant/components/sms/notify.py index 13f9cfc9f72..71ec04c5ef1 100644 --- a/homeassistant/components/sms/notify.py +++ b/homeassistant/components/sms/notify.py @@ -3,7 +3,7 @@ import logging import gammu # pylint: disable=import-error -from homeassistant.components.notify import BaseNotificationService +from homeassistant.components.notify import ATTR_DATA, BaseNotificationService from homeassistant.const import CONF_TARGET from .const import CONF_UNICODE, DOMAIN, GATEWAY, SMS_GATEWAY @@ -42,7 +42,14 @@ class SMSNotificationService(BaseNotificationService): _LOGGER.error("No target number specified, cannot send message") return - is_unicode = kwargs.get(CONF_UNICODE, True) + extended_data = kwargs.get(ATTR_DATA) + _LOGGER.debug("Extended data:%s", extended_data) + + if extended_data is None: + is_unicode = True + else: + is_unicode = extended_data.get(CONF_UNICODE, True) + smsinfo = { "Class": -1, "Unicode": is_unicode, From ef30ebd9e180b383400089ab84b27864e128fae5 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sun, 25 Sep 2022 07:32:59 -0400 Subject: [PATCH 699/955] Stop ignoring test coverage in zwave_js (#79049) --- .coveragerc | 2 -- 1 file changed, 2 deletions(-) diff --git a/.coveragerc b/.coveragerc index 9d8d87b1312..8bb6f3e5873 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1583,8 +1583,6 @@ omit = homeassistant/components/zhong_hong/climate.py homeassistant/components/ziggo_mediabox_xl/media_player.py homeassistant/components/zoneminder/* - homeassistant/components/zwave_js/discovery.py - homeassistant/components/zwave_js/sensor.py homeassistant/components/zwave_me/__init__.py homeassistant/components/zwave_me/binary_sensor.py homeassistant/components/zwave_me/button.py From 42bd66430593e202c95823b4c47e11c6715e5f7f Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sun, 25 Sep 2022 10:11:53 -0400 Subject: [PATCH 700/955] Add diagnostic sensor to Radarr (#79044) * Add diagnostic sensor to Radarr * coverage --- .coveragerc | 1 - homeassistant/components/radarr/__init__.py | 12 ++++----- .../components/radarr/coordinator.py | 9 +++---- homeassistant/components/radarr/sensor.py | 25 +++++++++++++------ tests/components/radarr/test_sensor.py | 23 ++++++++--------- 5 files changed, 38 insertions(+), 32 deletions(-) diff --git a/.coveragerc b/.coveragerc index 8bb6f3e5873..0a4b3803cd2 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1000,7 +1000,6 @@ omit = homeassistant/components/rachio/entity.py homeassistant/components/rachio/switch.py homeassistant/components/rachio/webhooks.py - homeassistant/components/radarr/sensor.py homeassistant/components/radio_browser/__init__.py homeassistant/components/radio_browser/media_source.py homeassistant/components/radiotherm/__init__.py diff --git a/homeassistant/components/radarr/__init__.py b/homeassistant/components/radarr/__init__.py index 467f95780e8..c4845f1ba30 100644 --- a/homeassistant/components/radarr/__init__.py +++ b/homeassistant/components/radarr/__init__.py @@ -7,6 +7,7 @@ from aiopyarr.radarr_client import RadarrClient from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( + ATTR_SW_VERSION, CONF_API_KEY, CONF_PLATFORM, CONF_URL, @@ -77,13 +78,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: "disk_space": DiskSpaceDataUpdateCoordinator(hass, host_configuration, radarr), "movie": MoviesDataUpdateCoordinator(hass, host_configuration, radarr), } - # Temporary, until we add diagnostic entities - _version = None for coordinator in coordinators.values(): await coordinator.async_config_entry_first_refresh() - if isinstance(coordinator, StatusDataUpdateCoordinator): - _version = coordinator.data - coordinator.system_version = _version hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinators await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @@ -105,11 +101,13 @@ class RadarrEntity(CoordinatorEntity[RadarrDataUpdateCoordinator]): @property def device_info(self) -> DeviceInfo: """Return device information about the Radarr instance.""" - return DeviceInfo( + device_info = DeviceInfo( configuration_url=self.coordinator.host_configuration.url, entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, self.coordinator.config_entry.entry_id)}, manufacturer=DEFAULT_NAME, name=self.coordinator.config_entry.title, - sw_version=self.coordinator.system_version, ) + if isinstance(self.coordinator, StatusDataUpdateCoordinator): + device_info[ATTR_SW_VERSION] = self.coordinator.data.version + return device_info diff --git a/homeassistant/components/radarr/coordinator.py b/homeassistant/components/radarr/coordinator.py index a66455c8cc3..a13637f84b1 100644 --- a/homeassistant/components/radarr/coordinator.py +++ b/homeassistant/components/radarr/coordinator.py @@ -5,7 +5,7 @@ from abc import abstractmethod from datetime import timedelta from typing import Generic, TypeVar, cast -from aiopyarr import RootFolder, exceptions +from aiopyarr import RootFolder, SystemStatus, exceptions from aiopyarr.models.host_configuration import PyArrHostConfiguration from aiopyarr.radarr_client import RadarrClient @@ -16,7 +16,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import DOMAIN, LOGGER -T = TypeVar("T", str, list[RootFolder], int) +T = TypeVar("T", SystemStatus, list[RootFolder], int) class RadarrDataUpdateCoordinator(DataUpdateCoordinator, Generic[T]): @@ -39,7 +39,6 @@ class RadarrDataUpdateCoordinator(DataUpdateCoordinator, Generic[T]): ) self.api_client = api_client self.host_configuration = host_configuration - self.system_version: str | None = None async def _async_update_data(self) -> T: """Get the latest data from Radarr.""" @@ -62,9 +61,9 @@ class RadarrDataUpdateCoordinator(DataUpdateCoordinator, Generic[T]): class StatusDataUpdateCoordinator(RadarrDataUpdateCoordinator): """Status update coordinator for Radarr.""" - async def _fetch_data(self) -> str: + async def _fetch_data(self) -> SystemStatus: """Fetch the data.""" - return (await self.api_client.async_get_system_status()).version + return await self.api_client.async_get_system_status() class DiskSpaceDataUpdateCoordinator(RadarrDataUpdateCoordinator): diff --git a/homeassistant/components/radarr/sensor.py b/homeassistant/components/radarr/sensor.py index ac678198cd7..6e11fb6d2bc 100644 --- a/homeassistant/components/radarr/sensor.py +++ b/homeassistant/components/radarr/sensor.py @@ -4,13 +4,15 @@ from __future__ import annotations from collections.abc import Callable from copy import deepcopy from dataclasses import dataclass +from datetime import timezone from typing import Generic -from aiopyarr import RootFolder +from aiopyarr import Diskspace, RootFolder import voluptuous as vol from homeassistant.components.sensor import ( PLATFORM_SCHEMA, + SensorDeviceClass, SensorEntity, SensorEntityDescription, ) @@ -28,6 +30,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType @@ -36,11 +39,11 @@ from .const import DEFAULT_NAME, DOMAIN from .coordinator import RadarrDataUpdateCoordinator, T -def get_space(coordinator: RadarrDataUpdateCoordinator, name: str) -> str: +def get_space(data: list[Diskspace], name: str) -> str: """Get space.""" space = [ mount.freeSpace / 1024 ** BYTE_SIZES.index(DATA_GIGABYTES) - for mount in coordinator.data + for mount in data if name in mount.path ] return f"{space[0]:.2f}" @@ -61,7 +64,7 @@ def get_modified_description( class RadarrSensorEntityDescriptionMixIn(Generic[T]): """Mixin for required keys.""" - value: Callable[[RadarrDataUpdateCoordinator[T], str], str] + value_fn: Callable[[T, str], str] @dataclass @@ -82,7 +85,7 @@ SENSOR_TYPES: dict[str, RadarrSensorEntityDescription] = { name="Disk space", native_unit_of_measurement=DATA_GIGABYTES, icon="mdi:harddisk", - value=get_space, + value_fn=get_space, description_fn=get_modified_description, ), "movie": RadarrSensorEntityDescription( @@ -91,7 +94,15 @@ SENSOR_TYPES: dict[str, RadarrSensorEntityDescription] = { native_unit_of_measurement="Movies", icon="mdi:television", entity_registry_enabled_default=False, - value=lambda coordinator, _: coordinator.data, + value_fn=lambda data, _: data, + ), + "status": RadarrSensorEntityDescription( + key="start_time", + name="Start time", + device_class=SensorDeviceClass.TIMESTAMP, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + value_fn=lambda data, _: data.startTime.replace(tzinfo=timezone.utc), ), } @@ -182,4 +193,4 @@ class RadarrSensor(RadarrEntity, SensorEntity): @property def native_value(self) -> StateType: """Return the state of the sensor.""" - return self.entity_description.value(self.coordinator, self.folder_name) + return self.entity_description.value_fn(self.coordinator.data, self.folder_name) diff --git a/tests/components/radarr/test_sensor.py b/tests/components/radarr/test_sensor.py index a95885f1b1b..a4caaaa6a54 100644 --- a/tests/components/radarr/test_sensor.py +++ b/tests/components/radarr/test_sensor.py @@ -1,33 +1,32 @@ """The tests for Radarr sensor platform.""" -from datetime import timedelta +from unittest.mock import AsyncMock -from homeassistant.components.radarr.sensor import SENSOR_TYPES -from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT +from homeassistant.components.sensor import SensorDeviceClass +from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT from homeassistant.core import HomeAssistant -import homeassistant.util.dt as dt_util from . import setup_integration -from tests.common import async_fire_time_changed from tests.test_util.aiohttp import AiohttpClientMocker -async def test_sensors(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker): +async def test_sensors( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + entity_registry_enabled_by_default: AsyncMock, +): """Test for successfully setting up the Radarr platform.""" - for description in SENSOR_TYPES.values(): - description.entity_registry_enabled_default = True await setup_integration(hass, aioclient_mock) - next_update = dt_util.utcnow() + timedelta(seconds=30) - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - state = hass.states.get("sensor.radarr_disk_space_downloads") assert state.state == "263.10" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "GB" state = hass.states.get("sensor.radarr_movies") assert state.state == "1" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "Movies" + state = hass.states.get("sensor.radarr_start_time") + assert state.state == "2020-09-01T23:50:20+00:00" + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP async def test_windows(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker): From bfd12730f2d523d571e4c54de457875a821c1fe8 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sun, 25 Sep 2022 20:08:56 +0200 Subject: [PATCH 701/955] Bump aiounifi to v35 (#79040) * Update imports Replace constants with enums * Import new request objects * Bump aiounifi to v35 --- homeassistant/components/unifi/__init__.py | 9 ++- homeassistant/components/unifi/const.py | 10 ++++ homeassistant/components/unifi/controller.py | 49 +++++++--------- .../components/unifi/device_tracker.py | 35 ++++-------- homeassistant/components/unifi/manifest.json | 2 +- homeassistant/components/unifi/services.py | 5 +- homeassistant/components/unifi/switch.py | 57 +++++++++++++------ homeassistant/components/unifi/update.py | 4 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/unifi/test_controller.py | 7 +-- 11 files changed, 98 insertions(+), 84 deletions(-) diff --git a/homeassistant/components/unifi/__init__.py b/homeassistant/components/unifi/__init__.py index 086bae8d8cf..84540f7bea4 100644 --- a/homeassistant/components/unifi/__init__.py +++ b/homeassistant/components/unifi/__init__.py @@ -9,8 +9,13 @@ from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.storage import Store from homeassistant.helpers.typing import ConfigType -from .const import CONF_CONTROLLER, DOMAIN as UNIFI_DOMAIN, UNIFI_WIRELESS_CLIENTS -from .controller import PLATFORMS, UniFiController, get_unifi_controller +from .const import ( + CONF_CONTROLLER, + DOMAIN as UNIFI_DOMAIN, + PLATFORMS, + UNIFI_WIRELESS_CLIENTS, +) +from .controller import UniFiController, get_unifi_controller from .errors import AuthenticationRequired, CannotConnect from .services import async_setup_services, async_unload_services diff --git a/homeassistant/components/unifi/const.py b/homeassistant/components/unifi/const.py index 406b8d23a18..e84e2795a71 100644 --- a/homeassistant/components/unifi/const.py +++ b/homeassistant/components/unifi/const.py @@ -1,9 +1,19 @@ """Constants for the UniFi Network integration.""" + import logging +from homeassistant.const import Platform + LOGGER = logging.getLogger(__package__) DOMAIN = "unifi" +PLATFORMS = [ + Platform.DEVICE_TRACKER, + Platform.SENSOR, + Platform.SWITCH, + Platform.UPDATE, +] + CONF_CONTROLLER = "controller" CONF_SITE_ID = "site" diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index 7446d6abbff..6ad8e416445 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -14,18 +14,9 @@ from aiounifi.controller import ( DATA_DPI_GROUP, DATA_DPI_GROUP_REMOVED, DATA_EVENT, - SIGNAL_CONNECTION_STATE, - SIGNAL_DATA, ) -from aiounifi.events import ( - ACCESS_POINT_CONNECTED, - GATEWAY_CONNECTED, - SWITCH_CONNECTED, - WIRED_CLIENT_CONNECTED, - WIRELESS_CLIENT_CONNECTED, - WIRELESS_GUEST_CONNECTED, -) -from aiounifi.websocket import STATE_DISCONNECTED, STATE_RUNNING +from aiounifi.models.event import EventKey +from aiounifi.websocket import WebsocketSignal, WebsocketState import async_timeout from homeassistant.config_entries import ConfigEntry @@ -74,6 +65,7 @@ from .const import ( DEFAULT_TRACK_WIRED_CLIENTS, DOMAIN as UNIFI_DOMAIN, LOGGER, + PLATFORMS, UNIFI_WIRELESS_CLIENTS, ) from .errors import AuthenticationRequired, CannotConnect @@ -81,17 +73,16 @@ from .switch import BLOCK_SWITCH, POE_SWITCH RETRY_TIMER = 15 CHECK_HEARTBEAT_INTERVAL = timedelta(seconds=1) -PLATFORMS = [Platform.DEVICE_TRACKER, Platform.SENSOR, Platform.SWITCH, Platform.UPDATE] CLIENT_CONNECTED = ( - WIRED_CLIENT_CONNECTED, - WIRELESS_CLIENT_CONNECTED, - WIRELESS_GUEST_CONNECTED, + EventKey.WIRED_CLIENT_CONNECTED, + EventKey.WIRELESS_CLIENT_CONNECTED, + EventKey.WIRELESS_GUEST_CONNECTED, ) DEVICE_CONNECTED = ( - ACCESS_POINT_CONNECTED, - GATEWAY_CONNECTED, - SWITCH_CONNECTED, + EventKey.ACCESS_POINT_CONNECTED, + EventKey.GATEWAY_CONNECTED, + EventKey.SWITCH_CONNECTED, ) @@ -204,15 +195,15 @@ class UniFiController: @callback def async_unifi_signalling_callback(self, signal, data): """Handle messages back from UniFi library.""" - if signal == SIGNAL_CONNECTION_STATE: + if signal == WebsocketSignal.CONNECTION_STATE: - if data == STATE_DISCONNECTED and self.available: + if data == WebsocketState.DISCONNECTED and self.available: LOGGER.warning("Lost connection to UniFi Network") - if (data == STATE_RUNNING and not self.available) or ( - data == STATE_DISCONNECTED and self.available + if (data == WebsocketState.RUNNING and not self.available) or ( + data == WebsocketState.DISCONNECTED and self.available ): - self.available = data == STATE_RUNNING + self.available = data == WebsocketState.RUNNING async_dispatcher_send(self.hass, self.signal_reachable) if not self.available: @@ -220,7 +211,7 @@ class UniFiController: else: LOGGER.info("Connected to UniFi Network") - elif signal == SIGNAL_DATA and data: + elif signal == WebsocketSignal.DATA and data: if DATA_EVENT in data: clients_connected = set() @@ -229,16 +220,16 @@ class UniFiController: for event in data[DATA_EVENT]: - if event.event in CLIENT_CONNECTED: + if event.key in CLIENT_CONNECTED: clients_connected.add(event.mac) - if not wireless_clients_connected and event.event in ( - WIRELESS_CLIENT_CONNECTED, - WIRELESS_GUEST_CONNECTED, + if not wireless_clients_connected and event.key in ( + EventKey.WIRELESS_CLIENT_CONNECTED, + EventKey.WIRELESS_GUEST_CONNECTED, ): wireless_clients_connected = True - elif event.event in DEVICE_CONNECTED: + elif event.key in DEVICE_CONNECTED: devices_connected.add(event.mac) if wireless_clients_connected: diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index fc9c41ce184..2ae7f2c394f 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -3,19 +3,8 @@ from datetime import timedelta import logging -from aiounifi.api import SOURCE_DATA, SOURCE_EVENT -from aiounifi.events import ( - ACCESS_POINT_UPGRADED, - GATEWAY_UPGRADED, - SWITCH_UPGRADED, - WIRED_CLIENT_CONNECTED, - WIRELESS_CLIENT_CONNECTED, - WIRELESS_CLIENT_ROAM, - WIRELESS_CLIENT_ROAMRADIO, - WIRELESS_GUEST_CONNECTED, - WIRELESS_GUEST_ROAM, - WIRELESS_GUEST_ROAMRADIO, -) +from aiounifi.interfaces.api_handlers import SOURCE_DATA, SOURCE_EVENT +from aiounifi.models.event import EventKey from homeassistant.components.device_tracker import DOMAIN, SourceType from homeassistant.components.device_tracker.config_entry import ScannerEntity @@ -60,16 +49,14 @@ CLIENT_STATIC_ATTRIBUTES = [ CLIENT_CONNECTED_ALL_ATTRIBUTES = CLIENT_CONNECTED_ATTRIBUTES + CLIENT_STATIC_ATTRIBUTES -DEVICE_UPGRADED = (ACCESS_POINT_UPGRADED, GATEWAY_UPGRADED, SWITCH_UPGRADED) - -WIRED_CONNECTION = (WIRED_CLIENT_CONNECTED,) +WIRED_CONNECTION = (EventKey.WIRED_CLIENT_CONNECTED,) WIRELESS_CONNECTION = ( - WIRELESS_CLIENT_CONNECTED, - WIRELESS_CLIENT_ROAM, - WIRELESS_CLIENT_ROAMRADIO, - WIRELESS_GUEST_CONNECTED, - WIRELESS_GUEST_ROAM, - WIRELESS_GUEST_ROAMRADIO, + EventKey.WIRELESS_CLIENT_CONNECTED, + EventKey.WIRELESS_CLIENT_ROAM, + EventKey.WIRELESS_CLIENT_ROAMRADIO, + EventKey.WIRELESS_GUEST_CONNECTED, + EventKey.WIRELESS_GUEST_ROAM, + EventKey.WIRELESS_GUEST_ROAMRADIO, ) @@ -233,8 +220,8 @@ class UniFiClientTracker(UniFiClientBase, ScannerEntity): and not self._only_listen_to_data_source ): - if (self.is_wired and self.client.event.event in WIRED_CONNECTION) or ( - not self.is_wired and self.client.event.event in WIRELESS_CONNECTION + if (self.is_wired and self.client.event.key in WIRED_CONNECTION) or ( + not self.is_wired and self.client.event.key in WIRELESS_CONNECTION ): self._is_connected = True self.schedule_update = False diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index 36186b6fed8..9e8a1ef28f3 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Network", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifi", - "requirements": ["aiounifi==34"], + "requirements": ["aiounifi==35"], "codeowners": ["@Kane610"], "quality_scale": "platinum", "ssdp": [ diff --git a/homeassistant/components/unifi/services.py b/homeassistant/components/unifi/services.py index fcde48528b3..4574a27016d 100644 --- a/homeassistant/components/unifi/services.py +++ b/homeassistant/components/unifi/services.py @@ -1,5 +1,6 @@ """UniFi Network services.""" +from aiounifi.models.client import ClientReconnectRequest, ClientRemoveRequest import voluptuous as vol from homeassistant.const import ATTR_DEVICE_ID @@ -77,7 +78,7 @@ async def async_reconnect_client(hass, data) -> None: ): continue - await controller.api.clients.reconnect(mac) + await controller.api.request(ClientReconnectRequest.create(mac)) async def async_remove_clients(hass, data) -> None: @@ -109,4 +110,4 @@ async def async_remove_clients(hass, data) -> None: clients_to_remove.append(client.mac) if clients_to_remove: - await controller.api.clients.remove_clients(macs=clients_to_remove) + await controller.api.request(ClientRemoveRequest.create(clients_to_remove)) diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index b9f23c31392..72083cdc219 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -8,13 +8,14 @@ Support for controlling deep packet inspection (DPI) restriction groups. import asyncio from typing import Any -from aiounifi.api import SOURCE_EVENT -from aiounifi.events import ( - WIRED_CLIENT_BLOCKED, - WIRED_CLIENT_UNBLOCKED, - WIRELESS_CLIENT_BLOCKED, - WIRELESS_CLIENT_UNBLOCKED, +from aiounifi.interfaces.api_handlers import SOURCE_EVENT +from aiounifi.models.client import ClientBlockRequest +from aiounifi.models.device import ( + DeviceSetOutletRelayRequest, + DeviceSetPoePortModeRequest, ) +from aiounifi.models.dpi_restriction_app import DPIRestrictionAppEnableRequest +from aiounifi.models.event import EventKey from homeassistant.components.switch import DOMAIN, SwitchDeviceClass, SwitchEntity from homeassistant.config_entries import ConfigEntry @@ -39,8 +40,8 @@ DPI_SWITCH = "dpi" POE_SWITCH = "poe" OUTLET_SWITCH = "outlet" -CLIENT_BLOCKED = (WIRED_CLIENT_BLOCKED, WIRELESS_CLIENT_BLOCKED) -CLIENT_UNBLOCKED = (WIRED_CLIENT_UNBLOCKED, WIRELESS_CLIENT_UNBLOCKED) +CLIENT_BLOCKED = (EventKey.WIRED_CLIENT_BLOCKED, EventKey.WIRELESS_CLIENT_BLOCKED) +CLIENT_UNBLOCKED = (EventKey.WIRED_CLIENT_UNBLOCKED, EventKey.WIRELESS_CLIENT_UNBLOCKED) async def async_setup_entry( @@ -272,11 +273,19 @@ class UniFiPOEClientSwitch(UniFiClient, SwitchEntity, RestoreEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Enable POE for client.""" - await self.device.set_port_poe_mode(self.client.switch_port, self.poe_mode) + 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.device.set_port_poe_mode(self.client.switch_port, "off") + await self.controller.api.request( + DeviceSetPoePortModeRequest.create( + self.device, self.client.switch_port, "off" + ) + ) @property def extra_state_attributes(self): @@ -324,9 +333,9 @@ class UniFiBlockClientSwitch(UniFiClient, SwitchEntity): """Update the clients state.""" if ( self.client.last_updated == SOURCE_EVENT - and self.client.event.event in CLIENT_BLOCKED + CLIENT_UNBLOCKED + and self.client.event.key in CLIENT_BLOCKED + CLIENT_UNBLOCKED ): - self._is_blocked = self.client.event.event in CLIENT_BLOCKED + self._is_blocked = self.client.event.key in CLIENT_BLOCKED super().async_update_callback() @@ -337,11 +346,15 @@ class UniFiBlockClientSwitch(UniFiClient, SwitchEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn on connectivity for client.""" - await self.controller.api.clients.unblock(self.client.mac) + await self.controller.api.request( + ClientBlockRequest.create(self.client.mac, False) + ) async def async_turn_off(self, **kwargs: Any) -> None: """Turn off connectivity for client.""" - await self.controller.api.clients.block(self.client.mac) + await self.controller.api.request( + ClientBlockRequest.create(self.client.mac, True) + ) @property def icon(self) -> str: @@ -449,7 +462,9 @@ class UniFiDPIRestrictionSwitch(UniFiBase, SwitchEntity): """Restrict access of apps related to DPI group.""" return await asyncio.gather( *[ - self.controller.api.dpi_apps.enable(app_id) + self.controller.api.request( + DPIRestrictionAppEnableRequest.create(app_id, True) + ) for app_id in self._item.dpiapp_ids ] ) @@ -458,7 +473,9 @@ class UniFiDPIRestrictionSwitch(UniFiBase, SwitchEntity): """Remove restriction of apps related to DPI group.""" return await asyncio.gather( *[ - self.controller.api.dpi_apps.disable(app_id) + self.controller.api.request( + DPIRestrictionAppEnableRequest.create(app_id, False) + ) for app_id in self._item.dpiapp_ids ] ) @@ -509,11 +526,15 @@ class UniFiOutletSwitch(UniFiBase, SwitchEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Enable outlet relay.""" - await self._item.set_outlet_relay_state(self._outlet_index, True) + await self.controller.api.request( + DeviceSetOutletRelayRequest.create(self._item, self._outlet_index, True) + ) async def async_turn_off(self, **kwargs: Any) -> None: """Disable outlet relay.""" - await self._item.set_outlet_relay_state(self._outlet_index, False) + await self.controller.api.request( + DeviceSetOutletRelayRequest.create(self._item, self._outlet_index, False) + ) @property def device_info(self) -> DeviceInfo: diff --git a/homeassistant/components/unifi/update.py b/homeassistant/components/unifi/update.py index 24967e043d9..ecfbe3549bf 100644 --- a/homeassistant/components/unifi/update.py +++ b/homeassistant/components/unifi/update.py @@ -4,6 +4,8 @@ from __future__ import annotations import logging from typing import Any +from aiounifi.models.device import DeviceUpgradeRequest + from homeassistant.components.update import ( DOMAIN, UpdateDeviceClass, @@ -136,4 +138,4 @@ class UniFiDeviceUpdateEntity(UniFiBase, UpdateEntity): self, version: str | None, backup: bool, **kwargs: Any ) -> None: """Install an update.""" - await self.controller.api.devices.upgrade(self.device.mac) + await self.controller.api.request(DeviceUpgradeRequest.create(self.device.mac)) diff --git a/requirements_all.txt b/requirements_all.txt index 9ce72ad763f..ffed2fdba6f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -276,7 +276,7 @@ aiosyncthing==0.5.1 aiotractive==0.5.4 # homeassistant.components.unifi -aiounifi==34 +aiounifi==35 # homeassistant.components.vlc_telnet aiovlc==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9850aab5c12..1b268270ff0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -251,7 +251,7 @@ aiosyncthing==0.5.1 aiotractive==0.5.4 # homeassistant.components.unifi -aiounifi==34 +aiounifi==35 # homeassistant.components.vlc_telnet aiovlc==0.1.0 diff --git a/tests/components/unifi/test_controller.py b/tests/components/unifi/test_controller.py index e420d031f46..5de99a3f434 100644 --- a/tests/components/unifi/test_controller.py +++ b/tests/components/unifi/test_controller.py @@ -25,13 +25,10 @@ from homeassistant.components.unifi.const import ( DEFAULT_TRACK_DEVICES, DEFAULT_TRACK_WIRED_CLIENTS, DOMAIN as UNIFI_DOMAIN, + PLATFORMS, UNIFI_WIRELESS_CLIENTS, ) -from homeassistant.components.unifi.controller import ( - PLATFORMS, - RETRY_TIMER, - get_unifi_controller, -) +from homeassistant.components.unifi.controller import RETRY_TIMER, get_unifi_controller from homeassistant.components.unifi.errors import AuthenticationRequired, CannotConnect from homeassistant.const import ( CONF_HOST, From 0cc03c37bbb8d2920b3c8e4577fa3c1010033feb Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 25 Sep 2022 15:19:06 -0400 Subject: [PATCH 702/955] Pin pyOpenSSL to 22.0.0 (#79066) --- homeassistant/package_constraints.txt | 3 +++ script/gen_requirements_all.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index aac5df14883..4198b4ee62b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -132,3 +132,6 @@ iso4217!=1.10.20220401 # Pandas 1.4.4 has issues with wheels om armhf + Py3.10 pandas==1.4.3 + +# pyopenssl 22.1.0 requires pycryptography > 38 and we have 37 +pyopenssl==22.0.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index d0eb830f088..eec89d4210c 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -142,6 +142,9 @@ iso4217!=1.10.20220401 # Pandas 1.4.4 has issues with wheels om armhf + Py3.10 pandas==1.4.3 + +# pyopenssl 22.1.0 requires pycryptography > 38 and we have 37 +pyopenssl==22.0.0 """ IGNORE_PRE_COMMIT_HOOK_ID = ( From f41b69e19e4c13cc8a865f380a2ca020d9dc2750 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 25 Sep 2022 17:39:42 -0400 Subject: [PATCH 703/955] Bump cryptography to 38 (#79067) --- homeassistant/components/cloud/manifest.json | 2 +- homeassistant/package_constraints.txt | 7 ++----- pyproject.toml | 2 +- requirements.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- script/gen_requirements_all.py | 3 --- 7 files changed, 7 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index 02ffa0a4775..3a6d942f5ea 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -2,7 +2,7 @@ "domain": "cloud", "name": "Home Assistant Cloud", "documentation": "https://www.home-assistant.io/integrations/cloud", - "requirements": ["hass-nabucasa==0.55.0"], + "requirements": ["hass-nabucasa==0.56.0"], "dependencies": ["http", "webhook"], "after_dependencies": ["google_assistant", "alexa"], "codeowners": ["@home-assistant/cloud"], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 4198b4ee62b..b285f568824 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,10 +16,10 @@ bluetooth-adapters==0.5.1 bluetooth-auto-recovery==0.3.3 certifi>=2021.5.30 ciso8601==2.2.0 -cryptography==37.0.4 +cryptography==38.0.1 dbus-fast==1.13.0 fnvhash==0.1.0 -hass-nabucasa==0.55.0 +hass-nabucasa==0.56.0 home-assistant-bluetooth==1.3.0 home-assistant-frontend==20220907.2 httpx==0.23.0 @@ -132,6 +132,3 @@ iso4217!=1.10.20220401 # Pandas 1.4.4 has issues with wheels om armhf + Py3.10 pandas==1.4.3 - -# pyopenssl 22.1.0 requires pycryptography > 38 and we have 37 -pyopenssl==22.0.0 diff --git a/pyproject.toml b/pyproject.toml index 8381098badd..c76b015c84a 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==37.0.4", + "cryptography==38.0.1", "orjson==3.7.11", "pip>=21.0,<22.3", "python-slugify==4.0.1", diff --git a/requirements.txt b/requirements.txt index 2f36b947bca..28d3c11081b 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==37.0.4 +cryptography==38.0.1 orjson==3.7.11 pip>=21.0,<22.3 python-slugify==4.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index ffed2fdba6f..894ca325b44 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -829,7 +829,7 @@ habitipy==0.2.0 hangups==0.4.18 # homeassistant.components.cloud -hass-nabucasa==0.55.0 +hass-nabucasa==0.56.0 # homeassistant.components.splunk hass_splunk==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1b268270ff0..9d771174d23 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -615,7 +615,7 @@ habitipy==0.2.0 hangups==0.4.18 # homeassistant.components.cloud -hass-nabucasa==0.55.0 +hass-nabucasa==0.56.0 # homeassistant.components.tasmota hatasmota==0.6.1 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index eec89d4210c..d0eb830f088 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -142,9 +142,6 @@ iso4217!=1.10.20220401 # Pandas 1.4.4 has issues with wheels om armhf + Py3.10 pandas==1.4.3 - -# pyopenssl 22.1.0 requires pycryptography > 38 and we have 37 -pyopenssl==22.0.0 """ IGNORE_PRE_COMMIT_HOOK_ID = ( From b820fed11a027e64c9f3b024645920d5b9797630 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sun, 25 Sep 2022 17:48:25 -0400 Subject: [PATCH 704/955] Fix Radarr import (#79037) --- homeassistant/components/radarr/sensor.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/radarr/sensor.py b/homeassistant/components/radarr/sensor.py index 6e11fb6d2bc..5a716c9d263 100644 --- a/homeassistant/components/radarr/sensor.py +++ b/homeassistant/components/radarr/sensor.py @@ -106,8 +106,6 @@ SENSOR_TYPES: dict[str, RadarrSensorEntityDescription] = { ), } -SENSOR_KEYS: list[str] = [description.key for description in SENSOR_TYPES.values()] - BYTE_SIZES = [ DATA_BYTES, DATA_KILOBYTES, @@ -122,7 +120,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_HOST, default="localhost"): cv.string, vol.Optional("include_paths", default=[]): cv.ensure_list, vol.Optional(CONF_MONITORED_CONDITIONS, default=["movies"]): vol.All( - cv.ensure_list, [vol.In(SENSOR_KEYS)] + cv.ensure_list ), vol.Optional(CONF_PORT, default=7878): cv.port, vol.Optional(CONF_SSL, default=False): cv.boolean, From b70027aec1b0d9cb9dcccb117c8fc55427319969 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sun, 25 Sep 2022 17:50:09 -0400 Subject: [PATCH 705/955] Add binary sensor to Radarr (#79043) * Add binary sensor to Radarr * uno mas --- homeassistant/components/radarr/__init__.py | 17 +++++++- .../components/radarr/binary_sensor.py | 41 +++++++++++++++++++ homeassistant/components/radarr/const.py | 7 ++++ .../components/radarr/coordinator.py | 12 +++++- homeassistant/components/radarr/sensor.py | 7 +--- tests/components/radarr/__init__.py | 6 +++ tests/components/radarr/fixtures/health.json | 8 ++++ tests/components/radarr/test_binary_sensor.py | 17 ++++++++ 8 files changed, 106 insertions(+), 9 deletions(-) create mode 100644 homeassistant/components/radarr/binary_sensor.py create mode 100644 tests/components/radarr/fixtures/health.json create mode 100644 tests/components/radarr/test_binary_sensor.py diff --git a/homeassistant/components/radarr/__init__.py b/homeassistant/components/radarr/__init__.py index c4845f1ba30..a36929d7435 100644 --- a/homeassistant/components/radarr/__init__.py +++ b/homeassistant/components/radarr/__init__.py @@ -17,7 +17,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity import DeviceInfo, EntityDescription from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -25,12 +25,13 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DEFAULT_NAME, DOMAIN from .coordinator import ( DiskSpaceDataUpdateCoordinator, + HealthDataUpdateCoordinator, MoviesDataUpdateCoordinator, RadarrDataUpdateCoordinator, StatusDataUpdateCoordinator, ) -PLATFORMS = [Platform.SENSOR] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @@ -76,6 +77,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinators: dict[str, RadarrDataUpdateCoordinator] = { "status": StatusDataUpdateCoordinator(hass, host_configuration, radarr), "disk_space": DiskSpaceDataUpdateCoordinator(hass, host_configuration, radarr), + "health": HealthDataUpdateCoordinator(hass, host_configuration, radarr), "movie": MoviesDataUpdateCoordinator(hass, host_configuration, radarr), } for coordinator in coordinators.values(): @@ -98,6 +100,17 @@ class RadarrEntity(CoordinatorEntity[RadarrDataUpdateCoordinator]): coordinator: RadarrDataUpdateCoordinator + def __init__( + self, + coordinator: RadarrDataUpdateCoordinator, + description: EntityDescription, + ) -> None: + """Create Radarr entity.""" + super().__init__(coordinator) + self.entity_description = description + self._attr_name = f"{DEFAULT_NAME} {description.name}" + self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{description.key}" + @property def device_info(self) -> DeviceInfo: """Return device information about the Radarr instance.""" diff --git a/homeassistant/components/radarr/binary_sensor.py b/homeassistant/components/radarr/binary_sensor.py new file mode 100644 index 00000000000..2a1a729e6f4 --- /dev/null +++ b/homeassistant/components/radarr/binary_sensor.py @@ -0,0 +1,41 @@ +"""Support for Radarr binary sensors.""" +from __future__ import annotations + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +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 RadarrEntity +from .const import DOMAIN, HEALTH_ISSUES + +BINARY_SENSOR_TYPE = BinarySensorEntityDescription( + key="health", + name="Health", + entity_category=EntityCategory.DIAGNOSTIC, + device_class=BinarySensorDeviceClass.PROBLEM, +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Radarr sensors based on a config entry.""" + coordinator = hass.data[DOMAIN][entry.entry_id]["health"] + async_add_entities([RadarrBinarySensor(coordinator, BINARY_SENSOR_TYPE)]) + + +class RadarrBinarySensor(RadarrEntity, BinarySensorEntity): + """Implementation of a Radarr binary sensor.""" + + @property + def is_on(self) -> bool: + """Return True if the entity is on.""" + return any(report.source in HEALTH_ISSUES for report in self.coordinator.data) diff --git a/homeassistant/components/radarr/const.py b/homeassistant/components/radarr/const.py index b3320cf63a4..b77e134ca34 100644 --- a/homeassistant/components/radarr/const.py +++ b/homeassistant/components/radarr/const.py @@ -8,4 +8,11 @@ DOMAIN: Final = "radarr" DEFAULT_NAME = "Radarr" DEFAULT_URL = "http://127.0.0.1:7878" +HEALTH_ISSUES = ( + "DownloadClientCheck", + "DownloadClientStatusCheck", + "IndexerRssCheck", + "IndexerSearchCheck", +) + LOGGER = logging.getLogger(__package__) diff --git a/homeassistant/components/radarr/coordinator.py b/homeassistant/components/radarr/coordinator.py index a13637f84b1..06ea32e790f 100644 --- a/homeassistant/components/radarr/coordinator.py +++ b/homeassistant/components/radarr/coordinator.py @@ -5,7 +5,7 @@ from abc import abstractmethod from datetime import timedelta from typing import Generic, TypeVar, cast -from aiopyarr import RootFolder, SystemStatus, exceptions +from aiopyarr import Health, RootFolder, SystemStatus, exceptions from aiopyarr.models.host_configuration import PyArrHostConfiguration from aiopyarr.radarr_client import RadarrClient @@ -16,7 +16,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import DOMAIN, LOGGER -T = TypeVar("T", SystemStatus, list[RootFolder], int) +T = TypeVar("T", SystemStatus, list[RootFolder], list[Health], int) class RadarrDataUpdateCoordinator(DataUpdateCoordinator, Generic[T]): @@ -74,6 +74,14 @@ class DiskSpaceDataUpdateCoordinator(RadarrDataUpdateCoordinator): return cast(list, await self.api_client.async_get_root_folders()) +class HealthDataUpdateCoordinator(RadarrDataUpdateCoordinator): + """Health update coordinator.""" + + async def _fetch_data(self) -> list[Health]: + """Fetch the health data.""" + return await self.api_client.async_get_failed_health_checks() + + class MoviesDataUpdateCoordinator(RadarrDataUpdateCoordinator): """Movies update coordinator.""" diff --git a/homeassistant/components/radarr/sensor.py b/homeassistant/components/radarr/sensor.py index 5a716c9d263..e424844c602 100644 --- a/homeassistant/components/radarr/sensor.py +++ b/homeassistant/components/radarr/sensor.py @@ -35,7 +35,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType from . import RadarrEntity -from .const import DEFAULT_NAME, DOMAIN +from .const import DOMAIN from .coordinator import RadarrDataUpdateCoordinator, T @@ -182,10 +182,7 @@ class RadarrSensor(RadarrEntity, SensorEntity): folder_name: str = "", ) -> None: """Create Radarr entity.""" - super().__init__(coordinator) - self.entity_description = description - self._attr_name = f"{DEFAULT_NAME} {description.name}" - self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{description.key}" + super().__init__(coordinator, description) self.folder_name = folder_name @property diff --git a/tests/components/radarr/__init__.py b/tests/components/radarr/__init__.py index c631291b37b..639d548e4be 100644 --- a/tests/components/radarr/__init__.py +++ b/tests/components/radarr/__init__.py @@ -82,6 +82,12 @@ def mock_connection( headers={"Content-Type": CONTENT_TYPE_JSON}, ) + aioclient_mock.get( + f"{url}/api/v3/health", + text=load_fixture("radarr/health.json"), + headers={"Content-Type": CONTENT_TYPE_JSON}, + ) + if windows: aioclient_mock.get( f"{url}/api/v3/rootfolder", diff --git a/tests/components/radarr/fixtures/health.json b/tests/components/radarr/fixtures/health.json new file mode 100644 index 00000000000..6cb6adbab1a --- /dev/null +++ b/tests/components/radarr/fixtures/health.json @@ -0,0 +1,8 @@ +[ + { + "source": "DownloadClientStatusCheck", + "type": "error", + "message": "All download clients are unavailable due to failures", + "wikiUrl": "https://wiki.servarr.com/radarr/system#completed-failed-download-handling" + } +] diff --git a/tests/components/radarr/test_binary_sensor.py b/tests/components/radarr/test_binary_sensor.py new file mode 100644 index 00000000000..e53f7c33163 --- /dev/null +++ b/tests/components/radarr/test_binary_sensor.py @@ -0,0 +1,17 @@ +"""The tests for Radarr binary sensor platform.""" +from homeassistant.components.binary_sensor import BinarySensorDeviceClass +from homeassistant.const import ATTR_DEVICE_CLASS, STATE_ON +from homeassistant.core import HomeAssistant + +from . import setup_integration + +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def test_binary_sensors(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker): + """Test for binary sensor values.""" + await setup_integration(hass, aioclient_mock) + + state = hass.states.get("binary_sensor.radarr_health") + assert state.state == STATE_ON + assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.PROBLEM From 917cf674de2db2216681dfec3ef9d63df573ace8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 25 Sep 2022 12:08:28 -1000 Subject: [PATCH 706/955] Handle battery services that only report low battery in HomeKit Controller (#79072) --- .../homekit_controller/binary_sensor.py | 46 ++- .../components/homekit_controller/number.py | 2 +- .../components/homekit_controller/sensor.py | 10 + .../fixtures/netamo_smart_co_alarm.json | 362 ++++++++++++++++++ .../specific_devices/test_aqara_switch.py | 2 + .../specific_devices/test_arlo_baby.py | 2 + .../specific_devices/test_eve_degree.py | 1 + .../specific_devices/test_hue_bridge.py | 2 + .../test_netamo_smart_co_alarm.py | 50 +++ .../test_ryse_smart_bridge.py | 7 + .../homekit_controller/test_device_trigger.py | 6 +- 11 files changed, 485 insertions(+), 5 deletions(-) create mode 100644 tests/components/homekit_controller/fixtures/netamo_smart_co_alarm.json create mode 100644 tests/components/homekit_controller/specific_devices/test_netamo_smart_co_alarm.py diff --git a/homeassistant/components/homekit_controller/binary_sensor.py b/homeassistant/components/homekit_controller/binary_sensor.py index 11c81e7e251..c980e31b50c 100644 --- a/homeassistant/components/homekit_controller/binary_sensor.py +++ b/homeassistant/components/homekit_controller/binary_sensor.py @@ -10,9 +10,11 @@ from homeassistant.components.binary_sensor import ( ) 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 . import KNOWN_DEVICES +from .connection import HKDevice from .entity import HomeKitEntity @@ -106,6 +108,29 @@ class HomeKitLeakSensor(HomeKitEntity, BinarySensorEntity): return self.service.value(CharacteristicsTypes.LEAK_DETECTED) == 1 +class HomeKitBatteryLowSensor(HomeKitEntity, BinarySensorEntity): + """Representation of a Homekit battery low sensor.""" + + _attr_device_class = BinarySensorDeviceClass.BATTERY + _attr_entity_category = EntityCategory.DIAGNOSTIC + + def get_characteristic_types(self) -> list[str]: + """Define the homekit characteristics the entity is tracking.""" + return [CharacteristicsTypes.STATUS_LO_BATT] + + @property + def name(self) -> str: + """Return the name of the sensor.""" + if name := self.accessory.name: + return f"{name} Low Battery" + return "Low Battery" + + @property + def is_on(self) -> bool: + """Return true if low battery is detected from the binary sensor.""" + return self.service.value(CharacteristicsTypes.STATUS_LO_BATT) == 1 + + ENTITY_TYPES = { ServicesTypes.MOTION_SENSOR: HomeKitMotionSensor, ServicesTypes.CONTACT_SENSOR: HomeKitContactSensor, @@ -113,6 +138,17 @@ ENTITY_TYPES = { ServicesTypes.CARBON_MONOXIDE_SENSOR: HomeKitCarbonMonoxideSensor, ServicesTypes.OCCUPANCY_SENSOR: HomeKitOccupancySensor, ServicesTypes.LEAK_SENSOR: HomeKitLeakSensor, + ServicesTypes.BATTERY_SERVICE: HomeKitBatteryLowSensor, +} + +# Only create the entity if it has the required characteristic +REQUIRED_CHAR_BY_TYPE = { + ServicesTypes.BATTERY_SERVICE: CharacteristicsTypes.STATUS_LO_BATT, +} +# Reject the service as another platform can represent it better +# if it has a specific characteristic +REJECT_CHAR_BY_TYPE = { + ServicesTypes.BATTERY_SERVICE: CharacteristicsTypes.BATTERY_LEVEL, } @@ -123,12 +159,20 @@ async def async_setup_entry( ) -> None: """Set up Homekit lighting.""" hkid = config_entry.data["AccessoryPairingID"] - conn = hass.data[KNOWN_DEVICES][hkid] + conn: HKDevice = hass.data[KNOWN_DEVICES][hkid] @callback def async_add_service(service: Service) -> bool: if not (entity_class := ENTITY_TYPES.get(service.type)): return False + if ( + required_char := REQUIRED_CHAR_BY_TYPE.get(service.type) + ) and not service.has(required_char): + return False + if (reject_char := REJECT_CHAR_BY_TYPE.get(service.type)) and service.has( + reject_char + ): + return False info = {"aid": service.accessory.aid, "iid": service.iid} async_add_entities([entity_class(conn, info)], True) return True diff --git a/homeassistant/components/homekit_controller/number.py b/homeassistant/components/homekit_controller/number.py index 29cad299902..6347ccb2a56 100644 --- a/homeassistant/components/homekit_controller/number.py +++ b/homeassistant/components/homekit_controller/number.py @@ -60,7 +60,7 @@ async def async_setup_entry( ) -> None: """Set up Homekit numbers.""" hkid = config_entry.data["AccessoryPairingID"] - conn = hass.data[KNOWN_DEVICES][hkid] + conn: HKDevice = hass.data[KNOWN_DEVICES][hkid] @callback def async_add_characteristic(char: Characteristic) -> bool: diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py index 04856a60347..3195cf6ee50 100644 --- a/homeassistant/components/homekit_controller/sensor.py +++ b/homeassistant/components/homekit_controller/sensor.py @@ -410,6 +410,7 @@ class HomeKitBatterySensor(HomeKitSensor): _attr_device_class = SensorDeviceClass.BATTERY _attr_native_unit_of_measurement = PERCENTAGE + _attr_entity_category = EntityCategory.DIAGNOSTIC def get_characteristic_types(self) -> list[str]: """Define the homekit characteristics the entity is tracking.""" @@ -517,6 +518,11 @@ ENTITY_TYPES = { ServicesTypes.BATTERY_SERVICE: HomeKitBatterySensor, } +# Only create the entity if it has the required characteristic +REQUIRED_CHAR_BY_TYPE = { + ServicesTypes.BATTERY_SERVICE: CharacteristicsTypes.BATTERY_LEVEL, +} + async def async_setup_entry( hass: HomeAssistant, @@ -531,6 +537,10 @@ async def async_setup_entry( def async_add_service(service: Service) -> bool: if not (entity_class := ENTITY_TYPES.get(service.type)): return False + if ( + required_char := REQUIRED_CHAR_BY_TYPE.get(service.type) + ) and not service.has(required_char): + return False info = {"aid": service.accessory.aid, "iid": service.iid} async_add_entities([entity_class(conn, info)], True) return True diff --git a/tests/components/homekit_controller/fixtures/netamo_smart_co_alarm.json b/tests/components/homekit_controller/fixtures/netamo_smart_co_alarm.json new file mode 100644 index 00000000000..432fd948d3e --- /dev/null +++ b/tests/components/homekit_controller/fixtures/netamo_smart_co_alarm.json @@ -0,0 +1,362 @@ +[ + { + "aid": 1, + "services": [ + { + "iid": 1, + "type": "000000A2-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000037-0000-1000-8000-0026BB765291", + "iid": 19, + "perms": ["pr"], + "format": "string", + "value": "2.2.0", + "description": "Version", + "maxLen": 64 + }, + { + "type": "000000A5-0000-1000-8000-0026BB765291", + "iid": 18, + "perms": ["pr"], + "format": "data", + "value": "" + } + ] + }, + { + "iid": 4, + "type": "EA22EA53-6227-55EA-AC24-73ACF3EEA0E8", + "characteristics": [ + { + "type": "000000A5-0000-1000-8000-0026BB765291", + "iid": 340, + "perms": ["pr"], + "format": "data", + "value": "" + }, + { + "type": "00F44C18-042E-5C4E-9A4C-561D44DCD804", + "iid": 339, + "perms": ["pr", "hd"], + "format": "string", + "value": "g8d8a6c", + "maxLen": 64 + } + ] + }, + { + "iid": 7, + "type": "0000003E-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 2, + "perms": ["pr"], + "format": "string", + "value": "Smart CO Alarm", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000014-0000-1000-8000-0026BB765291", + "iid": 3, + "perms": ["pw"], + "format": "bool", + "description": "Identify" + }, + { + "type": "00000053-0000-1000-8000-0026BB765291", + "iid": 8, + "perms": ["pr"], + "format": "string", + "value": "0", + "description": "Hardware Revision", + "maxLen": 64 + }, + { + "type": "00000020-0000-1000-8000-0026BB765291", + "iid": 4, + "perms": ["pr"], + "format": "string", + "value": "Netatmo", + "description": "Manufacturer", + "maxLen": 64 + }, + { + "type": "00000030-0000-1000-8000-0026BB765291", + "iid": 6, + "perms": ["pr"], + "format": "string", + "value": "1234", + "description": "Serial Number", + "maxLen": 64 + }, + { + "type": "00000021-0000-1000-8000-0026BB765291", + "iid": 5, + "perms": ["pr"], + "format": "string", + "value": "Smart CO Alarm", + "description": "Model", + "maxLen": 64 + }, + { + "type": "00000052-0000-1000-8000-0026BB765291", + "iid": 7, + "perms": ["pr"], + "format": "string", + "value": "1.0.3", + "description": "Firmware Revision", + "maxLen": 64 + }, + { + "type": "34AB8811-AC7F-4340-BAC3-FD6A85F9943B", + "iid": 47, + "perms": ["pr", "hd"], + "format": "string", + "value": "4.1;Sep 27 2021 12:54:49", + "maxLen": 64 + }, + { + "type": "00000220-0000-1000-8000-0026BB765291", + "iid": 325, + "perms": ["pr", "hd"], + "format": "data", + "value": "fa7beb3a4566c1fb" + } + ] + }, + { + "iid": 17, + "type": "00000055-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "0000004C-0000-1000-8000-0026BB765291", + "iid": 203, + "perms": [], + "format": "data", + "description": "Pair Setup" + }, + { + "type": "0000004F-0000-1000-8000-0026BB765291", + "iid": 205, + "perms": [], + "format": "uint8", + "description": "Pairing Features" + }, + { + "type": "0000004E-0000-1000-8000-0026BB765291", + "iid": 204, + "perms": [], + "format": "data", + "description": "Pair Verify" + }, + { + "type": "00000050-0000-1000-8000-0026BB765291", + "iid": 206, + "perms": ["pr", "pw"], + "format": "data", + "value": null, + "description": "Pairing Pairings" + } + ] + }, + { + "iid": 22, + "type": "0000007F-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000075-0000-1000-8000-0026BB765291", + "iid": 230, + "perms": ["pr", "ev"], + "format": "bool", + "value": true, + "description": "Status Active" + }, + { + "type": "00000077-0000-1000-8000-0026BB765291", + "iid": 231, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "description": "Status Fault", + "minValue": 0, + "maxValue": 1 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 42, + "perms": ["pr"], + "format": "string", + "value": "Carbon Monoxide Sensor", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000091-0000-1000-8000-0026BB765291", + "iid": 41, + "perms": ["pr", "ev"], + "format": "float", + "value": 0.0, + "description": "Carbon Monoxide Peak Level", + "minValue": 0.0, + "maxValue": 1000.0 + }, + { + "type": "00000019-4DDB-598F-A73F-006513F2DB6B", + "iid": 333, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "minValue": 0, + "maxValue": 3 + }, + { + "type": "00000090-0000-1000-8000-0026BB765291", + "iid": 40, + "perms": ["pr", "ev"], + "format": "float", + "value": 0.0, + "description": "Carbon Monoxide Level", + "minValue": 0.0, + "maxValue": 1000.0 + }, + { + "type": "0000000C-4DDB-598F-A73F-006513F2DB6B", + "iid": 326, + "perms": ["pr", "ev"], + "format": "bool", + "value": false + }, + { + "type": "0000007A-0000-1000-8000-0026BB765291", + "iid": 45, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "description": "Status Tampered", + "minValue": 0, + "maxValue": 1 + }, + { + "type": "00000001-4DDB-598F-A73F-006513F2DB6B", + "iid": 327, + "perms": ["pr", "pw", "ev"], + "format": "bool", + "value": false + }, + { + "type": "00000012-4DDB-598F-A73F-006513F2DB6B", + "iid": 328, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "minValue": 0, + "maxValue": 3 + }, + { + "type": "000000A5-0000-1000-8000-0026BB765291", + "iid": 43, + "perms": ["pr"], + "format": "data", + "value": "" + }, + { + "type": "00000069-0000-1000-8000-0026BB765291", + "iid": 229, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "description": "Carbon Monoxide Detected", + "minValue": 0, + "maxValue": 1 + } + ] + }, + { + "iid": 35, + "type": "00001801-0000-1000-8000-00805F9B34FB", + "characteristics": [] + }, + { + "iid": 36, + "type": "00000096-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 336, + "perms": ["pr"], + "format": "string", + "value": "Battery Service", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000079-0000-1000-8000-0026BB765291", + "iid": 337, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "description": "Status Low Battery", + "minValue": 0, + "maxValue": 1 + }, + { + "type": "00000002-4DDB-598F-A73F-006513F2DB6B", + "iid": 335, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "minValue": 0, + "maxValue": 4 + } + ] + }, + { + "iid": 40, + "type": "00000004-4DDB-598F-A73F-006513F2DB6B", + "characteristics": [ + { + "type": "00000007-4DDB-598F-A73F-006513F2DB6B", + "iid": 187, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "minValue": 0, + "maxValue": 14 + }, + { + "type": "000000A5-0000-1000-8000-0026BB765291", + "iid": 183, + "perms": ["pr"], + "format": "data", + "value": "" + }, + { + "type": "00000009-4DDB-598F-A73F-006513F2DB6B", + "iid": 184, + "perms": ["pw", "hd"], + "format": "uint8", + "minValue": 0, + "maxValue": 1 + }, + { + "type": "00000006-4DDB-598F-A73F-006513F2DB6B", + "iid": 188, + "perms": ["pr"], + "format": "data", + "value": "0000000000000000" + }, + { + "type": "0000000A-4DDB-598F-A73F-006513F2DB6B", + "iid": 324, + "perms": ["pr", "ev"], + "format": "uint32", + "value": 3 + } + ] + } + ] + } +] diff --git a/tests/components/homekit_controller/specific_devices/test_aqara_switch.py b/tests/components/homekit_controller/specific_devices/test_aqara_switch.py index f3039e754d8..793fb49af5b 100644 --- a/tests/components/homekit_controller/specific_devices/test_aqara_switch.py +++ b/tests/components/homekit_controller/specific_devices/test_aqara_switch.py @@ -9,6 +9,7 @@ https://github.com/home-assistant/core/pull/39090 from homeassistant.components.sensor import SensorStateClass from homeassistant.const import PERCENTAGE +from homeassistant.helpers.entity import EntityCategory from ..common import ( HUB_TEST_ACCESSORY_ID, @@ -43,6 +44,7 @@ async def test_aqara_switch_setup(hass): friendly_name="Programmable Switch Battery Sensor", unique_id="homekit-111a1111a1a111-5", capabilities={"state_class": SensorStateClass.MEASUREMENT}, + entity_category=EntityCategory.DIAGNOSTIC, unit_of_measurement=PERCENTAGE, state="100", ), diff --git a/tests/components/homekit_controller/specific_devices/test_arlo_baby.py b/tests/components/homekit_controller/specific_devices/test_arlo_baby.py index 9a88e992a85..1b2b4bda3d6 100644 --- a/tests/components/homekit_controller/specific_devices/test_arlo_baby.py +++ b/tests/components/homekit_controller/specific_devices/test_arlo_baby.py @@ -2,6 +2,7 @@ from homeassistant.components.sensor import SensorStateClass from homeassistant.const import PERCENTAGE, TEMP_CELSIUS +from homeassistant.helpers.entity import EntityCategory from ..common import ( HUB_TEST_ACCESSORY_ID, @@ -46,6 +47,7 @@ async def test_arlo_baby_setup(hass): entity_id="sensor.arlobabya0_battery", unique_id="homekit-00A0000000000-700", friendly_name="ArloBabyA0 Battery", + entity_category=EntityCategory.DIAGNOSTIC, capabilities={"state_class": SensorStateClass.MEASUREMENT}, unit_of_measurement=PERCENTAGE, state="82", diff --git a/tests/components/homekit_controller/specific_devices/test_eve_degree.py b/tests/components/homekit_controller/specific_devices/test_eve_degree.py index c3ca4b22f1a..eab2de030db 100644 --- a/tests/components/homekit_controller/specific_devices/test_eve_degree.py +++ b/tests/components/homekit_controller/specific_devices/test_eve_degree.py @@ -60,6 +60,7 @@ async def test_eve_degree_setup(hass): entity_id="sensor.eve_degree_aa11_battery", unique_id="homekit-AA00A0A00000-17", friendly_name="Eve Degree AA11 Battery", + entity_category=EntityCategory.DIAGNOSTIC, capabilities={"state_class": SensorStateClass.MEASUREMENT}, unit_of_measurement=PERCENTAGE, state="65", diff --git a/tests/components/homekit_controller/specific_devices/test_hue_bridge.py b/tests/components/homekit_controller/specific_devices/test_hue_bridge.py index ac6d8bdafa6..1092bb4f82c 100644 --- a/tests/components/homekit_controller/specific_devices/test_hue_bridge.py +++ b/tests/components/homekit_controller/specific_devices/test_hue_bridge.py @@ -2,6 +2,7 @@ from homeassistant.components.sensor import SensorStateClass from homeassistant.const import PERCENTAGE +from homeassistant.helpers.entity import EntityCategory from ..common import ( HUB_TEST_ACCESSORY_ID, @@ -44,6 +45,7 @@ async def test_hue_bridge_setup(hass): entity_id="sensor.hue_dimmer_switch_battery", capabilities={"state_class": SensorStateClass.MEASUREMENT}, friendly_name="Hue dimmer switch battery", + entity_category=EntityCategory.DIAGNOSTIC, unique_id="homekit-6623462389072572-644245094400", unit_of_measurement=PERCENTAGE, state="100", diff --git a/tests/components/homekit_controller/specific_devices/test_netamo_smart_co_alarm.py b/tests/components/homekit_controller/specific_devices/test_netamo_smart_co_alarm.py new file mode 100644 index 00000000000..b2c83a005f8 --- /dev/null +++ b/tests/components/homekit_controller/specific_devices/test_netamo_smart_co_alarm.py @@ -0,0 +1,50 @@ +""" +Regression tests for Netamo Smart CO Alarm. + +https://github.com/home-assistant/core/issues/78903 +""" +from homeassistant.helpers.entity import EntityCategory + +from ..common import ( + HUB_TEST_ACCESSORY_ID, + DeviceTestInfo, + EntityTestInfo, + assert_devices_and_entities_created, + setup_accessories_from_file, + setup_test_accessories, +) + + +async def test_netamo_smart_co_alarm_setup(hass): + """Test that a Netamo Smart CO Alarm can be correctly setup in HA.""" + accessories = await setup_accessories_from_file(hass, "netamo_smart_co_alarm.json") + await setup_test_accessories(hass, accessories) + + await assert_devices_and_entities_created( + hass, + DeviceTestInfo( + unique_id=HUB_TEST_ACCESSORY_ID, + name="Smart CO Alarm", + model="Smart CO Alarm", + manufacturer="Netatmo", + sw_version="1.0.3", + hw_version="0", + serial_number="1234", + devices=[], + entities=[ + EntityTestInfo( + entity_id="binary_sensor.smart_co_alarm_carbon_monoxide_sensor", + friendly_name="Smart CO Alarm Carbon Monoxide Sensor", + unique_id="homekit-1234-22", + state="off", + ), + EntityTestInfo( + entity_id="binary_sensor.smart_co_alarm_low_battery", + friendly_name="Smart CO Alarm Low Battery", + entity_category=EntityCategory.DIAGNOSTIC, + unique_id="homekit-1234-36", + state="off", + ), + ], + ), + ) diff --git a/tests/components/homekit_controller/specific_devices/test_ryse_smart_bridge.py b/tests/components/homekit_controller/specific_devices/test_ryse_smart_bridge.py index aa9dee89be2..a0c84472429 100644 --- a/tests/components/homekit_controller/specific_devices/test_ryse_smart_bridge.py +++ b/tests/components/homekit_controller/specific_devices/test_ryse_smart_bridge.py @@ -3,6 +3,7 @@ from homeassistant.components.cover import CoverEntityFeature from homeassistant.components.sensor import SensorStateClass from homeassistant.const import PERCENTAGE +from homeassistant.helpers.entity import EntityCategory from ..common import ( HUB_TEST_ACCESSORY_ID, @@ -53,6 +54,7 @@ async def test_ryse_smart_bridge_setup(hass): EntityTestInfo( entity_id="sensor.master_bath_south_ryse_shade_battery", friendly_name="Master Bath South RYSE Shade Battery", + entity_category=EntityCategory.DIAGNOSTIC, capabilities={"state_class": SensorStateClass.MEASUREMENT}, unique_id="homekit-00:00:00:00:00:00-2-64", unit_of_measurement=PERCENTAGE, @@ -80,6 +82,7 @@ async def test_ryse_smart_bridge_setup(hass): EntityTestInfo( entity_id="sensor.ryse_smartshade_ryse_shade_battery", friendly_name="RYSE SmartShade RYSE Shade Battery", + entity_category=EntityCategory.DIAGNOSTIC, capabilities={"state_class": SensorStateClass.MEASUREMENT}, unique_id="homekit-00:00:00:00:00:00-3-64", unit_of_measurement=PERCENTAGE, @@ -130,6 +133,7 @@ async def test_ryse_smart_bridge_four_shades_setup(hass): EntityTestInfo( entity_id="sensor.lr_left_ryse_shade_battery", friendly_name="LR Left RYSE Shade Battery", + entity_category=EntityCategory.DIAGNOSTIC, capabilities={"state_class": SensorStateClass.MEASUREMENT}, unique_id="homekit-00:00:00:00:00:00-2-64", unit_of_measurement=PERCENTAGE, @@ -157,6 +161,7 @@ async def test_ryse_smart_bridge_four_shades_setup(hass): EntityTestInfo( entity_id="sensor.lr_right_ryse_shade_battery", friendly_name="LR Right RYSE Shade Battery", + entity_category=EntityCategory.DIAGNOSTIC, capabilities={"state_class": SensorStateClass.MEASUREMENT}, unique_id="homekit-00:00:00:00:00:00-3-64", unit_of_measurement=PERCENTAGE, @@ -184,6 +189,7 @@ async def test_ryse_smart_bridge_four_shades_setup(hass): EntityTestInfo( entity_id="sensor.br_left_ryse_shade_battery", friendly_name="BR Left RYSE Shade Battery", + entity_category=EntityCategory.DIAGNOSTIC, capabilities={"state_class": SensorStateClass.MEASUREMENT}, unique_id="homekit-00:00:00:00:00:00-4-64", unit_of_measurement=PERCENTAGE, @@ -210,6 +216,7 @@ async def test_ryse_smart_bridge_four_shades_setup(hass): ), EntityTestInfo( entity_id="sensor.rzss_ryse_shade_battery", + entity_category=EntityCategory.DIAGNOSTIC, capabilities={"state_class": SensorStateClass.MEASUREMENT}, friendly_name="RZSS RYSE Shade Battery", unique_id="homekit-00:00:00:00:00:00-5-64", diff --git a/tests/components/homekit_controller/test_device_trigger.py b/tests/components/homekit_controller/test_device_trigger.py index 22404063663..a09525d9dec 100644 --- a/tests/components/homekit_controller/test_device_trigger.py +++ b/tests/components/homekit_controller/test_device_trigger.py @@ -98,7 +98,7 @@ async def test_enumerate_remote(hass, utcnow): "entity_id": "sensor.testdevice_battery", "platform": "device", "type": "battery_level", - "metadata": {"secondary": False}, + "metadata": {"secondary": True}, }, { "device_id": device.id, @@ -146,7 +146,7 @@ async def test_enumerate_button(hass, utcnow): "entity_id": "sensor.testdevice_battery", "platform": "device", "type": "battery_level", - "metadata": {"secondary": False}, + "metadata": {"secondary": True}, }, { "device_id": device.id, @@ -193,7 +193,7 @@ async def test_enumerate_doorbell(hass, utcnow): "entity_id": "sensor.testdevice_battery", "platform": "device", "type": "battery_level", - "metadata": {"secondary": False}, + "metadata": {"secondary": True}, }, { "device_id": device.id, From c2209111b29e34ddad5b8dd8bf2b1aa42008d792 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sun, 25 Sep 2022 19:26:50 -0400 Subject: [PATCH 707/955] Migrate Radarr to new entity naming style (#79042) Move Radarr to new naming scheme --- homeassistant/components/radarr/__init__.py | 2 +- tests/components/radarr/test_binary_sensor.py | 2 +- tests/components/radarr/test_sensor.py | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/radarr/__init__.py b/homeassistant/components/radarr/__init__.py index a36929d7435..c590a419c78 100644 --- a/homeassistant/components/radarr/__init__.py +++ b/homeassistant/components/radarr/__init__.py @@ -98,6 +98,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class RadarrEntity(CoordinatorEntity[RadarrDataUpdateCoordinator]): """Defines a base Radarr entity.""" + _attr_has_entity_name = True coordinator: RadarrDataUpdateCoordinator def __init__( @@ -108,7 +109,6 @@ class RadarrEntity(CoordinatorEntity[RadarrDataUpdateCoordinator]): """Create Radarr entity.""" super().__init__(coordinator) self.entity_description = description - self._attr_name = f"{DEFAULT_NAME} {description.name}" self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{description.key}" @property diff --git a/tests/components/radarr/test_binary_sensor.py b/tests/components/radarr/test_binary_sensor.py index e53f7c33163..ed82c795b8a 100644 --- a/tests/components/radarr/test_binary_sensor.py +++ b/tests/components/radarr/test_binary_sensor.py @@ -12,6 +12,6 @@ async def test_binary_sensors(hass: HomeAssistant, aioclient_mock: AiohttpClient """Test for binary sensor values.""" await setup_integration(hass, aioclient_mock) - state = hass.states.get("binary_sensor.radarr_health") + state = hass.states.get("binary_sensor.mock_title_health") assert state.state == STATE_ON assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.PROBLEM diff --git a/tests/components/radarr/test_sensor.py b/tests/components/radarr/test_sensor.py index a4caaaa6a54..9b330bdd8f3 100644 --- a/tests/components/radarr/test_sensor.py +++ b/tests/components/radarr/test_sensor.py @@ -18,13 +18,13 @@ async def test_sensors( """Test for successfully setting up the Radarr platform.""" await setup_integration(hass, aioclient_mock) - state = hass.states.get("sensor.radarr_disk_space_downloads") + state = hass.states.get("sensor.mock_title_disk_space_downloads") assert state.state == "263.10" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "GB" - state = hass.states.get("sensor.radarr_movies") + state = hass.states.get("sensor.mock_title_movies") assert state.state == "1" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "Movies" - state = hass.states.get("sensor.radarr_start_time") + state = hass.states.get("sensor.mock_title_start_time") assert state.state == "2020-09-01T23:50:20+00:00" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP @@ -33,5 +33,5 @@ async def test_windows(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker) """Test for successfully setting up the Radarr platform on Windows.""" await setup_integration(hass, aioclient_mock, windows=True) - state = hass.states.get("sensor.radarr_disk_space_tv") + state = hass.states.get("sensor.mock_title_disk_space_tv") assert state.state == "263.10" From 43c66b90df534ef92ffeeaf31659477d864ab0cc Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sun, 25 Sep 2022 19:47:38 -0400 Subject: [PATCH 708/955] Change Skybell color mode to RGB (#78078) --- homeassistant/components/skybell/light.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/skybell/light.py b/homeassistant/components/skybell/light.py index 2b3066a8827..311122c28e7 100644 --- a/homeassistant/components/skybell/light.py +++ b/homeassistant/components/skybell/light.py @@ -3,6 +3,8 @@ from __future__ import annotations from typing import Any +from aioskybell.helpers.const import BRIGHTNESS, RGB_COLOR + from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_RGB_COLOR, @@ -31,16 +33,16 @@ async def async_setup_entry( class SkybellLight(SkybellEntity, LightEntity): """A light implementation for Skybell devices.""" - _attr_supported_color_modes = {ColorMode.BRIGHTNESS, ColorMode.RGB} + _attr_color_mode = ColorMode.RGB + _attr_supported_color_modes = {ColorMode.RGB} async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the light.""" if ATTR_RGB_COLOR in kwargs: - rgb = kwargs[ATTR_RGB_COLOR] - await self._device.async_set_setting(ATTR_RGB_COLOR, rgb) + await self._device.async_set_setting(RGB_COLOR, kwargs[ATTR_RGB_COLOR]) if ATTR_BRIGHTNESS in kwargs: level = int((kwargs.get(ATTR_BRIGHTNESS, 0) * 100) / 255) - await self._device.async_set_setting(ATTR_BRIGHTNESS, level) + await self._device.async_set_setting(BRIGHTNESS, level) async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the light.""" @@ -55,3 +57,8 @@ class SkybellLight(SkybellEntity, LightEntity): def brightness(self) -> int: """Return the brightness of the light.""" return int((self._device.led_intensity * 255) / 100) + + @property + def rgb_color(self) -> tuple[int, int, int] | None: + """Return the rgb color value [int, int, int].""" + return self._device.led_rgb From 49f203c635c3d1a9a88914030f79fbfc97b5b768 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 25 Sep 2022 13:47:56 -1000 Subject: [PATCH 709/955] Add support for newer Magic Home sockets (#79074) --- homeassistant/components/flux_led/__init__.py | 7 +++++- tests/components/flux_led/__init__.py | 4 ++++ tests/components/flux_led/test_button.py | 22 +++++++++++++++++-- 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/__init__.py b/homeassistant/components/flux_led/__init__.py index 0284bf90ba0..717b4a685df 100644 --- a/homeassistant/components/flux_led/__init__.py +++ b/homeassistant/components/flux_led/__init__.py @@ -58,7 +58,12 @@ PLATFORMS_BY_TYPE: Final = { Platform.SENSOR, Platform.SWITCH, ], - DeviceType.Switch: [Platform.BUTTON, Platform.SELECT, Platform.SWITCH], + DeviceType.Switch: [ + Platform.BUTTON, + Platform.SELECT, + Platform.SENSOR, + Platform.SWITCH, + ], } DISCOVERY_INTERVAL: Final = timedelta(minutes=15) REQUEST_REFRESH_DELAY: Final = 1.5 diff --git a/tests/components/flux_led/__init__.py b/tests/components/flux_led/__init__.py index 2f4e5d62dcc..34f592110d0 100644 --- a/tests/components/flux_led/__init__.py +++ b/tests/components/flux_led/__init__.py @@ -175,6 +175,8 @@ def _mocked_switch() -> AIOWifiLedBulb: switch.pixels_per_segment = None switch.segments = None switch.music_pixels_per_segment = None + switch.paired_remotes = 2 + switch.remote_config = RemoteConfig.OPEN switch.music_segments = None switch.operating_mode = None switch.operating_modes = None @@ -183,6 +185,8 @@ def _mocked_switch() -> AIOWifiLedBulb: switch.ic_types = None switch.ic_type = None switch.requires_turn_on = True + switch.async_config_remotes = AsyncMock() + switch.async_unpair_remotes = AsyncMock() switch.async_set_time = AsyncMock() switch.async_reboot = AsyncMock() switch.async_setup = AsyncMock(side_effect=_save_setup_callback) diff --git a/tests/components/flux_led/test_button.py b/tests/components/flux_led/test_button.py index 992d8b18ce6..010b7f329ed 100644 --- a/tests/components/flux_led/test_button.py +++ b/tests/components/flux_led/test_button.py @@ -44,8 +44,8 @@ async def test_button_reboot(hass: HomeAssistant) -> None: switch.async_reboot.assert_called_once() -async def test_button_unpair_remotes(hass: HomeAssistant) -> None: - """Test that remotes can be unpaired.""" +async def test_button_unpair_remotes_bulb(hass: HomeAssistant) -> None: + """Test that remotes can be unpaired from a bulb.""" _mock_config_entry_for_bulb(hass) bulb = _mocked_bulb() bulb.discovery = FLUX_DISCOVERY @@ -60,3 +60,21 @@ async def test_button_unpair_remotes(hass: HomeAssistant) -> None: BUTTON_DOMAIN, "press", {ATTR_ENTITY_ID: entity_id}, blocking=True ) bulb.async_unpair_remotes.assert_called_once() + + +async def test_button_unpair_remotes_smart_switch(hass: HomeAssistant) -> None: + """Test that remotes can be unpaired from a smart switch.""" + _mock_config_entry_for_bulb(hass) + switch = _mocked_switch() + switch.discovery = FLUX_DISCOVERY + with _patch_discovery(device=FLUX_DISCOVERY), _patch_wifibulb(device=switch): + await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "button.bulb_rgbcw_ddeeff_unpair_remotes" + assert hass.states.get(entity_id) + + await hass.services.async_call( + BUTTON_DOMAIN, "press", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + switch.async_unpair_remotes.assert_called_once() From 39ddc37d7676861f511b41530cf00e65dde09726 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 26 Sep 2022 00:30:11 +0000 Subject: [PATCH 710/955] [ci skip] Translation update --- .../components/abode/translations/pt.json | 15 ++++++++--- .../accuweather/translations/pt.json | 12 +++++++-- .../accuweather/translations/sensor.pt.json | 9 +++++++ .../components/acmeda/translations/pt.json | 7 ++++++ .../components/adax/translations/pt.json | 17 +++++++++++++ .../components/adguard/translations/pt.json | 3 ++- .../components/aemet/translations/pt.json | 23 +++++++++++++++++ .../components/airly/translations/pt.json | 3 ++- .../components/airnow/translations/pt.json | 3 ++- .../components/airthings/translations/pt.json | 16 +++++++++++- .../components/airtouch4/translations/pt.json | 6 +++++ .../components/airzone/translations/pt.json | 6 +++++ .../aladdin_connect/translations/pt.json | 9 +++++++ .../amberelectric/translations/pt.json | 4 ++- .../android_ip_webcam/translations/nl.json | 14 +++++++++++ .../android_ip_webcam/translations/pt.json | 21 ++++++++++++++++ .../components/androidtv/translations/pt.json | 14 ++++++++++- .../components/apple_tv/translations/pt.json | 5 ++++ .../aseko_pool_live/translations/pt.json | 8 ++++++ .../aussie_broadband/translations/pt.json | 10 +++++++- .../components/awair/translations/nl.json | 3 ++- .../components/awair/translations/pt.json | 21 ++++++++++++++-- .../azure_devops/translations/pt.json | 2 +- .../azure_event_hub/translations/pt.json | 7 +++++- .../components/baf/translations/pt.json | 7 ++++++ .../components/balboa/translations/pt.json | 6 ++++- .../binary_sensor/translations/pt.json | 1 + .../components/bosch_shc/translations/pt.json | 5 ++++ .../components/braviatv/translations/nl.json | 10 ++++++-- .../components/braviatv/translations/pt.json | 4 ++- .../components/brunt/translations/pt.json | 10 ++++++-- .../components/bsblan/translations/pt.json | 1 + .../components/bthome/translations/pt.json | 6 +++++ .../buienradar/translations/pt.json | 7 ++++++ .../components/cast/translations/pt.json | 2 +- .../cloudflare/translations/pt.json | 5 ++++ .../components/coinbase/translations/pt.json | 16 ++++++++++++ .../coronavirus/translations/pt.json | 3 ++- .../components/cpuspeed/translations/pt.json | 6 +++++ .../crownstone/translations/pt.json | 23 +++++++++++++++++ .../components/daikin/translations/pt.json | 1 + .../components/denonavr/translations/pt.json | 4 +-- .../derivative/translations/pt.json | 3 +++ .../devolo_home_control/translations/pt.json | 4 ++- .../devolo_home_network/translations/pt.json | 17 +++++++++++++ .../dialogflow/translations/pt.json | 1 + .../components/discord/translations/pt.json | 12 ++++++--- .../components/dlna_dmr/translations/pt.json | 4 +++ .../components/ecowitt/translations/nl.json | 1 + .../components/esphome/translations/pt.json | 2 +- .../components/flux_led/translations/pt.json | 2 +- .../fully_kiosk/translations/nl.json | 20 +++++++++++++++ .../homekit_controller/translations/pt.json | 2 +- .../components/ifttt/translations/pt.json | 1 + .../components/kegtron/translations/nl.json | 22 ++++++++++++++++ .../keymitt_ble/translations/he.json | 3 ++- .../keymitt_ble/translations/nl.json | 22 ++++++++++++++++ .../components/lametric/translations/nl.json | 5 ++++ .../components/locative/translations/pt.json | 1 + .../components/melnor/translations/nl.json | 7 ++++++ .../nibe_heatpump/translations/nl.json | 10 ++++++++ .../components/nobo_hub/translations/nl.json | 2 ++ .../components/plaato/translations/pt.json | 1 + .../prusalink/translations/sensor.nl.json | 3 ++- .../components/radarr/translations/id.json | 1 + .../components/radarr/translations/nl.json | 25 +++++++++++++++++++ .../components/risco/translations/nl.json | 7 ++++++ .../components/samsungtv/translations/pt.json | 2 +- .../components/shelly/translations/ca.json | 8 ++++++ .../components/shelly/translations/en.json | 8 ++++++ .../components/shelly/translations/es.json | 8 ++++++ .../components/shelly/translations/fr.json | 8 ++++++ .../components/shelly/translations/he.json | 7 ++++++ .../components/shelly/translations/id.json | 8 ++++++ .../components/shelly/translations/it.json | 8 ++++++ .../components/shelly/translations/nl.json | 7 ++++++ .../components/shelly/translations/ru.json | 8 ++++++ .../shelly/translations/zh-Hant.json | 8 ++++++ .../components/skybell/translations/nl.json | 6 +++++ .../somfy_mylink/translations/pt.json | 2 +- .../components/switchbot/translations/nl.json | 8 ++++++ .../components/thermopro/translations/nl.json | 5 ++++ .../components/wilight/translations/pt.json | 2 +- .../xiaomi_miio/translations/select.nl.json | 4 +++ .../yalexs_ble/translations/nl.json | 4 ++- .../components/zha/translations/nl.json | 19 ++++++++++++++ .../components/zha/translations/pt.json | 3 +++ 87 files changed, 625 insertions(+), 41 deletions(-) create mode 100644 homeassistant/components/accuweather/translations/sensor.pt.json create mode 100644 homeassistant/components/android_ip_webcam/translations/pt.json create mode 100644 homeassistant/components/devolo_home_network/translations/pt.json create mode 100644 homeassistant/components/fully_kiosk/translations/nl.json create mode 100644 homeassistant/components/kegtron/translations/nl.json create mode 100644 homeassistant/components/keymitt_ble/translations/nl.json create mode 100644 homeassistant/components/melnor/translations/nl.json create mode 100644 homeassistant/components/nibe_heatpump/translations/nl.json create mode 100644 homeassistant/components/radarr/translations/nl.json diff --git a/homeassistant/components/abode/translations/pt.json b/homeassistant/components/abode/translations/pt.json index 3d6b007b471..7dc8448b288 100644 --- a/homeassistant/components/abode/translations/pt.json +++ b/homeassistant/components/abode/translations/pt.json @@ -6,20 +6,29 @@ }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o", - "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "invalid_mfa_code": "C\u00f3digo MFA inv\u00e1lido" }, "step": { + "mfa": { + "data": { + "mfa_code": "C\u00f3digo MFA (6 d\u00edgitos)" + }, + "title": "Introduza seu c\u00f3digo MFA para Abode" + }, "reauth_confirm": { "data": { "password": "Palavra-passe", "username": "Email" - } + }, + "title": "Preencha as informa\u00e7\u00f5es de login de Abode" }, "user": { "data": { "password": "Palavra-passe", "username": "Email" - } + }, + "title": "Preencha as informa\u00e7\u00f5es de login de Abode" } } } diff --git a/homeassistant/components/accuweather/translations/pt.json b/homeassistant/components/accuweather/translations/pt.json index 8b5d307e722..1346ee8367f 100644 --- a/homeassistant/components/accuweather/translations/pt.json +++ b/homeassistant/components/accuweather/translations/pt.json @@ -8,7 +8,8 @@ }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o", - "invalid_api_key": "Chave de API inv\u00e1lida" + "invalid_api_key": "Chave de API inv\u00e1lida", + "requests_exceeded": "O n\u00famero permitido de pedidos \u00e0 API do Accuweather foi excedido. \u00c9 necess\u00e1rio aguardar ou alterar a chave API." }, "step": { "user": { @@ -26,8 +27,15 @@ "user": { "data": { "forecast": "Previs\u00e3o meteorol\u00f3gica" - } + }, + "description": "Devido \u00e0s limita\u00e7\u00f5es da vers\u00e3o gratuita da chave AccuWeather API, quando se activa a previs\u00e3o do tempo, as actualiza\u00e7\u00f5es de dados ser\u00e3o realizadas a cada 80 minutos em vez de a cada 40 minutos." } } + }, + "system_health": { + "info": { + "can_reach_server": "Alcance o servidor AccuWeather", + "remaining_requests": "Pedidos permitidos restantes" + } } } \ No newline at end of file diff --git a/homeassistant/components/accuweather/translations/sensor.pt.json b/homeassistant/components/accuweather/translations/sensor.pt.json new file mode 100644 index 00000000000..9d4d6573509 --- /dev/null +++ b/homeassistant/components/accuweather/translations/sensor.pt.json @@ -0,0 +1,9 @@ +{ + "state": { + "accuweather__pressure_tendency": { + "falling": "A decrescer", + "rising": "A aumentar", + "steady": "Est\u00e1vel" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/acmeda/translations/pt.json b/homeassistant/components/acmeda/translations/pt.json index 8fcd9c13425..4eb37998165 100644 --- a/homeassistant/components/acmeda/translations/pt.json +++ b/homeassistant/components/acmeda/translations/pt.json @@ -2,6 +2,13 @@ "config": { "abort": { "no_devices_found": "Nenhum dispositivo encontrado na rede" + }, + "step": { + "user": { + "data": { + "id": "ID do anfitri\u00e3o" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/adax/translations/pt.json b/homeassistant/components/adax/translations/pt.json index b83b23758af..b8256278f36 100644 --- a/homeassistant/components/adax/translations/pt.json +++ b/homeassistant/components/adax/translations/pt.json @@ -1,6 +1,9 @@ { "config": { "abort": { + "already_configured": "DIspositivo j\u00e1 est\u00e1 configurado", + "heater_not_available": "Aquecedor n\u00e3o dispon\u00edvel. Tente reiniciar o aquecedor premindo + e OK durante alguns segundos.", + "heater_not_found": "Aquecedor n\u00e3o encontrado. Tente mover o aquecedor para mais perto do computador com Home Assistant.", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, "error": { @@ -9,8 +12,22 @@ "step": { "cloud": { "data": { + "account_id": "ID da conta", "password": "Palavra-passe" } + }, + "local": { + "data": { + "wifi_pswd": "Senha Wi-Fi", + "wifi_ssid": "Wi-Fi SSID" + }, + "description": "Reiniciar o aquecedor premindo + e OK at\u00e9 a visualiza\u00e7\u00e3o mostrar 'Reiniciar'. Depois premir e manter premido o bot\u00e3o OK no aquecedor at\u00e9 que o led azul comece a piscar antes de premir Submeter. A configura\u00e7\u00e3o do aquecedor pode demorar alguns minutos." + }, + "user": { + "data": { + "connection_type": "Selecione o tipo de liga\u00e7\u00e3o" + }, + "description": "Selecione o tipo de liga\u00e7\u00e3o. 'Local' requer aquecedores com bluetooth" } } } diff --git a/homeassistant/components/adguard/translations/pt.json b/homeassistant/components/adguard/translations/pt.json index e389261748d..28a63ea54e7 100644 --- a/homeassistant/components/adguard/translations/pt.json +++ b/homeassistant/components/adguard/translations/pt.json @@ -8,7 +8,8 @@ }, "step": { "hassio_confirm": { - "title": "AdGuard Home via Supervisor add-on" + "description": "Deseja configurar o Home Assistant para se ligar ao AdGuard Home fornecido pelo add-on: {addon}?", + "title": "AdGuard Home via add-on Supervisor" }, "user": { "data": { diff --git a/homeassistant/components/aemet/translations/pt.json b/homeassistant/components/aemet/translations/pt.json index cc227afe3a3..6b89c6c3015 100644 --- a/homeassistant/components/aemet/translations/pt.json +++ b/homeassistant/components/aemet/translations/pt.json @@ -1,7 +1,30 @@ { "config": { + "abort": { + "already_configured": "A localiza\u00e7\u00e3o j\u00e1 est\u00e1 configurada" + }, "error": { "invalid_api_key": "Chave de API inv\u00e1lida" + }, + "step": { + "user": { + "data": { + "api_key": "Chave da API", + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Nome da integra\u00e7\u00e3o" + }, + "description": "Para gerar a chave API v\u00e1 a https://opendata.aemet.es/centrodedescargas/altaUsuario" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "station_updates": "Recolha de dados das esta\u00e7\u00f5es meteorol\u00f3gicas AEMET" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/airly/translations/pt.json b/homeassistant/components/airly/translations/pt.json index aff2cb2399b..03b6ca72520 100644 --- a/homeassistant/components/airly/translations/pt.json +++ b/homeassistant/components/airly/translations/pt.json @@ -13,7 +13,8 @@ "latitude": "Latitude", "longitude": "Longitude", "name": "Nome" - } + }, + "description": "Para gerar a chave API v\u00e1 a https://developer.airly.eu/register" } } } diff --git a/homeassistant/components/airnow/translations/pt.json b/homeassistant/components/airnow/translations/pt.json index f846047c307..dd714aa6dad 100644 --- a/homeassistant/components/airnow/translations/pt.json +++ b/homeassistant/components/airnow/translations/pt.json @@ -14,7 +14,8 @@ "api_key": "Chave da API", "latitude": "Latitude", "longitude": "Longitude" - } + }, + "description": "Para gerar a chave API v\u00e1 a https://docs.airnowapi.org/account/request/" } } } diff --git a/homeassistant/components/airthings/translations/pt.json b/homeassistant/components/airthings/translations/pt.json index 3b5850222d9..ef1f838f139 100644 --- a/homeassistant/components/airthings/translations/pt.json +++ b/homeassistant/components/airthings/translations/pt.json @@ -1,7 +1,21 @@ { "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada" + }, "error": { - "cannot_connect": "Falha na liga\u00e7\u00e3o" + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "description": "Fa\u00e7a login em {url} para encontrar as suas credenciais", + "id": "ID", + "secret": "Segredo" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/airtouch4/translations/pt.json b/homeassistant/components/airtouch4/translations/pt.json index 4e8578a0a28..18ca3271c81 100644 --- a/homeassistant/components/airtouch4/translations/pt.json +++ b/homeassistant/components/airtouch4/translations/pt.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha de liga\u00e7\u00e3o" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/airzone/translations/pt.json b/homeassistant/components/airzone/translations/pt.json index f681da4210f..fa5aa3de317 100644 --- a/homeassistant/components/airzone/translations/pt.json +++ b/homeassistant/components/airzone/translations/pt.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/aladdin_connect/translations/pt.json b/homeassistant/components/aladdin_connect/translations/pt.json index 6c09ed1c852..5c109d9bc33 100644 --- a/homeassistant/components/aladdin_connect/translations/pt.json +++ b/homeassistant/components/aladdin_connect/translations/pt.json @@ -1,12 +1,21 @@ { "config": { "abort": { + "already_configured": "Dispositivo j\u00e1 configurado", "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" }, "error": { + "cannot_connect": "Falha de liga\u00e7\u00e3o", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, "step": { + "reauth_confirm": { + "data": { + "password": "Palavra-passe" + }, + "description": "A integra\u00e7\u00e3o Aladdin Connect precisa de re-autenticar a sua conta", + "title": "Re-autenticar integra\u00e7\u00e3o" + }, "user": { "data": { "username": "Nome de Utilizador" diff --git a/homeassistant/components/amberelectric/translations/pt.json b/homeassistant/components/amberelectric/translations/pt.json index 8320b3662d3..a58215237fb 100644 --- a/homeassistant/components/amberelectric/translations/pt.json +++ b/homeassistant/components/amberelectric/translations/pt.json @@ -1,7 +1,9 @@ { "config": { "error": { - "no_site": "Nenhum site fornecido" + "invalid_api_token": "Chave de API inv\u00e1lida", + "no_site": "Nenhum site fornecido", + "unknown_error": "Erro inesperado" } } } \ No newline at end of file diff --git a/homeassistant/components/android_ip_webcam/translations/nl.json b/homeassistant/components/android_ip_webcam/translations/nl.json index 37162761d86..e18be76b805 100644 --- a/homeassistant/components/android_ip_webcam/translations/nl.json +++ b/homeassistant/components/android_ip_webcam/translations/nl.json @@ -1,7 +1,21 @@ { "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, "error": { + "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Wachtwoord", + "port": "Poort", + "username": "Gebruikersnaam" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/android_ip_webcam/translations/pt.json b/homeassistant/components/android_ip_webcam/translations/pt.json new file mode 100644 index 00000000000..795ba71964f --- /dev/null +++ b/homeassistant/components/android_ip_webcam/translations/pt.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "user": { + "data": { + "host": "Servidor", + "password": "Palavra-passe", + "port": "Porta", + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/androidtv/translations/pt.json b/homeassistant/components/androidtv/translations/pt.json index 0d9b37a78f0..adc29b49b38 100644 --- a/homeassistant/components/androidtv/translations/pt.json +++ b/homeassistant/components/androidtv/translations/pt.json @@ -1,8 +1,20 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o", - "invalid_host": "Nome de servidor ou endere\u00e7o IP inv\u00e1lido." + "invalid_host": "Nome de servidor ou endere\u00e7o IP inv\u00e1lido.", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "host": "Servidor", + "port": "Porta" + } + } } }, "options": { diff --git a/homeassistant/components/apple_tv/translations/pt.json b/homeassistant/components/apple_tv/translations/pt.json index e54e421caa5..d05409a4587 100644 --- a/homeassistant/components/apple_tv/translations/pt.json +++ b/homeassistant/components/apple_tv/translations/pt.json @@ -1,8 +1,10 @@ { "config": { "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer", "no_devices_found": "Nenhum dispositivo encontrado na rede", + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida", "unknown": "Erro inesperado" }, "error": { @@ -28,6 +30,9 @@ "description": "\u00c9 necess\u00e1rio fazer o emparelhamento com protocolo `{protocol}`. Insira o c\u00f3digo PIN exibido no ecran. Os zeros iniciais devem ser omitidos, ou seja, digite 123 se o c\u00f3digo exibido for 0123.", "title": "Emparelhamento" }, + "protocol_disabled": { + "title": "N\u00e3o \u00e9 poss\u00edvel emparelhar" + }, "reconfigure": { "description": "Esta Apple TV apresenta dificuldades de liga\u00e7\u00e3o e precisa ser reconfigurada.", "title": "Reconfigura\u00e7\u00e3o do dispositivo" diff --git a/homeassistant/components/aseko_pool_live/translations/pt.json b/homeassistant/components/aseko_pool_live/translations/pt.json index 2933743c867..cdb482efaa8 100644 --- a/homeassistant/components/aseko_pool_live/translations/pt.json +++ b/homeassistant/components/aseko_pool_live/translations/pt.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/aussie_broadband/translations/pt.json b/homeassistant/components/aussie_broadband/translations/pt.json index 5a8312a5ac1..535612a4dbd 100644 --- a/homeassistant/components/aussie_broadband/translations/pt.json +++ b/homeassistant/components/aussie_broadband/translations/pt.json @@ -1,6 +1,10 @@ { "config": { + "abort": { + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" + }, "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, @@ -8,10 +12,12 @@ "reauth_confirm": { "data": { "password": "Palavra-passe" - } + }, + "title": "Reautenticar integra\u00e7\u00e3o" }, "user": { "data": { + "password": "Palavra-passe", "username": "Nome de Utilizador" } } @@ -19,6 +25,8 @@ }, "options": { "abort": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" } } diff --git a/homeassistant/components/awair/translations/nl.json b/homeassistant/components/awair/translations/nl.json index 6a42bca2372..7c70960c1b1 100644 --- a/homeassistant/components/awair/translations/nl.json +++ b/homeassistant/components/awair/translations/nl.json @@ -28,7 +28,8 @@ }, "local_pick": { "data": { - "device": "Apparaat" + "device": "Apparaat", + "host": "IP-adres" } }, "reauth": { diff --git a/homeassistant/components/awair/translations/pt.json b/homeassistant/components/awair/translations/pt.json index 7357334a827..1f922776179 100644 --- a/homeassistant/components/awair/translations/pt.json +++ b/homeassistant/components/awair/translations/pt.json @@ -2,24 +2,41 @@ "config": { "abort": { "already_configured": "Conta j\u00e1 configurada", + "already_configured_account": "Conta j\u00e1 configurada", + "already_configured_device": "O dispositivo j\u00e1 est\u00e1 configurado", "no_devices_found": "Nenhum dispositivo encontrado na rede", - "reauth_successful": "Token de Acesso actualizado com sucesso" + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida", + "unreachable": "Falha na liga\u00e7\u00e3o" }, "error": { "invalid_access_token": "Token de acesso inv\u00e1lido", - "unknown": "Erro inesperado" + "unknown": "Erro inesperado", + "unreachable": "Falha na liga\u00e7\u00e3o" }, "flow_title": "{model} ( {device_id} )", "step": { "cloud": { + "data": { + "access_token": "Token de Acesso", + "email": "Email" + }, "description": "Voc\u00ea deve se registrar para um token de acesso de desenvolvedor Awair em: {url}" }, "discovery_confirm": { "description": "Deseja configurar {model} ( {device_id} )?" }, "local": { + "data": { + "host": "Endere\u00e7o IP" + }, "description": "Siga [estas instru\u00e7\u00f5es]( {url} ) sobre como ativar a API local Awair. \n\n Clique em enviar quando terminar." }, + "local_pick": { + "data": { + "device": "Dispositivo", + "host": "Endere\u00e7o IP" + } + }, "reauth": { "data": { "access_token": "Token de Acesso", diff --git a/homeassistant/components/azure_devops/translations/pt.json b/homeassistant/components/azure_devops/translations/pt.json index b09f2cceda7..66fad0c2b08 100644 --- a/homeassistant/components/azure_devops/translations/pt.json +++ b/homeassistant/components/azure_devops/translations/pt.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Conta j\u00e1 configurada", - "reauth_successful": "Token de Acesso atualizado com sucesso" + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o", diff --git a/homeassistant/components/azure_event_hub/translations/pt.json b/homeassistant/components/azure_event_hub/translations/pt.json index d252c078a2c..cf10963fbd4 100644 --- a/homeassistant/components/azure_event_hub/translations/pt.json +++ b/homeassistant/components/azure_event_hub/translations/pt.json @@ -1,7 +1,12 @@ { "config": { "abort": { - "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "unknown": "Erro inesperado" } } } \ No newline at end of file diff --git a/homeassistant/components/baf/translations/pt.json b/homeassistant/components/baf/translations/pt.json index ce8a9287272..8dfe1f9f51e 100644 --- a/homeassistant/components/baf/translations/pt.json +++ b/homeassistant/components/baf/translations/pt.json @@ -2,6 +2,13 @@ "config": { "abort": { "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "step": { + "user": { + "data": { + "ip_address": "Endere\u00e7o IP" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/balboa/translations/pt.json b/homeassistant/components/balboa/translations/pt.json index f13cad90edc..04374af8e82 100644 --- a/homeassistant/components/balboa/translations/pt.json +++ b/homeassistant/components/balboa/translations/pt.json @@ -1,7 +1,11 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, "error": { - "cannot_connect": "Falha na liga\u00e7\u00e3o" + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "unknown": "Erro inesperado" }, "step": { "user": { diff --git a/homeassistant/components/binary_sensor/translations/pt.json b/homeassistant/components/binary_sensor/translations/pt.json index cfaf2e36bd3..6245d291f42 100644 --- a/homeassistant/components/binary_sensor/translations/pt.json +++ b/homeassistant/components/binary_sensor/translations/pt.json @@ -109,6 +109,7 @@ "on": "A carregar" }, "carbon_monoxide": { + "off": "Limpo", "on": "Detectado" }, "cold": { diff --git a/homeassistant/components/bosch_shc/translations/pt.json b/homeassistant/components/bosch_shc/translations/pt.json index e229572938d..1462f7a14a0 100644 --- a/homeassistant/components/bosch_shc/translations/pt.json +++ b/homeassistant/components/bosch_shc/translations/pt.json @@ -1,7 +1,12 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" + }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, "step": { diff --git a/homeassistant/components/braviatv/translations/nl.json b/homeassistant/components/braviatv/translations/nl.json index d3696024643..18a4f8881fb 100644 --- a/homeassistant/components/braviatv/translations/nl.json +++ b/homeassistant/components/braviatv/translations/nl.json @@ -2,21 +2,27 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd", - "no_ip_control": "IP-besturing is uitgeschakeld op uw tv of de tv wordt niet ondersteund." + "no_ip_control": "IP-besturing is uitgeschakeld op uw tv of de tv wordt niet ondersteund.", + "not_bravia_device": "Dit apparaat is geen Bravia-TV." }, "error": { "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", "invalid_host": "Ongeldige hostnaam of IP-adres", "unsupported_model": "Uw tv-model wordt niet ondersteund." }, "step": { "authorize": { "data": { - "pin": "Pincode" + "pin": "Pincode", + "use_psk": "PSK-authenticatie gebruiken" }, "description": "Voer de pincode in die wordt weergegeven op de Sony Bravia tv. \n\nAls de pincode niet wordt weergegeven, moet u de Home Assistant op uw tv afmelden, ga naar: Instellingen -> Netwerk -> Instellingen extern apparaat -> Afmelden extern apparaat.", "title": "Autoriseer Sony Bravia tv" }, + "confirm": { + "description": "Wil je beginnen met instellen?" + }, "user": { "data": { "host": "Host" diff --git a/homeassistant/components/braviatv/translations/pt.json b/homeassistant/components/braviatv/translations/pt.json index 0838bb5d632..5ee36fdbb85 100644 --- a/homeassistant/components/braviatv/translations/pt.json +++ b/homeassistant/components/braviatv/translations/pt.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "Esta TV j\u00e1 est\u00e1 configurada." + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "not_bravia_device": "O dispositivo n\u00e3o \u00e9 uma TV Bravia." }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "invalid_host": "Nome de servidor ou endere\u00e7o IP inv\u00e1lido.", "unsupported_model": "O seu modelo de TV n\u00e3o \u00e9 suportado." }, diff --git a/homeassistant/components/brunt/translations/pt.json b/homeassistant/components/brunt/translations/pt.json index 6f18afa4df3..8df574de6eb 100644 --- a/homeassistant/components/brunt/translations/pt.json +++ b/homeassistant/components/brunt/translations/pt.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Conta j\u00e1 configurada" + "already_configured": "Conta j\u00e1 configurada", + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o", @@ -10,11 +11,16 @@ }, "step": { "reauth_confirm": { + "data": { + "password": "Palavra-passe" + }, + "description": "Por favor, introduza novamente a palavra-passe para: {username}", "title": "Reautenticar integra\u00e7\u00e3o" }, "user": { "data": { - "password": "Palavra-passe" + "password": "Palavra-passe", + "username": "Nome de Utilizador" } } } diff --git a/homeassistant/components/bsblan/translations/pt.json b/homeassistant/components/bsblan/translations/pt.json index 3cb7a7d5891..0b09e208858 100644 --- a/homeassistant/components/bsblan/translations/pt.json +++ b/homeassistant/components/bsblan/translations/pt.json @@ -7,6 +7,7 @@ "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o" }, + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/bthome/translations/pt.json b/homeassistant/components/bthome/translations/pt.json index ee54701d78c..0294a42b129 100644 --- a/homeassistant/components/bthome/translations/pt.json +++ b/homeassistant/components/bthome/translations/pt.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer", + "no_devices_found": "Nenhum dispositivo encontrado na rede", + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" + }, "error": { "decryption_failed": "A chave de liga\u00e7\u00e3o fornecida n\u00e3o funcionou, os dados do sensor n\u00e3o puderam ser descriptografados. Por favor verifique e tente novamente.", "expected_32_characters": "Esperava-se uma chave de liga\u00e7\u00e3o hexadecimal de 32 caracteres." diff --git a/homeassistant/components/buienradar/translations/pt.json b/homeassistant/components/buienradar/translations/pt.json index 2e6515edd09..7d614a9f841 100644 --- a/homeassistant/components/buienradar/translations/pt.json +++ b/homeassistant/components/buienradar/translations/pt.json @@ -1,8 +1,15 @@ { "config": { + "abort": { + "already_configured": "A localiza\u00e7\u00e3o j\u00e1 est\u00e1 configurada" + }, + "error": { + "already_configured": "A localiza\u00e7\u00e3o j\u00e1 est\u00e1 configurada" + }, "step": { "user": { "data": { + "latitude": "Latitude", "longitude": "Longitude" } } diff --git a/homeassistant/components/cast/translations/pt.json b/homeassistant/components/cast/translations/pt.json index 34770733822..0238c7cbc94 100644 --- a/homeassistant/components/cast/translations/pt.json +++ b/homeassistant/components/cast/translations/pt.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "Apenas uma \u00fanica configura\u00e7\u00e3o do Google Cast \u00e9 necess\u00e1ria." + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." }, "step": { "config": { diff --git a/homeassistant/components/cloudflare/translations/pt.json b/homeassistant/components/cloudflare/translations/pt.json index 650823693d6..dd836339b2b 100644 --- a/homeassistant/components/cloudflare/translations/pt.json +++ b/homeassistant/components/cloudflare/translations/pt.json @@ -10,6 +10,11 @@ "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, "step": { + "reauth_confirm": { + "data": { + "api_token": "API Token" + } + }, "user": { "data": { "api_token": "API Token" diff --git a/homeassistant/components/coinbase/translations/pt.json b/homeassistant/components/coinbase/translations/pt.json index 49cb628dd85..dec932ffe10 100644 --- a/homeassistant/components/coinbase/translations/pt.json +++ b/homeassistant/components/coinbase/translations/pt.json @@ -1,7 +1,23 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "api_key": "Chave da API" + } + } + } + }, + "options": { + "error": { "unknown": "Erro inesperado" } } diff --git a/homeassistant/components/coronavirus/translations/pt.json b/homeassistant/components/coronavirus/translations/pt.json index e03867478c4..308eaef73f0 100644 --- a/homeassistant/components/coronavirus/translations/pt.json +++ b/homeassistant/components/coronavirus/translations/pt.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha na liga\u00e7\u00e3o" }, "step": { "user": { diff --git a/homeassistant/components/cpuspeed/translations/pt.json b/homeassistant/components/cpuspeed/translations/pt.json index cf03f249d96..f2a86ca8ca8 100644 --- a/homeassistant/components/cpuspeed/translations/pt.json +++ b/homeassistant/components/cpuspeed/translations/pt.json @@ -2,6 +2,12 @@ "config": { "abort": { "already_configured": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "step": { + "user": { + "description": "Quer dar inicio \u00e0 configura\u00e7\u00e3o?", + "title": "Velocidade da CPU" + } } } } \ No newline at end of file diff --git a/homeassistant/components/crownstone/translations/pt.json b/homeassistant/components/crownstone/translations/pt.json index 97ea705b32b..5a97b161335 100644 --- a/homeassistant/components/crownstone/translations/pt.json +++ b/homeassistant/components/crownstone/translations/pt.json @@ -1,6 +1,14 @@ { "config": { + "error": { + "unknown": "Erro inesperado" + }, "step": { + "usb_config": { + "data": { + "usb_path": "Caminho do Dispositivo USB" + } + }, "usb_manual_config": { "data": { "usb_manual_path": "Caminho do Dispositivo USB" @@ -8,9 +16,24 @@ }, "user": { "data": { + "email": "Email", "password": "Palavra-passe" } } } + }, + "options": { + "step": { + "usb_config": { + "data": { + "usb_path": "Caminho do Dispositivo USB" + } + }, + "usb_manual_config": { + "data": { + "usb_manual_path": "Caminho do Dispositivo USB" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/daikin/translations/pt.json b/homeassistant/components/daikin/translations/pt.json index 28cdb0596d7..0d1c8f842c0 100644 --- a/homeassistant/components/daikin/translations/pt.json +++ b/homeassistant/components/daikin/translations/pt.json @@ -5,6 +5,7 @@ "cannot_connect": "Falha na liga\u00e7\u00e3o" }, "error": { + "api_password": "Autentica\u00e7\u00e3o inv\u00e1lida, use a chave de API ou a palavra-passe.", "cannot_connect": "Falha na liga\u00e7\u00e3o", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" diff --git a/homeassistant/components/denonavr/translations/pt.json b/homeassistant/components/denonavr/translations/pt.json index 4a00952aaa5..fa509296c77 100644 --- a/homeassistant/components/denonavr/translations/pt.json +++ b/homeassistant/components/denonavr/translations/pt.json @@ -7,12 +7,12 @@ "step": { "select": { "data": { - "select_host": "IP do receptor" + "select_host": "IP do Receptor" } }, "user": { "data": { - "host": "endere\u00e7o de IP" + "host": "Endere\u00e7o IP" } } } diff --git a/homeassistant/components/derivative/translations/pt.json b/homeassistant/components/derivative/translations/pt.json index 0b3ad11d873..95b042df331 100644 --- a/homeassistant/components/derivative/translations/pt.json +++ b/homeassistant/components/derivative/translations/pt.json @@ -14,6 +14,9 @@ "options": { "step": { "init": { + "data": { + "round": "Precis\u00e3o" + }, "data_description": { "unit_prefix": "." } diff --git a/homeassistant/components/devolo_home_control/translations/pt.json b/homeassistant/components/devolo_home_control/translations/pt.json index d60cc81f541..f8e1acc359d 100644 --- a/homeassistant/components/devolo_home_control/translations/pt.json +++ b/homeassistant/components/devolo_home_control/translations/pt.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Conta j\u00e1 configurada" + "already_configured": "Conta j\u00e1 configurada", + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" }, "error": { "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" @@ -16,6 +17,7 @@ }, "zeroconf_confirm": { "data": { + "mydevolo_url": "mydevolo [VOID]", "password": "Palavra-passe" } } diff --git a/homeassistant/components/devolo_home_network/translations/pt.json b/homeassistant/components/devolo_home_network/translations/pt.json new file mode 100644 index 00000000000..fb6f8840412 --- /dev/null +++ b/homeassistant/components/devolo_home_network/translations/pt.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "unknown": "Erro inesperado" + }, + "flow_title": "{product} ({name})", + "step": { + "user": { + "data": { + "ip_address": "Endere\u00e7o IP" + }, + "description": "Quer dar inicio \u00e0 configura\u00e7\u00e3o?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dialogflow/translations/pt.json b/homeassistant/components/dialogflow/translations/pt.json index 56c91431f13..1c1db64bf45 100644 --- a/homeassistant/components/dialogflow/translations/pt.json +++ b/homeassistant/components/dialogflow/translations/pt.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "N\u00e3o ligado ao Home Assistant Cloud.", "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel.", "webhook_not_internet_accessible": "O seu Home Assistant necessita estar acess\u00edvel a partir da Internet para receber mensagens do tipo webhook." }, diff --git a/homeassistant/components/discord/translations/pt.json b/homeassistant/components/discord/translations/pt.json index 9925c2a7416..2c961f1708a 100644 --- a/homeassistant/components/discord/translations/pt.json +++ b/homeassistant/components/discord/translations/pt.json @@ -1,22 +1,26 @@ { "config": { "abort": { - "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado", + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o", - "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" }, "step": { "reauth_confirm": { "data": { "api_token": "API Token" - } + }, + "description": "Consulte a documenta\u00e7\u00e3o sobre como obter a sua chave de bot do Discord. \n\n{url}" }, "user": { "data": { "api_token": "API Token" - } + }, + "description": "Consulte a documenta\u00e7\u00e3o sobre como obter a sua chave de bot do Discord. \n\n{url}" } } } diff --git a/homeassistant/components/dlna_dmr/translations/pt.json b/homeassistant/components/dlna_dmr/translations/pt.json index 49f47abb540..ea08ade3537 100644 --- a/homeassistant/components/dlna_dmr/translations/pt.json +++ b/homeassistant/components/dlna_dmr/translations/pt.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o" }, diff --git a/homeassistant/components/ecowitt/translations/nl.json b/homeassistant/components/ecowitt/translations/nl.json index 7e198e836d7..1090e946378 100644 --- a/homeassistant/components/ecowitt/translations/nl.json +++ b/homeassistant/components/ecowitt/translations/nl.json @@ -1,6 +1,7 @@ { "config": { "error": { + "invalid_port": "Poort wordt al gebruikt.", "unknown": "Onverwachte fout" } } diff --git a/homeassistant/components/esphome/translations/pt.json b/homeassistant/components/esphome/translations/pt.json index 636749dd130..5b5e85922d9 100644 --- a/homeassistant/components/esphome/translations/pt.json +++ b/homeassistant/components/esphome/translations/pt.json @@ -10,7 +10,7 @@ "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "resolve_error": "N\u00e3o \u00e9 poss\u00edvel resolver o endere\u00e7o do ESP. Se este erro persistir, defina um endere\u00e7o IP est\u00e1tico: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" }, - "flow_title": "", + "flow_title": "{name}", "step": { "authenticate": { "data": { diff --git a/homeassistant/components/flux_led/translations/pt.json b/homeassistant/components/flux_led/translations/pt.json index 5e78131c687..c8c04537f40 100644 --- a/homeassistant/components/flux_led/translations/pt.json +++ b/homeassistant/components/flux_led/translations/pt.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" }, - "flow_title": "", + "flow_title": "{model} {id} ({ipaddr})", "step": { "user": { "data": { diff --git a/homeassistant/components/fully_kiosk/translations/nl.json b/homeassistant/components/fully_kiosk/translations/nl.json new file mode 100644 index 00000000000..359990b3e69 --- /dev/null +++ b/homeassistant/components/fully_kiosk/translations/nl.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Account is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Wachtwoord" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/pt.json b/homeassistant/components/homekit_controller/translations/pt.json index febadec444c..857226a9cdb 100644 --- a/homeassistant/components/homekit_controller/translations/pt.json +++ b/homeassistant/components/homekit_controller/translations/pt.json @@ -25,7 +25,7 @@ "data": { "pairing_code": "C\u00f3digo de emparelhamento" }, - "description": "Introduza o c\u00f3digo de emparelhamento do seu HomeKit (no formato XXX-XX-XXX) para utilizar este acess\u00f3rio", + "description": "Introduza o c\u00f3digo de emparelhamento do seu HomeKit (no formato XXX-XX-XXX) para utilizar este acess\u00f3rio.", "title": "Emparelhar com o acess\u00f3rio HomeKit" }, "user": { diff --git a/homeassistant/components/ifttt/translations/pt.json b/homeassistant/components/ifttt/translations/pt.json index 030af8e090b..b99c81b0739 100644 --- a/homeassistant/components/ifttt/translations/pt.json +++ b/homeassistant/components/ifttt/translations/pt.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "N\u00e3o ligado ao Home Assistant Cloud.", "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel.", "webhook_not_internet_accessible": "O seu Home Assistant necessita estar acess\u00edvel a partir da Internet para receber mensagens do tipo webhook." }, diff --git a/homeassistant/components/kegtron/translations/nl.json b/homeassistant/components/kegtron/translations/nl.json new file mode 100644 index 00000000000..320c86529fe --- /dev/null +++ b/homeassistant/components/kegtron/translations/nl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "already_in_progress": "De configuratie is momenteel al bezig", + "no_devices_found": "Geen apparaten gevonden op het netwerk", + "not_supported": "Apparaat is niet ondersteund." + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Wilt u {name} instellen?" + }, + "user": { + "data": { + "address": "Apparaat" + }, + "description": "Kies een apparaat om in te stellen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/keymitt_ble/translations/he.json b/homeassistant/components/keymitt_ble/translations/he.json index 934eb6df0c4..b02c2388374 100644 --- a/homeassistant/components/keymitt_ble/translations/he.json +++ b/homeassistant/components/keymitt_ble/translations/he.json @@ -4,6 +4,7 @@ "already_configured_device": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" - } + }, + "flow_title": "{name}" } } \ No newline at end of file diff --git a/homeassistant/components/keymitt_ble/translations/nl.json b/homeassistant/components/keymitt_ble/translations/nl.json new file mode 100644 index 00000000000..853de9222ba --- /dev/null +++ b/homeassistant/components/keymitt_ble/translations/nl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured_device": "Apparaat is al geconfigureerd", + "cannot_connect": "Kan geen verbinding maken", + "no_unconfigured_devices": "Geen niet-geconfigureerde apparaten gevonden.", + "unknown": "Onverwachte fout" + }, + "flow_title": "{name}", + "step": { + "init": { + "data": { + "name": "Naam" + }, + "title": "MicroBot-apparaat instellen" + }, + "link": { + "title": "Koppelen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lametric/translations/nl.json b/homeassistant/components/lametric/translations/nl.json index de776229764..f88338f4410 100644 --- a/homeassistant/components/lametric/translations/nl.json +++ b/homeassistant/components/lametric/translations/nl.json @@ -11,6 +11,11 @@ "unknown": "Onverwachte fout" }, "step": { + "choice_enter_manual_or_fetch_cloud": { + "menu_options": { + "manual_entry": "Handmatig invoeren" + } + }, "manual_entry": { "data": { "api_key": "API-sleutel", diff --git a/homeassistant/components/locative/translations/pt.json b/homeassistant/components/locative/translations/pt.json index 93575068121..4666c1e91de 100644 --- a/homeassistant/components/locative/translations/pt.json +++ b/homeassistant/components/locative/translations/pt.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "N\u00e3o ligado ao Home Assistant Cloud.", "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel.", "webhook_not_internet_accessible": "O seu Home Assistant necessita estar acess\u00edvel a partir da Internet para receber mensagens do tipo webhook." }, diff --git a/homeassistant/components/melnor/translations/nl.json b/homeassistant/components/melnor/translations/nl.json new file mode 100644 index 00000000000..14ebeed502e --- /dev/null +++ b/homeassistant/components/melnor/translations/nl.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "no_devices_found": "Er zijn geen Melnor Bluetooth-apparaten in de buurt." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nibe_heatpump/translations/nl.json b/homeassistant/components/nibe_heatpump/translations/nl.json new file mode 100644 index 00000000000..c227699ff21 --- /dev/null +++ b/homeassistant/components/nibe_heatpump/translations/nl.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "unknown": "Onverwachte fout" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nobo_hub/translations/nl.json b/homeassistant/components/nobo_hub/translations/nl.json index 8a25fd2ab6b..9a8e90bcdb5 100644 --- a/homeassistant/components/nobo_hub/translations/nl.json +++ b/homeassistant/components/nobo_hub/translations/nl.json @@ -4,6 +4,8 @@ "already_configured": "Apparaat is al geconfigureerd" }, "error": { + "cannot_connect": "Kan niet verbinden. Controleer het serienummer.", + "invalid_ip": "Ongeldig IP-adres", "invalid_serial": "Ongeldig serienummer", "unknown": "Onverwachte fout" }, diff --git a/homeassistant/components/plaato/translations/pt.json b/homeassistant/components/plaato/translations/pt.json index 3039abbcafb..7e145c2f063 100644 --- a/homeassistant/components/plaato/translations/pt.json +++ b/homeassistant/components/plaato/translations/pt.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Conta j\u00e1 configurada", + "cloud_not_connected": "N\u00e3o ligado ao Home Assistant Cloud.", "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel.", "webhook_not_internet_accessible": "O seu Home Assistant necessita estar acess\u00edvel a partir da Internet para receber mensagens do tipo webhook." }, diff --git a/homeassistant/components/prusalink/translations/sensor.nl.json b/homeassistant/components/prusalink/translations/sensor.nl.json index cd02b3cefd8..0dfc3902f68 100644 --- a/homeassistant/components/prusalink/translations/sensor.nl.json +++ b/homeassistant/components/prusalink/translations/sensor.nl.json @@ -3,7 +3,8 @@ "prusalink__printer_state": { "cancelling": "Annuleren", "idle": "Inactief", - "paused": "Gepauzeerd" + "paused": "Gepauzeerd", + "printing": "Afdrukken" } } } \ No newline at end of file diff --git a/homeassistant/components/radarr/translations/id.json b/homeassistant/components/radarr/translations/id.json index 12b85c3e43a..f6ad3195c71 100644 --- a/homeassistant/components/radarr/translations/id.json +++ b/homeassistant/components/radarr/translations/id.json @@ -32,6 +32,7 @@ "title": "Konfigurasi YAML Radarr dalam proses penghapusan" }, "removed_attributes": { + "description": "Beberapa perubahan besar telah dilakukan dalam menonaktifkan sensor hitungan Film dengan alasan kehati-hatian.\n\nSensor ini bisa menyebabkan masalah dengan database yang sangat besar. Jika masih ingin menggunakannya, Anda dapat melakukannya.\n\nNama film tidak lagi disertakan sebagai atribut dalam sensor film.\n\nItem \"Yang akan datang\" telah dihapus. Sensor ini sedang dimodernisasi sebagaimana layaknya item kalender. Ruang disk sekarang dipecah ke dalam sensor yang berbeda, satu untuk setiap folder.\n\nStatus dan perintah telah dihapus karena tampaknya tidak membawa nilai dalam otomasi.", "title": "Perubahan pada integrasi Radarr" } }, diff --git a/homeassistant/components/radarr/translations/nl.json b/homeassistant/components/radarr/translations/nl.json new file mode 100644 index 00000000000..436d0998a9e --- /dev/null +++ b/homeassistant/components/radarr/translations/nl.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Dienst is al geconfigureerd", + "reauth_successful": "Herauthenticatie geslaagd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, + "step": { + "reauth_confirm": { + "title": "Integratie herauthenticeren" + }, + "user": { + "data": { + "api_key": "API-sleutel", + "url": "URL", + "verify_ssl": "SSL-certificaat verifi\u00ebren" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/risco/translations/nl.json b/homeassistant/components/risco/translations/nl.json index 9b7bb7a43c8..db2a4996c77 100644 --- a/homeassistant/components/risco/translations/nl.json +++ b/homeassistant/components/risco/translations/nl.json @@ -16,6 +16,13 @@ "username": "Gebruikersnaam" } }, + "local": { + "data": { + "host": "Host", + "pin": "Pincode", + "port": "Poort" + } + }, "user": { "data": { "password": "Wachtwoord", diff --git a/homeassistant/components/samsungtv/translations/pt.json b/homeassistant/components/samsungtv/translations/pt.json index c0c3fb735c1..3bfc36fd84d 100644 --- a/homeassistant/components/samsungtv/translations/pt.json +++ b/homeassistant/components/samsungtv/translations/pt.json @@ -5,7 +5,7 @@ "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer", "cannot_connect": "Falha na liga\u00e7\u00e3o" }, - "flow_title": "", + "flow_title": "{device}", "step": { "user": { "data": { diff --git a/homeassistant/components/shelly/translations/ca.json b/homeassistant/components/shelly/translations/ca.json index a9b1347e62e..4dd4626b6dc 100644 --- a/homeassistant/components/shelly/translations/ca.json +++ b/homeassistant/components/shelly/translations/ca.json @@ -2,6 +2,8 @@ "config": { "abort": { "already_configured": "El dispositiu ja est\u00e0 configurat", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament", + "reauth_unsuccessful": "La re-autenticaci\u00f3 no ha tingut \u00e8xit, elimina la integraci\u00f3 i torna-la a configurar.", "unsupported_firmware": "El dispositiu utilitza una versi\u00f3 de programari no compatible." }, "error": { @@ -21,6 +23,12 @@ "username": "Nom d'usuari" } }, + "reauth_confirm": { + "data": { + "password": "Contrasenya", + "username": "Nom d'usuari" + } + }, "user": { "data": { "host": "Amfitri\u00f3" diff --git a/homeassistant/components/shelly/translations/en.json b/homeassistant/components/shelly/translations/en.json index f3e882c3016..dfc2e8aebec 100644 --- a/homeassistant/components/shelly/translations/en.json +++ b/homeassistant/components/shelly/translations/en.json @@ -2,6 +2,8 @@ "config": { "abort": { "already_configured": "Device is already configured", + "reauth_successful": "Re-authentication was successful", + "reauth_unsuccessful": "Re-authentication was unsuccessful, please remove the integration and set it up again.", "unsupported_firmware": "The device is using an unsupported firmware version." }, "error": { @@ -21,6 +23,12 @@ "username": "Username" } }, + "reauth_confirm": { + "data": { + "password": "Password", + "username": "Username" + } + }, "user": { "data": { "host": "Host" diff --git a/homeassistant/components/shelly/translations/es.json b/homeassistant/components/shelly/translations/es.json index f1ad9c7ffda..7acd73471b7 100644 --- a/homeassistant/components/shelly/translations/es.json +++ b/homeassistant/components/shelly/translations/es.json @@ -2,6 +2,8 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente", + "reauth_unsuccessful": "No se pudo volver a autenticar, por favor, elimina la integraci\u00f3n y vuelve a configurarla.", "unsupported_firmware": "El dispositivo est\u00e1 usando una versi\u00f3n de firmware no compatible." }, "error": { @@ -21,6 +23,12 @@ "username": "Nombre de usuario" } }, + "reauth_confirm": { + "data": { + "password": "Contrase\u00f1a", + "username": "Nombre de usuario" + } + }, "user": { "data": { "host": "Host" diff --git a/homeassistant/components/shelly/translations/fr.json b/homeassistant/components/shelly/translations/fr.json index 3ed78b53b63..be741fdac4c 100644 --- a/homeassistant/components/shelly/translations/fr.json +++ b/homeassistant/components/shelly/translations/fr.json @@ -2,6 +2,8 @@ "config": { "abort": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi", + "reauth_unsuccessful": "La r\u00e9authentification a \u00e9chou\u00e9, veuillez supprimer l'int\u00e9gration puis la configurer \u00e0 nouveau.", "unsupported_firmware": "L'appareil utilise une version de micrologiciel non prise en charge." }, "error": { @@ -21,6 +23,12 @@ "username": "Nom d'utilisateur" } }, + "reauth_confirm": { + "data": { + "password": "Mot de passe", + "username": "Nom d'utilisateur" + } + }, "user": { "data": { "host": "H\u00f4te" diff --git a/homeassistant/components/shelly/translations/he.json b/homeassistant/components/shelly/translations/he.json index 5eb76e4b55e..22ecaa0daf0 100644 --- a/homeassistant/components/shelly/translations/he.json +++ b/homeassistant/components/shelly/translations/he.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7", "unsupported_firmware": "\u05d4\u05d4\u05ea\u05e7\u05df \u05de\u05e9\u05ea\u05de\u05e9 \u05d1\u05d2\u05d9\u05e8\u05e1\u05ea \u05e7\u05d5\u05e9\u05d7\u05d4 \u05e9\u05d0\u05d9\u05e0\u05d4 \u05e0\u05ea\u05de\u05db\u05ea." }, "error": { @@ -20,6 +21,12 @@ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" } }, + "reauth_confirm": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + }, "user": { "data": { "host": "\u05de\u05d0\u05e8\u05d7" diff --git a/homeassistant/components/shelly/translations/id.json b/homeassistant/components/shelly/translations/id.json index 59af237d5d3..104494e01c6 100644 --- a/homeassistant/components/shelly/translations/id.json +++ b/homeassistant/components/shelly/translations/id.json @@ -2,6 +2,8 @@ "config": { "abort": { "already_configured": "Perangkat sudah dikonfigurasi", + "reauth_successful": "Autentikasi ulang berhasil", + "reauth_unsuccessful": "Autentikasi ulang tidak berhasil, hapus integrasi dan siapkan kembali.", "unsupported_firmware": "Perangkat menggunakan versi firmware yang tidak didukung." }, "error": { @@ -21,6 +23,12 @@ "username": "Nama Pengguna" } }, + "reauth_confirm": { + "data": { + "password": "Kata Sandi", + "username": "Nama Pengguna" + } + }, "user": { "data": { "host": "Host" diff --git a/homeassistant/components/shelly/translations/it.json b/homeassistant/components/shelly/translations/it.json index 7bd26761ad8..7b882246e66 100644 --- a/homeassistant/components/shelly/translations/it.json +++ b/homeassistant/components/shelly/translations/it.json @@ -2,6 +2,8 @@ "config": { "abort": { "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente", + "reauth_unsuccessful": "La riautenticazione non \u00e8 riuscita, rimuovi l'integrazione e configurala di nuovo.", "unsupported_firmware": "Il dispositivo utilizza una versione del firmware non supportata." }, "error": { @@ -21,6 +23,12 @@ "username": "Nome utente" } }, + "reauth_confirm": { + "data": { + "password": "Password", + "username": "Nome utente" + } + }, "user": { "data": { "host": "Host" diff --git a/homeassistant/components/shelly/translations/nl.json b/homeassistant/components/shelly/translations/nl.json index 2bc4c1df03e..70ffbad804b 100644 --- a/homeassistant/components/shelly/translations/nl.json +++ b/homeassistant/components/shelly/translations/nl.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd", + "reauth_successful": "Herauthenticatie geslaagd", "unsupported_firmware": "Het apparaat gebruikt een niet-ondersteunde firmwareversie." }, "error": { @@ -20,6 +21,12 @@ "username": "Gebruikersnaam" } }, + "reauth_confirm": { + "data": { + "password": "Wachtwoord", + "username": "Gebruikersnaam" + } + }, "user": { "data": { "host": "Host" diff --git a/homeassistant/components/shelly/translations/ru.json b/homeassistant/components/shelly/translations/ru.json index b80e58aa905..31b2a8ca9a1 100644 --- a/homeassistant/components/shelly/translations/ru.json +++ b/homeassistant/components/shelly/translations/ru.json @@ -2,6 +2,8 @@ "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.", + "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.", + "reauth_unsuccessful": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u0440\u043e\u0439\u0442\u0438 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u0443\u044e \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044e \u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0435\u0451 \u0441\u043d\u043e\u0432\u0430.", "unsupported_firmware": "\u0412 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u043d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u0430\u044f \u0432\u0435\u0440\u0441\u0438\u044f \u043f\u0440\u043e\u0448\u0438\u0432\u043a\u0438." }, "error": { @@ -21,6 +23,12 @@ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" } }, + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + } + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442" diff --git a/homeassistant/components/shelly/translations/zh-Hant.json b/homeassistant/components/shelly/translations/zh-Hant.json index a2727fcc933..728fcdccbfe 100644 --- a/homeassistant/components/shelly/translations/zh-Hant.json +++ b/homeassistant/components/shelly/translations/zh-Hant.json @@ -2,6 +2,8 @@ "config": { "abort": { "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f", + "reauth_unsuccessful": "\u91cd\u65b0\u9a57\u8b49\u5931\u6557\uff0c\u8acb\u79fb\u9664\u88dd\u7f6e\u4e26\u91cd\u65b0\u8a2d\u5b9a\u3002", "unsupported_firmware": "\u88dd\u7f6e\u4f7f\u7528\u7684\u97cc\u9ad4\u4e0d\u652f\u63f4\u3002" }, "error": { @@ -21,6 +23,12 @@ "username": "\u4f7f\u7528\u8005\u540d\u7a31" } }, + "reauth_confirm": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + } + }, "user": { "data": { "host": "\u4e3b\u6a5f\u7aef" diff --git a/homeassistant/components/skybell/translations/nl.json b/homeassistant/components/skybell/translations/nl.json index b937f595704..17c417b4ef9 100644 --- a/homeassistant/components/skybell/translations/nl.json +++ b/homeassistant/components/skybell/translations/nl.json @@ -10,6 +10,12 @@ "unknown": "Onverwachte fout" }, "step": { + "reauth_confirm": { + "data": { + "password": "Wachtwoord" + }, + "title": "Integratie herauthenticeren" + }, "user": { "data": { "email": "Email", diff --git a/homeassistant/components/somfy_mylink/translations/pt.json b/homeassistant/components/somfy_mylink/translations/pt.json index fe98366e28e..2baecf39acf 100644 --- a/homeassistant/components/somfy_mylink/translations/pt.json +++ b/homeassistant/components/somfy_mylink/translations/pt.json @@ -7,7 +7,7 @@ "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, - "flow_title": "" + "flow_title": "{mac} ({ip})" }, "options": { "abort": { diff --git a/homeassistant/components/switchbot/translations/nl.json b/homeassistant/components/switchbot/translations/nl.json index becb7173194..1860024e011 100644 --- a/homeassistant/components/switchbot/translations/nl.json +++ b/homeassistant/components/switchbot/translations/nl.json @@ -9,6 +9,14 @@ }, "flow_title": "{name}", "step": { + "confirm": { + "description": "Wilt u {name} instellen?" + }, + "password": { + "data": { + "password": "Wachtwoord" + } + }, "user": { "data": { "mac": "MAC-adres apparaat", diff --git a/homeassistant/components/thermopro/translations/nl.json b/homeassistant/components/thermopro/translations/nl.json index b4f5f4f85e5..a46f954fe5f 100644 --- a/homeassistant/components/thermopro/translations/nl.json +++ b/homeassistant/components/thermopro/translations/nl.json @@ -1,5 +1,10 @@ { "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "already_in_progress": "De configuratie is momenteel al bezig", + "no_devices_found": "Geen apparaten gevonden op het netwerk" + }, "flow_title": "{name}", "step": { "bluetooth_confirm": { diff --git a/homeassistant/components/wilight/translations/pt.json b/homeassistant/components/wilight/translations/pt.json index 7604ea73337..5953c977419 100644 --- a/homeassistant/components/wilight/translations/pt.json +++ b/homeassistant/components/wilight/translations/pt.json @@ -8,7 +8,7 @@ "flow_title": "WiLight: {name}", "step": { "confirm": { - "description": "Deseja configurar o WiLight {name} ? \n\n Suporta: {components}" + "description": "Deseja configurar o WiLight {name} ? \n Suporta: {components}" } } } diff --git a/homeassistant/components/xiaomi_miio/translations/select.nl.json b/homeassistant/components/xiaomi_miio/translations/select.nl.json index 0e93df956e9..8041e47ab3e 100644 --- a/homeassistant/components/xiaomi_miio/translations/select.nl.json +++ b/homeassistant/components/xiaomi_miio/translations/select.nl.json @@ -1,5 +1,9 @@ { "state": { + "xiaomi_miio__display_orientation": { + "left": "Links", + "right": "Rechts" + }, "xiaomi_miio__led_brightness": { "bright": "Helder", "dim": "Dim", diff --git a/homeassistant/components/yalexs_ble/translations/nl.json b/homeassistant/components/yalexs_ble/translations/nl.json index f8cb3d26757..a3c1aa2ead9 100644 --- a/homeassistant/components/yalexs_ble/translations/nl.json +++ b/homeassistant/components/yalexs_ble/translations/nl.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_in_progress": "De configuratie is momenteel al bezig" + "already_configured": "Apparaat is al geconfigureerd", + "already_in_progress": "De configuratie is momenteel al bezig", + "no_devices_found": "Geen apparaten gevonden op het netwerk" }, "error": { "cannot_connect": "Kan geen verbinding maken", diff --git a/homeassistant/components/zha/translations/nl.json b/homeassistant/components/zha/translations/nl.json index 4b28c5f7b07..dacddd201d9 100644 --- a/homeassistant/components/zha/translations/nl.json +++ b/homeassistant/components/zha/translations/nl.json @@ -10,9 +10,15 @@ }, "flow_title": "{name}", "step": { + "choose_automatic_backup": { + "title": "Automatische back-up herstellen" + }, "confirm": { "description": "Wilt u {name} instellen?" }, + "confirm_hardware": { + "description": "Wilt u {name} instellen?" + }, "pick_radio": { "data": { "radio_type": "Radio type" @@ -30,6 +36,9 @@ "title": "Instellingen" }, "upload_manual_backup": { + "data": { + "uploaded_backup_file": "Een bestand uploaden" + }, "title": "Upload een handmatige back-up" }, "user": { @@ -122,7 +131,17 @@ }, "flow_title": "{name}", "step": { + "choose_automatic_backup": { + "title": "Automatische back-up herstellen" + }, + "init": { + "description": "ZHA wordt gestopt. Wilt u doorgaan?", + "title": "ZHA opnieuw configureren" + }, "upload_manual_backup": { + "data": { + "uploaded_backup_file": "Een bestand uploaden" + }, "title": "Upload een handmatige back-up" } } diff --git a/homeassistant/components/zha/translations/pt.json b/homeassistant/components/zha/translations/pt.json index 075cde5efb7..7fe13349c9f 100644 --- a/homeassistant/components/zha/translations/pt.json +++ b/homeassistant/components/zha/translations/pt.json @@ -102,6 +102,9 @@ } }, "options": { + "abort": { + "single_instance_allowed": "Apenas uma configura\u00e7\u00e3o do ZHA \u00e9 permitida." + }, "step": { "init": { "description": "ZHA ser\u00e1 interrompido. Voc\u00ea deseja continuar?", From 697e7b3a201fab27eb3bdda1ed81c698193ac58f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 25 Sep 2022 20:53:20 -0400 Subject: [PATCH 711/955] TTS Cleanup and expose get audio (#79065) --- .../components/media_source/__init__.py | 3 +- homeassistant/components/tts/__init__.py | 168 ++++++++++++------ homeassistant/components/tts/media_source.py | 94 ++++++++-- tests/components/tts/test_init.py | 70 +++++++- 4 files changed, 250 insertions(+), 85 deletions(-) diff --git a/homeassistant/components/media_source/__init__.py b/homeassistant/components/media_source/__init__.py index a882798687e..47a5d7f6969 100644 --- a/homeassistant/components/media_source/__init__.py +++ b/homeassistant/components/media_source/__init__.py @@ -34,7 +34,7 @@ from .const import ( URI_SCHEME_REGEX, ) from .error import MediaSourceError, Unresolvable -from .models import BrowseMediaSource, MediaSourceItem, PlayMedia +from .models import BrowseMediaSource, MediaSource, MediaSourceItem, PlayMedia __all__ = [ "DOMAIN", @@ -46,6 +46,7 @@ __all__ = [ "PlayMedia", "MediaSourceItem", "Unresolvable", + "MediaSource", "MediaSourceError", "MEDIA_CLASS_MAP", "MEDIA_MIME_TYPES", diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index 7def2c84bc0..757c33e2653 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -11,7 +11,7 @@ import mimetypes import os from pathlib import Path import re -from typing import TYPE_CHECKING, Any, Optional, cast +from typing import TYPE_CHECKING, Any, Optional, TypedDict, cast from aiohttp import web import mutagen @@ -28,7 +28,6 @@ from homeassistant.components.media_player import ( SERVICE_PLAY_MEDIA, MediaType, ) -from homeassistant.components.media_source import generate_media_source_id from homeassistant.const import ( ATTR_ENTITY_ID, CONF_DESCRIPTION, @@ -48,6 +47,7 @@ from homeassistant.util.network import normalize_url from homeassistant.util.yaml import load_yaml from .const import DOMAIN +from .media_source import generate_media_source_id, media_source_id_to_kwargs _LOGGER = logging.getLogger(__name__) @@ -74,9 +74,6 @@ DEFAULT_CACHE = True DEFAULT_CACHE_DIR = "tts" DEFAULT_TIME_MEMORY = 300 -MEM_CACHE_FILENAME = "filename" -MEM_CACHE_VOICE = "voice" - SERVICE_CLEAR_CACHE = "clear_cache" SERVICE_SAY = "say" @@ -131,6 +128,24 @@ SCHEMA_SERVICE_SAY = vol.Schema( SCHEMA_SERVICE_CLEAR_CACHE = vol.Schema({}) +class TTSCache(TypedDict): + """Cached TTS file.""" + + filename: str + voice: bytes + + +async def async_get_media_source_audio( + hass: HomeAssistant, + media_source_id: str, +) -> tuple[str, bytes]: + """Get TTS audio as extension, data.""" + manager: SpeechManager = hass.data[DOMAIN] + return await manager.async_get_tts_audio( + **media_source_id_to_kwargs(media_source_id), + ) + + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up TTS.""" tts = SpeechManager(hass) @@ -197,21 +212,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_say_handle(service: ServiceCall) -> None: """Service handle for say.""" entity_ids = service.data[ATTR_ENTITY_ID] - message = service.data[ATTR_MESSAGE] - cache = service.data.get(ATTR_CACHE) - language = service.data.get(ATTR_LANGUAGE) - options = service.data.get(ATTR_OPTIONS) - - tts.process_options(p_type, language, options) - params = { - "message": message, - } - if cache is not None: - params["cache"] = "true" if cache else "false" - if language is not None: - params["language"] = language - if options is not None: - params.update(options) await hass.services.async_call( DOMAIN_MP, @@ -219,8 +219,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: { ATTR_ENTITY_ID: entity_ids, ATTR_MEDIA_CONTENT_ID: generate_media_source_id( - DOMAIN, - str(yarl.URL.build(path=p_type, query=params)), + hass, + engine=p_type, + message=service.data[ATTR_MESSAGE], + language=service.data.get(ATTR_LANGUAGE), + options=service.data.get(ATTR_OPTIONS), + cache=service.data.get(ATTR_CACHE), ), ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC, ATTR_MEDIA_ANNOUNCE: True, @@ -296,7 +300,7 @@ class SpeechManager: self.time_memory = DEFAULT_TIME_MEMORY self.base_url: str | None = None self.file_cache: dict[str, str] = {} - self.mem_cache: dict[str, dict[str, str | bytes]] = {} + self.mem_cache: dict[str, TTSCache] = {} async def async_init_cache( self, use_cache: bool, cache_dir: str, time_memory: int, base_url: str | None @@ -380,10 +384,11 @@ class SpeechManager: options = options or provider.default_options if options is not None: + supported_options = provider.supported_options or [] invalid_opts = [ opt_name for opt_name in options.keys() - if opt_name not in (provider.supported_options or []) + if opt_name not in supported_options ] if invalid_opts: raise HomeAssistantError(f"Invalid options found: {invalid_opts}") @@ -403,25 +408,25 @@ class SpeechManager: This method is a coroutine. """ language, options = self.process_options(engine, language, options) - options_key = _hash_options(options) if options else "-" - msg_hash = hashlib.sha1(bytes(message, "utf-8")).hexdigest() + cache_key = self._generate_cache_key(message, language, options, engine) use_cache = cache if cache is not None else self.use_cache - key = KEY_PATTERN.format( - msg_hash, language.replace("_", "-"), options_key, engine - ).lower() - # Is speech already in memory - if key in self.mem_cache: - filename = cast(str, self.mem_cache[key][MEM_CACHE_FILENAME]) + if cache_key in self.mem_cache: + filename = self.mem_cache[cache_key]["filename"] # Is file store in file cache - elif use_cache and key in self.file_cache: - filename = self.file_cache[key] - self.hass.async_create_task(self.async_file_to_mem(key)) + elif use_cache and cache_key in self.file_cache: + filename = self.file_cache[cache_key] + self.hass.async_create_task(self._async_file_to_mem(cache_key)) # Load speech from provider into memory else: - filename = await self.async_get_tts_audio( - engine, key, message, use_cache, language, options + filename = await self._async_get_tts_audio( + engine, + cache_key, + message, + use_cache, + language, + options, ) return f"/api/tts_proxy/{filename}" @@ -429,13 +434,54 @@ class SpeechManager: async def async_get_tts_audio( self, engine: str, - key: str, + message: str, + cache: bool | None = None, + language: str | None = None, + options: dict | None = None, + ) -> tuple[str, bytes]: + """Fetch TTS audio.""" + language, options = self.process_options(engine, language, options) + cache_key = self._generate_cache_key(message, language, options, engine) + use_cache = cache if cache is not None else self.use_cache + + # If we have the file, load it into memory if necessary + if cache_key not in self.mem_cache: + if use_cache and cache_key in self.file_cache: + await self._async_file_to_mem(cache_key) + else: + await self._async_get_tts_audio( + engine, cache_key, message, use_cache, language, options + ) + + extension = os.path.splitext(self.mem_cache[cache_key]["filename"])[1][1:] + data = self.mem_cache[cache_key]["voice"] + return extension, data + + @callback + def _generate_cache_key( + self, + message: str, + language: str, + options: dict | None, + engine: str, + ) -> str: + """Generate a cache key for a message.""" + options_key = _hash_options(options) if options else "-" + msg_hash = hashlib.sha1(bytes(message, "utf-8")).hexdigest() + return KEY_PATTERN.format( + msg_hash, language.replace("_", "-"), options_key, engine + ).lower() + + async def _async_get_tts_audio( + self, + engine: str, + cache_key: str, message: str, cache: bool, language: str, options: dict | None, ) -> str: - """Receive TTS and store for view in cache. + """Receive TTS, store for view in cache and return filename. This method is a coroutine. """ @@ -446,7 +492,7 @@ class SpeechManager: raise HomeAssistantError(f"No TTS from {engine} for '{message}'") # Create file infos - filename = f"{key}.{extension}".lower() + filename = f"{cache_key}.{extension}".lower() # Validate filename if not _RE_VOICE_FILE.match(filename): @@ -456,14 +502,18 @@ class SpeechManager: # Save to memory data = self.write_tags(filename, data, provider, message, language, options) - self._async_store_to_memcache(key, filename, data) + self._async_store_to_memcache(cache_key, filename, data) if cache: - self.hass.async_create_task(self.async_save_tts_audio(key, filename, data)) + self.hass.async_create_task( + self._async_save_tts_audio(cache_key, filename, data) + ) return filename - async def async_save_tts_audio(self, key: str, filename: str, data: bytes) -> None: + async def _async_save_tts_audio( + self, cache_key: str, filename: str, data: bytes + ) -> None: """Store voice data to file and file_cache. This method is a coroutine. @@ -477,17 +527,17 @@ class SpeechManager: try: await self.hass.async_add_executor_job(save_speech) - self.file_cache[key] = filename + self.file_cache[cache_key] = filename except OSError as err: _LOGGER.error("Can't write %s: %s", filename, err) - async def async_file_to_mem(self, key: str) -> None: + async def _async_file_to_mem(self, cache_key: str) -> None: """Load voice from file cache into memory. This method is a coroutine. """ - if not (filename := self.file_cache.get(key)): - raise HomeAssistantError(f"Key {key} not in file cache!") + if not (filename := self.file_cache.get(cache_key)): + raise HomeAssistantError(f"Key {cache_key} not in file cache!") voice_file = os.path.join(self.cache_dir, filename) @@ -499,20 +549,22 @@ class SpeechManager: try: data = await self.hass.async_add_executor_job(load_speech) except OSError as err: - del self.file_cache[key] + del self.file_cache[cache_key] raise HomeAssistantError(f"Can't read {voice_file}") from err - self._async_store_to_memcache(key, filename, data) + self._async_store_to_memcache(cache_key, filename, data) @callback - def _async_store_to_memcache(self, key: str, filename: str, data: bytes) -> None: + def _async_store_to_memcache( + self, cache_key: str, filename: str, data: bytes + ) -> None: """Store data to memcache and set timer to remove it.""" - self.mem_cache[key] = {MEM_CACHE_FILENAME: filename, MEM_CACHE_VOICE: data} + self.mem_cache[cache_key] = {"filename": filename, "voice": data} @callback def async_remove_from_mem() -> None: """Cleanup memcache.""" - self.mem_cache.pop(key, None) + self.mem_cache.pop(cache_key, None) self.hass.loop.call_later(self.time_memory, async_remove_from_mem) @@ -524,17 +576,17 @@ class SpeechManager: if not (record := _RE_VOICE_FILE.match(filename.lower())): raise HomeAssistantError("Wrong tts file format!") - key = KEY_PATTERN.format( + cache_key = KEY_PATTERN.format( record.group(1), record.group(2), record.group(3), record.group(4) ) - if key not in self.mem_cache: - if key not in self.file_cache: - raise HomeAssistantError(f"{key} not in cache!") - await self.async_file_to_mem(key) + if cache_key not in self.mem_cache: + if cache_key not in self.file_cache: + raise HomeAssistantError(f"{cache_key} not in cache!") + await self._async_file_to_mem(cache_key) content, _ = mimetypes.guess_type(filename) - return content, cast(bytes, self.mem_cache[key][MEM_CACHE_VOICE]) + return content, self.mem_cache[cache_key]["voice"] @staticmethod def write_tags( diff --git a/homeassistant/components/tts/media_source.py b/homeassistant/components/tts/media_source.py index eda64c804b8..c197632c11e 100644 --- a/homeassistant/components/tts/media_source.py +++ b/homeassistant/components/tts/media_source.py @@ -2,17 +2,18 @@ from __future__ import annotations import mimetypes -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, TypedDict from yarl import URL from homeassistant.components.media_player import BrowseError, MediaClass -from homeassistant.components.media_source.error import Unresolvable -from homeassistant.components.media_source.models import ( +from homeassistant.components.media_source import ( BrowseMediaSource, MediaSource, MediaSourceItem, PlayMedia, + Unresolvable, + generate_media_source_id as ms_generate_media_source_id, ) from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError @@ -29,6 +30,75 @@ async def async_get_media_source(hass: HomeAssistant) -> TTSMediaSource: return TTSMediaSource(hass) +@callback +def generate_media_source_id( + hass: HomeAssistant, + message: str, + engine: str | None = None, + language: str | None = None, + options: dict | None = None, + cache: bool | None = None, +) -> str: + """Generate a media source ID for text-to-speech.""" + manager: SpeechManager = hass.data[DOMAIN] + + if engine is not None: + pass + elif not manager.providers: + raise HomeAssistantError("No TTS providers available") + elif "cloud" in manager.providers: + engine = "cloud" + else: + engine = next(iter(manager.providers)) + + manager.process_options(engine, language, options) + params = { + "message": message, + } + if cache is not None: + params["cache"] = "true" if cache else "false" + if language is not None: + params["language"] = language + if options is not None: + params.update(options) + + return ms_generate_media_source_id( + DOMAIN, + str(URL.build(path=engine, query=params)), + ) + + +class MediaSourceOptions(TypedDict): + """Media source options.""" + + engine: str + message: str + language: str | None + options: dict | None + cache: bool | None + + +@callback +def media_source_id_to_kwargs(media_source_id: str) -> MediaSourceOptions: + """Turn a media source ID into options.""" + parsed = URL(media_source_id) + if "message" not in parsed.query: + raise Unresolvable("No message specified.") + + options = dict(parsed.query) + kwargs: MediaSourceOptions = { + "engine": parsed.name, + "message": options.pop("message"), + "language": options.pop("language", None), + "options": options, + "cache": None, + } + if "cache" in options: + kwargs["cache"] = options.pop("cache") == "true" + + return kwargs + + class TTSMediaSource(MediaSource): """Provide text-to-speech providers as media sources.""" @@ -41,24 +111,12 @@ class TTSMediaSource(MediaSource): async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia: """Resolve media to a url.""" - parsed = URL(item.identifier) - if "message" not in parsed.query: - raise Unresolvable("No message specified.") - - options = dict(parsed.query) - kwargs: dict[str, Any] = { - "engine": parsed.name, - "message": options.pop("message"), - "language": options.pop("language", None), - "options": options, - } - if "cache" in options: - kwargs["cache"] = options.pop("cache") == "true" - manager: SpeechManager = self.hass.data[DOMAIN] try: - url = await manager.async_get_url_path(**kwargs) + url = await manager.async_get_url_path( + **media_source_id_to_kwargs(item.identifier) + ) except HomeAssistantError as err: raise Unresolvable(str(err)) from err diff --git a/tests/components/tts/test_init.py b/tests/components/tts/test_init.py index 61c5ab00180..f521cbda58d 100644 --- a/tests/components/tts/test_init.py +++ b/tests/components/tts/test_init.py @@ -49,13 +49,18 @@ async def internal_url_mock(hass): ) -async def test_setup_component_demo(hass): +@pytest.fixture +async def setup_tts(hass): + """Mock TTS.""" + with patch("homeassistant.components.demo.async_setup", return_value=True): + assert await async_setup_component( + hass, tts.DOMAIN, {"tts": {"platform": "demo"}} + ) + await hass.async_block_till_done() + + +async def test_setup_component_demo(hass, setup_tts): """Set up the demo platform with defaults.""" - config = {tts.DOMAIN: {"platform": "demo"}} - - with assert_setup_component(1, tts.DOMAIN): - assert await async_setup_component(hass, tts.DOMAIN, config) - assert hass.services.has_service(tts.DOMAIN, "demo_say") assert hass.services.has_service(tts.DOMAIN, "clear_cache") assert f"{tts.DOMAIN}.demo" in hass.config.components @@ -421,12 +426,14 @@ async def test_setup_component_and_test_service_with_receive_voice( with assert_setup_component(1, tts.DOMAIN): assert await async_setup_component(hass, tts.DOMAIN, config) + message = "There is someone at the door." + await hass.services.async_call( tts.DOMAIN, "demo_say", { "entity_id": "media_player.something", - tts.ATTR_MESSAGE: "There is someone at the door.", + tts.ATTR_MESSAGE: message, }, blocking=True, ) @@ -440,13 +447,19 @@ async def test_setup_component_and_test_service_with_receive_voice( "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3", demo_data, demo_provider, - "There is someone at the door.", + message, "en", None, ) assert req.status == HTTPStatus.OK assert await req.read() == demo_data + extension, data = await tts.async_get_media_source_audio( + hass, calls[0].data[ATTR_MEDIA_CONTENT_ID] + ) + assert extension == "mp3" + assert demo_data == data + async def test_setup_component_and_test_service_with_receive_voice_german( hass, demo_provider, hass_client @@ -736,3 +749,44 @@ def test_invalid_base_url(value): """Test we catch bad base urls.""" with pytest.raises(vol.Invalid): tts.valid_base_url(value) + + +@pytest.mark.parametrize( + "engine,language,options,cache,result_engine,result_query", + ( + (None, None, None, None, "demo", ""), + (None, "de", None, None, "demo", "language=de"), + (None, "de", {"voice": "henk"}, None, "demo", "language=de&voice=henk"), + (None, "de", None, True, "demo", "cache=true&language=de"), + ), +) +async def test_generate_media_source_id( + hass, setup_tts, engine, language, options, cache, result_engine, result_query +): + """Test generating a media source ID.""" + media_source_id = tts.generate_media_source_id( + hass, "msg", engine, language, options, cache + ) + + assert media_source_id.startswith("media-source://tts/") + _, _, engine_query = media_source_id.rpartition("/") + engine, _, query = engine_query.partition("?") + assert engine == result_engine + assert query.startswith("message=msg") + assert query[12:] == result_query + + +@pytest.mark.parametrize( + "engine,language,options", + ( + ("not-loaded-engine", None, None), + (None, "unsupported-language", None), + (None, None, {"option": "not-supported"}), + ), +) +async def test_generate_media_source_id_invalid_options( + hass, setup_tts, engine, language, options +): + """Test generating a media source ID.""" + with pytest.raises(HomeAssistantError): + tts.generate_media_source_id(hass, "msg", engine, language, options, None) From f8d42e92463c32a86bb30f4c5786fe295ed5d657 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Mon, 26 Sep 2022 02:57:14 +0200 Subject: [PATCH 712/955] Add nibe heat pump select entities (#78942) --- .coveragerc | 1 + .../components/nibe_heatpump/__init__.py | 2 +- .../components/nibe_heatpump/select.py | 47 +++++++++++++++++++ 3 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/nibe_heatpump/select.py diff --git a/.coveragerc b/.coveragerc index 0a4b3803cd2..369944d60b0 100644 --- a/.coveragerc +++ b/.coveragerc @@ -835,6 +835,7 @@ omit = homeassistant/components/nfandroidtv/__init__.py homeassistant/components/nfandroidtv/notify.py homeassistant/components/nibe_heatpump/__init__.py + homeassistant/components/nibe_heatpump/select.py homeassistant/components/nibe_heatpump/sensor.py homeassistant/components/nibe_heatpump/binary_sensor.py homeassistant/components/niko_home_control/light.py diff --git a/homeassistant/components/nibe_heatpump/__init__.py b/homeassistant/components/nibe_heatpump/__init__.py index 79b6b50e843..ed3bc649453 100644 --- a/homeassistant/components/nibe_heatpump/__init__.py +++ b/homeassistant/components/nibe_heatpump/__init__.py @@ -39,7 +39,7 @@ from .const import ( LOGGER, ) -PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR] +PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SELECT, Platform.SENSOR] COIL_READ_RETRIES = 5 diff --git a/homeassistant/components/nibe_heatpump/select.py b/homeassistant/components/nibe_heatpump/select.py new file mode 100644 index 00000000000..27df1980287 --- /dev/null +++ b/homeassistant/components/nibe_heatpump/select.py @@ -0,0 +1,47 @@ +"""The Nibe Heat Pump select.""" +from __future__ import annotations + +from nibe.coil import Coil + +from homeassistant.components.select import ENTITY_ID_FORMAT, SelectEntity +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 DOMAIN, CoilEntity, Coordinator + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up platform.""" + + coordinator: Coordinator = hass.data[DOMAIN][config_entry.entry_id] + + async_add_entities( + Select(coordinator, coil) + for coil in coordinator.coils + if coil.is_writable and coil.mappings and not coil.is_boolean + ) + + +class Select(CoilEntity, SelectEntity): + """Select entity.""" + + _attr_entity_category = EntityCategory.CONFIG + + def __init__(self, coordinator: Coordinator, coil: Coil) -> None: + """Initialize entity.""" + 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: + self._attr_current_option = coil.value + + async def async_select_option(self, option: str) -> None: + """Support writing value.""" + await self._async_write_coil(option) From 3ad5d799c69727305b008226589e7f33d2486602 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 25 Sep 2022 15:02:29 -1000 Subject: [PATCH 713/955] Bump dbus-fast to 1.14.0 (#79063) --- 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 acf26c59e2e..636d5925651 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -10,7 +10,7 @@ "bleak-retry-connector==2.0.2", "bluetooth-adapters==0.5.1", "bluetooth-auto-recovery==0.3.3", - "dbus-fast==1.13.0" + "dbus-fast==1.14.0" ], "codeowners": ["@bdraco"], "config_flow": true, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b285f568824..c2e38e09345 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ bluetooth-auto-recovery==0.3.3 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==38.0.1 -dbus-fast==1.13.0 +dbus-fast==1.14.0 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.3.0 diff --git a/requirements_all.txt b/requirements_all.txt index 894ca325b44..8805209e6f4 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.13.0 +dbus-fast==1.14.0 # homeassistant.components.debugpy debugpy==1.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9d771174d23..1ce12de4a86 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -417,7 +417,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.13.0 +dbus-fast==1.14.0 # homeassistant.components.debugpy debugpy==1.6.3 From c1bc26b413b55ec74a0a208815f79b92660eaadc Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 26 Sep 2022 03:14:18 +0200 Subject: [PATCH 714/955] Finish migration of recorder to unit conversion (#78985) --- homeassistant/components/sensor/recorder.py | 94 +++++---------------- 1 file changed, 21 insertions(+), 73 deletions(-) diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 5ce764cfc90..f45a99e3a17 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -2,7 +2,7 @@ from __future__ import annotations from collections import defaultdict -from collections.abc import Callable, Iterable, MutableMapping +from collections.abc import Iterable, MutableMapping import datetime import itertools import logging @@ -23,27 +23,7 @@ from homeassistant.components.recorder.models import ( StatisticMetaData, StatisticResult, ) -from homeassistant.const import ( - ATTR_DEVICE_CLASS, - ATTR_UNIT_OF_MEASUREMENT, - ENERGY_KILO_WATT_HOUR, - ENERGY_MEGA_WATT_HOUR, - ENERGY_WATT_HOUR, - POWER_KILO_WATT, - POWER_WATT, - PRESSURE_BAR, - PRESSURE_HPA, - PRESSURE_INHG, - PRESSURE_KPA, - PRESSURE_MBAR, - PRESSURE_PA, - PRESSURE_PSI, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, - TEMP_KELVIN, - VOLUME_CUBIC_FEET, - VOLUME_CUBIC_METERS, -) +from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT from homeassistant.core import HomeAssistant, State from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity import entity_sources @@ -84,47 +64,6 @@ UNIT_CONVERTERS: dict[str, type[BaseUnitConverter]] = { SensorDeviceClass.GAS: VolumeConverter, } -UNIT_CONVERSIONS: dict[str, dict[str, Callable]] = { - # Convert energy to kWh - SensorDeviceClass.ENERGY: { - ENERGY_KILO_WATT_HOUR: lambda x: x - / EnergyConverter.UNIT_CONVERSION[ENERGY_KILO_WATT_HOUR], - ENERGY_MEGA_WATT_HOUR: lambda x: x - / EnergyConverter.UNIT_CONVERSION[ENERGY_MEGA_WATT_HOUR], - ENERGY_WATT_HOUR: lambda x: x - / EnergyConverter.UNIT_CONVERSION[ENERGY_WATT_HOUR], - }, - # Convert power to W - SensorDeviceClass.POWER: { - POWER_WATT: lambda x: x / PowerConverter.UNIT_CONVERSION[POWER_WATT], - POWER_KILO_WATT: lambda x: x / PowerConverter.UNIT_CONVERSION[POWER_KILO_WATT], - }, - # Convert pressure to Pa - # Note: PressureConverter.convert is bypassed to avoid redundant error checking - SensorDeviceClass.PRESSURE: { - PRESSURE_BAR: lambda x: x / PressureConverter.UNIT_CONVERSION[PRESSURE_BAR], - PRESSURE_HPA: lambda x: x / PressureConverter.UNIT_CONVERSION[PRESSURE_HPA], - PRESSURE_INHG: lambda x: x / PressureConverter.UNIT_CONVERSION[PRESSURE_INHG], - PRESSURE_KPA: lambda x: x / PressureConverter.UNIT_CONVERSION[PRESSURE_KPA], - PRESSURE_MBAR: lambda x: x / PressureConverter.UNIT_CONVERSION[PRESSURE_MBAR], - PRESSURE_PA: lambda x: x / PressureConverter.UNIT_CONVERSION[PRESSURE_PA], - PRESSURE_PSI: lambda x: x / PressureConverter.UNIT_CONVERSION[PRESSURE_PSI], - }, - # Convert temperature to °C - # Note: TemperatureConverter.convert is bypassed to avoid redundant error checking - SensorDeviceClass.TEMPERATURE: { - TEMP_CELSIUS: lambda x: x, - TEMP_FAHRENHEIT: TemperatureConverter.fahrenheit_to_celsius, - TEMP_KELVIN: TemperatureConverter.kelvin_to_celsius, - }, - # Convert volume to cubic meter - SensorDeviceClass.GAS: { - VOLUME_CUBIC_METERS: lambda x: x, - VOLUME_CUBIC_FEET: lambda x: x - / VolumeConverter.UNIT_CONVERSION[VOLUME_CUBIC_FEET], - }, -} - # Keep track of entities for which a warning about decreasing value has been logged SEEN_DIP = "sensor_seen_total_increasing_dip" WARN_DIP = "sensor_warn_total_increasing_dip" @@ -212,9 +151,9 @@ def _normalize_states( entity_id: str, ) -> tuple[str | None, str | None, list[tuple[float, State]]]: """Normalize units.""" - state_unit = None + state_unit: str | None = None - if device_class not in UNIT_CONVERSIONS: + if device_class not in UNIT_CONVERTERS: # We're not normalizing this device class, return the state as they are fstates = [] for state in entity_history: @@ -250,6 +189,7 @@ def _normalize_states( state_unit = fstates[0][1].attributes.get(ATTR_UNIT_OF_MEASUREMENT) return state_unit, state_unit, fstates + converter = UNIT_CONVERTERS[device_class] fstates = [] for state in entity_history: @@ -259,7 +199,7 @@ def _normalize_states( continue state_unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) # Exclude unsupported units from statistics - if state_unit not in UNIT_CONVERSIONS[device_class]: + if state_unit not in converter.VALID_UNITS: if WARN_UNSUPPORTED_UNIT not in hass.data: hass.data[WARN_UNSUPPORTED_UNIT] = set() if entity_id not in hass.data[WARN_UNSUPPORTED_UNIT]: @@ -272,7 +212,14 @@ def _normalize_states( ) continue - fstates.append((UNIT_CONVERSIONS[device_class][state_unit](fstate), state)) + fstates.append( + ( + converter.convert( + fstate, from_unit=state_unit, to_unit=converter.NORMALIZED_UNIT + ), + state, + ) + ) return UNIT_CONVERTERS[device_class].NORMALIZED_UNIT, state_unit, fstates @@ -655,7 +602,7 @@ def list_statistic_ids( ): continue - if device_class not in UNIT_CONVERSIONS: + if device_class not in UNIT_CONVERTERS: result[state.entity_id] = { "has_mean": "mean" in provided_statistics, "has_sum": "sum" in provided_statistics, @@ -667,10 +614,11 @@ def list_statistic_ids( } continue - if state_unit not in UNIT_CONVERSIONS[device_class]: + converter = UNIT_CONVERTERS[device_class] + if state_unit not in converter.VALID_UNITS: continue - statistics_unit = UNIT_CONVERTERS[device_class].NORMALIZED_UNIT + statistics_unit = converter.NORMALIZED_UNIT result[state.entity_id] = { "has_mean": "mean" in provided_statistics, "has_sum": "sum" in provided_statistics, @@ -721,7 +669,7 @@ def validate_statistics( ) metadata_unit = metadata[1]["unit_of_measurement"] - if device_class not in UNIT_CONVERSIONS: + if device_class not in UNIT_CONVERTERS: if state_unit != metadata_unit: # The unit has changed validation_result[entity_id].append( @@ -761,8 +709,8 @@ def validate_statistics( if ( state_class in STATE_CLASSES - and device_class in UNIT_CONVERSIONS - and state_unit not in UNIT_CONVERSIONS[device_class] + and device_class in UNIT_CONVERTERS + and state_unit not in UNIT_CONVERTERS[device_class].VALID_UNITS ): # The unit in the state is not supported for this device class validation_result[entity_id].append( From 92612c9fe3f12727caaa37ed8dcd48a47d764b0d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 25 Sep 2022 15:31:56 -1000 Subject: [PATCH 715/955] Add RSSI sensor to HomeKit Controller (#78906) --- .../homekit_controller/connection.py | 19 ++++-- .../components/homekit_controller/sensor.py | 65 ++++++++++++++++++- tests/components/homekit_controller/common.py | 35 ++++++++-- .../homekit_controller/test_sensor.py | 31 ++++++++- 4 files changed, 133 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index 8afbe6a70e4..05a0a589bf1 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -231,6 +231,9 @@ class HKDevice: self.async_update_available_state, timedelta(seconds=BLE_AVAILABILITY_CHECK_INTERVAL), ) + # BLE devices always get an RSSI sensor as well + if "sensor" not in self.platforms: + await self.async_load_platform("sensor") async def async_add_new_entities(self) -> None: """Add new entities to Home Assistant.""" @@ -455,7 +458,7 @@ class HKDevice: self.entities.append((accessory.aid, None, None)) break - def add_char_factory(self, add_entities_cb) -> None: + def add_char_factory(self, add_entities_cb: AddCharacteristicCb) -> None: """Add a callback to run when discovering new entities for accessories.""" self.char_factories.append(add_entities_cb) self._add_new_entities_for_char([add_entities_cb]) @@ -471,7 +474,7 @@ class HKDevice: self.entities.append((accessory.aid, service.iid, char.iid)) break - def add_listener(self, add_entities_cb) -> None: + def add_listener(self, add_entities_cb: AddServiceCb) -> None: """Add a callback to run when discovering new entities for services.""" self.listeners.append(add_entities_cb) self._add_new_entities([add_entities_cb]) @@ -513,22 +516,24 @@ class HKDevice: async def async_load_platforms(self) -> None: """Load any platforms needed by this HomeKit device.""" - tasks = [] + to_load: set[str] = set() for accessory in self.entity_map.accessories: for service in accessory.services: if service.type in HOMEKIT_ACCESSORY_DISPATCH: platform = HOMEKIT_ACCESSORY_DISPATCH[service.type] if platform not in self.platforms: - tasks.append(self.async_load_platform(platform)) + to_load.add(platform) for char in service.characteristics: if char.type in CHARACTERISTIC_PLATFORMS: platform = CHARACTERISTIC_PLATFORMS[char.type] if platform not in self.platforms: - tasks.append(self.async_load_platform(platform)) + to_load.add(platform) - if tasks: - await asyncio.gather(*tasks) + if to_load: + await asyncio.gather( + *[self.async_load_platform(platform) for platform in to_load] + ) @callback def async_update_available_state(self, *_: Any) -> None: diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py index 3195cf6ee50..564eb5ba9c6 100644 --- a/homeassistant/components/homekit_controller/sensor.py +++ b/homeassistant/components/homekit_controller/sensor.py @@ -3,11 +3,14 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass +import logging +from aiohomekit.model import Accessory, Transport from aiohomekit.model.characteristics import Characteristic, CharacteristicsTypes from aiohomekit.model.characteristics.const import ThreadNodeCapabilities, ThreadStatus from aiohomekit.model.services import Service, ServicesTypes +from homeassistant.components.bluetooth import async_ble_device_from_address from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, @@ -25,6 +28,7 @@ from homeassistant.const import ( PERCENTAGE, POWER_WATT, PRESSURE_HPA, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant, callback @@ -37,6 +41,8 @@ from .connection import HKDevice from .entity import CharacteristicEntity, HomeKitEntity from .utils import folded_name +_LOGGER = logging.getLogger(__name__) + @dataclass class HomeKitSensorEntityDescription(SensorEntityDescription): @@ -524,6 +530,45 @@ REQUIRED_CHAR_BY_TYPE = { } +class RSSISensor(HomeKitEntity, SensorEntity): + """HomeKit Controller RSSI sensor.""" + + _attr_device_class = SensorDeviceClass.SIGNAL_STRENGTH + _attr_entity_category = EntityCategory.DIAGNOSTIC + _attr_entity_registry_enabled_default = False + _attr_has_entity_name = True + _attr_native_unit_of_measurement = SIGNAL_STRENGTH_DECIBELS_MILLIWATT + _attr_should_poll = False + + def get_characteristic_types(self) -> list[str]: + """Define the homekit characteristics the entity cares about.""" + return [] + + @property + def available(self) -> bool: + """Return if the bluetooth device is available.""" + address = self._accessory.pairing_data["AccessoryAddress"] + return async_ble_device_from_address(self.hass, address) is not None + + @property + def name(self) -> str: + """Return the name of the sensor.""" + return "Signal strength" + + @property + def unique_id(self) -> str: + """Return the ID of this device.""" + serial = self.accessory_info.value(CharacteristicsTypes.SERIAL_NUMBER) + return f"homekit-{serial}-rssi" + + @property + def native_value(self) -> int | None: + """Return the current rssi value.""" + address = self._accessory.pairing_data["AccessoryAddress"] + ble_device = async_ble_device_from_address(self.hass, address) + return ble_device.rssi if ble_device else None + + async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -531,7 +576,7 @@ async def async_setup_entry( ) -> None: """Set up Homekit sensors.""" hkid = config_entry.data["AccessoryPairingID"] - conn = hass.data[KNOWN_DEVICES][hkid] + conn: HKDevice = hass.data[KNOWN_DEVICES][hkid] @callback def async_add_service(service: Service) -> bool: @@ -542,7 +587,7 @@ async def async_setup_entry( ) and not service.has(required_char): return False info = {"aid": service.accessory.aid, "iid": service.iid} - async_add_entities([entity_class(conn, info)], True) + async_add_entities([entity_class(conn, info)]) return True conn.add_listener(async_add_service) @@ -554,8 +599,22 @@ async def async_setup_entry( if description.probe and not description.probe(char): return False info = {"aid": char.service.accessory.aid, "iid": char.service.iid} - async_add_entities([SimpleSensor(conn, info, char, description)], True) + async_add_entities([SimpleSensor(conn, info, char, description)]) return True conn.add_char_factory(async_add_characteristic) + + @callback + def async_add_accessory(accessory: Accessory) -> bool: + if conn.pairing.transport != Transport.BLE: + return False + + accessory_info = accessory.services.first( + service_type=ServicesTypes.ACCESSORY_INFORMATION + ) + info = {"aid": accessory.aid, "iid": accessory_info.iid} + async_add_entities([RSSISensor(conn, info)]) + return True + + conn.add_accessory_factory(async_add_accessory) diff --git a/tests/components/homekit_controller/common.py b/tests/components/homekit_controller/common.py index fd543d55ffb..07cc2b5cae7 100644 --- a/tests/components/homekit_controller/common.py +++ b/tests/components/homekit_controller/common.py @@ -26,6 +26,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, State, callback from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -42,6 +43,19 @@ logger = logging.getLogger(__name__) # Root device in test harness always has an accessory id of this HUB_TEST_ACCESSORY_ID: Final[str] = "00:00:00:00:00:00:aid:1" +TEST_ACCESSORY_ADDRESS = "AA:BB:CC:DD:EE:FF" + + +TEST_DEVICE_SERVICE_INFO = BluetoothServiceInfo( + name="test_accessory", + address=TEST_ACCESSORY_ADDRESS, + rssi=-56, + manufacturer_data={}, + service_uuids=["0000ec88-0000-1000-8000-00805f9b34fb"], + service_data={}, + source="local", +) + @dataclass class EntityTestInfo: @@ -182,15 +196,17 @@ async def setup_platform(hass): return await async_get_controller(hass) -async def setup_test_accessories(hass, accessories): +async def setup_test_accessories(hass, accessories, connection=None): """Load a fake homekit device based on captured JSON profile.""" fake_controller = await setup_platform(hass) return await setup_test_accessories_with_controller( - hass, accessories, fake_controller + hass, accessories, fake_controller, connection ) -async def setup_test_accessories_with_controller(hass, accessories, fake_controller): +async def setup_test_accessories_with_controller( + hass, accessories, fake_controller, connection=None +): """Load a fake homekit device based on captured JSON profile.""" pairing_id = "00:00:00:00:00:00" @@ -200,11 +216,16 @@ async def setup_test_accessories_with_controller(hass, accessories, fake_control accessories_obj.add_accessory(accessory) pairing = await fake_controller.add_paired_device(accessories_obj, pairing_id) + data = {"AccessoryPairingID": pairing_id} + if connection == "BLE": + data["Connection"] = "BLE" + data["AccessoryAddress"] = TEST_ACCESSORY_ADDRESS + config_entry = MockConfigEntry( version=1, domain="homekit_controller", entry_id="TestData", - data={"AccessoryPairingID": pairing_id}, + data=data, title="test", ) config_entry.add_to_hass(hass) @@ -250,7 +271,9 @@ async def device_config_changed(hass, accessories): await hass.async_block_till_done() -async def setup_test_component(hass, setup_accessory, capitalize=False, suffix=None): +async def setup_test_component( + hass, setup_accessory, capitalize=False, suffix=None, connection=None +): """Load a fake homekit accessory based on a homekit accessory model. If capitalize is True, property names will be in upper case. @@ -271,7 +294,7 @@ async def setup_test_component(hass, setup_accessory, capitalize=False, suffix=N assert domain, "Cannot map test homekit services to Home Assistant domain" - config_entry, pairing = await setup_test_accessories(hass, [accessory]) + config_entry, pairing = await setup_test_accessories(hass, [accessory], connection) entity = "testdevice" if suffix is None else f"testdevice_{suffix}" return Helper(hass, ".".join((domain, entity)), pairing, accessory, config_entry) diff --git a/tests/components/homekit_controller/test_sensor.py b/tests/components/homekit_controller/test_sensor.py index 0adfec470c4..4bd6612026c 100644 --- a/tests/components/homekit_controller/test_sensor.py +++ b/tests/components/homekit_controller/test_sensor.py @@ -1,8 +1,12 @@ """Basic checks for HomeKit sensor.""" +from unittest.mock import patch + +from aiohomekit.model import Transport from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.characteristics.const import ThreadNodeCapabilities, ThreadStatus from aiohomekit.model.services import ServicesTypes from aiohomekit.protocol.statuscodes import HapStatusCode +from aiohomekit.testing import FakePairing from homeassistant.components.homekit_controller.sensor import ( thread_node_capability_to_str, @@ -10,7 +14,9 @@ from homeassistant.components.homekit_controller.sensor import ( ) from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass -from .common import Helper, setup_test_component +from .common import TEST_DEVICE_SERVICE_INFO, Helper, setup_test_component + +from tests.components.bluetooth import inject_bluetooth_service_info def create_temperature_sensor_service(accessory): @@ -349,3 +355,26 @@ def test_thread_status_to_str(): assert thread_status_to_str(ThreadStatus.JOINING) == "joining" assert thread_status_to_str(ThreadStatus.DETACHED) == "detached" assert thread_status_to_str(ThreadStatus.DISABLED) == "disabled" + + +async def test_rssi_sensor( + hass, utcnow, entity_registry_enabled_by_default, enable_bluetooth +): + """Test an rssi sensor.""" + + inject_bluetooth_service_info(hass, TEST_DEVICE_SERVICE_INFO) + + class FakeBLEPairing(FakePairing): + """Fake BLE pairing.""" + + @property + def transport(self): + return Transport.BLE + + with patch("aiohomekit.testing.FakePairing", FakeBLEPairing): + # Any accessory will do for this test, but we need at least + # one or the rssi sensor will not be created + await setup_test_component( + hass, create_battery_level_sensor, suffix="battery", connection="BLE" + ) + assert hass.states.get("sensor.testdevice_signal_strength").state == "-56" From 1b17c83095333a5365a9f55f3f53ec10167d0a00 Mon Sep 17 00:00:00 2001 From: Paul Annekov Date: Mon, 26 Sep 2022 04:49:55 +0300 Subject: [PATCH 716/955] More details about SMS modem (#75694) --- homeassistant/components/sms/const.py | 63 --------------- homeassistant/components/sms/gateway.py | 40 +++++++++- homeassistant/components/sms/sensor.py | 100 ++++++++++++++++++------ 3 files changed, 115 insertions(+), 88 deletions(-) diff --git a/homeassistant/components/sms/const.py b/homeassistant/components/sms/const.py index d055894f402..5c7a2ce86a4 100644 --- a/homeassistant/components/sms/const.py +++ b/homeassistant/components/sms/const.py @@ -1,9 +1,4 @@ """Constants for sms Component.""" -from typing import Final - -from homeassistant.components.sensor import SensorDeviceClass, SensorEntityDescription -from homeassistant.const import PERCENTAGE, SIGNAL_STRENGTH_DECIBELS -from homeassistant.helpers.entity import EntityCategory DOMAIN = "sms" SMS_GATEWAY = "SMS_GATEWAY" @@ -38,61 +33,3 @@ DEFAULT_BAUD_SPEEDS = [ {"value": "76800", "label": "76800"}, {"value": "115200", "label": "115200"}, ] - -SIGNAL_SENSORS: Final[dict[str, SensorEntityDescription]] = { - "SignalStrength": SensorEntityDescription( - key="SignalStrength", - name="Signal Strength", - device_class=SensorDeviceClass.SIGNAL_STRENGTH, - entity_category=EntityCategory.DIAGNOSTIC, - native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, - entity_registry_enabled_default=False, - ), - "SignalPercent": SensorEntityDescription( - key="SignalPercent", - icon="mdi:signal-cellular-3", - name="Signal Percent", - native_unit_of_measurement=PERCENTAGE, - entity_registry_enabled_default=True, - ), - "BitErrorRate": SensorEntityDescription( - key="BitErrorRate", - name="Bit Error Rate", - entity_category=EntityCategory.DIAGNOSTIC, - native_unit_of_measurement=PERCENTAGE, - entity_registry_enabled_default=False, - ), -} - -NETWORK_SENSORS: Final[dict[str, SensorEntityDescription]] = { - "NetworkName": SensorEntityDescription( - key="NetworkName", - name="Network Name", - entity_category=EntityCategory.DIAGNOSTIC, - entity_registry_enabled_default=False, - ), - "State": SensorEntityDescription( - key="State", - name="Network Status", - entity_registry_enabled_default=True, - ), - "NetworkCode": SensorEntityDescription( - key="NetworkCode", - name="GSM network code", - entity_category=EntityCategory.DIAGNOSTIC, - entity_registry_enabled_default=False, - ), - "CID": SensorEntityDescription( - key="CID", - name="Cell ID", - icon="mdi:radio-tower", - entity_category=EntityCategory.DIAGNOSTIC, - entity_registry_enabled_default=False, - ), - "LAC": SensorEntityDescription( - key="LAC", - name="Local Area Code", - entity_category=EntityCategory.DIAGNOSTIC, - entity_registry_enabled_default=False, - ), -} diff --git a/homeassistant/components/sms/gateway.py b/homeassistant/components/sms/gateway.py index 7a2f095abd1..36ada5421e0 100644 --- a/homeassistant/components/sms/gateway.py +++ b/homeassistant/components/sms/gateway.py @@ -21,10 +21,16 @@ class Gateway: self._worker.configure(config) self._hass = hass self._first_pull = True + self.manufacturer = None + self.model = None + self.firmware = None async def init_async(self): - """Initialize the sms gateway asynchronously.""" + """Initialize the sms gateway asynchronously. This method is also called in config flow to verify connection.""" await self._worker.init_async() + self.manufacturer = await self.get_manufacturer_async() + self.model = await self.get_model_async() + self.firmware = await self.get_firmware_async() def sms_pull(self, state_machine): """Pull device. @@ -156,7 +162,37 @@ class Gateway: async def get_network_info_async(self): """Get the current network info of the modem.""" - return await self._worker.get_network_info_async() + network_info = await self._worker.get_network_info_async() + # Looks like there is a bug and it's empty for any modem https://github.com/gammu/python-gammu/issues/31, so try workaround + if not network_info["NetworkName"]: + network_info["NetworkName"] = gammu.GSMNetworks.get( + network_info["NetworkCode"] + ) + return network_info + + async def get_manufacturer_async(self): + """Get the manufacturer of the modem.""" + return await self._worker.get_manufacturer_async() + + async def get_model_async(self): + """Get the model of the modem.""" + model = await self._worker.get_model_async() + if not model or not model[0]: + return + display = model[0] # Identification model + if model[1]: # Real model + display = f"{display} ({model[1]})" + return display + + async def get_firmware_async(self): + """Get the firmware information of the modem.""" + firmware = await self._worker.get_firmware_async() + if not firmware or not firmware[0]: + return + display = firmware[0] # Version + if firmware[1]: # Date + display = f"{display} ({firmware[1]})" + return display async def terminate_async(self): """Terminate modem connection.""" diff --git a/homeassistant/components/sms/sensor.py b/homeassistant/components/sms/sensor.py index de20a5b5d0f..f276b9a9e9e 100644 --- a/homeassistant/components/sms/sensor.py +++ b/homeassistant/components/sms/sensor.py @@ -1,19 +1,78 @@ """Support for SMS dongle sensor.""" -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import PERCENTAGE, SIGNAL_STRENGTH_DECIBELS from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import ( - DOMAIN, - GATEWAY, - NETWORK_COORDINATOR, - NETWORK_SENSORS, - SIGNAL_COORDINATOR, - SIGNAL_SENSORS, - SMS_GATEWAY, +from .const import DOMAIN, GATEWAY, NETWORK_COORDINATOR, SIGNAL_COORDINATOR, SMS_GATEWAY + +SIGNAL_SENSORS = ( + SensorEntityDescription( + key="SignalStrength", + name="Signal Strength", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="SignalPercent", + icon="mdi:signal-cellular-3", + name="Signal Percent", + native_unit_of_measurement=PERCENTAGE, + entity_registry_enabled_default=True, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="BitErrorRate", + name="Bit Error Rate", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=PERCENTAGE, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + ), +) + +NETWORK_SENSORS = ( + SensorEntityDescription( + key="NetworkName", + name="Network Name", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="State", + name="Network Status", + entity_registry_enabled_default=True, + ), + SensorEntityDescription( + key="NetworkCode", + name="GSM network code", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="CID", + name="Cell ID", + icon="mdi:radio-tower", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="LAC", + name="Local Area Code", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), ) @@ -29,21 +88,13 @@ async def async_setup_entry( gateway = sms_data[GATEWAY] unique_id = str(await gateway.get_imei_async()) entities = [] - for description in SIGNAL_SENSORS.values(): + for description in SIGNAL_SENSORS: entities.append( - DeviceSensor( - signal_coordinator, - description, - unique_id, - ) + DeviceSensor(signal_coordinator, description, unique_id, gateway) ) - for description in NETWORK_SENSORS.values(): + for description in NETWORK_SENSORS: entities.append( - DeviceSensor( - network_coordinator, - description, - unique_id, - ) + DeviceSensor(network_coordinator, description, unique_id, gateway) ) async_add_entities(entities, True) @@ -51,12 +102,15 @@ async def async_setup_entry( class DeviceSensor(CoordinatorEntity, SensorEntity): """Implementation of a device sensor.""" - def __init__(self, coordinator, description, unique_id): + def __init__(self, coordinator, description, unique_id, gateway): """Initialize the device sensor.""" super().__init__(coordinator) self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, unique_id)}, name="SMS Gateway", + manufacturer=gateway.manufacturer, + model=gateway.model, + sw_version=gateway.firmware, ) self._attr_unique_id = f"{unique_id}_{description.key}" self.entity_description = description From 81abeac83ed85c5753cb8f2ac317caf079cf1868 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Mon, 26 Sep 2022 03:55:58 +0200 Subject: [PATCH 717/955] Netatmo refactor to use pyatmo 7.0.1 (#73482) (#78523) Co-authored-by: Robert Svensson --- homeassistant/components/netatmo/__init__.py | 14 +- homeassistant/components/netatmo/api.py | 2 +- homeassistant/components/netatmo/camera.py | 254 +++--- homeassistant/components/netatmo/climate.py | 195 ++-- .../components/netatmo/config_flow.py | 4 +- homeassistant/components/netatmo/const.py | 66 +- homeassistant/components/netatmo/cover.py | 110 +++ .../components/netatmo/data_handler.py | 272 +++++- .../components/netatmo/device_trigger.py | 14 +- .../components/netatmo/diagnostics.py | 6 +- homeassistant/components/netatmo/light.py | 180 ++-- .../components/netatmo/manifest.json | 2 +- .../components/netatmo/media_source.py | 22 +- .../components/netatmo/netatmo_entity_base.py | 55 +- homeassistant/components/netatmo/select.py | 110 +-- homeassistant/components/netatmo/sensor.py | 631 ++++++------- homeassistant/components/netatmo/switch.py | 83 ++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/netatmo/common.py | 25 +- tests/components/netatmo/conftest.py | 4 +- tests/components/netatmo/fixtures/events.txt | 107 +-- .../netatmo/fixtures/getevents.json | 151 ++++ .../netatmo/fixtures/homesdata.json | 375 +++++++- .../homestatus_91763b24c43d3e344f424e8b.json | 831 ++++++++++++++++++ tests/components/netatmo/test_camera.py | 181 ++-- tests/components/netatmo/test_climate.py | 10 +- tests/components/netatmo/test_config_flow.py | 4 +- tests/components/netatmo/test_cover.py | 110 +++ .../components/netatmo/test_device_trigger.py | 27 +- tests/components/netatmo/test_diagnostics.py | 25 +- tests/components/netatmo/test_init.py | 76 +- tests/components/netatmo/test_light.py | 74 +- tests/components/netatmo/test_media_source.py | 4 +- tests/components/netatmo/test_select.py | 6 +- tests/components/netatmo/test_sensor.py | 95 +- tests/components/netatmo/test_switch.py | 65 ++ 37 files changed, 2915 insertions(+), 1279 deletions(-) create mode 100644 homeassistant/components/netatmo/cover.py create mode 100644 homeassistant/components/netatmo/switch.py create mode 100644 tests/components/netatmo/fixtures/getevents.json create mode 100644 tests/components/netatmo/test_cover.py create mode 100644 tests/components/netatmo/test_switch.py diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index 36847c85515..65b321d25aa 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -8,6 +8,7 @@ import secrets import aiohttp import pyatmo +from pyatmo.const import ALL_SCOPES as NETATMO_SCOPES import voluptuous as vol from homeassistant.components import cloud @@ -51,7 +52,6 @@ from .const import ( DATA_PERSONS, DATA_SCHEDULES, DOMAIN, - NETATMO_SCOPES, PLATFORMS, WEBHOOK_DEACTIVATION, WEBHOOK_PUSH_TYPE, @@ -150,10 +150,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: } data_handler = NetatmoDataHandler(hass, entry) - await data_handler.async_setup() hass.data[DOMAIN][entry.entry_id][DATA_HANDLER] = data_handler - - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + await data_handler.async_setup() async def unregister_webhook( call_or_event_or_dt: ServiceCall | Event | datetime | None, @@ -208,10 +206,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.info("Register Netatmo webhook: %s", webhook_url) except pyatmo.ApiError as err: _LOGGER.error("Error during webhook registration - %s", err) - - entry.async_on_unload( - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, unregister_webhook) - ) + else: + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, unregister_webhook) + ) async def manage_cloudhook(state: cloud.CloudConnectionState) -> None: if state is cloud.CloudConnectionState.CLOUD_CONNECTED: diff --git a/homeassistant/components/netatmo/api.py b/homeassistant/components/netatmo/api.py index e13032dc399..0b36745338e 100644 --- a/homeassistant/components/netatmo/api.py +++ b/homeassistant/components/netatmo/api.py @@ -7,7 +7,7 @@ import pyatmo from homeassistant.helpers import config_entry_oauth2_flow -class AsyncConfigEntryNetatmoAuth(pyatmo.auth.AbstractAsyncAuth): +class AsyncConfigEntryNetatmoAuth(pyatmo.AbstractAsyncAuth): """Provide Netatmo authentication tied to an OAuth2 based config entry.""" def __init__( diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index 3235d16479c..9254ff6e284 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -5,13 +5,14 @@ import logging from typing import Any, cast import aiohttp -import pyatmo +from pyatmo import ApiError as NetatmoApiError, modules as NaModules +from pyatmo.event import Event as NaEvent import voluptuous as vol from homeassistant.components.camera import Camera, CameraEntityFeature from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import HomeAssistantError, PlatformNotReady +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -20,28 +21,24 @@ from .const import ( ATTR_CAMERA_LIGHT_MODE, ATTR_PERSON, ATTR_PERSONS, - ATTR_PSEUDO, CAMERA_LIGHT_MODES, + CONF_URL_SECURITY, DATA_CAMERAS, DATA_EVENTS, - DATA_HANDLER, - DATA_PERSONS, DOMAIN, EVENT_TYPE_LIGHT_MODE, EVENT_TYPE_OFF, EVENT_TYPE_ON, MANUFACTURER, - MODELS, + NETATMO_CREATE_CAMERA, SERVICE_SET_CAMERA_LIGHT, SERVICE_SET_PERSON_AWAY, SERVICE_SET_PERSONS_HOME, - SIGNAL_NAME, - TYPE_SECURITY, WEBHOOK_LIGHT_MODE, WEBHOOK_NACAMERA_CONNECTION, WEBHOOK_PUSH_TYPE, ) -from .data_handler import CAMERA_DATA_CLASS_NAME, NetatmoDataHandler +from .data_handler import EVENT, HOME, SIGNAL_NAME, NetatmoDevice from .netatmo_entity_base import NetatmoBase _LOGGER = logging.getLogger(__name__) @@ -53,42 +50,15 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the Netatmo camera platform.""" - data_handler = hass.data[DOMAIN][entry.entry_id][DATA_HANDLER] - data_class = data_handler.data.get(CAMERA_DATA_CLASS_NAME) + @callback + def _create_entity(netatmo_device: NetatmoDevice) -> None: + entity = NetatmoCamera(netatmo_device) + async_add_entities([entity]) - if not data_class or not data_class.raw_data: - raise PlatformNotReady - - all_cameras = [] - for home in data_class.cameras.values(): - for camera in home.values(): - all_cameras.append(camera) - - entities = [ - NetatmoCamera( - data_handler, - camera["id"], - camera["type"], - camera["home_id"], - DEFAULT_QUALITY, - ) - for camera in all_cameras - ] - - for home in data_class.homes.values(): - if home.get("id") is None: - continue - - hass.data[DOMAIN][DATA_PERSONS][home["id"]] = { - person_id: person_data.get(ATTR_PSEUDO) - for person_id, person_data in data_handler.data[CAMERA_DATA_CLASS_NAME] - .persons[home["id"]] - .items() - } - - _LOGGER.debug("Adding cameras %s", entities) - async_add_entities(entities, True) + entry.async_on_unload( + async_dispatcher_connect(hass, NETATMO_CREATE_CAMERA, _create_entity) + ) platform = entity_platform.async_get_current_platform() @@ -118,41 +88,44 @@ class NetatmoCamera(NetatmoBase, Camera): def __init__( self, - data_handler: NetatmoDataHandler, - camera_id: str, - camera_type: str, - home_id: str, - quality: str, + netatmo_device: NetatmoDevice, ) -> None: """Set up for access to the Netatmo camera images.""" Camera.__init__(self) - super().__init__(data_handler) + super().__init__(netatmo_device.data_handler) - self._publishers.append( - {"name": CAMERA_DATA_CLASS_NAME, SIGNAL_NAME: CAMERA_DATA_CLASS_NAME} - ) - - self._id = camera_id - self._home_id = home_id - self._device_name = self._data.get_camera(camera_id=camera_id)["name"] - self._model = camera_type - self._netatmo_type = TYPE_SECURITY + self._camera = cast(NaModules.Camera, netatmo_device.device) + self._id = self._camera.entity_id + self._home_id = self._camera.home.entity_id + self._device_name = self._camera.name + self._model = self._camera.device_type + self._config_url = CONF_URL_SECURITY self._attr_unique_id = f"{self._id}-{self._model}" - self._quality = quality - self._vpnurl: str | None = None - self._localurl: str | None = None - self._status: str | None = None - self._sd_status: str | None = None - self._alim_status: str | None = None - self._is_local: str | None = None + self._quality = DEFAULT_QUALITY + self._monitoring: bool | None = None self._light_state = None + self._publishers.extend( + [ + { + "name": HOME, + "home_id": self._home_id, + SIGNAL_NAME: f"{HOME}-{self._home_id}", + }, + { + "name": EVENT, + "home_id": self._home_id, + SIGNAL_NAME: f"{EVENT}-{self._home_id}", + }, + ] + ) + async def async_added_to_hass(self) -> None: """Entity created.""" await super().async_added_to_hass() for event_type in (EVENT_TYPE_LIGHT_MODE, EVENT_TYPE_OFF, EVENT_TYPE_ON): - self.data_handler.config_entry.async_on_unload( + self.async_on_remove( async_dispatcher_connect( self.hass, f"signal-{DOMAIN}-webhook-{event_type}", @@ -173,13 +146,13 @@ class NetatmoCamera(NetatmoBase, Camera): if data["home_id"] == self._home_id and data["camera_id"] == self._id: if data[WEBHOOK_PUSH_TYPE] in ("NACamera-off", "NACamera-disconnection"): self._attr_is_streaming = False - self._status = "off" + self._monitoring = False elif data[WEBHOOK_PUSH_TYPE] in ( "NACamera-on", WEBHOOK_NACAMERA_CONNECTION, ): self._attr_is_streaming = True - self._status = "on" + self._monitoring = True elif data[WEBHOOK_PUSH_TYPE] == WEBHOOK_LIGHT_MODE: self._light_state = data["sub_type"] self._attr_extra_state_attributes.update( @@ -189,128 +162,107 @@ class NetatmoCamera(NetatmoBase, Camera): self.async_write_ha_state() return - @property - def _data(self) -> pyatmo.AsyncCameraData: - """Return data for this entity.""" - return cast( - pyatmo.AsyncCameraData, - self.data_handler.data[self._publishers[0]["name"]], - ) - async def async_camera_image( self, width: int | None = None, height: int | None = None ) -> bytes | None: """Return a still image response from the camera.""" try: - return cast( - bytes, await self._data.async_get_live_snapshot(camera_id=self._id) - ) + return cast(bytes, await self._camera.async_get_live_snapshot()) except ( aiohttp.ClientPayloadError, aiohttp.ContentTypeError, aiohttp.ServerDisconnectedError, aiohttp.ClientConnectorError, - pyatmo.exceptions.ApiError, + NetatmoApiError, ) as err: _LOGGER.debug("Could not fetch live camera image (%s)", err) return None @property - def available(self) -> bool: - """Return True if entity is available.""" - return bool(self._alim_status == "on" or self._status == "disconnected") - - @property - def motion_detection_enabled(self) -> bool: - """Return the camera motion detection status.""" - return bool(self._status == "on") - - @property - def is_on(self) -> bool: - """Return true if on.""" - return self.is_streaming + def supported_features(self) -> int: + """Return supported features.""" + supported_features: int = CameraEntityFeature.ON_OFF + if self._model != "NDB": + supported_features |= CameraEntityFeature.STREAM + return supported_features async def async_turn_off(self) -> None: """Turn off camera.""" - await self._data.async_set_state( - home_id=self._home_id, camera_id=self._id, monitoring="off" - ) + await self._camera.async_monitoring_off() async def async_turn_on(self) -> None: """Turn on camera.""" - await self._data.async_set_state( - home_id=self._home_id, camera_id=self._id, monitoring="on" - ) + await self._camera.async_monitoring_on() async def stream_source(self) -> str: """Return the stream source.""" - url = "{0}/live/files/{1}/index.m3u8" - if self._localurl: - return url.format(self._localurl, self._quality) - return url.format(self._vpnurl, self._quality) + if self._camera.is_local: + await self._camera.async_update_camera_urls() - @property - def model(self) -> str: - """Return the camera model.""" - return MODELS[self._model] + if self._camera.local_url: + return "{}/live/files/{}/index.m3u8".format( + self._camera.local_url, self._quality + ) + return f"{self._camera.vpn_url}/live/files/{self._quality}/index.m3u8" @callback def async_update_callback(self) -> None: """Update the entity's state.""" - camera = self._data.get_camera(self._id) - self._vpnurl, self._localurl = self._data.camera_urls(self._id) - self._status = camera.get("status") - self._sd_status = camera.get("sd_status") - self._alim_status = camera.get("alim_status") - self._is_local = camera.get("is_local") - self._attr_is_streaming = bool(self._status == "on") + self._attr_is_on = self._camera.alim_status is not None + self._attr_available = self._camera.alim_status is not None - if self._model == "NACamera": # Smart Indoor Camera - self.hass.data[DOMAIN][DATA_EVENTS][self._id] = self.process_events( - self._data.events.get(self._id, {}) - ) - elif self._model == "NOC": # Smart Outdoor Camera - self.hass.data[DOMAIN][DATA_EVENTS][self._id] = self.process_events( - self._data.outdoor_events.get(self._id, {}) - ) + if self._camera.monitoring is not None: + self._attr_is_streaming = self._camera.monitoring + self._attr_motion_detection_enabled = self._camera.monitoring + + self.hass.data[DOMAIN][DATA_EVENTS][self._id] = self.process_events( + self._camera.events + ) self._attr_extra_state_attributes.update( { "id": self._id, - "status": self._status, - "sd_status": self._sd_status, - "alim_status": self._alim_status, - "is_local": self._is_local, - "vpn_url": self._vpnurl, - "local_url": self._localurl, + "monitoring": self._monitoring, + "sd_status": self._camera.sd_status, + "alim_status": self._camera.alim_status, + "is_local": self._camera.is_local, + "vpn_url": self._camera.vpn_url, + "local_url": self._camera.local_url, "light_state": self._light_state, } ) - def process_events(self, events: dict) -> dict: + def process_events(self, event_list: list[NaEvent]) -> dict: """Add meta data to events.""" - for event in events.values(): - if "video_id" not in event: + events = {} + for event in event_list: + if not (video_id := event.video_id): continue - if self._is_local: - event[ - "media_url" - ] = f"{self._localurl}/vod/{event['video_id']}/files/{self._quality}/index.m3u8" - else: - event[ - "media_url" - ] = f"{self._vpnurl}/vod/{event['video_id']}/files/{self._quality}/index.m3u8" + event_data = event.__dict__ + event_data["subevents"] = [ + event.__dict__ + for event in event_data.get("subevents", []) + if not isinstance(event, dict) + ] + event_data["media_url"] = self.get_video_url(video_id) + events[event.event_time] = event_data return events + def get_video_url(self, video_id: str) -> str: + """Get video url.""" + if self._camera.is_local: + return f"{self._camera.local_url}/vod/{video_id}/files/{self._quality}/index.m3u8" + return f"{self._camera.vpn_url}/vod/{video_id}/files/{self._quality}/index.m3u8" + def fetch_person_ids(self, persons: list[str | None]) -> list[str]: - """Fetch matching person ids for give list of persons.""" + """Fetch matching person ids for given list of persons.""" person_ids = [] person_id_errors = [] for person in persons: person_id = None - for pid, data in self._data.persons[self._home_id].items(): - if data.get("pseudo") == person: + for pid, data in self._camera.home.persons.items(): + if data.pseudo == person: person_ids.append(pid) person_id = pid break @@ -328,9 +280,7 @@ class NetatmoCamera(NetatmoBase, Camera): persons = kwargs.get(ATTR_PERSONS, []) person_ids = self.fetch_person_ids(persons) - await self._data.async_set_persons_home( - person_ids=person_ids, home_id=self._home_id - ) + await self._camera.home.async_set_persons_home(person_ids=person_ids) _LOGGER.debug("Set %s as at home", persons) async def _service_set_person_away(self, **kwargs: Any) -> None: @@ -339,9 +289,8 @@ class NetatmoCamera(NetatmoBase, Camera): person_ids = self.fetch_person_ids([person] if person else []) person_id = next(iter(person_ids), None) - await self._data.async_set_persons_away( + await self._camera.home.async_set_persons_away( person_id=person_id, - home_id=self._home_id, ) if person_id: @@ -351,10 +300,11 @@ class NetatmoCamera(NetatmoBase, Camera): async def _service_set_camera_light(self, **kwargs: Any) -> None: """Service to set light mode.""" + if not isinstance(self._camera, NaModules.netatmo.NOC): + raise HomeAssistantError( + f"{self._model} <{self._device_name}> does not have a floodlight" + ) + mode = str(kwargs.get(ATTR_CAMERA_LIGHT_MODE)) _LOGGER.debug("Turn %s camera light for '%s'", mode, self._attr_name) - await self._data.async_set_state( - home_id=self._home_id, - camera_id=self._id, - floodlight=mode, - ) + await self._camera.async_set_floodlight_state(mode) diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index 6b30989dd8f..400004ee4d1 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -2,15 +2,16 @@ from __future__ import annotations import logging -from typing import Any +from typing import Any, cast -import pyatmo +from pyatmo.modules import NATherm1 import voluptuous as vol from homeassistant.components.climate import ( DEFAULT_MIN_TEMP, PRESET_AWAY, PRESET_BOOST, + PRESET_HOME, ClimateEntity, ClimateEntityFeature, HVACAction, @@ -25,12 +26,8 @@ from homeassistant.const import ( TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers import config_validation as cv, entity_platform -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -38,25 +35,17 @@ from .const import ( ATTR_HEATING_POWER_REQUEST, ATTR_SCHEDULE_NAME, ATTR_SELECTED_SCHEDULE, - DATA_HANDLER, - DATA_HOMES, + CONF_URL_ENERGY, DATA_SCHEDULES, DOMAIN, EVENT_TYPE_CANCEL_SET_POINT, EVENT_TYPE_SCHEDULE, EVENT_TYPE_SET_POINT, EVENT_TYPE_THERM_MODE, - NETATMO_CREATE_BATTERY, + NETATMO_CREATE_CLIMATE, SERVICE_SET_SCHEDULE, - SIGNAL_NAME, - TYPE_ENERGY, -) -from .data_handler import ( - CLIMATE_STATE_CLASS_NAME, - CLIMATE_TOPOLOGY_CLASS_NAME, - NetatmoDataHandler, - NetatmoDevice, ) +from .data_handler import HOME, SIGNAL_NAME, NetatmoRoom from .netatmo_entity_base import NetatmoBase _LOGGER = logging.getLogger(__name__) @@ -65,6 +54,9 @@ PRESET_FROST_GUARD = "Frost Guard" PRESET_SCHEDULE = "Schedule" PRESET_MANUAL = "Manual" +SUPPORT_FLAGS = ( + ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE +) SUPPORT_PRESET = [PRESET_AWAY, PRESET_BOOST, PRESET_FROST_GUARD, PRESET_SCHEDULE] STATE_NETATMO_SCHEDULE = "schedule" @@ -116,51 +108,22 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the Netatmo energy platform.""" - data_handler = hass.data[DOMAIN][entry.entry_id][DATA_HANDLER] - climate_topology = data_handler.data.get(CLIMATE_TOPOLOGY_CLASS_NAME) + @callback + def _create_entity(netatmo_device: NetatmoRoom) -> None: + entity = NetatmoThermostat(netatmo_device) + async_add_entities([entity]) - if not climate_topology or climate_topology.raw_data == {}: - raise PlatformNotReady - - entities = [] - for home_id in climate_topology.home_ids: - signal_name = f"{CLIMATE_STATE_CLASS_NAME}-{home_id}" - - await data_handler.subscribe( - CLIMATE_STATE_CLASS_NAME, signal_name, None, home_id=home_id - ) - - if (climate_state := data_handler.data[signal_name]) is None: - continue - - climate_topology.register_handler(home_id, climate_state.process_topology) - - for room in climate_state.homes[home_id].rooms.values(): - if room.device_type is None or room.device_type.value not in [ - NA_THERM, - NA_VALVE, - ]: - continue - entities.append(NetatmoThermostat(data_handler, room)) - - hass.data[DOMAIN][DATA_SCHEDULES][home_id] = climate_state.homes[ - home_id - ].schedules - - hass.data[DOMAIN][DATA_HOMES][home_id] = climate_state.homes[home_id].name - - _LOGGER.debug("Adding climate devices %s", entities) - async_add_entities(entities, True) + entry.async_on_unload( + async_dispatcher_connect(hass, NETATMO_CREATE_CLIMATE, _create_entity) + ) platform = entity_platform.async_get_current_platform() - - if climate_topology is not None: - platform.async_register_entity_service( - SERVICE_SET_SCHEDULE, - {vol.Required(ATTR_SCHEDULE_NAME): cv.string}, - "_async_service_set_schedule", - ) + platform.async_register_entity_service( + SERVICE_SET_SCHEDULE, + {vol.Required(ATTR_SCHEDULE_NAME): cv.string}, + "_async_service_set_schedule", + ) class NetatmoThermostat(NetatmoBase, ClimateEntity): @@ -169,42 +132,33 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): _attr_hvac_mode = HVACMode.AUTO _attr_max_temp = DEFAULT_MAX_TEMP _attr_preset_modes = SUPPORT_PRESET - _attr_supported_features = ( - ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE - ) + _attr_supported_features = SUPPORT_FLAGS _attr_target_temperature_step = PRECISION_HALVES _attr_temperature_unit = TEMP_CELSIUS - def __init__( - self, data_handler: NetatmoDataHandler, room: pyatmo.climate.NetatmoRoom - ) -> None: + def __init__(self, netatmo_device: NetatmoRoom) -> None: """Initialize the sensor.""" ClimateEntity.__init__(self) - super().__init__(data_handler) + super().__init__(netatmo_device.data_handler) - self._room = room + self._room = netatmo_device.room self._id = self._room.entity_id + self._home_id = self._room.home.entity_id - self._signal_name = f"{CLIMATE_STATE_CLASS_NAME}-{self._room.home.entity_id}" - self._climate_state: pyatmo.AsyncClimate = data_handler.data[self._signal_name] - + self._signal_name = f"{HOME}-{self._home_id}" self._publishers.extend( [ { - "name": CLIMATE_TOPOLOGY_CLASS_NAME, - SIGNAL_NAME: CLIMATE_TOPOLOGY_CLASS_NAME, - }, - { - "name": CLIMATE_STATE_CLASS_NAME, + "name": HOME, "home_id": self._room.home.entity_id, SIGNAL_NAME: self._signal_name, }, ] ) - self._model: str = getattr(room.device_type, "value") + self._model: str = f"{self._room.climate_type}" - self._netatmo_type = TYPE_ENERGY + self._config_url = CONF_URL_ENERGY self._attr_name = self._room.name self._away: bool | None = None @@ -231,7 +185,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): EVENT_TYPE_CANCEL_SET_POINT, EVENT_TYPE_SCHEDULE, ): - self.data_handler.config_entry.async_on_unload( + self.async_on_remove( async_dispatcher_connect( self.hass, f"signal-{DOMAIN}-webhook-{event_type}", @@ -239,21 +193,6 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): ) ) - for module in self._room.modules.values(): - if getattr(module.device_type, "value") not in [NA_THERM, NA_VALVE]: - continue - - async_dispatcher_send( - self.hass, - NETATMO_CREATE_BATTERY, - NetatmoDevice( - self.data_handler, - module, - self._id, - self._signal_name, - ), - ) - @callback def handle_event(self, event: dict) -> None: """Handle webhook events.""" @@ -289,7 +228,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): self._attr_target_temperature = self._hg_temperature elif self._attr_preset_mode == PRESET_AWAY: self._attr_target_temperature = self._away_temperature - elif self._attr_preset_mode == PRESET_SCHEDULE: + elif self._attr_preset_mode in [PRESET_SCHEDULE, PRESET_HOME]: self.async_update_callback() self.data_handler.async_force_update(self._signal_name) self.async_write_ha_state() @@ -322,6 +261,10 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): data["event_type"] == EVENT_TYPE_CANCEL_SET_POINT and self._room.entity_id == room["id"] ): + if self._attr_hvac_mode == HVACMode.OFF: + self._attr_hvac_mode = HVACMode.AUTO + self._attr_preset_mode = PRESET_MAP_NETATMO[PRESET_SCHEDULE] + self.async_update_callback() self.async_write_ha_state() return @@ -329,7 +272,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): @property def hvac_action(self) -> HVACAction: """Return the current running hvac operation if supported.""" - if self._model == NA_THERM and self._boilerstatus is not None: + if self._model != NA_VALVE and self._boilerstatus is not None: return CURRENT_HVAC_MAP_NETATMO[self._boilerstatus] # Maybe it is a valve if ( @@ -343,55 +286,36 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): if hvac_mode == HVACMode.OFF: await self.async_turn_off() elif hvac_mode == HVACMode.AUTO: - if self.hvac_mode == HVACMode.OFF: - await self.async_turn_on() await self.async_set_preset_mode(PRESET_SCHEDULE) elif hvac_mode == HVACMode.HEAT: await self.async_set_preset_mode(PRESET_BOOST) async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" - if self.hvac_mode == HVACMode.OFF: - await self.async_turn_on() - - if self.target_temperature == 0: - await self._climate_state.async_set_room_thermpoint( - self._room.entity_id, - STATE_NETATMO_HOME, - ) - if ( preset_mode in (PRESET_BOOST, STATE_NETATMO_MAX) and self._model == NA_VALVE - and self.hvac_mode == HVACMode.HEAT + and self._attr_hvac_mode == HVACMode.HEAT ): - await self._climate_state.async_set_room_thermpoint( - self._room.entity_id, + await self._room.async_therm_set( STATE_NETATMO_HOME, ) elif ( preset_mode in (PRESET_BOOST, STATE_NETATMO_MAX) and self._model == NA_VALVE ): - await self._climate_state.async_set_room_thermpoint( - self._room.entity_id, + await self._room.async_therm_set( STATE_NETATMO_MANUAL, DEFAULT_MAX_TEMP, ) elif ( preset_mode in (PRESET_BOOST, STATE_NETATMO_MAX) - and self.hvac_mode == HVACMode.HEAT + and self._attr_hvac_mode == HVACMode.HEAT ): - await self._climate_state.async_set_room_thermpoint( - self._room.entity_id, STATE_NETATMO_HOME - ) + await self._room.async_therm_set(STATE_NETATMO_HOME) elif preset_mode in (PRESET_BOOST, STATE_NETATMO_MAX): - await self._climate_state.async_set_room_thermpoint( - self._room.entity_id, PRESET_MAP_NETATMO[preset_mode] - ) + await self._room.async_therm_set(PRESET_MAP_NETATMO[preset_mode]) elif preset_mode in (PRESET_SCHEDULE, PRESET_FROST_GUARD, PRESET_AWAY): - await self._climate_state.async_set_thermmode( - PRESET_MAP_NETATMO[preset_mode] - ) + await self._room.async_therm_set(PRESET_MAP_NETATMO[preset_mode]) else: _LOGGER.error("Preset mode '%s' not available", preset_mode) @@ -399,33 +323,25 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature for 2 hours.""" - if (temp := kwargs.get(ATTR_TEMPERATURE)) is None: - return - await self._climate_state.async_set_room_thermpoint( - self._room.entity_id, STATE_NETATMO_MANUAL, min(temp, DEFAULT_MAX_TEMP) + await self._room.async_therm_set( + STATE_NETATMO_MANUAL, min(kwargs[ATTR_TEMPERATURE], DEFAULT_MAX_TEMP) ) - self.async_write_ha_state() async def async_turn_off(self) -> None: """Turn the entity off.""" if self._model == NA_VALVE: - await self._climate_state.async_set_room_thermpoint( - self._room.entity_id, + await self._room.async_therm_set( STATE_NETATMO_MANUAL, DEFAULT_MIN_TEMP, ) - elif self.hvac_mode != HVACMode.OFF: - await self._climate_state.async_set_room_thermpoint( - self._room.entity_id, STATE_NETATMO_OFF - ) + elif self._attr_hvac_mode != HVACMode.OFF: + await self._room.async_therm_set(STATE_NETATMO_OFF) self.async_write_ha_state() async def async_turn_on(self) -> None: """Turn the entity on.""" - await self._climate_state.async_set_room_thermpoint( - self._room.entity_id, STATE_NETATMO_HOME - ) + await self._room.async_therm_set(STATE_NETATMO_HOME) self.async_write_ha_state() @property @@ -466,8 +382,11 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): ] = self._room.heating_power_request else: for module in self._room.modules.values(): - self._boilerstatus = module.boiler_status - break + if hasattr(module, "boiler_status"): + module = cast(NATherm1, module) + if module.boiler_status is not None: + self._boilerstatus = module.boiler_status + break async def _async_service_set_schedule(self, **kwargs: Any) -> None: schedule_name = kwargs.get(ATTR_SCHEDULE_NAME) @@ -483,7 +402,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): _LOGGER.error("%s is not a valid schedule", kwargs.get(ATTR_SCHEDULE_NAME)) return - await self._climate_state.async_switch_home_schedule(schedule_id=schedule_id) + await self._room.home.async_switch_schedule(schedule_id=schedule_id) _LOGGER.debug( "Setting %s schedule to %s (%s)", self._room.home.entity_id, diff --git a/homeassistant/components/netatmo/config_flow.py b/homeassistant/components/netatmo/config_flow.py index ba63c76ad66..99fa195b118 100644 --- a/homeassistant/components/netatmo/config_flow.py +++ b/homeassistant/components/netatmo/config_flow.py @@ -6,6 +6,7 @@ import logging from typing import Any import uuid +from pyatmo.const import ALL_SCOPES import voluptuous as vol from homeassistant import config_entries @@ -25,7 +26,6 @@ from .const import ( CONF_UUID, CONF_WEATHER_AREAS, DOMAIN, - NETATMO_SCOPES, ) _LOGGER = logging.getLogger(__name__) @@ -54,7 +54,7 @@ class NetatmoFlowHandler( @property def extra_authorize_data(self) -> dict: """Extra data that needs to be appended to the authorize url.""" - return {"scope": " ".join(NETATMO_SCOPES)} + return {"scope": " ".join(ALL_SCOPES)} async def async_step_user(self, user_input: dict | None = None) -> FlowResult: """Handle a flow start.""" diff --git a/homeassistant/components/netatmo/const.py b/homeassistant/components/netatmo/const.py index 6bd66fa9644..e93d0c91a07 100644 --- a/homeassistant/components/netatmo/const.py +++ b/homeassistant/components/netatmo/const.py @@ -10,60 +10,17 @@ DEFAULT_ATTRIBUTION = f"Data provided by {MANUFACTURER}" PLATFORMS = [ Platform.CAMERA, Platform.CLIMATE, + Platform.COVER, Platform.LIGHT, Platform.SELECT, Platform.SENSOR, + Platform.SWITCH, ] -NETATMO_SCOPES = [ - "access_camera", - "access_presence", - "read_camera", - "read_homecoach", - "read_presence", - "read_smokedetector", - "read_station", - "read_thermostat", - "write_camera", - "write_presence", - "write_thermostat", -] - -MODEL_NAPLUG = "Relay" -MODEL_NATHERM1 = "Smart Thermostat" -MODEL_NRV = "Smart Radiator Valves" -MODEL_NOC = "Smart Outdoor Camera" -MODEL_NACAMERA = "Smart Indoor Camera" -MODEL_NSD = "Smart Smoke Alarm" -MODEL_NACAMDOORTAG = "Smart Door and Window Sensors" -MODEL_NHC = "Smart Indoor Air Quality Monitor" -MODEL_NAMAIN = "Smart Home Weather station – indoor module" -MODEL_NAMODULE1 = "Smart Home Weather station – outdoor module" -MODEL_NAMODULE4 = "Smart Additional Indoor module" -MODEL_NAMODULE3 = "Smart Rain Gauge" -MODEL_NAMODULE2 = "Smart Anemometer" -MODEL_PUBLIC = "Public Weather stations" - -MODELS = { - "NAPlug": MODEL_NAPLUG, - "NATherm1": MODEL_NATHERM1, - "NRV": MODEL_NRV, - "NACamera": MODEL_NACAMERA, - "NOC": MODEL_NOC, - "NSD": MODEL_NSD, - "NACamDoorTag": MODEL_NACAMDOORTAG, - "NHC": MODEL_NHC, - "NAMain": MODEL_NAMAIN, - "NAModule1": MODEL_NAMODULE1, - "NAModule4": MODEL_NAMODULE4, - "NAModule3": MODEL_NAMODULE3, - "NAModule2": MODEL_NAMODULE2, - "public": MODEL_PUBLIC, -} - -TYPE_SECURITY = "security" -TYPE_ENERGY = "energy" -TYPE_WEATHER = "weather" +CONF_URL_SECURITY = "https://home.netatmo.com/security" +CONF_URL_ENERGY = "https://my.netatmo.com/app/energy" +CONF_URL_WEATHER = "https://my.netatmo.com/app/weather" +CONF_URL_CONTROL = "https://home.netatmo.com/control" AUTH = "netatmo_auth" CONF_PUBLIC = "public_sensor_config" @@ -71,7 +28,18 @@ CAMERA_DATA = "netatmo_camera" HOME_DATA = "netatmo_home_data" DATA_HANDLER = "netatmo_data_handler" SIGNAL_NAME = "signal_name" + NETATMO_CREATE_BATTERY = "netatmo_create_battery" +NETATMO_CREATE_CAMERA = "netatmo_create_camera" +NETATMO_CREATE_CAMERA_LIGHT = "netatmo_create_camera_light" +NETATMO_CREATE_CLIMATE = "netatmo_create_climate" +NETATMO_CREATE_COVER = "netatmo_create_cover" +NETATMO_CREATE_LIGHT = "netatmo_create_light" +NETATMO_CREATE_ROOM_SENSOR = "netatmo_create_room_sensor" +NETATMO_CREATE_SELECT = "netatmo_create_select" +NETATMO_CREATE_SENSOR = "netatmo_create_sensor" +NETATMO_CREATE_SWITCH = "netatmo_create_switch" +NETATMO_CREATE_WEATHER_SENSOR = "netatmo_create_weather_sensor" CONF_AREA_NAME = "area_name" CONF_CLOUDHOOK_URL = "cloudhook_url" diff --git a/homeassistant/components/netatmo/cover.py b/homeassistant/components/netatmo/cover.py new file mode 100644 index 00000000000..6d755d828d3 --- /dev/null +++ b/homeassistant/components/netatmo/cover.py @@ -0,0 +1,110 @@ +"""Support for Netatmo/Bubendorff covers.""" +from __future__ import annotations + +import logging +from typing import Any, cast + +from pyatmo import modules as NaModules + +from homeassistant.components.cover import ( + ATTR_POSITION, + CoverDeviceClass, + CoverEntity, + CoverEntityFeature, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import CONF_URL_CONTROL, NETATMO_CREATE_COVER +from .data_handler import HOME, SIGNAL_NAME, NetatmoDevice +from .netatmo_entity_base import NetatmoBase + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Netatmo cover platform.""" + + @callback + def _create_entity(netatmo_device: NetatmoDevice) -> None: + entity = NetatmoCover(netatmo_device) + _LOGGER.debug("Adding cover %s", entity) + async_add_entities([entity]) + + entry.async_on_unload( + async_dispatcher_connect(hass, NETATMO_CREATE_COVER, _create_entity) + ) + + +class NetatmoCover(NetatmoBase, CoverEntity): + """Representation of a Netatmo cover device.""" + + _attr_supported_features = ( + CoverEntityFeature.OPEN + | CoverEntityFeature.CLOSE + | CoverEntityFeature.STOP + | CoverEntityFeature.SET_POSITION + ) + + def __init__(self, netatmo_device: NetatmoDevice) -> None: + """Initialize the Netatmo device.""" + super().__init__(netatmo_device.data_handler) + + self._cover = cast(NaModules.Shutter, netatmo_device.device) + + self._id = self._cover.entity_id + self._attr_name = self._device_name = self._cover.name + self._model = self._cover.device_type + self._config_url = CONF_URL_CONTROL + + self._home_id = self._cover.home.entity_id + self._attr_is_closed = self._cover.current_position == 0 + + self._signal_name = f"{HOME}-{self._home_id}" + self._publishers.extend( + [ + { + "name": HOME, + "home_id": self._home_id, + SIGNAL_NAME: self._signal_name, + }, + ] + ) + self._attr_unique_id = f"{self._id}-{self._model}" + + async def async_close_cover(self, **kwargs: Any) -> None: + """Close the cover.""" + await self._cover.async_close() + self._attr_is_closed = True + self.async_write_ha_state() + + async def async_open_cover(self, **kwargs: Any) -> None: + """Open the cover.""" + await self._cover.async_open() + self._attr_is_closed = False + self.async_write_ha_state() + + async def async_stop_cover(self, **kwargs: Any) -> None: + """Stop the cover.""" + await self._cover.async_stop() + + async def async_set_cover_position(self, **kwargs: Any) -> None: + """Move the cover shutter to a specific position.""" + await self._cover.async_set_target_position(kwargs[ATTR_POSITION]) + + @property + def device_class(self) -> str: + """Return the device class.""" + return CoverDeviceClass.SHUTTER + + @callback + def async_update_callback(self) -> None: + """Update the entity's state.""" + self._attr_is_closed = self._cover.current_position == 0 + self._attr_current_cover_position = self._cover.current_position diff --git a/homeassistant/components/netatmo/data_handler.py b/homeassistant/components/netatmo/data_handler.py index 50a3bed17ff..a376e6ee187 100644 --- a/homeassistant/components/netatmo/data_handler.py +++ b/homeassistant/components/netatmo/data_handler.py @@ -11,16 +11,34 @@ from time import time from typing import Any import pyatmo +from pyatmo.modules.device_types import DeviceCategory as NetatmoDeviceCategory from homeassistant.config_entries import ConfigEntry from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.event import async_track_time_interval from .const import ( AUTH, + DATA_PERSONS, + DATA_SCHEDULES, DOMAIN, MANUFACTURER, + NETATMO_CREATE_BATTERY, + NETATMO_CREATE_CAMERA, + NETATMO_CREATE_CAMERA_LIGHT, + NETATMO_CREATE_CLIMATE, + NETATMO_CREATE_COVER, + NETATMO_CREATE_LIGHT, + NETATMO_CREATE_ROOM_SENSOR, + NETATMO_CREATE_SELECT, + NETATMO_CREATE_SENSOR, + NETATMO_CREATE_SWITCH, + NETATMO_CREATE_WEATHER_SENSOR, + PLATFORMS, WEBHOOK_ACTIVATION, WEBHOOK_DEACTIVATION, WEBHOOK_NACAMERA_CONNECTION, @@ -29,30 +47,31 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -CAMERA_DATA_CLASS_NAME = "AsyncCameraData" -WEATHERSTATION_DATA_CLASS_NAME = "AsyncWeatherStationData" -HOMECOACH_DATA_CLASS_NAME = "AsyncHomeCoachData" -CLIMATE_TOPOLOGY_CLASS_NAME = "AsyncClimateTopology" -CLIMATE_STATE_CLASS_NAME = "AsyncClimate" -PUBLICDATA_DATA_CLASS_NAME = "AsyncPublicData" +SIGNAL_NAME = "signal_name" +ACCOUNT = "account" +HOME = "home" +WEATHER = "weather" +AIR_CARE = "air_care" +PUBLIC = "public" +EVENT = "event" -DATA_CLASSES = { - WEATHERSTATION_DATA_CLASS_NAME: pyatmo.AsyncWeatherStationData, - HOMECOACH_DATA_CLASS_NAME: pyatmo.AsyncHomeCoachData, - CAMERA_DATA_CLASS_NAME: pyatmo.AsyncCameraData, - CLIMATE_TOPOLOGY_CLASS_NAME: pyatmo.AsyncClimateTopology, - CLIMATE_STATE_CLASS_NAME: pyatmo.AsyncClimate, - PUBLICDATA_DATA_CLASS_NAME: pyatmo.AsyncPublicData, +PUBLISHERS = { + ACCOUNT: "async_update_topology", + HOME: "async_update_status", + WEATHER: "async_update_weather_stations", + AIR_CARE: "async_update_air_care", + PUBLIC: "async_update_public_weather", + EVENT: "async_update_events", } BATCH_SIZE = 3 DEFAULT_INTERVALS = { - CLIMATE_TOPOLOGY_CLASS_NAME: 3600, - CLIMATE_STATE_CLASS_NAME: 300, - CAMERA_DATA_CLASS_NAME: 900, - WEATHERSTATION_DATA_CLASS_NAME: 600, - HOMECOACH_DATA_CLASS_NAME: 300, - PUBLICDATA_DATA_CLASS_NAME: 600, + ACCOUNT: 10800, + HOME: 300, + WEATHER: 600, + AIR_CARE: 300, + PUBLIC: 600, + EVENT: 600, } SCAN_INTERVAL = 60 @@ -62,7 +81,27 @@ class NetatmoDevice: """Netatmo device class.""" data_handler: NetatmoDataHandler - device: pyatmo.climate.NetatmoModule + device: pyatmo.modules.Module + parent_id: str + signal_name: str + + +@dataclass +class NetatmoHome: + """Netatmo home class.""" + + data_handler: NetatmoDataHandler + home: pyatmo.Home + parent_id: str + signal_name: str + + +@dataclass +class NetatmoRoom: + """Netatmo room class.""" + + data_handler: NetatmoDataHandler + room: pyatmo.Room parent_id: str signal_name: str @@ -74,25 +113,27 @@ class NetatmoPublisher: name: str interval: int next_scan: float - subscriptions: list[CALLBACK_TYPE | None] + subscriptions: set[CALLBACK_TYPE | None] + method: str + kwargs: dict class NetatmoDataHandler: """Manages the Netatmo data handling.""" + account: pyatmo.AsyncAccount + def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: """Initialize self.""" self.hass = hass self.config_entry = config_entry self._auth = hass.data[DOMAIN][config_entry.entry_id][AUTH] self.publisher: dict[str, NetatmoPublisher] = {} - self.data: dict = {} self._queue: deque = deque() self._webhook: bool = False async def async_setup(self) -> None: """Set up the Netatmo data handler.""" - async_track_time_interval( self.hass, self.async_update, timedelta(seconds=SCAN_INTERVAL) ) @@ -105,17 +146,14 @@ class NetatmoDataHandler: ) ) - await asyncio.gather( - *[ - self.subscribe(data_class, data_class, None) - for data_class in ( - CLIMATE_TOPOLOGY_CLASS_NAME, - CAMERA_DATA_CLASS_NAME, - WEATHERSTATION_DATA_CLASS_NAME, - HOMECOACH_DATA_CLASS_NAME, - ) - ] + self.account = pyatmo.AsyncAccount(self._auth) + + await self.subscribe(ACCOUNT, ACCOUNT, None) + + await self.hass.config_entries.async_forward_entry_setups( + self.config_entry, PLATFORMS ) + await self.async_dispatch() async def async_update(self, event_time: datetime) -> None: """ @@ -153,19 +191,17 @@ class NetatmoDataHandler: elif event["data"][WEBHOOK_PUSH_TYPE] == WEBHOOK_NACAMERA_CONNECTION: _LOGGER.debug("%s camera reconnected", MANUFACTURER) - self.async_force_update(CAMERA_DATA_CLASS_NAME) + self.async_force_update(ACCOUNT) async def async_fetch_data(self, signal_name: str) -> None: """Fetch data and notify.""" - if self.data[signal_name] is None: - return - try: - await self.data[signal_name].async_update() + await getattr(self.account, self.publisher[signal_name].method)( + **self.publisher[signal_name].kwargs + ) except pyatmo.NoDevice as err: _LOGGER.debug(err) - self.data[signal_name] = None except pyatmo.ApiError as err: _LOGGER.debug(err) @@ -188,18 +224,21 @@ class NetatmoDataHandler: """Subscribe to publisher.""" if signal_name in self.publisher: if update_callback not in self.publisher[signal_name].subscriptions: - self.publisher[signal_name].subscriptions.append(update_callback) + self.publisher[signal_name].subscriptions.add(update_callback) return + if publisher == "public": + kwargs = {"area_id": self.account.register_public_weather_area(**kwargs)} + self.publisher[signal_name] = NetatmoPublisher( name=signal_name, interval=DEFAULT_INTERVALS[publisher], next_scan=time() + DEFAULT_INTERVALS[publisher], - subscriptions=[update_callback], + subscriptions={update_callback}, + method=PUBLISHERS[publisher], + kwargs=kwargs, ) - self.data[signal_name] = DATA_CLASSES[publisher](self._auth, **kwargs) - try: await self.async_fetch_data(signal_name) except KeyError: @@ -213,15 +252,158 @@ class NetatmoDataHandler: self, signal_name: str, update_callback: CALLBACK_TYPE | None ) -> None: """Unsubscribe from publisher.""" + if update_callback in self.publisher[signal_name].subscriptions: + return + self.publisher[signal_name].subscriptions.remove(update_callback) if not self.publisher[signal_name].subscriptions: self._queue.remove(self.publisher[signal_name]) self.publisher.pop(signal_name) - self.data.pop(signal_name) _LOGGER.debug("Publisher %s removed", signal_name) @property def webhook(self) -> bool: """Return the webhook state.""" return self._webhook + + async def async_dispatch(self) -> None: + """Dispatch the creation of entities.""" + await self.subscribe(WEATHER, WEATHER, None) + await self.subscribe(AIR_CARE, AIR_CARE, None) + + self.setup_air_care() + + for home in self.account.homes.values(): + signal_home = f"{HOME}-{home.entity_id}" + + await self.subscribe(HOME, signal_home, None, home_id=home.entity_id) + await self.subscribe(EVENT, signal_home, None, home_id=home.entity_id) + + self.setup_climate_schedule_select(home, signal_home) + self.setup_rooms(home, signal_home) + self.setup_modules(home, signal_home) + + self.hass.data[DOMAIN][DATA_PERSONS][home.entity_id] = { + person.entity_id: person.pseudo for person in home.persons.values() + } + + def setup_air_care(self) -> None: + """Set up home coach/air care modules.""" + for module in self.account.modules.values(): + if module.device_category is NetatmoDeviceCategory.air_care: + async_dispatcher_send( + self.hass, + NETATMO_CREATE_WEATHER_SENSOR, + NetatmoDevice( + self, + module, + AIR_CARE, + AIR_CARE, + ), + ) + + def setup_modules(self, home: pyatmo.Home, signal_home: str) -> None: + """Set up modules.""" + netatmo_type_signal_map = { + NetatmoDeviceCategory.camera: [ + NETATMO_CREATE_CAMERA, + NETATMO_CREATE_CAMERA_LIGHT, + ], + NetatmoDeviceCategory.dimmer: [NETATMO_CREATE_LIGHT], + NetatmoDeviceCategory.shutter: [NETATMO_CREATE_COVER], + NetatmoDeviceCategory.switch: [ + NETATMO_CREATE_LIGHT, + NETATMO_CREATE_SWITCH, + NETATMO_CREATE_SENSOR, + ], + NetatmoDeviceCategory.meter: [NETATMO_CREATE_SENSOR], + } + for module in home.modules.values(): + if not module.device_category: + continue + + for signal in netatmo_type_signal_map.get(module.device_category, []): + async_dispatcher_send( + self.hass, + signal, + NetatmoDevice( + self, + module, + home.entity_id, + signal_home, + ), + ) + if module.device_category is NetatmoDeviceCategory.weather: + async_dispatcher_send( + self.hass, + NETATMO_CREATE_WEATHER_SENSOR, + NetatmoDevice( + self, + module, + home.entity_id, + WEATHER, + ), + ) + + def setup_rooms(self, home: pyatmo.Home, signal_home: str) -> None: + """Set up rooms.""" + for room in home.rooms.values(): + if NetatmoDeviceCategory.climate in room.features: + async_dispatcher_send( + self.hass, + NETATMO_CREATE_CLIMATE, + NetatmoRoom( + self, + room, + home.entity_id, + signal_home, + ), + ) + + for module in room.modules.values(): + if module.device_category is NetatmoDeviceCategory.climate: + async_dispatcher_send( + self.hass, + NETATMO_CREATE_BATTERY, + NetatmoDevice( + self, + module, + room.entity_id, + signal_home, + ), + ) + + if "humidity" in room.features: + async_dispatcher_send( + self.hass, + NETATMO_CREATE_ROOM_SENSOR, + NetatmoRoom( + self, + room, + room.entity_id, + signal_home, + ), + ) + + def setup_climate_schedule_select( + self, home: pyatmo.Home, signal_home: str + ) -> None: + """Set up climate schedule per home.""" + if NetatmoDeviceCategory.climate in [ + next(iter(x)) for x in [room.features for room in home.rooms.values()] if x + ]: + self.hass.data[DOMAIN][DATA_SCHEDULES][home.entity_id] = self.account.homes[ + home.entity_id + ].schedules + + async_dispatcher_send( + self.hass, + NETATMO_CREATE_SELECT, + NetatmoHome( + self, + home, + home.entity_id, + signal_home, + ), + ) diff --git a/homeassistant/components/netatmo/device_trigger.py b/homeassistant/components/netatmo/device_trigger.py index 955671e3dc1..b037f45533f 100644 --- a/homeassistant/components/netatmo/device_trigger.py +++ b/homeassistant/components/netatmo/device_trigger.py @@ -31,10 +31,6 @@ from .const import ( DOMAIN, EVENT_TYPE_THERM_MODE, INDOOR_CAMERA_TRIGGERS, - MODEL_NACAMERA, - MODEL_NATHERM1, - MODEL_NOC, - MODEL_NRV, NETATMO_EVENT, OUTDOOR_CAMERA_TRIGGERS, ) @@ -42,10 +38,10 @@ from .const import ( CONF_SUBTYPE = "subtype" DEVICES = { - MODEL_NACAMERA: INDOOR_CAMERA_TRIGGERS, - MODEL_NOC: OUTDOOR_CAMERA_TRIGGERS, - MODEL_NATHERM1: CLIMATE_TRIGGERS, - MODEL_NRV: CLIMATE_TRIGGERS, + "NACamera": INDOOR_CAMERA_TRIGGERS, + "NOC": OUTDOOR_CAMERA_TRIGGERS, + "NATherm1": CLIMATE_TRIGGERS, + "NRV": CLIMATE_TRIGGERS, } SUBTYPES = { @@ -76,7 +72,7 @@ async def async_validate_trigger_config( device_registry = dr.async_get(hass) device = device_registry.async_get(config[CONF_DEVICE_ID]) - if not device: + if not device or device.model is None: raise InvalidDeviceAutomationConfig( f"Trigger invalid, device with ID {config[CONF_DEVICE_ID]} not found" ) diff --git a/homeassistant/components/netatmo/diagnostics.py b/homeassistant/components/netatmo/diagnostics.py index 6c82c7f1db7..cac9c695f19 100644 --- a/homeassistant/components/netatmo/diagnostics.py +++ b/homeassistant/components/netatmo/diagnostics.py @@ -6,7 +6,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from .const import DATA_HANDLER, DOMAIN -from .data_handler import CLIMATE_TOPOLOGY_CLASS_NAME, NetatmoDataHandler +from .data_handler import ACCOUNT, NetatmoDataHandler TO_REDACT = { "access_token", @@ -45,8 +45,8 @@ async def async_get_config_entry_diagnostics( TO_REDACT, ), "data": { - CLIMATE_TOPOLOGY_CLASS_NAME: async_redact_data( - getattr(data_handler.data[CLIMATE_TOPOLOGY_CLASS_NAME], "raw_data"), + ACCOUNT: async_redact_data( + getattr(data_handler.account, "raw_data"), TO_REDACT, ) }, diff --git a/homeassistant/components/netatmo/light.py b/homeassistant/components/netatmo/light.py index 6567ae770f2..b3e352eb7d8 100644 --- a/homeassistant/components/netatmo/light.py +++ b/homeassistant/components/netatmo/light.py @@ -4,25 +4,25 @@ from __future__ import annotations import logging from typing import Any, cast -import pyatmo +from pyatmo import modules as NaModules -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, callback -from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( - DATA_HANDLER, + CONF_URL_CONTROL, + CONF_URL_SECURITY, DOMAIN, EVENT_TYPE_LIGHT_MODE, - SIGNAL_NAME, - TYPE_SECURITY, + NETATMO_CREATE_CAMERA_LIGHT, + NETATMO_CREATE_LIGHT, WEBHOOK_LIGHT_MODE, WEBHOOK_PUSH_TYPE, ) -from .data_handler import CAMERA_DATA_CLASS_NAME, NetatmoDataHandler +from .data_handler import HOME, SIGNAL_NAME, NetatmoDevice from .netatmo_entity_base import NetatmoBase _LOGGER = logging.getLogger(__name__) @@ -32,66 +32,73 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the Netatmo camera light platform.""" - data_handler = hass.data[DOMAIN][entry.entry_id][DATA_HANDLER] - data_class = data_handler.data.get(CAMERA_DATA_CLASS_NAME) - if not data_class or data_class.raw_data == {}: - raise PlatformNotReady + @callback + def _create_camera_light_entity(netatmo_device: NetatmoDevice) -> None: + if not hasattr(netatmo_device.device, "floodlight"): + return - all_cameras = [] - for home in data_handler.data[CAMERA_DATA_CLASS_NAME].cameras.values(): - for camera in home.values(): - all_cameras.append(camera) + entity = NetatmoCameraLight(netatmo_device) + async_add_entities([entity]) - entities = [ - NetatmoLight( - data_handler, - camera["id"], - camera["type"], - camera["home_id"], + entry.async_on_unload( + async_dispatcher_connect( + hass, NETATMO_CREATE_CAMERA_LIGHT, _create_camera_light_entity ) - for camera in all_cameras - if camera["type"] == "NOC" - ] + ) - _LOGGER.debug("Adding camera lights %s", entities) - async_add_entities(entities, True) + @callback + def _create_entity(netatmo_device: NetatmoDevice) -> None: + if not hasattr(netatmo_device.device, "brightness"): + return + + entity = NetatmoLight(netatmo_device) + _LOGGER.debug("Adding light %s", entity) + async_add_entities([entity]) + + entry.async_on_unload( + async_dispatcher_connect(hass, NETATMO_CREATE_LIGHT, _create_entity) + ) -class NetatmoLight(NetatmoBase, LightEntity): +class NetatmoCameraLight(NetatmoBase, LightEntity): """Representation of a Netatmo Presence camera light.""" - _attr_color_mode = ColorMode.ONOFF _attr_has_entity_name = True - _attr_supported_color_modes = {ColorMode.ONOFF} def __init__( self, - data_handler: NetatmoDataHandler, - camera_id: str, - camera_type: str, - home_id: str, + netatmo_device: NetatmoDevice, ) -> None: """Initialize a Netatmo Presence camera light.""" LightEntity.__init__(self) - super().__init__(data_handler) + super().__init__(netatmo_device.data_handler) - self._publishers.append( - {"name": CAMERA_DATA_CLASS_NAME, SIGNAL_NAME: CAMERA_DATA_CLASS_NAME} - ) - self._id = camera_id - self._home_id = home_id - self._model = camera_type - self._netatmo_type = TYPE_SECURITY - self._device_name: str = self._data.get_camera(camera_id)["name"] + self._camera = cast(NaModules.NOC, netatmo_device.device) + self._id = self._camera.entity_id + self._home_id = self._camera.home.entity_id + self._device_name = self._camera.name + self._model = self._camera.device_type + self._config_url = CONF_URL_SECURITY self._is_on = False self._attr_unique_id = f"{self._id}-light" + self._signal_name = f"{HOME}-{self._home_id}" + self._publishers.extend( + [ + { + "name": HOME, + "home_id": self._camera.home.entity_id, + SIGNAL_NAME: self._signal_name, + }, + ] + ) + async def async_added_to_hass(self) -> None: """Entity created.""" await super().async_added_to_hass() - self.data_handler.config_entry.async_on_unload( + self.async_on_remove( async_dispatcher_connect( self.hass, f"signal-{DOMAIN}-webhook-{EVENT_TYPE_LIGHT_MODE}", @@ -117,14 +124,6 @@ class NetatmoLight(NetatmoBase, LightEntity): self.async_write_ha_state() return - @property - def _data(self) -> pyatmo.AsyncCameraData: - """Return data for this entity.""" - return cast( - pyatmo.AsyncCameraData, - self.data_handler.data[self._publishers[0]["name"]], - ) - @property def available(self) -> bool: """If the webhook is not established, mark as unavailable.""" @@ -138,22 +137,79 @@ class NetatmoLight(NetatmoBase, LightEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn camera floodlight on.""" _LOGGER.debug("Turn camera '%s' on", self.name) - await self._data.async_set_state( - home_id=self._home_id, - camera_id=self._id, - floodlight="on", - ) + await self._camera.async_floodlight_on() async def async_turn_off(self, **kwargs: Any) -> None: """Turn camera floodlight into auto mode.""" _LOGGER.debug("Turn camera '%s' to auto mode", self.name) - await self._data.async_set_state( - home_id=self._home_id, - camera_id=self._id, - floodlight="auto", - ) + await self._camera.async_floodlight_auto() @callback def async_update_callback(self) -> None: """Update the entity's state.""" - self._is_on = bool(self._data.get_light_state(self._id) == "on") + self._is_on = bool(self._camera.floodlight == "on") + + +class NetatmoLight(NetatmoBase, LightEntity): + """Representation of a dimmable light by Legrand/BTicino.""" + + def __init__( + self, + netatmo_device: NetatmoDevice, + ) -> None: + """Initialize a Netatmo light.""" + super().__init__(netatmo_device.data_handler) + + self._dimmer = cast(NaModules.NLFN, netatmo_device.device) + self._id = self._dimmer.entity_id + self._home_id = self._dimmer.home.entity_id + self._device_name = self._dimmer.name + self._attr_name = f"{self._device_name}" + self._model = self._dimmer.device_type + self._config_url = CONF_URL_CONTROL + self._attr_brightness = 0 + self._attr_unique_id = f"{self._id}-light" + + self._attr_supported_color_modes: set[str] = set() + + if not self._attr_supported_color_modes and self._dimmer.brightness is not None: + self._attr_supported_color_modes.add(ColorMode.BRIGHTNESS) + + self._signal_name = f"{HOME}-{self._home_id}" + self._publishers.extend( + [ + { + "name": HOME, + "home_id": self._dimmer.home.entity_id, + SIGNAL_NAME: self._signal_name, + }, + ] + ) + + @property + def is_on(self) -> bool: + """Return true if light is on.""" + return self._dimmer.on is True + + 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() + + 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() + + @callback + def async_update_callback(self) -> None: + """Update the entity's state.""" + if self._dimmer.brightness is not None: + # Netatmo uses a range of [0, 100] to control brightness + self._attr_brightness = round((self._dimmer.brightness / 100) * 255) + else: + self._attr_brightness = None diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index 2081f9bd274..b198c43bb39 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==6.2.4"], + "requirements": ["pyatmo==7.0.1"], "after_dependencies": ["cloud", "media_source"], "dependencies": ["application_credentials", "webhook"], "codeowners": ["@cgtobi"], diff --git a/homeassistant/components/netatmo/media_source.py b/homeassistant/components/netatmo/media_source.py index 948d162a613..58bf2f93c96 100644 --- a/homeassistant/components/netatmo/media_source.py +++ b/homeassistant/components/netatmo/media_source.py @@ -72,21 +72,13 @@ class NetatmoSource(MediaSource): self, source: str, camera_id: str, event_id: int | None = None ) -> BrowseMediaSource: if event_id and event_id in self.events[camera_id]: - created = dt.datetime.fromtimestamp(event_id) - if self.events[camera_id][event_id]["type"] == "outdoor": - thumbnail = ( - self.events[camera_id][event_id]["event_list"][0] - .get("snapshot", {}) - .get("url") - ) - message = remove_html_tags( - self.events[camera_id][event_id]["event_list"][0]["message"] - ) - else: - thumbnail = ( - self.events[camera_id][event_id].get("snapshot", {}).get("url") - ) - message = remove_html_tags(self.events[camera_id][event_id]["message"]) + created = dt.datetime.fromtimestamp( + self.events[camera_id][event_id]["event_time"] + ) + thumbnail = self.events[camera_id][event_id].get("snapshot", {}).get("url") + message = remove_html_tags( + self.events[camera_id][event_id].get("message", "") + ) title = f"{created} - {message}" else: title = self.hass.data[DOMAIN][DATA_CAMERAS].get(camera_id, MANUFACTURER) diff --git a/homeassistant/components/netatmo/netatmo_entity_base.py b/homeassistant/components/netatmo/netatmo_entity_base.py index e8a346ccd84..081d06f5d4f 100644 --- a/homeassistant/components/netatmo/netatmo_entity_base.py +++ b/homeassistant/components/netatmo/netatmo_entity_base.py @@ -3,20 +3,18 @@ from __future__ import annotations from typing import Any +from pyatmo.modules.device_types import ( + DEVICE_DESCRIPTION_MAP, + DeviceType as NetatmoDeviceType, +) + from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.core import callback from homeassistant.helpers import device_registry as dr from homeassistant.helpers.entity import DeviceInfo, Entity -from .const import ( - DATA_DEVICE_IDS, - DEFAULT_ATTRIBUTION, - DOMAIN, - MANUFACTURER, - MODELS, - SIGNAL_NAME, -) -from .data_handler import PUBLICDATA_DATA_CLASS_NAME, NetatmoDataHandler +from .const import DATA_DEVICE_IDS, DEFAULT_ATTRIBUTION, DOMAIN, SIGNAL_NAME +from .data_handler import PUBLIC, NetatmoDataHandler class NetatmoBase(Entity): @@ -30,38 +28,38 @@ class NetatmoBase(Entity): self._device_name: str = "" self._id: str = "" self._model: str = "" - self._netatmo_type: str = "" + self._config_url: str = "" self._attr_name = None self._attr_unique_id = None self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} async def async_added_to_hass(self) -> None: """Entity created.""" - for data_class in self._publishers: - signal_name = data_class[SIGNAL_NAME] + for publisher in self._publishers: + signal_name = publisher[SIGNAL_NAME] - if "home_id" in data_class: + if "home_id" in publisher: await self.data_handler.subscribe( - data_class["name"], + publisher["name"], signal_name, self.async_update_callback, - home_id=data_class["home_id"], + home_id=publisher["home_id"], ) - elif data_class["name"] == PUBLICDATA_DATA_CLASS_NAME: + elif publisher["name"] == PUBLIC: await self.data_handler.subscribe( - data_class["name"], + publisher["name"], signal_name, self.async_update_callback, - lat_ne=data_class["lat_ne"], - lon_ne=data_class["lon_ne"], - lat_sw=data_class["lat_sw"], - lon_sw=data_class["lon_sw"], + lat_ne=publisher["lat_ne"], + lon_ne=publisher["lon_ne"], + lat_sw=publisher["lat_sw"], + lon_sw=publisher["lon_sw"], ) else: await self.data_handler.subscribe( - data_class["name"], signal_name, self.async_update_callback + publisher["name"], signal_name, self.async_update_callback ) for sub in self.data_handler.publisher[signal_name].subscriptions: @@ -78,9 +76,9 @@ class NetatmoBase(Entity): """Run when entity will be removed from hass.""" await super().async_will_remove_from_hass() - for data_class in self._publishers: + for publisher in self._publishers: await self.data_handler.unsubscribe( - data_class[SIGNAL_NAME], self.async_update_callback + publisher[SIGNAL_NAME], self.async_update_callback ) @callback @@ -91,10 +89,13 @@ class NetatmoBase(Entity): @property def device_info(self) -> DeviceInfo: """Return the device info for the sensor.""" + manufacturer, model = DEVICE_DESCRIPTION_MAP[ + getattr(NetatmoDeviceType, self._model) + ] return DeviceInfo( - configuration_url=f"https://my.netatmo.com/app/{self._netatmo_type}", + configuration_url=self._config_url, identifiers={(DOMAIN, self._id)}, name=self._device_name, - manufacturer=MANUFACTURER, - model=MODELS[self._model], + manufacturer=manufacturer, + model=model, ) diff --git a/homeassistant/components/netatmo/select.py b/homeassistant/components/netatmo/select.py index 62e6ef25969..3651ae05e88 100644 --- a/homeassistant/components/netatmo/select.py +++ b/homeassistant/components/netatmo/select.py @@ -3,29 +3,20 @@ from __future__ import annotations import logging -import pyatmo - from homeassistant.components.select import SelectEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( - DATA_HANDLER, + CONF_URL_ENERGY, DATA_SCHEDULES, DOMAIN, EVENT_TYPE_SCHEDULE, - MANUFACTURER, - SIGNAL_NAME, - TYPE_ENERGY, -) -from .data_handler import ( - CLIMATE_STATE_CLASS_NAME, - CLIMATE_TOPOLOGY_CLASS_NAME, - NetatmoDataHandler, + NETATMO_CREATE_SELECT, ) +from .data_handler import HOME, SIGNAL_NAME, NetatmoHome from .netatmo_entity_base import NetatmoBase _LOGGER = logging.getLogger(__name__) @@ -35,100 +26,66 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the Netatmo energy platform schedule selector.""" - data_handler = hass.data[DOMAIN][entry.entry_id][DATA_HANDLER] - climate_topology = data_handler.data.get(CLIMATE_TOPOLOGY_CLASS_NAME) + @callback + def _create_entity(netatmo_home: NetatmoHome) -> None: + entity = NetatmoScheduleSelect(netatmo_home) + async_add_entities([entity]) - if not climate_topology or climate_topology.raw_data == {}: - raise PlatformNotReady - - entities = [] - for home_id in climate_topology.home_ids: - signal_name = f"{CLIMATE_STATE_CLASS_NAME}-{home_id}" - - await data_handler.subscribe( - CLIMATE_STATE_CLASS_NAME, signal_name, None, home_id=home_id - ) - - if (climate_state := data_handler.data[signal_name]) is None: - continue - - climate_topology.register_handler(home_id, climate_state.process_topology) - - hass.data[DOMAIN][DATA_SCHEDULES][home_id] = climate_state.homes[ - home_id - ].schedules - - entities = [ - NetatmoScheduleSelect( - data_handler, - home_id, - [schedule.name for schedule in schedules.values()], - ) - for home_id, schedules in hass.data[DOMAIN][DATA_SCHEDULES].items() - if schedules - ] - - _LOGGER.debug("Adding climate schedule select entities %s", entities) - async_add_entities(entities, True) + entry.async_on_unload( + async_dispatcher_connect(hass, NETATMO_CREATE_SELECT, _create_entity) + ) class NetatmoScheduleSelect(NetatmoBase, SelectEntity): """Representation a Netatmo thermostat schedule selector.""" def __init__( - self, data_handler: NetatmoDataHandler, home_id: str, options: list + self, + netatmo_home: NetatmoHome, ) -> None: """Initialize the select entity.""" SelectEntity.__init__(self) - super().__init__(data_handler) + super().__init__(netatmo_home.data_handler) - self._home_id = home_id - - self._climate_state_class = f"{CLIMATE_STATE_CLASS_NAME}-{self._home_id}" - self._climate_state: pyatmo.AsyncClimate = data_handler.data[ - self._climate_state_class - ] - - self._home = self._climate_state.homes[self._home_id] + self._home = netatmo_home.home + self._home_id = self._home.entity_id + self._signal_name = netatmo_home.signal_name self._publishers.extend( [ { - "name": CLIMATE_TOPOLOGY_CLASS_NAME, - SIGNAL_NAME: CLIMATE_TOPOLOGY_CLASS_NAME, - }, - { - "name": CLIMATE_STATE_CLASS_NAME, - "home_id": self._home_id, - SIGNAL_NAME: self._climate_state_class, + "name": HOME, + "home_id": self._home.entity_id, + SIGNAL_NAME: self._signal_name, }, ] ) self._device_name = self._home.name - self._attr_name = f"{MANUFACTURER} {self._device_name}" + self._attr_name = f"{self._device_name}" self._model: str = "NATherm1" - self._netatmo_type = TYPE_ENERGY + self._config_url = CONF_URL_ENERGY self._attr_unique_id = f"{self._home_id}-schedule-select" self._attr_current_option = getattr(self._home.get_selected_schedule(), "name") - self._attr_options = options + self._attr_options = [ + schedule.name for schedule in self._home.schedules.values() + ] async def async_added_to_hass(self) -> None: """Entity created.""" await super().async_added_to_hass() - for event_type in (EVENT_TYPE_SCHEDULE,): - self.data_handler.config_entry.async_on_unload( - async_dispatcher_connect( - self.hass, - f"signal-{DOMAIN}-webhook-{event_type}", - self.handle_event, - ) + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"signal-{DOMAIN}-webhook-{EVENT_TYPE_SCHEDULE}", + self.handle_event, ) + ) @callback def handle_event(self, event: dict) -> None: @@ -160,7 +117,7 @@ class NetatmoScheduleSelect(NetatmoBase, SelectEntity): option, sid, ) - await self._climate_state.async_switch_home_schedule(schedule_id=sid) + await self._home.async_switch_schedule(schedule_id=sid) break @callback @@ -169,8 +126,5 @@ class NetatmoScheduleSelect(NetatmoBase, SelectEntity): self._attr_current_option = getattr(self._home.get_selected_schedule(), "name") self.hass.data[DOMAIN][DATA_SCHEDULES][self._home_id] = self._home.schedules self._attr_options = [ - schedule.name - for schedule in self.hass.data[DOMAIN][DATA_SCHEDULES][ - self._home_id - ].values() + schedule.name for schedule in self._home.schedules.values() ] diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index bec9af96442..ff555ecd472 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -1,9 +1,9 @@ -"""Support for the Netatmo Weather Service.""" +"""Support for the Netatmo sensors.""" from __future__ import annotations from dataclasses import dataclass import logging -from typing import NamedTuple, cast +from typing import cast import pyatmo @@ -21,15 +21,15 @@ from homeassistant.const import ( DEGREE, LENGTH_MILLIMETERS, PERCENTAGE, + POWER_WATT, PRESSURE_MBAR, - SIGNAL_STRENGTH_DECIBELS_MILLIWATT, SOUND_PRESSURE_DB, SPEED_KILOMETERS_PER_HOUR, TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.device_registry import async_entries_for_config_entry from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -38,20 +38,18 @@ from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( + CONF_URL_ENERGY, + CONF_URL_WEATHER, CONF_WEATHER_AREAS, DATA_HANDLER, DOMAIN, NETATMO_CREATE_BATTERY, + NETATMO_CREATE_ROOM_SENSOR, + NETATMO_CREATE_SENSOR, + NETATMO_CREATE_WEATHER_SENSOR, SIGNAL_NAME, - TYPE_WEATHER, -) -from .data_handler import ( - HOMECOACH_DATA_CLASS_NAME, - PUBLICDATA_DATA_CLASS_NAME, - WEATHERSTATION_DATA_CLASS_NAME, - NetatmoDataHandler, - NetatmoDevice, ) +from .data_handler import HOME, PUBLIC, NetatmoDataHandler, NetatmoDevice, NetatmoRoom from .helper import NetatmoArea from .netatmo_entity_base import NetatmoBase @@ -62,10 +60,12 @@ SUPPORTED_PUBLIC_SENSOR_TYPES: tuple[str, ...] = ( "pressure", "humidity", "rain", - "windstrength", - "guststrength", + "wind_strength", + "gust_strength", "sum_rain_1", "sum_rain_24", + "wind_angle", + "gust_angle", ) @@ -85,7 +85,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( NetatmoSensorEntityDescription( key="temperature", name="Temperature", - netatmo_name="Temperature", + netatmo_name="temperature", entity_registry_enabled_default=True, native_unit_of_measurement=TEMP_CELSIUS, state_class=SensorStateClass.MEASUREMENT, @@ -101,7 +101,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( NetatmoSensorEntityDescription( key="co2", name="CO2", - netatmo_name="CO2", + netatmo_name="co2", native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, entity_registry_enabled_default=True, state_class=SensorStateClass.MEASUREMENT, @@ -110,7 +110,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( NetatmoSensorEntityDescription( key="pressure", name="Pressure", - netatmo_name="Pressure", + netatmo_name="pressure", entity_registry_enabled_default=True, native_unit_of_measurement=PRESSURE_MBAR, state_class=SensorStateClass.MEASUREMENT, @@ -126,7 +126,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( NetatmoSensorEntityDescription( key="noise", name="Noise", - netatmo_name="Noise", + netatmo_name="noise", entity_registry_enabled_default=True, native_unit_of_measurement=SOUND_PRESSURE_DB, icon="mdi:volume-high", @@ -135,7 +135,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( NetatmoSensorEntityDescription( key="humidity", name="Humidity", - netatmo_name="Humidity", + netatmo_name="humidity", entity_registry_enabled_default=True, native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, @@ -144,7 +144,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( NetatmoSensorEntityDescription( key="rain", name="Rain", - netatmo_name="Rain", + netatmo_name="rain", entity_registry_enabled_default=True, native_unit_of_measurement=LENGTH_MILLIMETERS, state_class=SensorStateClass.MEASUREMENT, @@ -156,7 +156,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( netatmo_name="sum_rain_1", entity_registry_enabled_default=False, native_unit_of_measurement=LENGTH_MILLIMETERS, - state_class=SensorStateClass.TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL, icon="mdi:weather-rainy", ), NetatmoSensorEntityDescription( @@ -171,7 +171,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( NetatmoSensorEntityDescription( key="battery_percent", name="Battery Percent", - netatmo_name="battery_percent", + netatmo_name="battery", entity_registry_enabled_default=True, entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=PERCENTAGE, @@ -181,14 +181,14 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( NetatmoSensorEntityDescription( key="windangle", name="Direction", - netatmo_name="WindAngle", + netatmo_name="wind_direction", entity_registry_enabled_default=True, icon="mdi:compass-outline", ), NetatmoSensorEntityDescription( key="windangle_value", name="Angle", - netatmo_name="WindAngle", + netatmo_name="wind_angle", entity_registry_enabled_default=False, native_unit_of_measurement=DEGREE, icon="mdi:compass-outline", @@ -197,7 +197,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( NetatmoSensorEntityDescription( key="windstrength", name="Wind Strength", - netatmo_name="WindStrength", + netatmo_name="wind_strength", entity_registry_enabled_default=True, native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, icon="mdi:weather-windy", @@ -206,14 +206,14 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( NetatmoSensorEntityDescription( key="gustangle", name="Gust Direction", - netatmo_name="GustAngle", + netatmo_name="gust_direction", entity_registry_enabled_default=False, icon="mdi:compass-outline", ), NetatmoSensorEntityDescription( key="gustangle_value", name="Gust Angle", - netatmo_name="GustAngle", + netatmo_name="gust_angle", entity_registry_enabled_default=False, native_unit_of_measurement=DEGREE, icon="mdi:compass-outline", @@ -222,7 +222,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( NetatmoSensorEntityDescription( key="guststrength", name="Gust Strength", - netatmo_name="GustStrength", + netatmo_name="gust_strength", entity_registry_enabled_default=False, native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, icon="mdi:weather-windy", @@ -239,39 +239,19 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( NetatmoSensorEntityDescription( key="rf_status", name="Radio", - netatmo_name="rf_status", + netatmo_name="rf_strength", entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:signal", ), - NetatmoSensorEntityDescription( - key="rf_status_lvl", - name="Radio Level", - netatmo_name="rf_status", - entity_registry_enabled_default=False, - entity_category=EntityCategory.DIAGNOSTIC, - native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, - state_class=SensorStateClass.MEASUREMENT, - device_class=SensorDeviceClass.SIGNAL_STRENGTH, - ), NetatmoSensorEntityDescription( key="wifi_status", name="Wifi", - netatmo_name="wifi_status", + netatmo_name="wifi_strength", entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:wifi", ), - NetatmoSensorEntityDescription( - key="wifi_status_lvl", - name="Wifi Level", - netatmo_name="wifi_status", - entity_registry_enabled_default=False, - entity_category=EntityCategory.DIAGNOSTIC, - native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, - state_class=SensorStateClass.MEASUREMENT, - device_class=SensorDeviceClass.SIGNAL_STRENGTH, - ), NetatmoSensorEntityDescription( key="health_idx", name="Health", @@ -279,136 +259,110 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( entity_registry_enabled_default=True, icon="mdi:cloud", ), + NetatmoSensorEntityDescription( + key="power", + name="Power", + netatmo_name="power", + entity_registry_enabled_default=True, + native_unit_of_measurement=POWER_WATT, + state_class=SensorStateClass.TOTAL, + device_class=SensorDeviceClass.POWER, + ), ) SENSOR_TYPES_KEYS = [desc.key for desc in SENSOR_TYPES] -MODULE_TYPE_OUTDOOR = "NAModule1" -MODULE_TYPE_WIND = "NAModule2" -MODULE_TYPE_RAIN = "NAModule3" -MODULE_TYPE_INDOOR = "NAModule4" - - -class BatteryData(NamedTuple): - """Metadata for a batter.""" - - full: int - high: int - medium: int - low: int - - -BATTERY_VALUES = { - MODULE_TYPE_WIND: BatteryData( - full=5590, - high=5180, - medium=4770, - low=4360, - ), - MODULE_TYPE_RAIN: BatteryData( - full=5500, - high=5000, - medium=4500, - low=4000, - ), - MODULE_TYPE_INDOOR: BatteryData( - full=5500, - high=5280, - medium=4920, - low=4560, - ), - MODULE_TYPE_OUTDOOR: BatteryData( - full=5500, - high=5000, - medium=4500, - low=4000, - ), -} - -PUBLIC = "public" +BATTERY_SENSOR_DESCRIPTION = NetatmoSensorEntityDescription( + key="battery", + name="Battery Percent", + netatmo_name="battery", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.BATTERY, +) async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: - """Set up the Netatmo weather and homecoach platform.""" - data_handler = hass.data[DOMAIN][entry.entry_id][DATA_HANDLER] - platform_not_ready = True + """Set up the Netatmo sensor platform.""" - async def find_entities(data_class_name: str) -> list: - """Find all entities.""" - all_module_infos = {} - data = data_handler.data + @callback + def _create_battery_entity(netatmo_device: NetatmoDevice) -> None: + if not hasattr(netatmo_device.device, "battery"): + return + entity = NetatmoClimateBatterySensor(netatmo_device) + async_add_entities([entity]) - if data_class_name not in data: - return [] + entry.async_on_unload( + async_dispatcher_connect(hass, NETATMO_CREATE_BATTERY, _create_battery_entity) + ) - if data[data_class_name] is None: - return [] + @callback + def _create_weather_sensor_entity(netatmo_device: NetatmoDevice) -> None: + async_add_entities( + NetatmoWeatherSensor(netatmo_device, description) + for description in SENSOR_TYPES + if description.netatmo_name in netatmo_device.device.features + ) - data_class = data[data_class_name] + entry.async_on_unload( + async_dispatcher_connect( + hass, NETATMO_CREATE_WEATHER_SENSOR, _create_weather_sensor_entity + ) + ) - for station_id in data_class.stations: - for module_id in data_class.get_modules(station_id): - all_module_infos[module_id] = data_class.get_module(module_id) - - all_module_infos[station_id] = data_class.get_station(station_id) - - entities = [] - for module in all_module_infos.values(): - if "_id" not in module: - _LOGGER.debug("Skipping module %s", module.get("module_name")) - continue - - conditions = [ - c.lower() - for c in data_class.get_monitored_conditions(module_id=module["_id"]) - if c.lower() in SENSOR_TYPES_KEYS + @callback + def _create_sensor_entity(netatmo_device: NetatmoDevice) -> None: + _LOGGER.debug( + "Adding %s sensor %s", + netatmo_device.device.device_category, + netatmo_device.device.name, + ) + async_add_entities( + [ + NetatmoSensor(netatmo_device, description) + for description in SENSOR_TYPES + if description.key in netatmo_device.device.features ] - for condition in conditions: - if f"{condition}_value" in SENSOR_TYPES_KEYS: - conditions.append(f"{condition}_value") - elif f"{condition}_lvl" in SENSOR_TYPES_KEYS: - conditions.append(f"{condition}_lvl") + ) - entities.extend( - [ - NetatmoSensor(data_handler, data_class_name, module, description) - for description in SENSOR_TYPES - if description.key in conditions - ] - ) + entry.async_on_unload( + async_dispatcher_connect(hass, NETATMO_CREATE_SENSOR, _create_sensor_entity) + ) - _LOGGER.debug("Adding weather sensors %s", entities) - return entities + @callback + def _create_room_sensor_entity(netatmo_device: NetatmoRoom) -> None: + async_add_entities( + NetatmoRoomSensor(netatmo_device, description) + for description in SENSOR_TYPES + if description.key in netatmo_device.room.features + ) - for data_class_name in ( - WEATHERSTATION_DATA_CLASS_NAME, - HOMECOACH_DATA_CLASS_NAME, - ): - data_class = data_handler.data.get(data_class_name) - - if data_class and data_class.raw_data: - platform_not_ready = False - - async_add_entities(await find_entities(data_class_name), True) + entry.async_on_unload( + async_dispatcher_connect( + hass, NETATMO_CREATE_ROOM_SENSOR, _create_room_sensor_entity + ) + ) device_registry = dr.async_get(hass) + data_handler = hass.data[DOMAIN][entry.entry_id][DATA_HANDLER] async def add_public_entities(update: bool = True) -> None: """Retrieve Netatmo public weather entities.""" entities = { device.name: device.id - for device in dr.async_entries_for_config_entry( + for device in async_entries_for_config_entry( device_registry, entry.entry_id ) - if device.model == "Public Weather stations" + if device.model == "Public Weather station" } new_entities = [] for area in [ NetatmoArea(**i) for i in entry.options.get(CONF_WEATHER_AREAS, {}).values() ]: - signal_name = f"{PUBLICDATA_DATA_CLASS_NAME}-{area.uuid}" + signal_name = f"{PUBLIC}-{area.uuid}" if area.area_name in entities: entities.pop(area.area_name) @@ -422,25 +376,21 @@ async def async_setup_entry( continue await data_handler.subscribe( - PUBLICDATA_DATA_CLASS_NAME, + PUBLIC, signal_name, None, lat_ne=area.lat_ne, lon_ne=area.lon_ne, lat_sw=area.lat_sw, lon_sw=area.lon_sw, + area_id=str(area.uuid), ) - data_class = data_handler.data.get(signal_name) - - if data_class and data_class.raw_data: - nonlocal platform_not_ready - platform_not_ready = False new_entities.extend( [ NetatmoPublicSensor(data_handler, area, description) for description in SENSOR_TYPES - if description.key in SUPPORTED_PUBLIC_SENSOR_TYPES + if description.netatmo_name in SUPPORTED_PUBLIC_SENSOR_TYPES ] ) @@ -454,68 +404,56 @@ async def async_setup_entry( hass, f"signal-{DOMAIN}-public-update-{entry.entry_id}", add_public_entities ) - @callback - def _create_entity(netatmo_device: NetatmoDevice) -> None: - entity = NetatmoClimateBatterySensor(netatmo_device) - _LOGGER.debug("Adding climate battery sensor %s", entity) - async_add_entities([entity]) - - entry.async_on_unload( - async_dispatcher_connect(hass, NETATMO_CREATE_BATTERY, _create_entity) - ) - await add_public_entities(False) - if platform_not_ready: - raise PlatformNotReady +class NetatmoWeatherSensor(NetatmoBase, SensorEntity): + """Implementation of a Netatmo weather/home coach sensor.""" -class NetatmoSensor(NetatmoBase, SensorEntity): - """Implementation of a Netatmo sensor.""" - + _attr_has_entity_name = True entity_description: NetatmoSensorEntityDescription def __init__( self, - data_handler: NetatmoDataHandler, - data_class_name: str, - module_info: dict, + netatmo_device: NetatmoDevice, description: NetatmoSensorEntityDescription, ) -> None: """Initialize the sensor.""" - super().__init__(data_handler) + super().__init__(netatmo_device.data_handler) self.entity_description = description - self._publishers.append({"name": data_class_name, SIGNAL_NAME: data_class_name}) + self._module = netatmo_device.device + self._id = self._module.entity_id + self._station_id = ( + self._module.bridge if self._module.bridge is not None else self._id + ) + self._device_name = self._module.name + category = getattr(self._module.device_category, "name") + self._publishers.extend( + [ + { + "name": category, + SIGNAL_NAME: category, + }, + ] + ) - self._id = module_info["_id"] - self._station_id = module_info.get("main_device", self._id) - - station = self._data.get_station(self._station_id) - if not (device := self._data.get_module(self._id)): - # Assume it's a station if module can't be found - device = station - - if device["type"] in ("NHC", "NAMain"): - self._device_name = module_info["station_name"] - else: - self._device_name = ( - f"{station['station_name']} " - f"{module_info.get('module_name', device['type'])}" - ) - - self._attr_name = f"{self._device_name} {description.name}" - self._model = device["type"] - self._netatmo_type = TYPE_WEATHER + self._attr_name = f"{description.name}" + self._model = self._module.device_type + self._config_url = CONF_URL_WEATHER self._attr_unique_id = f"{self._id}-{description.key}" - @property - def _data(self) -> pyatmo.AsyncWeatherStationData: - """Return data for this entity.""" - return cast( - pyatmo.AsyncWeatherStationData, - self.data_handler.data[self._publishers[0]["name"]], - ) + if hasattr(self._module, "place"): + place = cast( + pyatmo.modules.base_class.Place, getattr(self._module, "place") + ) + if hasattr(place, "location") and place.location is not None: + self._attr_extra_state_attributes.update( + { + ATTR_LATITUDE: place.location.latitude, + ATTR_LONGITUDE: place.location.longitude, + } + ) @property def available(self) -> bool: @@ -525,46 +463,25 @@ class NetatmoSensor(NetatmoBase, SensorEntity): @callback def async_update_callback(self) -> None: """Update the entity's state.""" - data = self._data.get_last_data(station_id=self._station_id, exclude=3600).get( - self._id - ) - - if data is None: - if self.state: - _LOGGER.debug( - "No data found for %s - %s (%s)", - self.name, - self._device_name, - self._id, - ) - self._attr_native_value = None + if ( + state := getattr(self._module, self.entity_description.netatmo_name) + ) is None: return - try: - state = data[self.entity_description.netatmo_name] - if self.entity_description.key in {"temperature", "pressure", "sum_rain_1"}: - self._attr_native_value = round(state, 1) - elif self.entity_description.key in {"windangle_value", "gustangle_value"}: - self._attr_native_value = fix_angle(state) - elif self.entity_description.key in {"windangle", "gustangle"}: - self._attr_native_value = process_angle(fix_angle(state)) - elif self.entity_description.key == "rf_status": - self._attr_native_value = process_rf(state) - elif self.entity_description.key == "wifi_status": - self._attr_native_value = process_wifi(state) - elif self.entity_description.key == "health_idx": - self._attr_native_value = process_health(state) - else: - self._attr_native_value = state - except KeyError: - if self.state: - _LOGGER.debug( - "No %s data found for %s", - self.entity_description.key, - self._device_name, - ) - self._attr_native_value = None - return + if self.entity_description.netatmo_name in { + "temperature", + "pressure", + "sum_rain_1", + }: + self._attr_native_value = round(state, 1) + elif self.entity_description.netatmo_name == "rf_strength": + self._attr_native_value = process_rf(state) + elif self.entity_description.netatmo_name == "wifi_strength": + self._attr_native_value = process_wifi(state) + elif self.entity_description.netatmo_name == "health_idx": + self._attr_native_value = process_health(state) + else: + self._attr_native_value = state self.async_write_ha_state() @@ -580,24 +497,25 @@ class NetatmoClimateBatterySensor(NetatmoBase, SensorEntity): ) -> None: """Initialize the sensor.""" super().__init__(netatmo_device.data_handler) - self.entity_description = NetatmoSensorEntityDescription( - key="battery_percent", - name="Battery Percent", - netatmo_name="battery_percent", - entity_registry_enabled_default=True, - entity_category=EntityCategory.DIAGNOSTIC, - native_unit_of_measurement=PERCENTAGE, - state_class=SensorStateClass.MEASUREMENT, - device_class=SensorDeviceClass.BATTERY, + self.entity_description = BATTERY_SENSOR_DESCRIPTION + + self._module = cast(pyatmo.modules.NRV, netatmo_device.device) + self._id = netatmo_device.parent_id + + self._publishers.extend( + [ + { + "name": HOME, + "home_id": netatmo_device.device.home.entity_id, + SIGNAL_NAME: netatmo_device.signal_name, + }, + ] ) - self._module = netatmo_device.device - self._id = netatmo_device.parent_id self._attr_name = f"{self._module.name} {self.entity_description.name}" - - self._signal_name = netatmo_device.signal_name self._room_id = self._module.room_id self._model = getattr(self._module.device_type, "value") + self._config_url = CONF_URL_ENERGY self._attr_unique_id = ( f"{self._id}-{self._module.entity_id}-{self.entity_description.key}" @@ -613,70 +531,54 @@ class NetatmoClimateBatterySensor(NetatmoBase, SensorEntity): return self._attr_available = True - self._attr_native_value = self._process_battery_state() - - def _process_battery_state(self) -> int | None: - """Construct room status.""" - if battery_state := self._module.battery_state: - return process_battery_percentage(battery_state) - - return None + self._attr_native_value = self._module.battery -def process_battery_percentage(data: str) -> int: - """Process battery data and return percent (int) for display.""" - mapping = { - "max": 100, - "full": 90, - "high": 75, - "medium": 50, - "low": 25, - "very low": 10, - } - return mapping[data] +class NetatmoSensor(NetatmoBase, SensorEntity): + """Implementation of a Netatmo sensor.""" + entity_description: NetatmoSensorEntityDescription -def fix_angle(angle: int) -> int: - """Fix angle when value is negative.""" - if angle < 0: - return 360 + angle - return angle + def __init__( + self, + netatmo_device: NetatmoDevice, + description: NetatmoSensorEntityDescription, + ) -> None: + """Initialize the sensor.""" + super().__init__(netatmo_device.data_handler) + self.entity_description = description + self._module = netatmo_device.device + self._id = self._module.entity_id -def process_angle(angle: int) -> str: - """Process angle and return string for display.""" - if angle >= 330: - return "N" - if angle >= 300: - return "NW" - if angle >= 240: - return "W" - if angle >= 210: - return "SW" - if angle >= 150: - return "S" - if angle >= 120: - return "SE" - if angle >= 60: - return "E" - if angle >= 30: - return "NE" - return "N" + self._publishers.extend( + [ + { + "name": HOME, + "home_id": netatmo_device.device.home.entity_id, + SIGNAL_NAME: netatmo_device.signal_name, + }, + ] + ) + self._attr_name = f"{self._module.name} {self.entity_description.name}" + self._room_id = self._module.room_id + self._model = getattr(self._module.device_type, "value") + self._config_url = CONF_URL_ENERGY -def process_battery(data: int, model: str) -> str: - """Process battery data and return string for display.""" - battery_data = BATTERY_VALUES[model] + self._attr_unique_id = ( + f"{self._id}-{self._module.entity_id}-{self.entity_description.key}" + ) - if data >= battery_data.full: - return "Full" - if data >= battery_data.high: - return "High" - if data >= battery_data.medium: - return "Medium" - if data >= battery_data.low: - return "Low" - return "Very Low" + @callback + def async_update_callback(self) -> None: + """Update the entity's state.""" + if (state := getattr(self._module, self.entity_description.key)) is None: + return + + self._attr_native_value = state + + self.async_write_ha_state() def process_health(health: int) -> str: @@ -714,9 +616,57 @@ def process_wifi(strength: int) -> str: return "Full" +class NetatmoRoomSensor(NetatmoBase, SensorEntity): + """Implementation of a Netatmo room sensor.""" + + entity_description: NetatmoSensorEntityDescription + + def __init__( + self, + netatmo_room: NetatmoRoom, + description: NetatmoSensorEntityDescription, + ) -> None: + """Initialize the sensor.""" + super().__init__(netatmo_room.data_handler) + self.entity_description = description + + self._room = netatmo_room.room + self._id = self._room.entity_id + + self._publishers.extend( + [ + { + "name": HOME, + "home_id": netatmo_room.room.home.entity_id, + SIGNAL_NAME: netatmo_room.signal_name, + }, + ] + ) + + self._attr_name = f"{self._room.name} {self.entity_description.name}" + self._room_id = self._room.entity_id + self._model = f"{self._room.climate_type}" + self._config_url = CONF_URL_ENERGY + + self._attr_unique_id = ( + f"{self._id}-{self._room.entity_id}-{self.entity_description.key}" + ) + + @callback + def async_update_callback(self) -> None: + """Update the entity's state.""" + if (state := getattr(self._room, self.entity_description.key)) is None: + return + + self._attr_native_value = state + + self.async_write_ha_state() + + class NetatmoPublicSensor(NetatmoBase, SensorEntity): """Represent a single sensor in a Netatmo.""" + _attr_has_entity_name = True entity_description: NetatmoSensorEntityDescription def __init__( @@ -729,11 +679,10 @@ class NetatmoPublicSensor(NetatmoBase, SensorEntity): super().__init__(data_handler) self.entity_description = description - self._signal_name = f"{PUBLICDATA_DATA_CLASS_NAME}-{area.uuid}" - + self._signal_name = f"{PUBLIC}-{area.uuid}" self._publishers.append( { - "name": PUBLICDATA_DATA_CLASS_NAME, + "name": PUBLIC, "lat_ne": area.lat_ne, "lon_ne": area.lon_ne, "lat_sw": area.lat_sw, @@ -743,12 +692,14 @@ class NetatmoPublicSensor(NetatmoBase, SensorEntity): } ) + self._station = data_handler.account.public_weather_areas[str(area.uuid)] + self.area = area self._mode = area.mode self._area_name = area.area_name self._id = self._area_name self._device_name = f"{self._area_name}" - self._attr_name = f"{self._device_name} {description.name}" + self._attr_name = f"{description.name}" self._show_on_map = area.show_on_map self._attr_unique_id = ( f"{self._device_name.replace(' ', '-')}-{description.key}" @@ -762,17 +713,12 @@ class NetatmoPublicSensor(NetatmoBase, SensorEntity): } ) - @property - def _data(self) -> pyatmo.AsyncPublicData: - """Return data for this entity.""" - return cast(pyatmo.AsyncPublicData, self.data_handler.data[self._signal_name]) - async def async_added_to_hass(self) -> None: """Entity created.""" await super().async_added_to_hass() assert self.device_info and "name" in self.device_info - self.data_handler.config_entry.async_on_unload( + self.async_on_remove( async_dispatcher_connect( self.hass, f"netatmo-config-{self.device_info['name']}", @@ -790,22 +736,11 @@ class NetatmoPublicSensor(NetatmoBase, SensorEntity): ) self.area = area - self._signal_name = f"{PUBLICDATA_DATA_CLASS_NAME}-{area.uuid}" - self._publishers = [ - { - "name": PUBLICDATA_DATA_CLASS_NAME, - "lat_ne": area.lat_ne, - "lon_ne": area.lon_ne, - "lat_sw": area.lat_sw, - "lon_sw": area.lon_sw, - "area_name": area.area_name, - SIGNAL_NAME: self._signal_name, - } - ] + self._signal_name = f"{PUBLIC}-{area.uuid}" self._mode = area.mode self._show_on_map = area.show_on_map await self.data_handler.subscribe( - PUBLICDATA_DATA_CLASS_NAME, + PUBLIC, self._signal_name, self.async_update_callback, lat_ne=area.lat_ne, @@ -819,22 +754,26 @@ class NetatmoPublicSensor(NetatmoBase, SensorEntity): """Update the entity's state.""" data = None - if self.entity_description.key == "temperature": - data = self._data.get_latest_temperatures() - elif self.entity_description.key == "pressure": - data = self._data.get_latest_pressures() - elif self.entity_description.key == "humidity": - data = self._data.get_latest_humidities() - elif self.entity_description.key == "rain": - data = self._data.get_latest_rain() - elif self.entity_description.key == "sum_rain_1": - data = self._data.get_60_min_rain() - elif self.entity_description.key == "sum_rain_24": - data = self._data.get_24_h_rain() - elif self.entity_description.key == "windstrength": - data = self._data.get_latest_wind_strengths() - elif self.entity_description.key == "guststrength": - data = self._data.get_latest_gust_strengths() + if self.entity_description.netatmo_name == "temperature": + data = self._station.get_latest_temperatures() + elif self.entity_description.netatmo_name == "pressure": + data = self._station.get_latest_pressures() + elif self.entity_description.netatmo_name == "humidity": + data = self._station.get_latest_humidities() + elif self.entity_description.netatmo_name == "rain": + data = self._station.get_latest_rain() + elif self.entity_description.netatmo_name == "sum_rain_1": + data = self._station.get_60_min_rain() + elif self.entity_description.netatmo_name == "sum_rain_24": + data = self._station.get_24_h_rain() + elif self.entity_description.netatmo_name == "wind_strength": + data = self._station.get_latest_wind_strengths() + elif self.entity_description.netatmo_name == "gust_strength": + data = self._station.get_latest_gust_strengths() + elif self.entity_description.netatmo_name == "wind_angle": + data = self._station.get_latest_wind_angles() + elif self.entity_description.netatmo_name == "gust_angle": + data = self._station.get_latest_gust_angles() if not data: if self.available: diff --git a/homeassistant/components/netatmo/switch.py b/homeassistant/components/netatmo/switch.py new file mode 100644 index 00000000000..338d073c205 --- /dev/null +++ b/homeassistant/components/netatmo/switch.py @@ -0,0 +1,83 @@ +"""Support for Netatmo/BTicino/Legrande switches.""" +from __future__ import annotations + +import logging +from typing import Any, cast + +from pyatmo import modules as NaModules + +from homeassistant.components.switch import SwitchEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import CONF_URL_CONTROL, NETATMO_CREATE_SWITCH +from .data_handler import HOME, SIGNAL_NAME, NetatmoDevice +from .netatmo_entity_base import NetatmoBase + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Netatmo switch platform.""" + + @callback + def _create_entity(netatmo_device: NetatmoDevice) -> None: + entity = NetatmoSwitch(netatmo_device) + _LOGGER.debug("Adding switch %s", entity) + async_add_entities([entity]) + + entry.async_on_unload( + async_dispatcher_connect(hass, NETATMO_CREATE_SWITCH, _create_entity) + ) + + +class NetatmoSwitch(NetatmoBase, SwitchEntity): + """Representation of a Netatmo switch device.""" + + def __init__( + self, + netatmo_device: NetatmoDevice, + ) -> None: + """Initialize the Netatmo device.""" + super().__init__(netatmo_device.data_handler) + + self._switch = cast(NaModules.Switch, netatmo_device.device) + + self._id = self._switch.entity_id + self._attr_name = self._device_name = self._switch.name + self._model = self._switch.device_type + self._config_url = CONF_URL_CONTROL + + self._home_id = self._switch.home.entity_id + + self._signal_name = f"{HOME}-{self._home_id}" + self._publishers.extend( + [ + { + "name": HOME, + "home_id": self._home_id, + SIGNAL_NAME: self._signal_name, + }, + ] + ) + self._attr_unique_id = f"{self._id}-{self._model}" + self._attr_is_on = self._switch.on + + @callback + def async_update_callback(self) -> None: + """Update the entity's state.""" + self._attr_is_on = self._switch.on + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the zone on.""" + await self._switch.async_on() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the zone off.""" + await self._switch.async_off() diff --git a/requirements_all.txt b/requirements_all.txt index 8805209e6f4..2f811e4d286 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1436,7 +1436,7 @@ pyalmond==0.0.2 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==6.2.4 +pyatmo==7.0.1 # homeassistant.components.atome pyatome==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1ce12de4a86..6a2182faf87 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1018,7 +1018,7 @@ pyalmond==0.0.2 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==6.2.4 +pyatmo==7.0.1 # homeassistant.components.apple_tv pyatv==0.10.3 diff --git a/tests/components/netatmo/common.py b/tests/components/netatmo/common.py index 784b428e8d0..375dce4e723 100644 --- a/tests/components/netatmo/common.py +++ b/tests/components/netatmo/common.py @@ -11,19 +11,6 @@ from tests.test_util.aiohttp import AiohttpClientMockResponse CLIENT_ID = "1234" CLIENT_SECRET = "5678" -ALL_SCOPES = [ - "read_station", - "read_camera", - "access_camera", - "write_camera", - "read_presence", - "access_presence", - "write_presence", - "read_homecoach", - "read_smokedetector", - "read_thermostat", - "write_thermostat", -] COMMON_RESPONSE = { "user_id": "91763b24c43d3e344f424e8d", @@ -43,10 +30,10 @@ DEFAULT_PLATFORMS = ["camera", "climate", "light", "sensor"] async def fake_post_request(*args, **kwargs): """Return fake data.""" - if "url" not in kwargs: + if "endpoint" not in kwargs: return "{}" - endpoint = kwargs["url"].split("/")[-1] + endpoint = kwargs["endpoint"].split("/")[-1] if endpoint in "snapshot_720.jpg": return b"test stream image bytes" @@ -59,7 +46,7 @@ async def fake_post_request(*args, **kwargs): "setthermmode", "switchhomeschedule", ]: - payload = f'{{"{endpoint}": true}}' + payload = {f"{endpoint}": True, "status": "ok"} elif endpoint == "homestatus": home_id = kwargs.get("params", {}).get("home_id") @@ -70,17 +57,17 @@ async def fake_post_request(*args, **kwargs): return AiohttpClientMockResponse( method="POST", - url=kwargs["url"], + url=kwargs["endpoint"], json=payload, ) async def fake_get_image(*args, **kwargs): """Return fake data.""" - if "url" not in kwargs: + if "endpoint" not in kwargs: return "{}" - endpoint = kwargs["url"].split("/")[-1] + endpoint = kwargs["endpoint"].split("/")[-1] if endpoint in "snapshot_720.jpg": return b"test stream image bytes" diff --git a/tests/components/netatmo/conftest.py b/tests/components/netatmo/conftest.py index 808e477e053..a10030fab08 100644 --- a/tests/components/netatmo/conftest.py +++ b/tests/components/netatmo/conftest.py @@ -2,9 +2,10 @@ from time import time from unittest.mock import AsyncMock, patch +from pyatmo.const import ALL_SCOPES import pytest -from .common import ALL_SCOPES, fake_get_image, fake_post_request +from .common import fake_get_image, fake_post_request from tests.common import MockConfigEntry @@ -60,6 +61,7 @@ def netatmo_auth(): "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth" ) as mock_auth: mock_auth.return_value.async_post_request.side_effect = fake_post_request + mock_auth.return_value.async_post_api_request.side_effect = fake_post_request mock_auth.return_value.async_get_image.side_effect = fake_get_image mock_auth.return_value.async_addwebhook.side_effect = AsyncMock() mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock() diff --git a/tests/components/netatmo/fixtures/events.txt b/tests/components/netatmo/fixtures/events.txt index f2bc29f782c..112abb7115a 100644 --- a/tests/components/netatmo/fixtures/events.txt +++ b/tests/components/netatmo/fixtures/events.txt @@ -1,61 +1,50 @@ { - "12:34:56:78:90:ab": { - 1599152672: { - "id": "12345", - "type": "person", - "time": 1599152672, - "camera_id": "12:34:56:78:90:ab", - "snapshot": { - "url": "https://netatmocameraimage", - }, - "video_id": "98765", - "video_status": "available", - "message": "Paulus seen", - "media_url": "http:///files/high/index.m3u8", - }, - 1599152673: { - "id": "12346", - "type": "person", - "time": 1599152673, - "camera_id": "12:34:56:78:90:ab", - "snapshot": { - "url": "https://netatmocameraimage", - }, - "message": "Tobias seen", - }, - 1599152674: { - "id": "12347", - "type": "outdoor", - "time": 1599152674, - "camera_id": "12:34:56:78:90:ac", - "snapshot": { - "url": "https://netatmocameraimage", - }, - "video_id": "98766", - "video_status": "available", - "event_list": [ - { - "type": "vehicle", - "time": 1599152674, - "id": "12347-0", - "offset": 0, - "message": "Vehicle detected", - "snapshot": { - "url": "https://netatmocameraimage", - }, - }, - { - "type": "human", - "time": 1599152674, - "id": "12347-1", - "offset": 8, - "message": "Person detected", - "snapshot": { - "url": "https://netatmocameraimage", - }, - }, - ], - "media_url": "http:///files/high/index.m3u8", - }, + "12:34:56:78:90:ab": { + 1654191519: { + "home_id": "91763b24c43d3e344f424e8b", + "entity_id": "000001", + "event_type": "human", + "event_time": 1654191519, + "module_id": "12:34:56:78:90:ab", + "snapshot": { + "url": "https://netatmocameraimage" + }, + "vignette": { + "url": "https://netatmocameraimage" + }, + "video_id": "0011", + "video_status": "available", + "message": "Bewegung erkannt", + "subevents": [], + "media_url": "http:///files/high/index.m3u8" + }, + 1654189491: { + "home_id": "91763b24c43d3e344f424e8b", + "entity_id": "000002", + "event_type": "person", + "event_time": 1654189491, + "module_id": "12:34:56:78:90:ab", + "snapshot": { + "url": "https://netatmocameraimage" + }, + "video_id": "0012", + "video_status": "available", + "message": "Jane gesehen", + "person_id": "1111", + "out_of_sight": False, + "subevents": [], + "media_url": "http:///files/high/index.m3u8" + }, + 1654289891: { + "home_id": "91763b24c43d3e344f424e8b", + "entity_id": "000002", + "event_type": "person", + "event_time": 1654289891, + "module_id": "12:34:56:78:90:ab", + "message": "Jane gesehen", + "person_id": "1111", + "out_of_sight": False, + "subevents": [] } -} \ No newline at end of file + } +} diff --git a/tests/components/netatmo/fixtures/getevents.json b/tests/components/netatmo/fixtures/getevents.json new file mode 100644 index 00000000000..7db4dbe5e9b --- /dev/null +++ b/tests/components/netatmo/fixtures/getevents.json @@ -0,0 +1,151 @@ +{ + "body": { + "home": { + "id": "91763b24c43d3e344f424e8b", + "events": [ + { + "id": "11111111111111111f7763a6d", + "type": "outdoor", + "time": 1645794709, + "module_id": "12:34:56:00:a5:a4", + "video_id": "11111111-2222-3333-4444-b42f0fc4cfad", + "video_status": "available", + "subevents": [ + { + "id": "11111111-2222-3333-4444-013560107fce", + "type": "human", + "time": 1645794709, + "verified": true, + "offset": 0, + "snapshot": { + "url": "https://netatmocameraimage.blob.core.windows.net/production/000000a722374" + }, + "vignette": { + "url": "https://netatmocameraimage.blob.core.windows.net/production/0000009625c0f" + }, + "message": "Person erfasst" + }, + { + "id": "11111111-2222-3333-4444-0b0bc962df43", + "type": "vehicle", + "time": 1645794716, + "verified": true, + "offset": 15, + "snapshot": { + "url": "https://netatmocameraimage.blob.core.windows.net/production/00000033f9f96" + }, + "vignette": { + "url": "https://netatmocameraimage.blob.core.windows.net/production/000000cba08af" + }, + "message": "Fahrzeug erfasst" + }, + { + "id": "11111111-2222-3333-4444-129e72195968", + "type": "human", + "time": 1645794716, + "verified": true, + "offset": 15, + "snapshot": { + "filename": "vod/11111/events/22222/snapshot_129e72195968.jpg" + }, + "vignette": { + "filename": "vod/11111/events/22222/vignette_129e72195968.jpg" + }, + "message": "Person erfasst" + }, + { + "id": "11111111-2222-3333-4444-dae4d7e4f24e", + "type": "human", + "time": 1645794718, + "verified": true, + "offset": 17, + "snapshot": { + "filename": "vod/11111/events/22222/snapshot_dae4d7e4f24e.jpg" + }, + "vignette": { + "filename": "vod/11111/events/22222/vignette_dae4d7e4f24e.jpg" + }, + "message": "Person erfasst" + } + ] + }, + { + "id": "1111111111111111e7e40c353", + "type": "connection", + "time": 1645784799, + "module_id": "12:34:56:00:a5:a4", + "message": "Front verbunden" + }, + { + "id": "11111111111111144e3115860", + "type": "boot", + "time": 1645784775, + "module_id": "12:34:56:00:a5:a4", + "message": "Front gestartet" + }, + { + "id": "11111111111111169804049ca", + "type": "disconnection", + "time": 1645773806, + "module_id": "12:34:56:00:a5:a4", + "message": "Front getrennt" + }, + { + "id": "1111111111111117cb8147ffd", + "type": "outdoor", + "time": 1645712826, + "module_id": "12:34:56:00:a5:a4", + "video_id": "11111111-2222-3333-4444-5091e1903f8d", + "video_status": "available", + "subevents": [ + { + "id": "11111111-2222-3333-4444-b7d28e3ccc38", + "type": "human", + "time": 1645712826, + "verified": true, + "offset": 0, + "snapshot": { + "url": "https://netatmocameraimage.blob.core.windows.net/production/000000a0ca642" + }, + "vignette": { + "url": "https://netatmocameraimage.blob.core.windows.net/production/00000031b0ed4" + }, + "message": "Person erfasst" + } + ] + }, + { + "id": "1111111111111119df3d2de6", + "type": "person_home", + "time": 1645902000, + "module_id": "12:34:56:00:f1:62", + "message": "Home Assistant Cloud definiert John Doe und Jane Doe als \"Zu Hause\"" + }, + { + "id": "1111111111111112c91b3628", + "type": "person", + "time": 1645901266, + "module_id": "12:34:56:00:f1:62", + "snapshot": { + "url": "https://netatmocameraimage.blob.core.windows.net/production/0000081d4f42875d9" + }, + "video_id": "11111111-2222-3333-4444-314d161525db", + "video_status": "available", + "message": "John Doe gesehen", + "person_id": "91827374-7e04-5298-83ad-a0cb8372dff1", + "out_of_sight": false + }, + { + "id": "1111111111111115166b1283", + "type": "tag_open", + "time": 1645897638, + "module_id": "12:34:56:00:86:99", + "message": "Window Hall: immer noch offen" + } + ] + } + }, + "status": "ok", + "time_exec": 0.24369096755981445, + "time_server": 1645897231 +} diff --git a/tests/components/netatmo/fixtures/homesdata.json b/tests/components/netatmo/fixtures/homesdata.json index a56ccb236b5..93c04388f4c 100644 --- a/tests/components/netatmo/fixtures/homesdata.json +++ b/tests/components/netatmo/fixtures/homesdata.json @@ -19,7 +19,13 @@ "id": "3688132631", "name": "Hall", "type": "custom", - "module_ids": ["12:34:56:00:f1:62"] + "module_ids": [ + "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" + ] }, { "id": "2833524037", @@ -32,6 +38,44 @@ "name": "Cocina", "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", + "type": "electrical_cabinet", + "module_ids": ["12:34:56:20:f5:44", "12:34:56:20:f5:8c"], + "modules": ["12:34:56:20:f5:44", "12:34:56:20:f5:8c"], + "therm_relay": "12:34:56:20:f5:44", + "true_temperature_available": true + }, + { + "id": "100007519", + "name": "Cabinet", + "type": "electrical_cabinet", + "module_ids": [ + "12:34:56:00:16:0e", + "12:34:56:00:16:0e#0", + "12:34:56:00:16:0e#1", + "12:34:56:00:16:0e#2", + "12:34:56:00:16:0e#3", + "12:34:56:00:16:0e#4", + "12:34:56:00:16:0e#5", + "12:34:56:00:16:0e#6", + "12:34:56:00:16:0e#7", + "12:34:56:00:16:0e#8" + ] + }, + { + "id": "1002003001", + "name": "Corridor", + "type": "corridor", + "module_ids": ["10:20:30:bd:b8:1e"] } ], "modules": [ @@ -75,7 +119,334 @@ "type": "NACamera", "name": "Hall", "setup_date": 1544828430, - "room_id": "3688132631" + "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", + "type": "NOC", + "name": "Garden", + "setup_date": 1544828430, + "reachable": true + }, + { + "id": "12:34:56:20:f5:44", + "type": "OTH", + "name": "Modulating Relay", + "setup_date": 1607443936, + "room_id": "222452125", + "reachable": true, + "modules_bridged": ["12:34:56:20:f5:8c"], + "hk_device_id": "12:34:56:20:d0:c5", + "capabilities": [ + { + "name": "automatism", + "available": true + } + ], + "max_modules_nb": 21 + }, + { + "id": "12:34:56:20:f5:8c", + "type": "OTM", + "name": "Bureau Modulate", + "setup_date": 1607443939, + "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", + "name": "module iDiamant", + "setup_date": 1562262465, + "room_id": "222452125", + "modules_bridged": ["0009999992"] + }, + { + "id": "0009999992", + "type": "NBR", + "name": "Entrance Blinds", + "setup_date": 1578551339, + "room_id": "3688132631", + "bridge": "12:34:56:30:d5:d4" + }, + { + "id": "12:34:56:37:11:ca", + "type": "NAMain", + "name": "NetatmoIndoor", + "setup_date": 1419453350, + "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" + ], + "customer_id": "C00016", + "hardware_version": 251, + "public_ext_data": false, + "public_ext_counter": 0, + "alarm_config": { + "default_alarm": [ + { + "db_alarm_number": 0 + }, + { + "db_alarm_number": 1 + }, + { + "db_alarm_number": 2 + }, + { + "db_alarm_number": 6 + }, + { + "db_alarm_number": 4 + }, + { + "db_alarm_number": 5 + }, + { + "db_alarm_number": 7 + }, + { + "db_alarm_number": 22 + } + ], + "personnalized": [ + { + "threshold": 20, + "data_type": 1, + "direction": 0, + "db_alarm_number": 8 + }, + { + "threshold": 17, + "data_type": 1, + "direction": 1, + "db_alarm_number": 9 + }, + { + "threshold": 65, + "data_type": 4, + "direction": 0, + "db_alarm_number": 16 + }, + { + "threshold": 19, + "data_type": 8, + "direction": 0, + "db_alarm_number": 22 + } + ] + }, + "module_offset": { + "12:34:56:80:bb:26": { + "a": 0.1 + } + } + }, + { + "id": "12:34:56:36:fc:de", + "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" + }, + { + "id": "12:34:56:05:51:20", + "type": "NAModule3", + "name": "Rain", + "setup_date": 1591770206, + "bridge": "12:34:56:37:11:ca" + }, + { + "id": "12:34:56:07:bb:3e", + "type": "NAModule4", + "name": "Bedroom", + "setup_date": 1484997703, + "bridge": "12:34:56:37:11:ca" + }, + { + "id": "12:34:56:26:68:92", + "type": "NHC", + "name": "Indoor", + "setup_date": 1571342643 + }, + { + "id": "12:34:56:26:cc:01", + "type": "BNS", + "name": "Child", + "setup_date": 1571634243 + }, + { + "id": "12:34:56:80:60:40", + "type": "NLG", + "name": "Prise Control", + "setup_date": 1641841257, + "room_id": "1310352496", + "modules_bridged": [ + "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" + ] + }, + { + "id": "12:34:56:80:00:12:ac:f2", + "type": "NLP", + "name": "Prise", + "setup_date": 1641841262, + "room_id": "1310352496", + "bridge": "12:34:56:80:60:40" + }, + { + "id": "12:34:56:80:00:c3:69:3c", + "type": "NLT", + "name": "Commande sans fil", + "setup_date": 1641841262, + "bridge": "12:34:56:80:60:40" + }, + { + "id": "12:34:56:00:16:0e", + "type": "NLE", + "name": "Écocompteur", + "setup_date": 1644496884, + "room_id": "100007519", + "modules_bridged": [ + "12:34:56:00:16:0e#0", + "12:34:56:00:16:0e#1", + "12:34:56:00:16:0e#2", + "12:34:56:00:16:0e#3", + "12:34:56:00:16:0e#4", + "12:34:56:00:16:0e#5", + "12:34:56:00:16:0e#6", + "12:34:56:00:16:0e#7", + "12:34:56:00:16:0e#8" + ] + }, + { + "id": "12:34:56:00:16:0e#0", + "type": "NLE", + "name": "Line 1", + "setup_date": 1644496886, + "room_id": "100007519", + "bridge": "12:34:56:00:16:0e" + }, + { + "id": "12:34:56:00:16:0e#1", + "type": "NLE", + "name": "Line 2", + "setup_date": 1644496886, + "room_id": "100007519", + "bridge": "12:34:56:00:16:0e" + }, + { + "id": "12:34:56:00:16:0e#2", + "type": "NLE", + "name": "Line 3", + "setup_date": 1644496886, + "room_id": "100007519", + "bridge": "12:34:56:00:16:0e" + }, + { + "id": "12:34:56:00:16:0e#3", + "type": "NLE", + "name": "Line 4", + "setup_date": 1644496886, + "room_id": "100007519", + "bridge": "12:34:56:00:16:0e" + }, + { + "id": "12:34:56:00:16:0e#4", + "type": "NLE", + "name": "Line 5", + "setup_date": 1644496886, + "room_id": "100007519", + "bridge": "12:34:56:00:16:0e" + }, + { + "id": "12:34:56:00:16:0e#5", + "type": "NLE", + "name": "Total", + "setup_date": 1644496886, + "room_id": "100007519", + "bridge": "12:34:56:00:16:0e" + }, + { + "id": "12:34:56:00:16:0e#6", + "type": "NLE", + "name": "Gas", + "setup_date": 1644496886, + "room_id": "100007519", + "bridge": "12:34:56:00:16:0e" + }, + { + "id": "12:34:56:00:16:0e#7", + "type": "NLE", + "name": "Hot water", + "setup_date": 1644496886, + "room_id": "100007519", + "bridge": "12:34:56:00:16:0e" + }, + { + "id": "12:34:56:00:16:0e#8", + "type": "NLE", + "name": "Cold water", + "setup_date": 1644496886, + "room_id": "100007519", + "bridge": "12:34:56:00:16:0e" + }, + { + "id": "12:34:56:00:00:a1:4c:da", + "type": "NLPC", + "name": "Consumption meter", + "setup_date": 1638376602, + "room_id": "100008999", + "bridge": "12:34:56:80:60:40" + }, + { + "id": "12:34:56:00:01:01:01:a1", + "type": "NLFN", + "name": "Bathroom light", + "setup_date": 1598367404, + "room_id": "1002003001", + "bridge": "12:34:56:80:60:40" } ], "schedules": [ diff --git a/tests/components/netatmo/fixtures/homestatus_91763b24c43d3e344f424e8b.json b/tests/components/netatmo/fixtures/homestatus_91763b24c43d3e344f424e8b.json index 12c7044aaaa..4cd5dceec3b 100644 --- a/tests/components/netatmo/fixtures/homestatus_91763b24c43d3e344f424e8b.json +++ b/tests/components/netatmo/fixtures/homestatus_91763b24c43d3e344f424e8b.json @@ -14,6 +14,25 @@ "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", @@ -50,6 +69,805 @@ "rf_strength": 59, "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": { + "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, + "bridge": "12:34:56:30:d5:d4" + }, + { + "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" + }, + { + "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" + }, + { + "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": [ @@ -85,6 +903,19 @@ "therm_setpoint_end_time": 0, "anticipating": false, "open_window": false + }, + { + "id": "2940411588", + "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, + "therm_setpoint_end_time": null, + "therm_setpoint_mode": "home" } ], "id": "91763b24c43d3e344f424e8b", diff --git a/tests/components/netatmo/test_camera.py b/tests/components/netatmo/test_camera.py index 5b01668925f..ea39497ce58 100644 --- a/tests/components/netatmo/test_camera.py +++ b/tests/components/netatmo/test_camera.py @@ -93,26 +93,36 @@ async def test_setup_component_with_webhook(hass, config_entry, netatmo_auth): assert hass.states.get(camera_entity_indoor).state == "streaming" assert hass.states.get(camera_entity_outdoor).attributes["light_state"] == "auto" - with patch("pyatmo.camera.AsyncCameraData.async_set_state") as mock_set_state: + with patch("pyatmo.home.Home.async_set_state") as mock_set_state: await hass.services.async_call( "camera", "turn_off", service_data={"entity_id": "camera.hall"} ) await hass.async_block_till_done() mock_set_state.assert_called_once_with( - home_id="91763b24c43d3e344f424e8b", - camera_id="12:34:56:00:f1:62", - monitoring="off", + { + "modules": [ + { + "id": "12:34:56:00:f1:62", + "monitoring": "off", + } + ] + } ) - with patch("pyatmo.camera.AsyncCameraData.async_set_state") as mock_set_state: + with patch("pyatmo.home.Home.async_set_state") as mock_set_state: await hass.services.async_call( "camera", "turn_on", service_data={"entity_id": "camera.hall"} ) await hass.async_block_till_done() mock_set_state.assert_called_once_with( - home_id="91763b24c43d3e344f424e8b", - camera_id="12:34:56:00:f1:62", - monitoring="on", + { + "modules": [ + { + "id": "12:34:56:00:f1:62", + "monitoring": "on", + } + ] + } ) @@ -135,15 +145,13 @@ async def test_camera_image_local(hass, config_entry, requests_mock, netatmo_aut assert cam is not None assert cam.state == STATE_STREAMING + assert cam.name == "Hall" stream_source = await camera.async_get_stream_source(hass, camera_entity_indoor) assert stream_source == stream_uri - requests_mock.get( - uri + "/live/snapshot_720.jpg", - content=IMAGE_BYTES_FROM_STREAM, - ) image = await camera.async_get_image(hass, camera_entity_indoor) + assert image.content == IMAGE_BYTES_FROM_STREAM @@ -156,10 +164,7 @@ async def test_camera_image_vpn(hass, config_entry, requests_mock, netatmo_auth) await hass.async_block_till_done() - uri = ( - "https://prodvpn-eu-2.netatmo.net/restricted/10.255.248.91/" - "6d278460699e56180d47ab47169efb31/MpEylTU2MDYzNjRVD-LJxUnIndumKzLboeAwMDqTTw,," - ) + 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" cam = hass.states.get(camera_entity_indoor) @@ -170,10 +175,6 @@ async def test_camera_image_vpn(hass, config_entry, requests_mock, netatmo_auth) stream_source = await camera.async_get_stream_source(hass, camera_entity_indoor) assert stream_source == stream_uri - requests_mock.get( - uri + "/live/snapshot_720.jpg", - content=IMAGE_BYTES_FROM_STREAM, - ) image = await camera.async_get_image(hass, camera_entity_indoor) assert image.content == IMAGE_BYTES_FROM_STREAM @@ -192,32 +193,26 @@ async def test_service_set_person_away(hass, config_entry, netatmo_auth): "person": "Richard Doe", } - with patch( - "pyatmo.camera.AsyncCameraData.async_set_persons_away" - ) as mock_set_persons_away: + with patch("pyatmo.home.Home.async_set_persons_away") as mock_set_persons_away: await hass.services.async_call( "netatmo", SERVICE_SET_PERSON_AWAY, service_data=data ) await hass.async_block_till_done() mock_set_persons_away.assert_called_once_with( person_id="91827376-7e04-5298-83af-a0cb8372dff3", - home_id="91763b24c43d3e344f424e8b", ) data = { "entity_id": "camera.hall", } - with patch( - "pyatmo.camera.AsyncCameraData.async_set_persons_away" - ) as mock_set_persons_away: + with patch("pyatmo.home.Home.async_set_persons_away") as mock_set_persons_away: await hass.services.async_call( "netatmo", SERVICE_SET_PERSON_AWAY, service_data=data ) await hass.async_block_till_done() mock_set_persons_away.assert_called_once_with( person_id=None, - home_id="91763b24c43d3e344f424e8b", ) @@ -289,16 +284,13 @@ async def test_service_set_persons_home(hass, config_entry, netatmo_auth): "persons": "John Doe", } - with patch( - "pyatmo.camera.AsyncCameraData.async_set_persons_home" - ) as mock_set_persons_home: + with patch("pyatmo.home.Home.async_set_persons_home") as mock_set_persons_home: await hass.services.async_call( "netatmo", SERVICE_SET_PERSONS_HOME, service_data=data ) await hass.async_block_till_done() mock_set_persons_home.assert_called_once_with( person_ids=["91827374-7e04-5298-83ad-a0cb8372dff1"], - home_id="91763b24c43d3e344f424e8b", ) @@ -316,16 +308,49 @@ async def test_service_set_camera_light(hass, config_entry, netatmo_auth): "camera_light_mode": "on", } - with patch("pyatmo.camera.AsyncCameraData.async_set_state") as mock_set_state: + expected_data = { + "modules": [ + { + "id": "12:34:56:00:a5:a4", + "floodlight": "on", + }, + ], + } + with patch("pyatmo.home.Home.async_set_state") as mock_set_state: await hass.services.async_call( "netatmo", SERVICE_SET_CAMERA_LIGHT, service_data=data ) await hass.async_block_till_done() - mock_set_state.assert_called_once_with( - home_id="91763b24c43d3e344f424e8b", - camera_id="12:34:56:00:a5:a4", - floodlight="on", + mock_set_state.assert_called_once_with(expected_data) + + +async def test_service_set_camera_light_invalid_type(hass, config_entry, netatmo_auth): + """Test service to set the indoor camera light mode.""" + with selected_platforms(["camera"]): + await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + + await hass.async_block_till_done() + + data = { + "entity_id": "camera.hall", + "camera_light_mode": "on", + } + + with patch("pyatmo.home.Home.async_set_state") as mock_set_state, pytest.raises( + HomeAssistantError + ) as excinfo: + await hass.services.async_call( + "netatmo", + SERVICE_SET_CAMERA_LIGHT, + service_data=data, + blocking=True, ) + await hass.async_block_till_done() + + mock_set_state.assert_not_called() + assert excinfo.value.args == ("NACamera does not have a floodlight",) @pytest.mark.skip @@ -342,13 +367,13 @@ async def test_camera_reconnect_webhook(hass, config_entry): with patch( "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth" ) as mock_auth, patch( - "homeassistant.components.netatmo.PLATFORMS", ["camera"] + "homeassistant.components.netatmo.data_handler.PLATFORMS", ["camera"] ), patch( "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", ), patch( "homeassistant.components.netatmo.webhook_generate_url" ) as mock_webhook: - mock_auth.return_value.async_post_request.side_effect = fake_post + mock_auth.return_value.async_post_api_request.side_effect = fake_post mock_auth.return_value.async_addwebhook.side_effect = AsyncMock() mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock() mock_webhook.return_value = "https://example.com" @@ -429,44 +454,6 @@ async def test_setup_component_no_devices(hass, config_entry): """Fake error during requesting backend data.""" nonlocal fake_post_hits fake_post_hits += 1 - return "{}" - - with patch( - "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth" - ) as mock_auth, patch( - "homeassistant.components.netatmo.PLATFORMS", ["camera"] - ), patch( - "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", - ), patch( - "homeassistant.components.netatmo.webhook_generate_url" - ): - mock_auth.return_value.async_post_request.side_effect = fake_post_no_data - mock_auth.return_value.async_addwebhook.side_effect = AsyncMock() - mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock() - - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - assert fake_post_hits == 4 - - -async def test_camera_image_raises_exception(hass, config_entry, requests_mock): - """Test setup with no devices.""" - fake_post_hits = 0 - - async def fake_post(*args, **kwargs): - """Return fake data.""" - nonlocal fake_post_hits - fake_post_hits += 1 - - if "url" not in kwargs: - return "{}" - - endpoint = kwargs["url"].split("/")[-1] - - if "snapshot_720.jpg" in endpoint: - raise pyatmo.exceptions.ApiError() - return await fake_post_request(*args, **kwargs) with patch( @@ -478,7 +465,45 @@ async def test_camera_image_raises_exception(hass, config_entry, requests_mock): ), patch( "homeassistant.components.netatmo.webhook_generate_url" ): - mock_auth.return_value.async_post_request.side_effect = fake_post + mock_auth.return_value.async_post_api_request.side_effect = fake_post_no_data + mock_auth.return_value.async_addwebhook.side_effect = AsyncMock() + mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock() + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert fake_post_hits == 9 + + +async def test_camera_image_raises_exception(hass, config_entry, requests_mock): + """Test setup with no devices.""" + fake_post_hits = 0 + + async def fake_post(*args, **kwargs): + """Return fake data.""" + nonlocal fake_post_hits + fake_post_hits += 1 + + if "endpoint" not in kwargs: + return "{}" + + endpoint = kwargs["endpoint"].split("/")[-1] + + if "snapshot_720.jpg" in endpoint: + raise pyatmo.ApiError() + + return await fake_post_request(*args, **kwargs) + + with patch( + "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth" + ) as mock_auth, patch( + "homeassistant.components.netatmo.data_handler.PLATFORMS", ["camera"] + ), patch( + "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", + ), patch( + "homeassistant.components.netatmo.webhook_generate_url" + ): + mock_auth.return_value.async_post_api_request.side_effect = fake_post mock_auth.return_value.async_get_image.side_effect = fake_post mock_auth.return_value.async_addwebhook.side_effect = AsyncMock() mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock() diff --git a/tests/components/netatmo/test_climate.py b/tests/components/netatmo/test_climate.py index 0d51a53ec71..cc23dc887bd 100644 --- a/tests/components/netatmo/test_climate.py +++ b/tests/components/netatmo/test_climate.py @@ -413,9 +413,7 @@ async def test_service_schedule_thermostats(hass, config_entry, caplog, netatmo_ climate_entity_livingroom = "climate.livingroom" # Test setting a valid schedule - with patch( - "pyatmo.climate.AsyncClimate.async_switch_home_schedule" - ) as mock_switch_home_schedule: + with patch("pyatmo.home.Home.async_switch_schedule") as mock_switch_schedule: await hass.services.async_call( "netatmo", SERVICE_SET_SCHEDULE, @@ -423,7 +421,7 @@ async def test_service_schedule_thermostats(hass, config_entry, caplog, netatmo_ blocking=True, ) await hass.async_block_till_done() - mock_switch_home_schedule.assert_called_once_with( + mock_switch_schedule.assert_called_once_with( schedule_id="b1b54a2f45795764f59d50d8" ) @@ -442,9 +440,7 @@ async def test_service_schedule_thermostats(hass, config_entry, caplog, netatmo_ ) # Test setting an invalid schedule - with patch( - "pyatmo.climate.AsyncClimate.async_switch_home_schedule" - ) as mock_switch_home_schedule: + with patch("pyatmo.home.Home.async_switch_schedule") as mock_switch_home_schedule: await hass.services.async_call( "netatmo", SERVICE_SET_SCHEDULE, diff --git a/tests/components/netatmo/test_config_flow.py b/tests/components/netatmo/test_config_flow.py index ae13268eda3..964c7696e64 100644 --- a/tests/components/netatmo/test_config_flow.py +++ b/tests/components/netatmo/test_config_flow.py @@ -1,6 +1,8 @@ """Test the Netatmo config flow.""" from unittest.mock import patch +from pyatmo.const import ALL_SCOPES + from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components import zeroconf from homeassistant.components.netatmo import config_flow @@ -14,8 +16,6 @@ from homeassistant.components.netatmo.const import ( from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.helpers import config_entry_oauth2_flow -from .common import ALL_SCOPES - from tests.common import MockConfigEntry CLIENT_ID = "1234" diff --git a/tests/components/netatmo/test_cover.py b/tests/components/netatmo/test_cover.py new file mode 100644 index 00000000000..c0f34f25b24 --- /dev/null +++ b/tests/components/netatmo/test_cover.py @@ -0,0 +1,110 @@ +"""The tests for Netatmo cover.""" +from unittest.mock import patch + +from homeassistant.components.cover import ( + ATTR_POSITION, + DOMAIN as COVER_DOMAIN, + SERVICE_CLOSE_COVER, + SERVICE_OPEN_COVER, + SERVICE_SET_COVER_POSITION, + SERVICE_STOP_COVER, +) +from homeassistant.const import ATTR_ENTITY_ID + +from .common import selected_platforms + + +async def test_cover_setup_and_services(hass, config_entry, netatmo_auth): + """Test setup and services.""" + with selected_platforms(["cover"]): + await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + + switch_entity = "cover.entrance_blinds" + + assert hass.states.get(switch_entity).state == "closed" + + # Test cover open + with patch("pyatmo.home.Home.async_set_state") as mock_set_state: + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: switch_entity}, + blocking=True, + ) + await hass.async_block_till_done() + mock_set_state.assert_called_once_with( + { + "modules": [ + { + "id": "0009999992", + "target_position": 100, + "bridge": "12:34:56:30:d5:d4", + } + ] + } + ) + + # Test cover close + with patch("pyatmo.home.Home.async_set_state") as mock_set_state: + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: switch_entity}, + blocking=True, + ) + await hass.async_block_till_done() + mock_set_state.assert_called_once_with( + { + "modules": [ + { + "id": "0009999992", + "target_position": 0, + "bridge": "12:34:56:30:d5:d4", + } + ] + } + ) + + # Test stop cover + with patch("pyatmo.home.Home.async_set_state") as mock_set_state: + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_STOP_COVER, + {ATTR_ENTITY_ID: switch_entity}, + blocking=True, + ) + await hass.async_block_till_done() + mock_set_state.assert_called_once_with( + { + "modules": [ + { + "id": "0009999992", + "target_position": -1, + "bridge": "12:34:56:30:d5:d4", + } + ] + } + ) + + # Test set cover position + with patch("pyatmo.home.Home.async_set_state") as mock_set_state: + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_SET_COVER_POSITION, + {ATTR_ENTITY_ID: switch_entity, ATTR_POSITION: 50}, + blocking=True, + ) + await hass.async_block_till_done() + mock_set_state.assert_called_once_with( + { + "modules": [ + { + "id": "0009999992", + "target_position": 50, + "bridge": "12:34:56:30:d5:d4", + } + ] + } + ) diff --git a/tests/components/netatmo/test_device_trigger.py b/tests/components/netatmo/test_device_trigger.py index 88e455b9f67..25b86f8410e 100644 --- a/tests/components/netatmo/test_device_trigger.py +++ b/tests/components/netatmo/test_device_trigger.py @@ -7,11 +7,6 @@ from homeassistant.components.netatmo import DOMAIN as NETATMO_DOMAIN from homeassistant.components.netatmo.const import ( CLIMATE_TRIGGERS, INDOOR_CAMERA_TRIGGERS, - MODEL_NACAMERA, - MODEL_NAPLUG, - MODEL_NATHERM1, - MODEL_NOC, - MODEL_NRV, NETATMO_EVENT, OUTDOOR_CAMERA_TRIGGERS, ) @@ -52,10 +47,10 @@ def calls(hass): @pytest.mark.parametrize( "platform,device_type,event_types", [ - ("camera", MODEL_NOC, OUTDOOR_CAMERA_TRIGGERS), - ("camera", MODEL_NACAMERA, INDOOR_CAMERA_TRIGGERS), - ("climate", MODEL_NRV, CLIMATE_TRIGGERS), - ("climate", MODEL_NATHERM1, CLIMATE_TRIGGERS), + ("camera", "NOC", OUTDOOR_CAMERA_TRIGGERS), + ("camera", "NACamera", INDOOR_CAMERA_TRIGGERS), + ("climate", "NRV", CLIMATE_TRIGGERS), + ("climate", "NATherm1", CLIMATE_TRIGGERS), ], ) async def test_get_triggers( @@ -110,15 +105,15 @@ async def test_get_triggers( @pytest.mark.parametrize( "platform,camera_type,event_type", - [("camera", MODEL_NOC, trigger) for trigger in OUTDOOR_CAMERA_TRIGGERS] - + [("camera", MODEL_NACAMERA, trigger) for trigger in INDOOR_CAMERA_TRIGGERS] + [("camera", "NOC", trigger) for trigger in OUTDOOR_CAMERA_TRIGGERS] + + [("camera", "NACamera", trigger) for trigger in INDOOR_CAMERA_TRIGGERS] + [ - ("climate", MODEL_NRV, trigger) + ("climate", "NRV", trigger) for trigger in CLIMATE_TRIGGERS if trigger not in SUBTYPES ] + [ - ("climate", MODEL_NATHERM1, trigger) + ("climate", "NATherm1", trigger) for trigger in CLIMATE_TRIGGERS if trigger not in SUBTYPES ], @@ -188,12 +183,12 @@ async def test_if_fires_on_event( @pytest.mark.parametrize( "platform,camera_type,event_type,sub_type", [ - ("climate", MODEL_NRV, trigger, subtype) + ("climate", "NRV", trigger, subtype) for trigger in SUBTYPES for subtype in SUBTYPES[trigger] ] + [ - ("climate", MODEL_NATHERM1, trigger, subtype) + ("climate", "NATherm1", trigger, subtype) for trigger in SUBTYPES for subtype in SUBTYPES[trigger] ], @@ -267,7 +262,7 @@ async def test_if_fires_on_event_with_subtype( @pytest.mark.parametrize( "platform,device_type,event_type", - [("climate", MODEL_NAPLUG, trigger) for trigger in CLIMATE_TRIGGERS], + [("climate", "NAPLUG", trigger) for trigger in CLIMATE_TRIGGERS], ) async def test_if_invalid_device( hass, device_reg, entity_reg, platform, device_type, event_type diff --git a/tests/components/netatmo/test_diagnostics.py b/tests/components/netatmo/test_diagnostics.py index 3c4a2090f1b..cf9a76e38a3 100644 --- a/tests/components/netatmo/test_diagnostics.py +++ b/tests/components/netatmo/test_diagnostics.py @@ -18,7 +18,7 @@ async def test_entry_diagnostics(hass, hass_client, config_entry): ), patch( "homeassistant.components.netatmo.webhook_generate_url" ): - mock_auth.return_value.async_post_request.side_effect = fake_post_request + mock_auth.return_value.async_post_api_request.side_effect = fake_post_request mock_auth.return_value.async_addwebhook.side_effect = AsyncMock() mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock() assert await async_setup_component(hass, "netatmo", {}) @@ -39,16 +39,27 @@ async def test_entry_diagnostics(hass, hass_client, config_entry): "expires_in": 60, "refresh_token": REDACTED, "scope": [ - "read_station", - "read_camera", "access_camera", - "write_camera", - "read_presence", + "access_doorbell", "access_presence", - "write_presence", + "read_bubendorff", + "read_camera", + "read_carbonmonoxidedetector", + "read_doorbell", "read_homecoach", + "read_magellan", + "read_mx", + "read_presence", + "read_smarther", "read_smokedetector", + "read_station", "read_thermostat", + "write_bubendorff", + "write_camera", + "write_magellan", + "write_mx", + "write_presence", + "write_smarther", "write_thermostat", ], "type": "Bearer", @@ -88,5 +99,5 @@ async def test_entry_diagnostics(hass, hass_client, config_entry): "webhook_registered": False, } - for home in result["data"]["AsyncClimateTopology"]["homes"]: + for home in result["data"]["account"]["homes"]: assert home["coordinates"] == REDACTED diff --git a/tests/components/netatmo/test_init.py b/tests/components/netatmo/test_init.py index 911fd6c309a..373d7e19765 100644 --- a/tests/components/netatmo/test_init.py +++ b/tests/components/netatmo/test_init.py @@ -1,11 +1,10 @@ """The tests for Netatmo component.""" -import asyncio from datetime import timedelta from time import time from unittest.mock import AsyncMock, patch import aiohttp -import pyatmo +from pyatmo.const import ALL_SCOPES from homeassistant import config_entries from homeassistant.components.netatmo import DOMAIN @@ -15,7 +14,6 @@ from homeassistant.setup import async_setup_component from homeassistant.util import dt from .common import ( - ALL_SCOPES, FAKE_WEBHOOK_ACTIVATION, fake_post_request, selected_platforms, @@ -60,7 +58,7 @@ async def test_setup_component(hass, config_entry): ) as mock_impl, patch( "homeassistant.components.netatmo.webhook_generate_url" ) as mock_webhook: - mock_auth.return_value.async_post_request.side_effect = fake_post_request + mock_auth.return_value.async_post_api_request.side_effect = fake_post_request mock_auth.return_value.async_addwebhook.side_effect = AsyncMock() mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock() assert await async_setup_component(hass, "netatmo", {}) @@ -100,9 +98,9 @@ async def test_setup_component_with_config(hass, config_entry): ) as mock_webhook, patch( "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth", ) as mock_auth, patch( - "homeassistant.components.netatmo.PLATFORMS", ["sensor"] + "homeassistant.components.netatmo.data_handler.PLATFORMS", ["sensor"] ): - mock_auth.return_value.async_post_request.side_effect = fake_post + mock_auth.return_value.async_post_api_request.side_effect = fake_post mock_auth.return_value.async_addwebhook.side_effect = AsyncMock() mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock() @@ -112,7 +110,7 @@ async def test_setup_component_with_config(hass, config_entry): await hass.async_block_till_done() - assert fake_post_hits == 9 + assert fake_post_hits == 8 mock_impl.assert_called_once() mock_webhook.assert_called_once() @@ -162,7 +160,7 @@ async def test_setup_without_https(hass, config_entry, caplog): ), patch( "homeassistant.components.netatmo.webhook_generate_url" ) as mock_async_generate_url: - mock_auth.return_value.async_post_request.side_effect = fake_post_request + mock_auth.return_value.async_post_api_request.side_effect = fake_post_request mock_async_generate_url.return_value = "http://example.com" assert await async_setup_component( hass, "netatmo", {"netatmo": {"client_id": "123", "client_secret": "abc"}} @@ -200,7 +198,7 @@ async def test_setup_with_cloud(hass, config_entry): ), patch( "homeassistant.components.netatmo.webhook_generate_url" ): - mock_auth.return_value.async_post_request.side_effect = fake_post_request + mock_auth.return_value.async_post_api_request.side_effect = fake_post_request assert await async_setup_component( hass, "netatmo", {"netatmo": {"client_id": "123", "client_secret": "abc"}} ) @@ -266,7 +264,7 @@ async def test_setup_with_cloudhook(hass): ), patch( "homeassistant.components.netatmo.webhook_generate_url" ): - mock_auth.return_value.async_post_request.side_effect = fake_post_request + mock_auth.return_value.async_post_api_request.side_effect = fake_post_request mock_auth.return_value.async_addwebhook.side_effect = AsyncMock() mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock() assert await async_setup_component(hass, "netatmo", {}) @@ -289,52 +287,6 @@ async def test_setup_with_cloudhook(hass): assert not hass.config_entries.async_entries(DOMAIN) -async def test_setup_component_api_error(hass, config_entry): - """Test error on setup of the netatmo component.""" - with patch( - "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth", - ) as mock_auth, patch( - "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", - ) as mock_impl, patch( - "homeassistant.components.netatmo.webhook_generate_url" - ): - mock_auth.return_value.async_post_request.side_effect = ( - pyatmo.exceptions.ApiError() - ) - - mock_auth.return_value.async_addwebhook.side_effect = AsyncMock() - mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock() - assert await async_setup_component(hass, "netatmo", {}) - - await hass.async_block_till_done() - - mock_auth.assert_called_once() - mock_impl.assert_called_once() - - -async def test_setup_component_api_timeout(hass, config_entry): - """Test timeout on setup of the netatmo component.""" - with patch( - "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth", - ) as mock_auth, patch( - "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", - ) as mock_impl, patch( - "homeassistant.components.netatmo.webhook_generate_url" - ): - mock_auth.return_value.async_post_request.side_effect = ( - asyncio.exceptions.TimeoutError() - ) - - mock_auth.return_value.async_addwebhook.side_effect = AsyncMock() - mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock() - assert await async_setup_component(hass, "netatmo", {}) - - await hass.async_block_till_done() - - mock_auth.assert_called_once() - mock_impl.assert_called_once() - - async def test_setup_component_with_delay(hass, config_entry): """Test setup of the netatmo component with delayed startup.""" hass.state = CoreState.not_running @@ -348,9 +300,9 @@ async def test_setup_component_with_delay(hass, config_entry): ) as mock_impl, patch( "homeassistant.components.netatmo.webhook_generate_url" ) as mock_webhook, patch( - "pyatmo.AbstractAsyncAuth.async_post_request", side_effect=fake_post_request - ) as mock_post_request, patch( - "homeassistant.components.netatmo.PLATFORMS", ["light"] + "pyatmo.AbstractAsyncAuth.async_post_api_request", side_effect=fake_post_request + ) as mock_post_api_request, patch( + "homeassistant.components.netatmo.data_handler.PLATFORMS", ["light"] ): assert await async_setup_component( @@ -359,7 +311,7 @@ async def test_setup_component_with_delay(hass, config_entry): await hass.async_block_till_done() - assert mock_post_request.call_count == 8 + assert mock_post_api_request.call_count == 7 mock_impl.assert_called_once() mock_webhook.assert_not_called() @@ -422,7 +374,7 @@ async def test_setup_component_invalid_token_scope(hass): ) as mock_impl, patch( "homeassistant.components.netatmo.webhook_generate_url" ) as mock_webhook: - mock_auth.return_value.async_post_request.side_effect = fake_post_request + mock_auth.return_value.async_post_api_request.side_effect = fake_post_request mock_auth.return_value.async_addwebhook.side_effect = AsyncMock() mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock() assert await async_setup_component(hass, "netatmo", {}) @@ -465,7 +417,7 @@ async def test_setup_component_invalid_token(hass, config_entry): ) as mock_webhook, patch( "homeassistant.helpers.config_entry_oauth2_flow.OAuth2Session" ) as mock_session: - mock_auth.return_value.async_post_request.side_effect = fake_post_request + mock_auth.return_value.async_post_api_request.side_effect = fake_post_request mock_auth.return_value.async_addwebhook.side_effect = AsyncMock() mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock() mock_session.return_value.async_ensure_token_valid.side_effect = ( diff --git a/tests/components/netatmo/test_light.py b/tests/components/netatmo/test_light.py index 433841f3878..ced24c738e3 100644 --- a/tests/components/netatmo/test_light.py +++ b/tests/components/netatmo/test_light.py @@ -14,8 +14,8 @@ from .common import FAKE_WEBHOOK_ACTIVATION, selected_platforms, simulate_webhoo from tests.test_util.aiohttp import AiohttpClientMockResponse -async def test_light_setup_and_services(hass, config_entry, netatmo_auth): - """Test setup and services.""" +async def test_camera_light_setup_and_services(hass, config_entry, netatmo_auth): + """Test camera ligiht setup and services.""" with selected_platforms(["light"]): await hass.config_entries.async_setup(config_entry.entry_id) @@ -53,7 +53,7 @@ async def test_light_setup_and_services(hass, config_entry, netatmo_auth): assert hass.states.get(light_entity).state == "on" # Test turning light off - with patch("pyatmo.camera.AsyncCameraData.async_set_state") as mock_set_state: + with patch("pyatmo.home.Home.async_set_state") as mock_set_state: await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_OFF, @@ -62,13 +62,11 @@ async def test_light_setup_and_services(hass, config_entry, netatmo_auth): ) await hass.async_block_till_done() mock_set_state.assert_called_once_with( - home_id="91763b24c43d3e344f424e8b", - camera_id="12:34:56:00:a5:a4", - floodlight="auto", + {"modules": [{"id": "12:34:56:00:a5:a4", "floodlight": "auto"}]} ) # Test turning light on - with patch("pyatmo.camera.AsyncCameraData.async_set_state") as mock_set_state: + with patch("pyatmo.home.Home.async_set_state") as mock_set_state: await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, @@ -77,9 +75,7 @@ async def test_light_setup_and_services(hass, config_entry, netatmo_auth): ) await hass.async_block_till_done() mock_set_state.assert_called_once_with( - home_id="91763b24c43d3e344f424e8b", - camera_id="12:34:56:00:a5:a4", - floodlight="on", + {"modules": [{"id": "12:34:56:00:a5:a4", "floodlight": "on"}]} ) @@ -93,7 +89,7 @@ async def test_setup_component_no_devices(hass, config_entry): fake_post_hits += 1 return AiohttpClientMockResponse( method="POST", - url=kwargs["url"], + url=kwargs["endpoint"], json={}, ) @@ -106,7 +102,7 @@ async def test_setup_component_no_devices(hass, config_entry): ), patch( "homeassistant.components.netatmo.webhook_generate_url" ): - mock_auth.return_value.async_post_request.side_effect = ( + mock_auth.return_value.async_post_api_request.side_effect = ( fake_post_request_no_data ) mock_auth.return_value.async_addwebhook.side_effect = AsyncMock() @@ -125,3 +121,57 @@ async def test_setup_component_no_devices(hass, config_entry): assert hass.config_entries.async_entries(DOMAIN) assert len(hass.states.async_all()) == 0 + + +async def test_light_setup_and_services(hass, config_entry, netatmo_auth): + """Test setup and services.""" + with selected_platforms(["light"]): + await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + + light_entity = "light.bathroom_light" + + assert hass.states.get(light_entity).state == "off" + + # Test turning light off + with patch("pyatmo.home.Home.async_set_state") as mock_set_state: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: light_entity}, + blocking=True, + ) + await hass.async_block_till_done() + mock_set_state.assert_called_once_with( + { + "modules": [ + { + "id": "12:34:56:00:01:01:01:a1", + "on": False, + "bridge": "12:34:56:80:60:40", + } + ] + } + ) + + # Test turning light on + with patch("pyatmo.home.Home.async_set_state") as mock_set_state: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: light_entity}, + blocking=True, + ) + await hass.async_block_till_done() + mock_set_state.assert_called_once_with( + { + "modules": [ + { + "id": "12:34:56:00:01:01:01:a1", + "on": True, + "bridge": "12:34:56:80:60:40", + } + ] + } + ) diff --git a/tests/components/netatmo/test_media_source.py b/tests/components/netatmo/test_media_source.py index 01422dfd118..96566af6832 100644 --- a/tests/components/netatmo/test_media_source.py +++ b/tests/components/netatmo/test_media_source.py @@ -70,13 +70,13 @@ async def test_async_browse_media(hass): # Test successful event listing media = await async_browse_media( - hass, f"{URI_SCHEME}{DOMAIN}/events/12:34:56:78:90:ab/1599152672" + hass, f"{URI_SCHEME}{DOMAIN}/events/12:34:56:78:90:ab/1654191519" ) assert media # Test successful event resolve media = await async_resolve_media( - hass, f"{URI_SCHEME}{DOMAIN}/events/12:34:56:78:90:ab/1599152672", None + hass, f"{URI_SCHEME}{DOMAIN}/events/12:34:56:78:90:ab/1654191519", None ) assert media == PlayMedia( url="http:///files/high/index.m3u8", mime_type="application/x-mpegURL" diff --git a/tests/components/netatmo/test_select.py b/tests/components/netatmo/test_select.py index 12168a03ad8..ea8e88ce8de 100644 --- a/tests/components/netatmo/test_select.py +++ b/tests/components/netatmo/test_select.py @@ -19,7 +19,7 @@ async def test_select_schedule_thermostats(hass, config_entry, caplog, netatmo_a await hass.async_block_till_done() webhook_id = config_entry.data[CONF_WEBHOOK_ID] - select_entity = "select.netatmo_myhome" + select_entity = "select.myhome" assert hass.states.get(select_entity).state == "Default" @@ -40,9 +40,7 @@ async def test_select_schedule_thermostats(hass, config_entry, caplog, netatmo_a ] # Test setting a different schedule - with patch( - "pyatmo.climate.AsyncClimate.async_switch_home_schedule" - ) as mock_switch_home_schedule: + with patch("pyatmo.home.Home.async_switch_schedule") as mock_switch_home_schedule: await hass.services.async_call( SELECT_DOMAIN, SERVICE_SELECT_OPTION, diff --git a/tests/components/netatmo/test_sensor.py b/tests/components/netatmo/test_sensor.py index 9adc7423bd6..99e76389c13 100644 --- a/tests/components/netatmo/test_sensor.py +++ b/tests/components/netatmo/test_sensor.py @@ -4,7 +4,6 @@ from unittest.mock import patch import pytest from homeassistant.components.netatmo import sensor -from homeassistant.components.netatmo.sensor import MODULE_TYPE_WIND from homeassistant.helpers import entity_registry as er from .common import TEST_TIME, selected_platforms @@ -17,7 +16,7 @@ async def test_weather_sensor(hass, config_entry, netatmo_auth): await hass.async_block_till_done() - prefix = "sensor.mystation_" + prefix = "sensor.netatmoindoor_" assert hass.states.get(f"{prefix}temperature").state == "24.6" assert hass.states.get(f"{prefix}humidity").state == "36" @@ -102,77 +101,19 @@ async def test_process_health(health, expected): assert sensor.process_health(health) == expected -@pytest.mark.parametrize( - "model, data, expected", - [ - (MODULE_TYPE_WIND, 5591, "Full"), - (MODULE_TYPE_WIND, 5181, "High"), - (MODULE_TYPE_WIND, 4771, "Medium"), - (MODULE_TYPE_WIND, 4361, "Low"), - (MODULE_TYPE_WIND, 4300, "Very Low"), - ], -) -async def test_process_battery(model, data, expected): - """Test battery level translation.""" - assert sensor.process_battery(data, model) == expected - - -@pytest.mark.parametrize( - "angle, expected", - [ - (0, "N"), - (40, "NE"), - (70, "E"), - (130, "SE"), - (160, "S"), - (220, "SW"), - (250, "W"), - (310, "NW"), - (340, "N"), - ], -) -async def test_process_angle(angle, expected): - """Test wind direction translation.""" - assert sensor.process_angle(angle) == expected - - -@pytest.mark.parametrize( - "angle, expected", - [(-1, 359), (-40, 320)], -) -async def test_fix_angle(angle, expected): - """Test wind angle fix.""" - assert sensor.fix_angle(angle) == expected - - @pytest.mark.parametrize( "uid, name, expected", [ - ("12:34:56:37:11:ca-reachable", "netatmo_mystation_reachable", "True"), - ("12:34:56:03:1b:e4-rf_status", "netatmo_mystation_yard_radio", "Full"), - ( - "12:34:56:05:25:6e-rf_status", - "netatmo_valley_road_rain_gauge_radio", - "Medium", - ), - ( - "12:34:56:36:fc:de-rf_status_lvl", - "netatmo_mystation_netatmooutdoor_radio_level", - "65", - ), - ( - "12:34:56:37:11:ca-wifi_status_lvl", - "netatmo_mystation_wifi_level", - "45", - ), + ("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:37:11:ca-wifi_status", - "netatmo_mystation_wifi_status", + "mystation_wifi_strength", "Full", ), ( "12:34:56:37:11:ca-temp_trend", - "netatmo_mystation_temperature_trend", + "mystation_temperature_trend", "stable", ), ( @@ -182,33 +123,47 @@ async def test_fix_angle(angle, expected): ), ("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:03:1b:e4-windangle", "netatmo_mystation_garden_direction", "SW"), + ("12:34:56:03:1b:e4-windangle", "netatmoindoor_garden_direction", "SW"), ( "12:34:56:03:1b:e4-windangle_value", - "netatmo_mystation_garden_angle", + "netatmoindoor_garden_angle", "217", ), ("12:34:56:03:1b:e4-gustangle", "mystation_garden_gust_direction", "S"), ( "12:34:56:03:1b:e4-gustangle", - "netatmo_mystation_garden_gust_direction", + "netatmoindoor_garden_gust_direction", "S", ), ( "12:34:56:03:1b:e4-gustangle_value", - "netatmo_mystation_garden_gust_angle_value", + "netatmoindoor_garden_gust_angle", "206", ), ( "12:34:56:03:1b:e4-guststrength", - "netatmo_mystation_garden_gust_strength", + "netatmoindoor_garden_gust_strength", "9", ), + ( + "12:34:56:03:1b:e4-rf_status", + "netatmoindoor_garden_rf_strength", + "Full", + ), ( "12:34:56:26:68:92-health_idx", - "netatmo_baby_bedroom_health", + "baby_bedroom_health", "Fine", ), + ( + "12:34:56:26:68:92-wifi_status", + "baby_bedroom_wifi", + "High", + ), + ("Home-max-windangle_value", "home_max_wind_angle", "17"), + ("Home-max-gustangle_value", "home_max_gust_angle", "217"), + ("Home-max-guststrength", "home_max_gust_strength", "31"), + ("Home-max-sum_rain_1", "home_max_sum_rain_1", "0.2"), ], ) async def test_weather_sensor_enabling( diff --git a/tests/components/netatmo/test_switch.py b/tests/components/netatmo/test_switch.py new file mode 100644 index 00000000000..dc11ac22746 --- /dev/null +++ b/tests/components/netatmo/test_switch.py @@ -0,0 +1,65 @@ +"""The tests for Netatmo switch.""" +from unittest.mock import patch + +from homeassistant.components.switch import ( + DOMAIN as SWITCH_DOMAIN, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.const import ATTR_ENTITY_ID + +from .common import selected_platforms + + +async def test_switch_setup_and_services(hass, config_entry, netatmo_auth): + """Test setup and services.""" + with selected_platforms(["switch"]): + await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + + switch_entity = "switch.prise" + + assert hass.states.get(switch_entity).state == "on" + + # Test turning switch off + with patch("pyatmo.home.Home.async_set_state") as mock_set_state: + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: switch_entity}, + blocking=True, + ) + await hass.async_block_till_done() + mock_set_state.assert_called_once_with( + { + "modules": [ + { + "id": "12:34:56:80:00:12:ac:f2", + "on": False, + "bridge": "12:34:56:80:60:40", + } + ] + } + ) + + # Test turning switch on + with patch("pyatmo.home.Home.async_set_state") as mock_set_state: + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: switch_entity}, + blocking=True, + ) + await hass.async_block_till_done() + mock_set_state.assert_called_once_with( + { + "modules": [ + { + "id": "12:34:56:80:00:12:ac:f2", + "on": True, + "bridge": "12:34:56:80:60:40", + } + ] + } + ) From c6fd2bde4638fbd38b99817968bfacd3a228cb32 Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Mon, 26 Sep 2022 03:59:18 +0200 Subject: [PATCH 718/955] Migrate Overkiz to new entity naming style (#76687) --- homeassistant/components/overkiz/button.py | 10 +-- homeassistant/components/overkiz/entity.py | 6 +- homeassistant/components/overkiz/number.py | 10 +-- homeassistant/components/overkiz/select.py | 6 +- homeassistant/components/overkiz/sensor.py | 84 +++++++++++----------- homeassistant/components/overkiz/switch.py | 2 +- 6 files changed, 58 insertions(+), 60 deletions(-) diff --git a/homeassistant/components/overkiz/button.py b/homeassistant/components/overkiz/button.py index 546c24cb6d1..a4c2bc3c2da 100644 --- a/homeassistant/components/overkiz/button.py +++ b/homeassistant/components/overkiz/button.py @@ -27,20 +27,20 @@ BUTTON_DESCRIPTIONS: list[OverkizButtonDescription] = [ # My Position (cover, light) OverkizButtonDescription( key="my", - name="My Position", + name="My position", icon="mdi:star", ), # Identify OverkizButtonDescription( key="identify", # startIdentify and identify are reversed... Swap this when fixed in API. - name="Start Identify", + name="Start identify", icon="mdi:human-greeting-variant", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), OverkizButtonDescription( key="stopIdentify", - name="Stop Identify", + name="Stop identify", icon="mdi:human-greeting-variant", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -52,10 +52,10 @@ BUTTON_DESCRIPTIONS: list[OverkizButtonDescription] = [ entity_category=EntityCategory.DIAGNOSTIC, ), # RTDIndoorSiren / RTDOutdoorSiren - OverkizButtonDescription(key="dingDong", name="Ding Dong", icon="mdi:bell-ring"), + OverkizButtonDescription(key="dingDong", name="Ding dong", icon="mdi:bell-ring"), OverkizButtonDescription(key="bip", name="Bip", icon="mdi:bell-ring"), OverkizButtonDescription( - key="fastBipSequence", name="Fast Bip Sequence", icon="mdi:bell-ring" + key="fastBipSequence", name="Fast bip sequence", icon="mdi:bell-ring" ), OverkizButtonDescription(key="ring", name="Ring", icon="mdi:bell-ring"), # DynamicScreen (ogp:blind) uses goToAlias (id 1: favorite1) instead of 'my' diff --git a/homeassistant/components/overkiz/entity.py b/homeassistant/components/overkiz/entity.py index 5728349c5d0..4cb5ad1ede7 100644 --- a/homeassistant/components/overkiz/entity.py +++ b/homeassistant/components/overkiz/entity.py @@ -19,6 +19,8 @@ from .executor import OverkizExecutor class OverkizEntity(CoordinatorEntity[OverkizDataUpdateCoordinator]): """Representation of an Overkiz device entity.""" + _attr_has_entity_name = True + def __init__( self, device_url: str, coordinator: OverkizDataUpdateCoordinator ) -> None: @@ -31,7 +33,6 @@ class OverkizEntity(CoordinatorEntity[OverkizDataUpdateCoordinator]): self._attr_assumed_state = not self.device.states self._attr_available = self.device.available self._attr_unique_id = self.device.device_url - self._attr_name = self.device.label self._attr_device_info = self.generate_device_info() @@ -102,9 +103,6 @@ class OverkizDescriptiveEntity(OverkizEntity): self.entity_description = description self._attr_unique_id = f"{super().unique_id}-{self.entity_description.key}" - if self.entity_description.name: - self._attr_name = f"{super().name} {self.entity_description.name}" - # Used by state translations for sensor and select entities @unique diff --git a/homeassistant/components/overkiz/number.py b/homeassistant/components/overkiz/number.py index 3fb5cb2bcff..3b4107256a5 100644 --- a/homeassistant/components/overkiz/number.py +++ b/homeassistant/components/overkiz/number.py @@ -80,7 +80,7 @@ NUMBER_DESCRIPTIONS: list[OverkizNumberDescription] = [ # Cover: My Position (0 - 100) OverkizNumberDescription( key=OverkizState.CORE_MEMORIZED_1_POSITION, - name="My Position", + name="My position", icon="mdi:content-save-cog", command=OverkizCommand.SET_MEMORIZED_1_POSITION, native_min_value=0, @@ -90,7 +90,7 @@ NUMBER_DESCRIPTIONS: list[OverkizNumberDescription] = [ # WaterHeater: Expected Number Of Shower (2 - 4) OverkizNumberDescription( key=OverkizState.CORE_EXPECTED_NUMBER_OF_SHOWER, - name="Expected Number Of Shower", + name="Expected number of shower", icon="mdi:shower-head", command=OverkizCommand.SET_EXPECTED_NUMBER_OF_SHOWER, native_min_value=2, @@ -100,7 +100,7 @@ NUMBER_DESCRIPTIONS: list[OverkizNumberDescription] = [ # SomfyHeatingTemperatureInterface OverkizNumberDescription( key=OverkizState.CORE_ECO_ROOM_TEMPERATURE, - name="Eco Room Temperature", + name="Eco room temperature", icon="mdi:thermometer", command=OverkizCommand.SET_ECO_TEMPERATURE, device_class=NumberDeviceClass.TEMPERATURE, @@ -111,7 +111,7 @@ NUMBER_DESCRIPTIONS: list[OverkizNumberDescription] = [ ), OverkizNumberDescription( key=OverkizState.CORE_COMFORT_ROOM_TEMPERATURE, - name="Comfort Room Temperature", + name="Comfort room temperature", icon="mdi:home-thermometer-outline", command=OverkizCommand.SET_COMFORT_TEMPERATURE, device_class=NumberDeviceClass.TEMPERATURE, @@ -122,7 +122,7 @@ NUMBER_DESCRIPTIONS: list[OverkizNumberDescription] = [ ), OverkizNumberDescription( key=OverkizState.CORE_SECURED_POSITION_TEMPERATURE, - name="Freeze Protection Temperature", + name="Freeze protection temperature", icon="mdi:sun-thermometer-outline", command=OverkizCommand.SET_SECURED_POSITION_TEMPERATURE, device_class=NumberDeviceClass.TEMPERATURE, diff --git a/homeassistant/components/overkiz/select.py b/homeassistant/components/overkiz/select.py index 8482932e2e4..3b40eccfbf6 100644 --- a/homeassistant/components/overkiz/select.py +++ b/homeassistant/components/overkiz/select.py @@ -76,7 +76,7 @@ SELECT_DESCRIPTIONS: list[OverkizSelectDescription] = [ ), OverkizSelectDescription( key=OverkizState.IO_MEMORIZED_SIMPLE_VOLUME, - name="Memorized Simple Volume", + name="Memorized simple volume", icon="mdi:volume-high", options=[OverkizCommandParam.STANDARD, OverkizCommandParam.HIGHEST], select_option=_select_option_memorized_simple_volume, @@ -86,7 +86,7 @@ SELECT_DESCRIPTIONS: list[OverkizSelectDescription] = [ # SomfyHeatingTemperatureInterface OverkizSelectDescription( key=OverkizState.OVP_HEATING_TEMPERATURE_INTERFACE_OPERATING_MODE, - name="Operating Mode", + name="Operating mode", icon="mdi:sun-snowflake", options=[OverkizCommandParam.HEATING, OverkizCommandParam.COOLING], select_option=lambda option, execute_command: execute_command( @@ -97,7 +97,7 @@ SELECT_DESCRIPTIONS: list[OverkizSelectDescription] = [ # StatefulAlarmController OverkizSelectDescription( key=OverkizState.CORE_ACTIVE_ZONES, - name="Active Zones", + name="Active zones", icon="mdi:shield-lock", options=["", "A", "B", "C", "A,B", "B,C", "A,C", "A,B,C"], select_option=_select_option_active_zone, diff --git a/homeassistant/components/overkiz/sensor.py b/homeassistant/components/overkiz/sensor.py index cbe4234a5ac..83f123eaad7 100644 --- a/homeassistant/components/overkiz/sensor.py +++ b/homeassistant/components/overkiz/sensor.py @@ -48,7 +48,7 @@ class OverkizSensorDescription(SensorEntityDescription): SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ OverkizSensorDescription( key=OverkizState.CORE_BATTERY_LEVEL, - name="Battery Level", + name="Battery level", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, state_class=SensorStateClass.MEASUREMENT, @@ -64,7 +64,7 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ ), OverkizSensorDescription( key=OverkizState.CORE_RSSI_LEVEL, - name="RSSI Level", + name="RSSI level", native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, device_class=SensorDeviceClass.SIGNAL_STRENGTH, state_class=SensorStateClass.MEASUREMENT, @@ -74,20 +74,20 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ ), OverkizSensorDescription( key=OverkizState.CORE_EXPECTED_NUMBER_OF_SHOWER, - name="Expected Number Of Shower", + name="Expected number of shower", icon="mdi:shower-head", state_class=SensorStateClass.MEASUREMENT, ), OverkizSensorDescription( key=OverkizState.CORE_NUMBER_OF_SHOWER_REMAINING, - name="Number of Shower Remaining", + name="Number of shower remaining", icon="mdi:shower-head", state_class=SensorStateClass.MEASUREMENT, ), # V40 is measured in litres (L) and shows the amount of warm (mixed) water with a temperature of 40 C, which can be drained from a switched off electric water heater. OverkizSensorDescription( key=OverkizState.CORE_V40_WATER_VOLUME_ESTIMATION, - name="Water Volume Estimation at 40 °C", + name="Water volume estimation at 40 °C", icon="mdi:water", native_unit_of_measurement=VOLUME_LITERS, entity_registry_enabled_default=False, @@ -95,50 +95,50 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ ), OverkizSensorDescription( key=OverkizState.CORE_WATER_CONSUMPTION, - name="Water Consumption", + name="Water consumption", icon="mdi:water", native_unit_of_measurement=VOLUME_LITERS, state_class=SensorStateClass.MEASUREMENT, ), OverkizSensorDescription( key=OverkizState.IO_OUTLET_ENGINE, - name="Outlet Engine", + name="Outlet engine", icon="mdi:fan-chevron-down", native_unit_of_measurement=VOLUME_LITERS, state_class=SensorStateClass.MEASUREMENT, ), OverkizSensorDescription( key=OverkizState.IO_INLET_ENGINE, - name="Inlet Engine", + name="Inlet engine", icon="mdi:fan-chevron-up", native_unit_of_measurement=VOLUME_FLOW_RATE_CUBIC_METERS_PER_HOUR, state_class=SensorStateClass.MEASUREMENT, ), OverkizSensorDescription( key=OverkizState.HLRRWIFI_ROOM_TEMPERATURE, - name="Room Temperature", + name="Room temperature", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=TEMP_CELSIUS, ), OverkizSensorDescription( key=OverkizState.IO_MIDDLE_WATER_TEMPERATURE, - name="Middle Water Temperature", + name="Middle water temperature", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=TEMP_CELSIUS, ), OverkizSensorDescription( key=OverkizState.CORE_FOSSIL_ENERGY_CONSUMPTION, - name="Fossil Energy Consumption", + name="Fossil energy consumption", ), OverkizSensorDescription( key=OverkizState.CORE_GAS_CONSUMPTION, - name="Gas Consumption", + name="Gas consumption", ), OverkizSensorDescription( key=OverkizState.CORE_THERMAL_ENERGY_CONSUMPTION, - name="Thermal Energy Consumption", + name="Thermal energy consumption", ), # LightSensor/LuminanceSensor OverkizSensorDescription( @@ -151,21 +151,21 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ # ElectricitySensor/CumulativeElectricPowerConsumptionSensor OverkizSensorDescription( key=OverkizState.CORE_ELECTRIC_ENERGY_CONSUMPTION, - name="Electric Energy Consumption", + name="Electric energy consumption", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_WATT_HOUR, # core:MeasuredValueType = core:ElectricalEnergyInWh (not for modbus:YutakiV2DHWElectricalEnergyConsumptionComponent) state_class=SensorStateClass.TOTAL_INCREASING, # core:MeasurementCategory attribute = electric/overall ), OverkizSensorDescription( key=OverkizState.CORE_ELECTRIC_POWER_CONSUMPTION, - name="Electric Power Consumption", + name="Electric power consumption", device_class=SensorDeviceClass.POWER, native_unit_of_measurement=POWER_WATT, # core:MeasuredValueType = core:ElectricalEnergyInWh (not for modbus:YutakiV2DHWElectricalEnergyConsumptionComponent) state_class=SensorStateClass.MEASUREMENT, ), OverkizSensorDescription( key=OverkizState.CORE_CONSUMPTION_TARIFF1, - name="Consumption Tariff 1", + name="Consumption tariff 1", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_WATT_HOUR, # core:MeasuredValueType = core:ElectricalEnergyInWh entity_registry_enabled_default=False, @@ -173,7 +173,7 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ ), OverkizSensorDescription( key=OverkizState.CORE_CONSUMPTION_TARIFF2, - name="Consumption Tariff 2", + name="Consumption tariff 2", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_WATT_HOUR, # core:MeasuredValueType = core:ElectricalEnergyInWh entity_registry_enabled_default=False, @@ -181,7 +181,7 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ ), OverkizSensorDescription( key=OverkizState.CORE_CONSUMPTION_TARIFF3, - name="Consumption Tariff 3", + name="Consumption tariff 3", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_WATT_HOUR, # core:MeasuredValueType = core:ElectricalEnergyInWh entity_registry_enabled_default=False, @@ -189,7 +189,7 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ ), OverkizSensorDescription( key=OverkizState.CORE_CONSUMPTION_TARIFF4, - name="Consumption Tariff 4", + name="Consumption tariff 4", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_WATT_HOUR, # core:MeasuredValueType = core:ElectricalEnergyInWh entity_registry_enabled_default=False, @@ -197,7 +197,7 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ ), OverkizSensorDescription( key=OverkizState.CORE_CONSUMPTION_TARIFF5, - name="Consumption Tariff 5", + name="Consumption tariff 5", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_WATT_HOUR, # core:MeasuredValueType = core:ElectricalEnergyInWh entity_registry_enabled_default=False, @@ -205,7 +205,7 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ ), OverkizSensorDescription( key=OverkizState.CORE_CONSUMPTION_TARIFF6, - name="Consumption Tariff 6", + name="Consumption tariff 6", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_WATT_HOUR, # core:MeasuredValueType = core:ElectricalEnergyInWh entity_registry_enabled_default=False, @@ -213,7 +213,7 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ ), OverkizSensorDescription( key=OverkizState.CORE_CONSUMPTION_TARIFF7, - name="Consumption Tariff 7", + name="Consumption tariff 7", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_WATT_HOUR, # core:MeasuredValueType = core:ElectricalEnergyInWh entity_registry_enabled_default=False, @@ -221,7 +221,7 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ ), OverkizSensorDescription( key=OverkizState.CORE_CONSUMPTION_TARIFF8, - name="Consumption Tariff 8", + name="Consumption tariff 8", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_WATT_HOUR, # core:MeasuredValueType = core:ElectricalEnergyInWh entity_registry_enabled_default=False, @@ -229,7 +229,7 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ ), OverkizSensorDescription( key=OverkizState.CORE_CONSUMPTION_TARIFF9, - name="Consumption Tariff 9", + name="Consumption tariff 9", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_WATT_HOUR, # core:MeasuredValueType = core:ElectricalEnergyInWh entity_registry_enabled_default=False, @@ -238,7 +238,7 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ # HumiditySensor/RelativeHumiditySensor OverkizSensorDescription( key=OverkizState.CORE_RELATIVE_HUMIDITY, - name="Relative Humidity", + name="Relative humidity", native_value=lambda value: round(cast(float, value), 2), device_class=SensorDeviceClass.HUMIDITY, native_unit_of_measurement=PERCENTAGE, # core:MeasuredValueType = core:RelativeValueInPercentage @@ -256,21 +256,21 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ # WeatherSensor/WeatherForecastSensor OverkizSensorDescription( key=OverkizState.CORE_WEATHER_STATUS, - name="Weather Status", + name="Weather status", device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=TEMP_CELSIUS, state_class=SensorStateClass.MEASUREMENT, ), OverkizSensorDescription( key=OverkizState.CORE_MINIMUM_TEMPERATURE, - name="Minimum Temperature", + name="Minimum temperature", device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=TEMP_CELSIUS, state_class=SensorStateClass.MEASUREMENT, ), OverkizSensorDescription( key=OverkizState.CORE_MAXIMUM_TEMPERATURE, - name="Maximum Temperature", + name="Maximum temperature", device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=TEMP_CELSIUS, state_class=SensorStateClass.MEASUREMENT, @@ -278,7 +278,7 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ # AirSensor/COSensor OverkizSensorDescription( key=OverkizState.CORE_CO_CONCENTRATION, - name="CO Concentration", + name="CO concentration", device_class=SensorDeviceClass.CO, native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, state_class=SensorStateClass.MEASUREMENT, @@ -286,7 +286,7 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ # AirSensor/CO2Sensor OverkizSensorDescription( key=OverkizState.CORE_CO2_CONCENTRATION, - name="CO2 Concentration", + name="CO2 concentration", device_class=SensorDeviceClass.CO2, native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, state_class=SensorStateClass.MEASUREMENT, @@ -294,7 +294,7 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ # SunSensor/SunEnergySensor OverkizSensorDescription( key=OverkizState.CORE_SUN_ENERGY, - name="Sun Energy", + name="Sun energy", native_value=lambda value: round(cast(float, value), 2), icon="mdi:solar-power", state_class=SensorStateClass.MEASUREMENT, @@ -302,7 +302,7 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ # WindSensor/WindSpeedSensor OverkizSensorDescription( key=OverkizState.CORE_WIND_SPEED, - name="Wind Speed", + name="Wind speed", native_value=lambda value: round(cast(float, value), 2), icon="mdi:weather-windy", state_class=SensorStateClass.MEASUREMENT, @@ -310,14 +310,14 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ # SmokeSensor/SmokeSensor OverkizSensorDescription( key=OverkizState.IO_SENSOR_ROOM, - name="Sensor Room", + name="Sensor room", device_class=OverkizDeviceClass.SENSOR_ROOM, entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:spray-bottle", ), OverkizSensorDescription( key=OverkizState.IO_PRIORITY_LOCK_ORIGINATOR, - name="Priority Lock Originator", + name="Priority lock originator", device_class=OverkizDeviceClass.PRIORITY_LOCK_ORIGINATOR, icon="mdi:lock", entity_registry_enabled_default=False, @@ -327,14 +327,14 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ ), OverkizSensorDescription( key=OverkizState.CORE_PRIORITY_LOCK_TIMER, - name="Priority Lock Timer", + name="Priority lock timer", icon="mdi:lock-clock", native_unit_of_measurement=TIME_SECONDS, entity_registry_enabled_default=False, ), OverkizSensorDescription( key=OverkizState.CORE_DISCRETE_RSSI_LEVEL, - name="Discrete RSSI Level", + name="Discrete RSSI level", entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, device_class=OverkizDeviceClass.DISCRETE_RSSI_LEVEL, @@ -342,7 +342,7 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ ), OverkizSensorDescription( key=OverkizState.CORE_SENSOR_DEFECT, - name="Sensor Defect", + name="Sensor defect", entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, device_class=OverkizDeviceClass.SENSOR_DEFECT, @@ -353,14 +353,14 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ # DomesticHotWaterProduction/WaterHeatingSystem OverkizSensorDescription( key=OverkizState.IO_HEAT_PUMP_OPERATING_TIME, - name="Heat Pump Operating Time", + name="Heat pump operating time", device_class=SensorDeviceClass.DURATION, entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=TIME_SECONDS, ), OverkizSensorDescription( key=OverkizState.IO_ELECTRIC_BOOSTER_OPERATING_TIME, - name="Electric Booster Operating Time", + name="Electric booster operating time", device_class=SensorDeviceClass.DURATION, native_unit_of_measurement=TIME_SECONDS, entity_category=EntityCategory.DIAGNOSTIC, @@ -368,14 +368,14 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ # Cover OverkizSensorDescription( key=OverkizState.CORE_TARGET_CLOSURE, - name="Target Closure", + name="Target closure", native_unit_of_measurement=PERCENTAGE, entity_registry_enabled_default=False, ), # ThreeWayWindowHandle/WindowHandle OverkizSensorDescription( key=OverkizState.CORE_THREE_WAY_HANDLE_DIRECTION, - name="Three Way Handle Direction", + name="Three way handle direction", device_class=OverkizDeviceClass.THREE_WAY_HANDLE_DIRECTION, ), ] @@ -454,7 +454,7 @@ class OverkizHomeKitSetupCodeSensor(OverkizEntity, SensorEntity): ) -> None: """Initialize the device.""" super().__init__(device_url, coordinator) - self._attr_name = "HomeKit Setup Code" + self._attr_name = "HomeKit setup code" @property def native_value(self) -> str | None: diff --git a/homeassistant/components/overkiz/switch.py b/homeassistant/components/overkiz/switch.py index a198964b5d3..a7dcfe54a2a 100644 --- a/homeassistant/components/overkiz/switch.py +++ b/homeassistant/components/overkiz/switch.py @@ -98,7 +98,7 @@ SWITCH_DESCRIPTIONS: list[OverkizSwitchDescription] = [ ), OverkizSwitchDescription( key=UIWidget.MY_FOX_SECURITY_CAMERA, - name="Camera Shutter", + name="Camera shutter", turn_on=OverkizCommand.OPEN, turn_off=OverkizCommand.CLOSE, icon="mdi:camera-lock", From faad904cbcf5e994238e89b699c6ff332d570ec4 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 26 Sep 2022 04:01:27 +0200 Subject: [PATCH 719/955] Remove unnecessary boolean checks for callables (#78819) --- homeassistant/components/goodwe/number.py | 3 +-- homeassistant/components/kostal_plenticore/sensor.py | 2 +- homeassistant/helpers/schema_config_entry_flow.py | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/goodwe/number.py b/homeassistant/components/goodwe/number.py index f8d3979879f..b8196b05252 100644 --- a/homeassistant/components/goodwe/number.py +++ b/homeassistant/components/goodwe/number.py @@ -128,7 +128,6 @@ class InverterNumberEntity(NumberEntity): async def async_set_native_value(self, value: float) -> None: """Set new value.""" - if self.entity_description.setter: - await self.entity_description.setter(self._inverter, int(value)) + await self.entity_description.setter(self._inverter, int(value)) self._attr_native_value = value self.async_write_ha_state() diff --git a/homeassistant/components/kostal_plenticore/sensor.py b/homeassistant/components/kostal_plenticore/sensor.py index 6555a35c8bc..29b42e88b50 100644 --- a/homeassistant/components/kostal_plenticore/sensor.py +++ b/homeassistant/components/kostal_plenticore/sensor.py @@ -797,4 +797,4 @@ class PlenticoreDataSensor(CoordinatorEntity, SensorEntity): raw_value = self.coordinator.data[self.module_id][self.data_id] - return self._formatter(raw_value) if self._formatter else raw_value + return self._formatter(raw_value) diff --git a/homeassistant/helpers/schema_config_entry_flow.py b/homeassistant/helpers/schema_config_entry_flow.py index 12ffa7cc101..413161eb150 100644 --- a/homeassistant/helpers/schema_config_entry_flow.py +++ b/homeassistant/helpers/schema_config_entry_flow.py @@ -134,7 +134,7 @@ class SchemaCommonFlowHandler: self._options.update(user_input) next_step_id: str = step_id - if form_step.next_step and (user_input is not None or form_step.schema is None): + if user_input is not None or form_step.schema is None: # Get next step next_step_id_or_end_flow = form_step.next_step(self._options) if next_step_id_or_end_flow is None: From 49eeeae51da329284070eb7b91ed6cc8078d2f19 Mon Sep 17 00:00:00 2001 From: HarvsG <11440490+HarvsG@users.noreply.github.com> Date: Mon, 26 Sep 2022 03:02:35 +0100 Subject: [PATCH 720/955] Fix Bayesian sensor to use negative observations (#67631) Co-authored-by: Diogo Gomes --- CODEOWNERS | 2 + .../components/bayesian/binary_sensor.py | 105 ++++--- .../components/bayesian/manifest.json | 2 +- .../components/bayesian/test_binary_sensor.py | 275 +++++++++++++----- 4 files changed, 278 insertions(+), 106 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 917a279228a..101c40370ef 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -127,6 +127,8 @@ build.json @home-assistant/supervisor /tests/components/baf/ @bdraco @jfroy /homeassistant/components/balboa/ @garbled1 /tests/components/balboa/ @garbled1 +/homeassistant/components/bayesian/ @HarvsG +/tests/components/bayesian/ @HarvsG /homeassistant/components/beewi_smartclim/ @alemuro /homeassistant/components/binary_sensor/ @home-assistant/core /tests/components/binary_sensor/ @home-assistant/core diff --git a/homeassistant/components/bayesian/binary_sensor.py b/homeassistant/components/bayesian/binary_sensor.py index 5641480ba98..73ebcc8b37e 100644 --- a/homeassistant/components/bayesian/binary_sensor.py +++ b/homeassistant/components/bayesian/binary_sensor.py @@ -16,6 +16,7 @@ from homeassistant.const import ( CONF_PLATFORM, CONF_STATE, CONF_VALUE_TEMPLATE, + STATE_UNAVAILABLE, STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant, callback @@ -60,7 +61,7 @@ NUMERIC_STATE_SCHEMA = vol.Schema( vol.Optional(CONF_ABOVE): vol.Coerce(float), vol.Optional(CONF_BELOW): vol.Coerce(float), vol.Required(CONF_P_GIVEN_T): vol.Coerce(float), - vol.Optional(CONF_P_GIVEN_F): vol.Coerce(float), + vol.Required(CONF_P_GIVEN_F): vol.Coerce(float), }, required=True, ) @@ -71,7 +72,7 @@ STATE_SCHEMA = vol.Schema( vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_TO_STATE): cv.string, vol.Required(CONF_P_GIVEN_T): vol.Coerce(float), - vol.Optional(CONF_P_GIVEN_F): vol.Coerce(float), + vol.Required(CONF_P_GIVEN_F): vol.Coerce(float), }, required=True, ) @@ -81,7 +82,7 @@ TEMPLATE_SCHEMA = vol.Schema( CONF_PLATFORM: CONF_TEMPLATE, vol.Required(CONF_VALUE_TEMPLATE): cv.template, vol.Required(CONF_P_GIVEN_T): vol.Coerce(float), - vol.Optional(CONF_P_GIVEN_F): vol.Coerce(float), + vol.Required(CONF_P_GIVEN_F): vol.Coerce(float), }, required=True, ) @@ -160,6 +161,7 @@ class BayesianBinarySensor(BinarySensorEntity): self.observation_handlers = { "numeric_state": self._process_numeric_state, "state": self._process_state, + "multi_state": self._process_multi_state, } async def async_added_to_hass(self) -> None: @@ -185,10 +187,6 @@ class BayesianBinarySensor(BinarySensorEntity): When a state changes, we must update our list of current observations, then calculate the new probability. """ - new_state = event.data.get("new_state") - - if new_state is None or new_state.state == STATE_UNKNOWN: - return entity = event.data.get("entity_id") @@ -210,7 +208,6 @@ class BayesianBinarySensor(BinarySensorEntity): template = track_template_result.template result = track_template_result.result entity = event and event.data.get("entity_id") - if isinstance(result, TemplateError): _LOGGER.error( "TemplateError('%s') " @@ -221,15 +218,12 @@ class BayesianBinarySensor(BinarySensorEntity): self.entity_id, ) - should_trigger = False + observation = None else: - should_trigger = result_as_boolean(result) + observation = result_as_boolean(result) for obs in self.observations_by_template[template]: - if should_trigger: - obs_entry = {"entity_id": entity, **obs} - else: - obs_entry = None + obs_entry = {"entity_id": entity, "observation": observation, **obs} self.current_observations[obs["id"]] = obs_entry if event: @@ -259,6 +253,7 @@ class BayesianBinarySensor(BinarySensorEntity): def _initialize_current_observations(self): local_observations = OrderedDict({}) + for entity in self.observations_by_entity: local_observations.update(self._record_entity_observations(entity)) return local_observations @@ -269,13 +264,13 @@ class BayesianBinarySensor(BinarySensorEntity): for entity_obs in self.observations_by_entity[entity]: platform = entity_obs["platform"] - should_trigger = self.observation_handlers[platform](entity_obs) - - if should_trigger: - obs_entry = {"entity_id": entity, **entity_obs} - else: - obs_entry = None + observation = self.observation_handlers[platform](entity_obs) + obs_entry = { + "entity_id": entity, + "observation": observation, + **entity_obs, + } local_observations[entity_obs["id"]] = obs_entry return local_observations @@ -285,11 +280,28 @@ class BayesianBinarySensor(BinarySensorEntity): for obs in self.current_observations.values(): if obs is not None: - prior = update_probability( - prior, - obs["prob_given_true"], - obs.get("prob_given_false", 1 - obs["prob_given_true"]), - ) + if obs["observation"] is True: + prior = update_probability( + prior, + obs["prob_given_true"], + obs["prob_given_false"], + ) + elif obs["observation"] is False: + prior = update_probability( + prior, + 1 - obs["prob_given_true"], + 1 - obs["prob_given_false"], + ) + elif obs["observation"] is None: + if obs["entity_id"] is not None: + _LOGGER.debug( + "Observation for entity '%s' returned None, it will not be used for Bayesian updating", + obs["entity_id"], + ) + else: + _LOGGER.debug( + "Observation for template entity returned None rather than a valid boolean, it will not be used for Bayesian updating", + ) return prior @@ -307,17 +319,21 @@ class BayesianBinarySensor(BinarySensorEntity): for all relevant observations to be looked up via their `entity_id`. """ - observations_by_entity = {} - for ind, obs in enumerate(self._observations): - obs["id"] = ind + observations_by_entity: dict[str, list[OrderedDict]] = {} + for i, obs in enumerate(self._observations): + obs["id"] = i if "entity_id" not in obs: continue + observations_by_entity.setdefault(obs["entity_id"], []).append(obs) - entity_ids = [obs["entity_id"]] - - for e_id in entity_ids: - observations_by_entity.setdefault(e_id, []).append(obs) + for li_of_dicts in observations_by_entity.values(): + if len(li_of_dicts) == 1: + continue + for ord_dict in li_of_dicts: + if ord_dict["platform"] != "state": + continue + ord_dict["platform"] = "multi_state" return observations_by_entity @@ -348,10 +364,12 @@ class BayesianBinarySensor(BinarySensorEntity): return observations_by_template def _process_numeric_state(self, entity_observation): - """Return True if numeric condition is met.""" + """Return True if numeric condition is met, return False if not, return None otherwise.""" entity = entity_observation["entity_id"] try: + if condition.state(self.hass, entity, [STATE_UNKNOWN, STATE_UNAVAILABLE]): + return None return condition.async_numeric_state( self.hass, entity, @@ -361,18 +379,31 @@ class BayesianBinarySensor(BinarySensorEntity): entity_observation, ) except ConditionError: - return False + return None def _process_state(self, entity_observation): """Return True if state conditions are met.""" entity = entity_observation["entity_id"] try: + if condition.state(self.hass, entity, [STATE_UNKNOWN, STATE_UNAVAILABLE]): + return None + return condition.state( self.hass, entity, entity_observation.get("to_state") ) except ConditionError: - return False + return None + + def _process_multi_state(self, entity_observation): + """Return True if state conditions are met.""" + entity = entity_observation["entity_id"] + + try: + if condition.state(self.hass, entity, entity_observation.get("to_state")): + return True + except ConditionError: + return None @property def extra_state_attributes(self): @@ -390,7 +421,9 @@ class BayesianBinarySensor(BinarySensorEntity): { obs.get("entity_id") for obs in self.current_observations.values() - if obs is not None and obs.get("entity_id") is not None + if obs is not None + and obs.get("entity_id") is not None + and obs.get("observation") is not None } ), ATTR_PROBABILITY: round(self.probability, 2), diff --git a/homeassistant/components/bayesian/manifest.json b/homeassistant/components/bayesian/manifest.json index 6a84beb1df6..1b5a466f0a2 100644 --- a/homeassistant/components/bayesian/manifest.json +++ b/homeassistant/components/bayesian/manifest.json @@ -2,7 +2,7 @@ "domain": "bayesian", "name": "Bayesian", "documentation": "https://www.home-assistant.io/integrations/bayesian", - "codeowners": [], + "codeowners": ["@HarvsG"], "quality_scale": "internal", "iot_class": "local_polling" } diff --git a/tests/components/bayesian/test_binary_sensor.py b/tests/components/bayesian/test_binary_sensor.py index 2f45e0e475e..357cacb4214 100644 --- a/tests/components/bayesian/test_binary_sensor.py +++ b/tests/components/bayesian/test_binary_sensor.py @@ -13,6 +13,7 @@ from homeassistant.const import ( SERVICE_RELOAD, STATE_OFF, STATE_ON, + STATE_UNAVAILABLE, STATE_UNKNOWN, ) from homeassistant.core import Context, callback @@ -56,7 +57,7 @@ async def test_load_values_when_added_to_hass(hass): async def test_unknown_state_does_not_influence_probability(hass): """Test that an unknown state does not change the output probability.""" - + prior = 0.2 config = { "binary_sensor": { "name": "Test_Binary", @@ -70,11 +71,12 @@ async def test_unknown_state_does_not_influence_probability(hass): "prob_given_false": 0.4, } ], - "prior": 0.2, + "prior": prior, "probability_threshold": 0.32, } } - + hass.states.async_set("sensor.test_monitored", "on") + await hass.async_block_till_done() hass.states.async_set("sensor.test_monitored", STATE_UNKNOWN) await hass.async_block_till_done() @@ -82,7 +84,8 @@ async def test_unknown_state_does_not_influence_probability(hass): await hass.async_block_till_done() state = hass.states.get("binary_sensor.test_binary") - assert state.attributes.get("observations") == [] + assert state.attributes.get("occurred_observation_entities") == [] + assert state.attributes.get("probability") == prior async def test_sensor_numeric_state(hass): @@ -97,7 +100,8 @@ async def test_sensor_numeric_state(hass): "entity_id": "sensor.test_monitored", "below": 10, "above": 5, - "prob_given_true": 0.6, + "prob_given_true": 0.7, + "prob_given_false": 0.4, }, { "platform": "numeric_state", @@ -105,7 +109,7 @@ async def test_sensor_numeric_state(hass): "below": 7, "above": 5, "prob_given_true": 0.9, - "prob_given_false": 0.1, + "prob_given_false": 0.2, }, ], "prior": 0.2, @@ -115,40 +119,61 @@ async def test_sensor_numeric_state(hass): assert await async_setup_component(hass, "binary_sensor", config) await hass.async_block_till_done() + hass.states.async_set("sensor.test_monitored", 6) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test_binary") + + assert state.attributes.get("occurred_observation_entities") == [ + "sensor.test_monitored" + ] + assert abs(state.attributes.get("probability") - 0.304) < 0.01 + # A = sensor.test_binary being ON + # B = sensor.test_monitored in the range [5, 10] + # Bayes theorum is P(A|B) = P(B|A) * P(A) / P(B|A)*P(A) + P(B|~A)*P(~A). + # Where P(B|A) is prob_given_true and P(B|~A) is prob_given_false + # Calculated using P(A) = 0.2, P(B|A) = 0.7, P(B|~A) = 0.4 -> 0.30 + hass.states.async_set("sensor.test_monitored", 4) await hass.async_block_till_done() state = hass.states.get("binary_sensor.test_binary") - assert [] == state.attributes.get("observations") - assert state.attributes.get("probability") == 0.2 + assert state.attributes.get("occurred_observation_entities") == [ + "sensor.test_monitored" + ] + assert abs(state.attributes.get("probability") - 0.111) < 0.01 + # As abve but since the value is equal to 4 then this is a negative observation (~B) where P(~B) == 1 - P(B) because B is binary + # We therefore want to calculate P(A|~B) so we use P(~B|A) (1-0.7) and P(~B|~A) (1-0.4) + # Calculated using bayes theorum where P(A) = 0.2, P(~B|A) = 1-0.7 (as negative observation), P(~B|notA) = 1-0.4 -> 0.11 assert state.state == "off" hass.states.async_set("sensor.test_monitored", 6) await hass.async_block_till_done() - hass.states.async_set("sensor.test_monitored", 4) - await hass.async_block_till_done() - hass.states.async_set("sensor.test_monitored", 6) hass.states.async_set("sensor.test_monitored1", 6) await hass.async_block_till_done() state = hass.states.get("binary_sensor.test_binary") - assert state.attributes.get("observations")[0]["prob_given_true"] == 0.6 + assert state.attributes.get("observations")[0]["prob_given_true"] == 0.7 assert state.attributes.get("observations")[1]["prob_given_true"] == 0.9 - assert state.attributes.get("observations")[1]["prob_given_false"] == 0.1 - assert round(abs(0.77 - state.attributes.get("probability")), 7) == 0 + assert state.attributes.get("observations")[1]["prob_given_false"] == 0.2 + assert abs(state.attributes.get("probability") - 0.663) < 0.01 + # Here we have two positive observations as both are in range. We do a 2-step bayes. The output of the first is used as the (updated) prior in the second. + # 1st step P(A) = 0.2, P(B|A) = 0.7, P(B|notA) = 0.4 -> 0.304 + # 2nd update: P(A) = 0.304, P(B|A) = 0.9, P(B|notA) = 0.2 -> 0.663 assert state.state == "on" - hass.states.async_set("sensor.test_monitored", 6) hass.states.async_set("sensor.test_monitored1", 0) await hass.async_block_till_done() hass.states.async_set("sensor.test_monitored", 4) await hass.async_block_till_done() state = hass.states.get("binary_sensor.test_binary") - assert state.attributes.get("probability") == 0.2 + assert abs(state.attributes.get("probability") - 0.0153) < 0.01 + # Calculated using bayes theorum where P(A) = 0.2, P(~B|A) = 0.3, P(~B|notA) = 0.6 -> 0.11 + # 2nd update: P(A) = 0.111, P(~B|A) = 0.1, P(~B|notA) = 0.8 assert state.state == "off" @@ -162,6 +187,7 @@ async def test_sensor_numeric_state(hass): async def test_sensor_state(hass): """Test sensor on state platform observations.""" + prior = 0.2 config = { "binary_sensor": { "name": "Test_Binary", @@ -175,7 +201,7 @@ async def test_sensor_state(hass): "prob_given_false": 0.4, } ], - "prior": 0.2, + "prior": prior, "probability_threshold": 0.32, } } @@ -184,36 +210,51 @@ async def test_sensor_state(hass): await hass.async_block_till_done() hass.states.async_set("sensor.test_monitored", "on") - + await hass.async_block_till_done() state = hass.states.get("binary_sensor.test_binary") - assert [] == state.attributes.get("observations") - assert state.attributes.get("probability") == 0.2 - + assert state.attributes.get("occurred_observation_entities") == [ + "sensor.test_monitored" + ] + assert state.attributes.get("observations")[0]["prob_given_true"] == 0.8 + assert state.attributes.get("observations")[0]["prob_given_false"] == 0.4 + assert abs(0.0769 - state.attributes.get("probability")) < 0.01 + # Calculated using bayes theorum where P(A) = 0.2, P(~B|A) = 0.2 (as negative observation), P(~B|notA) = 0.6 assert state.state == "off" hass.states.async_set("sensor.test_monitored", "off") await hass.async_block_till_done() - hass.states.async_set("sensor.test_monitored", "on") - await hass.async_block_till_done() - hass.states.async_set("sensor.test_monitored", "off") - await hass.async_block_till_done() - state = hass.states.get("binary_sensor.test_binary") - assert state.attributes.get("observations")[0]["prob_given_true"] == 0.8 - assert state.attributes.get("observations")[0]["prob_given_false"] == 0.4 - assert round(abs(0.33 - state.attributes.get("probability")), 7) == 0 + assert state.attributes.get("occurred_observation_entities") == [ + "sensor.test_monitored" + ] + assert abs(0.33 - state.attributes.get("probability")) < 0.01 + # Calculated using bayes theorum where P(A) = 0.2, P(~B|A) = 0.8 (as negative observation), P(~B|notA) = 0.4 assert state.state == "on" - hass.states.async_set("sensor.test_monitored", "off") + hass.states.async_remove("sensor.test_monitored") await hass.async_block_till_done() - hass.states.async_set("sensor.test_monitored", "on") - await hass.async_block_till_done() - state = hass.states.get("binary_sensor.test_binary") - assert round(abs(0.2 - state.attributes.get("probability")), 7) == 0 + assert state.attributes.get("occurred_observation_entities") == [] + assert abs(prior - state.attributes.get("probability")) < 0.01 + assert state.state == "off" + + hass.states.async_set("sensor.test_monitored", STATE_UNAVAILABLE) + await hass.async_block_till_done() + state = hass.states.get("binary_sensor.test_binary") + + assert state.attributes.get("occurred_observation_entities") == [] + assert abs(prior - state.attributes.get("probability")) < 0.01 + assert state.state == "off" + + hass.states.async_set("sensor.test_monitored", STATE_UNKNOWN) + await hass.async_block_till_done() + state = hass.states.get("binary_sensor.test_binary") + + assert state.attributes.get("occurred_observation_entities") == [] + assert abs(prior - state.attributes.get("probability")) < 0.01 assert state.state == "off" @@ -243,32 +284,29 @@ async def test_sensor_value_template(hass): state = hass.states.get("binary_sensor.test_binary") - assert [] == state.attributes.get("observations") - assert state.attributes.get("probability") == 0.2 + assert state.attributes.get("occurred_observation_entities") == [] + assert abs(0.0769 - state.attributes.get("probability")) < 0.01 + # Calculated using bayes theorum where P(A) = 0.2, P(~B|A) = 0.2 (as negative observation), P(~B|notA) = 0.6 assert state.state == "off" - hass.states.async_set("sensor.test_monitored", "off") - await hass.async_block_till_done() - hass.states.async_set("sensor.test_monitored", "on") - await hass.async_block_till_done() hass.states.async_set("sensor.test_monitored", "off") await hass.async_block_till_done() state = hass.states.get("binary_sensor.test_binary") assert state.attributes.get("observations")[0]["prob_given_true"] == 0.8 assert state.attributes.get("observations")[0]["prob_given_false"] == 0.4 - assert round(abs(0.33 - state.attributes.get("probability")), 7) == 0 + assert abs(0.33333 - state.attributes.get("probability")) < 0.01 + # Calculated using bayes theorum where P(A) = 0.2, P(B|A) = 0.8, P(B|notA) = 0.4 assert state.state == "on" - hass.states.async_set("sensor.test_monitored", "off") - await hass.async_block_till_done() hass.states.async_set("sensor.test_monitored", "on") await hass.async_block_till_done() state = hass.states.get("binary_sensor.test_binary") - assert round(abs(0.2 - state.attributes.get("probability")), 7) == 0 + assert abs(0.076923 - state.attributes.get("probability")) < 0.01 + # Calculated using bayes theorum where P(A) = 0.2, P(~B|A) = 0.2 (as negative observation), P(~B|notA) = 0.6 assert state.state == "off" @@ -285,6 +323,7 @@ async def test_threshold(hass): "entity_id": "sensor.test_monitored", "to_state": "on", "prob_given_true": 1.0, + "prob_given_false": 0.0, } ], "prior": 0.5, @@ -305,7 +344,14 @@ async def test_threshold(hass): async def test_multiple_observations(hass): - """Test sensor with multiple observations of same entity.""" + """ + Test sensor with multiple observations of same entity. + + these entries should be labelled as 'multi_state' and negative observations ignored - as the outcome is not known to be binary. + Before the merge of #67631 this practice was a common work-around for bayesian's ignoring of negative observations, + this also preserves that function + """ + config = { "binary_sensor": { "name": "Test_Binary", @@ -323,7 +369,7 @@ async def test_multiple_observations(hass): "entity_id": "sensor.test_monitored", "to_state": "red", "prob_given_true": 0.2, - "prob_given_false": 0.4, + "prob_given_false": 0.6, }, ], "prior": 0.2, @@ -335,40 +381,118 @@ async def test_multiple_observations(hass): await hass.async_block_till_done() hass.states.async_set("sensor.test_monitored", "off") + await hass.async_block_till_done() state = hass.states.get("binary_sensor.test_binary") - for key, attrs in state.attributes.items(): + for _, attrs in state.attributes.items(): json.dumps(attrs) - assert [] == state.attributes.get("observations") + assert state.attributes.get("occurred_observation_entities") == [] assert state.attributes.get("probability") == 0.2 + # probability should be the same as the prior as negative observations are ignored in multi-state assert state.state == "off" - hass.states.async_set("sensor.test_monitored", "blue") - await hass.async_block_till_done() - hass.states.async_set("sensor.test_monitored", "off") - await hass.async_block_till_done() hass.states.async_set("sensor.test_monitored", "blue") await hass.async_block_till_done() state = hass.states.get("binary_sensor.test_binary") + assert state.attributes.get("occurred_observation_entities") == [ + "sensor.test_monitored" + ] assert state.attributes.get("observations")[0]["prob_given_true"] == 0.8 assert state.attributes.get("observations")[0]["prob_given_false"] == 0.4 assert round(abs(0.33 - state.attributes.get("probability")), 7) == 0 + # Calculated using bayes theorum where P(A) = 0.2, P(B|A) = 0.8, P(B|notA) = 0.4 assert state.state == "on" - hass.states.async_set("sensor.test_monitored", "blue") - await hass.async_block_till_done() hass.states.async_set("sensor.test_monitored", "red") await hass.async_block_till_done() state = hass.states.get("binary_sensor.test_binary") - assert round(abs(0.11 - state.attributes.get("probability")), 7) == 0 + assert abs(0.076923 - state.attributes.get("probability")) < 0.01 + # Calculated using bayes theorum where P(A) = 0.2, P(B|A) = 0.2, P(B|notA) = 0.6 assert state.state == "off" + assert state.attributes.get("observations")[0]["platform"] == "multi_state" + assert state.attributes.get("observations")[1]["platform"] == "multi_state" + + +async def test_multiple_numeric_observations(hass): + """Test sensor with multiple numeric observations of same entity.""" + + config = { + "binary_sensor": { + "platform": "bayesian", + "name": "Test_Binary", + "observations": [ + { + "platform": "numeric_state", + "entity_id": "sensor.test_monitored", + "below": 10, + "above": 0, + "prob_given_true": 0.4, + "prob_given_false": 0.0001, + }, + { + "platform": "numeric_state", + "entity_id": "sensor.test_monitored", + "below": 100, + "above": 30, + "prob_given_true": 0.6, + "prob_given_false": 0.0001, + }, + ], + "prior": 0.1, + } + } + + assert await async_setup_component(hass, "binary_sensor", config) + await hass.async_block_till_done() + + hass.states.async_set("sensor.test_monitored", STATE_UNKNOWN) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test_binary") + + for _, attrs in state.attributes.items(): + json.dumps(attrs) + assert state.attributes.get("occurred_observation_entities") == [] + assert state.attributes.get("probability") == 0.1 + # No observations made so probability should be the prior + + assert state.state == "off" + + hass.states.async_set("sensor.test_monitored", 20) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test_binary") + + assert state.attributes.get("occurred_observation_entities") == [ + "sensor.test_monitored" + ] + assert round(abs(0.026 - state.attributes.get("probability")), 7) < 0.01 + # Step 1 Calculated where P(A) = 0.1, P(~B|A) = 0.6 (negative obs), P(~B|notA) = 0.9999 -> 0.0625 + # Step 2 P(A) = 0.0625, P(B|A) = 0.4 (negative obs), P(B|notA) = 0.9999 -> 0.26 + + assert state.state == "off" + + hass.states.async_set("sensor.test_monitored", 35) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test_binary") + assert state.attributes.get("occurred_observation_entities") == [ + "sensor.test_monitored" + ] + assert abs(1 - state.attributes.get("probability")) < 0.01 + # Step 1 Calculated where P(A) = 0.1, P(~B|A) = 0.6 (negative obs), P(~B|notA) = 0.9999 -> 0.0625 + # Step 2 P(A) = 0.0625, P(B|A) = 0.6, P(B|notA) = 0.0001 -> 0.9975 + + assert state.state == "on" + assert state.attributes.get("observations")[0]["platform"] == "numeric_state" + assert state.attributes.get("observations")[1]["platform"] == "numeric_state" async def test_probability_updates(hass): @@ -377,8 +501,8 @@ async def test_probability_updates(hass): prob_given_false = [0.7, 0.4, 0.2] prior = 0.5 - for pt, pf in zip(prob_given_true, prob_given_false): - prior = bayesian.update_probability(prior, pt, pf) + for p_t, p_f in zip(prob_given_true, prob_given_false): + prior = bayesian.update_probability(prior, p_t, p_f) assert round(abs(0.720000 - prior), 7) == 0 @@ -386,8 +510,8 @@ async def test_probability_updates(hass): prob_given_false = [0.6, 0.4, 0.2] prior = 0.7 - for pt, pf in zip(prob_given_true, prob_given_false): - prior = bayesian.update_probability(prior, pt, pf) + for p_t, p_f in zip(prob_given_true, prob_given_false): + prior = bayesian.update_probability(prior, p_t, p_f) assert round(abs(0.9130434782608695 - prior), 7) == 0 @@ -410,6 +534,7 @@ async def test_observed_entities(hass): "platform": "template", "value_template": "{{is_state('sensor.test_monitored1','on') and is_state('sensor.test_monitored','off')}}", "prob_given_true": 0.9, + "prob_given_false": 0.1, }, ], "prior": 0.2, @@ -426,7 +551,9 @@ async def test_observed_entities(hass): await hass.async_block_till_done() state = hass.states.get("binary_sensor.test_binary") - assert [] == state.attributes.get("occurred_observation_entities") + assert state.attributes.get("occurred_observation_entities") == [ + "sensor.test_monitored" + ] hass.states.async_set("sensor.test_monitored", "off") await hass.async_block_till_done() @@ -463,6 +590,7 @@ async def test_state_attributes_are_serializable(hass): "platform": "template", "value_template": "{{is_state('sensor.test_monitored1','on') and is_state('sensor.test_monitored','off')}}", "prob_given_true": 0.9, + "prob_given_false": 0.1, }, ], "prior": 0.2, @@ -479,15 +607,17 @@ async def test_state_attributes_are_serializable(hass): await hass.async_block_till_done() state = hass.states.get("binary_sensor.test_binary") - assert [] == state.attributes.get("occurred_observation_entities") + assert state.attributes.get("occurred_observation_entities") == [ + "sensor.test_monitored" + ] hass.states.async_set("sensor.test_monitored", "off") await hass.async_block_till_done() state = hass.states.get("binary_sensor.test_binary") - assert ["sensor.test_monitored"] == state.attributes.get( - "occurred_observation_entities" - ) + assert state.attributes.get("occurred_observation_entities") == [ + "sensor.test_monitored" + ] hass.states.async_set("sensor.test_monitored1", "on") await hass.async_block_till_done() @@ -497,7 +627,7 @@ async def test_state_attributes_are_serializable(hass): state.attributes.get("occurred_observation_entities") ) - for key, attrs in state.attributes.items(): + for _, attrs in state.attributes.items(): json.dumps(attrs) @@ -512,6 +642,7 @@ async def test_template_error(hass, caplog): "platform": "template", "value_template": "{{ xyz + 1 }}", "prob_given_true": 0.9, + "prob_given_false": 0.1, }, ], "prior": 0.2, @@ -633,11 +764,16 @@ async def test_monitored_sensor_goes_away(hass): await hass.async_block_till_done() assert hass.states.get("binary_sensor.test_binary").state == "on" + # Calculated using bayes theorum where P(A) = 0.2, P(B|A) = 0.9, P(B|notA) = 0.4 -> 0.36 (>0.32) hass.states.async_remove("sensor.test_monitored") await hass.async_block_till_done() - assert hass.states.get("binary_sensor.test_binary").state == "on" + assert ( + hass.states.get("binary_sensor.test_binary").attributes.get("probability") + == 0.2 + ) + assert hass.states.get("binary_sensor.test_binary").state == "off" async def test_reload(hass): @@ -696,7 +832,8 @@ async def test_template_triggers(hass): { "platform": "template", "value_template": "{{ states.input_boolean.test.state }}", - "prob_given_true": 1999.9, + "prob_given_true": 1.0, + "prob_given_false": 0.0, }, ], "prior": 0.2, @@ -735,8 +872,8 @@ async def test_state_triggers(hass): "platform": "state", "entity_id": "sensor.test_monitored", "to_state": "off", - "prob_given_true": 999.9, - "prob_given_false": 999.4, + "prob_given_true": 0.9999, + "prob_given_false": 0.9994, }, ], "prior": 0.2, From 82bab545df3d18afe8e561652790e80857bd2be5 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sun, 25 Sep 2022 22:12:07 -0400 Subject: [PATCH 721/955] Add missing doc strings and requirements in Google Sheets (#78616) --- CODEOWNERS | 1 + homeassistant/components/google_sheets/strings.json | 4 ++++ homeassistant/components/google_sheets/translations/en.json | 4 ++++ requirements_test_all.txt | 3 +++ tests/components/google_sheets/__init__.py | 1 + 5 files changed, 13 insertions(+) create mode 100644 tests/components/google_sheets/__init__.py diff --git a/CODEOWNERS b/CODEOWNERS index 101c40370ef..50d62976dfb 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -420,6 +420,7 @@ build.json @home-assistant/supervisor /tests/components/google_assistant/ @home-assistant/cloud /homeassistant/components/google_cloud/ @lufton /homeassistant/components/google_sheets/ @tkdrob +/tests/components/google_sheets/ @tkdrob /homeassistant/components/google_travel_time/ @eifinger /tests/components/google_travel_time/ @eifinger /homeassistant/components/govee_ble/ @bdraco diff --git a/homeassistant/components/google_sheets/strings.json b/homeassistant/components/google_sheets/strings.json index 858f6856954..2170f6e4c1d 100644 --- a/homeassistant/components/google_sheets/strings.json +++ b/homeassistant/components/google_sheets/strings.json @@ -4,6 +4,10 @@ "pick_implementation": { "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]" }, + "reauth_confirm": { + "title": "[%key:common::config_flow::title::reauth%]", + "description": "The Google Sheets integration needs to re-authenticate your account" + }, "auth": { "title": "Link Google Account" } diff --git a/homeassistant/components/google_sheets/translations/en.json b/homeassistant/components/google_sheets/translations/en.json index c7348a0fa40..fff3cb88d68 100644 --- a/homeassistant/components/google_sheets/translations/en.json +++ b/homeassistant/components/google_sheets/translations/en.json @@ -25,6 +25,10 @@ }, "pick_implementation": { "title": "Pick Authentication Method" + }, + "reauth_confirm": { + "description": "The Google Sheets integration needs to re-authenticate your account", + "title": "Reauthenticate Integration" } } } diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6a2182faf87..0b5586e63d9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -595,6 +595,9 @@ gridnet==4.0.0 # homeassistant.components.growatt_server growattServer==1.2.2 +# homeassistant.components.google_sheets +gspread==5.5.0 + # homeassistant.components.profiler guppy3==3.1.2 diff --git a/tests/components/google_sheets/__init__.py b/tests/components/google_sheets/__init__.py new file mode 100644 index 00000000000..dd9d801f239 --- /dev/null +++ b/tests/components/google_sheets/__init__.py @@ -0,0 +1 @@ +"""Tests for the Google Sheets integration.""" From bd01f90d42184666896f7c6161d842493bacf911 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sun, 25 Sep 2022 22:40:34 -0400 Subject: [PATCH 722/955] Migrate attributes to sensors in Litter-Robot (#78580) --- .../components/litterrobot/__init__.py | 18 ++++ .../components/litterrobot/binary_sensor.py | 95 +++++++++++++++++++ .../components/litterrobot/strings.json | 6 ++ .../litterrobot/translations/en.json | 8 +- .../litterrobot/test_binary_sensor.py | 31 ++++++ 5 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/litterrobot/binary_sensor.py create mode 100644 tests/components/litterrobot/test_binary_sensor.py diff --git a/homeassistant/components/litterrobot/__init__.py b/homeassistant/components/litterrobot/__init__.py index d4a4f3bfe91..cf14239b22d 100644 --- a/homeassistant/components/litterrobot/__init__.py +++ b/homeassistant/components/litterrobot/__init__.py @@ -6,12 +6,15 @@ from pylitterbot import FeederRobot, LitterRobot, LitterRobot3, Robot from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue +from homeassistant.helpers.typing import ConfigType from .const import DOMAIN from .hub import LitterRobotHub PLATFORMS_BY_TYPE = { Robot: ( + Platform.BINARY_SENSOR, Platform.SELECT, Platform.SENSOR, Platform.SWITCH, @@ -33,6 +36,21 @@ def get_platforms_for_robots(robots: list[Robot]) -> set[Platform]: } +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the Litter-Robot integration.""" + async_create_issue( + hass, + DOMAIN, + "migrated_attributes", + breaks_in_ha_version="2022.12.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="migrated_attributes", + ) + + return True + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Litter-Robot from a config entry.""" hass.data.setdefault(DOMAIN, {}) diff --git a/homeassistant/components/litterrobot/binary_sensor.py b/homeassistant/components/litterrobot/binary_sensor.py new file mode 100644 index 00000000000..781cfb73b7f --- /dev/null +++ b/homeassistant/components/litterrobot/binary_sensor.py @@ -0,0 +1,95 @@ +"""Support for Litter-Robot binary sensors.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import Generic + +from pylitterbot import LitterRobot, Robot + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +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 .const import DOMAIN +from .entity import LitterRobotEntity, _RobotT +from .hub import LitterRobotHub + + +@dataclass +class RequiredKeysMixin(Generic[_RobotT]): + """A class that describes robot binary sensor entity required keys.""" + + is_on_fn: Callable[[_RobotT], bool] + + +@dataclass +class RobotBinarySensorEntityDescription( + BinarySensorEntityDescription, RequiredKeysMixin[_RobotT] +): + """A class that describes robot binary sensor entities.""" + + +class LitterRobotBinarySensorEntity(LitterRobotEntity[_RobotT], BinarySensorEntity): + """Litter-Robot binary sensor entity.""" + + entity_description: RobotBinarySensorEntityDescription[_RobotT] + + @property + def is_on(self) -> bool: + """Return the state.""" + return self.entity_description.is_on_fn(self.robot) + + +BINARY_SENSOR_MAP: dict[type[Robot], tuple[RobotBinarySensorEntityDescription, ...]] = { + LitterRobot: ( + RobotBinarySensorEntityDescription[LitterRobot]( + key="sleeping", + name="Sleeping", + icon="mdi:sleep", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + is_on_fn=lambda robot: robot.is_sleeping, + ), + RobotBinarySensorEntityDescription[LitterRobot]( + key="sleep_mode", + name="Sleep mode", + icon="mdi:sleep", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + is_on_fn=lambda robot: robot.sleep_mode_enabled, + ), + ), + Robot: ( + RobotBinarySensorEntityDescription[Robot]( + key="power_status", + name="Power status", + device_class=BinarySensorDeviceClass.PLUG, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + is_on_fn=lambda robot: robot.power_status == "AC", + ), + ), +} + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Litter-Robot binary sensors using config entry.""" + hub: LitterRobotHub = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + LitterRobotBinarySensorEntity(robot=robot, hub=hub, description=description) + for robot in hub.account.robots + for robot_type, entity_descriptions in BINARY_SENSOR_MAP.items() + if isinstance(robot, robot_type) + for description in entity_descriptions + ) diff --git a/homeassistant/components/litterrobot/strings.json b/homeassistant/components/litterrobot/strings.json index 140a0308188..f2256249b8e 100644 --- a/homeassistant/components/litterrobot/strings.json +++ b/homeassistant/components/litterrobot/strings.json @@ -24,5 +24,11 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } + }, + "issues": { + "migrated_attributes": { + "title": "Litter-Robot attributes are now their own sensors", + "description": "The vacuum entity attributes are now available as diagnostic sensors.\n\nPlease adjust any automations or scripts you may have that use these attributes." + } } } diff --git a/homeassistant/components/litterrobot/translations/en.json b/homeassistant/components/litterrobot/translations/en.json index 3d6ab4dfaaa..3d500bb0f10 100644 --- a/homeassistant/components/litterrobot/translations/en.json +++ b/homeassistant/components/litterrobot/translations/en.json @@ -24,5 +24,11 @@ } } } + }, + "issues": { + "migrated_attributes": { + "title": "Litter-Robot attributes are now their own sensors", + "description": "The vacuum entity attributes are now available as diagnostic sensors.\n\nPlease adjust any automations or scripts you may have that use these attributes." + } } -} \ No newline at end of file +} diff --git a/tests/components/litterrobot/test_binary_sensor.py b/tests/components/litterrobot/test_binary_sensor.py new file mode 100644 index 00000000000..cbcdd447760 --- /dev/null +++ b/tests/components/litterrobot/test_binary_sensor.py @@ -0,0 +1,31 @@ +"""Test the Litter-Robot binary sensor entity.""" +from unittest.mock import AsyncMock, MagicMock + +import pytest + +from homeassistant.components.binary_sensor import ( + DOMAIN as PLATFORM_DOMAIN, + BinarySensorDeviceClass, +) +from homeassistant.const import ATTR_DEVICE_CLASS +from homeassistant.core import HomeAssistant + +from .conftest import setup_integration + + +@pytest.mark.freeze_time("2022-09-18 23:00:44+00:00") +async def test_binary_sensors( + hass: HomeAssistant, + mock_account: MagicMock, + entity_registry_enabled_by_default: AsyncMock, +) -> None: + """Tests binary sensors.""" + await setup_integration(hass, mock_account, PLATFORM_DOMAIN) + + state = hass.states.get("binary_sensor.test_sleeping") + assert state.state == "off" + state = hass.states.get("binary_sensor.test_sleep_mode") + assert state.state == "on" + state = hass.states.get("binary_sensor.test_power_status") + assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.PLUG + assert state.state == "on" From f26fadbdfc45c4fb147997d4efc7e4929d2b27ed Mon Sep 17 00:00:00 2001 From: Justin Sherman Date: Sun, 25 Sep 2022 20:08:31 -0700 Subject: [PATCH 723/955] Add range to min_max (#78282) --- .../components/min_max/config_flow.py | 1 + homeassistant/components/min_max/sensor.py | 17 ++++++ tests/components/min_max/test_sensor.py | 56 ++++++++++++++++++- 3 files changed, 73 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/min_max/config_flow.py b/homeassistant/components/min_max/config_flow.py index 2114a5406d0..0fed67f15b9 100644 --- a/homeassistant/components/min_max/config_flow.py +++ b/homeassistant/components/min_max/config_flow.py @@ -22,6 +22,7 @@ _STATISTIC_MEASURES = [ selector.SelectOptionDict(value="mean", label="Arithmetic mean"), selector.SelectOptionDict(value="median", label="Median"), selector.SelectOptionDict(value="last", label="Most recently updated"), + selector.SelectOptionDict(value="range", label="Statistical range"), ] diff --git a/homeassistant/components/min_max/sensor.py b/homeassistant/components/min_max/sensor.py index 615aebc8e39..0f53875861d 100644 --- a/homeassistant/components/min_max/sensor.py +++ b/homeassistant/components/min_max/sensor.py @@ -39,6 +39,7 @@ ATTR_MEAN = "mean" ATTR_MEDIAN = "median" ATTR_LAST = "last" ATTR_LAST_ENTITY_ID = "last_entity_id" +ATTR_RANGE = "range" ICON = "mdi:calculator" @@ -48,6 +49,7 @@ SENSOR_TYPES = { ATTR_MEAN: "mean", ATTR_MEDIAN: "median", ATTR_LAST: "last", + ATTR_RANGE: "range", } SENSOR_TYPE_TO_ATTR = {v: k for k, v in SENSOR_TYPES.items()} @@ -158,6 +160,19 @@ def calc_median(sensor_values, round_digits): return round(statistics.median(result), round_digits) +def calc_range(sensor_values, round_digits): + """Calculate range value, honoring unknown states.""" + result = [ + sensor_value + for _, sensor_value in sensor_values + if sensor_value not in [STATE_UNKNOWN, STATE_UNAVAILABLE] + ] + + if not result: + return None + return round(max(result) - min(result), round_digits) + + class MinMaxSensor(SensorEntity): """Representation of a min/max sensor.""" @@ -180,6 +195,7 @@ class MinMaxSensor(SensorEntity): self._unit_of_measurement = None self._unit_of_measurement_mismatch = False self.min_value = self.max_value = self.mean = self.last = self.median = None + self.range = None self.min_entity_id = self.max_entity_id = self.last_entity_id = None self.count_sensors = len(self._entity_ids) self.states = {} @@ -288,3 +304,4 @@ class MinMaxSensor(SensorEntity): self.max_entity_id, self.max_value = calc_max(sensor_values) self.mean = calc_mean(sensor_values, self._round_digits) self.median = calc_median(sensor_values, self._round_digits) + self.range = calc_range(sensor_values, self._round_digits) diff --git a/tests/components/min_max/test_sensor.py b/tests/components/min_max/test_sensor.py index 72728ac20b6..47435dfbaf3 100644 --- a/tests/components/min_max/test_sensor.py +++ b/tests/components/min_max/test_sensor.py @@ -26,6 +26,8 @@ MEAN = round(sum(VALUES) / COUNT, 2) MEAN_1_DIGIT = round(sum(VALUES) / COUNT, 1) MEAN_4_DIGITS = round(sum(VALUES) / COUNT, 4) MEDIAN = round(statistics.median(VALUES), 2) +RANGE_1_DIGIT = round(max(VALUES) - min(VALUES), 1) +RANGE_4_DIGITS = round(max(VALUES) - min(VALUES), 4) async def test_default_name_sensor(hass): @@ -160,7 +162,7 @@ async def test_mean_1_digit_sensor(hass): async def test_mean_4_digit_sensor(hass): - """Test the mean with 1-digit precision sensor.""" + """Test the mean with 4-digit precision sensor.""" config = { "sensor": { "platform": "min_max", @@ -211,6 +213,58 @@ async def test_median_sensor(hass): assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT +async def test_range_4_digit_sensor(hass): + """Test the range with 4-digit precision sensor.""" + config = { + "sensor": { + "platform": "min_max", + "name": "test_range", + "type": "range", + "round_digits": 4, + "entity_ids": ["sensor.test_1", "sensor.test_2", "sensor.test_3"], + } + } + + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() + + entity_ids = config["sensor"]["entity_ids"] + + for entity_id, value in dict(zip(entity_ids, VALUES)).items(): + hass.states.async_set(entity_id, value) + await hass.async_block_till_done() + + state = hass.states.get("sensor.test_range") + + assert str(float(RANGE_4_DIGITS)) == state.state + + +async def test_range_1_digit_sensor(hass): + """Test the range with 1-digit precision sensor.""" + config = { + "sensor": { + "platform": "min_max", + "name": "test_range", + "type": "range", + "round_digits": 1, + "entity_ids": ["sensor.test_1", "sensor.test_2", "sensor.test_3"], + } + } + + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() + + entity_ids = config["sensor"]["entity_ids"] + + for entity_id, value in dict(zip(entity_ids, VALUES)).items(): + hass.states.async_set(entity_id, value) + await hass.async_block_till_done() + + state = hass.states.get("sensor.test_range") + + assert str(float(RANGE_1_DIGIT)) == state.state + + async def test_not_enough_sensor_value(hass): """Test that there is nothing done if not enough values available.""" config = { From 7aa53feff4ad347dbca228447464a95d70e48f93 Mon Sep 17 00:00:00 2001 From: Tom Puttemans Date: Mon, 26 Sep 2022 05:15:50 +0200 Subject: [PATCH 724/955] Add config flow and MQTT autodiscover to dsmr_reader integration (#71617) Co-authored-by: J. Nick Koston Co-authored-by: G Johansson Co-authored-by: Paulus Schoutsen --- .coveragerc | 4 +- CODEOWNERS | 3 +- .../components/dsmr_reader/__init__.py | 18 +++++++ .../components/dsmr_reader/config_flow.py | 40 ++++++++++++++++ homeassistant/components/dsmr_reader/const.py | 3 ++ .../components/dsmr_reader/manifest.json | 4 +- .../components/dsmr_reader/sensor.py | 31 ++++++++++-- .../components/dsmr_reader/strings.json | 18 +++++++ .../dsmr_reader/translations/en.json | 18 +++++++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/mqtt.py | 3 ++ tests/components/dsmr_reader/__init__.py | 1 + .../dsmr_reader/test_config_flow.py | 47 +++++++++++++++++++ 13 files changed, 185 insertions(+), 6 deletions(-) create mode 100644 homeassistant/components/dsmr_reader/config_flow.py create mode 100644 homeassistant/components/dsmr_reader/const.py create mode 100644 homeassistant/components/dsmr_reader/strings.json create mode 100644 homeassistant/components/dsmr_reader/translations/en.json create mode 100644 tests/components/dsmr_reader/__init__.py create mode 100644 tests/components/dsmr_reader/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 369944d60b0..44f8a54fe7a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -238,7 +238,9 @@ omit = homeassistant/components/doorbird/util.py homeassistant/components/dovado/* homeassistant/components/downloader/* - homeassistant/components/dsmr_reader/* + homeassistant/components/dsmr_reader/__init__.py + homeassistant/components/dsmr_reader/definitions.py + homeassistant/components/dsmr_reader/sensor.py homeassistant/components/dte_energy_bridge/sensor.py homeassistant/components/dublin_bus_transport/sensor.py homeassistant/components/dunehd/__init__.py diff --git a/CODEOWNERS b/CODEOWNERS index 50d62976dfb..3d610993dfe 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -263,7 +263,8 @@ build.json @home-assistant/supervisor /tests/components/doorbird/ @oblogic7 @bdraco @flacjacket /homeassistant/components/dsmr/ @Robbie1221 @frenck /tests/components/dsmr/ @Robbie1221 @frenck -/homeassistant/components/dsmr_reader/ @depl0y +/homeassistant/components/dsmr_reader/ @depl0y @glodenox +/tests/components/dsmr_reader/ @depl0y @glodenox /homeassistant/components/dunehd/ @bieniu /tests/components/dunehd/ @bieniu /homeassistant/components/dwd_weather_warnings/ @runningman84 @stephan192 @Hummel95 diff --git a/homeassistant/components/dsmr_reader/__init__.py b/homeassistant/components/dsmr_reader/__init__.py index 946be91d1a5..89b0da699e5 100644 --- a/homeassistant/components/dsmr_reader/__init__.py +++ b/homeassistant/components/dsmr_reader/__init__.py @@ -1 +1,19 @@ """The DSMR Reader component.""" + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +PLATFORMS = [Platform.SENSOR] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up the DSMR Reader integration.""" + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload the DSMR Reader integration.""" + # no data stored in hass.data + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/dsmr_reader/config_flow.py b/homeassistant/components/dsmr_reader/config_flow.py new file mode 100644 index 00000000000..2f08894d125 --- /dev/null +++ b/homeassistant/components/dsmr_reader/config_flow.py @@ -0,0 +1,40 @@ +"""Config flow to configure DSMR Reader.""" +from __future__ import annotations + +from collections.abc import Awaitable +import logging +from typing import Any + +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.config_entry_flow import DiscoveryFlowHandler + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def _async_has_devices(_: HomeAssistant) -> bool: + """MQTT is set as dependency, so that should be sufficient.""" + return True + + +class DsmrReaderFlowHandler(DiscoveryFlowHandler[Awaitable[bool]], domain=DOMAIN): + """Handle DSMR Reader config flow. The MQTT step is inherited from the parent class.""" + + VERSION = 1 + + def __init__(self) -> None: + """Set up the config flow.""" + super().__init__(DOMAIN, "DSMR Reader", _async_has_devices) + + async def async_step_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm setup.""" + if user_input is None: + return self.async_show_form( + step_id="confirm", + ) + + return await super().async_step_confirm(user_input) diff --git a/homeassistant/components/dsmr_reader/const.py b/homeassistant/components/dsmr_reader/const.py new file mode 100644 index 00000000000..1f1679028d5 --- /dev/null +++ b/homeassistant/components/dsmr_reader/const.py @@ -0,0 +1,3 @@ +"""Constant values for DSMR Reader.""" + +DOMAIN = "dsmr_reader" diff --git a/homeassistant/components/dsmr_reader/manifest.json b/homeassistant/components/dsmr_reader/manifest.json index 0b631e790ed..df68e183fdf 100644 --- a/homeassistant/components/dsmr_reader/manifest.json +++ b/homeassistant/components/dsmr_reader/manifest.json @@ -1,8 +1,10 @@ { "domain": "dsmr_reader", "name": "DSMR Reader", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dsmr_reader", "dependencies": ["mqtt"], - "codeowners": ["@depl0y"], + "mqtt": ["dsmr/#"], + "codeowners": ["@depl0y", "@glodenox"], "iot_class": "local_push" } diff --git a/homeassistant/components/dsmr_reader/sensor.py b/homeassistant/components/dsmr_reader/sensor.py index 603b5682f42..81458a94739 100644 --- a/homeassistant/components/dsmr_reader/sensor.py +++ b/homeassistant/components/dsmr_reader/sensor.py @@ -3,15 +3,16 @@ from __future__ import annotations from homeassistant.components import mqtt from homeassistant.components.sensor import SensorEntity +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import slugify +from .const import DOMAIN from .definitions import SENSORS, DSMRReaderSensorEntityDescription -DOMAIN = "dsmr_reader" - async def async_setup_platform( hass: HomeAssistant, @@ -19,7 +20,31 @@ async def async_setup_platform( async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up DSMR Reader sensors.""" + """Set up DSMR Reader sensors via configuration.yaml and show deprecation warning.""" + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2022.12.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, + config: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up DSMR Reader sensors from config entry.""" async_add_entities(DSMRSensor(description) for description in SENSORS) diff --git a/homeassistant/components/dsmr_reader/strings.json b/homeassistant/components/dsmr_reader/strings.json new file mode 100644 index 00000000000..17e28cca884 --- /dev/null +++ b/homeassistant/components/dsmr_reader/strings.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" + }, + "step": { + "confirm": { + "description": "Make sure to configure the 'split topic' data sources in DSMR Reader." + } + } + }, + "issues": { + "deprecated_yaml": { + "title": "The DSMR Reader configuration is being removed", + "description": "Configuring DSMR Reader using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the DSMR Reader YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } + } +} diff --git a/homeassistant/components/dsmr_reader/translations/en.json b/homeassistant/components/dsmr_reader/translations/en.json new file mode 100644 index 00000000000..a2acb20b9b0 --- /dev/null +++ b/homeassistant/components/dsmr_reader/translations/en.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Already configured. Only a single configuration possible." + }, + "step": { + "confirm": { + "description": "Make sure to configure the 'split topic' data sources in DSMR Reader." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Configuring DSMR Reader using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the DSMR Reader YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The DSMR Reader 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 21154b35a7a..23da091c09c 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -83,6 +83,7 @@ FLOWS = { "dnsip", "doorbird", "dsmr", + "dsmr_reader", "dunehd", "dynalite", "eafm", diff --git a/homeassistant/generated/mqtt.py b/homeassistant/generated/mqtt.py index 6b22ca26221..7c4203eaec2 100644 --- a/homeassistant/generated/mqtt.py +++ b/homeassistant/generated/mqtt.py @@ -4,6 +4,9 @@ To update, run python3 -m script.hassfest """ MQTT = { + "dsmr_reader": [ + "dsmr/#", + ], "tasmota": [ "tasmota/discovery/#", ], diff --git a/tests/components/dsmr_reader/__init__.py b/tests/components/dsmr_reader/__init__.py new file mode 100644 index 00000000000..27b818f0bfe --- /dev/null +++ b/tests/components/dsmr_reader/__init__.py @@ -0,0 +1 @@ +"""Tests for the dsmr_reader component.""" diff --git a/tests/components/dsmr_reader/test_config_flow.py b/tests/components/dsmr_reader/test_config_flow.py new file mode 100644 index 00000000000..1de79b0d02d --- /dev/null +++ b/tests/components/dsmr_reader/test_config_flow.py @@ -0,0 +1,47 @@ +"""Tests for the config flow.""" +from homeassistant.components.dsmr_reader.const import DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + + +async def test_import_step(hass: HomeAssistant): + """Test the import step.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + ) + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "DSMR Reader" + + second_result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + ) + assert second_result["type"] == FlowResultType.ABORT + assert second_result["reason"] == "single_instance_allowed" + + +async def test_user_step(hass: HomeAssistant): + """Test the user step call.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "confirm" + assert result["errors"] is None + + config_result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + assert config_result["type"] == FlowResultType.CREATE_ENTRY + assert config_result["title"] == "DSMR Reader" + + duplicate_result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert duplicate_result["type"] == FlowResultType.ABORT + assert duplicate_result["reason"] == "single_instance_allowed" From b617d2bab030e112dc88851ec46bb739fdac3b54 Mon Sep 17 00:00:00 2001 From: Jeef Date: Sun, 25 Sep 2022 21:24:46 -0600 Subject: [PATCH 725/955] IntelliFire Fan Support (#74181) Co-authored-by: J. Nick Koston Co-authored-by: Paulus Schoutsen --- .coveragerc | 1 + .../components/intellifire/__init__.py | 8 +- homeassistant/components/intellifire/fan.py | 130 ++++++++++++++++++ 3 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/intellifire/fan.py diff --git a/.coveragerc b/.coveragerc index 44f8a54fe7a..da6ae8b4cc1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -574,6 +574,7 @@ omit = homeassistant/components/intellifire/climate.py homeassistant/components/intellifire/coordinator.py homeassistant/components/intellifire/entity.py + homeassistant/components/intellifire/fan.py homeassistant/components/intellifire/sensor.py homeassistant/components/intellifire/switch.py homeassistant/components/intesishome/* diff --git a/homeassistant/components/intellifire/__init__.py b/homeassistant/components/intellifire/__init__.py index f5b6085781b..020136f078c 100644 --- a/homeassistant/components/intellifire/__init__.py +++ b/homeassistant/components/intellifire/__init__.py @@ -20,7 +20,13 @@ from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from .const import CONF_USER_ID, DOMAIN, LOGGER from .coordinator import IntellifireDataUpdateCoordinator -PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.CLIMATE, Platform.SWITCH] +PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.CLIMATE, + Platform.FAN, + Platform.SENSOR, + Platform.SWITCH, +] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/intellifire/fan.py b/homeassistant/components/intellifire/fan.py new file mode 100644 index 00000000000..d37bef2189a --- /dev/null +++ b/homeassistant/components/intellifire/fan.py @@ -0,0 +1,130 @@ +"""Fan definition for Intellifire.""" +from __future__ import annotations + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass +import math +from typing import Any + +from intellifire4py import IntellifireControlAsync, IntellifirePollData + +from homeassistant.components.fan import ( + FanEntity, + FanEntityDescription, + FanEntityFeature, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util.percentage import ( + percentage_to_ranged_value, + ranged_value_to_percentage, +) + +from .const import DOMAIN, LOGGER +from .coordinator import IntellifireDataUpdateCoordinator +from .entity import IntellifireEntity + + +@dataclass +class IntellifireFanRequiredKeysMixin: + """Required keys for fan entity.""" + + set_fn: Callable[[IntellifireControlAsync, int], Awaitable] + value_fn: Callable[[IntellifirePollData], bool] + speed_range: tuple[int, int] + + +@dataclass +class IntellifireFanEntityDescription( + FanEntityDescription, IntellifireFanRequiredKeysMixin +): + """Describes a fan entity.""" + + +INTELLIFIRE_FANS: tuple[IntellifireFanEntityDescription, ...] = ( + IntellifireFanEntityDescription( + key="fan", + name="Fan", + has_entity_name=True, + set_fn=lambda control_api, speed: control_api.set_fan_speed(speed=speed), + value_fn=lambda data: data.fanspeed, + speed_range=(1, 4), + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the fans.""" + coordinator: IntellifireDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + if coordinator.data.has_fan: + async_add_entities( + IntellifireFan(coordinator=coordinator, description=description) + for description in INTELLIFIRE_FANS + ) + return + LOGGER.debug("Disabling Fan - IntelliFire device does not appear to have one") + + +class IntellifireFan(IntellifireEntity, FanEntity): + """This is Fan entity for the fireplace.""" + + entity_description: IntellifireFanEntityDescription + _attr_supported_features = FanEntityFeature.SET_SPEED + + @property + def is_on(self) -> bool: + """Return on or off.""" + return self.entity_description.value_fn(self.coordinator.read_api.data) >= 1 + + @property + def percentage(self) -> int | None: + """Return fan percentage.""" + return ranged_value_to_percentage( + self.entity_description.speed_range, self.coordinator.read_api.data.fanspeed + ) + + @property + def speed_count(self) -> int: + """Count of supported speeds.""" + return self.entity_description.speed_range[1] + + async def async_set_percentage(self, percentage: int) -> None: + """Set the speed percentage of the fan.""" + # Calculate percentage steps + LOGGER.debug("Setting Fan Speed %s", percentage) + + int_value = math.ceil( + percentage_to_ranged_value(self.entity_description.speed_range, percentage) + ) + await self.entity_description.set_fn(self.coordinator.control_api, int_value) + await self.coordinator.async_request_refresh() + + async def async_turn_on( + self, + percentage: int | None = None, + preset_mode: str | None = None, + **kwargs: Any, + ) -> None: + """Turn on the fan.""" + if percentage: + int_value = math.ceil( + percentage_to_ranged_value( + self.entity_description.speed_range, percentage + ) + ) + else: + int_value = 1 + await self.entity_description.set_fn(self.coordinator.control_api, int_value) + await self.coordinator.async_request_refresh() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off the fan.""" + self.coordinator.control_api.fan_off() + await self.entity_description.set_fn(self.coordinator.control_api, 0) + await self.coordinator.async_request_refresh() From d9b58e5ef17bcc08979fef71e1ad0e5c7b2db682 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 26 Sep 2022 05:30:17 +0200 Subject: [PATCH 726/955] Netgear add router switches (#72171) --- homeassistant/components/netgear/button.py | 4 +- homeassistant/components/netgear/router.py | 87 ++++++++---- homeassistant/components/netgear/sensor.py | 4 +- homeassistant/components/netgear/switch.py | 154 +++++++++++++++++++-- homeassistant/components/netgear/update.py | 4 +- 5 files changed, 208 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/netgear/button.py b/homeassistant/components/netgear/button.py index e14600ff52b..15ee2652068 100644 --- a/homeassistant/components/netgear/button.py +++ b/homeassistant/components/netgear/button.py @@ -15,7 +15,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DOMAIN, KEY_COORDINATOR, KEY_ROUTER -from .router import NetgearRouter, NetgearRouterEntity +from .router import NetgearRouter, NetgearRouterCoordinatorEntity @dataclass @@ -55,7 +55,7 @@ async def async_setup_entry( ) -class NetgearRouterButtonEntity(NetgearRouterEntity, ButtonEntity): +class NetgearRouterButtonEntity(NetgearRouterCoordinatorEntity, ButtonEntity): """Netgear Router button entity.""" entity_description: NetgearButtonEntityDescription diff --git a/homeassistant/components/netgear/router.py b/homeassistant/components/netgear/router.py index f69e88e83e2..c6384a44351 100644 --- a/homeassistant/components/netgear/router.py +++ b/homeassistant/components/netgear/router.py @@ -20,7 +20,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, format_mac -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -87,14 +87,14 @@ class NetgearRouter: ) self._consider_home = timedelta(seconds=consider_home_int) - self._api: Netgear = None - self._api_lock = asyncio.Lock() + self.api: Netgear = None + self.api_lock = asyncio.Lock() self.devices: dict[str, Any] = {} def _setup(self) -> bool: """Set up a Netgear router sync portion.""" - self._api = get_api( + self.api = get_api( self._password, self._host, self._username, @@ -102,7 +102,7 @@ class NetgearRouter: self._ssl, ) - self._info = self._api.get_info() + self._info = self.api.get_info() if self._info is None: return False @@ -130,7 +130,7 @@ class NetgearRouter: self.method_version = 2 if self.method_version == 2 and self.track_devices: - if not self._api.get_attached_devices_2(): + if not self.api.get_attached_devices_2(): _LOGGER.error( "Netgear Model '%s' in MODELS_V2 list, but failed to get attached devices using V2", self.model, @@ -141,7 +141,7 @@ class NetgearRouter: async def async_setup(self) -> bool: """Set up a Netgear router.""" - async with self._api_lock: + async with self.api_lock: if not await self.hass.async_add_executor_job(self._setup): return False @@ -175,14 +175,14 @@ class NetgearRouter: async def async_get_attached_devices(self) -> list: """Get the devices connected to the router.""" if self.method_version == 1: - async with self._api_lock: + async with self.api_lock: return await self.hass.async_add_executor_job( - self._api.get_attached_devices + self.api.get_attached_devices ) - async with self._api_lock: + async with self.api_lock: return await self.hass.async_add_executor_job( - self._api.get_attached_devices_2 + self.api.get_attached_devices_2 ) async def async_update_device_trackers(self, now=None) -> bool: @@ -221,57 +221,57 @@ class NetgearRouter: async def async_get_traffic_meter(self) -> dict[str, Any] | None: """Get the traffic meter data of the router.""" - async with self._api_lock: - return await self.hass.async_add_executor_job(self._api.get_traffic_meter) + async with self.api_lock: + return await self.hass.async_add_executor_job(self.api.get_traffic_meter) async def async_get_speed_test(self) -> dict[str, Any] | None: """Perform a speed test and get the results from the router.""" - async with self._api_lock: + async with self.api_lock: return await self.hass.async_add_executor_job( - self._api.get_new_speed_test_result + self.api.get_new_speed_test_result ) async def async_get_link_status(self) -> dict[str, Any] | None: """Check the ethernet link status of the router.""" - async with self._api_lock: - return await self.hass.async_add_executor_job(self._api.check_ethernet_link) + async with self.api_lock: + return await self.hass.async_add_executor_job(self.api.check_ethernet_link) async def async_allow_block_device(self, mac: str, allow_block: str) -> None: """Allow or block a device connected to the router.""" - async with self._api_lock: + async with self.api_lock: await self.hass.async_add_executor_job( - self._api.allow_block_device, mac, allow_block + self.api.allow_block_device, mac, allow_block ) async def async_get_utilization(self) -> dict[str, Any] | None: """Get the system information about utilization of the router.""" - async with self._api_lock: - return await self.hass.async_add_executor_job(self._api.get_system_info) + async with self.api_lock: + return await self.hass.async_add_executor_job(self.api.get_system_info) async def async_reboot(self) -> None: """Reboot the router.""" - async with self._api_lock: - await self.hass.async_add_executor_job(self._api.reboot) + async with self.api_lock: + await self.hass.async_add_executor_job(self.api.reboot) async def async_check_new_firmware(self) -> dict[str, Any] | None: """Check for new firmware of the router.""" - async with self._api_lock: - return await self.hass.async_add_executor_job(self._api.check_new_firmware) + async with self.api_lock: + return await self.hass.async_add_executor_job(self.api.check_new_firmware) async def async_update_new_firmware(self) -> None: """Update the router to the latest firmware.""" - async with self._api_lock: - await self.hass.async_add_executor_job(self._api.update_new_firmware) + async with self.api_lock: + await self.hass.async_add_executor_job(self.api.update_new_firmware) @property def port(self) -> int: """Port used by the API.""" - return self._api.port + return self.api.port @property def ssl(self) -> bool: """SSL used by the API.""" - return self._api.ssl + return self.api.ssl class NetgearBaseEntity(CoordinatorEntity): @@ -340,7 +340,7 @@ class NetgearDeviceEntity(NetgearBaseEntity): ) -class NetgearRouterEntity(CoordinatorEntity): +class NetgearRouterCoordinatorEntity(CoordinatorEntity): """Base class for a Netgear router entity.""" def __init__( @@ -379,3 +379,30 @@ class NetgearRouterEntity(CoordinatorEntity): return DeviceInfo( identifiers={(DOMAIN, self._router.unique_id)}, ) + + +class NetgearRouterEntity(Entity): + """Base class for a Netgear router entity without coordinator.""" + + def __init__(self, router: NetgearRouter) -> None: + """Initialize a Netgear device.""" + self._router = router + self._name = router.device_name + self._unique_id = router.serial_number + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return self._unique_id + + @property + def name(self) -> str: + """Return the name.""" + return self._name + + @property + def device_info(self) -> DeviceInfo: + """Return the device information.""" + return DeviceInfo( + identifiers={(DOMAIN, self._router.unique_id)}, + ) diff --git a/homeassistant/components/netgear/sensor.py b/homeassistant/components/netgear/sensor.py index 1ada340d1e1..c38142a3dc5 100644 --- a/homeassistant/components/netgear/sensor.py +++ b/homeassistant/components/netgear/sensor.py @@ -36,7 +36,7 @@ from .const import ( KEY_COORDINATOR_UTIL, KEY_ROUTER, ) -from .router import NetgearDeviceEntity, NetgearRouter, NetgearRouterEntity +from .router import NetgearDeviceEntity, NetgearRouter, NetgearRouterCoordinatorEntity _LOGGER = logging.getLogger(__name__) @@ -381,7 +381,7 @@ class NetgearSensorEntity(NetgearDeviceEntity, SensorEntity): self._state = self._device[self._attribute] -class NetgearRouterSensorEntity(NetgearRouterEntity, RestoreSensor): +class NetgearRouterSensorEntity(NetgearRouterCoordinatorEntity, RestoreSensor): """Representation of a device connected to a Netgear router.""" _attr_entity_registry_enabled_default = False diff --git a/homeassistant/components/netgear/switch.py b/homeassistant/components/netgear/switch.py index 7eab606382d..6491ecf0abe 100644 --- a/homeassistant/components/netgear/switch.py +++ b/homeassistant/components/netgear/switch.py @@ -1,4 +1,7 @@ """Support for Netgear switches.""" +from collections.abc import Callable +from dataclasses import dataclass +from datetime import timedelta import logging from typing import Any @@ -12,10 +15,11 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DOMAIN, KEY_COORDINATOR, KEY_ROUTER -from .router import NetgearDeviceEntity, NetgearRouter +from .router import NetgearDeviceEntity, NetgearRouter, NetgearRouterEntity _LOGGER = logging.getLogger(__name__) +SCAN_INTERVAL = timedelta(seconds=300) SWITCH_TYPES = [ SwitchEntityDescription( @@ -27,11 +31,96 @@ SWITCH_TYPES = [ ] +@dataclass +class NetgearSwitchEntityDescriptionRequired: + """Required attributes of NetgearSwitchEntityDescription.""" + + update: Callable[[NetgearRouter], bool] + action: Callable[[NetgearRouter], bool] + + +@dataclass +class NetgearSwitchEntityDescription( + SwitchEntityDescription, NetgearSwitchEntityDescriptionRequired +): + """Class describing Netgear Switch entities.""" + + +ROUTER_SWITCH_TYPES = [ + NetgearSwitchEntityDescription( + key="access_control", + name="Access Control", + icon="mdi:block-helper", + entity_category=EntityCategory.CONFIG, + update=lambda router: router.api.get_block_device_enable_status, + action=lambda router: router.api.set_block_device_enable, + ), + NetgearSwitchEntityDescription( + key="traffic_meter", + name="Traffic Meter", + icon="mdi:wifi-arrow-up-down", + entity_category=EntityCategory.CONFIG, + update=lambda router: router.api.get_traffic_meter_enabled, + action=lambda router: router.api.enable_traffic_meter, + ), + NetgearSwitchEntityDescription( + key="parental_control", + name="Parental Control", + icon="mdi:account-child-outline", + entity_category=EntityCategory.CONFIG, + update=lambda router: router.api.get_parental_control_enable_status, + action=lambda router: router.api.enable_parental_control, + ), + NetgearSwitchEntityDescription( + key="qos", + name="Quality of Service", + icon="mdi:wifi-star", + entity_category=EntityCategory.CONFIG, + update=lambda router: router.api.get_qos_enable_status, + action=lambda router: router.api.set_qos_enable_status, + ), + NetgearSwitchEntityDescription( + key="2g_guest_wifi", + name="2.4G Guest Wifi", + icon="mdi:wifi", + entity_category=EntityCategory.CONFIG, + update=lambda router: router.api.get_2g_guest_access_enabled, + action=lambda router: router.api.set_2g_guest_access_enabled, + ), + NetgearSwitchEntityDescription( + key="5g_guest_wifi", + name="5G Guest Wifi", + icon="mdi:wifi", + entity_category=EntityCategory.CONFIG, + update=lambda router: router.api.get_5g_guest_access_enabled, + action=lambda router: router.api.set_5g_guest_access_enabled, + ), + NetgearSwitchEntityDescription( + key="smart_connect", + name="Smart Connect", + icon="mdi:wifi", + entity_category=EntityCategory.CONFIG, + update=lambda router: router.api.get_smart_connect_enabled, + action=lambda router: router.api.set_smart_connect_enabled, + ), +] + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up switches for Netgear component.""" router = hass.data[DOMAIN][entry.entry_id][KEY_ROUTER] + + # Router entities + router_entities = [] + + for description in ROUTER_SWITCH_TYPES: + router_entities.append(NetgearRouterSwitchEntity(router, description)) + + async_add_entities(router_entities) + + # Entities per network device coordinator = hass.data[DOMAIN][entry.entry_id][KEY_COORDINATOR] tracked = set() @@ -80,14 +169,9 @@ class NetgearAllowBlock(NetgearDeviceEntity, SwitchEntity): self.entity_description = entity_description self._name = f"{self.get_device_name()} {self.entity_description.name}" self._unique_id = f"{self._mac}-{self.entity_description.key}" - self._state = None + self._attr_is_on = None self.async_update_device() - @property - def is_on(self): - """Return true if switch is on.""" - return self._state - async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" await self._router.async_allow_block_device(self._mac, ALLOW) @@ -104,6 +188,58 @@ class NetgearAllowBlock(NetgearDeviceEntity, SwitchEntity): self._device = self._router.devices[self._mac] self._active = self._device["active"] if self._device[self.entity_description.key] is None: - self._state = None + self._attr_is_on = None else: - self._state = self._device[self.entity_description.key] == "Allow" + self._attr_is_on = self._device[self.entity_description.key] == "Allow" + + +class NetgearRouterSwitchEntity(NetgearRouterEntity, SwitchEntity): + """Representation of a Netgear router switch.""" + + _attr_entity_registry_enabled_default = False + entity_description: NetgearSwitchEntityDescription + + def __init__( + self, + router: NetgearRouter, + entity_description: NetgearSwitchEntityDescription, + ) -> None: + """Initialize a Netgear device.""" + super().__init__(router) + self.entity_description = entity_description + self._name = f"{router.device_name} {entity_description.name}" + self._unique_id = f"{router.serial_number}-{entity_description.key}" + + self._attr_is_on = None + self._attr_available = False + + async def async_added_to_hass(self): + """Fetch state when entity is added.""" + await self.async_update() + await super().async_added_to_hass() + + async def async_update(self): + """Poll the state of the switch.""" + async with self._router.api_lock: + response = await self.hass.async_add_executor_job( + self.entity_description.update(self._router) + ) + if response is None: + self._attr_available = False + else: + self._attr_is_on = response + self._attr_available = True + + async def async_turn_on(self, **kwargs): + """Turn the switch on.""" + async with self._router.api_lock: + await self.hass.async_add_executor_job( + self.entity_description.action(self._router), True + ) + + async def async_turn_off(self, **kwargs): + """Turn the switch off.""" + async with self._router.api_lock: + await self.hass.async_add_executor_job( + self.entity_description.action(self._router), False + ) diff --git a/homeassistant/components/netgear/update.py b/homeassistant/components/netgear/update.py index e913d488c8e..b0e9a26864b 100644 --- a/homeassistant/components/netgear/update.py +++ b/homeassistant/components/netgear/update.py @@ -15,7 +15,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DOMAIN, KEY_COORDINATOR_FIRMWARE, KEY_ROUTER -from .router import NetgearRouter, NetgearRouterEntity +from .router import NetgearRouter, NetgearRouterCoordinatorEntity LOGGER = logging.getLogger(__name__) @@ -31,7 +31,7 @@ async def async_setup_entry( async_add_entities(entities) -class NetgearUpdateEntity(NetgearRouterEntity, UpdateEntity): +class NetgearUpdateEntity(NetgearRouterCoordinatorEntity, UpdateEntity): """Update entity for a Netgear device.""" _attr_device_class = UpdateDeviceClass.FIRMWARE From 75104159c6a163dce969f8510b1216420b0a1aeb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 25 Sep 2022 20:07:50 -1000 Subject: [PATCH 727/955] Fix mqtt tests (#79079) --- tests/components/mqtt/test_common.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/components/mqtt/test_common.py b/tests/components/mqtt/test_common.py index 0ac7e64d1bb..a9cfb88bfb6 100644 --- a/tests/components/mqtt/test_common.py +++ b/tests/components/mqtt/test_common.py @@ -18,6 +18,7 @@ from homeassistant.const import ( SERVICE_RELOAD, STATE_UNAVAILABLE, ) +from homeassistant.generated.mqtt import MQTT from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.setup import async_setup_component @@ -48,6 +49,8 @@ DEFAULT_CONFIG_DEVICE_INFO_MAC = { _SENTINEL = object() +DISCOVERY_COUNT = len(MQTT) + async def help_test_availability_when_connection_lost( hass, mqtt_mock_entry_with_yaml_config, domain, config @@ -1083,7 +1086,7 @@ async def help_test_entity_id_update_subscriptions( state = hass.states.get(f"{domain}.test") assert state is not None - assert mqtt_mock.async_subscribe.call_count == len(topics) + 3 + assert mqtt_mock.async_subscribe.call_count == len(topics) + 2 + DISCOVERY_COUNT for topic in topics: mqtt_mock.async_subscribe.assert_any_call(topic, ANY, ANY, ANY) mqtt_mock.async_subscribe.reset_mock() From 691028dfb4947f28c5a30e6e2d135404ac0a0a60 Mon Sep 17 00:00:00 2001 From: Avi Miller Date: Mon, 26 Sep 2022 17:08:36 +1000 Subject: [PATCH 728/955] Enable the move firmware effect on multizone lights (#78918) Co-authored-by: J. Nick Koston --- homeassistant/components/lifx/coordinator.py | 42 +++++++- homeassistant/components/lifx/light.py | 13 ++- homeassistant/components/lifx/manager.py | 83 +++++++++++++-- homeassistant/components/lifx/manifest.json | 2 +- homeassistant/components/lifx/services.yaml | 34 +++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/lifx/__init__.py | 6 +- tests/components/lifx/test_light.py | 102 ++++++++++++++++++- 9 files changed, 268 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/lifx/coordinator.py b/homeassistant/components/lifx/coordinator.py index 7d3a51562d1..a6d61d91d28 100644 --- a/homeassistant/components/lifx/coordinator.py +++ b/homeassistant/components/lifx/coordinator.py @@ -3,10 +3,11 @@ from __future__ import annotations import asyncio from datetime import timedelta +from enum import IntEnum from functools import partial from typing import Any, cast -from aiolifx.aiolifx import Light +from aiolifx.aiolifx import Light, MultiZoneDirection, MultiZoneEffectType from aiolifx.connection import LIFXConnection from homeassistant.const import Platform @@ -37,6 +38,15 @@ REQUEST_REFRESH_DELAY = 0.35 LIFX_IDENTIFY_DELAY = 3.0 +class FirmwareEffect(IntEnum): + """Enumeration of LIFX firmware effects.""" + + OFF = 0 + MOVE = 1 + MORPH = 2 + FLAME = 3 + + class LIFXUpdateCoordinator(DataUpdateCoordinator): """DataUpdateCoordinator to gather data for a specific lifx device.""" @@ -51,7 +61,9 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator): self.connection = connection self.device: Light = connection.device self.lock = asyncio.Lock() + self.active_effect = FirmwareEffect.OFF update_interval = timedelta(seconds=10) + super().__init__( hass, _LOGGER, @@ -139,6 +151,7 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator): # Update model-specific configuration if lifx_features(self.device)["multizone"]: await self.async_update_color_zones() + await self.async_update_multizone_effect() if lifx_features(self.device)["hev"]: await self.async_get_hev_cycle() @@ -219,6 +232,33 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator): ) ) + async def async_update_multizone_effect(self) -> None: + """Update the device firmware effect running state.""" + await async_execute_lifx(self.device.get_multizone_effect) + self.active_effect = FirmwareEffect[self.device.effect.get("effect", "OFF")] + + async def async_set_multizone_effect( + self, effect: str, speed: float, direction: str, power_on: bool = True + ) -> None: + """Control the firmware-based Move effect on a multizone device.""" + if lifx_features(self.device)["multizone"] is True: + if power_on and self.device.power_level == 0: + await self.async_set_power(True, 0) + + await async_execute_lifx( + partial( + self.device.set_multizone_effect, + effect=MultiZoneEffectType[effect.upper()].value, + speed=speed, + direction=MultiZoneDirection[direction.upper()].value, + ) + ) + self.active_effect = FirmwareEffect[effect.upper()] + + def async_get_active_effect(self) -> int: + """Return the enum value of the currently active firmware effect.""" + return self.active_effect.value + async def async_set_hev_cycle_state(self, enable: bool, duration: int = 0) -> None: """Start or stop an HEV cycle on a LIFX Clean bulb.""" if lifx_features(self.device)["hev"]: diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index 314f7bd915e..aa02e42a9bf 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -38,10 +38,11 @@ from .const import ( DOMAIN, INFRARED_BRIGHTNESS, ) -from .coordinator import LIFXUpdateCoordinator +from .coordinator import FirmwareEffect, LIFXUpdateCoordinator from .entity import LIFXEntity from .manager import ( SERVICE_EFFECT_COLORLOOP, + SERVICE_EFFECT_MOVE, SERVICE_EFFECT_PULSE, SERVICE_EFFECT_STOP, LIFXManager, @@ -139,6 +140,7 @@ class LIFXLight(LIFXEntity, LightEntity): color_mode = ColorMode.BRIGHTNESS self._attr_color_mode = color_mode self._attr_supported_color_modes = {color_mode} + self._attr_effect = None @property def brightness(self) -> int: @@ -163,6 +165,8 @@ class LIFXLight(LIFXEntity, LightEntity): """Return the name of the currently running effect.""" if effect := self.effects_conductor.effect(self.bulb): return f"effect_{effect.name}" + if effect := self.coordinator.async_get_active_effect(): + return f"effect_{FirmwareEffect(effect).name.lower()}" return None async def update_during_transition(self, when: int) -> None: @@ -361,6 +365,13 @@ class LIFXColor(LIFXLight): class LIFXStrip(LIFXColor): """Representation of a LIFX light strip with multiple zones.""" + _attr_effect_list = [ + SERVICE_EFFECT_COLORLOOP, + SERVICE_EFFECT_PULSE, + SERVICE_EFFECT_MOVE, + SERVICE_EFFECT_STOP, + ] + async def set_color( self, hsbk: list[float | int | None], diff --git a/homeassistant/components/lifx/manager.py b/homeassistant/components/lifx/manager.py index 28693ae6a60..c199ee8a9a1 100644 --- a/homeassistant/components/lifx/manager.py +++ b/homeassistant/components/lifx/manager.py @@ -1,6 +1,7 @@ """Support for LIFX lights.""" from __future__ import annotations +import asyncio from collections.abc import Callable from datetime import timedelta from typing import Any @@ -28,21 +29,35 @@ from homeassistant.core import HomeAssistant, ServiceCall, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.service import async_extract_referenced_entity_ids -from .const import _LOGGER, DATA_LIFX_MANAGER, DOMAIN +from .const import DATA_LIFX_MANAGER, DOMAIN +from .coordinator import LIFXUpdateCoordinator, Light from .util import convert_8_to_16, find_hsbk SCAN_INTERVAL = timedelta(seconds=10) - SERVICE_EFFECT_PULSE = "effect_pulse" SERVICE_EFFECT_COLORLOOP = "effect_colorloop" +SERVICE_EFFECT_MOVE = "effect_move" SERVICE_EFFECT_STOP = "effect_stop" +ATTR_POWER_OFF = "power_off" ATTR_POWER_ON = "power_on" ATTR_PERIOD = "period" ATTR_CYCLES = "cycles" ATTR_SPREAD = "spread" ATTR_CHANGE = "change" +ATTR_DIRECTION = "direction" +ATTR_SPEED = "speed" + +EFFECT_MOVE = "MOVE" +EFFECT_OFF = "OFF" + +EFFECT_MOVE_DEFAULT_SPEED = 3.0 +EFFECT_MOVE_DEFAULT_DIRECTION = "right" +EFFECT_MOVE_DIRECTION_RIGHT = "right" +EFFECT_MOVE_DIRECTION_LEFT = "left" + +EFFECT_MOVE_DIRECTIONS = [EFFECT_MOVE_DIRECTION_LEFT, EFFECT_MOVE_DIRECTION_RIGHT] PULSE_MODE_BLINK = "blink" PULSE_MODE_BREATHE = "breathe" @@ -110,10 +125,20 @@ LIFX_EFFECT_STOP_SCHEMA = cv.make_entity_service_schema({}) SERVICES = ( SERVICE_EFFECT_STOP, SERVICE_EFFECT_PULSE, + SERVICE_EFFECT_MOVE, SERVICE_EFFECT_COLORLOOP, ) +LIFX_EFFECT_MOVE_SCHEMA = cv.make_entity_service_schema( + { + **LIFX_EFFECT_SCHEMA, + ATTR_SPEED: vol.All(vol.Coerce(float), vol.Clamp(min=0.1, max=60)), + ATTR_DIRECTION: vol.In(EFFECT_MOVE_DIRECTIONS), + } +) + + class LIFXManager: """Representation of all known LIFX entities.""" @@ -168,6 +193,13 @@ class LIFXManager: schema=LIFX_EFFECT_COLORLOOP_SCHEMA, ) + self.hass.services.async_register( + DOMAIN, + SERVICE_EFFECT_MOVE, + service_handler, + schema=LIFX_EFFECT_MOVE_SCHEMA, + ) + self.hass.services.async_register( DOMAIN, SERVICE_EFFECT_STOP, @@ -179,15 +211,35 @@ class LIFXManager: self, entity_ids: set[str], service: str, **kwargs: Any ) -> None: """Start a light effect on entities.""" - bulbs = [ - coordinator.device - for entry_id, coordinator in self.hass.data[DOMAIN].items() - if entry_id != DATA_LIFX_MANAGER - and self.entry_id_to_entity_id[entry_id] in entity_ids - ] - _LOGGER.debug("Starting effect %s on %s", service, bulbs) - if service == SERVICE_EFFECT_PULSE: + coordinators: list[LIFXUpdateCoordinator] = [] + bulbs: list[Light] = [] + + for entry_id, coordinator in self.hass.data[DOMAIN].items(): + if ( + entry_id != DATA_LIFX_MANAGER + and self.entry_id_to_entity_id[entry_id] in entity_ids + ): + coordinators.append(coordinator) + bulbs.append(coordinator.device) + + if service == SERVICE_EFFECT_MOVE: + await asyncio.gather( + *( + coordinator.async_set_multizone_effect( + effect=EFFECT_MOVE, + speed=kwargs.get(ATTR_SPEED, EFFECT_MOVE_DEFAULT_SPEED), + direction=kwargs.get( + ATTR_DIRECTION, EFFECT_MOVE_DEFAULT_DIRECTION + ), + power_on=kwargs.get(ATTR_POWER_ON, False), + ) + for coordinator in coordinators + ) + ) + + elif service == SERVICE_EFFECT_PULSE: + effect = aiolifx_effects.EffectPulse( power_on=kwargs.get(ATTR_POWER_ON), period=kwargs.get(ATTR_PERIOD), @@ -196,6 +248,7 @@ class LIFXManager: hsbk=find_hsbk(self.hass, **kwargs), ) await self.effects_conductor.start(effect, bulbs) + elif service == SERVICE_EFFECT_COLORLOOP: preprocess_turn_on_alternatives(self.hass, kwargs) @@ -212,5 +265,15 @@ class LIFXManager: brightness=brightness, ) await self.effects_conductor.start(effect, bulbs) + elif service == SERVICE_EFFECT_STOP: + await self.effects_conductor.stop(bulbs) + + for coordinator in coordinators: + await coordinator.async_set_multizone_effect( + effect=EFFECT_OFF, + speed=EFFECT_MOVE_DEFAULT_SPEED, + direction=EFFECT_MOVE_DEFAULT_DIRECTION, + power_on=False, + ) diff --git a/homeassistant/components/lifx/manifest.json b/homeassistant/components/lifx/manifest.json index bbc2e1bea15..45321f22b66 100644 --- a/homeassistant/components/lifx/manifest.json +++ b/homeassistant/components/lifx/manifest.json @@ -3,7 +3,7 @@ "name": "LIFX", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/lifx", - "requirements": ["aiolifx==0.8.4", "aiolifx_effects==0.2.2"], + "requirements": ["aiolifx==0.8.5", "aiolifx_effects==0.2.2"], "quality_scale": "platinum", "dependencies": ["network"], "homekit": { diff --git a/homeassistant/components/lifx/services.yaml b/homeassistant/components/lifx/services.yaml index 5208be89638..fc2e522dcd4 100644 --- a/homeassistant/components/lifx/services.yaml +++ b/homeassistant/components/lifx/services.yaml @@ -171,6 +171,40 @@ effect_colorloop: default: true selector: boolean: +effect_move: + name: Move effect + description: Start the firmware-based Move effect on a LIFX Z, Lightstrip or Beam. + target: + entity: + integration: lifx + domain: light + fields: + speed: + name: Speed + description: How long in seconds for the effect to move across the length of the light. + default: 3.0 + selector: + number: + min: 0.1 + max: 60 + step: 0.1 + unit_of_measurement: seconds + direction: + name: Direction + description: Direction the effect will move across the device. + default: right + selector: + select: + mode: dropdown + options: + - right + - left + power_on: + name: Power on + description: Powered off lights will be turned on before starting the effect. + default: true + selector: + boolean: effect_stop: name: Stop effect diff --git a/requirements_all.txt b/requirements_all.txt index 2f811e4d286..ea34b58493f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -193,7 +193,7 @@ aiokafka==0.7.2 aiokef==0.2.16 # homeassistant.components.lifx -aiolifx==0.8.4 +aiolifx==0.8.5 # homeassistant.components.lifx aiolifx_effects==0.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0b5586e63d9..dd9568217d1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -171,7 +171,7 @@ aiohue==4.5.0 aiokafka==0.7.2 # homeassistant.components.lifx -aiolifx==0.8.4 +aiolifx==0.8.5 # homeassistant.components.lifx aiolifx_effects==0.2.2 diff --git a/tests/components/lifx/__init__.py b/tests/components/lifx/__init__.py index 8f6e19188b6..72a355877e1 100644 --- a/tests/components/lifx/__init__.py +++ b/tests/components/lifx/__init__.py @@ -145,9 +145,13 @@ def _mocked_infrared_bulb() -> Light: def _mocked_light_strip() -> Light: bulb = _mocked_bulb() bulb.product = 31 # LIFX Z + bulb.color_zones = [MagicMock(), MagicMock()] + bulb.effect = {"effect": "MOVE", "speed": 3, "duration": 0, "direction": "RIGHT"} bulb.get_color_zones = MockLifxCommand(bulb) bulb.set_color_zones = MockLifxCommand(bulb) - bulb.color_zones = [MagicMock(), MagicMock()] + bulb.get_multizone_effect = MockLifxCommand(bulb) + bulb.set_multizone_effect = MockLifxCommand(bulb) + return bulb diff --git a/tests/components/lifx/test_light.py b/tests/components/lifx/test_light.py index 6555e483f5f..e7c18989767 100644 --- a/tests/components/lifx/test_light.py +++ b/tests/components/lifx/test_light.py @@ -10,7 +10,12 @@ from homeassistant.components import lifx from homeassistant.components.lifx import DOMAIN from homeassistant.components.lifx.const import ATTR_POWER from homeassistant.components.lifx.light import ATTR_INFRARED, ATTR_ZONES -from homeassistant.components.lifx.manager import SERVICE_EFFECT_COLORLOOP +from homeassistant.components.lifx.manager import ( + ATTR_DIRECTION, + ATTR_SPEED, + SERVICE_EFFECT_COLORLOOP, + SERVICE_EFFECT_MOVE, +) from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_MODE, @@ -24,7 +29,13 @@ from homeassistant.components.light import ( DOMAIN as LIGHT_DOMAIN, ColorMode, ) -from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, STATE_OFF, STATE_UNAVAILABLE +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_HOST, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, +) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -401,6 +412,93 @@ async def test_light_strip(hass: HomeAssistant) -> None: ) +async def test_lightstrip_move_effect(hass: HomeAssistant) -> None: + """Test the firmware move effect on a light strip.""" + config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=SERIAL + ) + config_entry.add_to_hass(hass) + bulb = _mocked_light_strip() + bulb.power_level = 0 + bulb.color = [65535, 65535, 65535, 65535] + 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" + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "effect_move"}, + blocking=True, + ) + + assert len(bulb.set_power.calls) == 1 + assert len(bulb.set_multizone_effect.calls) == 1 + + call_dict = bulb.set_multizone_effect.calls[0][1] + call_dict.pop("callb") + assert call_dict == { + "effect": 1, + "speed": 3.0, + "direction": 0, + } + bulb.get_multizone_effect.reset_mock() + bulb.set_multizone_effect.reset_mock() + bulb.set_power.reset_mock() + + bulb.power_level = 0 + await hass.services.async_call( + DOMAIN, + SERVICE_EFFECT_MOVE, + {ATTR_ENTITY_ID: entity_id, ATTR_SPEED: 4.5, ATTR_DIRECTION: "left"}, + blocking=True, + ) + + bulb.power_level = 65535 + bulb.effect = {"name": "effect_move", "enable": 1} + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=30)) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state.state == STATE_ON + + assert len(bulb.set_power.calls) == 1 + assert len(bulb.set_multizone_effect.calls) == 1 + call_dict = bulb.set_multizone_effect.calls[0][1] + call_dict.pop("callb") + assert call_dict == { + "effect": 1, + "speed": 4.5, + "direction": 1, + } + bulb.get_multizone_effect.reset_mock() + bulb.set_multizone_effect.reset_mock() + bulb.set_power.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "effect_stop"}, + blocking=True, + ) + assert len(bulb.set_power.calls) == 0 + assert len(bulb.set_multizone_effect.calls) == 1 + call_dict = bulb.set_multizone_effect.calls[0][1] + call_dict.pop("callb") + assert call_dict == { + "effect": 0, + "speed": 3.0, + "direction": 0, + } + bulb.get_multizone_effect.reset_mock() + bulb.set_multizone_effect.reset_mock() + bulb.set_power.reset_mock() + + async def test_color_light_with_temp( hass: HomeAssistant, mock_effect_conductor ) -> None: From 87e0c555db35f1e3a52358f80786a0b4f9fb0ea8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 25 Sep 2022 21:43:38 -1000 Subject: [PATCH 729/955] Bump aiohomekit to 2.0.1 (#79080) --- 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 79dbca9109c..2f5f8911968 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.0.0"], + "requirements": ["aiohomekit==2.0.1"], "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 ea34b58493f..aa7981ce1cd 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.0.0 +aiohomekit==2.0.1 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dd9568217d1..e00ffe1deb3 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.0.0 +aiohomekit==2.0.1 # homeassistant.components.emulated_hue # homeassistant.components.http From fe5e3320d41c462775c1fc22a788021aba81d538 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 26 Sep 2022 10:22:45 +0200 Subject: [PATCH 730/955] Adjust switch as X to inherit entity category (#79081) --- homeassistant/components/switch_as_x/cover.py | 2 ++ .../components/switch_as_x/entity.py | 4 ++- homeassistant/components/switch_as_x/fan.py | 2 ++ homeassistant/components/switch_as_x/light.py | 2 ++ homeassistant/components/switch_as_x/lock.py | 2 ++ homeassistant/components/switch_as_x/siren.py | 2 ++ tests/components/switch_as_x/test_init.py | 35 +++++++++++++++++++ 7 files changed, 48 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/switch_as_x/cover.py b/homeassistant/components/switch_as_x/cover.py index e480151a946..9d7a7bf6178 100644 --- a/homeassistant/components/switch_as_x/cover.py +++ b/homeassistant/components/switch_as_x/cover.py @@ -32,6 +32,7 @@ async def async_setup_entry( ) wrapped_switch = registry.async_get(entity_id) device_id = wrapped_switch.device_id if wrapped_switch else None + entity_category = wrapped_switch.entity_category if wrapped_switch else None async_add_entities( [ @@ -40,6 +41,7 @@ async def async_setup_entry( entity_id, config_entry.entry_id, device_id, + entity_category, ) ] ) diff --git a/homeassistant/components/switch_as_x/entity.py b/homeassistant/components/switch_as_x/entity.py index 4d478154763..0bd784eacef 100644 --- a/homeassistant/components/switch_as_x/entity.py +++ b/homeassistant/components/switch_as_x/entity.py @@ -13,7 +13,7 @@ from homeassistant.const import ( ) from homeassistant.core import Event, callback from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.entity import Entity, ToggleEntity +from homeassistant.helpers.entity import Entity, EntityCategory, ToggleEntity from homeassistant.helpers.event import async_track_state_change_event @@ -28,9 +28,11 @@ class BaseEntity(Entity): switch_entity_id: str, unique_id: str | None, device_id: str | None = None, + entity_category: EntityCategory | None = None, ) -> None: """Initialize Light Switch.""" self._device_id = device_id + self._attr_entity_category = entity_category self._attr_name = name self._attr_unique_id = unique_id self._switch_entity_id = switch_entity_id diff --git a/homeassistant/components/switch_as_x/fan.py b/homeassistant/components/switch_as_x/fan.py index d4f16a93ef6..bfc4d2e037e 100644 --- a/homeassistant/components/switch_as_x/fan.py +++ b/homeassistant/components/switch_as_x/fan.py @@ -25,6 +25,7 @@ async def async_setup_entry( ) wrapped_switch = registry.async_get(entity_id) device_id = wrapped_switch.device_id if wrapped_switch else None + entity_category = wrapped_switch.entity_category if wrapped_switch else None async_add_entities( [ @@ -33,6 +34,7 @@ async def async_setup_entry( entity_id, config_entry.entry_id, device_id, + entity_category, ) ] ) diff --git a/homeassistant/components/switch_as_x/light.py b/homeassistant/components/switch_as_x/light.py index 1071d6b4480..c8181bf35f8 100644 --- a/homeassistant/components/switch_as_x/light.py +++ b/homeassistant/components/switch_as_x/light.py @@ -23,6 +23,7 @@ async def async_setup_entry( ) wrapped_switch = registry.async_get(entity_id) device_id = wrapped_switch.device_id if wrapped_switch else None + entity_category = wrapped_switch.entity_category if wrapped_switch else None async_add_entities( [ @@ -31,6 +32,7 @@ async def async_setup_entry( entity_id, config_entry.entry_id, device_id, + entity_category, ) ] ) diff --git a/homeassistant/components/switch_as_x/lock.py b/homeassistant/components/switch_as_x/lock.py index a4e3b2ec180..a0aac15a702 100644 --- a/homeassistant/components/switch_as_x/lock.py +++ b/homeassistant/components/switch_as_x/lock.py @@ -32,6 +32,7 @@ async def async_setup_entry( ) wrapped_switch = registry.async_get(entity_id) device_id = wrapped_switch.device_id if wrapped_switch else None + entity_category = wrapped_switch.entity_category if wrapped_switch else None async_add_entities( [ @@ -40,6 +41,7 @@ async def async_setup_entry( entity_id, config_entry.entry_id, device_id, + entity_category, ) ] ) diff --git a/homeassistant/components/switch_as_x/siren.py b/homeassistant/components/switch_as_x/siren.py index 591546cd20a..635aa4e2d79 100644 --- a/homeassistant/components/switch_as_x/siren.py +++ b/homeassistant/components/switch_as_x/siren.py @@ -23,6 +23,7 @@ async def async_setup_entry( ) wrapped_switch = registry.async_get(entity_id) device_id = wrapped_switch.device_id if wrapped_switch else None + entity_category = wrapped_switch.entity_category if wrapped_switch else None async_add_entities( [ @@ -31,6 +32,7 @@ async def async_setup_entry( entity_id, config_entry.entry_id, device_id, + entity_category, ) ] ) diff --git a/tests/components/switch_as_x/test_init.py b/tests/components/switch_as_x/test_init.py index 9c3eec1884c..35c901e2d5e 100644 --- a/tests/components/switch_as_x/test_init.py +++ b/tests/components/switch_as_x/test_init.py @@ -18,6 +18,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.entity import EntityCategory from tests.common import MockConfigEntry @@ -403,3 +404,37 @@ async def test_reset_hidden_by( # Check hidden by is reset switch_entity_entry = registry.async_get(switch_entity_entry.entity_id) assert switch_entity_entry.hidden_by == hidden_by_after + + +@pytest.mark.parametrize("target_domain", PLATFORMS_TO_TEST) +async def test_entity_category_inheritance( + hass: HomeAssistant, + target_domain: Platform, +) -> None: + """Test the entity category is inherited from source device.""" + registry = er.async_get(hass) + + switch_entity_entry = registry.async_get_or_create("switch", "test", "unique") + registry.async_update_entity( + switch_entity_entry.entity_id, entity_category=EntityCategory.CONFIG + ) + + # Add the config entry + switch_as_x_config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + CONF_ENTITY_ID: switch_entity_entry.id, + CONF_TARGET_DOMAIN: target_domain, + }, + title="ABC", + ) + switch_as_x_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(switch_as_x_config_entry.entry_id) + await hass.async_block_till_done() + + entity_entry = registry.async_get(f"{target_domain}.abc") + assert entity_entry + assert entity_entry.device_id == switch_entity_entry.device_id + assert entity_entry.entity_category is EntityCategory.CONFIG From 92e9e7fb6c4310c853acfc7bb32b172796e46829 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Mon, 26 Sep 2022 10:40:11 +0200 Subject: [PATCH 731/955] Add nibe heat pump number entities (#78941) * Add number platform * Enable number platform * Adjust typing * Update homeassistant/components/nibe_heatpump/number.py Co-authored-by: Paulus Schoutsen * Fix format Co-authored-by: Paulus Schoutsen --- .coveragerc | 1 + .../components/nibe_heatpump/__init__.py | 7 +- .../components/nibe_heatpump/number.py | 68 +++++++++++++++++++ 3 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/nibe_heatpump/number.py diff --git a/.coveragerc b/.coveragerc index da6ae8b4cc1..cb42ac67231 100644 --- a/.coveragerc +++ b/.coveragerc @@ -838,6 +838,7 @@ omit = homeassistant/components/nfandroidtv/__init__.py homeassistant/components/nfandroidtv/notify.py homeassistant/components/nibe_heatpump/__init__.py + homeassistant/components/nibe_heatpump/number.py homeassistant/components/nibe_heatpump/select.py homeassistant/components/nibe_heatpump/sensor.py homeassistant/components/nibe_heatpump/binary_sensor.py diff --git a/homeassistant/components/nibe_heatpump/__init__.py b/homeassistant/components/nibe_heatpump/__init__.py index ed3bc649453..35c43f85543 100644 --- a/homeassistant/components/nibe_heatpump/__init__.py +++ b/homeassistant/components/nibe_heatpump/__init__.py @@ -39,7 +39,12 @@ from .const import ( LOGGER, ) -PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SELECT, Platform.SENSOR] +PLATFORMS: list[Platform] = [ + Platform.BINARY_SENSOR, + Platform.NUMBER, + Platform.SELECT, + Platform.SENSOR, +] COIL_READ_RETRIES = 5 diff --git a/homeassistant/components/nibe_heatpump/number.py b/homeassistant/components/nibe_heatpump/number.py new file mode 100644 index 00000000000..11c6917ec1c --- /dev/null +++ b/homeassistant/components/nibe_heatpump/number.py @@ -0,0 +1,68 @@ +"""The Nibe Heat Pump numbers.""" +from __future__ import annotations + +from nibe.coil import Coil + +from homeassistant.components.number import ENTITY_ID_FORMAT, NumberEntity +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 DOMAIN, CoilEntity, Coordinator + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up platform.""" + + coordinator: Coordinator = hass.data[DOMAIN][config_entry.entry_id] + + async_add_entities( + Number(coordinator, coil) + for coil in coordinator.coils + if coil.is_writable and not coil.mappings + ) + + +def _get_numeric_limits(size: str): + """Calculate the integer limits of a signed or unsigned integer value.""" + if size[0] == "u": + return (0, pow(2, int(size[1:])) - 1) + if size[0] == "s": + return (-pow(2, int(size[1:]) - 1), pow(2, int(size[1:]) - 1) - 1) + raise ValueError(f"Invalid size type specified {size}") + + +class Number(CoilEntity, NumberEntity): + """Number entity.""" + + _attr_entity_category = EntityCategory.CONFIG + + def __init__(self, coordinator: Coordinator, coil: Coil) -> None: + """Initialize entity.""" + super().__init__(coordinator, coil, ENTITY_ID_FORMAT) + if coil.min is None or coil.max is None: + ( + self._attr_native_min_value, + self._attr_native_max_value, + ) = _get_numeric_limits(coil.size) + else: + self._attr_native_min_value = float(coil.min) + self._attr_native_max_value = float(coil.max) + + self._attr_native_unit_of_measurement = coil.unit + self._attr_native_value = None + + def _async_read_coil(self, coil: Coil) -> None: + try: + self._attr_native_value = float(coil.value) + except ValueError: + self._attr_native_value = None + + async def async_set_native_value(self, value: float) -> None: + """Set new value.""" + await self._async_write_coil(value) From 2d7b3647138fbe7eb97eaefcc57dc01d9bddde01 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Mon, 26 Sep 2022 01:52:03 -0700 Subject: [PATCH 732/955] Bump ha-av to v10.0.0.b5 (#78977) --- homeassistant/components/generic/manifest.json | 2 +- homeassistant/components/stream/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/generic/manifest.json b/homeassistant/components/generic/manifest.json index 98ca2986a86..8749a45d3de 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.0b4", "pillow==9.2.0"], + "requirements": ["ha-av==10.0.0b5", "pillow==9.2.0"], "documentation": "https://www.home-assistant.io/integrations/generic", "codeowners": ["@davet2001"], "iot_class": "local_push" diff --git a/homeassistant/components/stream/manifest.json b/homeassistant/components/stream/manifest.json index 6c090b93ac2..262c20e420a 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.0b4"], + "requirements": ["PyTurboJPEG==1.6.7", "ha-av==10.0.0b5"], "dependencies": ["http"], "codeowners": ["@hunterjm", "@uvjustin", "@allenporter"], "quality_scale": "internal", diff --git a/requirements_all.txt b/requirements_all.txt index aa7981ce1cd..001c0de667d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -814,7 +814,7 @@ guppy3==3.1.2 # homeassistant.components.generic # homeassistant.components.stream -ha-av==10.0.0b4 +ha-av==10.0.0b5 # homeassistant.components.ffmpeg ha-ffmpeg==3.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e00ffe1deb3..bdfb4dc144f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -603,7 +603,7 @@ guppy3==3.1.2 # homeassistant.components.generic # homeassistant.components.stream -ha-av==10.0.0b4 +ha-av==10.0.0b5 # homeassistant.components.ffmpeg ha-ffmpeg==3.0.2 From 677cba582b3533ee4ea4f70f7cdcba81887ba2a3 Mon Sep 17 00:00:00 2001 From: IceBotYT <34712694+IceBotYT@users.noreply.github.com> Date: Mon, 26 Sep 2022 05:35:33 -0400 Subject: [PATCH 733/955] Fix name truncation and unusual entity names for LaCrosse View (#78254) --- homeassistant/components/lacrosse_view/sensor.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/lacrosse_view/sensor.py b/homeassistant/components/lacrosse_view/sensor.py index 46c4671a109..0f60ef4ca10 100644 --- a/homeassistant/components/lacrosse_view/sensor.py +++ b/homeassistant/components/lacrosse_view/sensor.py @@ -70,14 +70,14 @@ SENSOR_DESCRIPTIONS = { "HeatIndex": LaCrosseSensorEntityDescription( key="HeatIndex", device_class=SensorDeviceClass.TEMPERATURE, - name="Heat Index", + name="Heat index", state_class=SensorStateClass.MEASUREMENT, value_fn=get_value, native_unit_of_measurement=TEMP_FAHRENHEIT, ), "WindSpeed": LaCrosseSensorEntityDescription( key="WindSpeed", - name="Wind Speed", + name="Wind speed", state_class=SensorStateClass.MEASUREMENT, value_fn=get_value, native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, @@ -136,6 +136,7 @@ class LaCrosseViewSensor( """LaCrosse View sensor.""" entity_description: LaCrosseSensorEntityDescription + _attr_has_entity_name: bool = True def __init__( self, @@ -148,10 +149,9 @@ class LaCrosseViewSensor( self.entity_description = description self._attr_unique_id = f"{sensor.sensor_id}-{description.key}" - self._attr_name = f"{sensor.location.name} {description.name}" self._attr_device_info = { "identifiers": {(DOMAIN, sensor.sensor_id)}, - "name": sensor.name.split(" ")[0], + "name": sensor.name, "manufacturer": "LaCrosse Technology", "model": sensor.model, "via_device": (DOMAIN, sensor.location.id), From edc9c0d09cdc772280742d41c7cba48bab5afb2d Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 26 Sep 2022 13:24:15 +0200 Subject: [PATCH 734/955] Update aioecowitt to 2022.09.3 (#79087) --- homeassistant/components/ecowitt/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ecowitt/manifest.json b/homeassistant/components/ecowitt/manifest.json index 4306311d2ec..9ba16231867 100644 --- a/homeassistant/components/ecowitt/manifest.json +++ b/homeassistant/components/ecowitt/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ecowitt", "dependencies": ["webhook"], - "requirements": ["aioecowitt==2022.09.2"], + "requirements": ["aioecowitt==2022.09.3"], "codeowners": ["@pvizeli"], "iot_class": "local_push" } diff --git a/requirements_all.txt b/requirements_all.txt index 001c0de667d..3c24488e1b2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -150,7 +150,7 @@ aioeafm==0.1.2 aioeagle==1.1.0 # homeassistant.components.ecowitt -aioecowitt==2022.09.2 +aioecowitt==2022.09.3 # homeassistant.components.emonitor aioemonitor==1.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bdfb4dc144f..48c064c744e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -137,7 +137,7 @@ aioeafm==0.1.2 aioeagle==1.1.0 # homeassistant.components.ecowitt -aioecowitt==2022.09.2 +aioecowitt==2022.09.3 # homeassistant.components.emonitor aioemonitor==1.0.5 From f463b22053ffd1ad9db34b0282211ac23288c716 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Mon, 26 Sep 2022 14:46:43 +0200 Subject: [PATCH 735/955] Add nibe heat pump switch entities (#78943) * Add switch platform * Enable switch platform * Drop unneeded variable --- .coveragerc | 3 +- .../components/nibe_heatpump/__init__.py | 1 + .../components/nibe_heatpump/switch.py | 52 +++++++++++++++++++ 3 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/nibe_heatpump/switch.py diff --git a/.coveragerc b/.coveragerc index cb42ac67231..98992bed247 100644 --- a/.coveragerc +++ b/.coveragerc @@ -838,10 +838,11 @@ omit = homeassistant/components/nfandroidtv/__init__.py homeassistant/components/nfandroidtv/notify.py homeassistant/components/nibe_heatpump/__init__.py + homeassistant/components/nibe_heatpump/binary_sensor.py homeassistant/components/nibe_heatpump/number.py homeassistant/components/nibe_heatpump/select.py homeassistant/components/nibe_heatpump/sensor.py - homeassistant/components/nibe_heatpump/binary_sensor.py + homeassistant/components/nibe_heatpump/switch.py homeassistant/components/niko_home_control/light.py homeassistant/components/nilu/air_quality.py homeassistant/components/nissan_leaf/* diff --git a/homeassistant/components/nibe_heatpump/__init__.py b/homeassistant/components/nibe_heatpump/__init__.py index 35c43f85543..590afba5b79 100644 --- a/homeassistant/components/nibe_heatpump/__init__.py +++ b/homeassistant/components/nibe_heatpump/__init__.py @@ -44,6 +44,7 @@ PLATFORMS: list[Platform] = [ Platform.NUMBER, Platform.SELECT, Platform.SENSOR, + Platform.SWITCH, ] COIL_READ_RETRIES = 5 diff --git a/homeassistant/components/nibe_heatpump/switch.py b/homeassistant/components/nibe_heatpump/switch.py new file mode 100644 index 00000000000..133e3d7244c --- /dev/null +++ b/homeassistant/components/nibe_heatpump/switch.py @@ -0,0 +1,52 @@ +"""The Nibe Heat Pump switch.""" +from __future__ import annotations + +from typing import Any + +from nibe.coil import Coil + +from homeassistant.components.switch import ENTITY_ID_FORMAT, 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 DOMAIN, CoilEntity, Coordinator + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up platform.""" + + coordinator: Coordinator = hass.data[DOMAIN][config_entry.entry_id] + + async_add_entities( + Switch(coordinator, coil) + for coil in coordinator.coils + if coil.is_writable and coil.is_boolean + ) + + +class Switch(CoilEntity, SwitchEntity): + """Switch entity.""" + + _attr_entity_category = EntityCategory.CONFIG + + def __init__(self, coordinator: Coordinator, coil: Coil) -> None: + """Initialize entity.""" + super().__init__(coordinator, coil, ENTITY_ID_FORMAT) + self._attr_is_on = None + + def _async_read_coil(self, coil: Coil) -> None: + self._attr_is_on = coil.value == "ON" + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the entity on.""" + await self._async_write_coil("ON") + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the entity off.""" + await self._async_write_coil("OFF") From a8b8b245d1059d710da2c409785e2aaa5944a5ba Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Mon, 26 Sep 2022 08:54:17 -0400 Subject: [PATCH 736/955] Allow multiple entries in Tautulli (#74406) --- homeassistant/components/tautulli/__init__.py | 4 +- .../components/tautulli/config_flow.py | 3 +- homeassistant/components/tautulli/sensor.py | 2 +- .../components/tautulli/strings.json | 2 +- .../components/tautulli/translations/en.json | 2 +- tests/components/tautulli/__init__.py | 24 +------ tests/components/tautulli/test_config_flow.py | 69 ++++++++++++------- 7 files changed, 53 insertions(+), 53 deletions(-) diff --git a/homeassistant/components/tautulli/__init__.py b/homeassistant/components/tautulli/__init__.py index bd5032014bc..a90d78380b4 100644 --- a/homeassistant/components/tautulli/__init__.py +++ b/homeassistant/components/tautulli/__init__.py @@ -30,7 +30,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) coordinator = TautulliDataUpdateCoordinator(hass, host_configuration, api_client) await coordinator.async_config_entry_first_refresh() - hass.data[DOMAIN] = coordinator + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True @@ -39,7 +39,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 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.pop(DOMAIN) + hass.data[DOMAIN].pop(entry.entry_id) return unload_ok diff --git a/homeassistant/components/tautulli/config_flow.py b/homeassistant/components/tautulli/config_flow.py index f06405825c9..532852687da 100644 --- a/homeassistant/components/tautulli/config_flow.py +++ b/homeassistant/components/tautulli/config_flow.py @@ -24,10 +24,9 @@ class TautulliConfigFlow(ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle a flow initiated by the user.""" - if self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") errors = {} if user_input is not None: + self._async_abort_entries_match({CONF_URL: user_input[CONF_URL]}) if (error := await self.validate_input(user_input)) is None: return self.async_create_entry( title=DEFAULT_NAME, diff --git a/homeassistant/components/tautulli/sensor.py b/homeassistant/components/tautulli/sensor.py index 5653aa5dc57..7109199fdcd 100644 --- a/homeassistant/components/tautulli/sensor.py +++ b/homeassistant/components/tautulli/sensor.py @@ -232,7 +232,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Tautulli sensor.""" - coordinator: TautulliDataUpdateCoordinator = hass.data[DOMAIN] + coordinator: TautulliDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] entities: list[TautulliSensor | TautulliSessionSensor] = [ TautulliSensor( coordinator, diff --git a/homeassistant/components/tautulli/strings.json b/homeassistant/components/tautulli/strings.json index 9b561ea6f5b..90c64a6a8d6 100644 --- a/homeassistant/components/tautulli/strings.json +++ b/homeassistant/components/tautulli/strings.json @@ -23,7 +23,7 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } } diff --git a/homeassistant/components/tautulli/translations/en.json b/homeassistant/components/tautulli/translations/en.json index c24497cfbd9..078f02d7bcd 100644 --- a/homeassistant/components/tautulli/translations/en.json +++ b/homeassistant/components/tautulli/translations/en.json @@ -2,7 +2,7 @@ "config": { "abort": { "reauth_successful": "Re-authentication was successful", - "single_instance_allowed": "Already configured. Only a single configuration possible." + "already_configured": "Service is already configured" }, "error": { "cannot_connect": "Failed to connect", diff --git a/tests/components/tautulli/__init__.py b/tests/components/tautulli/__init__.py index f296830bef4..b48488c7216 100644 --- a/tests/components/tautulli/__init__.py +++ b/tests/components/tautulli/__init__.py @@ -2,14 +2,9 @@ from unittest.mock import AsyncMock, patch -from homeassistant.components.tautulli.const import CONF_MONITORED_USERS, DOMAIN +from homeassistant.components.tautulli.const import DOMAIN from homeassistant.const import ( CONF_API_KEY, - CONF_HOST, - CONF_MONITORED_CONDITIONS, - CONF_PATH, - CONF_PORT, - CONF_SSL, CONF_URL, CONF_VERIFY_SSL, CONTENT_TYPE_JSON, @@ -22,7 +17,6 @@ from tests.test_util.aiohttp import AiohttpClientMocker API_KEY = "abcd" URL = "http://1.2.3.4:8181/test" NAME = "Tautulli" -SSL = False VERIFY_SSL = True CONF_DATA = { @@ -30,19 +24,6 @@ CONF_DATA = { CONF_URL: URL, CONF_VERIFY_SSL: VERIFY_SSL, } -CONF_IMPORT_DATA = { - CONF_API_KEY: API_KEY, - CONF_HOST: "1.2.3.4", - CONF_MONITORED_CONDITIONS: ["Stream count"], - CONF_MONITORED_USERS: ["test"], - CONF_PORT: "8181", - CONF_PATH: "/test", - CONF_SSL: SSL, - CONF_VERIFY_SSL: VERIFY_SSL, -} - -DEFAULT_USERS = [{11111111: {"enabled": False}, 22222222: {"enabled": False}}] -SELECTED_USERNAMES = ["user1"] def patch_config_flow_tautulli(mocked_tautulli) -> AsyncMock: @@ -104,9 +85,6 @@ async def setup_integration( CONF_VERIFY_SSL: VERIFY_SSL, CONF_API_KEY: api_key, }, - options={ - CONF_MONITORED_USERS: DEFAULT_USERS, - }, ) entry.add_to_hass(hass) diff --git a/tests/components/tautulli/test_config_flow.py b/tests/components/tautulli/test_config_flow.py index d846f0915d0..d39f9c1e3a1 100644 --- a/tests/components/tautulli/test_config_flow.py +++ b/tests/components/tautulli/test_config_flow.py @@ -6,36 +6,15 @@ from pytautulli import exceptions from homeassistant import data_entry_flow from homeassistant.components.tautulli.const import DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER -from homeassistant.const import CONF_API_KEY, CONF_SOURCE +from homeassistant.const import CONF_API_KEY, CONF_SOURCE, CONF_URL, CONF_VERIFY_SSL from homeassistant.core import HomeAssistant -from . import ( - CONF_DATA, - CONF_IMPORT_DATA, - NAME, - patch_config_flow_tautulli, - setup_integration, -) +from . import CONF_DATA, NAME, patch_config_flow_tautulli, setup_integration from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMocker -async def test_flow_user_single_instance_allowed(hass: HomeAssistant) -> None: - """Test user step single instance allowed.""" - entry = MockConfigEntry(domain=DOMAIN, data=CONF_DATA) - entry.add_to_hass(hass) - - with patch_config_flow_tautulli(AsyncMock()): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - data=CONF_IMPORT_DATA, - ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT - assert result["reason"] == "single_instance_allowed" - - async def test_flow_user(hass: HomeAssistant) -> None: """Test user initiated flow.""" result = await hass.config_entries.flow.async_init( @@ -126,6 +105,50 @@ async def test_flow_user_unknown_error(hass: HomeAssistant) -> None: assert result2["data"] == CONF_DATA +async def test_flow_user_already_configured(hass: HomeAssistant) -> None: + """Test user step already configured.""" + entry = MockConfigEntry(domain=DOMAIN, data=CONF_DATA) + entry.add_to_hass(hass) + + with patch_config_flow_tautulli(AsyncMock()): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data=CONF_DATA, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_flow_user_multiple_entries_allowed(hass: HomeAssistant) -> None: + """Test user step can configure multiple entries.""" + entry = MockConfigEntry(domain=DOMAIN, data=CONF_DATA) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={CONF_SOURCE: SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + input = { + CONF_URL: "http://1.2.3.5:8181/test", + CONF_API_KEY: "efgh", + CONF_VERIFY_SSL: True, + } + with patch_config_flow_tautulli(AsyncMock()): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=input, + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == NAME + assert result2["data"] == input + + async def test_flow_reauth( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: From 75f6f9b5e200ad44f1564786b40d9382dda21e08 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 26 Sep 2022 03:12:08 -1000 Subject: [PATCH 737/955] Improve performance of Bluetooth device fallback (#79078) --- homeassistant/components/bluetooth/manager.py | 17 ++++ .../components/bluetooth/manifest.json | 6 +- homeassistant/components/bluetooth/models.py | 27 +++--- homeassistant/components/bluetooth/scanner.py | 11 +++ homeassistant/components/bluetooth/usage.py | 2 +- homeassistant/components/esphome/bluetooth.py | 5 + homeassistant/package_constraints.txt | 6 +- requirements_all.txt | 6 +- requirements_test_all.txt | 6 +- tests/components/bluetooth/test_models.py | 93 +++++++++++++++++++ 10 files changed, 155 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index a7d1e141b3d..4229b285939 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -235,6 +235,23 @@ class BluetoothManager: self._cancel_unavailable_tracking.clear() uninstall_multiple_bleak_catcher() + async def async_get_devices_by_address( + self, address: str, connectable: bool + ) -> list[BLEDevice]: + """Get devices by address.""" + types_ = (True,) if connectable else (True, False) + return [ + device + for device in await asyncio.gather( + *( + scanner.async_get_device_by_address(address) + for type_ in types_ + for scanner in self._get_scanners_by_type(type_) + ) + ) + if device is not None + ] + @hass_callback def async_all_discovered_devices(self, connectable: bool) -> Iterable[BLEDevice]: """Return all of discovered devices from all the scanners including duplicates.""" diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 636d5925651..91b15583086 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -6,9 +6,9 @@ "after_dependencies": ["hassio"], "quality_scale": "internal", "requirements": [ - "bleak==0.18.0", - "bleak-retry-connector==2.0.2", - "bluetooth-adapters==0.5.1", + "bleak==0.18.1", + "bleak-retry-connector==2.1.0", + "bluetooth-adapters==0.5.2", "bluetooth-auto-recovery==0.3.3", "dbus-fast==1.14.0" ], diff --git a/homeassistant/components/bluetooth/models.py b/homeassistant/components/bluetooth/models.py index 100d9f69c03..d93f8efc1e2 100644 --- a/homeassistant/components/bluetooth/models.py +++ b/homeassistant/components/bluetooth/models.py @@ -91,6 +91,10 @@ class BaseHaScanner: def discovered_devices(self) -> list[BLEDevice]: """Return a list of discovered devices.""" + @abstractmethod + async def async_get_device_by_address(self, address: str) -> BLEDevice | None: + """Get a device by address.""" + async def async_diagnostics(self) -> dict[str, Any]: """Return diagnostic information about the scanner.""" return { @@ -256,7 +260,9 @@ class HaBleakClientWrapper(BleakClient): async def connect(self, **kwargs: Any) -> bool: """Connect to the specified GATT server.""" if not self._backend: - wrapped_backend = self._async_get_backend() + wrapped_backend = ( + self._async_get_backend() or await self._async_get_fallback_backend() + ) self._backend = wrapped_backend.client( await freshen_ble_device(wrapped_backend.device) or wrapped_backend.device, @@ -286,7 +292,7 @@ class HaBleakClientWrapper(BleakClient): return _HaWrappedBleakBackend(ble_device, connector.client) @hass_callback - def _async_get_backend(self) -> _HaWrappedBleakBackend: + def _async_get_backend(self) -> _HaWrappedBleakBackend | None: """Get the bleak backend for the given address.""" assert MANAGER is not None address = self.__address @@ -297,6 +303,10 @@ class HaBleakClientWrapper(BleakClient): if backend := self._async_get_backend_for_ble_device(ble_device): return backend + return None + + async def _async_get_fallback_backend(self) -> _HaWrappedBleakBackend: + """Get a fallback backend for the given address.""" # # The preferred backend cannot currently connect the device # because it is likely out of connection slots. @@ -304,16 +314,11 @@ class HaBleakClientWrapper(BleakClient): # We need to try all backends to find one that can # connect to the device. # - # Currently we have to search all the discovered devices - # because the bleak API does not allow us to get the - # details for a specific device. - # + assert MANAGER is not None + address = self.__address + devices = await MANAGER.async_get_devices_by_address(address, True) for ble_device in sorted( - ( - ble_device - for ble_device in MANAGER.async_all_discovered_devices(True) - if ble_device.address == address - ), + devices, key=lambda ble_device: ble_device.rssi or NO_RSSI_VALUE, reverse=True, ): diff --git a/homeassistant/components/bluetooth/scanner.py b/homeassistant/components/bluetooth/scanner.py index 3f089326eb2..857a0e4c01c 100644 --- a/homeassistant/components/bluetooth/scanner.py +++ b/homeassistant/components/bluetooth/scanner.py @@ -17,6 +17,7 @@ from bleak.backends.bluezdbus.advertisement_monitor import OrPattern from bleak.backends.bluezdbus.scanner import BlueZScannerArgs from bleak.backends.device import BLEDevice from bleak.backends.scanner import AdvertisementData +from bleak_retry_connector import get_device_by_adapter from dbus_fast import InvalidMessageError from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_callback @@ -140,6 +141,16 @@ class HaScanner(BaseHaScanner): """Return a list of discovered devices.""" return self.scanner.discovered_devices + async def async_get_device_by_address(self, address: str) -> BLEDevice | None: + """Get a device by address.""" + if platform.system() == "Linux": + return await get_device_by_adapter(address, self.adapter) + # We don't have a fast version of this for MacOS yet + return next( + (device for device in self.discovered_devices if device.address == address), + None, + ) + async def async_diagnostics(self) -> dict[str, Any]: """Return diagnostic information about the scanner.""" base_diag = await super().async_diagnostics() diff --git a/homeassistant/components/bluetooth/usage.py b/homeassistant/components/bluetooth/usage.py index 23388c84302..d282ca7415b 100644 --- a/homeassistant/components/bluetooth/usage.py +++ b/homeassistant/components/bluetooth/usage.py @@ -13,7 +13,7 @@ ORIGINAL_BLEAK_CLIENT = bleak.BleakClient def install_multiple_bleak_catcher() -> None: """Wrap the bleak classes to return the shared instance if multiple instances are detected.""" bleak.BleakScanner = HaBleakScannerWrapper # type: ignore[misc, assignment] - bleak.BleakClient = HaBleakClientWrapper # type: ignore[misc, assignment] + bleak.BleakClient = HaBleakClientWrapper # type: ignore[misc] def uninstall_multiple_bleak_catcher() -> None: diff --git a/homeassistant/components/esphome/bluetooth.py b/homeassistant/components/esphome/bluetooth.py index 94351293c7b..71dfc798ffd 100644 --- a/homeassistant/components/esphome/bluetooth.py +++ b/homeassistant/components/esphome/bluetooth.py @@ -1,4 +1,5 @@ """Bluetooth scanner for esphome.""" +from __future__ import annotations from collections.abc import Callable import datetime @@ -77,6 +78,10 @@ class ESPHomeScannner(BaseHaScanner): """Return a list of discovered devices.""" return list(self._discovered_devices.values()) + async def async_get_device_by_address(self, address: str) -> BLEDevice | None: + """Get a device by address.""" + return self._discovered_devices.get(address) + @callback def async_on_advertisement(self, adv: BluetoothLEAdvertisement) -> None: """Call the registered callback.""" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c2e38e09345..d0dc41935db 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,9 +10,9 @@ atomicwrites-homeassistant==1.4.1 attrs==21.2.0 awesomeversion==22.9.0 bcrypt==3.1.7 -bleak-retry-connector==2.0.2 -bleak==0.18.0 -bluetooth-adapters==0.5.1 +bleak-retry-connector==2.1.0 +bleak==0.18.1 +bluetooth-adapters==0.5.2 bluetooth-auto-recovery==0.3.3 certifi>=2021.5.30 ciso8601==2.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 3c24488e1b2..92c69e28ad3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -410,10 +410,10 @@ bimmer_connected==0.10.4 bizkaibus==0.1.1 # homeassistant.components.bluetooth -bleak-retry-connector==2.0.2 +bleak-retry-connector==2.1.0 # homeassistant.components.bluetooth -bleak==0.18.0 +bleak==0.18.1 # homeassistant.components.blebox blebox_uniapi==2.0.2 @@ -435,7 +435,7 @@ bluemaestro-ble==0.2.0 # bluepy==1.3.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.5.1 +bluetooth-adapters==0.5.2 # homeassistant.components.bluetooth bluetooth-auto-recovery==0.3.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 48c064c744e..84bf3a4451e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -331,10 +331,10 @@ bellows==0.33.1 bimmer_connected==0.10.4 # homeassistant.components.bluetooth -bleak-retry-connector==2.0.2 +bleak-retry-connector==2.1.0 # homeassistant.components.bluetooth -bleak==0.18.0 +bleak==0.18.1 # homeassistant.components.blebox blebox_uniapi==2.0.2 @@ -346,7 +346,7 @@ blinkpy==0.19.2 bluemaestro-ble==0.2.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.5.1 +bluetooth-adapters==0.5.2 # homeassistant.components.bluetooth bluetooth-auto-recovery==0.3.3 diff --git a/tests/components/bluetooth/test_models.py b/tests/components/bluetooth/test_models.py index 2321a64e1e3..d126dcac301 100644 --- a/tests/components/bluetooth/test_models.py +++ b/tests/components/bluetooth/test_models.py @@ -1,4 +1,5 @@ """Tests for the Bluetooth integration models.""" +from __future__ import annotations from unittest.mock import patch @@ -197,6 +198,12 @@ async def test_ble_device_with_proxy_client_out_of_connections_uses_best_availab """Return a list of discovered devices.""" return [switchbot_proxy_device_has_connection_slot] + async def async_get_device_by_address(self, address: str) -> BLEDevice | None: + """Return a list of discovered devices.""" + if address == switchbot_proxy_device_has_connection_slot.address: + return switchbot_proxy_device_has_connection_slot + return None + scanner = FakeScanner() cancel = manager.async_register_scanner(scanner, True) assert manager.async_discovered_devices(True) == [ @@ -210,3 +217,89 @@ async def test_ble_device_with_proxy_client_out_of_connections_uses_best_availab client.set_disconnected_callback(lambda client: None) await client.disconnect() cancel() + + +async def test_ble_device_with_proxy_client_out_of_connections_uses_best_available_macos( + hass, enable_bluetooth, macos_adapter +): + """Test we switch to the next available proxy when one runs out of connections on MacOS.""" + manager = _get_manager() + + switchbot_proxy_device_no_connection_slot = BLEDevice( + "44:44:33:11:23:45", + "wohand_no_connection_slot", + { + "connector": HaBluetoothConnector( + MockBleakClient, "mock_bleak_client", lambda: False + ), + "path": "/org/bluez/hci0/dev_44_44_33_11_23_45", + }, + rssi=-30, + ) + switchbot_proxy_device_no_connection_slot.metadata["delegate"] = 0 + + switchbot_proxy_device_has_connection_slot = BLEDevice( + "44:44:33:11:23:45", + "wohand_has_connection_slot", + { + "connector": HaBluetoothConnector( + MockBleakClient, "mock_bleak_client", lambda: True + ), + "path": "/org/bluez/hci0/dev_44_44_33_11_23_45", + }, + rssi=-40, + ) + switchbot_proxy_device_has_connection_slot.metadata["delegate"] = 0 + + switchbot_device = BLEDevice( + "44:44:33:11:23:45", + "wohand", + {}, + rssi=-100, + ) + switchbot_device.metadata["delegate"] = 0 + switchbot_adv = AdvertisementData( + local_name="wohand", service_uuids=[], manufacturer_data={1: b"\x01"} + ) + + inject_advertisement_with_source( + hass, switchbot_device, switchbot_adv, "00:00:00:00:00:01" + ) + inject_advertisement_with_source( + hass, + switchbot_proxy_device_has_connection_slot, + switchbot_adv, + "esp32_has_connection_slot", + ) + inject_advertisement_with_source( + hass, + switchbot_proxy_device_no_connection_slot, + switchbot_adv, + "esp32_no_connection_slot", + ) + + class FakeScanner(BaseHaScanner): + @property + def discovered_devices(self) -> list[BLEDevice]: + """Return a list of discovered devices.""" + return [switchbot_proxy_device_has_connection_slot] + + async def async_get_device_by_address(self, address: str) -> BLEDevice | None: + """Return a list of discovered devices.""" + if address == switchbot_proxy_device_has_connection_slot.address: + return switchbot_proxy_device_has_connection_slot + return None + + scanner = FakeScanner() + cancel = manager.async_register_scanner(scanner, True) + assert manager.async_discovered_devices(True) == [ + switchbot_proxy_device_no_connection_slot + ] + + client = HaBleakClientWrapper(switchbot_proxy_device_no_connection_slot) + with patch("bleak.get_platform_client_backend_type"): + await client.connect() + assert client.is_connected is True + client.set_disconnected_callback(lambda client: None) + await client.disconnect() + cancel() From 2667f0b792b1f936aeb5958cc40d5dee26350bf6 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Mon, 26 Sep 2022 15:55:29 +0200 Subject: [PATCH 738/955] Bump plugwise to v0.21.3, add related new features (#76610) Co-authored-by: Franck Nijhof --- homeassistant/components/plugwise/climate.py | 75 +++++----- homeassistant/components/plugwise/const.py | 2 +- .../components/plugwise/manifest.json | 2 +- homeassistant/components/plugwise/number.py | 35 ++++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/plugwise/conftest.py | 100 ++++++++++++- .../all_data.json | 57 +++---- .../all_data.json | 30 ++-- .../notifications.json | 0 .../fixtures/m_adam_cooling/all_data.json | 139 ++++++++++++++++++ .../m_adam_cooling/notifications.json | 1 + .../fixtures/m_adam_heating/all_data.json | 137 +++++++++++++++++ .../m_adam_heating/notifications.json | 1 + .../m_anna_heatpump_cooling/all_data.json | 88 +++++++++++ .../notifications.json | 1 + .../m_anna_heatpump_idle/all_data.json | 88 +++++++++++ .../m_anna_heatpump_idle/notifications.json | 1 + tests/components/plugwise/test_climate.py | 136 ++++++++++++++--- tests/components/plugwise/test_diagnostics.py | 57 +++---- tests/components/plugwise/test_number.py | 6 +- 21 files changed, 828 insertions(+), 132 deletions(-) rename tests/components/plugwise/fixtures/{anna_heatpump => anna_heatpump_heating}/all_data.json (81%) rename tests/components/plugwise/fixtures/{anna_heatpump => anna_heatpump_heating}/notifications.json (100%) create mode 100644 tests/components/plugwise/fixtures/m_adam_cooling/all_data.json create mode 100644 tests/components/plugwise/fixtures/m_adam_cooling/notifications.json create mode 100644 tests/components/plugwise/fixtures/m_adam_heating/all_data.json create mode 100644 tests/components/plugwise/fixtures/m_adam_heating/notifications.json create mode 100644 tests/components/plugwise/fixtures/m_anna_heatpump_cooling/all_data.json create mode 100644 tests/components/plugwise/fixtures/m_anna_heatpump_cooling/notifications.json create mode 100644 tests/components/plugwise/fixtures/m_anna_heatpump_idle/all_data.json create mode 100644 tests/components/plugwise/fixtures/m_anna_heatpump_idle/notifications.json diff --git a/homeassistant/components/plugwise/climate.py b/homeassistant/components/plugwise/climate.py index 155c4f73bb6..84dc4576700 100644 --- a/homeassistant/components/plugwise/climate.py +++ b/homeassistant/components/plugwise/climate.py @@ -13,9 +13,10 @@ from homeassistant.components.climate import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, DOMAIN, THERMOSTAT_CLASSES +from .const import DOMAIN, MASTER_THERMOSTATS from .coordinator import PlugwiseDataUpdateCoordinator from .entity import PlugwiseEntity from .util import plugwise_command @@ -31,7 +32,7 @@ async def async_setup_entry( async_add_entities( PlugwiseClimateEntity(coordinator, device_id) for device_id, device in coordinator.data.devices.items() - if device["dev_class"] in THERMOSTAT_CLASSES + if device["dev_class"] in MASTER_THERMOSTATS ) @@ -59,26 +60,27 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity): # Determine hvac modes and current hvac mode self._attr_hvac_modes = [HVACMode.HEAT] - if self.coordinator.data.gateway.get("cooling_present"): + if self.coordinator.data.gateway["cooling_present"]: self._attr_hvac_modes.append(HVACMode.COOL) - if self.device.get("available_schedules") != ["None"]: + if self.device["available_schedules"] != ["None"]: self._attr_hvac_modes.append(HVACMode.AUTO) - self._attr_min_temp = self.device.get("lower_bound", DEFAULT_MIN_TEMP) - self._attr_max_temp = self.device.get("upper_bound", DEFAULT_MAX_TEMP) - if resolution := self.device.get("resolution"): - # Ensure we don't drop below 0.1 - self._attr_target_temperature_step = max(resolution, 0.1) + self._attr_min_temp = self.device["thermostat"]["lower_bound"] + self._attr_max_temp = self.device["thermostat"]["upper_bound"] + # Ensure we don't drop below 0.1 + self._attr_target_temperature_step = max( + self.device["thermostat"]["resolution"], 0.1 + ) @property - def current_temperature(self) -> float | None: + def current_temperature(self) -> float: """Return the current temperature.""" - return self.device["sensors"].get("temperature") + return self.device["sensors"]["temperature"] @property - def target_temperature(self) -> float | None: + def target_temperature(self) -> float: """Return the temperature we try to reach.""" - return self.device["sensors"].get("setpoint") + return self.device["thermostat"]["setpoint"] @property def hvac_mode(self) -> HVACMode: @@ -88,23 +90,24 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity): return HVACMode(mode) @property - def hvac_action(self) -> HVACAction: + def hvac_action(self) -> HVACAction | None: """Return the current running hvac operation if supported.""" # When control_state is present, prefer this data - if "control_state" in self.device: - if self.device.get("control_state") == "cooling": - return HVACAction.COOLING - # Support preheating state as heating, until preheating is added as a separate state - if self.device.get("control_state") in ["heating", "preheating"]: - return HVACAction.HEATING - else: - heater_central_data = self.coordinator.data.devices[ - self.coordinator.data.gateway["heater_id"] - ] - if heater_central_data["binary_sensors"].get("heating_state"): - return HVACAction.HEATING - if heater_central_data["binary_sensors"].get("cooling_state"): - return HVACAction.COOLING + if (control_state := self.device.get("control_state")) == "cooling": + return HVACAction.COOLING + # Support preheating state as heating, until preheating is added as a separate state + if control_state in ["heating", "preheating"]: + return HVACAction.HEATING + if control_state == "off": + return HVACAction.IDLE + + hc_data = self.coordinator.data.devices[ + self.coordinator.data.gateway["heater_id"] + ] + if hc_data["binary_sensors"]["heating_state"]: + return HVACAction.HEATING + if hc_data["binary_sensors"].get("cooling_state"): + return HVACAction.COOLING return HVACAction.IDLE @@ -117,25 +120,29 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity): def extra_state_attributes(self) -> Mapping[str, Any] | None: """Return entity specific state attributes.""" return { - "available_schemas": self.device.get("available_schedules"), - "selected_schema": self.device.get("selected_schedule"), + "available_schemas": self.device["available_schedules"], + "selected_schema": self.device["selected_schedule"], } @plugwise_command async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" - if ((temperature := kwargs.get(ATTR_TEMPERATURE)) is None) or ( - self._attr_max_temp < temperature < self._attr_min_temp + if ((temperature := kwargs.get(ATTR_TEMPERATURE)) is None) or not ( + self._attr_min_temp <= temperature <= self._attr_max_temp ): - raise ValueError("Invalid temperature requested") + raise ValueError("Invalid temperature change requested") + await self.coordinator.api.set_temperature(self.device["location"], temperature) @plugwise_command async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set the hvac mode.""" + if hvac_mode not in self.hvac_modes: + raise HomeAssistantError("Unsupported hvac_mode") + await self.coordinator.api.set_schedule_state( self.device["location"], - self.device.get("last_used"), + self.device["last_used"], "on" if hvac_mode == HVACMode.AUTO else "off", ) diff --git a/homeassistant/components/plugwise/const.py b/homeassistant/components/plugwise/const.py index d56d9c06ff5..c2d0d75c8a0 100644 --- a/homeassistant/components/plugwise/const.py +++ b/homeassistant/components/plugwise/const.py @@ -48,7 +48,7 @@ DEFAULT_SCAN_INTERVAL: Final[dict[str, timedelta]] = { } DEFAULT_USERNAME: Final = "smile" -THERMOSTAT_CLASSES: Final[list[str]] = [ +MASTER_THERMOSTATS: Final[list[str]] = [ "thermostat", "thermostatic_radiator_valve", "zone_thermometer", diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json index bf7fc453f89..9c8ea6f3be7 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.18.7"], + "requirements": ["plugwise==0.21.3"], "codeowners": ["@CoMPaTech", "@bouwew", "@brefra", "@frenck"], "zeroconf": ["_plugwise._tcp.local."], "config_flow": true, diff --git a/homeassistant/components/plugwise/number.py b/homeassistant/components/plugwise/number.py index 380be9111ba..9100e006968 100644 --- a/homeassistant/components/plugwise/number.py +++ b/homeassistant/components/plugwise/number.py @@ -27,7 +27,11 @@ from .entity import PlugwiseEntity class PlugwiseEntityDescriptionMixin: """Mixin values for Plugwse entities.""" - command: Callable[[Smile, float], Awaitable[None]] + command: Callable[[Smile, str, float], Awaitable[None]] + native_max_value_key: str + native_min_value_key: str + native_step_key: str + native_value_key: str @dataclass @@ -40,11 +44,15 @@ class PlugwiseNumberEntityDescription( NUMBER_TYPES = ( PlugwiseNumberEntityDescription( key="maximum_boiler_temperature", - command=lambda api, value: api.set_max_boiler_temperature(value), + command=lambda api, number, value: api.set_number_setpoint(number, value), device_class=NumberDeviceClass.TEMPERATURE, name="Maximum boiler temperature setpoint", entity_category=EntityCategory.CONFIG, + native_max_value_key="upper_bound", + native_min_value_key="lower_bound", + native_step_key="resolution", native_unit_of_measurement=TEMP_CELSIUS, + native_value_key="setpoint", ), ) @@ -91,24 +99,37 @@ class PlugwiseNumberEntity(PlugwiseEntity, NumberEntity): @property def native_step(self) -> float: """Return the setpoint step value.""" - return max(self.device["resolution"], 1) + return max( + self.device[self.entity_description.key][ + self.entity_description.native_step_key + ], + 1, + ) @property def native_value(self) -> float: """Return the present setpoint value.""" - return self.device[self.entity_description.key] + return self.device[self.entity_description.key][ + self.entity_description.native_value_key + ] @property def native_min_value(self) -> float: """Return the setpoint min. value.""" - return self.device["lower_bound"] + return self.device[self.entity_description.key][ + self.entity_description.native_min_value_key + ] @property def native_max_value(self) -> float: """Return the setpoint max. value.""" - return self.device["upper_bound"] + return self.device[self.entity_description.key][ + self.entity_description.native_max_value_key + ] async def async_set_native_value(self, value: float) -> None: """Change to the new setpoint value.""" - await self.entity_description.command(self.coordinator.api, value) + await self.entity_description.command( + self.coordinator.api, self.entity_description.key, value + ) await self.coordinator.async_request_refresh() diff --git a/requirements_all.txt b/requirements_all.txt index 92c69e28ad3..370b136b4c0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1306,7 +1306,7 @@ plexauth==0.0.6 plexwebsocket==0.0.13 # homeassistant.components.plugwise -plugwise==0.18.7 +plugwise==0.21.3 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 84bf3a4451e..aa579fb4d55 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -930,7 +930,7 @@ plexauth==0.0.6 plexwebsocket==0.0.13 # homeassistant.components.plugwise -plugwise==0.18.7 +plugwise==0.21.3 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/tests/components/plugwise/conftest.py b/tests/components/plugwise/conftest.py index 012734315f6..d7941f74450 100644 --- a/tests/components/plugwise/conftest.py +++ b/tests/components/plugwise/conftest.py @@ -93,10 +93,108 @@ def mock_smile_adam() -> Generator[None, MagicMock, None]: yield smile +@pytest.fixture +def mock_smile_adam_2() -> Generator[None, MagicMock, None]: + """Create a 2nd Mock Adam environment for testing exceptions.""" + chosen_env = "m_adam_heating" + + with patch( + "homeassistant.components.plugwise.gateway.Smile", autospec=True + ) as smile_mock: + smile = smile_mock.return_value + + smile.gateway_id = "da224107914542988a88561b4452b0f6" + smile.heater_id = "056ee145a816487eaa69243c3280f8bf" + smile.smile_version = "3.6.4" + smile.smile_type = "thermostat" + smile.smile_hostname = "smile98765" + smile.smile_name = "Adam" + + smile.connect.return_value = True + + smile.notifications = _read_json(chosen_env, "notifications") + smile.async_update.return_value = _read_json(chosen_env, "all_data") + + yield smile + + +@pytest.fixture +def mock_smile_adam_3() -> Generator[None, MagicMock, None]: + """Create a 3rd Mock Adam environment for testing exceptions.""" + chosen_env = "m_adam_cooling" + + with patch( + "homeassistant.components.plugwise.gateway.Smile", autospec=True + ) as smile_mock: + smile = smile_mock.return_value + + smile.gateway_id = "da224107914542988a88561b4452b0f6" + smile.heater_id = "056ee145a816487eaa69243c3280f8bf" + smile.smile_version = "3.6.4" + smile.smile_type = "thermostat" + smile.smile_hostname = "smile98765" + smile.smile_name = "Adam" + + smile.connect.return_value = True + + smile.notifications = _read_json(chosen_env, "notifications") + smile.async_update.return_value = _read_json(chosen_env, "all_data") + + yield smile + + @pytest.fixture def mock_smile_anna() -> Generator[None, MagicMock, None]: """Create a Mock Anna environment for testing exceptions.""" - chosen_env = "anna_heatpump" + chosen_env = "anna_heatpump_heating" + with patch( + "homeassistant.components.plugwise.gateway.Smile", autospec=True + ) as smile_mock: + smile = smile_mock.return_value + + smile.gateway_id = "015ae9ea3f964e668e490fa39da3870b" + smile.heater_id = "1cbf783bb11e4a7c8a6843dee3a86927" + smile.smile_version = "4.0.15" + smile.smile_type = "thermostat" + smile.smile_hostname = "smile98765" + smile.smile_name = "Anna" + + smile.connect.return_value = True + + smile.notifications = _read_json(chosen_env, "notifications") + smile.async_update.return_value = _read_json(chosen_env, "all_data") + + yield smile + + +@pytest.fixture +def mock_smile_anna_2() -> Generator[None, MagicMock, None]: + """Create a 2nd Mock Anna environment for testing exceptions.""" + chosen_env = "m_anna_heatpump_cooling" + with patch( + "homeassistant.components.plugwise.gateway.Smile", autospec=True + ) as smile_mock: + smile = smile_mock.return_value + + smile.gateway_id = "015ae9ea3f964e668e490fa39da3870b" + smile.heater_id = "1cbf783bb11e4a7c8a6843dee3a86927" + smile.smile_version = "4.0.15" + smile.smile_type = "thermostat" + smile.smile_hostname = "smile98765" + smile.smile_name = "Anna" + + smile.connect.return_value = True + + smile.notifications = _read_json(chosen_env, "notifications") + smile.async_update.return_value = _read_json(chosen_env, "all_data") + + yield smile + + +@pytest.fixture +def mock_smile_anna_3() -> Generator[None, MagicMock, None]: + """Create a 3rd Mock Anna environment for testing exceptions.""" + chosen_env = "m_anna_heatpump_idle" with patch( "homeassistant.components.plugwise.gateway.Smile", autospec=True ) as smile_mock: diff --git a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/all_data.json b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/all_data.json index 6ef0716e4b6..08792347af0 100644 --- a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/all_data.json +++ b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/all_data.json @@ -20,9 +20,12 @@ "name": "Zone Lisa Bios", "zigbee_mac_address": "ABCD012345670A06", "vendor": "Plugwise", - "lower_bound": 0.0, - "upper_bound": 99.9, - "resolution": 0.01, + "thermostat": { + "setpoint": 13.0, + "lower_bound": 0.0, + "upper_bound": 99.9, + "resolution": 0.01 + }, "preset_modes": ["home", "asleep", "away", "no_frost"], "active_preset": "away", "available_schedules": [ @@ -50,9 +53,6 @@ "name": "Floor kraan", "zigbee_mac_address": "ABCD012345670A02", "vendor": "Plugwise", - "lower_bound": 0.0, - "upper_bound": 100.0, - "resolution": 0.01, "sensors": { "temperature": 26.0, "setpoint": 21.5, @@ -69,9 +69,6 @@ "name": "Bios Cv Thermostatic Radiator ", "zigbee_mac_address": "ABCD012345670A09", "vendor": "Plugwise", - "lower_bound": 0.0, - "upper_bound": 100.0, - "resolution": 0.01, "sensors": { "temperature": 17.2, "setpoint": 13.0, @@ -89,9 +86,12 @@ "name": "Zone Lisa WK", "zigbee_mac_address": "ABCD012345670A07", "vendor": "Plugwise", - "lower_bound": 0.0, - "upper_bound": 99.9, - "resolution": 0.01, + "thermostat": { + "setpoint": 21.5, + "lower_bound": 0.0, + "upper_bound": 99.9, + "resolution": 0.01 + }, "preset_modes": ["home", "asleep", "away", "no_frost"], "active_preset": "home", "available_schedules": [ @@ -138,9 +138,6 @@ "name": "Thermostatic Radiator Jessie", "zigbee_mac_address": "ABCD012345670A10", "vendor": "Plugwise", - "lower_bound": 0.0, - "upper_bound": 100.0, - "resolution": 0.01, "sensors": { "temperature": 17.1, "setpoint": 15.0, @@ -285,9 +282,12 @@ "name": "Zone Thermostat Jessie", "zigbee_mac_address": "ABCD012345670A03", "vendor": "Plugwise", - "lower_bound": 0.0, - "upper_bound": 99.9, - "resolution": 0.01, + "thermostat": { + "setpoint": 15.0, + "lower_bound": 0.0, + "upper_bound": 99.9, + "resolution": 0.01 + }, "preset_modes": ["home", "asleep", "away", "no_frost"], "active_preset": "asleep", "available_schedules": [ @@ -315,9 +315,6 @@ "name": "Thermostatic Radiator Badkamer", "zigbee_mac_address": "ABCD012345670A17", "vendor": "Plugwise", - "lower_bound": 0.0, - "upper_bound": 100.0, - "resolution": 0.01, "sensors": { "temperature": 19.1, "setpoint": 14.0, @@ -335,9 +332,12 @@ "name": "Zone Thermostat Badkamer", "zigbee_mac_address": "ABCD012345670A08", "vendor": "Plugwise", - "lower_bound": 0.0, - "upper_bound": 99.9, - "resolution": 0.01, + "thermostat": { + "setpoint": 14.0, + "lower_bound": 0.0, + "upper_bound": 99.9, + "resolution": 0.01 + }, "preset_modes": ["home", "asleep", "away", "no_frost"], "active_preset": "away", "available_schedules": [ @@ -384,9 +384,12 @@ "name": "CV Kraan Garage", "zigbee_mac_address": "ABCD012345670A11", "vendor": "Plugwise", - "lower_bound": 0.0, - "upper_bound": 100.0, - "resolution": 0.01, + "thermostat": { + "setpoint": 5.5, + "lower_bound": 0.0, + "upper_bound": 100.0, + "resolution": 0.01 + }, "preset_modes": ["home", "asleep", "away", "no_frost"], "active_preset": "no_frost", "available_schedules": [ diff --git a/tests/components/plugwise/fixtures/anna_heatpump/all_data.json b/tests/components/plugwise/fixtures/anna_heatpump_heating/all_data.json similarity index 81% rename from tests/components/plugwise/fixtures/anna_heatpump/all_data.json rename to tests/components/plugwise/fixtures/anna_heatpump_heating/all_data.json index 6fcb841cf3e..1cc94ca6347 100644 --- a/tests/components/plugwise/fixtures/anna_heatpump/all_data.json +++ b/tests/components/plugwise/fixtures/anna_heatpump_heating/all_data.json @@ -1,6 +1,6 @@ [ { - "smile_name": "Anna", + "smile_name": "Smile", "gateway_id": "015ae9ea3f964e668e490fa39da3870b", "heater_id": "1cbf783bb11e4a7c8a6843dee3a86927", "cooling_present": true, @@ -10,13 +10,16 @@ "1cbf783bb11e4a7c8a6843dee3a86927": { "dev_class": "heater_central", "location": "a57efe5f145f498c9be62a9b63626fbf", - "model": "Generic heater", + "model": "Generic heater/cooler", "name": "OpenTherm", "vendor": "Techneco", - "lower_bound": 0.0, - "upper_bound": 100.0, - "resolution": 1.0, - "maximum_boiler_temperature": 60.0, + "maximum_boiler_temperature": { + "setpoint": 60.0, + "lower_bound": 0.0, + "upper_bound": 100.0, + "resolution": 1.0 + }, + "elga_cooling_enabled": true, "binary_sensors": { "dhw_state": false, "heating_state": true, @@ -43,8 +46,8 @@ "hardware": "AME Smile 2.0 board", "location": "a57efe5f145f498c9be62a9b63626fbf", "mac_address": "012345670001", - "model": "Anna", - "name": "Anna", + "model": "Smile", + "name": "Smile", "vendor": "Plugwise B.V.", "binary_sensors": { "plugwise_notification": false @@ -61,9 +64,12 @@ "model": "Anna", "name": "Anna", "vendor": "Plugwise", - "lower_bound": 4.0, - "upper_bound": 30.0, - "resolution": 0.1, + "thermostat": { + "setpoint": 20.5, + "lower_bound": 4.0, + "upper_bound": 30.0, + "resolution": 0.1 + }, "preset_modes": ["no_frost", "home", "away", "asleep", "vacation"], "active_preset": "home", "available_schedules": ["standaard"], @@ -72,7 +78,7 @@ "mode": "auto", "sensors": { "temperature": 19.3, - "setpoint": 21.0, + "setpoint": 20.5, "illuminance": 86.0, "cooling_activation_outdoor_temperature": 21.0, "cooling_deactivation_threshold": 4.0 diff --git a/tests/components/plugwise/fixtures/anna_heatpump/notifications.json b/tests/components/plugwise/fixtures/anna_heatpump_heating/notifications.json similarity index 100% rename from tests/components/plugwise/fixtures/anna_heatpump/notifications.json rename to tests/components/plugwise/fixtures/anna_heatpump_heating/notifications.json diff --git a/tests/components/plugwise/fixtures/m_adam_cooling/all_data.json b/tests/components/plugwise/fixtures/m_adam_cooling/all_data.json new file mode 100644 index 00000000000..1f7c82983d4 --- /dev/null +++ b/tests/components/plugwise/fixtures/m_adam_cooling/all_data.json @@ -0,0 +1,139 @@ +[ + { + "smile_name": "Adam", + "gateway_id": "da224107914542988a88561b4452b0f6", + "heater_id": "056ee145a816487eaa69243c3280f8bf", + "cooling_present": true, + "notifications": {} + }, + { + "ad4838d7d35c4d6ea796ee12ae5aedf8": { + "dev_class": "thermostat", + "location": "f2bf9048bef64cc5b6d5110154e33c81", + "model": "Anna", + "name": "Anna", + "vendor": "Plugwise B.V.", + "thermostat": { + "setpoint": 18.5, + "lower_bound": 1.0, + "upper_bound": 35.0, + "resolution": 0.01 + }, + "preset_modes": ["home", "asleep", "away", "no_frost"], + "active_preset": "asleep", + "available_schedules": ["Weekschema", "Badkamer", "Test"], + "selected_schedule": "None", + "last_used": "Weekschema", + "control_state": "cooling", + "mode": "cool", + "sensors": { "temperature": 18.1, "setpoint": 18.5 } + }, + "1772a4ea304041adb83f357b751341ff": { + "dev_class": "thermo_sensor", + "firmware": "2020-11-04T01:00:00+01:00", + "hardware": "1", + "location": "f871b8c4d63549319221e294e4f88074", + "model": "Tom/Floor", + "name": "Tom Badkamer", + "zigbee_mac_address": "ABCD012345670A01", + "vendor": "Plugwise", + "sensors": { + "temperature": 21.6, + "battery": 99, + "temperature_difference": 2.3, + "valve_position": 0.0 + } + }, + "e2f4322d57924fa090fbbc48b3a140dc": { + "dev_class": "zone_thermostat", + "firmware": "2016-10-10T02:00:00+02:00", + "hardware": "255", + "location": "f871b8c4d63549319221e294e4f88074", + "model": "Lisa", + "name": "Lisa Badkamer", + "zigbee_mac_address": "ABCD012345670A04", + "vendor": "Plugwise", + "thermostat": { + "setpoint": 15.0, + "lower_bound": 0.0, + "upper_bound": 99.9, + "resolution": 0.01 + }, + "preset_modes": ["home", "asleep", "away", "no_frost"], + "active_preset": "home", + "available_schedules": ["Weekschema", "Badkamer", "Test"], + "selected_schedule": "Badkamer", + "last_used": "Badkamer", + "control_state": "off", + "mode": "auto", + "sensors": { + "temperature": 17.9, + "battery": 56, + "setpoint": 15.0 + } + }, + "da224107914542988a88561b4452b0f6": { + "dev_class": "gateway", + "firmware": "3.6.4", + "hardware": "AME Smile 2.0 board", + "location": "bc93488efab249e5bc54fd7e175a6f91", + "mac_address": "012345670001", + "model": "Adam", + "name": "Adam", + "zigbee_mac_address": "ABCD012345670101", + "vendor": "Plugwise B.V.", + "regulation_mode": "cooling", + "regulation_modes": [ + "cooling", + "heating", + "off", + "bleeding_cold", + "bleeding_hot" + ], + "binary_sensors": { + "plugwise_notification": false + }, + "sensors": { + "outdoor_temperature": -1.25 + } + }, + "056ee145a816487eaa69243c3280f8bf": { + "dev_class": "heater_central", + "location": "bc93488efab249e5bc54fd7e175a6f91", + "model": "Generic heater", + "name": "OpenTherm", + "maximum_boiler_temperature": { + "setpoint": 60.0, + "lower_bound": 25.0, + "upper_bound": 95.0, + "resolution": 0.01 + }, + "adam_cooling_enabled": true, + "binary_sensors": { + "cooling_state": true, + "dhw_state": false, + "heating_state": false, + "flame_state": false + }, + "sensors": { + "water_temperature": 37.0, + "intended_boiler_temperature": 38.1 + }, + "switches": { + "dhw_cm_switch": false + } + }, + "e8ef2a01ed3b4139a53bf749204fe6b4": { + "dev_class": "switching", + "model": "Switchgroup", + "name": "Test", + "members": [ + "2568cc4b9c1e401495d4741a5f89bee1", + "29542b2b6a6a4169acecc15c72a599b8" + ], + "switches": { + "relay": true + } + } + } +] diff --git a/tests/components/plugwise/fixtures/m_adam_cooling/notifications.json b/tests/components/plugwise/fixtures/m_adam_cooling/notifications.json new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/tests/components/plugwise/fixtures/m_adam_cooling/notifications.json @@ -0,0 +1 @@ +{} diff --git a/tests/components/plugwise/fixtures/m_adam_heating/all_data.json b/tests/components/plugwise/fixtures/m_adam_heating/all_data.json new file mode 100644 index 00000000000..0a00a5b7b1c --- /dev/null +++ b/tests/components/plugwise/fixtures/m_adam_heating/all_data.json @@ -0,0 +1,137 @@ +[ + { + "smile_name": "Adam", + "gateway_id": "da224107914542988a88561b4452b0f6", + "heater_id": "056ee145a816487eaa69243c3280f8bf", + "cooling_present": false, + "notifications": {} + }, + { + "ad4838d7d35c4d6ea796ee12ae5aedf8": { + "dev_class": "thermostat", + "location": "f2bf9048bef64cc5b6d5110154e33c81", + "model": "Anna", + "name": "Anna", + "vendor": "Plugwise B.V.", + "thermostat": { + "setpoint": 18.5, + "lower_bound": 1.0, + "upper_bound": 35.0, + "resolution": 0.01 + }, + "preset_modes": ["home", "asleep", "away", "no_frost"], + "active_preset": "asleep", + "available_schedules": ["Weekschema", "Badkamer", "Test"], + "selected_schedule": "None", + "last_used": "Weekschema", + "control_state": "heating", + "mode": "heat", + "sensors": { "temperature": 18.1, "setpoint": 18.5 } + }, + "1772a4ea304041adb83f357b751341ff": { + "dev_class": "thermo_sensor", + "firmware": "2020-11-04T01:00:00+01:00", + "hardware": "1", + "location": "f871b8c4d63549319221e294e4f88074", + "model": "Tom/Floor", + "name": "Tom Badkamer", + "zigbee_mac_address": "ABCD012345670A01", + "vendor": "Plugwise", + "sensors": { + "temperature": 21.6, + "battery": 99, + "temperature_difference": 2.3, + "valve_position": 0.0 + } + }, + "e2f4322d57924fa090fbbc48b3a140dc": { + "dev_class": "zone_thermostat", + "firmware": "2016-10-10T02:00:00+02:00", + "hardware": "255", + "location": "f871b8c4d63549319221e294e4f88074", + "model": "Lisa", + "name": "Lisa Badkamer", + "zigbee_mac_address": "ABCD012345670A04", + "vendor": "Plugwise", + "thermostat": { + "setpoint": 15.0, + "lower_bound": 0.0, + "upper_bound": 99.9, + "resolution": 0.01 + }, + "preset_modes": ["home", "asleep", "away", "no_frost"], + "active_preset": "home", + "available_schedules": ["Weekschema", "Badkamer", "Test"], + "selected_schedule": "Badkamer", + "last_used": "Badkamer", + "control_state": "off", + "mode": "auto", + "sensors": { + "temperature": 17.9, + "battery": 56, + "setpoint": 15.0 + } + }, + "da224107914542988a88561b4452b0f6": { + "dev_class": "gateway", + "firmware": "3.6.4", + "hardware": "AME Smile 2.0 board", + "location": "bc93488efab249e5bc54fd7e175a6f91", + "mac_address": "012345670001", + "model": "Adam", + "name": "Adam", + "zigbee_mac_address": "ABCD012345670101", + "vendor": "Plugwise B.V.", + "regulation_mode": "heating", + "regulation_modes": ["heating", "off", "bleeding_cold", "bleeding_hot"], + "binary_sensors": { + "plugwise_notification": false + }, + "sensors": { + "outdoor_temperature": -1.25 + } + }, + "056ee145a816487eaa69243c3280f8bf": { + "dev_class": "heater_central", + "location": "bc93488efab249e5bc54fd7e175a6f91", + "model": "Generic heater", + "name": "OpenTherm", + "maximum_boiler_temperature": { + "setpoint": 60.0, + "lower_bound": 25.0, + "upper_bound": 95.0, + "resolution": 0.01 + }, + "domestic_hot_water_setpoint": { + "setpoint": 60.0, + "lower_bound": 40.0, + "upper_bound": 60.0, + "resolution": 0.01 + }, + "binary_sensors": { + "dhw_state": false, + "heating_state": true, + "flame_state": false + }, + "sensors": { + "water_temperature": 37.0, + "intended_boiler_temperature": 38.1 + }, + "switches": { + "dhw_cm_switch": false + } + }, + "e8ef2a01ed3b4139a53bf749204fe6b4": { + "dev_class": "switching", + "model": "Switchgroup", + "name": "Test", + "members": [ + "2568cc4b9c1e401495d4741a5f89bee1", + "29542b2b6a6a4169acecc15c72a599b8" + ], + "switches": { + "relay": true + } + } + } +] diff --git a/tests/components/plugwise/fixtures/m_adam_heating/notifications.json b/tests/components/plugwise/fixtures/m_adam_heating/notifications.json new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/tests/components/plugwise/fixtures/m_adam_heating/notifications.json @@ -0,0 +1 @@ +{} diff --git a/tests/components/plugwise/fixtures/m_anna_heatpump_cooling/all_data.json b/tests/components/plugwise/fixtures/m_anna_heatpump_cooling/all_data.json new file mode 100644 index 00000000000..a9a92126265 --- /dev/null +++ b/tests/components/plugwise/fixtures/m_anna_heatpump_cooling/all_data.json @@ -0,0 +1,88 @@ +[ + { + "smile_name": "Smile", + "gateway_id": "015ae9ea3f964e668e490fa39da3870b", + "heater_id": "1cbf783bb11e4a7c8a6843dee3a86927", + "cooling_present": true, + "notifications": {} + }, + { + "1cbf783bb11e4a7c8a6843dee3a86927": { + "dev_class": "heater_central", + "location": "a57efe5f145f498c9be62a9b63626fbf", + "model": "Generic heater/cooler", + "name": "OpenTherm", + "vendor": "Techneco", + "maximum_boiler_temperature": { + "setpoint": 60.0, + "lower_bound": 0.0, + "upper_bound": 100.0, + "resolution": 1.0 + }, + "elga_cooling_enabled": true, + "binary_sensors": { + "dhw_state": false, + "heating_state": false, + "compressor_state": true, + "cooling_state": true, + "slave_boiler_state": false, + "flame_state": false + }, + "sensors": { + "water_temperature": 29.1, + "intended_boiler_temperature": 0.0, + "modulation_level": 52, + "return_temperature": 25.1, + "water_pressure": 1.57, + "outdoor_air_temperature": 3.0 + }, + "switches": { + "dhw_cm_switch": false + } + }, + "015ae9ea3f964e668e490fa39da3870b": { + "dev_class": "gateway", + "firmware": "4.0.15", + "hardware": "AME Smile 2.0 board", + "location": "a57efe5f145f498c9be62a9b63626fbf", + "mac_address": "012345670001", + "model": "Smile", + "name": "Smile", + "vendor": "Plugwise B.V.", + "binary_sensors": { + "plugwise_notification": false + }, + "sensors": { + "outdoor_temperature": 28.2 + } + }, + "3cb70739631c4d17a86b8b12e8a5161b": { + "dev_class": "thermostat", + "firmware": "2018-02-08T11:15:53+01:00", + "hardware": "6539-1301-5002", + "location": "c784ee9fdab44e1395b8dee7d7a497d5", + "model": "Anna", + "name": "Anna", + "vendor": "Plugwise", + "thermostat": { + "setpoint": 24.0, + "lower_bound": 4.0, + "upper_bound": 30.0, + "resolution": 0.1 + }, + "preset_modes": ["no_frost", "home", "away", "asleep", "vacation"], + "active_preset": "home", + "available_schedules": ["standaard"], + "selected_schedule": "standaard", + "last_used": "standaard", + "mode": "auto", + "sensors": { + "temperature": 26.3, + "setpoint": 24.0, + "illuminance": 86.0, + "cooling_activation_outdoor_temperature": 21.0, + "cooling_deactivation_threshold": 4.0 + } + } + } +] diff --git a/tests/components/plugwise/fixtures/m_anna_heatpump_cooling/notifications.json b/tests/components/plugwise/fixtures/m_anna_heatpump_cooling/notifications.json new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/tests/components/plugwise/fixtures/m_anna_heatpump_cooling/notifications.json @@ -0,0 +1 @@ +{} diff --git a/tests/components/plugwise/fixtures/m_anna_heatpump_idle/all_data.json b/tests/components/plugwise/fixtures/m_anna_heatpump_idle/all_data.json new file mode 100644 index 00000000000..0c1fef1a171 --- /dev/null +++ b/tests/components/plugwise/fixtures/m_anna_heatpump_idle/all_data.json @@ -0,0 +1,88 @@ +[ + { + "smile_name": "Smile", + "gateway_id": "015ae9ea3f964e668e490fa39da3870b", + "heater_id": "1cbf783bb11e4a7c8a6843dee3a86927", + "cooling_present": true, + "notifications": {} + }, + { + "1cbf783bb11e4a7c8a6843dee3a86927": { + "dev_class": "heater_central", + "location": "a57efe5f145f498c9be62a9b63626fbf", + "model": "Generic heater/cooler", + "name": "OpenTherm", + "vendor": "Techneco", + "maximum_boiler_temperature": { + "setpoint": 60.0, + "lower_bound": 0.0, + "upper_bound": 100.0, + "resolution": 1.0 + }, + "elga_cooling_enabled": true, + "binary_sensors": { + "dhw_state": false, + "heating_state": false, + "compressor_state": false, + "cooling_state": false, + "slave_boiler_state": false, + "flame_state": false + }, + "sensors": { + "water_temperature": 29.1, + "intended_boiler_temperature": 0.0, + "modulation_level": 52, + "return_temperature": 25.1, + "water_pressure": 1.57, + "outdoor_air_temperature": 3.0 + }, + "switches": { + "dhw_cm_switch": false + } + }, + "015ae9ea3f964e668e490fa39da3870b": { + "dev_class": "gateway", + "firmware": "4.0.15", + "hardware": "AME Smile 2.0 board", + "location": "a57efe5f145f498c9be62a9b63626fbf", + "mac_address": "012345670001", + "model": "Smile", + "name": "Smile", + "vendor": "Plugwise B.V.", + "binary_sensors": { + "plugwise_notification": false + }, + "sensors": { + "outdoor_temperature": 20.2 + } + }, + "3cb70739631c4d17a86b8b12e8a5161b": { + "dev_class": "thermostat", + "firmware": "2018-02-08T11:15:53+01:00", + "hardware": "6539-1301-5002", + "location": "c784ee9fdab44e1395b8dee7d7a497d5", + "model": "Anna", + "name": "Anna", + "vendor": "Plugwise", + "thermostat": { + "setpoint": 20.5, + "lower_bound": 4.0, + "upper_bound": 30.0, + "resolution": 0.1 + }, + "preset_modes": ["no_frost", "home", "away", "asleep", "vacation"], + "active_preset": "home", + "available_schedules": ["standaard"], + "selected_schedule": "standaard", + "last_used": "standaard", + "mode": "auto", + "sensors": { + "temperature": 21.3, + "setpoint": 20.5, + "illuminance": 86.0, + "cooling_activation_outdoor_temperature": 21.0, + "cooling_deactivation_threshold": 4.0 + } + } + } +] diff --git a/tests/components/plugwise/fixtures/m_anna_heatpump_idle/notifications.json b/tests/components/plugwise/fixtures/m_anna_heatpump_idle/notifications.json new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/tests/components/plugwise/fixtures/m_anna_heatpump_idle/notifications.json @@ -0,0 +1 @@ +{} diff --git a/tests/components/plugwise/test_climate.py b/tests/components/plugwise/test_climate.py index 58faeda8d7c..bcca1a32abb 100644 --- a/tests/components/plugwise/test_climate.py +++ b/tests/components/plugwise/test_climate.py @@ -1,7 +1,7 @@ """Tests for the Plugwise Climate integration.""" from unittest.mock import MagicMock -from plugwise.exceptions import PlugwiseException +from plugwise.exceptions import PlugwiseError import pytest from homeassistant.components.climate import HVACMode @@ -16,12 +16,10 @@ async def test_adam_climate_entity_attributes( ) -> None: """Test creation of adam climate device environment.""" state = hass.states.get("climate.zone_lisa_wk") - assert state - assert state.attributes["hvac_modes"] == [ - HVACMode.HEAT, - HVACMode.AUTO, - ] + assert state.state == HVACMode.AUTO + assert state.attributes["hvac_modes"] == [HVACMode.HEAT, HVACMode.AUTO] + # hvac_action is not asserted as the fixture is not in line with recent firmware functionality assert "preset_modes" in state.attributes assert "no_frost" in state.attributes["preset_modes"] @@ -37,11 +35,9 @@ async def test_adam_climate_entity_attributes( state = hass.states.get("climate.zone_thermostat_jessie") assert state - - assert state.attributes["hvac_modes"] == [ - HVACMode.HEAT, - HVACMode.AUTO, - ] + assert state.state == HVACMode.AUTO + assert state.attributes["hvac_modes"] == [HVACMode.HEAT, HVACMode.AUTO] + # hvac_action is not asserted as the fixture is not in line with recent firmware functionality assert "preset_modes" in state.attributes assert "no_frost" in state.attributes["preset_modes"] @@ -55,13 +51,46 @@ async def test_adam_climate_entity_attributes( assert state.attributes["target_temp_step"] == 0.1 +async def test_adam_2_climate_entity_attributes( + hass: HomeAssistant, mock_smile_adam_2: MagicMock, init_integration: MockConfigEntry +) -> None: + """Test creation of adam climate device environment.""" + state = hass.states.get("climate.anna") + assert state + assert state.state == HVACMode.HEAT + assert state.attributes["hvac_action"] == "heating" + assert state.attributes["hvac_modes"] == [HVACMode.HEAT, HVACMode.AUTO] + + state = hass.states.get("climate.lisa_badkamer") + assert state + assert state.state == HVACMode.AUTO + assert state.attributes["hvac_action"] == "idle" + assert state.attributes["hvac_modes"] == [HVACMode.HEAT, HVACMode.AUTO] + + +async def test_adam_3_climate_entity_attributes( + hass: HomeAssistant, mock_smile_adam_3: MagicMock, init_integration: MockConfigEntry +) -> None: + """Test creation of adam climate device environment.""" + state = hass.states.get("climate.anna") + + assert state + assert state.state == HVACMode.COOL + assert state.attributes["hvac_action"] == "cooling" + assert state.attributes["hvac_modes"] == [ + HVACMode.HEAT, + HVACMode.COOL, + HVACMode.AUTO, + ] + + async def test_adam_climate_adjust_negative_testing( hass: HomeAssistant, mock_smile_adam: MagicMock, init_integration: MockConfigEntry ) -> None: """Test exceptions of climate entities.""" - mock_smile_adam.set_preset.side_effect = PlugwiseException - mock_smile_adam.set_schedule_state.side_effect = PlugwiseException - mock_smile_adam.set_temperature.side_effect = PlugwiseException + mock_smile_adam.set_preset.side_effect = PlugwiseError + mock_smile_adam.set_schedule_state.side_effect = PlugwiseError + mock_smile_adam.set_temperature.side_effect = PlugwiseError with pytest.raises(HomeAssistantError): await hass.services.async_call( @@ -107,6 +136,14 @@ async def test_adam_climate_entity_climate_changes( "c50f167537524366a5af7aa3942feb1e", 25.0 ) + with pytest.raises(ValueError): + await hass.services.async_call( + "climate", + "set_temperature", + {"entity_id": "climate.zone_lisa_wk", "temperature": 150}, + blocking=True, + ) + await hass.services.async_call( "climate", "set_preset_mode", @@ -143,32 +180,82 @@ async def test_adam_climate_entity_climate_changes( "82fa13f017d240daa0d0ea1775420f24", "home" ) + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + "climate", + "set_hvac_mode", + { + "entity_id": "climate.zone_thermostat_jessie", + "hvac_mode": "dry", + }, + blocking=True, + ) + async def test_anna_climate_entity_attributes( - hass: HomeAssistant, mock_smile_anna: MagicMock, init_integration: MockConfigEntry + hass: HomeAssistant, + mock_smile_anna: MagicMock, + init_integration: MockConfigEntry, ) -> None: """Test creation of anna climate device environment.""" state = hass.states.get("climate.anna") assert state assert state.state == HVACMode.AUTO + assert state.attributes["hvac_action"] == "heating" assert state.attributes["hvac_modes"] == [ HVACMode.HEAT, HVACMode.COOL, HVACMode.AUTO, ] + assert "no_frost" in state.attributes["preset_modes"] assert "home" in state.attributes["preset_modes"] assert state.attributes["current_temperature"] == 19.3 - assert state.attributes["hvac_action"] == "heating" assert state.attributes["preset_mode"] == "home" assert state.attributes["supported_features"] == 17 - assert state.attributes["temperature"] == 21.0 + assert state.attributes["temperature"] == 20.5 assert state.attributes["min_temp"] == 4.0 assert state.attributes["max_temp"] == 30.0 assert state.attributes["target_temp_step"] == 0.1 +async def test_anna_2_climate_entity_attributes( + hass: HomeAssistant, + mock_smile_anna_2: MagicMock, + init_integration: MockConfigEntry, +) -> None: + """Test creation of anna climate device environment.""" + state = hass.states.get("climate.anna") + assert state + assert state.state == HVACMode.AUTO + assert state.attributes["hvac_action"] == "cooling" + assert state.attributes["hvac_modes"] == [ + HVACMode.HEAT, + HVACMode.COOL, + HVACMode.AUTO, + ] + assert state.attributes["temperature"] == 24.0 + assert state.attributes["supported_features"] == 17 + + +async def test_anna_3_climate_entity_attributes( + hass: HomeAssistant, + mock_smile_anna_3: MagicMock, + init_integration: MockConfigEntry, +) -> None: + """Test creation of anna climate device environment.""" + state = hass.states.get("climate.anna") + assert state + assert state.state == HVACMode.AUTO + assert state.attributes["hvac_action"] == "idle" + assert state.attributes["hvac_modes"] == [ + HVACMode.HEAT, + HVACMode.COOL, + HVACMode.AUTO, + ] + + async def test_anna_climate_entity_climate_changes( hass: HomeAssistant, mock_smile_anna: MagicMock, init_integration: MockConfigEntry ) -> None: @@ -182,7 +269,8 @@ async def test_anna_climate_entity_climate_changes( assert mock_smile_anna.set_temperature.call_count == 1 mock_smile_anna.set_temperature.assert_called_with( - "c784ee9fdab44e1395b8dee7d7a497d5", 25.0 + "c784ee9fdab44e1395b8dee7d7a497d5", + 25.0, ) await hass.services.async_call( @@ -209,3 +297,15 @@ async def test_anna_climate_entity_climate_changes( mock_smile_anna.set_schedule_state.assert_called_with( "c784ee9fdab44e1395b8dee7d7a497d5", "standaard", "off" ) + + await hass.services.async_call( + "climate", + "set_hvac_mode", + {"entity_id": "climate.anna", "hvac_mode": "auto"}, + blocking=True, + ) + + assert mock_smile_anna.set_schedule_state.call_count == 2 + mock_smile_anna.set_schedule_state.assert_called_with( + "c784ee9fdab44e1395b8dee7d7a497d5", "standaard", "on" + ) diff --git a/tests/components/plugwise/test_diagnostics.py b/tests/components/plugwise/test_diagnostics.py index 372f410cd81..3e3b2259e15 100644 --- a/tests/components/plugwise/test_diagnostics.py +++ b/tests/components/plugwise/test_diagnostics.py @@ -40,9 +40,12 @@ async def test_diagnostics( "name": "Zone Lisa Bios", "zigbee_mac_address": "ABCD012345670A06", "vendor": "Plugwise", - "lower_bound": 0.0, - "upper_bound": 99.9, - "resolution": 0.01, + "thermostat": { + "setpoint": 13.0, + "lower_bound": 0.0, + "upper_bound": 99.9, + "resolution": 0.01, + }, "preset_modes": ["home", "asleep", "away", "no_frost"], "active_preset": "away", "available_schedules": [ @@ -66,9 +69,6 @@ async def test_diagnostics( "name": "Floor kraan", "zigbee_mac_address": "ABCD012345670A02", "vendor": "Plugwise", - "lower_bound": 0.0, - "upper_bound": 100.0, - "resolution": 0.01, "sensors": { "temperature": 26.0, "setpoint": 21.5, @@ -85,9 +85,6 @@ async def test_diagnostics( "name": "Bios Cv Thermostatic Radiator ", "zigbee_mac_address": "ABCD012345670A09", "vendor": "Plugwise", - "lower_bound": 0.0, - "upper_bound": 100.0, - "resolution": 0.01, "sensors": { "temperature": 17.2, "setpoint": 13.0, @@ -105,9 +102,12 @@ async def test_diagnostics( "name": "Zone Lisa WK", "zigbee_mac_address": "ABCD012345670A07", "vendor": "Plugwise", - "lower_bound": 0.0, - "upper_bound": 99.9, - "resolution": 0.01, + "thermostat": { + "setpoint": 21.5, + "lower_bound": 0.0, + "upper_bound": 99.9, + "resolution": 0.01, + }, "preset_modes": ["home", "asleep", "away", "no_frost"], "active_preset": "home", "available_schedules": [ @@ -146,9 +146,6 @@ async def test_diagnostics( "name": "Thermostatic Radiator Jessie", "zigbee_mac_address": "ABCD012345670A10", "vendor": "Plugwise", - "lower_bound": 0.0, - "upper_bound": 100.0, - "resolution": 0.01, "sensors": { "temperature": 17.1, "setpoint": 15.0, @@ -274,9 +271,12 @@ async def test_diagnostics( "name": "Zone Thermostat Jessie", "zigbee_mac_address": "ABCD012345670A03", "vendor": "Plugwise", - "lower_bound": 0.0, - "upper_bound": 99.9, - "resolution": 0.01, + "thermostat": { + "setpoint": 15.0, + "lower_bound": 0.0, + "upper_bound": 99.9, + "resolution": 0.01, + }, "preset_modes": ["home", "asleep", "away", "no_frost"], "active_preset": "asleep", "available_schedules": [ @@ -300,9 +300,6 @@ async def test_diagnostics( "name": "Thermostatic Radiator Badkamer", "zigbee_mac_address": "ABCD012345670A17", "vendor": "Plugwise", - "lower_bound": 0.0, - "upper_bound": 100.0, - "resolution": 0.01, "sensors": { "temperature": 19.1, "setpoint": 14.0, @@ -320,9 +317,12 @@ async def test_diagnostics( "name": "Zone Thermostat Badkamer", "zigbee_mac_address": "ABCD012345670A08", "vendor": "Plugwise", - "lower_bound": 0.0, - "upper_bound": 99.9, - "resolution": 0.01, + "thermostat": { + "setpoint": 14.0, + "lower_bound": 0.0, + "upper_bound": 99.9, + "resolution": 0.01, + }, "preset_modes": ["home", "asleep", "away", "no_frost"], "active_preset": "away", "available_schedules": [ @@ -362,9 +362,12 @@ async def test_diagnostics( "name": "CV Kraan Garage", "zigbee_mac_address": "ABCD012345670A11", "vendor": "Plugwise", - "lower_bound": 0.0, - "upper_bound": 100.0, - "resolution": 0.01, + "thermostat": { + "setpoint": 5.5, + "lower_bound": 0.0, + "upper_bound": 100.0, + "resolution": 0.01, + }, "preset_modes": ["home", "asleep", "away", "no_frost"], "active_preset": "no_frost", "available_schedules": [ diff --git a/tests/components/plugwise/test_number.py b/tests/components/plugwise/test_number.py index a4e084e5d3a..da31b8038c8 100644 --- a/tests/components/plugwise/test_number.py +++ b/tests/components/plugwise/test_number.py @@ -36,5 +36,7 @@ async def test_anna_max_boiler_temp_change( blocking=True, ) - assert mock_smile_anna.set_max_boiler_temperature.call_count == 1 - mock_smile_anna.set_max_boiler_temperature.assert_called_with(65) + assert mock_smile_anna.set_number_setpoint.call_count == 1 + mock_smile_anna.set_number_setpoint.assert_called_with( + "maximum_boiler_temperature", 65.0 + ) From 408069cb0c0468a6df620c5305e67ea74207c5dc Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 26 Sep 2022 15:59:01 +0200 Subject: [PATCH 739/955] Always install requirements of after_dependencies (#79094) --- homeassistant/setup.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 8d5f9e52025..d85e4043505 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -357,11 +357,10 @@ async def async_process_deps_reqs( if failed_deps := await _async_process_dependencies(hass, config, integration): raise DependencyError(failed_deps) - if not hass.config.skip_pip and integration.requirements: - async with hass.timeout.async_freeze(integration.domain): - await requirements.async_get_integration_with_requirements( - hass, integration.domain - ) + async with hass.timeout.async_freeze(integration.domain): + await requirements.async_get_integration_with_requirements( + hass, integration.domain + ) processed.add(integration.domain) From 58e9ad8c1dce44c1ab5453bce6eb5b8a6b68298f Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 26 Sep 2022 16:07:45 +0200 Subject: [PATCH 740/955] Start deprecation yaml moon (#77780) --- homeassistant/components/moon/sensor.py | 10 ++++++++++ homeassistant/components/moon/strings.json | 6 ++++++ homeassistant/components/moon/translations/en.json | 6 ++++++ 3 files changed, 22 insertions(+) diff --git a/homeassistant/components/moon/sensor.py b/homeassistant/components/moon/sensor.py index f3a3b3f48fa..b033dccc296 100644 --- a/homeassistant/components/moon/sensor.py +++ b/homeassistant/components/moon/sensor.py @@ -15,6 +15,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType import homeassistant.util.dt as dt_util @@ -52,6 +53,15 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Moon sensor.""" + async_create_issue( + hass, + DOMAIN, + "removed_yaml", + breaks_in_ha_version="2022.12.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="removed_yaml", + ) hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, diff --git a/homeassistant/components/moon/strings.json b/homeassistant/components/moon/strings.json index d5bb204a740..76ce886ded8 100644 --- a/homeassistant/components/moon/strings.json +++ b/homeassistant/components/moon/strings.json @@ -9,5 +9,11 @@ "abort": { "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" } + }, + "issues": { + "removed_yaml": { + "title": "The Moon YAML configuration has been removed", + "description": "Configuring Moon using YAML has been removed.\n\nYour existing YAML configuration is not used by Home Assistant.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } } } diff --git a/homeassistant/components/moon/translations/en.json b/homeassistant/components/moon/translations/en.json index 0f324f7b64b..2f6f73a9982 100644 --- a/homeassistant/components/moon/translations/en.json +++ b/homeassistant/components/moon/translations/en.json @@ -9,5 +9,11 @@ } } }, + "issues": { + "removed_yaml": { + "description": "Configuring Moon using YAML has been removed.\n\nYour existing YAML configuration is not used by Home Assistant.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Moon YAML configuration has been removed" + } + }, "title": "Moon" } \ No newline at end of file From 05568792d104d2c81cfe9493adf7a0372834632c Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 26 Sep 2022 16:08:27 +0200 Subject: [PATCH 741/955] Start deprecation yaml season (#77781) --- homeassistant/components/season/sensor.py | 10 ++++++++++ homeassistant/components/season/strings.json | 6 ++++++ homeassistant/components/season/translations/en.json | 6 ++++++ 3 files changed, 22 insertions(+) diff --git a/homeassistant/components/season/sensor.py b/homeassistant/components/season/sensor.py index 5fc161062bb..8bb8773860d 100644 --- a/homeassistant/components/season/sensor.py +++ b/homeassistant/components/season/sensor.py @@ -17,6 +17,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util.dt import utcnow @@ -62,6 +63,15 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the season sensor platform.""" + async_create_issue( + hass, + DOMAIN, + "removed_yaml", + breaks_in_ha_version="2022.12.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="removed_yaml", + ) hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, diff --git a/homeassistant/components/season/strings.json b/homeassistant/components/season/strings.json index c75c0f1c507..25d121d16e6 100644 --- a/homeassistant/components/season/strings.json +++ b/homeassistant/components/season/strings.json @@ -10,5 +10,11 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" } + }, + "issues": { + "removed_yaml": { + "title": "The Season YAML configuration has been removed", + "description": "Configuring Season using YAML has been removed.\n\nYour existing YAML configuration is not used by Home Assistant.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } } } diff --git a/homeassistant/components/season/translations/en.json b/homeassistant/components/season/translations/en.json index 1638f3c0a20..5508c305fea 100644 --- a/homeassistant/components/season/translations/en.json +++ b/homeassistant/components/season/translations/en.json @@ -10,5 +10,11 @@ } } } + }, + "issues": { + "removed_yaml": { + "description": "Configuring Season using YAML has been removed.\n\nYour existing YAML configuration is not used by Home Assistant.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Season YAML configuration has been removed" + } } } \ No newline at end of file From fc62ba58a63c07ec432130c5f5900fbf096ea9cb Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 26 Sep 2022 16:11:07 +0200 Subject: [PATCH 742/955] Start deprecation yaml uptime (#77782) --- homeassistant/components/uptime/sensor.py | 10 ++++++++++ homeassistant/components/uptime/strings.json | 6 ++++++ homeassistant/components/uptime/translations/en.json | 6 ++++++ 3 files changed, 22 insertions(+) diff --git a/homeassistant/components/uptime/sensor.py b/homeassistant/components/uptime/sensor.py index 3f7b7f5da25..ec65051867a 100644 --- a/homeassistant/components/uptime/sensor.py +++ b/homeassistant/components/uptime/sensor.py @@ -15,6 +15,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType import homeassistant.util.dt as dt_util @@ -38,6 +39,15 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the uptime sensor platform.""" + async_create_issue( + hass, + DOMAIN, + "removed_yaml", + breaks_in_ha_version="2022.12.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="removed_yaml", + ) hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, diff --git a/homeassistant/components/uptime/strings.json b/homeassistant/components/uptime/strings.json index 9ceb91de9ba..3d374015acb 100644 --- a/homeassistant/components/uptime/strings.json +++ b/homeassistant/components/uptime/strings.json @@ -9,5 +9,11 @@ "abort": { "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" } + }, + "issues": { + "removed_yaml": { + "title": "The Uptime YAML configuration has been removed", + "description": "Configuring Uptime using YAML has been removed.\n\nYour existing YAML configuration is not used by Home Assistant.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } } } diff --git a/homeassistant/components/uptime/translations/en.json b/homeassistant/components/uptime/translations/en.json index 5d38ae74e21..ec70b67c0e7 100644 --- a/homeassistant/components/uptime/translations/en.json +++ b/homeassistant/components/uptime/translations/en.json @@ -9,5 +9,11 @@ } } }, + "issues": { + "removed_yaml": { + "description": "Configuring Uptime using YAML has been removed.\n\nYour existing YAML configuration is not used by Home Assistant.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Uptime YAML configuration has been removed" + } + }, "title": "Uptime" } \ No newline at end of file From 2a94c42ceac13252be11474b3b4d7bec90e1ed2c Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 26 Sep 2022 16:17:39 +0200 Subject: [PATCH 743/955] Support VLC 4 pause (#77302) * Support VLC 4 pause * Clean vlc tests types --- homeassistant/components/vlc_telnet/media_player.py | 10 +++++++--- tests/components/vlc_telnet/test_config_flow.py | 4 +--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/vlc_telnet/media_player.py b/homeassistant/components/vlc_telnet/media_player.py index ef3406bda28..5d366b6a8aa 100644 --- a/homeassistant/components/vlc_telnet/media_player.py +++ b/homeassistant/components/vlc_telnet/media_player.py @@ -260,7 +260,12 @@ class VlcDevice(MediaPlayerEntity): @catch_vlc_errors async def async_media_play(self) -> None: """Send play command.""" - await self._vlc.play() + status = await self._vlc.status() + if status.state == "paused": + # If already paused, play by toggling pause. + await self._vlc.pause() + else: + await self._vlc.play() self._attr_state = MediaPlayerState.PLAYING @catch_vlc_errors @@ -268,8 +273,7 @@ class VlcDevice(MediaPlayerEntity): """Send pause command.""" status = await self._vlc.status() if status.state != "paused": - # Make sure we're not already paused since VLCTelnet.pause() toggles - # pause. + # Make sure we're not already paused as pausing again will unpause. await self._vlc.pause() self._attr_state = MediaPlayerState.PAUSED diff --git a/tests/components/vlc_telnet/test_config_flow.py b/tests/components/vlc_telnet/test_config_flow.py index 66ca5c2cb20..5e712c71b24 100644 --- a/tests/components/vlc_telnet/test_config_flow.py +++ b/tests/components/vlc_telnet/test_config_flow.py @@ -15,8 +15,6 @@ from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry -# mypy: allow-untyped-calls - @pytest.mark.parametrize( "input_data, entry_data", @@ -139,7 +137,7 @@ async def test_errors( async def test_reauth_flow(hass: HomeAssistant) -> None: """Test successful reauth flow.""" - entry_data = { + entry_data: dict[str, Any] = { "password": "old-password", "host": "1.1.1.1", "port": 8888, From de3be96bdb28790968b341eb28e1f2d06376181b Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Mon, 26 Sep 2022 17:10:16 +0200 Subject: [PATCH 744/955] Remove deprecated YAML import for here_travel_time (#77959) --- .../here_travel_time/config_flow.py | 117 +--------- .../components/here_travel_time/sensor.py | 102 +-------- .../here_travel_time/test_config_flow.py | 208 +----------------- .../here_travel_time/test_sensor.py | 25 --- 4 files changed, 12 insertions(+), 440 deletions(-) diff --git a/homeassistant/components/here_travel_time/config_flow.py b/homeassistant/components/here_travel_time/config_flow.py index d42e6d6bf3e..09faf95177d 100644 --- a/homeassistant/components/here_travel_time/config_flow.py +++ b/homeassistant/components/here_travel_time/config_flow.py @@ -10,7 +10,6 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import ( CONF_API_KEY, - CONF_ENTITY_NAMESPACE, CONF_LATITUDE, CONF_LONGITUDE, CONF_MODE, @@ -27,19 +26,22 @@ from homeassistant.helpers.selector import ( ) from .const import ( - CONF_ARRIVAL, CONF_ARRIVAL_TIME, - CONF_DEPARTURE, CONF_DEPARTURE_TIME, CONF_DESTINATION, + CONF_DESTINATION_ENTITY_ID, + CONF_DESTINATION_LATITUDE, + CONF_DESTINATION_LONGITUDE, CONF_ORIGIN, + CONF_ORIGIN_ENTITY_ID, + CONF_ORIGIN_LATITUDE, + CONF_ORIGIN_LONGITUDE, CONF_ROUTE_MODE, CONF_TRAFFIC_MODE, DEFAULT_NAME, DOMAIN, ROUTE_MODE_FASTEST, ROUTE_MODES, - TRAFFIC_MODE_DISABLED, TRAFFIC_MODE_ENABLED, TRAFFIC_MODES, TRAVEL_MODE_CAR, @@ -47,57 +49,10 @@ from .const import ( TRAVEL_MODES, UNITS, ) -from .sensor import ( - CONF_DESTINATION_ENTITY_ID, - CONF_DESTINATION_LATITUDE, - CONF_DESTINATION_LONGITUDE, - CONF_ORIGIN_ENTITY_ID, - CONF_ORIGIN_LATITUDE, - CONF_ORIGIN_LONGITUDE, -) _LOGGER = logging.getLogger(__name__) -def is_dupe_import( - entry: config_entries.ConfigEntry, - user_input: dict[str, Any], - options: dict[str, Any], -) -> bool: - """Return whether imported config already exists.""" - # Check the main data keys - if any( - user_input[key] != entry.data[key] - for key in (CONF_API_KEY, CONF_MODE, CONF_NAME) - ): - return False - - # Check origin/destination - for key in ( - CONF_DESTINATION_LATITUDE, - CONF_DESTINATION_LONGITUDE, - CONF_ORIGIN_LATITUDE, - CONF_ORIGIN_LONGITUDE, - CONF_DESTINATION_ENTITY_ID, - CONF_ORIGIN_ENTITY_ID, - ): - if user_input.get(key) != entry.data.get(key): - return False - - # We have to check for options that don't have defaults - for key in ( - CONF_TRAFFIC_MODE, - CONF_UNIT_SYSTEM, - CONF_ROUTE_MODE, - CONF_ARRIVAL_TIME, - CONF_DEPARTURE_TIME, - ): - if options.get(key) != entry.options.get(key): - return False - - return True - - def validate_api_key(api_key: str) -> None: """Validate the user input allows us to connect.""" known_working_origin = [38.9, -77.04833] @@ -275,66 +230,6 @@ class HERETravelTimeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) return self.async_show_form(step_id="destination_entity", data_schema=schema) - async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult: - """Import from configuration.yaml.""" - options: dict[str, Any] = {} - user_input, options = self._transform_import_input(user_input) - # We need to prevent duplicate imports - if any( - is_dupe_import(entry, user_input, options) - for entry in self.hass.config_entries.async_entries(DOMAIN) - if entry.source == config_entries.SOURCE_IMPORT - ): - return self.async_abort(reason="already_configured") - return self.async_create_entry( - title=user_input[CONF_NAME], data=user_input, options=options - ) - - def _transform_import_input( - self, import_input: dict[str, Any] - ) -> tuple[dict[str, Any], dict[str, Any]]: - """Transform platform schema input to new model.""" - options: dict[str, Any] = {} - user_input: dict[str, Any] = {} - - if import_input.get(CONF_ORIGIN_LATITUDE) is not None: - user_input[CONF_ORIGIN_LATITUDE] = import_input[CONF_ORIGIN_LATITUDE] - user_input[CONF_ORIGIN_LONGITUDE] = import_input[CONF_ORIGIN_LONGITUDE] - else: - user_input[CONF_ORIGIN_ENTITY_ID] = import_input[CONF_ORIGIN_ENTITY_ID] - - if import_input.get(CONF_DESTINATION_LATITUDE) is not None: - user_input[CONF_DESTINATION_LATITUDE] = import_input[ - CONF_DESTINATION_LATITUDE - ] - user_input[CONF_DESTINATION_LONGITUDE] = import_input[ - CONF_DESTINATION_LONGITUDE - ] - else: - user_input[CONF_DESTINATION_ENTITY_ID] = import_input[ - CONF_DESTINATION_ENTITY_ID - ] - - user_input[CONF_API_KEY] = import_input[CONF_API_KEY] - user_input[CONF_MODE] = import_input[CONF_MODE] - user_input[CONF_NAME] = import_input[CONF_NAME] - if (namespace := import_input.get(CONF_ENTITY_NAMESPACE)) is not None: - user_input[CONF_NAME] = f"{namespace} {user_input[CONF_NAME]}" - - options[CONF_TRAFFIC_MODE] = ( - TRAFFIC_MODE_ENABLED - if import_input.get(CONF_TRAFFIC_MODE, False) - else TRAFFIC_MODE_DISABLED - ) - options[CONF_ROUTE_MODE] = import_input.get(CONF_ROUTE_MODE) - options[CONF_UNIT_SYSTEM] = import_input.get( - CONF_UNIT_SYSTEM, self.hass.config.units.name - ) - options[CONF_ARRIVAL_TIME] = import_input.get(CONF_ARRIVAL, None) - options[CONF_DEPARTURE_TIME] = import_input.get(CONF_DEPARTURE, None) - - return user_input, options - class HERETravelTimeOptionsFlow(config_entries.OptionsFlow): """Handle HERE Travel Time options.""" diff --git a/homeassistant/components/here_travel_time/sensor.py b/homeassistant/components/here_travel_time/sensor.py index 6bb4052dd9f..74a9ae357e1 100644 --- a/homeassistant/components/here_travel_time/sensor.py +++ b/homeassistant/components/here_travel_time/sensor.py @@ -3,38 +3,30 @@ from __future__ import annotations from collections.abc import Mapping from datetime import timedelta -import logging from typing import Any -import voluptuous as vol - from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, SensorEntity, SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_LATITUDE, ATTR_LONGITUDE, - CONF_API_KEY, CONF_MODE, CONF_NAME, - CONF_UNIT_SYSTEM, CONF_UNIT_SYSTEM_IMPERIAL, LENGTH_KILOMETERS, LENGTH_MILES, TIME_MINUTES, ) from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.start import async_at_start -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import HereTravelTimeDataUpdateCoordinator @@ -47,83 +39,13 @@ from .const import ( ATTR_ORIGIN, ATTR_ORIGIN_NAME, ATTR_ROUTE, - CONF_ARRIVAL, - CONF_DEPARTURE, - CONF_DESTINATION_ENTITY_ID, - CONF_DESTINATION_LATITUDE, - CONF_DESTINATION_LONGITUDE, - CONF_ORIGIN_ENTITY_ID, - CONF_ORIGIN_LATITUDE, - CONF_ORIGIN_LONGITUDE, - CONF_ROUTE_MODE, - CONF_TRAFFIC_MODE, - DEFAULT_NAME, DOMAIN, ICON_CAR, ICONS, - ROUTE_MODE_FASTEST, - ROUTE_MODES, - TRAVEL_MODE_BICYCLE, - TRAVEL_MODE_CAR, - TRAVEL_MODE_PEDESTRIAN, - TRAVEL_MODE_PUBLIC, - TRAVEL_MODE_PUBLIC_TIME_TABLE, - TRAVEL_MODE_TRUCK, - TRAVEL_MODES, - UNITS, ) -_LOGGER = logging.getLogger(__name__) - - SCAN_INTERVAL = timedelta(minutes=5) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_API_KEY): cv.string, - vol.Inclusive( - CONF_DESTINATION_LATITUDE, "destination_coordinates" - ): cv.latitude, - vol.Inclusive( - CONF_DESTINATION_LONGITUDE, "destination_coordinates" - ): cv.longitude, - vol.Exclusive(CONF_DESTINATION_LATITUDE, "destination"): cv.latitude, - vol.Exclusive(CONF_DESTINATION_ENTITY_ID, "destination"): cv.entity_id, - vol.Inclusive(CONF_ORIGIN_LATITUDE, "origin_coordinates"): cv.latitude, - vol.Inclusive(CONF_ORIGIN_LONGITUDE, "origin_coordinates"): cv.longitude, - vol.Exclusive(CONF_ORIGIN_LATITUDE, "origin"): cv.latitude, - vol.Exclusive(CONF_ORIGIN_ENTITY_ID, "origin"): cv.entity_id, - vol.Optional(CONF_DEPARTURE): cv.time, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_MODE, default=TRAVEL_MODE_CAR): vol.In(TRAVEL_MODES), - vol.Optional(CONF_ROUTE_MODE, default=ROUTE_MODE_FASTEST): vol.In(ROUTE_MODES), - vol.Optional(CONF_TRAFFIC_MODE, default=False): cv.boolean, - vol.Optional(CONF_UNIT_SYSTEM): vol.In(UNITS), - } -) - -PLATFORM_SCHEMA = vol.All( - cv.has_at_least_one_key(CONF_DESTINATION_LATITUDE, CONF_DESTINATION_ENTITY_ID), - cv.has_at_least_one_key(CONF_ORIGIN_LATITUDE, CONF_ORIGIN_ENTITY_ID), - cv.key_value_schemas( - CONF_MODE, - { - None: PLATFORM_SCHEMA, - TRAVEL_MODE_BICYCLE: PLATFORM_SCHEMA, - TRAVEL_MODE_CAR: PLATFORM_SCHEMA, - TRAVEL_MODE_PEDESTRIAN: PLATFORM_SCHEMA, - TRAVEL_MODE_PUBLIC: PLATFORM_SCHEMA, - TRAVEL_MODE_TRUCK: PLATFORM_SCHEMA, - TRAVEL_MODE_PUBLIC_TIME_TABLE: PLATFORM_SCHEMA.extend( - { - vol.Exclusive(CONF_ARRIVAL, "arrival_departure"): cv.time, - vol.Exclusive(CONF_DEPARTURE, "arrival_departure"): cv.time, - } - ), - }, - ), -) - def sensor_descriptions(travel_mode: str) -> tuple[SensorEntityDescription, ...]: """Construct SensorEntityDescriptions.""" @@ -150,28 +72,6 @@ def sensor_descriptions(travel_mode: str) -> tuple[SensorEntityDescription, ...] ) -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the HERE travel time platform.""" - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=config, - ) - ) - - _LOGGER.warning( - "Your HERE travel time configuration has been imported into the UI; " - "please remove it from configuration.yaml as support for it will be " - "removed in a future release" - ) - - async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, diff --git a/tests/components/here_travel_time/test_config_flow.py b/tests/components/here_travel_time/test_config_flow.py index 23e3c1c81c7..b56f97a8053 100644 --- a/tests/components/here_travel_time/test_config_flow.py +++ b/tests/components/here_travel_time/test_config_flow.py @@ -7,10 +7,13 @@ import pytest from homeassistant import config_entries, data_entry_flow from homeassistant.components.here_travel_time.const import ( - CONF_ARRIVAL, CONF_ARRIVAL_TIME, - CONF_DEPARTURE, CONF_DEPARTURE_TIME, + CONF_DESTINATION_ENTITY_ID, + CONF_DESTINATION_LATITUDE, + CONF_DESTINATION_LONGITUDE, + CONF_ORIGIN_LATITUDE, + CONF_ORIGIN_LONGITUDE, CONF_ROUTE_MODE, CONF_TRAFFIC_MODE, DOMAIN, @@ -19,20 +22,10 @@ from homeassistant.components.here_travel_time.const import ( TRAVEL_MODE_CAR, TRAVEL_MODE_PUBLIC_TIME_TABLE, ) -from homeassistant.components.here_travel_time.sensor import ( - CONF_DESTINATION_ENTITY_ID, - CONF_DESTINATION_LATITUDE, - CONF_DESTINATION_LONGITUDE, - CONF_ORIGIN_ENTITY_ID, - CONF_ORIGIN_LATITUDE, - CONF_ORIGIN_LONGITUDE, -) from homeassistant.const import ( CONF_API_KEY, - CONF_ENTITY_NAMESPACE, CONF_MODE, CONF_NAME, - CONF_SCAN_INTERVAL, CONF_UNIT_SYSTEM, CONF_UNIT_SYSTEM_IMPERIAL, CONF_UNIT_SYSTEM_METRIC, @@ -412,194 +405,3 @@ async def test_options_flow_no_time_step( CONF_ROUTE_MODE: ROUTE_MODE_FASTEST, CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED, } - - -@pytest.mark.usefixtures("valid_response") -async def test_import_flow_entity_id(hass: HomeAssistant) -> None: - """Test import_flow with entity ids.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_API_KEY: CONF_API_KEY, - CONF_ORIGIN_ENTITY_ID: "sensor.origin", - CONF_DESTINATION_ENTITY_ID: "sensor.destination", - CONF_NAME: "test_name", - CONF_MODE: TRAVEL_MODE_CAR, - CONF_DEPARTURE: "08:00:00", - CONF_ROUTE_MODE: ROUTE_MODE_FASTEST, - CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL, - CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED, - CONF_ENTITY_NAMESPACE: "namespace", - CONF_SCAN_INTERVAL: 2678400, - }, - ) - await hass.async_block_till_done() - - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert result["title"] == "namespace test_name" - - entry = hass.config_entries.async_entries(DOMAIN)[0] - assert entry.data == { - CONF_NAME: "namespace test_name", - CONF_API_KEY: CONF_API_KEY, - CONF_ORIGIN_ENTITY_ID: "sensor.origin", - CONF_DESTINATION_ENTITY_ID: "sensor.destination", - CONF_MODE: TRAVEL_MODE_CAR, - } - assert entry.options == { - CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL, - CONF_ROUTE_MODE: ROUTE_MODE_FASTEST, - CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED, - CONF_DEPARTURE_TIME: "08:00:00", - CONF_ARRIVAL_TIME: None, - } - - -@pytest.mark.usefixtures("valid_response") -async def test_import_flow_coordinates(hass: HomeAssistant) -> None: - """Test import_flow with coordinates.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_API_KEY: CONF_API_KEY, - CONF_ORIGIN_LATITUDE: CAR_ORIGIN_LATITUDE, - CONF_ORIGIN_LONGITUDE: CAR_ORIGIN_LONGITUDE, - CONF_DESTINATION_LATITUDE: CAR_DESTINATION_LATITUDE, - CONF_DESTINATION_LONGITUDE: CAR_DESTINATION_LONGITUDE, - CONF_NAME: "test_name", - CONF_MODE: TRAVEL_MODE_CAR, - CONF_ARRIVAL: "08:00:00", - CONF_ROUTE_MODE: ROUTE_MODE_FASTEST, - CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, - CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED, - }, - ) - await hass.async_block_till_done() - - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert result["title"] == "test_name" - - entry = hass.config_entries.async_entries(DOMAIN)[0] - assert entry.data == { - CONF_NAME: "test_name", - CONF_API_KEY: CONF_API_KEY, - CONF_ORIGIN_LATITUDE: CAR_ORIGIN_LATITUDE, - CONF_ORIGIN_LONGITUDE: CAR_ORIGIN_LONGITUDE, - CONF_DESTINATION_LATITUDE: CAR_DESTINATION_LATITUDE, - CONF_DESTINATION_LONGITUDE: CAR_DESTINATION_LONGITUDE, - CONF_MODE: TRAVEL_MODE_CAR, - } - assert entry.options == { - CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, - CONF_ROUTE_MODE: ROUTE_MODE_FASTEST, - CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED, - CONF_DEPARTURE_TIME: None, - CONF_ARRIVAL_TIME: "08:00:00", - CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, - } - - -@pytest.mark.usefixtures("valid_response") -async def test_dupe_import(hass: HomeAssistant) -> None: - """Test duplicate import.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_API_KEY: CONF_API_KEY, - CONF_ORIGIN_LATITUDE: CAR_ORIGIN_LATITUDE, - CONF_ORIGIN_LONGITUDE: CAR_ORIGIN_LONGITUDE, - CONF_DESTINATION_LATITUDE: CAR_DESTINATION_LATITUDE, - CONF_DESTINATION_LONGITUDE: CAR_DESTINATION_LONGITUDE, - CONF_NAME: "test_name", - CONF_MODE: TRAVEL_MODE_CAR, - CONF_ARRIVAL: "08:00:00", - CONF_ROUTE_MODE: ROUTE_MODE_FASTEST, - CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, - CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED, - }, - ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - await hass.async_block_till_done() - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_API_KEY: CONF_API_KEY, - CONF_ORIGIN_LATITUDE: CAR_ORIGIN_LATITUDE, - CONF_ORIGIN_LONGITUDE: CAR_ORIGIN_LONGITUDE, - CONF_DESTINATION_LATITUDE: CAR_DESTINATION_LATITUDE, - CONF_DESTINATION_LONGITUDE: CAR_DESTINATION_LONGITUDE, - CONF_NAME: "test_name2", - CONF_MODE: TRAVEL_MODE_CAR, - CONF_ARRIVAL: "08:00:00", - CONF_ROUTE_MODE: ROUTE_MODE_FASTEST, - CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, - CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED, - }, - ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - await hass.async_block_till_done() - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_API_KEY: CONF_API_KEY, - CONF_ORIGIN_LATITUDE: CAR_ORIGIN_LATITUDE, - CONF_ORIGIN_LONGITUDE: CAR_ORIGIN_LONGITUDE, - CONF_DESTINATION_LATITUDE: CAR_DESTINATION_LATITUDE, - CONF_DESTINATION_LONGITUDE: CAR_DESTINATION_LONGITUDE, - CONF_NAME: "test_name", - CONF_MODE: TRAVEL_MODE_CAR, - CONF_ARRIVAL: "08:00:01", - CONF_ROUTE_MODE: ROUTE_MODE_FASTEST, - CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, - CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED, - }, - ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - await hass.async_block_till_done() - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_API_KEY: CONF_API_KEY, - CONF_ORIGIN_LATITUDE: CAR_ORIGIN_LATITUDE, - CONF_ORIGIN_LONGITUDE: CAR_ORIGIN_LONGITUDE, - CONF_DESTINATION_LATITUDE: "40.0", - CONF_DESTINATION_LONGITUDE: CAR_DESTINATION_LONGITUDE, - CONF_NAME: "test_name", - CONF_MODE: TRAVEL_MODE_CAR, - CONF_ARRIVAL: "08:00:01", - CONF_ROUTE_MODE: ROUTE_MODE_FASTEST, - CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, - CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED, - }, - ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - await hass.async_block_till_done() - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_API_KEY: CONF_API_KEY, - CONF_ORIGIN_LATITUDE: CAR_ORIGIN_LATITUDE, - CONF_ORIGIN_LONGITUDE: CAR_ORIGIN_LONGITUDE, - CONF_DESTINATION_LATITUDE: CAR_DESTINATION_LATITUDE, - CONF_DESTINATION_LONGITUDE: CAR_DESTINATION_LONGITUDE, - CONF_NAME: "test_name", - CONF_MODE: TRAVEL_MODE_CAR, - CONF_ARRIVAL: "08:00:00", - CONF_ROUTE_MODE: ROUTE_MODE_FASTEST, - CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, - CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED, - }, - ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT - assert result["reason"] == "already_configured" diff --git a/tests/components/here_travel_time/test_sensor.py b/tests/components/here_travel_time/test_sensor.py index 849c123e72e..60b1f5fcced 100644 --- a/tests/components/here_travel_time/test_sensor.py +++ b/tests/components/here_travel_time/test_sensor.py @@ -474,28 +474,3 @@ async def test_route_not_found(hass: HomeAssistant, caplog): await hass.async_block_till_done() assert NO_ROUTE_ERROR_MESSAGE in caplog.text - - -@pytest.mark.usefixtures("valid_response") -async def test_setup_platform(hass: HomeAssistant, caplog): - """Test that setup platform migration works.""" - config = { - "sensor": { - "platform": DOMAIN, - "name": "test", - "origin_latitude": CAR_ORIGIN_LATITUDE, - "origin_longitude": CAR_ORIGIN_LONGITUDE, - "destination_latitude": CAR_DESTINATION_LATITUDE, - "destination_longitude": CAR_DESTINATION_LONGITUDE, - "api_key": API_KEY, - } - } - with patch( - "homeassistant.components.here_travel_time.async_setup_entry", return_value=True - ): - await async_setup_component(hass, "sensor", config) - await hass.async_block_till_done() - assert ( - "Your HERE travel time configuration has been imported into the UI" - in caplog.text - ) From ed812b5ee43671af47787e1ecf906e2e516c0000 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 26 Sep 2022 19:22:07 +0200 Subject: [PATCH 745/955] Remove unused alexa code (#79100) --- homeassistant/components/alexa/entities.py | 6 ------ homeassistant/components/alexa/errors.py | 4 ---- 2 files changed, 10 deletions(-) diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index 8319e146d9f..e002969952a 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -300,12 +300,6 @@ class AlexaEntity: """ raise NotImplementedError - def get_interface(self, capability) -> AlexaCapability: - """Return the given AlexaInterface. - - Raises _UnsupportedInterface. - """ - def interfaces(self) -> list[AlexaCapability]: """Return a list of supported interfaces. diff --git a/homeassistant/components/alexa/errors.py b/homeassistant/components/alexa/errors.py index 0ce00f1fe48..5f0de6f7467 100644 --- a/homeassistant/components/alexa/errors.py +++ b/homeassistant/components/alexa/errors.py @@ -8,10 +8,6 @@ from homeassistant.exceptions import HomeAssistantError from .const import API_TEMP_UNITS -class UnsupportedInterface(HomeAssistantError): - """This entity does not support the requested Smart Home API interface.""" - - class UnsupportedProperty(HomeAssistantError): """This entity does not support the requested Smart Home API property.""" From c89d776d37dee4bb6b213fa0fd3b7c10878d5e40 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 26 Sep 2022 08:42:56 -1000 Subject: [PATCH 746/955] Bump bleak-retry-connector to 2.1.3 (#79105) --- 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 91b15583086..e90b73df0da 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -7,7 +7,7 @@ "quality_scale": "internal", "requirements": [ "bleak==0.18.1", - "bleak-retry-connector==2.1.0", + "bleak-retry-connector==2.1.3", "bluetooth-adapters==0.5.2", "bluetooth-auto-recovery==0.3.3", "dbus-fast==1.14.0" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d0dc41935db..15fe4b709bd 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.1.0 +bleak-retry-connector==2.1.3 bleak==0.18.1 bluetooth-adapters==0.5.2 bluetooth-auto-recovery==0.3.3 diff --git a/requirements_all.txt b/requirements_all.txt index 370b136b4c0..83636058e8d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -410,7 +410,7 @@ bimmer_connected==0.10.4 bizkaibus==0.1.1 # homeassistant.components.bluetooth -bleak-retry-connector==2.1.0 +bleak-retry-connector==2.1.3 # homeassistant.components.bluetooth bleak==0.18.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index aa579fb4d55..a3477ade8b0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -331,7 +331,7 @@ bellows==0.33.1 bimmer_connected==0.10.4 # homeassistant.components.bluetooth -bleak-retry-connector==2.1.0 +bleak-retry-connector==2.1.3 # homeassistant.components.bluetooth bleak==0.18.1 From 1f6d19bb99ac2040ab9b0ec333b466a92426a2c2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 26 Sep 2022 09:50:08 -1000 Subject: [PATCH 747/955] Bump dbus-fast to 0.15.1 (#79111) changelog: https://github.com/Bluetooth-Devices/dbus-fast/compare/v1.14.0...v1.15.1 --- 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 e90b73df0da..c7e71fcfd08 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -10,7 +10,7 @@ "bleak-retry-connector==2.1.3", "bluetooth-adapters==0.5.2", "bluetooth-auto-recovery==0.3.3", - "dbus-fast==1.14.0" + "dbus-fast==1.15.1" ], "codeowners": ["@bdraco"], "config_flow": true, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 15fe4b709bd..e4c2de90e75 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ bluetooth-auto-recovery==0.3.3 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==38.0.1 -dbus-fast==1.14.0 +dbus-fast==1.15.1 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.3.0 diff --git a/requirements_all.txt b/requirements_all.txt index 83636058e8d..3a4f2f451ed 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.14.0 +dbus-fast==1.15.1 # homeassistant.components.debugpy debugpy==1.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a3477ade8b0..5ce8b6914fe 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -417,7 +417,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.14.0 +dbus-fast==1.15.1 # homeassistant.components.debugpy debugpy==1.6.3 From e8156adb13611147f55ca3f0e6c0a7e78e97e590 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 26 Sep 2022 22:10:06 +0200 Subject: [PATCH 748/955] Update mypy to 0.981 (#79115) --- homeassistant/components/esphome/climate.py | 18 ++++----- homeassistant/components/esphome/cover.py | 10 ++--- homeassistant/components/esphome/fan.py | 8 ++-- homeassistant/components/esphome/light.py | 16 ++++---- homeassistant/components/esphome/lock.py | 8 ++-- .../components/esphome/media_player.py | 6 +-- homeassistant/components/esphome/number.py | 2 +- homeassistant/components/esphome/select.py | 2 +- homeassistant/components/esphome/sensor.py | 4 +- homeassistant/components/esphome/switch.py | 2 +- homeassistant/components/izone/climate.py | 16 ++++---- .../components/norway_air/air_quality.py | 10 ++--- homeassistant/components/plex/media_player.py | 38 +++++++++---------- homeassistant/components/recorder/models.py | 8 ++-- requirements_test.txt | 2 +- 15 files changed, 75 insertions(+), 75 deletions(-) diff --git a/homeassistant/components/esphome/climate.py b/homeassistant/components/esphome/climate.py index 6dbc9a58ea5..4f38d1caa24 100644 --- a/homeassistant/components/esphome/climate.py +++ b/homeassistant/components/esphome/climate.py @@ -212,13 +212,13 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti features |= ClimateEntityFeature.SWING_MODE return features - @property # type: ignore[misc] + @property @esphome_state_property def hvac_mode(self) -> str | None: """Return current operation ie. heat, cool, idle.""" return _CLIMATE_MODES.from_esphome(self._state.mode) - @property # type: ignore[misc] + @property @esphome_state_property def hvac_action(self) -> str | None: """Return current action.""" @@ -227,7 +227,7 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti return None return _CLIMATE_ACTIONS.from_esphome(self._state.action) - @property # type: ignore[misc] + @property @esphome_state_property def fan_mode(self) -> str | None: """Return current fan setting.""" @@ -235,7 +235,7 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti self._state.fan_mode ) - @property # type: ignore[misc] + @property @esphome_state_property def preset_mode(self) -> str | None: """Return current preset mode.""" @@ -243,31 +243,31 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti self._state.preset_compat(self._api_version) ) - @property # type: ignore[misc] + @property @esphome_state_property def swing_mode(self) -> str | None: """Return current swing mode.""" return _SWING_MODES.from_esphome(self._state.swing_mode) - @property # type: ignore[misc] + @property @esphome_state_property def current_temperature(self) -> float | None: """Return the current temperature.""" return self._state.current_temperature - @property # type: ignore[misc] + @property @esphome_state_property def target_temperature(self) -> float | None: """Return the temperature we try to reach.""" return self._state.target_temperature - @property # type: ignore[misc] + @property @esphome_state_property def target_temperature_low(self) -> float | None: """Return the lowbound target temperature we try to reach.""" return self._state.target_temperature_low - @property # type: ignore[misc] + @property @esphome_state_property def target_temperature_high(self) -> float | None: """Return the highbound target temperature we try to reach.""" diff --git a/homeassistant/components/esphome/cover.py b/homeassistant/components/esphome/cover.py index bf179ff25a9..10662977307 100644 --- a/homeassistant/components/esphome/cover.py +++ b/homeassistant/components/esphome/cover.py @@ -65,26 +65,26 @@ class EsphomeCover(EsphomeEntity[CoverInfo, CoverState], CoverEntity): """Return true if we do optimistic updates.""" return self._static_info.assumed_state - @property # type: ignore[misc] + @property @esphome_state_property def is_closed(self) -> bool | None: """Return if the cover is closed or not.""" # Check closed state with api version due to a protocol change return self._state.is_closed(self._api_version) - @property # type: ignore[misc] + @property @esphome_state_property def is_opening(self) -> bool: """Return if the cover is opening or not.""" return self._state.current_operation == CoverOperation.IS_OPENING - @property # type: ignore[misc] + @property @esphome_state_property def is_closing(self) -> bool: """Return if the cover is closing or not.""" return self._state.current_operation == CoverOperation.IS_CLOSING - @property # type: ignore[misc] + @property @esphome_state_property def current_cover_position(self) -> int | None: """Return current position of cover. 0 is closed, 100 is open.""" @@ -92,7 +92,7 @@ class EsphomeCover(EsphomeEntity[CoverInfo, CoverState], CoverEntity): return None return round(self._state.position * 100.0) - @property # type: ignore[misc] + @property @esphome_state_property def current_cover_tilt_position(self) -> int | None: """Return current position of cover tilt. 0 is closed, 100 is open.""" diff --git a/homeassistant/components/esphome/fan.py b/homeassistant/components/esphome/fan.py index 207b9d3f9f8..772a1b8befa 100644 --- a/homeassistant/components/esphome/fan.py +++ b/homeassistant/components/esphome/fan.py @@ -112,13 +112,13 @@ class EsphomeFan(EsphomeEntity[FanInfo, FanState], FanEntity): key=self._static_info.key, direction=_FAN_DIRECTIONS.from_hass(direction) ) - @property # type: ignore[misc] + @property @esphome_state_property def is_on(self) -> bool | None: """Return true if the entity is on.""" return self._state.state - @property # type: ignore[misc] + @property @esphome_state_property def percentage(self) -> int | None: """Return the current speed percentage.""" @@ -141,7 +141,7 @@ class EsphomeFan(EsphomeEntity[FanInfo, FanState], FanEntity): return len(ORDERED_NAMED_FAN_SPEEDS) return self._static_info.supported_speed_levels - @property # type: ignore[misc] + @property @esphome_state_property def oscillating(self) -> bool | None: """Return the oscillation state.""" @@ -149,7 +149,7 @@ class EsphomeFan(EsphomeEntity[FanInfo, FanState], FanEntity): return None return self._state.oscillating - @property # type: ignore[misc] + @property @esphome_state_property def current_direction(self) -> str | None: """Return the current fan direction.""" diff --git a/homeassistant/components/esphome/light.py b/homeassistant/components/esphome/light.py index 2f536e82b47..624dfc8950f 100644 --- a/homeassistant/components/esphome/light.py +++ b/homeassistant/components/esphome/light.py @@ -130,7 +130,7 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity): """Return whether the client supports the new color mode system natively.""" return self._api_version >= APIVersion(1, 6) - @property # type: ignore[misc] + @property @esphome_state_property def is_on(self) -> bool | None: """Return true if the light is on.""" @@ -260,13 +260,13 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity): data["transition_length"] = kwargs[ATTR_TRANSITION] await self._client.light_command(**data) - @property # type: ignore[misc] + @property @esphome_state_property def brightness(self) -> int | None: """Return the brightness of this light between 0..255.""" return round(self._state.brightness * 255) - @property # type: ignore[misc] + @property @esphome_state_property def color_mode(self) -> str | None: """Return the color mode of the light.""" @@ -277,7 +277,7 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity): return _color_mode_to_ha(self._state.color_mode) - @property # type: ignore[misc] + @property @esphome_state_property def rgb_color(self) -> tuple[int, int, int] | None: """Return the rgb color value [int, int, int].""" @@ -294,7 +294,7 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity): round(self._state.blue * self._state.color_brightness * 255), ) - @property # type: ignore[misc] + @property @esphome_state_property def rgbw_color(self) -> tuple[int, int, int, int] | None: """Return the rgbw color value [int, int, int, int].""" @@ -302,7 +302,7 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity): rgb = cast("tuple[int, int, int]", self.rgb_color) return (*rgb, white) - @property # type: ignore[misc] + @property @esphome_state_property def rgbww_color(self) -> tuple[int, int, int, int, int] | None: """Return the rgbww color value [int, int, int, int, int].""" @@ -330,13 +330,13 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity): round(self._state.warm_white * 255), ) - @property # type: ignore[misc] + @property @esphome_state_property def color_temp(self) -> float | None: # type: ignore[override] """Return the CT color value in mireds.""" return self._state.color_temperature - @property # type: ignore[misc] + @property @esphome_state_property def effect(self) -> str | None: """Return the current effect.""" diff --git a/homeassistant/components/esphome/lock.py b/homeassistant/components/esphome/lock.py index 62c7c6de0dd..bcfa0131518 100644 --- a/homeassistant/components/esphome/lock.py +++ b/homeassistant/components/esphome/lock.py @@ -49,25 +49,25 @@ class EsphomeLock(EsphomeEntity[LockInfo, LockEntityState], LockEntity): return self._static_info.code_format return None - @property # type: ignore[misc] + @property @esphome_state_property def is_locked(self) -> bool | None: """Return true if the lock is locked.""" return self._state.state == LockState.LOCKED - @property # type: ignore[misc] + @property @esphome_state_property def is_locking(self) -> bool | None: """Return true if the lock is locking.""" return self._state.state == LockState.LOCKING - @property # type: ignore[misc] + @property @esphome_state_property def is_unlocking(self) -> bool | None: """Return true if the lock is unlocking.""" return self._state.state == LockState.UNLOCKING - @property # type: ignore[misc] + @property @esphome_state_property def is_jammed(self) -> bool | None: """Return true if the lock is jammed (incomplete locking).""" diff --git a/homeassistant/components/esphome/media_player.py b/homeassistant/components/esphome/media_player.py index 6b72d366754..d7a70737690 100644 --- a/homeassistant/components/esphome/media_player.py +++ b/homeassistant/components/esphome/media_player.py @@ -64,19 +64,19 @@ class EsphomeMediaPlayer( _attr_device_class = MediaPlayerDeviceClass.SPEAKER - @property # type: ignore[misc] + @property @esphome_state_property def state(self) -> MediaPlayerState | None: """Return current state.""" return _STATES.from_esphome(self._state.state) - @property # type: ignore[misc] + @property @esphome_state_property def is_volume_muted(self) -> bool: """Return true if volume is muted.""" return self._state.muted - @property # type: ignore[misc] + @property @esphome_state_property def volume_level(self) -> float | None: """Volume level of the media player (0..1).""" diff --git a/homeassistant/components/esphome/number.py b/homeassistant/components/esphome/number.py index ed721f2db5e..a00d4456227 100644 --- a/homeassistant/components/esphome/number.py +++ b/homeassistant/components/esphome/number.py @@ -74,7 +74,7 @@ class EsphomeNumber(EsphomeEntity[NumberInfo, NumberState], NumberEntity): return NUMBER_MODES.from_esphome(self._static_info.mode) return NumberMode.AUTO - @property # type: ignore[misc] + @property @esphome_state_property def native_value(self) -> float | None: """Return the state of the entity.""" diff --git a/homeassistant/components/esphome/select.py b/homeassistant/components/esphome/select.py index 190fca52889..79af0455346 100644 --- a/homeassistant/components/esphome/select.py +++ b/homeassistant/components/esphome/select.py @@ -36,7 +36,7 @@ class EsphomeSelect(EsphomeEntity[SelectInfo, SelectState], SelectEntity): """Return a set of selectable options.""" return self._static_info.options - @property # type: ignore[misc] + @property @esphome_state_property def current_option(self) -> str | None: """Return the state of the entity.""" diff --git a/homeassistant/components/esphome/sensor.py b/homeassistant/components/esphome/sensor.py index f7f137f4592..4b316f6a640 100644 --- a/homeassistant/components/esphome/sensor.py +++ b/homeassistant/components/esphome/sensor.py @@ -76,7 +76,7 @@ class EsphomeSensor(EsphomeEntity[SensorInfo, SensorState], SensorEntity): """Return if this sensor should force a state update.""" return self._static_info.force_update - @property # type: ignore[misc] + @property @esphome_state_property def native_value(self) -> datetime | str | None: """Return the state of the entity.""" @@ -121,7 +121,7 @@ class EsphomeSensor(EsphomeEntity[SensorInfo, SensorState], SensorEntity): class EsphomeTextSensor(EsphomeEntity[TextSensorInfo, TextSensorState], SensorEntity): """A text sensor implementation for ESPHome.""" - @property # type: ignore[misc] + @property @esphome_state_property def native_value(self) -> str | None: """Return the state of the entity.""" diff --git a/homeassistant/components/esphome/switch.py b/homeassistant/components/esphome/switch.py index 2970edf7af0..db5084df378 100644 --- a/homeassistant/components/esphome/switch.py +++ b/homeassistant/components/esphome/switch.py @@ -36,7 +36,7 @@ class EsphomeSwitch(EsphomeEntity[SwitchInfo, SwitchState], SwitchEntity): """Return true if we do optimistic updates.""" return self._static_info.assumed_state - @property # type: ignore[misc] + @property @esphome_state_property def is_on(self) -> bool | None: """Return true if the switch is on.""" diff --git a/homeassistant/components/izone/climate.py b/homeassistant/components/izone/climate.py index 155ec7ba210..31ad5f7860d 100644 --- a/homeassistant/components/izone/climate.py +++ b/homeassistant/components/izone/climate.py @@ -295,7 +295,7 @@ class ControllerDevice(ClimateEntity): return key assert False, "Should be unreachable" - @property # type: ignore[misc] + @property @_return_on_connection_error([]) def hvac_modes(self) -> list[HVACMode]: """Return the list of available operation modes.""" @@ -303,13 +303,13 @@ class ControllerDevice(ClimateEntity): return [HVACMode.OFF, HVACMode.FAN_ONLY] return [HVACMode.OFF, *self._state_to_pizone] - @property # type: ignore[misc] + @property @_return_on_connection_error(PRESET_NONE) def preset_mode(self): """Eco mode is external air.""" return PRESET_ECO if self._controller.free_air else PRESET_NONE - @property # type: ignore[misc] + @property @_return_on_connection_error([PRESET_NONE]) def preset_modes(self): """Available preset modes, normal or eco.""" @@ -317,7 +317,7 @@ class ControllerDevice(ClimateEntity): return [PRESET_NONE, PRESET_ECO] return [PRESET_NONE] - @property # type: ignore[misc] + @property @_return_on_connection_error() def current_temperature(self) -> float | None: """Return the current temperature.""" @@ -347,7 +347,7 @@ class ControllerDevice(ClimateEntity): return None return zone.target_temperature - @property # type: ignore[misc] + @property @_return_on_connection_error() def target_temperature(self) -> float | None: """Return the temperature we try to reach (either from control zone or master unit).""" @@ -375,13 +375,13 @@ class ControllerDevice(ClimateEntity): """Return the list of available fan modes.""" return list(self._fan_to_pizone) - @property # type: ignore[misc] + @property @_return_on_connection_error(0.0) def min_temp(self) -> float: """Return the minimum temperature.""" return self._controller.temp_min - @property # type: ignore[misc] + @property @_return_on_connection_error(50.0) def max_temp(self) -> float: """Return the maximum temperature.""" @@ -516,7 +516,7 @@ class ZoneDevice(ClimateEntity): """Return the name of the entity.""" return self._name - @property # type: ignore[misc] + @property @_return_on_connection_error(0) def supported_features(self) -> int: """Return the list of supported features.""" diff --git a/homeassistant/components/norway_air/air_quality.py b/homeassistant/components/norway_air/air_quality.py index b6182d7ed84..b4acdc3bdc9 100644 --- a/homeassistant/components/norway_air/air_quality.py +++ b/homeassistant/components/norway_air/air_quality.py @@ -106,31 +106,31 @@ class AirSensor(AirQualityEntity): """Return the name of the sensor.""" return self._name - @property # type: ignore[misc] + @property @round_state def air_quality_index(self): """Return the Air Quality Index (AQI).""" return self._api.data.get("aqi") - @property # type: ignore[misc] + @property @round_state def nitrogen_dioxide(self): """Return the NO2 (nitrogen dioxide) level.""" return self._api.data.get("no2_concentration") - @property # type: ignore[misc] + @property @round_state def ozone(self): """Return the O3 (ozone) level.""" return self._api.data.get("o3_concentration") - @property # type: ignore[misc] + @property @round_state def particulate_matter_2_5(self): """Return the particulate matter 2.5 level.""" return self._api.data.get("pm25_concentration") - @property # type: ignore[misc] + @property @round_state def particulate_matter_10(self): """Return the particulate matter 10 level.""" diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index b92e513fafb..84e0f084210 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -249,7 +249,7 @@ class PlexMediaPlayer(MediaPlayerEntity): else: self._attr_state = MediaPlayerState.IDLE - @property # type: ignore[misc] + @property @needs_session def username(self): """Return the username of the client owner.""" @@ -280,109 +280,109 @@ class PlexMediaPlayer(MediaPlayerEntity): return "video" - @property # type: ignore[misc] + @property @needs_session def session_key(self): """Return current session key.""" return self.session.sessionKey - @property # type: ignore[misc] + @property @needs_session def media_library_title(self): """Return the library name of playing media.""" return self.session.media_library_title - @property # type: ignore[misc] + @property @needs_session def media_content_id(self): """Return the content ID of current playing media.""" return self.session.media_content_id - @property # type: ignore[misc] + @property @needs_session def media_content_type(self): """Return the content type of current playing media.""" return self.session.media_content_type - @property # type: ignore[misc] + @property @needs_session def media_content_rating(self): """Return the content rating of current playing media.""" return self.session.media_content_rating - @property # type: ignore[misc] + @property @needs_session def media_artist(self): """Return the artist of current playing media, music track only.""" return self.session.media_artist - @property # type: ignore[misc] + @property @needs_session def media_album_name(self): """Return the album name of current playing media, music track only.""" return self.session.media_album_name - @property # type: ignore[misc] + @property @needs_session def media_album_artist(self): """Return the album artist of current playing media, music only.""" return self.session.media_album_artist - @property # type: ignore[misc] + @property @needs_session def media_track(self): """Return the track number of current playing media, music only.""" return self.session.media_track - @property # type: ignore[misc] + @property @needs_session def media_duration(self): """Return the duration of current playing media in seconds.""" return self.session.media_duration - @property # type: ignore[misc] + @property @needs_session def media_position(self): """Return the duration of current playing media in seconds.""" return self.session.media_position - @property # type: ignore[misc] + @property @needs_session def media_position_updated_at(self): """When was the position of the current playing media valid.""" return self.session.media_position_updated_at - @property # type: ignore[misc] + @property @needs_session def media_image_url(self): """Return the image URL of current playing media.""" return self.session.media_image_url - @property # type: ignore[misc] + @property @needs_session def media_summary(self): """Return the summary of current playing media.""" return self.session.media_summary - @property # type: ignore[misc] + @property @needs_session def media_title(self): """Return the title of current playing media.""" return self.session.media_title - @property # type: ignore[misc] + @property @needs_session def media_season(self): """Return the season of current playing media (TV Show only).""" return self.session.media_season - @property # type: ignore[misc] + @property @needs_session def media_series_title(self): """Return the title of the series of current playing media.""" return self.session.media_series_title - @property # type: ignore[misc] + @property @needs_session def media_episode(self): """Return the episode of current playing media (TV Show only).""" diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index 78ebaabc0fd..4f6d8a990a6 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -150,7 +150,7 @@ class LazyState(State): self.attr_cache = attr_cache @property # type: ignore[override] - def attributes(self) -> dict[str, Any]: # type: ignore[override] + def attributes(self) -> dict[str, Any]: """State attributes.""" if self._attributes is None: self._attributes = decode_attributes_from_row(self._row, self.attr_cache) @@ -162,7 +162,7 @@ class LazyState(State): self._attributes = value @property # type: ignore[override] - def context(self) -> Context: # type: ignore[override] + def context(self) -> Context: """State context.""" if self._context is None: self._context = Context(id=None) @@ -174,7 +174,7 @@ class LazyState(State): self._context = value @property # type: ignore[override] - def last_changed(self) -> datetime: # type: ignore[override] + def last_changed(self) -> datetime: """Last changed datetime.""" if self._last_changed is None: if (last_changed := self._row.last_changed) is not None: @@ -189,7 +189,7 @@ class LazyState(State): self._last_changed = value @property # type: ignore[override] - def last_updated(self) -> datetime: # type: ignore[override] + def last_updated(self) -> datetime: """Last updated datetime.""" if self._last_updated is None: self._last_updated = process_timestamp(self._row.last_updated) diff --git a/requirements_test.txt b/requirements_test.txt index a94e29ef4fb..9eccb9abb68 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -12,7 +12,7 @@ codecov==2.1.12 coverage==6.4.4 freezegun==1.2.1 mock-open==1.4.0 -mypy==0.971 +mypy==0.981 pre-commit==2.20.0 pylint==2.15.0 pipdeptree==2.3.1 From f48d1a1f07e6513535aa8687c8b6129b4f6f1bab Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 26 Sep 2022 22:10:59 +0200 Subject: [PATCH 749/955] Remove unused icloud code (#79116) --- homeassistant/components/icloud/device_tracker.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/homeassistant/components/icloud/device_tracker.py b/homeassistant/components/icloud/device_tracker.py index a273b7909e2..44297a21112 100644 --- a/homeassistant/components/icloud/device_tracker.py +++ b/homeassistant/components/icloud/device_tracker.py @@ -3,14 +3,13 @@ from __future__ import annotations from typing import Any -from homeassistant.components.device_tracker import AsyncSeeCallback, SourceType +from homeassistant.components.device_tracker import SourceType from homeassistant.components.device_tracker.config_entry import TrackerEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .account import IcloudAccount, IcloudDevice from .const import ( @@ -21,15 +20,6 @@ from .const import ( ) -async def async_setup_scanner( - hass: HomeAssistant, - config: ConfigType, - async_see: AsyncSeeCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> bool: - """Old way of setting up the iCloud tracker.""" - - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: From 3ae092f2720f0c29884cbd2fbf461617f4e87b8d Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Mon, 26 Sep 2022 23:17:33 +0200 Subject: [PATCH 750/955] Update xknx to 1.1.0 - Routing flow control (#79118) xknx 1.1.0 --- homeassistant/components/knx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index c0aa6c3941c..0f2f1201415 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -3,7 +3,7 @@ "name": "KNX", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/knx", - "requirements": ["xknx==1.0.2"], + "requirements": ["xknx==1.1.0"], "codeowners": ["@Julius2342", "@farmio", "@marvin-w"], "quality_scale": "platinum", "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 3a4f2f451ed..7d6711d1660 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2548,7 +2548,7 @@ xboxapi==2.0.1 xiaomi-ble==0.10.0 # homeassistant.components.knx -xknx==1.0.2 +xknx==1.1.0 # homeassistant.components.bluesound # homeassistant.components.fritz diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5ce8b6914fe..92d16c1951a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1758,7 +1758,7 @@ xbox-webapi==2.0.11 xiaomi-ble==0.10.0 # homeassistant.components.knx -xknx==1.0.2 +xknx==1.1.0 # homeassistant.components.bluesound # homeassistant.components.fritz From 0950674146d220b4157f1ce46c575980029ed496 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Mon, 26 Sep 2022 18:22:58 -0400 Subject: [PATCH 751/955] Remove issue from Radarr (#79127) --- homeassistant/components/radarr/__init__.py | 9 --------- homeassistant/components/radarr/strings.json | 4 ---- homeassistant/components/radarr/translations/en.json | 4 ---- 3 files changed, 17 deletions(-) diff --git a/homeassistant/components/radarr/__init__.py b/homeassistant/components/radarr/__init__.py index c590a419c78..5e32f64b7ad 100644 --- a/homeassistant/components/radarr/__init__.py +++ b/homeassistant/components/radarr/__init__.py @@ -50,15 +50,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: severity=IssueSeverity.WARNING, translation_key="deprecated_yaml", ) - async_create_issue( - hass, - DOMAIN, - "removed_attributes", - breaks_in_ha_version="2022.10.0", - is_fixable=False, - severity=IssueSeverity.WARNING, - translation_key="removed_attributes", - ) return True diff --git a/homeassistant/components/radarr/strings.json b/homeassistant/components/radarr/strings.json index 47e7aebce02..6fa9b64c2c8 100644 --- a/homeassistant/components/radarr/strings.json +++ b/homeassistant/components/radarr/strings.json @@ -39,10 +39,6 @@ "deprecated_yaml": { "title": "The Radarr YAML configuration is being removed", "description": "Configuring Radarr using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Radarr YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." - }, - "removed_attributes": { - "title": "Changes to the Radarr integration", - "description": "Some breaking changes has been made in disabling the Movies count sensor out of caution.\n\nThis sensor can cause problems with massive databases. If you still wish to use it, you may do so.\n\nMovie names are no longer included as attributes in the movies sensor.\n\nUpcoming has been removed. It is being modernized as calendar items should be. Disk space is now split into different sensors, one for each folder.\n\nStatus and commands have been removed as they don't appear to have real value for automations." } } } diff --git a/homeassistant/components/radarr/translations/en.json b/homeassistant/components/radarr/translations/en.json index 168c3cc2fe2..cba064fd429 100644 --- a/homeassistant/components/radarr/translations/en.json +++ b/homeassistant/components/radarr/translations/en.json @@ -30,10 +30,6 @@ "deprecated_yaml": { "description": "Configuring Radarr using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Radarr YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", "title": "The Radarr YAML configuration is being removed" - }, - "removed_attributes": { - "description": "Some breaking changes has been made in disabling the Movies count sensor out of caution.\n\nThis sensor can cause problems with massive databases. If you still wish to use it, you may do so.\n\nMovie names are no longer included as attributes in the movies sensor.\n\nUpcoming has been removed. It is being modernized as calendar items should be. Disk space is now split into different sensors, one for each folder.\n\nStatus and commands have been removed as they don't appear to have real value for automations.", - "title": "Changes to the Radarr integration" } }, "options": { From 9c62c55f42439b85b3fe594f9424385d428527dc Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 27 Sep 2022 00:32:32 +0200 Subject: [PATCH 752/955] Fix rfxtrx typing (#79125) --- homeassistant/components/rfxtrx/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index 31a0e4694ba..d0d3793fd82 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -3,7 +3,7 @@ from __future__ import annotations import asyncio import binascii -from collections.abc import Callable +from collections.abc import Callable, Mapping import copy import logging from typing import Any, NamedTuple, cast @@ -116,7 +116,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -def _create_rfx(config: dict[str, Any]) -> rfxtrxmod.Connect: +def _create_rfx(config: Mapping[str, Any]) -> rfxtrxmod.Connect: """Construct a rfx object based on config.""" modes = config.get(CONF_PROTOCOLS) From 7216d4c636792919e93d64402918d210771348e5 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 27 Sep 2022 00:36:10 +0200 Subject: [PATCH 753/955] Add debug logging for MQTT templates (#79016) --- homeassistant/components/mqtt/models.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/homeassistant/components/mqtt/models.py b/homeassistant/components/mqtt/models.py index 566f18bc791..274f7019210 100644 --- a/homeassistant/components/mqtt/models.py +++ b/homeassistant/components/mqtt/models.py @@ -6,6 +6,7 @@ from collections import deque from collections.abc import Callable, Coroutine from dataclasses import dataclass, field import datetime as dt +import logging from typing import TYPE_CHECKING, Any, TypedDict, Union import attr @@ -24,6 +25,8 @@ if TYPE_CHECKING: _SENTINEL = object() +_LOGGER = logging.getLogger(__name__) + ATTR_THIS = "this" PublishPayloadType = Union[str, bytes, int, float, None] @@ -138,6 +141,11 @@ class MqttCommandTemplate: if variables is not None: values.update(variables) + _LOGGER.debug( + "Rendering outgoing payload with variables %s and %s", + values, + self._command_template, + ) return _convert_outgoing_payload( self._command_template.async_render(values, parse_result=False) ) @@ -196,10 +204,23 @@ class MqttValueTemplate: values[ATTR_THIS] = self._template_state if default == _SENTINEL: + _LOGGER.debug( + "Rendering incoming payload '%s' with variables %s and %s", + payload, + values, + self._value_template, + ) return self._value_template.async_render_with_possible_json_value( payload, variables=values ) + _LOGGER.debug( + "Rendering incoming payload '%s' with variables %s with default value '%s' and %s", + payload, + values, + default, + self._value_template, + ) return self._value_template.async_render_with_possible_json_value( payload, default, variables=values ) From 4460953ff4e06dbd893e39d6c93e8c00d12ecd85 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 27 Sep 2022 00:31:59 +0000 Subject: [PATCH 754/955] [ci skip] Translation update --- .../components/awair/translations/hu.json | 2 +- .../components/braviatv/translations/bg.json | 7 ++- .../components/braviatv/translations/hu.json | 12 +++-- .../components/braviatv/translations/no.json | 12 +++-- .../components/demo/translations/hu.json | 2 +- .../dsmr_reader/translations/bg.json | 7 +++ .../dsmr_reader/translations/ca.json | 7 +++ .../dsmr_reader/translations/de.json | 18 +++++++ .../dsmr_reader/translations/es.json | 18 +++++++ .../dsmr_reader/translations/fr.json | 12 +++++ .../dsmr_reader/translations/hu.json | 18 +++++++ .../dsmr_reader/translations/nl.json | 7 +++ .../dsmr_reader/translations/no.json | 18 +++++++ .../dsmr_reader/translations/ru.json | 18 +++++++ .../dsmr_reader/translations/zh-Hant.json | 18 +++++++ .../google_sheets/translations/bg.json | 3 ++ .../google_sheets/translations/ca.json | 3 ++ .../google_sheets/translations/de.json | 4 ++ .../google_sheets/translations/es.json | 4 ++ .../google_sheets/translations/fr.json | 4 ++ .../google_sheets/translations/hu.json | 4 ++ .../google_sheets/translations/nl.json | 3 ++ .../google_sheets/translations/no.json | 4 ++ .../google_sheets/translations/ru.json | 4 ++ .../google_sheets/translations/zh-Hant.json | 4 ++ .../components/lidarr/translations/hu.json | 2 +- .../litterrobot/translations/de.json | 6 +++ .../litterrobot/translations/en.json | 6 +-- .../litterrobot/translations/es.json | 6 +++ .../litterrobot/translations/hu.json | 6 +++ .../litterrobot/translations/no.json | 6 +++ .../litterrobot/translations/ru.json | 6 +++ .../litterrobot/translations/zh-Hant.json | 6 +++ .../components/moon/translations/de.json | 6 +++ .../components/moon/translations/es.json | 6 +++ .../components/moon/translations/ru.json | 6 +++ .../components/moon/translations/zh-Hant.json | 6 +++ .../nibe_heatpump/translations/hu.json | 13 ++++- .../components/radarr/translations/bg.json | 9 ++++ .../components/radarr/translations/en.json | 4 ++ .../components/radarr/translations/es.json | 2 +- .../components/radarr/translations/hu.json | 48 +++++++++++++++++++ .../rainmachine/translations/hu.json | 13 +++++ .../components/season/translations/de.json | 6 +++ .../components/season/translations/es.json | 6 +++ .../components/season/translations/ru.json | 6 +++ .../season/translations/zh-Hant.json | 6 +++ .../components/shelly/translations/bg.json | 7 +++ .../components/shelly/translations/de.json | 8 ++++ .../components/shelly/translations/hu.json | 8 ++++ .../components/shelly/translations/no.json | 8 ++++ .../simplisafe/translations/hu.json | 2 +- .../components/tautulli/translations/ca.json | 1 + .../components/tautulli/translations/de.json | 1 + .../components/tautulli/translations/en.json | 3 +- .../components/tautulli/translations/es.json | 1 + .../components/tautulli/translations/nl.json | 1 + .../components/tautulli/translations/ru.json | 1 + .../tautulli/translations/zh-Hant.json | 1 + .../components/uptime/translations/ca.json | 5 ++ .../components/uptime/translations/de.json | 6 +++ .../components/uptime/translations/es.json | 6 +++ .../components/uptime/translations/ru.json | 6 +++ .../uptime/translations/zh-Hant.json | 6 +++ .../yalexs_ble/translations/hu.json | 2 +- 65 files changed, 449 insertions(+), 18 deletions(-) create mode 100644 homeassistant/components/dsmr_reader/translations/bg.json create mode 100644 homeassistant/components/dsmr_reader/translations/ca.json create mode 100644 homeassistant/components/dsmr_reader/translations/de.json create mode 100644 homeassistant/components/dsmr_reader/translations/es.json create mode 100644 homeassistant/components/dsmr_reader/translations/fr.json create mode 100644 homeassistant/components/dsmr_reader/translations/hu.json create mode 100644 homeassistant/components/dsmr_reader/translations/nl.json create mode 100644 homeassistant/components/dsmr_reader/translations/no.json create mode 100644 homeassistant/components/dsmr_reader/translations/ru.json create mode 100644 homeassistant/components/dsmr_reader/translations/zh-Hant.json create mode 100644 homeassistant/components/radarr/translations/hu.json diff --git a/homeassistant/components/awair/translations/hu.json b/homeassistant/components/awair/translations/hu.json index 1c940eed6c2..2e7be72e57f 100644 --- a/homeassistant/components/awair/translations/hu.json +++ b/homeassistant/components/awair/translations/hu.json @@ -56,7 +56,7 @@ "access_token": "Hozz\u00e1f\u00e9r\u00e9si token", "email": "E-mail" }, - "description": "Regisztr\u00e1lnia kell az Awair fejleszt\u0151i hozz\u00e1f\u00e9r\u00e9si tokenj\u00e9hez a k\u00f6vetkez\u0151 c\u00edmen: https://developer.getawair.com/onboard/login", + "description": "V\u00e1lassza a helyi lehet\u0151s\u00e9get a legjobb \u00e9lm\u00e9ny \u00e9rdek\u00e9ben. Csak akkor haszn\u00e1lja a felh\u0151t, ha az eszk\u00f6z nem ugyanahhoz a h\u00e1l\u00f3zathoz csatlakozik, mint a Home Assistant, vagy ha r\u00e9gebbi eszk\u00f6zzel rendelkezik.", "menu_options": { "cloud": "Felh\u0151n kereszt\u00fcli csatlakoz\u00e1s", "local": "Lok\u00e1lis csatlakoz\u00e1s (aj\u00e1nlott)" diff --git a/homeassistant/components/braviatv/translations/bg.json b/homeassistant/components/braviatv/translations/bg.json index ef8edbdab20..f43846e2283 100644 --- a/homeassistant/components/braviatv/translations/bg.json +++ b/homeassistant/components/braviatv/translations/bg.json @@ -1,10 +1,12 @@ { "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", + "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." }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \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_host": "\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", "unsupported_model": "\u041c\u043e\u0434\u0435\u043b\u044a\u0442 \u043d\u0430 \u0432\u0430\u0448\u0438\u044f \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 \u043d\u0435 \u0441\u0435 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430." }, @@ -14,6 +16,9 @@ "pin": "\u041f\u0418\u041d \u043a\u043e\u0434" } }, + "confirm": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u0437\u0430\u043f\u043e\u0447\u043d\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0442\u0430?" + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442" diff --git a/homeassistant/components/braviatv/translations/hu.json b/homeassistant/components/braviatv/translations/hu.json index d0d372df898..02b64050ee2 100644 --- a/homeassistant/components/braviatv/translations/hu.json +++ b/homeassistant/components/braviatv/translations/hu.json @@ -2,21 +2,27 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "no_ip_control": "Az IP-vez\u00e9rl\u00e9s le van tiltva a TV-n, vagy a TV nem t\u00e1mogatja." + "no_ip_control": "Az IP-vez\u00e9rl\u00e9s le van tiltva a TV-n, vagy a TV nem t\u00e1mogatja.", + "not_bravia_device": "A k\u00e9sz\u00fcl\u00e9k nem egy Bravia TV." }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "invalid_host": "\u00c9rv\u00e9nytelen hosztn\u00e9v vagy IP-c\u00edm", "unsupported_model": "A TV modell nem t\u00e1mogatott." }, "step": { "authorize": { "data": { - "pin": "PIN-k\u00f3d" + "pin": "PIN-k\u00f3d", + "use_psk": "PSK hiteles\u00edt\u00e9s haszn\u00e1lata" }, - "description": "\u00cdrja be a Sony Bravia TV -n l\u00e1that\u00f3 PIN -k\u00f3dot. \n\nHa a PIN -k\u00f3d nem jelenik meg, t\u00f6r\u00f6lje a Home Assistant regisztr\u00e1ci\u00f3j\u00e1t a t\u00e9v\u00e9n, l\u00e9pjen a k\u00f6vetkez\u0151re: Be\u00e1ll\u00edt\u00e1sok - > H\u00e1l\u00f3zat - > T\u00e1voli eszk\u00f6z be\u00e1ll\u00edt\u00e1sai - > T\u00e1vol\u00edtsa el a t\u00e1voli eszk\u00f6z regisztr\u00e1ci\u00f3j\u00e1t.", + "description": "\u00cdrja be a Sony Bravia TV -n l\u00e1that\u00f3 PIN -k\u00f3dot. \n\nHa a PIN -k\u00f3d nem jelenik meg, t\u00f6r\u00f6lje a Home Assistant regisztr\u00e1ci\u00f3j\u00e1t a t\u00e9v\u00e9n, az al\u00e1bbiak szerint: Be\u00e1ll\u00edt\u00e1sok - > H\u00e1l\u00f3zat - > T\u00e1voli eszk\u00f6z be\u00e1ll\u00edt\u00e1sai - > T\u00e1vol\u00edtsa el a t\u00e1voli eszk\u00f6z regisztr\u00e1ci\u00f3j\u00e1t.\n\nA PIN-k\u00f3d helyett haszn\u00e1lhat PSK-t (Pre-Shared-Key). A PSK egy felhaszn\u00e1l\u00f3 \u00e1ltal meghat\u00e1rozott titkos kulcs, amelyet a hozz\u00e1f\u00e9r\u00e9s ellen\u0151rz\u00e9s\u00e9re haszn\u00e1lnak. Ez a hiteles\u00edt\u00e9si m\u00f3dszer aj\u00e1nlott, mivel stabilabb. A PSK enged\u00e9lyez\u00e9s\u00e9hez a TV-n, l\u00e9pjen a k\u00f6vetkez\u0151 oldalra: Be\u00e1ll\u00edt\u00e1sok -> H\u00e1l\u00f3zat -> Otthoni h\u00e1l\u00f3zat be\u00e1ll\u00edt\u00e1sa -> IP-vez\u00e9rl\u00e9s. Ezut\u00e1n jel\u00f6lje be a \"PSK hiteles\u00edt\u00e9s haszn\u00e1lata\" jel\u00f6l\u0151n\u00e9gyzetet, \u00e9s adja meg a PSK-t a PIN-k\u00f3d helyett.", "title": "Sony Bravia TV enged\u00e9lyez\u00e9se" }, + "confirm": { + "description": "El szeretn\u00e9 kezdeni a be\u00e1ll\u00edt\u00e1st?" + }, "user": { "data": { "host": "C\u00edm" diff --git a/homeassistant/components/braviatv/translations/no.json b/homeassistant/components/braviatv/translations/no.json index 067a2fcda97..7fa16b90e8a 100644 --- a/homeassistant/components/braviatv/translations/no.json +++ b/homeassistant/components/braviatv/translations/no.json @@ -2,21 +2,27 @@ "config": { "abort": { "already_configured": "Enheten er allerede konfigurert", - "no_ip_control": "IP-kontrollen er deaktivert p\u00e5 TVen eller TV-en st\u00f8ttes ikke." + "no_ip_control": "IP-kontrollen er deaktivert p\u00e5 TVen eller TV-en st\u00f8ttes ikke.", + "not_bravia_device": "Enheten er ikke en Bravia TV." }, "error": { "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", "invalid_host": "Ugyldig vertsnavn eller IP-adresse", "unsupported_model": "TV-modellen din st\u00f8ttes ikke." }, "step": { "authorize": { "data": { - "pin": "PIN kode" + "pin": "PIN kode", + "use_psk": "Bruk PSK-autentisering" }, - "description": "Angi PIN-koden som vises p\u00e5 Sony Bravia TV. \n\nHvis PIN-koden ikke vises, m\u00e5 du avregistrere Home Assistant p\u00e5 TV-en, g\u00e5 til: Innstillinger -> Nettverk -> Innstillinger for ekstern enhet -> Avregistrere ekstern enhet.", + "description": "Skriv inn PIN-koden som vises p\u00e5 Sony Bravia TV. \n\n Hvis PIN-koden ikke vises, m\u00e5 du avregistrere Home Assistant p\u00e5 TV-en din, g\u00e5 til: Innstillinger - > Nettverk - > Innstillinger for ekstern enhet - > Avregistrer ekstern enhet. \n\n Du kan bruke PSK (Pre-Shared-Key) i stedet for PIN. PSK er en brukerdefinert hemmelig n\u00f8kkel som brukes til tilgangskontroll. Denne autentiseringsmetoden anbefales som mer stabil. For \u00e5 aktivere PSK p\u00e5 TV-en, g\u00e5 til: Innstillinger - > Nettverk - > Oppsett for hjemmenettverk - > IP-kontroll. Kryss s\u00e5 av \u00abBruk PSK-autentisering\u00bb-boksen og skriv inn din PSK i stedet for PIN-kode.", "title": "Godkjenn Sony Bravia TV" }, + "confirm": { + "description": "Vil du starte oppsettet?" + }, "user": { "data": { "host": "Vert" diff --git a/homeassistant/components/demo/translations/hu.json b/homeassistant/components/demo/translations/hu.json index a5aa872fc5c..ece6bad6bfd 100644 --- a/homeassistant/components/demo/translations/hu.json +++ b/homeassistant/components/demo/translations/hu.json @@ -15,7 +15,7 @@ "fix_flow": { "step": { "confirm": { - "description": "Nyomja meg az OK gombot, ha a villog\u00f3 folyad\u00e9kot felt\u00f6lt\u00f6tt\u00e9k.", + "description": "Nyomja meg az OK gombot, ha a folyad\u00e9kot felt\u00f6lt\u00f6tt\u00e9k.", "title": "A villog\u00f3 folyad\u00e9kot fel kell t\u00f6lteni" } } diff --git a/homeassistant/components/dsmr_reader/translations/bg.json b/homeassistant/components/dsmr_reader/translations/bg.json new file mode 100644 index 00000000000..1c6120581b0 --- /dev/null +++ b/homeassistant/components/dsmr_reader/translations/bg.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "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." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dsmr_reader/translations/ca.json b/homeassistant/components/dsmr_reader/translations/ca.json new file mode 100644 index 00000000000..cc92e3ec9f1 --- /dev/null +++ b/homeassistant/components/dsmr_reader/translations/ca.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dsmr_reader/translations/de.json b/homeassistant/components/dsmr_reader/translations/de.json new file mode 100644 index 00000000000..963a3a74e2e --- /dev/null +++ b/homeassistant/components/dsmr_reader/translations/de.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, + "step": { + "confirm": { + "description": "Stelle sicher, dass du die Datenquellen \u201eSplit Topic\u201c in DSMR Reader konfigurierst." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Die Konfiguration von DSMR Reader mit YAML wird entfernt. \n\nDeine vorhandene YAML-Konfiguration wurde automatisch in die Benutzeroberfl\u00e4che importiert. \n\nEntferne die DSMR Reader YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die DSMR-Reader-Konfiguration wird entfernt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dsmr_reader/translations/es.json b/homeassistant/components/dsmr_reader/translations/es.json new file mode 100644 index 00000000000..006c14b3522 --- /dev/null +++ b/homeassistant/components/dsmr_reader/translations/es.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." + }, + "step": { + "confirm": { + "description": "Aseg\u00farate de configurar las fuentes de datos de 'split topic' en DSMR Reader." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Se va a eliminar la configuraci\u00f3n de DSMR Reader mediante YAML. \n\nTu configuraci\u00f3n YAML existente se ha importado a la IU autom\u00e1ticamente. \n\nElimina la configuraci\u00f3n YAML de DSMR Reader de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "Se va a eliminar la configuraci\u00f3n de DSMR Reader" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dsmr_reader/translations/fr.json b/homeassistant/components/dsmr_reader/translations/fr.json new file mode 100644 index 00000000000..9761a37f4b0 --- /dev/null +++ b/homeassistant/components/dsmr_reader/translations/fr.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." + } + }, + "issues": { + "deprecated_yaml": { + "title": "La configuration de DSMR Reader sera bient\u00f4t supprim\u00e9e" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dsmr_reader/translations/hu.json b/homeassistant/components/dsmr_reader/translations/hu.json new file mode 100644 index 00000000000..4936b2363c5 --- /dev/null +++ b/homeassistant/components/dsmr_reader/translations/hu.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." + }, + "step": { + "confirm": { + "description": "Gy\u0151z\u0151dj\u00f6n meg r\u00f3la, hogy a DSMR Readerben be\u00e1ll\u00edtotta az 'split topic' adatforr\u00e1sokat." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "A DSMR Reader YAML haszn\u00e1lat\u00e1val t\u00f6rt\u00e9n\u0151 konfigur\u00e1l\u00e1sa 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 probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a DSMR Reader YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", + "title": "A DSMR Reader konfigur\u00e1ci\u00f3ja elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dsmr_reader/translations/nl.json b/homeassistant/components/dsmr_reader/translations/nl.json new file mode 100644 index 00000000000..703ac8614c4 --- /dev/null +++ b/homeassistant/components/dsmr_reader/translations/nl.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dsmr_reader/translations/no.json b/homeassistant/components/dsmr_reader/translations/no.json new file mode 100644 index 00000000000..93a942bb163 --- /dev/null +++ b/homeassistant/components/dsmr_reader/translations/no.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." + }, + "step": { + "confirm": { + "description": "S\u00f8rg for \u00e5 konfigurere \"delt emne\"-datakildene i DSMR Reader." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfigurering av DSMR Reader med YAML blir fjernet. \n\n Din eksisterende YAML-konfigurasjon har blitt importert til brukergrensesnittet automatisk. \n\n Fjern DSMR Reader YAML-konfigurasjonen fra filen configuration.yaml og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "DSMR Reader-konfigurasjonen fjernes" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dsmr_reader/translations/ru.json b/homeassistant/components/dsmr_reader/translations/ru.json new file mode 100644 index 00000000000..d610f616319 --- /dev/null +++ b/homeassistant/components/dsmr_reader/translations/ru.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." + }, + "step": { + "confirm": { + "description": "\u041e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0438 \u0434\u0430\u043d\u043d\u044b\u0445 'split topic' \u0432 DSMR Reader." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 DSMR Reader \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 DSMR Reader \u0447\u0435\u0440\u0435\u0437 YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dsmr_reader/translations/zh-Hant.json b/homeassistant/components/dsmr_reader/translations/zh-Hant.json new file mode 100644 index 00000000000..99ee4a41b13 --- /dev/null +++ b/homeassistant/components/dsmr_reader/translations/zh-Hant.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + }, + "step": { + "confirm": { + "description": "\u78ba\u5b9a\u65bc DSMR Reader \u5167\u8a2d\u5b9a 'split topic' \u8cc7\u6599\u4f86\u6e90\u3002" + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a\u7684 DSMR Reader \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 DSMR Reader YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "DSMR Reader \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_sheets/translations/bg.json b/homeassistant/components/google_sheets/translations/bg.json index edd42ae2269..80ba164940b 100644 --- a/homeassistant/components/google_sheets/translations/bg.json +++ b/homeassistant/components/google_sheets/translations/bg.json @@ -17,6 +17,9 @@ }, "pick_implementation": { "title": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u043c\u0435\u0442\u043e\u0434 \u0437\u0430 \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" + }, + "reauth_confirm": { + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" } } } diff --git a/homeassistant/components/google_sheets/translations/ca.json b/homeassistant/components/google_sheets/translations/ca.json index bc67a556573..35d26781c3a 100644 --- a/homeassistant/components/google_sheets/translations/ca.json +++ b/homeassistant/components/google_sheets/translations/ca.json @@ -25,6 +25,9 @@ }, "pick_implementation": { "title": "Selecciona el m\u00e8tode d'autenticaci\u00f3" + }, + "reauth_confirm": { + "title": "Reautenticaci\u00f3 de la integraci\u00f3" } } } diff --git a/homeassistant/components/google_sheets/translations/de.json b/homeassistant/components/google_sheets/translations/de.json index a8c198f5b29..f203b3e1133 100644 --- a/homeassistant/components/google_sheets/translations/de.json +++ b/homeassistant/components/google_sheets/translations/de.json @@ -25,6 +25,10 @@ }, "pick_implementation": { "title": "W\u00e4hle die Authentifizierungsmethode" + }, + "reauth_confirm": { + "description": "Die Google Sheets-Integration muss dein Konto neu authentifizieren", + "title": "Integration erneut authentifizieren" } } } diff --git a/homeassistant/components/google_sheets/translations/es.json b/homeassistant/components/google_sheets/translations/es.json index b538396dfb2..5cf0a4ecff4 100644 --- a/homeassistant/components/google_sheets/translations/es.json +++ b/homeassistant/components/google_sheets/translations/es.json @@ -25,6 +25,10 @@ }, "pick_implementation": { "title": "Selecciona el m\u00e9todo de autenticaci\u00f3n" + }, + "reauth_confirm": { + "description": "La integraci\u00f3n de Hojas de c\u00e1lculo de Google necesita volver a autenticar tu cuenta", + "title": "Volver a autenticar la integraci\u00f3n" } } } diff --git a/homeassistant/components/google_sheets/translations/fr.json b/homeassistant/components/google_sheets/translations/fr.json index 74f69cf8ebe..b286d61d377 100644 --- a/homeassistant/components/google_sheets/translations/fr.json +++ b/homeassistant/components/google_sheets/translations/fr.json @@ -22,6 +22,10 @@ }, "pick_implementation": { "title": "S\u00e9lectionner une m\u00e9thode d'authentification" + }, + "reauth_confirm": { + "description": "L'int\u00e9gration Google Sheets doit r\u00e9-authentifier votre compte", + "title": "R\u00e9-authentifier l'int\u00e9gration" } } } diff --git a/homeassistant/components/google_sheets/translations/hu.json b/homeassistant/components/google_sheets/translations/hu.json index 38afcd5db15..c9d044b57b6 100644 --- a/homeassistant/components/google_sheets/translations/hu.json +++ b/homeassistant/components/google_sheets/translations/hu.json @@ -25,6 +25,10 @@ }, "pick_implementation": { "title": "V\u00e1lasszon egy hiteles\u00edt\u00e9si m\u00f3dszert" + }, + "reauth_confirm": { + "description": "A Google T\u00e1bl\u00e1zatok-integr\u00e1ci\u00f3nak \u00fajra kell hiteles\u00edtenie a fi\u00f3kj\u00e1t", + "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" } } } diff --git a/homeassistant/components/google_sheets/translations/nl.json b/homeassistant/components/google_sheets/translations/nl.json index bf3a9a5db0a..d530b4e3add 100644 --- a/homeassistant/components/google_sheets/translations/nl.json +++ b/homeassistant/components/google_sheets/translations/nl.json @@ -17,6 +17,9 @@ }, "pick_implementation": { "title": "Kies een authenticatie methode" + }, + "reauth_confirm": { + "title": "Integratie herauthenticeren" } } } diff --git a/homeassistant/components/google_sheets/translations/no.json b/homeassistant/components/google_sheets/translations/no.json index ad70662b3e2..c4cec211828 100644 --- a/homeassistant/components/google_sheets/translations/no.json +++ b/homeassistant/components/google_sheets/translations/no.json @@ -25,6 +25,10 @@ }, "pick_implementation": { "title": "Velg godkjenningsmetode" + }, + "reauth_confirm": { + "description": "Google Regneark-integreringen m\u00e5 godkjenne kontoen din p\u00e5 nytt", + "title": "Godkjenne integrering p\u00e5 nytt" } } } diff --git a/homeassistant/components/google_sheets/translations/ru.json b/homeassistant/components/google_sheets/translations/ru.json index 34b6905c1ae..71f9d0449d8 100644 --- a/homeassistant/components/google_sheets/translations/ru.json +++ b/homeassistant/components/google_sheets/translations/ru.json @@ -25,6 +25,10 @@ }, "pick_implementation": { "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" + }, + "reauth_confirm": { + "description": "\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Google.", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" } } } diff --git a/homeassistant/components/google_sheets/translations/zh-Hant.json b/homeassistant/components/google_sheets/translations/zh-Hant.json index 29ca87347e8..8afd7ddaf11 100644 --- a/homeassistant/components/google_sheets/translations/zh-Hant.json +++ b/homeassistant/components/google_sheets/translations/zh-Hant.json @@ -25,6 +25,10 @@ }, "pick_implementation": { "title": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f" + }, + "reauth_confirm": { + "description": "Google Sheets \u6574\u5408\u9700\u8981\u91cd\u65b0\u8a8d\u8b49\u60a8\u7684\u5e33\u865f", + "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" } } } diff --git a/homeassistant/components/lidarr/translations/hu.json b/homeassistant/components/lidarr/translations/hu.json index f1b35c83d98..a47d23df43c 100644 --- a/homeassistant/components/lidarr/translations/hu.json +++ b/homeassistant/components/lidarr/translations/hu.json @@ -8,7 +8,7 @@ "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt", - "wrong_app": "Helytelen alkalmaz\u00e1s \u00e9rhet\u0151 el. K\u00e9rj\u00fck, pr\u00f3b\u00e1lja \u00fajra", + "wrong_app": "Helytelen alkalmaz\u00e1s el\u00e9rve. K\u00e9rj\u00fck, pr\u00f3b\u00e1lja \u00fajra", "zeroconf_failed": "Az API-kulcs nem tal\u00e1lhat\u00f3. K\u00e9rem, adja meg" }, "step": { diff --git a/homeassistant/components/litterrobot/translations/de.json b/homeassistant/components/litterrobot/translations/de.json index 18bb6458cf3..8259aa1d16c 100644 --- a/homeassistant/components/litterrobot/translations/de.json +++ b/homeassistant/components/litterrobot/translations/de.json @@ -24,5 +24,11 @@ } } } + }, + "issues": { + "migrated_attributes": { + "description": "Die Vakuumentit\u00e4tsattribute sind jetzt als Diagnosesensoren verf\u00fcgbar. \n\nBitte passe eventuell vorhandene Automatisierungen oder Skripte an, die diese Attribute verwenden.", + "title": "Litter-Robot-Attribute sind jetzt ihre eigenen Sensoren" + } } } \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/en.json b/homeassistant/components/litterrobot/translations/en.json index 3d500bb0f10..76c0dcb79c9 100644 --- a/homeassistant/components/litterrobot/translations/en.json +++ b/homeassistant/components/litterrobot/translations/en.json @@ -27,8 +27,8 @@ }, "issues": { "migrated_attributes": { - "title": "Litter-Robot attributes are now their own sensors", - "description": "The vacuum entity attributes are now available as diagnostic sensors.\n\nPlease adjust any automations or scripts you may have that use these attributes." + "description": "The vacuum entity attributes are now available as diagnostic sensors.\n\nPlease adjust any automations or scripts you may have that use these attributes.", + "title": "Litter-Robot attributes are now their own sensors" } } -} +} \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/es.json b/homeassistant/components/litterrobot/translations/es.json index 64e3408d47f..6ece2f2c221 100644 --- a/homeassistant/components/litterrobot/translations/es.json +++ b/homeassistant/components/litterrobot/translations/es.json @@ -24,5 +24,11 @@ } } } + }, + "issues": { + "migrated_attributes": { + "description": "Los atributos de la entidad aspiradora ahora est\u00e1n disponibles como sensores de diagn\u00f3stico. \n\nPor favor, ajusta cualquier automatizaci\u00f3n o script que puedas tener que use estos atributos.", + "title": "Los atributos de Litter-Robot ahora son sus propios sensores" + } } } \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/hu.json b/homeassistant/components/litterrobot/translations/hu.json index e960eef8867..c35fc251f8a 100644 --- a/homeassistant/components/litterrobot/translations/hu.json +++ b/homeassistant/components/litterrobot/translations/hu.json @@ -24,5 +24,11 @@ } } } + }, + "issues": { + "migrated_attributes": { + "description": "A az entit\u00e1s attrib\u00fatumok mostant\u00f3l diagnosztikai \u00e9rz\u00e9kel\u0151k\u00e9nt \u00e1llnak rendelkez\u00e9sre.\n\nK\u00e9rrem, korrig\u00e1lja azokat az automatizmusokat vagy szkripteket, amelyek ezeket az attrib\u00fatumokat haszn\u00e1lj\u00e1k.", + "title": "A Litter-Robot attrib\u00fatumok most m\u00e1r \u00f6n\u00e1ll\u00f3 \u00e9rz\u00e9kel\u0151k lettek" + } } } \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/no.json b/homeassistant/components/litterrobot/translations/no.json index 40e3013cf45..6268bdf0ff7 100644 --- a/homeassistant/components/litterrobot/translations/no.json +++ b/homeassistant/components/litterrobot/translations/no.json @@ -24,5 +24,11 @@ } } } + }, + "issues": { + "migrated_attributes": { + "description": "Vakuumenhetsattributtene er n\u00e5 tilgjengelige som diagnostiske sensorer. \n\n Juster eventuelle automatiseringer eller skript du m\u00e5tte ha som bruker disse attributtene.", + "title": "Litter-Robot-attributter er n\u00e5 deres egne sensorer" + } } } \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/ru.json b/homeassistant/components/litterrobot/translations/ru.json index a336adcc787..7df59645cb8 100644 --- a/homeassistant/components/litterrobot/translations/ru.json +++ b/homeassistant/components/litterrobot/translations/ru.json @@ -24,5 +24,11 @@ } } } + }, + "issues": { + "migrated_attributes": { + "description": "\u0410\u0442\u0440\u0438\u0431\u0443\u0442\u044b \u043e\u0431\u044a\u0435\u043a\u0442\u0430 \u043f\u044b\u043b\u0435\u0441\u043e\u0441\u0430 \u0442\u0435\u043f\u0435\u0440\u044c \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b \u043a\u0430\u043a \u0441\u0435\u043d\u0441\u043e\u0440\u044b \u0434\u0438\u0430\u0433\u043d\u043e\u0441\u0442\u0438\u043a\u0438.\n\u041e\u0442\u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u0443\u0439\u0442\u0435 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0438 \u0438 \u0441\u043a\u0440\u0438\u043f\u0442\u044b, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0449\u0438\u0435 \u044d\u0442\u0438 \u0430\u0442\u0440\u0438\u0431\u0443\u0442\u044b.", + "title": "\u0410\u0442\u0440\u0438\u0431\u0443\u0442\u044b Litter-Robot \u0442\u0435\u043f\u0435\u0440\u044c \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u044b \u043a\u0430\u043a \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u0435 \u0441\u0435\u043d\u0441\u043e\u0440\u044b." + } } } \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/zh-Hant.json b/homeassistant/components/litterrobot/translations/zh-Hant.json index d83d49912ea..e3d32a2d75f 100644 --- a/homeassistant/components/litterrobot/translations/zh-Hant.json +++ b/homeassistant/components/litterrobot/translations/zh-Hant.json @@ -24,5 +24,11 @@ } } } + }, + "issues": { + "migrated_attributes": { + "description": "\u5438\u5875\u5668\u5be6\u9ad4\u5c6c\u6027\u73fe\u5728\u53ef\u4f5c\u70ba\u8a3a\u65b7\u8cc7\u6599\u611f\u6e2c\u5668\u3002\n\n\u5047\u5982\u6709\u81ea\u52d5\u5316\u6216\u8173\u672c\u4f7f\u7528\u5230\u9019\u4e9b\u5c6c\u6027\u3001\u8acb\u9032\u884c\u8abf\u6574\u3002", + "title": "Litter-Robot \u5c6c\u6027\u73fe\u5728\u6709\u7368\u7acb\u7684\u611f\u6e2c\u5668" + } } } \ No newline at end of file diff --git a/homeassistant/components/moon/translations/de.json b/homeassistant/components/moon/translations/de.json index 4d8d2d45284..00408fde1b0 100644 --- a/homeassistant/components/moon/translations/de.json +++ b/homeassistant/components/moon/translations/de.json @@ -9,5 +9,11 @@ } } }, + "issues": { + "removed_yaml": { + "description": "Das Konfigurieren von Mond mit YAML wurde entfernt. \n\nDeine vorhandene YAML-Konfiguration wird von Home Assistant nicht verwendet. \n\nEntferne die YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die Mond YAML-Konfiguration wurde entfernt" + } + }, "title": "Mond" } \ No newline at end of file diff --git a/homeassistant/components/moon/translations/es.json b/homeassistant/components/moon/translations/es.json index 4bd878ba9f3..2cdf14282e2 100644 --- a/homeassistant/components/moon/translations/es.json +++ b/homeassistant/components/moon/translations/es.json @@ -9,5 +9,11 @@ } } }, + "issues": { + "removed_yaml": { + "description": "Se ha eliminado la configuraci\u00f3n de Moon mediante YAML. \n\nHome Assistant no utiliza tu configuraci\u00f3n YAML existente. \n\nElimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "Se ha eliminado la configuraci\u00f3n YAML de Moon" + } + }, "title": "Luna" } \ No newline at end of file diff --git a/homeassistant/components/moon/translations/ru.json b/homeassistant/components/moon/translations/ru.json index 90f93873205..cc9beaddb52 100644 --- a/homeassistant/components/moon/translations/ru.json +++ b/homeassistant/components/moon/translations/ru.json @@ -9,5 +9,11 @@ } } }, + "issues": { + "removed_yaml": { + "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0441\u0435\u043d\u0441\u043e\u0440\u0430 \u041b\u0443\u043d\u044b \u0442\u0435\u043f\u0435\u0440\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\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 \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant. \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": "\u0423\u0434\u0430\u043b\u0435\u043d\u0430 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0441\u0435\u043d\u0441\u043e\u0440\u0430 \u041b\u0443\u043d\u044b \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML" + } + }, "title": "\u041b\u0443\u043d\u0430" } \ No newline at end of file diff --git a/homeassistant/components/moon/translations/zh-Hant.json b/homeassistant/components/moon/translations/zh-Hant.json index c84da0f79f2..29acac079e1 100644 --- a/homeassistant/components/moon/translations/zh-Hant.json +++ b/homeassistant/components/moon/translations/zh-Hant.json @@ -9,5 +9,11 @@ } } }, + "issues": { + "removed_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Moon \u7684\u529f\u80fd\u5373\u5c07\u79fb\u9664\u3002\n\nHome Assistant \u5c07\u4e0d\u518d\u4f7f\u7528\u73fe\u6709\u7684 YAML \u8a2d\u5b9a\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Moon YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" + } + }, "title": "\u6708\u76f8" } \ No newline at end of file diff --git a/homeassistant/components/nibe_heatpump/translations/hu.json b/homeassistant/components/nibe_heatpump/translations/hu.json index 16c26cdba0e..4fcff29a560 100644 --- a/homeassistant/components/nibe_heatpump/translations/hu.json +++ b/homeassistant/components/nibe_heatpump/translations/hu.json @@ -8,7 +8,18 @@ "address_in_use": "A kiv\u00e1lasztott port m\u00e1r haszn\u00e1latban van ezen a rendszeren.", "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" + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt", + "write": "Hiba a h\u0151szivatty\u00fa \u00edr\u00e1si k\u00e9relm\u00e9ben. Ellen\u0151rizze a portot, c\u00edmet." + }, + "step": { + "user": { + "data": { + "ip_address": "T\u00e1voli IP-c\u00edm", + "listening_port": "Helyi port", + "remote_read_port": "T\u00e1voli olvas\u00e1si port", + "remote_write_port": "T\u00e1voli \u00edr\u00e1si port" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/radarr/translations/bg.json b/homeassistant/components/radarr/translations/bg.json index 42d6e3511de..5ce0eec5412 100644 --- a/homeassistant/components/radarr/translations/bg.json +++ b/homeassistant/components/radarr/translations/bg.json @@ -20,5 +20,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "upcoming_days": "\u0411\u0440\u043e\u0439 \u043f\u0440\u0435\u0434\u0441\u0442\u043e\u044f\u0449\u0438 \u0434\u043d\u0438 \u0437\u0430 \u043f\u043e\u043a\u0430\u0437\u0432\u0430\u043d\u0435" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/radarr/translations/en.json b/homeassistant/components/radarr/translations/en.json index cba064fd429..168c3cc2fe2 100644 --- a/homeassistant/components/radarr/translations/en.json +++ b/homeassistant/components/radarr/translations/en.json @@ -30,6 +30,10 @@ "deprecated_yaml": { "description": "Configuring Radarr using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Radarr YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", "title": "The Radarr YAML configuration is being removed" + }, + "removed_attributes": { + "description": "Some breaking changes has been made in disabling the Movies count sensor out of caution.\n\nThis sensor can cause problems with massive databases. If you still wish to use it, you may do so.\n\nMovie names are no longer included as attributes in the movies sensor.\n\nUpcoming has been removed. It is being modernized as calendar items should be. Disk space is now split into different sensors, one for each folder.\n\nStatus and commands have been removed as they don't appear to have real value for automations.", + "title": "Changes to the Radarr integration" } }, "options": { diff --git a/homeassistant/components/radarr/translations/es.json b/homeassistant/components/radarr/translations/es.json index bb8c8888654..5f88c7ef8f4 100644 --- a/homeassistant/components/radarr/translations/es.json +++ b/homeassistant/components/radarr/translations/es.json @@ -28,7 +28,7 @@ }, "issues": { "deprecated_yaml": { - "description": "Se eliminar\u00e1 la configuraci\u00f3n de Radarr mediante YAML. \n\nTu configuraci\u00f3n YAML existente se ha importado a la IU autom\u00e1ticamente. \n\nElimina la configuraci\u00f3n YAML de Radarr de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "description": "Se va a eliminar la configuraci\u00f3n de Radarr mediante YAML. \n\nTu configuraci\u00f3n YAML existente se ha importado a la IU autom\u00e1ticamente. \n\nElimina la configuraci\u00f3n YAML de Radarr de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", "title": "Se va a eliminar la configuraci\u00f3n YAML de Radarr" }, "removed_attributes": { diff --git a/homeassistant/components/radarr/translations/hu.json b/homeassistant/components/radarr/translations/hu.json new file mode 100644 index 00000000000..f00034f5f5f --- /dev/null +++ b/homeassistant/components/radarr/translations/hu.json @@ -0,0 +1,48 @@ +{ + "config": { + "abort": { + "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt", + "wrong_app": "Helytelen alkalmaz\u00e1s el\u00e9rve. K\u00e9rj\u00fck, pr\u00f3b\u00e1lja \u00fajra", + "zeroconf_failed": "Az API-kulcs nem tal\u00e1lhat\u00f3. K\u00e9rem, adja meg" + }, + "step": { + "reauth_confirm": { + "description": "A Radarr integr\u00e1ci\u00f3j\u00e1t manu\u00e1lisan kell \u00fajra hiteles\u00edteni a Radarr API-val", + "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" + }, + "user": { + "data": { + "api_key": "API kulcs", + "url": "URL", + "verify_ssl": "SSL-tan\u00fas\u00edtv\u00e1ny ellen\u0151rz\u00e9se" + }, + "description": "Az API-kulcs automatikusan lek\u00e9rhet\u0151, ha a bejelentkez\u00e9si hiteles\u00edt\u0151 adatok nem lettek be\u00e1ll\u00edtva az alkalmaz\u00e1sban.\nAz API-kulcs a Radarr webes felhaszn\u00e1l\u00f3i fel\u00fclet Be\u00e1ll\u00edt\u00e1sok > \u00c1ltal\u00e1nos men\u00fcpontj\u00e1ban tal\u00e1lhat\u00f3." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "A Radarr YAML haszn\u00e1lat\u00e1val t\u00f6rt\u00e9n\u0151 konfigur\u00e1l\u00e1sa 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 Radarr YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", + "title": "A Radarr YAML konfigur\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + }, + "removed_attributes": { + "description": "N\u00e9h\u00e1ny v\u00e1ltoztat\u00e1s t\u00f6rt\u00e9nt a Filmek sz\u00e1m\u00e1nak \u00e9rz\u00e9kel\u0151j\u00e9nek \u00f3vatoss\u00e1gb\u00f3l t\u00f6rt\u00e9n\u0151 letilt\u00e1s\u00e1ban.\n\nEz az \u00e9rz\u00e9kel\u0151 probl\u00e9m\u00e1kat okozhat hatalmas adatb\u00e1zisok eset\u00e9n. Ha tov\u00e1bbra is haszn\u00e1lni szeretn\u00e9, megteheti.\n\nA filmek nevei t\u00f6bb\u00e9 nem szerepelnek attrib\u00fatumk\u00e9nt a filmek \u00e9rz\u00e9kel\u0151ben.\n\nAz Upcoming elt\u00e1vol\u00edt\u00e1sra ker\u00fclt. Korszer\u0171s\u00edt\u00e9sre ker\u00fcl, ahogyan a napt\u00e1relemeknek is kell. A lemezter\u00fclet mostant\u00f3l k\u00fcl\u00f6nb\u00f6z\u0151 \u00e9rz\u00e9kel\u0151kre van felosztva, egy-egy mapp\u00e1hoz.\n\nA st\u00e1tusz \u00e9s a parancsok elt\u00e1vol\u00edt\u00e1sra ker\u00fcltek, mivel \u00fagy t\u0171nik, hogy nincs val\u00f3di \u00e9rt\u00e9k\u00fck az automatiz\u00e1l\u00e1sok sz\u00e1m\u00e1ra.", + "title": "A Radarr-integr\u00e1ci\u00f3 v\u00e1ltoz\u00e1sai" + } + }, + "options": { + "step": { + "init": { + "data": { + "upcoming_days": "A megjelen\u00edteni k\u00edv\u00e1nt k\u00f6vetkez\u0151 napok sz\u00e1ma" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainmachine/translations/hu.json b/homeassistant/components/rainmachine/translations/hu.json index 0aa7e0aeeb4..f86c3905108 100644 --- a/homeassistant/components/rainmachine/translations/hu.json +++ b/homeassistant/components/rainmachine/translations/hu.json @@ -18,6 +18,19 @@ } } }, + "issues": { + "replaced_old_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "Friss\u00edtse az ezt az entit\u00e1st haszn\u00e1l\u00f3 automatiz\u00e1l\u00e1sokat vagy szkripteket, hogy helyette a k\u00f6vetkez\u0151t haszn\u00e1ja: `{replacement_entity_id}`", + "title": "{old_entity_id} entit\u00e1s elt\u00e1vol\u00edt\u00e1sra ker\u00fcl." + } + } + }, + "title": "{old_entity_id} entit\u00e1s elt\u00e1vol\u00edt\u00e1sra ker\u00fcl." + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/season/translations/de.json b/homeassistant/components/season/translations/de.json index fe2819b3c40..c01c6f1b963 100644 --- a/homeassistant/components/season/translations/de.json +++ b/homeassistant/components/season/translations/de.json @@ -10,5 +10,11 @@ } } } + }, + "issues": { + "removed_yaml": { + "description": "Das Konfigurieren von Saison mit YAML wurde entfernt. \n\nDeine vorhandene YAML-Konfiguration wird von Home Assistant nicht verwendet. \n\nEntferne die YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die Saison YAML-Konfiguration wurde entfernt" + } } } \ No newline at end of file diff --git a/homeassistant/components/season/translations/es.json b/homeassistant/components/season/translations/es.json index 0d22ef0bc1c..a92d65637da 100644 --- a/homeassistant/components/season/translations/es.json +++ b/homeassistant/components/season/translations/es.json @@ -10,5 +10,11 @@ } } } + }, + "issues": { + "removed_yaml": { + "description": "Se ha eliminado la configuraci\u00f3n de Season mediante YAML. \n\nHome Assistant no utiliza tu configuraci\u00f3n YAML existente. \n\nElimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "Se ha eliminado la configuraci\u00f3n YAML de Season" + } } } \ No newline at end of file diff --git a/homeassistant/components/season/translations/ru.json b/homeassistant/components/season/translations/ru.json index 4a914a520e5..fdde0dd6319 100644 --- a/homeassistant/components/season/translations/ru.json +++ b/homeassistant/components/season/translations/ru.json @@ -10,5 +10,11 @@ } } } + }, + "issues": { + "removed_yaml": { + "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0441\u0435\u043d\u0441\u043e\u0440\u0430 \u0441\u0435\u0437\u043e\u043d\u0430 \u0442\u0435\u043f\u0435\u0440\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\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 \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant. \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": "\u0423\u0434\u0430\u043b\u0435\u043d\u0430 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0441\u0435\u043d\u0441\u043e\u0440\u0430 \u0421\u0435\u0437\u043e\u043d\u0430 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML" + } } } \ No newline at end of file diff --git a/homeassistant/components/season/translations/zh-Hant.json b/homeassistant/components/season/translations/zh-Hant.json index 738ccab2c26..9f9363edf18 100644 --- a/homeassistant/components/season/translations/zh-Hant.json +++ b/homeassistant/components/season/translations/zh-Hant.json @@ -10,5 +10,11 @@ } } } + }, + "issues": { + "removed_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Season \u7684\u529f\u80fd\u5373\u5c07\u79fb\u9664\u3002\n\nHome Assistant \u5c07\u4e0d\u518d\u4f7f\u7528\u73fe\u6709\u7684 YAML \u8a2d\u5b9a\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Season YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" + } } } \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/bg.json b/homeassistant/components/shelly/translations/bg.json index a000ef9933d..131d4bf19c6 100644 --- a/homeassistant/components/shelly/translations/bg.json +++ b/homeassistant/components/shelly/translations/bg.json @@ -2,6 +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\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", "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": { @@ -17,6 +18,12 @@ "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" } }, + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442" diff --git a/homeassistant/components/shelly/translations/de.json b/homeassistant/components/shelly/translations/de.json index 19a5108a752..4990f40a93a 100644 --- a/homeassistant/components/shelly/translations/de.json +++ b/homeassistant/components/shelly/translations/de.json @@ -2,6 +2,8 @@ "config": { "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich", + "reauth_unsuccessful": "Die erneute Authentifizierung war nicht erfolgreich. Bitte entferne die Integration und richte sie erneut ein.", "unsupported_firmware": "Das Ger\u00e4t verwendet eine nicht unterst\u00fctzte Firmware-Version." }, "error": { @@ -21,6 +23,12 @@ "username": "Benutzername" } }, + "reauth_confirm": { + "data": { + "password": "Passwort", + "username": "Benutzername" + } + }, "user": { "data": { "host": "Host" diff --git a/homeassistant/components/shelly/translations/hu.json b/homeassistant/components/shelly/translations/hu.json index 71a9e96126b..7878e7d28f8 100644 --- a/homeassistant/components/shelly/translations/hu.json +++ b/homeassistant/components/shelly/translations/hu.json @@ -2,6 +2,8 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt.", + "reauth_unsuccessful": "Az \u00fajrahiteles\u00edt\u00e9s sikertelen volt, k\u00e9rem, t\u00e1vol\u00edtsa el az integr\u00e1ci\u00f3t, \u00e9s \u00e1ll\u00edtsa be \u00fajra.", "unsupported_firmware": "Az eszk\u00f6z nem t\u00e1mogatott firmware verzi\u00f3t haszn\u00e1l." }, "error": { @@ -21,6 +23,12 @@ "username": "Felhaszn\u00e1l\u00f3n\u00e9v" } }, + "reauth_confirm": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + }, "user": { "data": { "host": "C\u00edm" diff --git a/homeassistant/components/shelly/translations/no.json b/homeassistant/components/shelly/translations/no.json index 072d0bf4ee7..e6cd94ee09a 100644 --- a/homeassistant/components/shelly/translations/no.json +++ b/homeassistant/components/shelly/translations/no.json @@ -2,6 +2,8 @@ "config": { "abort": { "already_configured": "Enheten er allerede konfigurert", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", + "reauth_unsuccessful": "Re-autentisering mislyktes. Fjern integrasjonen og konfigurer den p\u00e5 nytt.", "unsupported_firmware": "Enheten bruker en ikke-st\u00f8ttet firmwareversjon." }, "error": { @@ -21,6 +23,12 @@ "username": "Brukernavn" } }, + "reauth_confirm": { + "data": { + "password": "Passord", + "username": "Brukernavn" + } + }, "user": { "data": { "host": "Vert" diff --git a/homeassistant/components/simplisafe/translations/hu.json b/homeassistant/components/simplisafe/translations/hu.json index 1ac24d8468c..d908785a266 100644 --- a/homeassistant/components/simplisafe/translations/hu.json +++ b/homeassistant/components/simplisafe/translations/hu.json @@ -35,7 +35,7 @@ "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, - "description": "A SimpliSafe a webes alkalmaz\u00e1son kereszt\u00fcl hiteles\u00edti a felhaszn\u00e1l\u00f3kat. A technikai korl\u00e1toz\u00e1sok miatt a folyamat v\u00e9g\u00e9n van egy k\u00e9zi l\u00e9p\u00e9s: k\u00e9rem, hogy a kezd\u00e9s el\u0151tt olvassa el a [dokument\u00e1ci\u00f3t](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code).\n\nHa k\u00e9szen \u00e1ll, kattintson [ide]({url}) a SimpliSafe webes alkalmaz\u00e1s megnyit\u00e1s\u00e1hoz \u00e9s a hiteles\u00edt\u0151 adatok megad\u00e1s\u00e1hoz. Ha a folyamat befejez\u0151d\u00f6tt, t\u00e9rjen vissza ide, \u00e9s adja meg a SimpliSafe webalkalmaz\u00e1s URL-c\u00edm\u00e9r\u0151l sz\u00e1rmaz\u00f3 enged\u00e9lyez\u00e9si k\u00f3dot." + "description": "A SimpliSafe a webes alkalmaz\u00e1son kereszt\u00fcl hiteles\u00edti a felhaszn\u00e1l\u00f3kat. A technikai korl\u00e1toz\u00e1sok miatt a folyamat v\u00e9g\u00e9n van egy manu\u00e1lis l\u00e9p\u00e9s; k\u00e9rj\u00fck, hogy a kezd\u00e9s el\u0151tt olvassa el a [dokument\u00e1ci\u00f3t](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code).\n\nHa k\u00e9szen \u00e1ll, kattintson [ide]({url}) a SimpliSafe webes alkalmaz\u00e1s megnyit\u00e1s\u00e1hoz \u00e9s adja meg a hiteles\u00edt\u0151 adatokat. Ha m\u00e1r bejelentkezett a SimpliSafe rendszerbe a b\u00f6ng\u00e9sz\u0151ben, akkor \u00e9rdemes egy \u00faj lapot nyitni, majd a fenti URL-t bem\u00e1solni/beilleszteni abba a lapba.\n\nHa a folyamat befejez\u0151d\u00f6tt, t\u00e9rjen vissza ide, \u00e9s adja meg az enged\u00e9lyez\u00e9si k\u00f3dot a `com.simplisafe.mobile` URL-r\u0151l." } } }, diff --git a/homeassistant/components/tautulli/translations/ca.json b/homeassistant/components/tautulli/translations/ca.json index 6e53dd83be5..cc1ea05a46e 100644 --- a/homeassistant/components/tautulli/translations/ca.json +++ b/homeassistant/components/tautulli/translations/ca.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "El servei ja est\u00e0 configurat", "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament", "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." }, diff --git a/homeassistant/components/tautulli/translations/de.json b/homeassistant/components/tautulli/translations/de.json index bb1e2bc8c95..fe6cc4f82ac 100644 --- a/homeassistant/components/tautulli/translations/de.json +++ b/homeassistant/components/tautulli/translations/de.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Der Dienst ist bereits konfiguriert", "reauth_successful": "Die erneute Authentifizierung war erfolgreich", "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, diff --git a/homeassistant/components/tautulli/translations/en.json b/homeassistant/components/tautulli/translations/en.json index 078f02d7bcd..daefd71bf2c 100644 --- a/homeassistant/components/tautulli/translations/en.json +++ b/homeassistant/components/tautulli/translations/en.json @@ -1,8 +1,9 @@ { "config": { "abort": { + "already_configured": "Service is already configured", "reauth_successful": "Re-authentication was successful", - "already_configured": "Service is already configured" + "single_instance_allowed": "Already configured. Only a single configuration possible." }, "error": { "cannot_connect": "Failed to connect", diff --git a/homeassistant/components/tautulli/translations/es.json b/homeassistant/components/tautulli/translations/es.json index e1c7c71b2f4..2bbdc4facea 100644 --- a/homeassistant/components/tautulli/translations/es.json +++ b/homeassistant/components/tautulli/translations/es.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "El servicio ya est\u00e1 configurado", "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente", "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, diff --git a/homeassistant/components/tautulli/translations/nl.json b/homeassistant/components/tautulli/translations/nl.json index 5e791e33a8f..f01a1fdb17d 100644 --- a/homeassistant/components/tautulli/translations/nl.json +++ b/homeassistant/components/tautulli/translations/nl.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Dienst is al geconfigureerd", "reauth_successful": "Herauthenticatie geslaagd", "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." }, diff --git a/homeassistant/components/tautulli/translations/ru.json b/homeassistant/components/tautulli/translations/ru.json index fc5e6584157..4f777441385 100644 --- a/homeassistant/components/tautulli/translations/ru.json +++ b/homeassistant/components/tautulli/translations/ru.json @@ -1,6 +1,7 @@ { "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.", "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e.", "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." }, diff --git a/homeassistant/components/tautulli/translations/zh-Hant.json b/homeassistant/components/tautulli/translations/zh-Hant.json index aed9ce361ee..c21af61cfa5 100644 --- a/homeassistant/components/tautulli/translations/zh-Hant.json +++ b/homeassistant/components/tautulli/translations/zh-Hant.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f", "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, diff --git a/homeassistant/components/uptime/translations/ca.json b/homeassistant/components/uptime/translations/ca.json index fe8852d4488..bbd6caebc11 100644 --- a/homeassistant/components/uptime/translations/ca.json +++ b/homeassistant/components/uptime/translations/ca.json @@ -9,5 +9,10 @@ } } }, + "issues": { + "removed_yaml": { + "title": "La configuraci\u00f3 YAML d'Uptime s'ha eliminat" + } + }, "title": "Temps en funcionament" } \ No newline at end of file diff --git a/homeassistant/components/uptime/translations/de.json b/homeassistant/components/uptime/translations/de.json index 1aa173edbd8..483e9303e29 100644 --- a/homeassistant/components/uptime/translations/de.json +++ b/homeassistant/components/uptime/translations/de.json @@ -9,5 +9,11 @@ } } }, + "issues": { + "removed_yaml": { + "description": "Die Konfiguration von Betriebszeit mit YAML wurde entfernt. \n\nDeine vorhandene YAML-Konfiguration wird von Home Assistant nicht verwendet. \n\nEntferne die YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die Betriebszeit YAML-Konfiguration wurde entfernt" + } + }, "title": "Betriebszeit" } \ No newline at end of file diff --git a/homeassistant/components/uptime/translations/es.json b/homeassistant/components/uptime/translations/es.json index e04b528f153..90329bbd7f8 100644 --- a/homeassistant/components/uptime/translations/es.json +++ b/homeassistant/components/uptime/translations/es.json @@ -9,5 +9,11 @@ } } }, + "issues": { + "removed_yaml": { + "description": "Se ha eliminado la configuraci\u00f3n Uptime mediante YAML. \n\nHome Assistant no utiliza tu configuraci\u00f3n YAML existente. \n\nElimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "Se ha eliminado la configuraci\u00f3n YAML de Uptime" + } + }, "title": "Tiempo de funcionamiento" } \ No newline at end of file diff --git a/homeassistant/components/uptime/translations/ru.json b/homeassistant/components/uptime/translations/ru.json index 7c6270221a7..d08db07b3b0 100644 --- a/homeassistant/components/uptime/translations/ru.json +++ b/homeassistant/components/uptime/translations/ru.json @@ -9,5 +9,11 @@ } } }, + "issues": { + "removed_yaml": { + "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0441\u0435\u043d\u0441\u043e\u0440\u0430 \u0432\u0440\u0435\u043c\u0435\u043d\u0438 \u0440\u0430\u0431\u043e\u0442\u044b \u0441\u0435\u0440\u0432\u0435\u0440\u0430 \u0442\u0435\u043f\u0435\u0440\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\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 \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant. \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": "\u0423\u0434\u0430\u043b\u0435\u043d\u0430 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0441\u0435\u043d\u0441\u043e\u0440\u0430 \u0432\u0440\u0435\u043c\u0435\u043d\u0438 \u0440\u0430\u0431\u043e\u0442\u044b \u0441\u0435\u0440\u0432\u0435\u0440\u0430 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML" + } + }, "title": "\u0412\u0440\u0435\u043c\u044f \u0440\u0430\u0431\u043e\u0442\u044b \u0441\u0435\u0440\u0432\u0435\u0440\u0430" } \ No newline at end of file diff --git a/homeassistant/components/uptime/translations/zh-Hant.json b/homeassistant/components/uptime/translations/zh-Hant.json index ed0b902348c..6a9fc2057a1 100644 --- a/homeassistant/components/uptime/translations/zh-Hant.json +++ b/homeassistant/components/uptime/translations/zh-Hant.json @@ -9,5 +9,11 @@ } } }, + "issues": { + "removed_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Uptime \u7684\u529f\u80fd\u5373\u5c07\u79fb\u9664\u3002\n\nHome Assistant \u5c07\u4e0d\u518d\u4f7f\u7528\u73fe\u6709\u7684 YAML \u8a2d\u5b9a\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Uptime YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" + } + }, "title": "\u904b\u4f5c\u6642\u9593" } \ No newline at end of file diff --git a/homeassistant/components/yalexs_ble/translations/hu.json b/homeassistant/components/yalexs_ble/translations/hu.json index fc957171d4f..b0ca6ccdac4 100644 --- a/homeassistant/components/yalexs_ble/translations/hu.json +++ b/homeassistant/components/yalexs_ble/translations/hu.json @@ -24,7 +24,7 @@ "key": "Offline kulcs (32 b\u00e1jtos hexa karakterl\u00e1nc)", "slot": "Offline kulcshely (eg\u00e9sz sz\u00e1m 0 \u00e9s 255 k\u00f6z\u00f6tt)" }, - "description": "Tekintse meg a {docs_url} c\u00edmen tal\u00e1lhat\u00f3 dokument\u00e1ci\u00f3t, hogy hogyan szerezze meg az offline kulcsot." + "description": "Az offline kulcs megtal\u00e1l\u00e1s\u00e1nak m\u00f3dja a dokument\u00e1ci\u00f3ban tal\u00e1lhat\u00f3." } } } From 50e732d00f5d2116712518eeecccbcd7ae939b8b Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 27 Sep 2022 02:37:04 +0200 Subject: [PATCH 755/955] Add image_processing device_class StrEnum (#79124) --- .../components/image_processing/__init__.py | 27 ++++++++++++------- .../openalpr_local/image_processing.py | 8 +++--- .../seven_segments/image_processing.py | 8 +++--- 3 files changed, 23 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/image_processing/__init__.py b/homeassistant/components/image_processing/__init__.py index 8987a366aee..29adafe90b8 100644 --- a/homeassistant/components/image_processing/__init__.py +++ b/homeassistant/components/image_processing/__init__.py @@ -8,6 +8,7 @@ from typing import Any, Final, TypedDict, final import voluptuous as vol +from homeassistant.backports.enum import StrEnum from homeassistant.components.camera import Image from homeassistant.const import ( ATTR_ENTITY_ID, @@ -30,11 +31,19 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = "image_processing" SCAN_INTERVAL = timedelta(seconds=10) -DEVICE_CLASSES = [ - "alpr", # Automatic license plate recognition - "face", # Face - "ocr", # OCR -] + +class ImageProcessingDeviceClass(StrEnum): + """Device class for image processing entities.""" + + # Automatic license plate recognition + ALPR = "alpr" + + # Face + FACE = "face" + + # OCR + OCR = "ocr" + SERVICE_SCAN = "scan" @@ -113,6 +122,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: class ImageProcessingEntity(Entity): """Base entity class for image processing.""" + _attr_device_class: ImageProcessingDeviceClass | str | None timeout = DEFAULT_TIMEOUT @property @@ -156,6 +166,8 @@ class ImageProcessingEntity(Entity): class ImageProcessingFaceEntity(ImageProcessingEntity): """Base entity class for face image processing.""" + _attr_device_class = ImageProcessingDeviceClass.FACE + def __init__(self) -> None: """Initialize base face identify/verify entity.""" self.faces: list[FaceInformation] = [] @@ -185,11 +197,6 @@ class ImageProcessingFaceEntity(ImageProcessingEntity): return state - @property - def device_class(self) -> str: - """Return the class of this device, from component DEVICE_CLASSES.""" - return "face" - @final @property def state_attributes(self) -> dict[str, Any]: diff --git a/homeassistant/components/openalpr_local/image_processing.py b/homeassistant/components/openalpr_local/image_processing.py index 87d189fdbd8..0237bc1f60c 100644 --- a/homeassistant/components/openalpr_local/image_processing.py +++ b/homeassistant/components/openalpr_local/image_processing.py @@ -12,6 +12,7 @@ from homeassistant.components.image_processing import ( ATTR_CONFIDENCE, CONF_CONFIDENCE, PLATFORM_SCHEMA, + ImageProcessingDeviceClass, ImageProcessingEntity, ) from homeassistant.const import ( @@ -102,6 +103,8 @@ async def async_setup_platform( class ImageProcessingAlprEntity(ImageProcessingEntity): """Base entity class for ALPR image processing.""" + _attr_device_class = ImageProcessingDeviceClass.ALPR + def __init__(self) -> None: """Initialize base ALPR entity.""" self.plates: dict[str, float] = {} @@ -120,11 +123,6 @@ class ImageProcessingAlprEntity(ImageProcessingEntity): plate = i_pl return plate - @property - def device_class(self): - """Return the class of this device, from component DEVICE_CLASSES.""" - return "alpr" - @property def extra_state_attributes(self): """Return device specific state attributes.""" diff --git a/homeassistant/components/seven_segments/image_processing.py b/homeassistant/components/seven_segments/image_processing.py index 77aa036f361..b6accf30de8 100644 --- a/homeassistant/components/seven_segments/image_processing.py +++ b/homeassistant/components/seven_segments/image_processing.py @@ -11,6 +11,7 @@ import voluptuous as vol from homeassistant.components.image_processing import ( PLATFORM_SCHEMA, + ImageProcessingDeviceClass, ImageProcessingEntity, ) from homeassistant.const import CONF_ENTITY_ID, CONF_NAME, CONF_SOURCE @@ -69,6 +70,8 @@ async def async_setup_platform( class ImageProcessingSsocr(ImageProcessingEntity): """Representation of the seven segments OCR image processing entity.""" + _attr_device_class = ImageProcessingDeviceClass.OCR + def __init__(self, hass, camera_entity, config, name): """Initialize seven segments processing.""" self.hass = hass @@ -105,11 +108,6 @@ class ImageProcessingSsocr(ImageProcessingEntity): ) self._command.append(self.filepath) - @property - def device_class(self): - """Return the class of this device, from component DEVICE_CLASSES.""" - return "ocr" - @property def camera_entity(self): """Return camera entity id from process pictures.""" From 1e3a3d32ad69b17fa01030462ba8fad61305d2e6 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 27 Sep 2022 02:38:19 +0200 Subject: [PATCH 756/955] Use explicit return value in frontend (#79122) --- homeassistant/components/frontend/__init__.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 188ecb8ff98..40989f41f19 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -548,11 +548,9 @@ class IndexView(web_urldispatcher.AbstractResource): """Return a dict with additional info useful for introspection.""" return {"panels": list(self.hass.data[DATA_PANELS])} - def freeze(self) -> None: - """Freeze the resource.""" - def raw_match(self, path: str) -> bool: """Perform a raw match against path.""" + return False def get_template(self) -> jinja2.Template: """Get template.""" From 499c3410d1177eeec478af366e275a41b3e6ea60 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Mon, 26 Sep 2022 17:40:23 -0700 Subject: [PATCH 757/955] Add browse media to forked-daapd (#79009) * Add browse media to forked-daapd * Use elif in async_browse_image * Add tests * Add tests * Add test * Fix test --- .../components/forked_daapd/browse_media.py | 331 ++++++++++++++++++ .../components/forked_daapd/const.py | 9 +- .../components/forked_daapd/media_player.py | 118 +++++-- tests/components/forked_daapd/conftest.py | 28 ++ .../forked_daapd/test_browse_media.py | 294 ++++++++++++++++ .../forked_daapd/test_media_player.py | 55 +-- 6 files changed, 776 insertions(+), 59 deletions(-) create mode 100644 homeassistant/components/forked_daapd/browse_media.py create mode 100644 tests/components/forked_daapd/conftest.py create mode 100644 tests/components/forked_daapd/test_browse_media.py diff --git a/homeassistant/components/forked_daapd/browse_media.py b/homeassistant/components/forked_daapd/browse_media.py new file mode 100644 index 00000000000..9ea7104186e --- /dev/null +++ b/homeassistant/components/forked_daapd/browse_media.py @@ -0,0 +1,331 @@ +"""Browse media for forked-daapd.""" +from __future__ import annotations + +from collections.abc import Sequence +from dataclasses import dataclass +from typing import TYPE_CHECKING, Any, Union, cast +from urllib.parse import quote, unquote + +from homeassistant.components.media_player import BrowseMedia, MediaClass, MediaType +from homeassistant.components.media_player.errors import BrowseError +from homeassistant.helpers.network import is_internal_request + +from .const import CAN_PLAY_TYPE, URI_SCHEMA + +if TYPE_CHECKING: + from . import media_player + +MEDIA_TYPE_DIRECTORY = "directory" + +TOP_LEVEL_LIBRARY = { + "Albums": (MediaClass.ALBUM, MediaType.ALBUM, ""), + "Artists": (MediaClass.ARTIST, MediaType.ARTIST, ""), + "Playlists": (MediaClass.PLAYLIST, MediaType.PLAYLIST, ""), + "Albums by Genre": (MediaClass.GENRE, MediaType.GENRE, MediaType.ALBUM), + "Tracks by Genre": (MediaClass.GENRE, MediaType.GENRE, MediaType.TRACK), + "Artists by Genre": (MediaClass.GENRE, MediaType.GENRE, MediaType.ARTIST), + "Directories": (MediaClass.DIRECTORY, MEDIA_TYPE_DIRECTORY, ""), +} +MEDIA_TYPE_TO_MEDIA_CLASS = { + MediaType.ALBUM: MediaClass.ALBUM, + MediaType.APP: MediaClass.APP, + MediaType.ARTIST: MediaClass.ARTIST, + MediaType.TRACK: MediaClass.TRACK, + MediaType.PLAYLIST: MediaClass.PLAYLIST, + MediaType.GENRE: MediaClass.GENRE, + MEDIA_TYPE_DIRECTORY: MediaClass.DIRECTORY, +} +CAN_EXPAND_TYPE = { + MediaType.ALBUM, + MediaType.ARTIST, + MediaType.PLAYLIST, + MediaType.GENRE, + MEDIA_TYPE_DIRECTORY, +} +# The keys and values in the below dict are identical only because the +# HA constants happen to align with the Owntone constants. +OWNTONE_TYPE_TO_MEDIA_TYPE = { + "track": MediaType.TRACK, + "playlist": MediaType.PLAYLIST, + "artist": MediaType.ARTIST, + "album": MediaType.ALBUM, + "genre": MediaType.GENRE, + MediaType.APP: MediaType.APP, # This is just for passthrough + MEDIA_TYPE_DIRECTORY: MEDIA_TYPE_DIRECTORY, # This is just for passthrough +} +MEDIA_TYPE_TO_OWNTONE_TYPE = {v: k for k, v in OWNTONE_TYPE_TO_MEDIA_TYPE.items()} + +""" +media_content_id is a uri in the form of SCHEMA:Title:OwnToneURI:Subtype (Subtype only used for Genre) +OwnToneURI is in format library:type:id (for directories, id is path) +media_content_type - type of item (mostly used to check if playable or can expand) +Owntone type may differ from media_content_type when media_content_type is a directory +Owntown type is used in our own branching, but media_content_type is used for determining playability +""" + + +@dataclass +class MediaContent: + """Class for representing Owntone media content.""" + + title: str + type: str + id_or_path: str + subtype: str + + def __init__(self, media_content_id: str) -> None: + """Create MediaContent from media_content_id.""" + ( + _schema, + self.title, + _library, + self.type, + self.id_or_path, + self.subtype, + ) = media_content_id.split(":") + self.title = unquote(self.title) # Title may have special characters + self.id_or_path = unquote(self.id_or_path) # May have special characters + self.type = OWNTONE_TYPE_TO_MEDIA_TYPE[self.type] + + +def create_owntone_uri(media_type: str, id_or_path: str) -> str: + """Create an Owntone uri.""" + return f"library:{MEDIA_TYPE_TO_OWNTONE_TYPE[media_type]}:{quote(id_or_path)}" + + +def create_media_content_id( + title: str, + owntone_uri: str = "", + media_type: str = "", + id_or_path: str = "", + subtype: str = "", +) -> str: + """Create a media_content_id. + + Either owntone_uri or both type and id_or_path must be specified. + """ + if not owntone_uri: + owntone_uri = create_owntone_uri(media_type, id_or_path) + return f"{URI_SCHEMA}:{quote(title)}:{owntone_uri}:{subtype}" + + +def is_owntone_media_content_id(media_content_id: str) -> bool: + """Return whether this media_content_id is from our integration.""" + return media_content_id[: len(URI_SCHEMA)] == URI_SCHEMA + + +def convert_to_owntone_uri(media_content_id: str) -> str: + """Convert media_content_id to Owntone URI.""" + return ":".join(media_content_id.split(":")[2:-1]) + + +async def get_owntone_content( + master: media_player.ForkedDaapdMaster, + media_content_id: str, +) -> BrowseMedia: + """Create response for the given media_content_id.""" + + media_content = MediaContent(media_content_id) + result: list[dict[str, int | str]] | dict[str, Any] | None = None + if media_content.type == MediaType.APP: + return base_owntone_library() + # Query API for next level + if media_content.type == MEDIA_TYPE_DIRECTORY: + # returns tracks, directories, and playlists + directory_path = media_content.id_or_path + if directory_path: + result = await master.api.get_directory(directory=directory_path) + else: + result = await master.api.get_directory() + if result is None: + raise BrowseError( + f"Media not found for {media_content.type} / {media_content_id}" + ) + # Fill in children with subdirectories + children = [] + assert isinstance(result, dict) + for directory in result["directories"]: + path = directory["path"] + children.append( + BrowseMedia( + title=path, + media_class=MediaClass.DIRECTORY, + media_content_id=create_media_content_id( + title=path, media_type=MEDIA_TYPE_DIRECTORY, id_or_path=path + ), + media_content_type=MEDIA_TYPE_DIRECTORY, + can_play=False, + can_expand=True, + ) + ) + result = result["tracks"]["items"] + result["playlists"]["items"] + return create_browse_media_response( + master, + media_content, + cast(list[dict[str, Union[int, str]]], result), + children, + ) + if media_content.id_or_path == "": # top level search + if media_content.type == MediaType.ALBUM: + result = ( + await master.api.get_albums() + ) # list of albums with name, artist, uri + elif media_content.type == MediaType.ARTIST: + result = await master.api.get_artists() # list of artists with name, uri + elif media_content.type == MediaType.GENRE: + if result := await master.api.get_genres(): # returns list of genre names + for item in result: # pylint: disable=not-an-iterable + # add generated genre uris to list of genre names + item["uri"] = create_owntone_uri( + MediaType.GENRE, cast(str, item["name"]) + ) + elif media_content.type == MediaType.PLAYLIST: + result = ( + await master.api.get_playlists() + ) # list of playlists with name, uri + if result is None: + raise BrowseError( + f"Media not found for {media_content.type} / {media_content_id}" + ) + return create_browse_media_response( + master, + media_content, + cast(list[dict[str, Union[int, str]]], result), + ) + # Not a directory or top level of library + # We should have content type and id + if media_content.type == MediaType.ALBUM: + result = await master.api.get_tracks(album_id=media_content.id_or_path) + elif media_content.type == MediaType.ARTIST: + result = await master.api.get_albums(artist_id=media_content.id_or_path) + elif media_content.type == MediaType.GENRE: + if media_content.subtype in { + MediaType.ALBUM, + MediaType.ARTIST, + MediaType.TRACK, + }: + result = await master.api.get_genre( + media_content.id_or_path, media_type=media_content.subtype + ) + elif media_content.type == MediaType.PLAYLIST: + result = await master.api.get_tracks(playlist_id=media_content.id_or_path) + + if result is None: + raise BrowseError( + f"Media not found for {media_content.type} / {media_content_id}" + ) + + return create_browse_media_response( + master, media_content, cast(list[dict[str, Union[int, str]]], result) + ) + + +def create_browse_media_response( + master: media_player.ForkedDaapdMaster, + media_content: MediaContent, + result: list[dict[str, int | str]], + children: list[BrowseMedia] | None = None, +) -> BrowseMedia: + """Convert the results into a browse media response.""" + internal_request = is_internal_request(master.hass) + if not children: # Directory searches will pass in subdirectories as children + children = [] + for item in result: + assert isinstance(item["uri"], str) + media_type = OWNTONE_TYPE_TO_MEDIA_TYPE[item["uri"].split(":")[1]] + title = item.get("name") or item.get("title") # only tracks use title + assert isinstance(title, str) + media_content_id = create_media_content_id( + title=f"{media_content.title} / {title}", + owntone_uri=item["uri"], + subtype=media_content.subtype, + ) + if artwork := item.get("artwork_url"): + thumbnail = ( + master.api.full_url(cast(str, artwork)) + if internal_request + else master.get_browse_image_url(media_type, media_content_id) + ) + else: + thumbnail = None + children.append( + BrowseMedia( + title=title, + media_class=MEDIA_TYPE_TO_MEDIA_CLASS[media_type], + media_content_id=media_content_id, + media_content_type=media_type, + can_play=media_type in CAN_PLAY_TYPE, + can_expand=media_type in CAN_EXPAND_TYPE, + thumbnail=thumbnail, + ) + ) + return BrowseMedia( + title=media_content.id_or_path + if media_content.type == MEDIA_TYPE_DIRECTORY + else media_content.title, + media_class=MEDIA_TYPE_TO_MEDIA_CLASS[media_content.type], + media_content_id="", + media_content_type=media_content.type, + can_play=media_content.type in CAN_PLAY_TYPE, + can_expand=media_content.type in CAN_EXPAND_TYPE, + children=children, + ) + + +def base_owntone_library() -> BrowseMedia: + """Return the base of our Owntone library.""" + children = [ + BrowseMedia( + title=name, + media_class=media_class, + media_content_id=create_media_content_id( + title=name, media_type=media_type, subtype=media_subtype + ), + media_content_type=MEDIA_TYPE_DIRECTORY, + can_play=False, + can_expand=True, + ) + for name, (media_class, media_type, media_subtype) in TOP_LEVEL_LIBRARY.items() + ] + return BrowseMedia( + title="Owntone Library", + media_class=MediaClass.APP, + media_content_id=create_media_content_id( + title="Owntone Library", media_type=MediaType.APP + ), + media_content_type=MediaType.APP, + can_play=False, + can_expand=True, + children=children, + thumbnail="https://brands.home-assistant.io/_/forked_daapd/logo.png", + ) + + +def library(other: Sequence[BrowseMedia] | None) -> BrowseMedia: + """Create response to describe contents of library.""" + + top_level_items = [ + BrowseMedia( + title="Owntone Library", + media_class=MediaClass.APP, + media_content_id=create_media_content_id( + title="Owntone Library", media_type=MediaType.APP + ), + media_content_type=MediaType.APP, + can_play=False, + can_expand=True, + thumbnail="https://brands.home-assistant.io/_/forked_daapd/logo.png", + ) + ] + if other: + top_level_items.extend(other) + + return BrowseMedia( + title="Owntone", + media_class=MediaClass.DIRECTORY, + media_content_id="", + media_content_type=MEDIA_TYPE_DIRECTORY, + can_play=False, + can_expand=True, + children=top_level_items, + ) diff --git a/homeassistant/components/forked_daapd/const.py b/homeassistant/components/forked_daapd/const.py index 85b51b3b6ae..bd0ab02e6c2 100644 --- a/homeassistant/components/forked_daapd/const.py +++ b/homeassistant/components/forked_daapd/const.py @@ -1,5 +1,5 @@ """Const for forked-daapd.""" -from homeassistant.components.media_player import MediaPlayerEntityFeature +from homeassistant.components.media_player import MediaPlayerEntityFeature, MediaType CALLBACK_TIMEOUT = 8 # max time between command and callback from forked-daapd server CAN_PLAY_TYPE = { @@ -11,6 +11,12 @@ CAN_PLAY_TYPE = { "audio/x-ms-wma", "audio/aiff", "audio/wav", + MediaType.TRACK, + MediaType.PLAYLIST, + MediaType.ARTIST, + MediaType.ALBUM, + MediaType.GENRE, + MediaType.MUSIC, } CONF_LIBRESPOT_JAVA_PORT = "librespot_java_port" CONF_MAX_PLAYLISTS = "max_playlists" @@ -82,3 +88,4 @@ SUPPORTED_FEATURES_ZONE = ( | MediaPlayerEntityFeature.TURN_OFF ) TTS_TIMEOUT = 20 # max time to wait between TTS getting sent and starting to play +URI_SCHEMA = "owntone" diff --git a/homeassistant/components/forked_daapd/media_player.py b/homeassistant/components/forked_daapd/media_player.py index 389942a9f59..3229e192884 100644 --- a/homeassistant/components/forked_daapd/media_player.py +++ b/homeassistant/components/forked_daapd/media_player.py @@ -32,6 +32,12 @@ from homeassistant.helpers.dispatcher import ( from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.dt import utcnow +from .browse_media import ( + convert_to_owntone_uri, + get_owntone_content, + is_owntone_media_content_id, + library, +) from .const import ( CALLBACK_TIMEOUT, CAN_PLAY_TYPE, @@ -238,7 +244,8 @@ class ForkedDaapdMaster(MediaPlayerEntity): self, clientsession, api, ip_address, api_port, api_password, config_entry ): """Initialize the ForkedDaapd Master Device.""" - self._api = api + # Leave the api public so the browse media helpers can use it + self.api = api self._player = STARTUP_DATA[ "player" ] # _player, _outputs, and _queue are loaded straight from api @@ -416,13 +423,13 @@ class ForkedDaapdMaster(MediaPlayerEntity): async def async_turn_on(self) -> None: """Restore the last on outputs state.""" # restore state - await self._api.set_volume(volume=self._last_volume * 100) + await self.api.set_volume(volume=self._last_volume * 100) if self._last_outputs: futures: list[asyncio.Task[int]] = [] for output in self._last_outputs: futures.append( asyncio.create_task( - self._api.change_output( + self.api.change_output( output["id"], selected=output["selected"], volume=output["volume"], @@ -431,7 +438,7 @@ class ForkedDaapdMaster(MediaPlayerEntity): ) await asyncio.wait(futures) else: # enable all outputs - await self._api.set_enabled_outputs( + await self.api.set_enabled_outputs( [output["id"] for output in self._outputs] ) @@ -440,7 +447,7 @@ class ForkedDaapdMaster(MediaPlayerEntity): await self.async_media_pause() self._last_outputs = self._outputs if any(output["selected"] for output in self._outputs): - await self._api.set_enabled_outputs([]) + await self.api.set_enabled_outputs([]) async def async_toggle(self) -> None: """Toggle the power on the device. @@ -568,32 +575,32 @@ class ForkedDaapdMaster(MediaPlayerEntity): target_volume = 0 else: target_volume = self._last_volume # restore volume level - await self._api.set_volume(volume=target_volume * 100) + await self.api.set_volume(volume=target_volume * 100) async def async_set_volume_level(self, volume: float) -> None: """Set volume - input range [0,1].""" - await self._api.set_volume(volume=volume * 100) + await self.api.set_volume(volume=volume * 100) async def async_media_play(self) -> None: """Start playback.""" if self._use_pipe_control(): await self._pipe_call(self._use_pipe_control(), "async_media_play") else: - await self._api.start_playback() + await self.api.start_playback() async def async_media_pause(self) -> None: """Pause playback.""" if self._use_pipe_control(): await self._pipe_call(self._use_pipe_control(), "async_media_pause") else: - await self._api.pause_playback() + await self.api.pause_playback() async def async_media_stop(self) -> None: """Stop playback.""" if self._use_pipe_control(): await self._pipe_call(self._use_pipe_control(), "async_media_stop") else: - await self._api.stop_playback() + await self.api.stop_playback() async def async_media_previous_track(self) -> None: """Skip to previous track.""" @@ -602,32 +609,32 @@ class ForkedDaapdMaster(MediaPlayerEntity): self._use_pipe_control(), "async_media_previous_track" ) else: - await self._api.previous_track() + await self.api.previous_track() async def async_media_next_track(self) -> None: """Skip to next track.""" if self._use_pipe_control(): await self._pipe_call(self._use_pipe_control(), "async_media_next_track") else: - await self._api.next_track() + await self.api.next_track() async def async_media_seek(self, position: float) -> None: """Seek to position.""" - await self._api.seek(position_ms=position * 1000) + await self.api.seek(position_ms=position * 1000) async def async_clear_playlist(self) -> None: """Clear playlist.""" - await self._api.clear_queue() + await self.api.clear_queue() async def async_set_shuffle(self, shuffle: bool) -> None: """Enable/disable shuffle mode.""" - await self._api.shuffle(shuffle) + await self.api.shuffle(shuffle) @property def media_image_url(self): """Image url of current playing media.""" if url := self._track_info.get("artwork_url"): - url = self._api.full_url(url) + url = self.api.full_url(url) return url async def _save_and_set_tts_volumes(self): @@ -635,11 +642,11 @@ class ForkedDaapdMaster(MediaPlayerEntity): self._last_volume = self.volume_level self._last_outputs = self._outputs if self._outputs: - await self._api.set_volume(volume=self._tts_volume * 100) + await self.api.set_volume(volume=self._tts_volume * 100) futures = [] for output in self._outputs: futures.append( - self._api.change_output( + self.api.change_output( output["id"], selected=True, volume=self._tts_volume * 100 ) ) @@ -660,12 +667,20 @@ class ForkedDaapdMaster(MediaPlayerEntity): self, media_type: MediaType | str, media_id: str, **kwargs: Any ) -> None: """Play a URI.""" + + # Preprocess media_ids if media_source.is_media_source_id(media_id): media_type = MediaType.MUSIC play_item = await media_source.async_resolve_media( self.hass, media_id, self.entity_id ) media_id = play_item.url + elif is_owntone_media_content_id(media_id): + media_id = convert_to_owntone_uri(media_id) + + if media_type not in CAN_PLAY_TYPE: + _LOGGER.warning("Media type '%s' not supported", media_type) + return if media_type == MediaType.MUSIC: media_id = async_process_play_media_url(self.hass, media_id) @@ -684,7 +699,7 @@ class ForkedDaapdMaster(MediaPlayerEntity): ATTR_MEDIA_ENQUEUE, MediaPlayerEnqueue.REPLACE ) if enqueue in {True, MediaPlayerEnqueue.ADD, MediaPlayerEnqueue.REPLACE}: - return await self._api.add_to_queue( + return await self.api.add_to_queue( uris=media_id, playback="start", clear=enqueue == MediaPlayerEnqueue.REPLACE, @@ -699,13 +714,13 @@ class ForkedDaapdMaster(MediaPlayerEntity): 0, ) if enqueue == MediaPlayerEnqueue.NEXT: - return await self._api.add_to_queue( + return await self.api.add_to_queue( uris=media_id, playback="start", position=current_position + 1, ) # enqueue == MediaPlayerEnqueue.PLAY - return await self._api.add_to_queue( + return await self.api.add_to_queue( uris=media_id, playback="start", position=current_position, @@ -732,7 +747,7 @@ class ForkedDaapdMaster(MediaPlayerEntity): ) self._tts_requested = True await sleep_future - await self._api.add_to_queue(uris=media_id, playback="start", clear=True) + await self.api.add_to_queue(uris=media_id, playback="start", clear=True) try: async with async_timeout.timeout(TTS_TIMEOUT): await self._tts_playing_event.wait() @@ -751,7 +766,7 @@ class ForkedDaapdMaster(MediaPlayerEntity): if saved_mute: # mute if we were muted await self.async_mute_volume(True) if self._use_pipe_control(): # resume pipe - await self._api.add_to_queue( + await self.api.add_to_queue( uris=self._sources_uris[self._source], clear=True ) if saved_state == MediaPlayerState.PLAYING: @@ -760,13 +775,13 @@ class ForkedDaapdMaster(MediaPlayerEntity): if not saved_queue: return # Restore stashed queue - await self._api.add_to_queue( + await self.api.add_to_queue( uris=",".join(item["uri"] for item in saved_queue["items"]), playback="start", playback_from_position=saved_queue_position, clear=True, ) - await self._api.seek(position_ms=saved_song_position) + await self.api.seek(position_ms=saved_song_position) if saved_state == MediaPlayerState.PAUSED: await self.async_media_pause() return @@ -788,9 +803,9 @@ class ForkedDaapdMaster(MediaPlayerEntity): if not self._use_pipe_control(): # playlist or clear ends up at default self._source = SOURCE_NAME_DEFAULT if self._sources_uris.get(source): # load uris for pipes or playlists - await self._api.add_to_queue(uris=self._sources_uris[source], clear=True) + await self.api.add_to_queue(uris=self._sources_uris[source], clear=True) elif source == SOURCE_NAME_CLEAR: # clear playlist - await self._api.clear_queue() + await self.api.clear_queue() self.async_write_ha_state() def _use_pipe_control(self): @@ -813,11 +828,50 @@ class ForkedDaapdMaster(MediaPlayerEntity): media_content_id: str | None = None, ) -> BrowseMedia: """Implement the websocket media browsing helper.""" - return await media_source.async_browse_media( - self.hass, - media_content_id, - content_filter=lambda bm: bm.media_content_type in CAN_PLAY_TYPE, - ) + if media_content_id is None or media_source.is_media_source_id( + media_content_id + ): + ms_result = await media_source.async_browse_media( + self.hass, + media_content_id, + content_filter=lambda bm: bm.media_content_type in CAN_PLAY_TYPE, + ) + if media_content_type is None: + # This is the base level, so we combine our library with the media source + return library(ms_result.children) + return ms_result + # media_content_type should only be None if media_content_id is None + assert media_content_type + return await get_owntone_content(self, media_content_id) + + async def async_get_browse_image( + self, + media_content_type: str, + media_content_id: str, + media_image_id: str | None = None, + ) -> tuple[bytes | None, str | None]: + """Fetch image for media browser.""" + + if media_content_type not in { + MediaType.TRACK, + MediaType.ALBUM, + MediaType.ARTIST, + }: + return None, None + owntone_uri = convert_to_owntone_uri(media_content_id) + item_id_str = owntone_uri.rsplit(":", maxsplit=1)[-1] + if media_content_type == MediaType.TRACK: + result = await self.api.get_track(int(item_id_str)) + elif media_content_type == MediaType.ALBUM: + if result := await self.api.get_albums(): + result = next( + (item for item in result if item["id"] == item_id_str), None + ) + elif result := await self.api.get_artists(): + result = next((item for item in result if item["id"] == item_id_str), None) + if url := result.get("artwork_url"): + return await self._async_fetch_image(self.api.full_url(url)) + return None, None class ForkedDaapdUpdater: diff --git a/tests/components/forked_daapd/conftest.py b/tests/components/forked_daapd/conftest.py new file mode 100644 index 00000000000..b9dd7087aef --- /dev/null +++ b/tests/components/forked_daapd/conftest.py @@ -0,0 +1,28 @@ +"""Common fixtures for forked_daapd tests.""" + +import pytest + +from homeassistant.components.forked_daapd.const import CONF_TTS_PAUSE_TIME, DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT + +from tests.common import MockConfigEntry + + +@pytest.fixture(name="config_entry") +def config_entry_fixture(): + """Create hass config_entry fixture.""" + data = { + CONF_HOST: "192.168.1.1", + CONF_PORT: "2345", + CONF_PASSWORD: "", + } + return MockConfigEntry( + version=1, + domain=DOMAIN, + title="", + data=data, + options={CONF_TTS_PAUSE_TIME: 0}, + source=SOURCE_USER, + entry_id=1, + ) diff --git a/tests/components/forked_daapd/test_browse_media.py b/tests/components/forked_daapd/test_browse_media.py new file mode 100644 index 00000000000..6c7b77b97ea --- /dev/null +++ b/tests/components/forked_daapd/test_browse_media.py @@ -0,0 +1,294 @@ +"""Media browsing tests for the forked_daapd media player platform.""" + +from http import HTTPStatus +from unittest.mock import patch + +from homeassistant.components import media_source +from homeassistant.components.forked_daapd.browse_media import create_media_content_id +from homeassistant.components.media_player import MediaType +from homeassistant.components.websocket_api.const import TYPE_RESULT +from homeassistant.setup import async_setup_component + +TEST_MASTER_ENTITY_NAME = "media_player.forked_daapd_server" + + +async def test_async_browse_media(hass, hass_ws_client, config_entry): + """Test browse media.""" + + assert await async_setup_component(hass, media_source.DOMAIN, {}) + await hass.async_block_till_done() + + with patch( + "homeassistant.components.forked_daapd.media_player.ForkedDaapdAPI", + autospec=True, + ) as mock_api: + config_entry.add_to_hass(hass) + await config_entry.async_setup(hass) + await hass.async_block_till_done() + + mock_api.return_value.full_url = lambda x: "http://owntone_instance/" + x + mock_api.return_value.get_directory.side_effect = [ + { + "directories": [ + {"path": "/music/srv/Audiobooks"}, + {"path": "/music/srv/Music"}, + {"path": "/music/srv/Playlists"}, + {"path": "/music/srv/Podcasts"}, + ], + "tracks": { + "items": [ + { + "id": 1, + "title": "input.pipe", + "artist": "Unknown artist", + "artist_sort": "Unknown artist", + "album": "Unknown album", + "album_sort": "Unknown album", + "album_id": "4201163758598356043", + "album_artist": "Unknown artist", + "album_artist_sort": "Unknown artist", + "album_artist_id": "4187901437947843388", + "genre": "Unknown genre", + "year": 0, + "track_number": 0, + "disc_number": 0, + "length_ms": 0, + "play_count": 0, + "skip_count": 0, + "time_added": "2018-11-24T08:41:35Z", + "seek_ms": 0, + "media_kind": "music", + "data_kind": "pipe", + "path": "/music/srv/input.pipe", + "uri": "library:track:1", + "artwork_url": "/artwork/item/1", + } + ], + "total": 1, + "offset": 0, + "limit": -1, + }, + "playlists": { + "items": [ + { + "id": 8, + "name": "radio", + "path": "/music/srv/radio.m3u", + "smart_playlist": True, + "uri": "library:playlist:8", + } + ], + "total": 1, + "offset": 0, + "limit": -1, + }, + } + ] + 4 * [ + {"directories": [], "tracks": {"items": []}, "playlists": {"items": []}} + ] + mock_api.return_value.get_albums.return_value = [ + { + "id": "8009851123233197743", + "name": "Add Violence", + "name_sort": "Add Violence", + "artist": "Nine Inch Nails", + "artist_id": "32561671101664759", + "track_count": 5, + "length_ms": 1634961, + "uri": "library:album:8009851123233197743", + }, + ] + mock_api.return_value.get_artists.return_value = [ + { + "id": "3815427709949443149", + "name": "ABAY", + "name_sort": "ABAY", + "album_count": 1, + "track_count": 10, + "length_ms": 2951554, + "uri": "library:artist:3815427709949443149", + }, + ] + mock_api.return_value.get_genres.return_value = [ + {"name": "Classical"}, + {"name": "Drum & Bass"}, + {"name": "Pop"}, + {"name": "Rock/Pop"}, + {"name": "'90s Alternative"}, + ] + mock_api.return_value.get_playlists.return_value = [ + { + "id": 1, + "name": "radio", + "path": "/music/srv/radio.m3u", + "smart_playlist": False, + "uri": "library:playlist:1", + }, + ] + + # Request playlist through WebSocket + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 1, + "type": "media_player/browse_media", + "entity_id": TEST_MASTER_ENTITY_NAME, + } + ) + msg = await client.receive_json() + # Assert WebSocket response + assert msg["id"] == 1 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + + msg_id = 2 + + async def browse_children(children): + """Browse the children of this BrowseMedia.""" + nonlocal msg_id + for child in children: + if child["can_expand"]: + print("EXPANDING CHILD", child) + await client.send_json( + { + "id": msg_id, + "type": "media_player/browse_media", + "entity_id": TEST_MASTER_ENTITY_NAME, + "media_content_type": child["media_content_type"], + "media_content_id": child["media_content_id"], + } + ) + msg = await client.receive_json() + assert msg["success"] + msg_id += 1 + await browse_children(msg["result"]["children"]) + + await browse_children(msg["result"]["children"]) + + +async def test_async_browse_media_not_found(hass, hass_ws_client, config_entry): + """Test browse media not found.""" + + assert await async_setup_component(hass, media_source.DOMAIN, {}) + await hass.async_block_till_done() + + with patch( + "homeassistant.components.forked_daapd.media_player.ForkedDaapdAPI", + autospec=True, + ) as mock_api: + config_entry.add_to_hass(hass) + await config_entry.async_setup(hass) + await hass.async_block_till_done() + + mock_api.return_value.get_directory.return_value = None + mock_api.return_value.get_albums.return_value = None + mock_api.return_value.get_artists.return_value = None + mock_api.return_value.get_genres.return_value = None + mock_api.return_value.get_playlists.return_value = None + + # Request playlist through WebSocket + client = await hass_ws_client(hass) + msg_id = 1 + for media_type in ( + "directory", + MediaType.ALBUM, + MediaType.ARTIST, + MediaType.GENRE, + MediaType.PLAYLIST, + ): + await client.send_json( + { + "id": msg_id, + "type": "media_player/browse_media", + "entity_id": TEST_MASTER_ENTITY_NAME, + "media_content_type": media_type, + "media_content_id": ( + media_content_id := create_media_content_id( + "title", f"library:{media_type}:" + ) + ), + } + ) + msg = await client.receive_json() + # Assert WebSocket response + assert msg["id"] == msg_id + assert msg["type"] == TYPE_RESULT + assert not msg["success"] + assert ( + msg["error"]["message"] + == f"Media not found for {media_type} / {media_content_id}" + ) + msg_id += 1 + + +async def test_async_browse_image(hass, hass_client, config_entry): + """Test browse media images.""" + + with patch( + "homeassistant.components.forked_daapd.media_player.ForkedDaapdAPI", + autospec=True, + ) as mock_api: + config_entry.add_to_hass(hass) + await config_entry.async_setup(hass) + await hass.async_block_till_done() + client = await hass_client() + mock_api.return_value.full_url = lambda x: "http://owntone_instance/" + x + mock_api.return_value.get_albums.return_value = [ + {"id": "8009851123233197743", "artwork_url": "some_album_image"}, + ] + mock_api.return_value.get_artists.return_value = [ + {"id": "3815427709949443149", "artwork_url": "some_artist_image"}, + ] + mock_api.return_value.get_track.return_value = { + "id": 456, + "artwork_url": "some_track_image", + } + media_content_id = create_media_content_id( + "title", media_type=MediaType.ALBUM, id_or_path="8009851123233197743" + ) + + with patch( + "homeassistant.components.media_player.async_fetch_image" + ) as mock_fetch_image: + for media_type, media_id in ( + (MediaType.ALBUM, "8009851123233197743"), + (MediaType.ARTIST, "3815427709949443149"), + (MediaType.TRACK, "456"), + ): + mock_fetch_image.return_value = (b"image_bytes", media_type) + media_content_id = create_media_content_id( + "title", media_type=media_type, id_or_path=media_id + ) + resp = await client.get( + f"/api/media_player_proxy/{TEST_MASTER_ENTITY_NAME}/browse_media/{media_type}/{media_content_id}" + ) + assert ( + mock_fetch_image.call_args[0][2] + == f"http://owntone_instance/some_{media_type}_image" + ) + assert resp.status == HTTPStatus.OK + assert resp.content_type == media_type + assert await resp.read() == b"image_bytes" + + +async def test_async_browse_image_missing(hass, hass_client, config_entry, caplog): + """Test browse media images with no image available.""" + + with patch( + "homeassistant.components.forked_daapd.media_player.ForkedDaapdAPI", + autospec=True, + ) as mock_api: + config_entry.add_to_hass(hass) + await config_entry.async_setup(hass) + await hass.async_block_till_done() + client = await hass_client() + mock_api.return_value.full_url = lambda x: "http://owntone_instance/" + x + mock_api.return_value.get_track.return_value = {} + + media_content_id = create_media_content_id( + "title", media_type=MediaType.TRACK, id_or_path="456" + ) + resp = await client.get( + f"/api/media_player_proxy/{TEST_MASTER_ENTITY_NAME}/browse_media/{MediaType.TRACK}/{media_content_id}" + ) + assert resp.status == HTTPStatus.INTERNAL_SERVER_ERROR diff --git a/tests/components/forked_daapd/test_media_player.py b/tests/components/forked_daapd/test_media_player.py index 307eb8deea6..893b6c875e2 100644 --- a/tests/components/forked_daapd/test_media_player.py +++ b/tests/components/forked_daapd/test_media_player.py @@ -4,12 +4,12 @@ from unittest.mock import patch import pytest +from homeassistant.components.forked_daapd.browse_media import create_media_content_id from homeassistant.components.forked_daapd.const import ( CONF_LIBRESPOT_JAVA_PORT, CONF_MAX_PLAYLISTS, CONF_TTS_PAUSE_TIME, CONF_TTS_VOLUME, - DOMAIN, SIGNAL_UPDATE_OUTPUTS, SIGNAL_UPDATE_PLAYER, SIGNAL_UPDATE_QUEUE, @@ -54,25 +54,21 @@ from homeassistant.components.media_player import ( MediaPlayerEnqueue, MediaType, ) -from homeassistant.config_entries import SOURCE_USER from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, ATTR_SUPPORTED_FEATURES, - CONF_HOST, - CONF_PASSWORD, - CONF_PORT, STATE_ON, STATE_PAUSED, STATE_UNAVAILABLE, ) -from tests.common import MockConfigEntry, async_mock_signal +from tests.common import async_mock_signal TEST_MASTER_ENTITY_NAME = "media_player.forked_daapd_server" TEST_ZONE_ENTITY_NAMES = [ "media_player.forked_daapd_output_" + x - for x in ["kitchen", "computer", "daapd_fifo"] + for x in ("kitchen", "computer", "daapd_fifo") ] OPTIONS_DATA = { @@ -290,25 +286,6 @@ SAMPLE_PIPES = [ SAMPLE_PLAYLISTS = [{"id": 7, "name": "test_playlist", "uri": "library:playlist:2"}] -@pytest.fixture(name="config_entry") -def config_entry_fixture(): - """Create hass config_entry fixture.""" - data = { - CONF_HOST: "192.168.1.1", - CONF_PORT: "2345", - CONF_PASSWORD: "", - } - return MockConfigEntry( - version=1, - domain=DOMAIN, - title="", - data=data, - options={CONF_TTS_PAUSE_TIME: 0}, - source=SOURCE_USER, - entry_id=1, - ) - - @pytest.fixture(name="get_request_return_values") async def get_request_return_values_fixture(): """Get request return values we can change later.""" @@ -888,3 +865,29 @@ async def test_async_play_media_enqueue(hass, mock_api_object): mock_api_object.add_to_queue.assert_called_with( uris="http://example.com/next.mp3", playback="start", position=1 ) + + +async def test_play_owntone_media(hass, mock_api_object): + """Test async play media with an owntone source.""" + initial_state = hass.states.get(TEST_MASTER_ENTITY_NAME) + await _service_call( + hass, + TEST_MASTER_ENTITY_NAME, + SERVICE_PLAY_MEDIA, + { + ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC, + ATTR_MEDIA_CONTENT_ID: create_media_content_id( + "some song", "library:track:456" + ), + ATTR_MEDIA_ENQUEUE: MediaPlayerEnqueue.PLAY, + }, + ) + state = hass.states.get(TEST_MASTER_ENTITY_NAME) + assert state.state == initial_state.state + assert state.last_updated > initial_state.last_updated + mock_api_object.add_to_queue.assert_called_with( + uris="library:track:456", + playback="start", + position=0, + playback_from_position=0, + ) From c5a58c85018664405da81f5dcd3b3a3077577a60 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 27 Sep 2022 08:05:28 +0200 Subject: [PATCH 758/955] Fix MQTT setup after changing config entry flow options (#79103) Fix issues with config flow options --- homeassistant/components/mqtt/__init__.py | 19 ++------- homeassistant/components/mqtt/config_flow.py | 4 +- tests/components/mqtt/test_config_flow.py | 41 +++++++++++++++++--- 3 files changed, 42 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 306132c4f36..62aad6ca7fe 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -248,20 +248,7 @@ async def _async_config_entry_updated(hass: HomeAssistant, entry: ConfigEntry) - Causes for this is config entry options changing. """ - mqtt_data = get_mqtt_data(hass) - assert (client := mqtt_data.client) is not None - - if (conf := mqtt_data.config) is None: - conf = CONFIG_SCHEMA_BASE(dict(entry.data)) - - mqtt_data.config = _merge_extended_config(entry, conf) - await client.async_disconnect() - client.init_client() - await client.async_connect() - - await discovery.async_stop(hass) - if client.conf.get(CONF_DISCOVERY): - await _async_setup_discovery(hass, cast(ConfigType, mqtt_data.config), entry) + await hass.config_entries.async_reload(entry.entry_id) async def async_fetch_config(hass: HomeAssistant, entry: ConfigEntry) -> dict | None: @@ -317,7 +304,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if mqtt_data.subscriptions_to_restore: mqtt_data.client.subscriptions = mqtt_data.subscriptions_to_restore mqtt_data.subscriptions_to_restore = [] - entry.add_update_listener(_async_config_entry_updated) + mqtt_data.reload_dispatchers.append( + entry.add_update_listener(_async_config_entry_updated) + ) await mqtt_data.client.async_connect() diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index afa2d98af2b..55fd68f8b6a 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -262,7 +262,9 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow): updated_config.update(self.broker_config) updated_config.update(options_config) self.hass.config_entries.async_update_entry( - self.config_entry, data=updated_config + self.config_entry, + data=updated_config, + title=str(self.broker_config[CONF_BROKER]), ) return self.async_create_entry(title="", data={}) diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index dba06e5cd5b..2ad216a3b7b 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -1,5 +1,5 @@ """Test config flow.""" -from unittest.mock import patch +from unittest.mock import AsyncMock, patch import pytest import voluptuous as vol @@ -23,6 +23,15 @@ def mock_finish_setup(): yield mock_finish +@pytest.fixture +def mock_reload_after_entry_update(): + """Mock out the reload after updating the entry.""" + with patch( + "homeassistant.components.mqtt._async_config_entry_updated" + ) as mock_reload: + yield mock_reload + + @pytest.fixture def mock_try_connection(): """Mock the try connection method.""" @@ -180,6 +189,8 @@ async def test_manual_config_set( mock_try_connection.assert_called_once_with(hass, "127.0.0.1", 1883, None, None) # Check config entry got setup assert len(mock_finish_setup.mock_calls) == 1 + config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] + assert config_entry.title == "127.0.0.1" async def test_user_single_instance(hass): @@ -269,7 +280,16 @@ async def test_hassio_confirm(hass, mock_try_connection_success, mock_finish_set assert len(mock_finish_setup.mock_calls) == 1 -async def test_option_flow(hass, mqtt_mock_entry_no_yaml_config, mock_try_connection): +@patch( + "homeassistant.config.async_hass_config_yaml", + AsyncMock(return_value={}), +) +async def test_option_flow( + hass, + mqtt_mock_entry_no_yaml_config, + mock_try_connection, + mock_reload_after_entry_update, +): """Test config flow options.""" mqtt_mock = await mqtt_mock_entry_no_yaml_config() mock_try_connection.return_value = True @@ -339,11 +359,16 @@ async def test_option_flow(hass, mqtt_mock_entry_no_yaml_config, mock_try_connec } await hass.async_block_till_done() - assert mqtt_mock.async_connect.call_count == 1 + assert config_entry.title == "another-broker" + # assert that the entry was reloaded with the new config + assert mock_reload_after_entry_update.call_count == 1 async def test_disable_birth_will( - hass, mqtt_mock_entry_no_yaml_config, mock_try_connection + hass, + mqtt_mock_entry_no_yaml_config, + mock_try_connection, + mock_reload_after_entry_update, ): """Test disabling birth and will.""" mqtt_mock = await mqtt_mock_entry_no_yaml_config() @@ -404,7 +429,8 @@ async def test_disable_birth_will( } await hass.async_block_till_done() - assert mqtt_mock.async_connect.call_count == 1 + # assert that the entry was reloaded with the new config + assert mock_reload_after_entry_update.call_count == 1 def get_default(schema, key): @@ -426,7 +452,10 @@ def get_suggested(schema, key): async def test_option_flow_default_suggested_values( - hass, mqtt_mock_entry_no_yaml_config, mock_try_connection_success + hass, + mqtt_mock_entry_no_yaml_config, + mock_try_connection_success, + mock_reload_after_entry_update, ): """Test config flow options has default/suggested values.""" await mqtt_mock_entry_no_yaml_config() From dc82ae4f692eb59483ba0af79a64c6f3f8b2e88a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 27 Sep 2022 07:16:03 +0100 Subject: [PATCH 759/955] Make VALID_UNITS a set (#79104) * Make VALID_UNITS a set * Also adjust weather --- homeassistant/components/weather/__init__.py | 22 +++++++------- homeassistant/util/pressure.py | 2 +- homeassistant/util/unit_conversion.py | 30 ++++++++++---------- homeassistant/util/unit_system.py | 4 +-- 4 files changed, 29 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index bb358e8d980..2014c5b4eed 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -99,31 +99,31 @@ SCAN_INTERVAL = timedelta(seconds=30) ROUNDING_PRECISION = 2 -VALID_UNITS_PRESSURE: tuple[str, ...] = ( +VALID_UNITS_PRESSURE: set[str] = { PRESSURE_HPA, PRESSURE_MBAR, PRESSURE_INHG, PRESSURE_MMHG, -) -VALID_UNITS_TEMPERATURE: tuple[str, ...] = ( +} +VALID_UNITS_TEMPERATURE: set[str] = { TEMP_CELSIUS, TEMP_FAHRENHEIT, -) -VALID_UNITS_PRECIPITATION: tuple[str, ...] = ( +} +VALID_UNITS_PRECIPITATION: set[str] = { LENGTH_MILLIMETERS, LENGTH_INCHES, -) -VALID_UNITS_VISIBILITY: tuple[str, ...] = ( +} +VALID_UNITS_VISIBILITY: set[str] = { LENGTH_KILOMETERS, LENGTH_MILES, -) -VALID_UNITS_WIND_SPEED: tuple[str, ...] = ( +} +VALID_UNITS_WIND_SPEED: set[str] = { SPEED_FEET_PER_SECOND, SPEED_KILOMETERS_PER_HOUR, SPEED_KNOTS, SPEED_METERS_PER_SECOND, SPEED_MILES_PER_HOUR, -) +} UNIT_CONVERSIONS: dict[str, Callable[[float, str, str], float]] = { ATTR_WEATHER_PRESSURE_UNIT: PressureConverter.convert, @@ -133,7 +133,7 @@ UNIT_CONVERSIONS: dict[str, Callable[[float, str, str], float]] = { ATTR_WEATHER_WIND_SPEED_UNIT: SpeedConverter.convert, } -VALID_UNITS: dict[str, tuple[str, ...]] = { +VALID_UNITS: dict[str, set[str]] = { ATTR_WEATHER_PRESSURE_UNIT: VALID_UNITS_PRESSURE, ATTR_WEATHER_TEMPERATURE_UNIT: VALID_UNITS_TEMPERATURE, ATTR_WEATHER_VISIBILITY_UNIT: VALID_UNITS_VISIBILITY, diff --git a/homeassistant/util/pressure.py b/homeassistant/util/pressure.py index adcadf6dfdb..93da51086e6 100644 --- a/homeassistant/util/pressure.py +++ b/homeassistant/util/pressure.py @@ -18,7 +18,7 @@ from homeassistant.const import ( # pylint: disable=unused-import # noqa: F401 from .unit_conversion import PressureConverter UNIT_CONVERSION: dict[str, float] = PressureConverter.UNIT_CONVERSION -VALID_UNITS: tuple[str, ...] = PressureConverter.VALID_UNITS +VALID_UNITS = PressureConverter.VALID_UNITS def convert(value: float, from_unit: str, to_unit: str) -> float: diff --git a/homeassistant/util/unit_conversion.py b/homeassistant/util/unit_conversion.py index 363c1bf5f3c..344aef9028a 100644 --- a/homeassistant/util/unit_conversion.py +++ b/homeassistant/util/unit_conversion.py @@ -76,7 +76,7 @@ class BaseUnitConverter: UNIT_CLASS: str NORMALIZED_UNIT: str - VALID_UNITS: tuple[str, ...] + VALID_UNITS: set[str] @classmethod def _check_arguments(cls, value: float, from_unit: str, to_unit: str) -> None: @@ -131,7 +131,7 @@ class DistanceConverter(BaseUnitConverterWithUnitConversion): LENGTH_YARD: 1 / _YARD_TO_M, LENGTH_MILES: 1 / _MILE_TO_M, } - VALID_UNITS: tuple[str, ...] = ( + VALID_UNITS = { LENGTH_KILOMETERS, LENGTH_MILES, LENGTH_FEET, @@ -140,7 +140,7 @@ class DistanceConverter(BaseUnitConverterWithUnitConversion): LENGTH_MILLIMETERS, LENGTH_INCHES, LENGTH_YARD, - ) + } class EnergyConverter(BaseUnitConverterWithUnitConversion): @@ -153,11 +153,11 @@ class EnergyConverter(BaseUnitConverterWithUnitConversion): ENERGY_KILO_WATT_HOUR: 1, ENERGY_MEGA_WATT_HOUR: 1 / 1000, } - VALID_UNITS: tuple[str, ...] = ( + VALID_UNITS = { ENERGY_WATT_HOUR, ENERGY_KILO_WATT_HOUR, ENERGY_MEGA_WATT_HOUR, - ) + } class PowerConverter(BaseUnitConverterWithUnitConversion): @@ -169,10 +169,10 @@ class PowerConverter(BaseUnitConverterWithUnitConversion): POWER_WATT: 1, POWER_KILO_WATT: 1 / 1000, } - VALID_UNITS: tuple[str, ...] = ( + VALID_UNITS = { POWER_WATT, POWER_KILO_WATT, - ) + } class PressureConverter(BaseUnitConverterWithUnitConversion): @@ -191,7 +191,7 @@ class PressureConverter(BaseUnitConverterWithUnitConversion): PRESSURE_PSI: 1 / 6894.757, PRESSURE_MMHG: 1 / 133.322, } - VALID_UNITS: tuple[str, ...] = ( + VALID_UNITS = { PRESSURE_PA, PRESSURE_HPA, PRESSURE_KPA, @@ -201,7 +201,7 @@ class PressureConverter(BaseUnitConverterWithUnitConversion): PRESSURE_INHG, PRESSURE_PSI, PRESSURE_MMHG, - ) + } class SpeedConverter(BaseUnitConverterWithUnitConversion): @@ -219,7 +219,7 @@ class SpeedConverter(BaseUnitConverterWithUnitConversion): SPEED_MILES_PER_HOUR: _HRS_TO_SECS / _MILE_TO_M, SPEED_MILLIMETERS_PER_DAY: _DAYS_TO_SECS / _MM_TO_M, } - VALID_UNITS: tuple[str, ...] = ( + VALID_UNITS = { SPEED_FEET_PER_SECOND, SPEED_INCHES_PER_DAY, SPEED_INCHES_PER_HOUR, @@ -228,7 +228,7 @@ class SpeedConverter(BaseUnitConverterWithUnitConversion): SPEED_METERS_PER_SECOND, SPEED_MILES_PER_HOUR, SPEED_MILLIMETERS_PER_DAY, - ) + } class TemperatureConverter(BaseUnitConverter): @@ -236,11 +236,11 @@ class TemperatureConverter(BaseUnitConverter): UNIT_CLASS = "temperature" NORMALIZED_UNIT = TEMP_CELSIUS - VALID_UNITS: tuple[str, ...] = ( + VALID_UNITS = { TEMP_CELSIUS, TEMP_FAHRENHEIT, TEMP_KELVIN, - ) + } @classmethod def convert( @@ -317,11 +317,11 @@ class VolumeConverter(BaseUnitConverterWithUnitConversion): VOLUME_CUBIC_METERS: 1, VOLUME_CUBIC_FEET: 1 / _CUBIC_FOOT_TO_CUBIC_METER, } - VALID_UNITS: tuple[str, ...] = ( + VALID_UNITS = { VOLUME_LITERS, VOLUME_MILLILITERS, VOLUME_GALLONS, VOLUME_FLUID_OUNCE, VOLUME_CUBIC_METERS, VOLUME_CUBIC_FEET, - ) + } diff --git a/homeassistant/util/unit_system.py b/homeassistant/util/unit_system.py index 5d8334c6686..1492a883c36 100644 --- a/homeassistant/util/unit_system.py +++ b/homeassistant/util/unit_system.py @@ -42,7 +42,7 @@ from . import ( LENGTH_UNITS = distance_util.VALID_UNITS -MASS_UNITS: tuple[str, ...] = (MASS_POUNDS, MASS_OUNCES, MASS_KILOGRAMS, MASS_GRAMS) +MASS_UNITS: set[str] = {MASS_POUNDS, MASS_OUNCES, MASS_KILOGRAMS, MASS_GRAMS} PRESSURE_UNITS = pressure_util.VALID_UNITS @@ -50,7 +50,7 @@ VOLUME_UNITS = volume_util.VALID_UNITS WIND_SPEED_UNITS = speed_util.VALID_UNITS -TEMPERATURE_UNITS: tuple[str, ...] = (TEMP_FAHRENHEIT, TEMP_CELSIUS) +TEMPERATURE_UNITS: set[str] = {TEMP_FAHRENHEIT, TEMP_CELSIUS} def _is_valid_unit(unit: str, unit_type: str) -> bool: From fb32e745fcaaea451298de0d0ff8a0229b8f49f4 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Tue, 27 Sep 2022 08:24:58 +0200 Subject: [PATCH 760/955] Listen to out of band coil updates in Nibe Heat Pumps (#78976) Listen to callbacks --- .../components/nibe_heatpump/__init__.py | 92 +++++++++++++++---- 1 file changed, 75 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/nibe_heatpump/__init__.py b/homeassistant/components/nibe_heatpump/__init__.py index 590afba5b79..b9921df4e1e 100644 --- a/homeassistant/components/nibe_heatpump/__init__.py +++ b/homeassistant/components/nibe_heatpump/__init__.py @@ -2,7 +2,10 @@ from __future__ import annotations from collections import defaultdict +from collections.abc import Callable, Iterable from datetime import timedelta +from functools import cached_property +from typing import Any, Generic, TypeVar from nibe.coil import Coil from nibe.connection import Connection @@ -18,7 +21,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, Platform, ) -from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr from homeassistant.helpers.entity import DeviceInfo, async_generate_entity_id @@ -105,7 +108,52 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -class Coordinator(DataUpdateCoordinator[dict[int, Coil]]): +_DataTypeT = TypeVar("_DataTypeT") +_ContextTypeT = TypeVar("_ContextTypeT") + + +class ContextCoordinator( + Generic[_DataTypeT, _ContextTypeT], DataUpdateCoordinator[_DataTypeT] +): + """Update coordinator with context adjustments.""" + + @cached_property + def context_callbacks(self) -> dict[_ContextTypeT, list[CALLBACK_TYPE]]: + """Return a dict of all callbacks registered for a given context.""" + callbacks: dict[_ContextTypeT, list[CALLBACK_TYPE]] = defaultdict(list) + for update_callback, context in list(self._listeners.values()): + assert isinstance(context, set) + for address in context: + callbacks[address].append(update_callback) + return callbacks + + @callback + def async_update_context_listeners(self, contexts: Iterable[_ContextTypeT]) -> None: + """Update all listeners given a set of contexts.""" + update_callbacks: set[CALLBACK_TYPE] = set() + for context in contexts: + update_callbacks.update(self.context_callbacks.get(context, [])) + + for update_callback in update_callbacks: + update_callback() + + @callback + def async_add_listener( + self, update_callback: CALLBACK_TYPE, context: Any = None + ) -> Callable[[], None]: + """Wrap standard function to prune cached callback database.""" + release = super().async_add_listener(update_callback, context) + self.__dict__.pop("context_callbacks", None) + + @callback + def release_update(): + release() + self.__dict__.pop("context_callbacks", None) + + return release_update + + +class Coordinator(ContextCoordinator[dict[int, Coil], int]): """Update coordinator for nibe heat pumps.""" config_entry: ConfigEntry @@ -122,9 +170,18 @@ class Coordinator(DataUpdateCoordinator[dict[int, Coil]]): ) self.data = {} + self.seed: dict[int, Coil] = {} self.connection = connection self.heatpump = heatpump + heatpump.subscribe(heatpump.COIL_UPDATE_EVENT, self._on_coil_update) + + def _on_coil_update(self, coil: Coil): + """Handle callback on coil updates.""" + self.data[coil.address] = coil + self.seed[coil.address] = coil + self.async_update_context_listeners([coil.address]) + @property def coils(self) -> list[Coil]: """Return the full coil database.""" @@ -157,9 +214,9 @@ class Coordinator(DataUpdateCoordinator[dict[int, Coil]]): coil.value = value coil = await self.connection.write_coil(coil) - if self.data: - self.data[coil.address] = coil - self.async_update_listeners() + self.data[coil.address] = coil + + self.async_update_context_listeners([coil.address]) async def _async_update_data(self) -> dict[int, Coil]: @retry( @@ -169,25 +226,26 @@ class Coordinator(DataUpdateCoordinator[dict[int, Coil]]): async def read_coil(coil: Coil): return await self.connection.read_coil(coil) - callbacks: dict[int, list[CALLBACK_TYPE]] = defaultdict(list) - for update_callback, context in list(self._listeners.values()): - assert isinstance(context, set) - for address in context: - callbacks[address].append(update_callback) - result: dict[int, Coil] = {} - for address, callback_list in callbacks.items(): + 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 + try: coil = self.heatpump.get_coil_by_address(address) - self.data[coil.address] = result[coil.address] = await read_coil(coil) - except (CoilReadException, RetryError) as exception: - raise UpdateFailed(f"Failed to update: {exception}") from exception except CoilNotFoundException as exception: self.logger.debug("Skipping missing coil: %s", exception) + continue - for update_callback in callback_list: - update_callback() + 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) return result From 7e9be812cac06340835433d8b3171f779f920b3a Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 27 Sep 2022 08:25:56 +0200 Subject: [PATCH 761/955] Add unique id to entity reg list response (#78945) * Add unique id to entity reg list response * Update test_entity_registry.py --- homeassistant/components/config/entity_registry.py | 2 +- tests/components/config/test_entity_registry.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/config/entity_registry.py b/homeassistant/components/config/entity_registry.py index ea75ac4d043..e006cd8062e 100644 --- a/homeassistant/components/config/entity_registry.py +++ b/homeassistant/components/config/entity_registry.py @@ -238,6 +238,7 @@ def _entry_dict(entry: er.RegistryEntry) -> dict[str, Any]: "hidden_by": entry.hidden_by, "icon": entry.icon, "id": entry.id, + "unique_id": entry.unique_id, "name": entry.name, "original_name": entry.original_name, "platform": entry.platform, @@ -253,5 +254,4 @@ def _entry_ext_dict(entry: er.RegistryEntry) -> dict[str, Any]: data["options"] = entry.options data["original_device_class"] = entry.original_device_class data["original_icon"] = entry.original_icon - data["unique_id"] = entry.unique_id return data diff --git a/tests/components/config/test_entity_registry.py b/tests/components/config/test_entity_registry.py index 30153195eec..2f4cd980d8e 100644 --- a/tests/components/config/test_entity_registry.py +++ b/tests/components/config/test_entity_registry.py @@ -70,6 +70,7 @@ async def test_list_entities(hass, client): "hidden_by": None, "icon": None, "id": ANY, + "unique_id": ANY, "name": "Hello World", "original_name": None, "platform": "test_platform", @@ -85,6 +86,7 @@ async def test_list_entities(hass, client): "hidden_by": None, "icon": None, "id": ANY, + "unique_id": ANY, "name": None, "original_name": None, "platform": "test_platform", @@ -122,6 +124,7 @@ async def test_list_entities(hass, client): "hidden_by": None, "icon": None, "id": ANY, + "unique_id": ANY, "name": "Hello World", "original_name": None, "platform": "test_platform", From df47fda2a04bdf5c093881e1e947717b2a261b41 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 27 Sep 2022 07:27:26 +0100 Subject: [PATCH 762/955] Remove parametrization in recorder websocket api tests (#78864) Remove parametrization in websocket api tests --- .../components/recorder/test_websocket_api.py | 56 +++++-------------- 1 file changed, 15 insertions(+), 41 deletions(-) diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index 008894250be..78bdc34d1cc 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -82,29 +82,14 @@ TEMPERATURE_SENSOR_F_ATTRIBUTES = { } -@pytest.mark.parametrize( - "units, attributes, state, value", - [ - (IMPERIAL_SYSTEM, POWER_SENSOR_KW_ATTRIBUTES, 10, 10), - (METRIC_SYSTEM, POWER_SENSOR_KW_ATTRIBUTES, 10, 10), - (IMPERIAL_SYSTEM, TEMPERATURE_SENSOR_C_ATTRIBUTES, 10, 10), - (METRIC_SYSTEM, TEMPERATURE_SENSOR_C_ATTRIBUTES, 10, 10), - (IMPERIAL_SYSTEM, TEMPERATURE_SENSOR_F_ATTRIBUTES, 10, 10), - (METRIC_SYSTEM, TEMPERATURE_SENSOR_F_ATTRIBUTES, 10, 10), - (IMPERIAL_SYSTEM, PRESSURE_SENSOR_HPA_ATTRIBUTES, 1000, 1000), - (METRIC_SYSTEM, PRESSURE_SENSOR_HPA_ATTRIBUTES, 1000, 1000), - ], -) -async def test_statistics_during_period( - hass, hass_ws_client, recorder_mock, units, attributes, state, value -): +async def test_statistics_during_period(hass, hass_ws_client, recorder_mock): """Test statistics_during_period.""" now = dt_util.utcnow() - hass.config.units = units + hass.config.units = IMPERIAL_SYSTEM await async_setup_component(hass, "sensor", {}) await async_recorder_block_till_done(hass) - hass.states.async_set("sensor.test", state, attributes=attributes) + hass.states.async_set("sensor.test", 10, attributes=POWER_SENSOR_KW_ATTRIBUTES) await async_wait_recording_done(hass) do_adhoc_statistics(hass, start=now) @@ -142,9 +127,9 @@ async def test_statistics_during_period( "statistic_id": "sensor.test", "start": now.isoformat(), "end": (now + timedelta(minutes=5)).isoformat(), - "mean": approx(value), - "min": approx(value), - "max": approx(value), + "mean": approx(10), + "min": approx(10), + "max": approx(10), "last_reset": None, "state": None, "sum": None, @@ -388,32 +373,21 @@ async def test_statistics_during_period_invalid_unit_conversion( assert response["error"]["code"] == "invalid_format" -@pytest.mark.parametrize( - "units, attributes, state, value", - [ - (IMPERIAL_SYSTEM, POWER_SENSOR_KW_ATTRIBUTES, 10, 10), - (METRIC_SYSTEM, POWER_SENSOR_KW_ATTRIBUTES, 10, 10), - (IMPERIAL_SYSTEM, TEMPERATURE_SENSOR_C_ATTRIBUTES, 10, 10), - (METRIC_SYSTEM, TEMPERATURE_SENSOR_C_ATTRIBUTES, 10, 10), - (IMPERIAL_SYSTEM, PRESSURE_SENSOR_HPA_ATTRIBUTES, 1000, 1000), - (METRIC_SYSTEM, PRESSURE_SENSOR_HPA_ATTRIBUTES, 1000, 1000), - ], -) async def test_statistics_during_period_in_the_past( - hass, hass_ws_client, recorder_mock, units, attributes, state, value + hass, hass_ws_client, recorder_mock ): """Test statistics_during_period in the past.""" hass.config.set_time_zone("UTC") now = dt_util.utcnow().replace() - hass.config.units = units + hass.config.units = IMPERIAL_SYSTEM await async_setup_component(hass, "sensor", {}) await async_recorder_block_till_done(hass) past = now - timedelta(days=3) with freeze_time(past): - hass.states.async_set("sensor.test", state, attributes=attributes) + hass.states.async_set("sensor.test", 10, attributes=POWER_SENSOR_KW_ATTRIBUTES) await async_wait_recording_done(hass) sensor_state = hass.states.get("sensor.test") @@ -470,9 +444,9 @@ async def test_statistics_during_period_in_the_past( "statistic_id": "sensor.test", "start": stats_start.isoformat(), "end": (stats_start + timedelta(minutes=5)).isoformat(), - "mean": approx(value), - "min": approx(value), - "max": approx(value), + "mean": approx(10), + "min": approx(10), + "max": approx(10), "last_reset": None, "state": None, "sum": None, @@ -498,9 +472,9 @@ async def test_statistics_during_period_in_the_past( "statistic_id": "sensor.test", "start": start_of_day.isoformat(), "end": (start_of_day + timedelta(days=1)).isoformat(), - "mean": approx(value), - "min": approx(value), - "max": approx(value), + "mean": approx(10), + "min": approx(10), + "max": approx(10), "last_reset": None, "state": None, "sum": None, From 772581dd283684bf122e8bc2862dd869e853af49 Mon Sep 17 00:00:00 2001 From: Kenneth Henderick Date: Tue, 27 Sep 2022 08:31:41 +0200 Subject: [PATCH 763/955] Microsoft TTS: Add support for gender and type (#78848) * Add support for gender and type * Reformat code --- homeassistant/components/microsoft/tts.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/microsoft/tts.py b/homeassistant/components/microsoft/tts.py index 840b35c2f85..7deb8f27c68 100644 --- a/homeassistant/components/microsoft/tts.py +++ b/homeassistant/components/microsoft/tts.py @@ -166,6 +166,16 @@ class MicrosoftProvider(Provider): """Return list of supported languages.""" return SUPPORTED_LANGUAGES + @property + def supported_options(self): + """Return list of supported options like voice, emotion.""" + return [CONF_GENDER, CONF_TYPE] + + @property + def default_options(self): + """Return a dict include default options.""" + return {CONF_GENDER: self._gender, CONF_TYPE: self._type} + def get_tts_audio(self, message, language, options=None): """Load TTS from Microsoft.""" if language is None: @@ -175,8 +185,8 @@ class MicrosoftProvider(Provider): trans = pycsspeechtts.TTSTranslator(self._apikey, self._region) data = trans.speak( language=language, - gender=self._gender, - voiceType=self._type, + gender=options[CONF_GENDER], + voiceType=options[CONF_TYPE], output=self._output, rate=self._rate, volume=self._volume, From c52d0f74957c8800f4ad23b63bbb06f58fedd0f3 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 27 Sep 2022 08:44:58 +0200 Subject: [PATCH 764/955] Support converting statistics to another unit (#79117) --- homeassistant/components/recorder/core.py | 16 + .../components/recorder/statistics.py | 113 ++++++- homeassistant/components/recorder/tasks.py | 18 ++ .../components/recorder/websocket_api.py | 28 ++ .../components/recorder/test_websocket_api.py | 279 +++++++++++++++++- 5 files changed, 448 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 8277828abbc..17828a2e87e 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -72,6 +72,7 @@ from .queries import find_shared_attributes_id, find_shared_data_id from .run_history import RunHistory from .tasks import ( AdjustStatisticsTask, + ChangeStatisticsUnitTask, ClearStatisticsTask, CommitTask, DatabaseLockTask, @@ -511,6 +512,21 @@ class Recorder(threading.Thread): ) ) + @callback + def async_change_statistics_unit( + self, + statistic_id: str, + *, + new_unit_of_measurement: str, + old_unit_of_measurement: str, + ) -> None: + """Change statistics unit for a statistic_id.""" + self.queue_task( + ChangeStatisticsUnitTask( + statistic_id, new_unit_of_measurement, old_unit_of_measurement + ) + ) + @callback def async_import_statistics( self, metadata: StatisticMetaData, stats: Iterable[StatisticData] diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 3be5aa70a74..7f44243c029 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -41,7 +41,13 @@ from homeassistant.util.unit_conversion import ( ) from .const import DOMAIN, MAX_ROWS_TO_PURGE, SupportedDialect -from .db_schema import Statistics, StatisticsMeta, StatisticsRuns, StatisticsShortTerm +from .db_schema import ( + Statistics, + StatisticsBase, + StatisticsMeta, + StatisticsRuns, + StatisticsShortTerm, +) from .models import ( StatisticData, StatisticMetaData, @@ -208,6 +214,35 @@ def _get_display_to_statistic_unit_converter( ) +def _get_unit_converter( + from_unit: str, to_unit: str +) -> Callable[[float | None], float | None]: + """Prepare a converter from a unit to another unit.""" + + def convert_units( + val: float | None, conv: type[BaseUnitConverter], from_unit: str, to_unit: str + ) -> float | None: + """Return converted val.""" + if val is None: + return val + return conv.convert(val, from_unit=from_unit, to_unit=to_unit) + + for conv in STATISTIC_UNIT_TO_UNIT_CONVERTER.values(): + if from_unit in conv.VALID_UNITS and to_unit in conv.VALID_UNITS: + return partial( + convert_units, conv=conv, from_unit=from_unit, to_unit=to_unit + ) + raise HomeAssistantError + + +def can_convert_units(from_unit: str | None, to_unit: str | None) -> bool: + """Return True if it's possible to convert from from_unit to to_unit.""" + for converter in STATISTIC_UNIT_TO_UNIT_CONVERTER.values(): + if from_unit in converter.VALID_UNITS and to_unit in converter.VALID_UNITS: + return True + return False + + @dataclasses.dataclass class PlatformCompiledStatistics: """Compiled Statistics from a platform.""" @@ -1614,3 +1649,79 @@ def adjust_statistics( ) return True + + +def _change_statistics_unit_for_table( + session: Session, + table: type[StatisticsBase], + metadata_id: int, + convert: Callable[[float | None], float | None], +) -> None: + """Insert statistics in the database.""" + columns = [table.id, table.mean, table.min, table.max, table.state, table.sum] + query = session.query(*columns).filter_by(metadata_id=bindparam("metadata_id")) + rows = execute(query.params(metadata_id=metadata_id)) + for row in rows: + session.query(table).filter(table.id == row.id).update( + { + table.mean: convert(row.mean), + table.min: convert(row.min), + table.max: convert(row.max), + table.state: convert(row.state), + table.sum: convert(row.sum), + }, + synchronize_session=False, + ) + + +def change_statistics_unit( + instance: Recorder, + statistic_id: str, + new_unit: str, + old_unit: str, +) -> None: + """Change statistics unit for a statistic_id.""" + with session_scope(session=instance.get_session()) as session: + metadata = get_metadata_with_session( + instance.hass, session, statistic_ids=(statistic_id,) + ).get(statistic_id) + + # Guard against the statistics being removed or updated before the + # change_statistics_unit job executes + if ( + metadata is None + or metadata[1]["source"] != DOMAIN + or metadata[1]["unit_of_measurement"] != old_unit + ): + _LOGGER.warning("Could not change statistics unit for %s", statistic_id) + return + + metadata_id = metadata[0] + + convert = _get_unit_converter(old_unit, new_unit) + for table in (StatisticsShortTerm, Statistics): + _change_statistics_unit_for_table(session, table, metadata_id, convert) + session.query(StatisticsMeta).filter( + StatisticsMeta.statistic_id == statistic_id + ).update({StatisticsMeta.unit_of_measurement: new_unit}) + + +@callback +def async_change_statistics_unit( + hass: HomeAssistant, + statistic_id: str, + *, + new_unit_of_measurement: str, + old_unit_of_measurement: str, +) -> None: + """Change statistics unit for a statistic_id.""" + if not can_convert_units(old_unit_of_measurement, new_unit_of_measurement): + raise HomeAssistantError( + f"Can't convert {old_unit_of_measurement} to {new_unit_of_measurement}" + ) + + get_instance(hass).async_change_statistics_unit( + statistic_id, + new_unit_of_measurement=new_unit_of_measurement, + old_unit_of_measurement=old_unit_of_measurement, + ) diff --git a/homeassistant/components/recorder/tasks.py b/homeassistant/components/recorder/tasks.py index 1ab84a1ce5a..63fb14cc598 100644 --- a/homeassistant/components/recorder/tasks.py +++ b/homeassistant/components/recorder/tasks.py @@ -31,6 +31,24 @@ class RecorderTask(abc.ABC): """Handle the task.""" +@dataclass +class ChangeStatisticsUnitTask(RecorderTask): + """Object to store statistics_id and unit to convert unit of statistics.""" + + statistic_id: str + new_unit_of_measurement: str + old_unit_of_measurement: str + + def run(self, instance: Recorder) -> None: + """Handle the task.""" + statistics.change_statistics_unit( + instance, + self.statistic_id, + self.new_unit_of_measurement, + self.old_unit_of_measurement, + ) + + @dataclass class ClearStatisticsTask(RecorderTask): """Object to store statistics_ids which for which to remove statistics.""" diff --git a/homeassistant/components/recorder/websocket_api.py b/homeassistant/components/recorder/websocket_api.py index 69c96a86c10..a4bb1da59e8 100644 --- a/homeassistant/components/recorder/websocket_api.py +++ b/homeassistant/components/recorder/websocket_api.py @@ -30,6 +30,7 @@ from homeassistant.util.unit_conversion import ( from .const import MAX_QUEUE_BACKLOG from .statistics import ( async_add_external_statistics, + async_change_statistics_unit, async_import_statistics, list_statistic_ids, statistics_during_period, @@ -46,6 +47,7 @@ def async_setup(hass: HomeAssistant) -> None: websocket_api.async_register_command(hass, ws_adjust_sum_statistics) websocket_api.async_register_command(hass, ws_backup_end) 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_statistics_during_period) websocket_api.async_register_command(hass, ws_get_statistics_metadata) @@ -255,6 +257,32 @@ def ws_update_statistics_metadata( connection.send_result(msg["id"]) +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required("type"): "recorder/change_statistics_unit", + vol.Required("statistic_id"): str, + vol.Required("new_unit_of_measurement"): vol.Any(str, None), + vol.Required("old_unit_of_measurement"): vol.Any(str, None), + } +) +@callback +def ws_change_statistics_unit( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict +) -> None: + """Change the unit_of_measurement for a statistic_id. + + All existing statistics will be converted to the new unit. + """ + async_change_statistics_unit( + hass, + msg["statistic_id"], + new_unit_of_measurement=msg["new_unit_of_measurement"], + old_unit_of_measurement=msg["old_unit_of_measurement"], + ) + connection.send_result(msg["id"]) + + @websocket_api.require_admin @websocket_api.websocket_command( { diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index 78bdc34d1cc..39a50010f1b 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -812,15 +812,17 @@ async def test_clear_statistics(hass, hass_ws_client, recorder_mock): assert response["result"] == {"sensor.test2": expected_response["sensor.test2"]} -@pytest.mark.parametrize("new_unit", ["dogs", None]) +@pytest.mark.parametrize( + "new_unit, new_unit_class", [("dogs", None), (None, None), ("W", "power")] +) async def test_update_statistics_metadata( - hass, hass_ws_client, recorder_mock, new_unit + hass, hass_ws_client, recorder_mock, new_unit, new_unit_class ): """Test removing statistics.""" now = dt_util.utcnow() units = METRIC_SYSTEM - attributes = POWER_SENSOR_KW_ATTRIBUTES + attributes = POWER_SENSOR_KW_ATTRIBUTES | {"device_class": None} state = 10 hass.config.units = units @@ -845,8 +847,8 @@ async def test_update_statistics_metadata( "has_sum": False, "name": None, "source": "recorder", - "statistics_unit_of_measurement": "W", - "unit_class": "power", + "statistics_unit_of_measurement": "kW", + "unit_class": None, } ] @@ -874,10 +876,277 @@ async def test_update_statistics_metadata( "name": None, "source": "recorder", "statistics_unit_of_measurement": new_unit, + "unit_class": new_unit_class, + } + ] + + await client.send_json( + { + "id": 5, + "type": "recorder/statistics_during_period", + "start_time": now.isoformat(), + "statistic_ids": ["sensor.test"], + "period": "5minute", + "units": {"power": "W"}, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + "sensor.test": [ + { + "end": (now + timedelta(minutes=5)).isoformat(), + "last_reset": None, + "max": 10.0, + "mean": 10.0, + "min": 10.0, + "start": now.isoformat(), + "state": None, + "statistic_id": "sensor.test", + "sum": None, + } + ], + } + + +async def test_change_statistics_unit(hass, hass_ws_client, recorder_mock): + """Test change unit of recorded statistics.""" + now = dt_util.utcnow() + + units = METRIC_SYSTEM + attributes = POWER_SENSOR_KW_ATTRIBUTES | {"device_class": None} + state = 10 + + hass.config.units = units + await async_setup_component(hass, "sensor", {}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.test", state, attributes=attributes) + await async_wait_recording_done(hass) + + do_adhoc_statistics(hass, period="hourly", start=now) + await async_recorder_block_till_done(hass) + + client = await hass_ws_client() + + await client.send_json({"id": 1, "type": "recorder/list_statistic_ids"}) + response = await client.receive_json() + assert response["success"] + assert response["result"] == [ + { + "statistic_id": "sensor.test", + "display_unit_of_measurement": "kW", + "has_mean": True, + "has_sum": False, + "name": None, + "source": "recorder", + "statistics_unit_of_measurement": "kW", "unit_class": None, } ] + await client.send_json( + { + "id": 2, + "type": "recorder/statistics_during_period", + "start_time": now.isoformat(), + "statistic_ids": ["sensor.test"], + "period": "5minute", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + "sensor.test": [ + { + "end": (now + timedelta(minutes=5)).isoformat(), + "last_reset": None, + "max": 10.0, + "mean": 10.0, + "min": 10.0, + "start": now.isoformat(), + "state": None, + "statistic_id": "sensor.test", + "sum": None, + } + ], + } + + await client.send_json( + { + "id": 3, + "type": "recorder/change_statistics_unit", + "statistic_id": "sensor.test", + "new_unit_of_measurement": "W", + "old_unit_of_measurement": "kW", + } + ) + response = await client.receive_json() + assert response["success"] + await async_recorder_block_till_done(hass) + + await client.send_json({"id": 4, "type": "recorder/list_statistic_ids"}) + response = await client.receive_json() + assert response["success"] + assert response["result"] == [ + { + "statistic_id": "sensor.test", + "display_unit_of_measurement": "kW", + "has_mean": True, + "has_sum": False, + "name": None, + "source": "recorder", + "statistics_unit_of_measurement": "W", + "unit_class": "power", + } + ] + + await client.send_json( + { + "id": 5, + "type": "recorder/statistics_during_period", + "start_time": now.isoformat(), + "statistic_ids": ["sensor.test"], + "period": "5minute", + "units": {"power": "W"}, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + "sensor.test": [ + { + "end": (now + timedelta(minutes=5)).isoformat(), + "last_reset": None, + "max": 10000.0, + "mean": 10000.0, + "min": 10000.0, + "start": now.isoformat(), + "state": None, + "statistic_id": "sensor.test", + "sum": None, + } + ], + } + + +async def test_change_statistics_unit_errors( + hass, hass_ws_client, recorder_mock, caplog +): + """Test change unit of recorded statistics.""" + now = dt_util.utcnow() + ws_id = 0 + + units = METRIC_SYSTEM + attributes = POWER_SENSOR_KW_ATTRIBUTES | {"device_class": None} + state = 10 + + expected_statistic_ids = [ + { + "statistic_id": "sensor.test", + "display_unit_of_measurement": "kW", + "has_mean": True, + "has_sum": False, + "name": None, + "source": "recorder", + "statistics_unit_of_measurement": "kW", + "unit_class": None, + } + ] + + expected_statistics = { + "sensor.test": [ + { + "end": (now + timedelta(minutes=5)).isoformat(), + "last_reset": None, + "max": 10.0, + "mean": 10.0, + "min": 10.0, + "start": now.isoformat(), + "state": None, + "statistic_id": "sensor.test", + "sum": None, + } + ], + } + + async def assert_statistic_ids(expected): + nonlocal ws_id + ws_id += 1 + await client.send_json({"id": ws_id, "type": "recorder/list_statistic_ids"}) + response = await client.receive_json() + assert response["success"] + assert response["result"] == expected + + async def assert_statistics(expected): + nonlocal ws_id + ws_id += 1 + await client.send_json( + { + "id": ws_id, + "type": "recorder/statistics_during_period", + "start_time": now.isoformat(), + "statistic_ids": ["sensor.test"], + "period": "5minute", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == expected + + hass.config.units = units + await async_setup_component(hass, "sensor", {}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.test", state, attributes=attributes) + await async_wait_recording_done(hass) + + do_adhoc_statistics(hass, period="hourly", start=now) + await async_recorder_block_till_done(hass) + + client = await hass_ws_client() + + await assert_statistic_ids(expected_statistic_ids) + await assert_statistics(expected_statistics) + + # Try changing to an invalid unit + ws_id += 1 + await client.send_json( + { + "id": ws_id, + "type": "recorder/change_statistics_unit", + "statistic_id": "sensor.test", + "old_unit_of_measurement": "kW", + "new_unit_of_measurement": "dogs", + } + ) + response = await client.receive_json() + assert not response["success"] + assert response["error"]["message"] == "Can't convert kW to dogs" + + await async_recorder_block_till_done(hass) + + await assert_statistic_ids(expected_statistic_ids) + await assert_statistics(expected_statistics) + + # Try changing from the wrong unit + ws_id += 1 + await client.send_json( + { + "id": ws_id, + "type": "recorder/change_statistics_unit", + "statistic_id": "sensor.test", + "old_unit_of_measurement": "W", + "new_unit_of_measurement": "kW", + } + ) + response = await client.receive_json() + assert response["success"] + + await async_recorder_block_till_done(hass) + + assert "Could not change statistics unit for sensor.test" in caplog.text + await assert_statistic_ids(expected_statistic_ids) + await assert_statistics(expected_statistics) + async def test_recorder_info(hass, hass_ws_client, recorder_mock): """Test getting recorder status.""" From cba3b6ad944408b9ffd906f4da5e5f5fd615b174 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 27 Sep 2022 09:08:54 +0200 Subject: [PATCH 765/955] Add serial_number to device registry entries (#77713) --- .../components/config/device_registry.py | 1 + homeassistant/helpers/device_registry.py | 13 +- .../components/config/test_device_registry.py | 3 + tests/helpers/test_device_registry.py | 136 +++++++++++++++++- 4 files changed, 148 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/config/device_registry.py b/homeassistant/components/config/device_registry.py index 8edd9f1f4d3..118b898ec4c 100644 --- a/homeassistant/components/config/device_registry.py +++ b/homeassistant/components/config/device_registry.py @@ -167,6 +167,7 @@ def _entry_dict(entry): "model": entry.model, "name_by_user": entry.name_by_user, "name": entry.name, + "serial_number": entry.serial_number, "sw_version": entry.sw_version, "via_device_id": entry.via_device_id, } diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 908db74d40d..16ff67bed19 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -32,7 +32,7 @@ DATA_REGISTRY = "device_registry" EVENT_DEVICE_REGISTRY_UPDATED = "device_registry_updated" STORAGE_KEY = "core.device_registry" STORAGE_VERSION_MAJOR = 1 -STORAGE_VERSION_MINOR = 3 +STORAGE_VERSION_MINOR = 4 SAVE_DELAY = 10 CLEANUP_DELAY = 10 @@ -83,6 +83,7 @@ class DeviceEntry: model: str | None = attr.ib(default=None) name_by_user: str | None = attr.ib(default=None) name: str | None = attr.ib(default=None) + serial_number: str | None = attr.ib(default=None) suggested_area: str | None = attr.ib(default=None) sw_version: str | None = attr.ib(default=None) via_device_id: str | None = attr.ib(default=None) @@ -180,6 +181,10 @@ class DeviceRegistryStore(storage.Store[dict[str, list[dict[str, Any]]]]): # Introduced in 2022.2 for device in old_data["devices"]: device["hw_version"] = device.get("hw_version") + if old_minor_version < 4: + # Introduced in 2022.10 + for device in old_data["devices"]: + device["serial_number"] = device.get("serial_number") if old_major_version > 1: raise NotImplementedError @@ -301,6 +306,7 @@ class DeviceRegistry: manufacturer: str | None | UndefinedType = UNDEFINED, model: str | None | UndefinedType = UNDEFINED, name: str | None | UndefinedType = UNDEFINED, + serial_number: str | None | UndefinedType = UNDEFINED, suggested_area: str | None | UndefinedType = UNDEFINED, sw_version: str | None | UndefinedType = UNDEFINED, via_device: tuple[str, str] | None = None, @@ -366,6 +372,7 @@ class DeviceRegistry: merge_identifiers=identifiers or UNDEFINED, model=model, name=name, + serial_number=serial_number, suggested_area=suggested_area, sw_version=sw_version, via_device_id=via_device_id, @@ -395,6 +402,7 @@ class DeviceRegistry: name: str | None | UndefinedType = UNDEFINED, new_identifiers: set[tuple[str, str]] | UndefinedType = UNDEFINED, remove_config_entry_id: str | UndefinedType = UNDEFINED, + serial_number: str | None | UndefinedType = UNDEFINED, suggested_area: str | None | UndefinedType = UNDEFINED, sw_version: str | None | UndefinedType = UNDEFINED, via_device_id: str | None | UndefinedType = UNDEFINED, @@ -479,6 +487,7 @@ class DeviceRegistry: ("model", model), ("name", name), ("name_by_user", name_by_user), + ("serial_number", serial_number), ("suggested_area", suggested_area), ("sw_version", sw_version), ("via_device_id", via_device_id), @@ -566,6 +575,7 @@ class DeviceRegistry: model=device["model"], name_by_user=device["name_by_user"], name=device["name"], + serial_number=device["serial_number"], sw_version=device["sw_version"], via_device_id=device["via_device_id"], ) @@ -608,6 +618,7 @@ class DeviceRegistry: "model": entry.model, "name_by_user": entry.name_by_user, "name": entry.name, + "serial_number": entry.serial_number, "sw_version": entry.sw_version, "via_device_id": entry.via_device_id, } diff --git a/tests/components/config/test_device_registry.py b/tests/components/config/test_device_registry.py index 4f47e463751..0903c81a424 100644 --- a/tests/components/config/test_device_registry.py +++ b/tests/components/config/test_device_registry.py @@ -64,6 +64,7 @@ async def test_list_devices(hass, client, registry): "model": "model", "name_by_user": None, "name": None, + "serial_number": None, "sw_version": None, "via_device_id": None, }, @@ -80,6 +81,7 @@ async def test_list_devices(hass, client, registry): "model": "model", "name_by_user": None, "name": None, + "serial_number": None, "sw_version": None, "via_device_id": dev1, }, @@ -106,6 +108,7 @@ async def test_list_devices(hass, client, registry): "model": "model", "name_by_user": None, "name": None, + "serial_number": None, "sw_version": None, "via_device_id": None, } diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index 2c9a7956874..fe702b9c294 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -189,6 +189,7 @@ async def test_loading_from_storage(hass, hass_storage): "model": "model", "name_by_user": "Test Friendly Name", "name": "name", + "serial_number": "serial_no", "sw_version": "version", "via_device_id": None, } @@ -231,6 +232,7 @@ async def test_loading_from_storage(hass, hass_storage): model="model", name_by_user="Test Friendly Name", name="name", + serial_number="serial_no", suggested_area=None, # Not stored sw_version="version", ) @@ -261,8 +263,8 @@ async def test_loading_from_storage(hass, hass_storage): @pytest.mark.parametrize("load_registries", [False]) -async def test_migration_1_1_to_1_3(hass, hass_storage): - """Test migration from version 1.1 to 1.3.""" +async def test_migration_1_1_to_1_4(hass, hass_storage): + """Test migration from version 1.1 to 1.4.""" hass_storage[device_registry.STORAGE_KEY] = { "version": 1, "minor_version": 1, @@ -350,6 +352,7 @@ async def test_migration_1_1_to_1_3(hass, hass_storage): "model": "model", "name": "name", "name_by_user": None, + "serial_number": None, "sw_version": "new_version", "via_device_id": None, }, @@ -367,6 +370,7 @@ async def test_migration_1_1_to_1_3(hass, hass_storage): "model": None, "name_by_user": None, "name": None, + "serial_number": None, "sw_version": None, "via_device_id": None, }, @@ -385,8 +389,8 @@ async def test_migration_1_1_to_1_3(hass, hass_storage): @pytest.mark.parametrize("load_registries", [False]) -async def test_migration_1_2_to_1_3(hass, hass_storage): - """Test migration from version 1.2 to 1.3.""" +async def test_migration_1_2_to_1_4(hass, hass_storage): + """Test migration from version 1.2 to 1.4.""" hass_storage[device_registry.STORAGE_KEY] = { "version": 1, "minor_version": 2, @@ -473,6 +477,7 @@ async def test_migration_1_2_to_1_3(hass, hass_storage): "model": "model", "name": "name", "name_by_user": None, + "serial_number": None, "sw_version": "new_version", "via_device_id": None, }, @@ -490,6 +495,126 @@ async def test_migration_1_2_to_1_3(hass, hass_storage): "model": None, "name_by_user": None, "name": None, + "serial_number": None, + "sw_version": None, + "via_device_id": None, + }, + ], + "deleted_devices": [], + }, + } + + +@pytest.mark.parametrize("load_registries", [False]) +async def test_migration_1_3_to_1_4(hass, hass_storage): + """Test migration from version 1.3 to 1.4.""" + hass_storage[device_registry.STORAGE_KEY] = { + "version": 1, + "minor_version": 3, + "key": device_registry.STORAGE_KEY, + "data": { + "devices": [ + { + "area_id": None, + "config_entries": ["1234"], + "configuration_url": None, + "connections": [["Zigbee", "01.23.45.67.89"]], + "disabled_by": None, + "entry_type": "service", + "hw_version": "hw_version", + "id": "abcdefghijklm", + "identifiers": [["serial", "12:34:56:AB:CD:EF"]], + "manufacturer": "manufacturer", + "model": "model", + "name": "name", + "name_by_user": None, + "sw_version": "version", + "via_device_id": None, + }, + { + "area_id": None, + "config_entries": [None], + "configuration_url": None, + "connections": [], + "disabled_by": None, + "entry_type": None, + "hw_version": None, + "id": "invalid-entry-type", + "identifiers": [["serial", "mock-id-invalid-entry"]], + "manufacturer": None, + "model": None, + "name_by_user": None, + "name": None, + "sw_version": None, + "via_device_id": None, + }, + ], + "deleted_devices": [], + }, + } + + await device_registry.async_load(hass) + registry = device_registry.async_get(hass) + + # Test data was loaded + entry = registry.async_get_or_create( + config_entry_id="1234", + connections={("Zigbee", "01.23.45.67.89")}, + identifiers={("serial", "12:34:56:AB:CD:EF")}, + ) + assert entry.id == "abcdefghijklm" + + # Update to trigger a store + entry = registry.async_get_or_create( + config_entry_id="1234", + connections={("Zigbee", "01.23.45.67.89")}, + identifiers={("serial", "12:34:56:AB:CD:EF")}, + sw_version="new_version", + ) + assert entry.id == "abcdefghijklm" + + # Check we store migrated data + await flush_store(registry._store) + + assert hass_storage[device_registry.STORAGE_KEY] == { + "version": device_registry.STORAGE_VERSION_MAJOR, + "minor_version": device_registry.STORAGE_VERSION_MINOR, + "key": device_registry.STORAGE_KEY, + "data": { + "devices": [ + { + "area_id": None, + "config_entries": ["1234"], + "configuration_url": None, + "connections": [["Zigbee", "01.23.45.67.89"]], + "disabled_by": None, + "entry_type": "service", + "hw_version": "hw_version", + "id": "abcdefghijklm", + "identifiers": [["serial", "12:34:56:AB:CD:EF"]], + "manufacturer": "manufacturer", + "model": "model", + "name": "name", + "name_by_user": None, + "serial_number": None, + "sw_version": "new_version", + "via_device_id": None, + }, + { + "area_id": None, + "config_entries": [None], + "configuration_url": None, + "connections": [], + "disabled_by": None, + "entry_type": None, + "hw_version": None, + "id": "invalid-entry-type", + "identifiers": [["serial", "mock-id-invalid-entry"]], + "manufacturer": None, + "model": None, + "name_by_user": None, + "name": None, + "serial_number": None, "sw_version": None, "via_device_id": None, }, @@ -918,6 +1043,7 @@ async def test_update(hass, registry, update_events): name_by_user="Test Friendly Name", name="name", new_identifiers=new_identifiers, + serial_number="serial_no", suggested_area="suggested_area", sw_version="version", via_device_id="98765B", @@ -939,6 +1065,7 @@ async def test_update(hass, registry, update_events): model="Test Model", name_by_user="Test Friendly Name", name="name", + serial_number="serial_no", suggested_area="suggested_area", sw_version="version", via_device_id="98765B", @@ -978,6 +1105,7 @@ async def test_update(hass, registry, update_events): "model": None, "name": None, "name_by_user": None, + "serial_number": None, "suggested_area": None, "sw_version": None, "via_device_id": None, From 2167cf540d656852bb33a19b349c966113fb58ed Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 27 Sep 2022 10:52:16 +0200 Subject: [PATCH 766/955] Drop some unused constants from recorder (#79138) --- homeassistant/components/recorder/statistics.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 7f44243c029..bf81d42d2fa 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -109,13 +109,6 @@ QUERY_STATISTICS_SUMMARY_SUM = [ .label("rownum"), ] -QUERY_STATISTICS_SUMMARY_SUM_LEGACY = [ - StatisticsShortTerm.metadata_id, - StatisticsShortTerm.last_reset, - StatisticsShortTerm.state, - StatisticsShortTerm.sum, -] - QUERY_STATISTIC_META = [ StatisticsMeta.id, StatisticsMeta.statistic_id, @@ -127,11 +120,6 @@ QUERY_STATISTIC_META = [ StatisticsMeta.name, ] -QUERY_STATISTIC_META_ID = [ - StatisticsMeta.id, - StatisticsMeta.statistic_id, -] - STATISTIC_UNIT_TO_UNIT_CLASS: dict[str | None, str] = { EnergyConverter.NORMALIZED_UNIT: EnergyConverter.UNIT_CLASS, From 5bcef1c7ce1ff07cb8bd233ce6991c59240bbcdc Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 27 Sep 2022 13:51:38 +0200 Subject: [PATCH 767/955] Indicate in statistics issues when units can be converted (#79121) --- homeassistant/components/sensor/recorder.py | 19 +- tests/components/sensor/test_recorder.py | 211 ++++++++++++++++++++ 2 files changed, 225 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index f45a99e3a17..cfa6d5a36fe 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -670,11 +670,16 @@ def validate_statistics( metadata_unit = metadata[1]["unit_of_measurement"] if device_class not in UNIT_CONVERTERS: + issue_type = ( + "units_changed_can_convert" + if statistics.can_convert_units(metadata_unit, state_unit) + else "units_changed" + ) if state_unit != metadata_unit: # The unit has changed validation_result[entity_id].append( statistics.ValidationIssue( - "units_changed", + issue_type, { "statistic_id": entity_id, "state_unit": state_unit, @@ -684,16 +689,20 @@ def validate_statistics( ) elif metadata_unit != UNIT_CONVERTERS[device_class].NORMALIZED_UNIT: # The unit in metadata is not supported for this device class + statistics_unit = UNIT_CONVERTERS[device_class].NORMALIZED_UNIT + issue_type = ( + "unsupported_unit_metadata_can_convert" + if statistics.can_convert_units(metadata_unit, statistics_unit) + else "unsupported_unit_metadata" + ) validation_result[entity_id].append( statistics.ValidationIssue( - "unsupported_unit_metadata", + issue_type, { "statistic_id": entity_id, "device_class": device_class, "metadata_unit": metadata_unit, - "supported_unit": UNIT_CONVERTERS[ - device_class - ].NORMALIZED_UNIT, + "supported_unit": statistics_unit, }, ) ) diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index f240c0f8af8..8b9671fbe9c 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -63,6 +63,10 @@ GAS_SENSOR_ATTRIBUTES = { "state_class": "total", "unit_of_measurement": "m³", } +KW_SENSOR_ATTRIBUTES = { + "state_class": "measurement", + "unit_of_measurement": "kW", +} @pytest.fixture(autouse=True) @@ -3119,6 +3123,96 @@ async def test_validate_statistics_supported_device_class_2( await assert_validation_result(client, expected) +@pytest.mark.parametrize( + "units, attributes, unit", + [ + (IMPERIAL_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W"), + ], +) +async def test_validate_statistics_supported_device_class_3( + hass, hass_ws_client, recorder_mock, units, attributes, unit +): + """Test validate_statistics.""" + id = 1 + + def next_id(): + nonlocal id + id += 1 + return id + + async def assert_validation_result(client, expected_result): + await client.send_json( + {"id": next_id(), "type": "recorder/validate_statistics"} + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == expected_result + + now = dt_util.utcnow() + + hass.config.units = units + await async_setup_component(hass, "sensor", {}) + await async_recorder_block_till_done(hass) + client = await hass_ws_client() + + # No statistics, no state - empty response + await assert_validation_result(client, {}) + + # No statistics, valid state - empty response + initial_attributes = {"state_class": "measurement", "unit_of_measurement": "kW"} + hass.states.async_set("sensor.test", 10, attributes=initial_attributes) + await hass.async_block_till_done() + await assert_validation_result(client, {}) + + # Statistics has run, device class set - expect error + do_adhoc_statistics(hass, start=now) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.test", 12, attributes=attributes) + await hass.async_block_till_done() + expected = { + "sensor.test": [ + { + "data": { + "device_class": attributes["device_class"], + "metadata_unit": "kW", + "statistic_id": "sensor.test", + "supported_unit": unit, + }, + "type": "unsupported_unit_metadata_can_convert", + } + ], + } + await assert_validation_result(client, expected) + + # Invalid state too, expect double errors + hass.states.async_set( + "sensor.test", 13, attributes={**attributes, **{"unit_of_measurement": "dogs"}} + ) + await async_recorder_block_till_done(hass) + expected = { + "sensor.test": [ + { + "data": { + "device_class": attributes["device_class"], + "metadata_unit": "kW", + "statistic_id": "sensor.test", + "supported_unit": unit, + }, + "type": "unsupported_unit_metadata_can_convert", + }, + { + "data": { + "device_class": attributes["device_class"], + "state_unit": "dogs", + "statistic_id": "sensor.test", + }, + "type": "unsupported_unit_state", + }, + ], + } + await assert_validation_result(client, expected) + + @pytest.mark.parametrize( "units, attributes, unit", [ @@ -3477,6 +3571,123 @@ async def test_validate_statistics_unsupported_device_class( await assert_validation_result(client, expected) +@pytest.mark.parametrize( + "attributes", + [KW_SENSOR_ATTRIBUTES], +) +async def test_validate_statistics_unsupported_device_class_2( + hass, recorder_mock, hass_ws_client, attributes +): + """Test validate_statistics.""" + id = 1 + + def next_id(): + nonlocal id + id += 1 + return id + + async def assert_validation_result(client, expected_result): + await client.send_json( + {"id": next_id(), "type": "recorder/validate_statistics"} + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == expected_result + + async def assert_statistic_ids(expected_result): + with session_scope(hass=hass) as session: + db_states = list(session.query(StatisticsMeta)) + assert len(db_states) == len(expected_result) + for i in range(len(db_states)): + assert db_states[i].statistic_id == expected_result[i]["statistic_id"] + assert ( + db_states[i].unit_of_measurement + == expected_result[i]["unit_of_measurement"] + ) + + now = dt_util.utcnow() + + await async_setup_component(hass, "sensor", {}) + await async_recorder_block_till_done(hass) + client = await hass_ws_client() + + # No statistics, no state - empty response + await assert_validation_result(client, {}) + + # No statistics, original unit - empty response + hass.states.async_set("sensor.test", 10, attributes=attributes) + await assert_validation_result(client, {}) + + # No statistics, changed unit - empty response + hass.states.async_set( + "sensor.test", 11, attributes={**attributes, **{"unit_of_measurement": "W"}} + ) + await assert_validation_result(client, {}) + + # Run statistics, no statistics will be generated because of conflicting units + await async_recorder_block_till_done(hass) + do_adhoc_statistics(hass, start=now) + await async_recorder_block_till_done(hass) + await assert_statistic_ids([]) + + # No statistics, changed unit - empty response + hass.states.async_set( + "sensor.test", 12, attributes={**attributes, **{"unit_of_measurement": "W"}} + ) + await assert_validation_result(client, {}) + + # Run statistics one hour later, only the "W" state will be considered + await async_recorder_block_till_done(hass) + do_adhoc_statistics(hass, start=now + timedelta(hours=1)) + await async_recorder_block_till_done(hass) + await assert_statistic_ids( + [{"statistic_id": "sensor.test", "unit_of_measurement": "W"}] + ) + await assert_validation_result(client, {}) + + # Change back to original unit - expect error + hass.states.async_set("sensor.test", 13, attributes=attributes) + await async_recorder_block_till_done(hass) + expected = { + "sensor.test": [ + { + "data": { + "metadata_unit": "W", + "state_unit": "kW", + "statistic_id": "sensor.test", + }, + "type": "units_changed_can_convert", + } + ], + } + await assert_validation_result(client, expected) + + # Changed unit - empty response + hass.states.async_set( + "sensor.test", 14, attributes={**attributes, **{"unit_of_measurement": "W"}} + ) + await async_recorder_block_till_done(hass) + await assert_validation_result(client, {}) + + # Valid state, statistic runs again - empty response + await async_recorder_block_till_done(hass) + do_adhoc_statistics(hass, start=now) + await async_recorder_block_till_done(hass) + await assert_validation_result(client, {}) + + # Remove the state - empty response + hass.states.async_remove("sensor.test") + expected = { + "sensor.test": [ + { + "data": {"statistic_id": "sensor.test"}, + "type": "no_state", + } + ], + } + await assert_validation_result(client, expected) + + def record_meter_states(hass, zero, entity_id, _attributes, seq): """Record some test states. From 88500145d235768470f553e6e32a840ef2bce026 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 27 Sep 2022 14:09:55 +0200 Subject: [PATCH 768/955] Goodwe reset to 0 at midnight (#76793) * reset at midnight * fix styling * Update sensor.py * Update homeassistant/components/goodwe/sensor.py Co-authored-by: Erik Montnemery * fix missing import * Only reset if inverter is offline at midnight * fix issort * Add detailed explanation Co-authored-by: Erik Montnemery --- homeassistant/components/goodwe/sensor.py | 42 ++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/goodwe/sensor.py b/homeassistant/components/goodwe/sensor.py index 6dcdc6e8cb1..bf01f449724 100644 --- a/homeassistant/components/goodwe/sensor.py +++ b/homeassistant/components/goodwe/sensor.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass +from datetime import timedelta from typing import Any, cast from goodwe import Inverter, Sensor, SensorKind @@ -23,19 +24,28 @@ from homeassistant.const import ( POWER_WATT, TEMP_CELSIUS, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.event import async_track_point_in_time from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, ) +import homeassistant.util.dt as dt_util from .const import DOMAIN, KEY_COORDINATOR, KEY_DEVICE_INFO, KEY_INVERTER # Sensor name of battery SoC BATTERY_SOC = "battery_soc" +# Sensors that are reset to 0 at midnight. +# The inverter is only powered by the solar panels and not mains power, so it goes dead when the sun goes down. +# The "_day" sensors are reset to 0 when the inverter wakes up in the morning when the sun comes up and power to the inverter is restored. +# This makes sure daily values are reset at midnight instead of at sunrise. +# When the inverter has a battery connected, HomeAssistant will not reset the values but let the inverter reset them by looking at the unavailable state of the inverter. +DAILY_RESET = ["e_day", "e_load_day"] + _MAIN_SENSORS = ( "ppv", "house_consumption", @@ -167,6 +177,7 @@ class InverterSensor(CoordinatorEntity, SensorEntity): self._attr_device_class = SensorDeviceClass.BATTERY self._sensor = sensor self._previous_value = None + self._stop_reset = None @property def native_value(self): @@ -190,3 +201,32 @@ class InverterSensor(CoordinatorEntity, SensorEntity): return cast(GoodweSensorEntityDescription, self.entity_description).available( self ) + + @callback + def async_reset(self, now): + """Reset the value back to 0 at midnight.""" + if not self.coordinator.last_update_success: + self._previous_value = 0 + self.coordinator.data[self._sensor.id_] = 0 + self.async_write_ha_state() + next_midnight = dt_util.start_of_local_day(dt_util.utcnow() + timedelta(days=1)) + self._stop_reset = async_track_point_in_time( + self.hass, self.async_reset, next_midnight + ) + + async def async_added_to_hass(self): + """Schedule reset task at midnight.""" + if self._sensor.id_ in DAILY_RESET: + next_midnight = dt_util.start_of_local_day( + dt_util.utcnow() + timedelta(days=1) + ) + self._stop_reset = async_track_point_in_time( + self.hass, self.async_reset, next_midnight + ) + await super().async_added_to_hass() + + async def async_will_remove_from_hass(self): + """Remove reset task at midnight.""" + if self._sensor.id_ in DAILY_RESET and self._stop_reset is not None: + self._stop_reset() + await super().async_will_remove_from_hass() From a58f91997207777a23988323c902afe103d80547 Mon Sep 17 00:00:00 2001 From: Tom Puttemans Date: Tue, 27 Sep 2022 14:46:18 +0200 Subject: [PATCH 769/955] Add unique ID to dsmr_reader sensors (#79101) --- homeassistant/components/dsmr_reader/sensor.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/dsmr_reader/sensor.py b/homeassistant/components/dsmr_reader/sensor.py index 81458a94739..7130380cbf5 100644 --- a/homeassistant/components/dsmr_reader/sensor.py +++ b/homeassistant/components/dsmr_reader/sensor.py @@ -40,12 +40,12 @@ async def async_setup_platform( async def async_setup_entry( - hass: HomeAssistant, - config: ConfigEntry, + _: HomeAssistant, + config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up DSMR Reader sensors from config entry.""" - async_add_entities(DSMRSensor(description) for description in SENSORS) + async_add_entities(DSMRSensor(description, config_entry) for description in SENSORS) class DSMRSensor(SensorEntity): @@ -53,12 +53,15 @@ class DSMRSensor(SensorEntity): entity_description: DSMRReaderSensorEntityDescription - def __init__(self, description: DSMRReaderSensorEntityDescription) -> None: + def __init__( + self, description: DSMRReaderSensorEntityDescription, config_entry: ConfigEntry + ) -> None: """Initialize the sensor.""" self.entity_description = description slug = slugify(description.key.replace("/", "_")) self.entity_id = f"sensor.{slug}" + self._attr_unique_id = f"{config_entry.entry_id}-{slug}" async def async_added_to_hass(self) -> None: """Subscribe to MQTT events.""" From bfcc18e5b8d3a8603ec0fe7750929e059a13ee7b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 27 Sep 2022 15:34:00 +0100 Subject: [PATCH 770/955] Add distance to SensorDeviceClass (#77951) * Add distance to SensorDeviceClass * Adjust recorder * Adjust tests * Adjust recorder * Update __init__.py * Update test_websocket_api.py * Update test_websocket_api.py * Update test_websocket_api.py * Update strings.json * Fix tests * Adjust docstring --- .../components/recorder/statistics.py | 3 + .../components/recorder/websocket_api.py | 11 ++- homeassistant/components/sensor/__init__.py | 6 ++ .../components/sensor/device_condition.py | 3 + .../components/sensor/device_trigger.py | 3 + homeassistant/components/sensor/recorder.py | 2 + homeassistant/components/sensor/strings.json | 2 + .../components/sensor/translations/en.json | 2 + .../components/recorder/test_websocket_api.py | 18 +++++ tests/components/sensor/test_init.py | 70 +++++++++++++++++-- tests/components/sensor/test_recorder.py | 14 ++++ 11 files changed, 126 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index bf81d42d2fa..6f82626dc37 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -33,6 +33,7 @@ from homeassistant.helpers.typing import UNDEFINED, UndefinedType from homeassistant.util import dt as dt_util from homeassistant.util.unit_conversion import ( BaseUnitConverter, + DistanceConverter, EnergyConverter, PowerConverter, PressureConverter, @@ -122,6 +123,7 @@ QUERY_STATISTIC_META = [ STATISTIC_UNIT_TO_UNIT_CLASS: dict[str | None, str] = { + DistanceConverter.NORMALIZED_UNIT: DistanceConverter.UNIT_CLASS, EnergyConverter.NORMALIZED_UNIT: EnergyConverter.UNIT_CLASS, PowerConverter.NORMALIZED_UNIT: PowerConverter.UNIT_CLASS, PressureConverter.NORMALIZED_UNIT: PressureConverter.UNIT_CLASS, @@ -130,6 +132,7 @@ STATISTIC_UNIT_TO_UNIT_CLASS: dict[str | None, str] = { } STATISTIC_UNIT_TO_UNIT_CONVERTER: dict[str | None, type[BaseUnitConverter]] = { + DistanceConverter.NORMALIZED_UNIT: DistanceConverter, EnergyConverter.NORMALIZED_UNIT: EnergyConverter, PowerConverter.NORMALIZED_UNIT: PowerConverter, PressureConverter.NORMALIZED_UNIT: PressureConverter, diff --git a/homeassistant/components/recorder/websocket_api.py b/homeassistant/components/recorder/websocket_api.py index a4bb1da59e8..2b500fb428a 100644 --- a/homeassistant/components/recorder/websocket_api.py +++ b/homeassistant/components/recorder/websocket_api.py @@ -21,6 +21,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.json import JSON_DUMP from homeassistant.util import dt as dt_util from homeassistant.util.unit_conversion import ( + DistanceConverter, EnergyConverter, PowerConverter, PressureConverter, @@ -123,6 +124,7 @@ async def ws_handle_get_statistics_during_period( vol.Required("period"): vol.Any("5minute", "hour", "day", "month"), vol.Optional("units"): vol.Schema( { + vol.Optional("distance"): vol.In(DistanceConverter.VALID_UNITS), vol.Optional("energy"): vol.In(EnergyConverter.VALID_UNITS), vol.Optional("power"): vol.In(PowerConverter.VALID_UNITS), vol.Optional("pressure"): vol.In(PressureConverter.VALID_UNITS), @@ -299,8 +301,8 @@ async def ws_adjust_sum_statistics( ) -> None: """Adjust sum statistics. - If the statistics is stored as kWh, it's allowed to make an adjustment in Wh or MWh - If the statistics is stored as m³, it's allowed to make an adjustment in ft³ + If the statistics is stored as NORMALIZED_UNIT, + it's allowed to make an adjustment in VALID_UNIT """ start_time_str = msg["start_time"] @@ -322,6 +324,11 @@ async def ws_adjust_sum_statistics( def valid_units(statistics_unit: str | None, display_unit: str | None) -> bool: if statistics_unit == display_unit: return True + if ( + statistics_unit == DistanceConverter.NORMALIZED_UNIT + and display_unit in DistanceConverter.VALID_UNITS + ): + return True if statistics_unit == ENERGY_KILO_WATT_HOUR and display_unit in ( ENERGY_MEGA_WATT_HOUR, ENERGY_WATT_HOUR, diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index ab19f053a22..29157d01660 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -60,6 +60,7 @@ from homeassistant.helpers.typing import ConfigType, StateType from homeassistant.util import dt as dt_util from homeassistant.util.unit_conversion import ( BaseUnitConverter, + DistanceConverter, PressureConverter, TemperatureConverter, ) @@ -102,6 +103,9 @@ class SensorDeviceClass(StrEnum): # date (ISO8601) DATE = "date" + # distance (LENGTH_*) + DISTANCE = "distance" + # fixed duration (TIME_DAYS, TIME_HOURS, TIME_MINUTES, TIME_SECONDS) DURATION = "duration" @@ -209,11 +213,13 @@ STATE_CLASS_TOTAL_INCREASING: Final = "total_increasing" STATE_CLASSES: Final[list[str]] = [cls.value for cls in SensorStateClass] UNIT_CONVERTERS: dict[str, type[BaseUnitConverter]] = { + SensorDeviceClass.DISTANCE: DistanceConverter, SensorDeviceClass.PRESSURE: PressureConverter, SensorDeviceClass.TEMPERATURE: TemperatureConverter, } UNIT_RATIOS: dict[str, dict[str, float]] = { + SensorDeviceClass.DISTANCE: DistanceConverter.UNIT_CONVERSION, SensorDeviceClass.PRESSURE: PressureConverter.UNIT_CONVERSION, SensorDeviceClass.TEMPERATURE: { TEMP_CELSIUS: 1.0, diff --git a/homeassistant/components/sensor/device_condition.py b/homeassistant/components/sensor/device_condition.py index 437671a7e30..6ecce8b1a13 100644 --- a/homeassistant/components/sensor/device_condition.py +++ b/homeassistant/components/sensor/device_condition.py @@ -36,6 +36,7 @@ CONF_IS_BATTERY_LEVEL = "is_battery_level" CONF_IS_CO = "is_carbon_monoxide" CONF_IS_CO2 = "is_carbon_dioxide" CONF_IS_CURRENT = "is_current" +CONF_IS_DISTANCE = "is_distance" CONF_IS_ENERGY = "is_energy" CONF_IS_FREQUENCY = "is_frequency" CONF_IS_HUMIDITY = "is_humidity" @@ -66,6 +67,7 @@ ENTITY_CONDITIONS = { SensorDeviceClass.CO: [{CONF_TYPE: CONF_IS_CO}], SensorDeviceClass.CO2: [{CONF_TYPE: CONF_IS_CO2}], SensorDeviceClass.CURRENT: [{CONF_TYPE: CONF_IS_CURRENT}], + SensorDeviceClass.DISTANCE: [{CONF_TYPE: CONF_IS_DISTANCE}], SensorDeviceClass.ENERGY: [{CONF_TYPE: CONF_IS_ENERGY}], SensorDeviceClass.FREQUENCY: [{CONF_TYPE: CONF_IS_FREQUENCY}], SensorDeviceClass.GAS: [{CONF_TYPE: CONF_IS_GAS}], @@ -104,6 +106,7 @@ CONDITION_SCHEMA = vol.All( CONF_IS_CO, CONF_IS_CO2, CONF_IS_CURRENT, + CONF_IS_DISTANCE, CONF_IS_ENERGY, CONF_IS_FREQUENCY, CONF_IS_GAS, diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py index 3cce6b74a81..cd009842b97 100644 --- a/homeassistant/components/sensor/device_trigger.py +++ b/homeassistant/components/sensor/device_trigger.py @@ -35,6 +35,7 @@ CONF_BATTERY_LEVEL = "battery_level" CONF_CO = "carbon_monoxide" CONF_CO2 = "carbon_dioxide" CONF_CURRENT = "current" +CONF_DISTANCE = "distance" CONF_ENERGY = "energy" CONF_FREQUENCY = "frequency" CONF_GAS = "gas" @@ -65,6 +66,7 @@ ENTITY_TRIGGERS = { SensorDeviceClass.CO: [{CONF_TYPE: CONF_CO}], SensorDeviceClass.CO2: [{CONF_TYPE: CONF_CO2}], SensorDeviceClass.CURRENT: [{CONF_TYPE: CONF_CURRENT}], + SensorDeviceClass.DISTANCE: [{CONF_TYPE: CONF_DISTANCE}], SensorDeviceClass.ENERGY: [{CONF_TYPE: CONF_ENERGY}], SensorDeviceClass.FREQUENCY: [{CONF_TYPE: CONF_FREQUENCY}], SensorDeviceClass.GAS: [{CONF_TYPE: CONF_GAS}], @@ -104,6 +106,7 @@ TRIGGER_SCHEMA = vol.All( CONF_CO, CONF_CO2, CONF_CURRENT, + CONF_DISTANCE, CONF_ENERGY, CONF_FREQUENCY, CONF_GAS, diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index cfa6d5a36fe..5196dc562df 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -30,6 +30,7 @@ from homeassistant.helpers.entity import entity_sources from homeassistant.util import dt as dt_util from homeassistant.util.unit_conversion import ( BaseUnitConverter, + DistanceConverter, EnergyConverter, PowerConverter, PressureConverter, @@ -57,6 +58,7 @@ DEFAULT_STATISTICS = { } UNIT_CONVERTERS: dict[str, type[BaseUnitConverter]] = { + SensorDeviceClass.DISTANCE: DistanceConverter, SensorDeviceClass.ENERGY: EnergyConverter, SensorDeviceClass.POWER: PowerConverter, SensorDeviceClass.PRESSURE: PressureConverter, diff --git a/homeassistant/components/sensor/strings.json b/homeassistant/components/sensor/strings.json index bac12bd8cb2..d79f1035f62 100644 --- a/homeassistant/components/sensor/strings.json +++ b/homeassistant/components/sensor/strings.json @@ -6,6 +6,7 @@ "is_battery_level": "Current {entity_name} battery level", "is_carbon_monoxide": "Current {entity_name} carbon monoxide concentration level", "is_carbon_dioxide": "Current {entity_name} carbon dioxide concentration level", + "is_distance": "Current {entity_name} distance", "is_gas": "Current {entity_name} gas", "is_humidity": "Current {entity_name} humidity", "is_illuminance": "Current {entity_name} illuminance", @@ -36,6 +37,7 @@ "battery_level": "{entity_name} battery level changes", "carbon_monoxide": "{entity_name} carbon monoxide concentration changes", "carbon_dioxide": "{entity_name} carbon dioxide concentration changes", + "distance": "{entity_name} distance changes", "gas": "{entity_name} gas changes", "humidity": "{entity_name} humidity changes", "illuminance": "{entity_name} illuminance changes", diff --git a/homeassistant/components/sensor/translations/en.json b/homeassistant/components/sensor/translations/en.json index 5c38db03687..7e2223521eb 100644 --- a/homeassistant/components/sensor/translations/en.json +++ b/homeassistant/components/sensor/translations/en.json @@ -6,6 +6,7 @@ "is_carbon_dioxide": "Current {entity_name} carbon dioxide concentration level", "is_carbon_monoxide": "Current {entity_name} carbon monoxide concentration level", "is_current": "Current {entity_name} current", + "is_distance": "Current {entity_name} distance", "is_energy": "Current {entity_name} energy", "is_frequency": "Current {entity_name} frequency", "is_gas": "Current {entity_name} gas", @@ -36,6 +37,7 @@ "carbon_dioxide": "{entity_name} carbon dioxide concentration changes", "carbon_monoxide": "{entity_name} carbon monoxide concentration changes", "current": "{entity_name} current changes", + "distance": "{entity_name} distance changes", "energy": "{entity_name} energy changes", "frequency": "{entity_name} frequency changes", "gas": "{entity_name} gas changes", diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index 39a50010f1b..4790889951b 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -30,6 +30,16 @@ from .common import ( from tests.common import async_fire_time_changed +DISTANCE_SENSOR_FT_ATTRIBUTES = { + "device_class": "distance", + "state_class": "measurement", + "unit_of_measurement": "ft", +} +DISTANCE_SENSOR_M_ATTRIBUTES = { + "device_class": "distance", + "state_class": "measurement", + "unit_of_measurement": "m", +} ENERGY_SENSOR_KWH_ATTRIBUTES = { "device_class": "energy", "state_class": "total", @@ -141,6 +151,9 @@ async def test_statistics_during_period(hass, hass_ws_client, recorder_mock): @pytest.mark.parametrize( "attributes, state, value, custom_units, converted_value", [ + (DISTANCE_SENSOR_M_ATTRIBUTES, 10, 10, {"distance": "cm"}, 1000), + (DISTANCE_SENSOR_M_ATTRIBUTES, 10, 10, {"distance": "m"}, 10), + (DISTANCE_SENSOR_M_ATTRIBUTES, 10, 10, {"distance": "in"}, 10 / 0.0254), (POWER_SENSOR_KW_ATTRIBUTES, 10, 10, {"power": "W"}, 10000), (POWER_SENSOR_KW_ATTRIBUTES, 10, 10, {"power": "kW"}, 10), (PRESSURE_SENSOR_HPA_ATTRIBUTES, 10, 10, {"pressure": "Pa"}, 1000), @@ -327,6 +340,7 @@ async def test_sum_statistics_during_period_unit_conversion( @pytest.mark.parametrize( "custom_units", [ + {"distance": "L"}, {"energy": "W"}, {"power": "Pa"}, {"pressure": "K"}, @@ -538,6 +552,10 @@ async def test_statistics_during_period_bad_end_time( @pytest.mark.parametrize( "units, attributes, display_unit, statistics_unit, unit_class", [ + (IMPERIAL_SYSTEM, DISTANCE_SENSOR_M_ATTRIBUTES, "m", "m", "distance"), + (METRIC_SYSTEM, DISTANCE_SENSOR_M_ATTRIBUTES, "m", "m", "distance"), + (IMPERIAL_SYSTEM, DISTANCE_SENSOR_FT_ATTRIBUTES, "ft", "m", "distance"), + (METRIC_SYSTEM, DISTANCE_SENSOR_FT_ATTRIBUTES, "ft", "m", "distance"), (IMPERIAL_SYSTEM, ENERGY_SENSOR_WH_ATTRIBUTES, "Wh", "kWh", "energy"), (METRIC_SYSTEM, ENERGY_SENSOR_WH_ATTRIBUTES, "Wh", "kWh", "energy"), (IMPERIAL_SYSTEM, GAS_SENSOR_FT3_ATTRIBUTES, "ft³", "m³", "volume"), diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index 3a593b0e6cc..ad672b56801 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -8,6 +8,10 @@ from pytest import approx from homeassistant.components.sensor import SensorDeviceClass from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, + LENGTH_CENTIMETERS, + LENGTH_INCHES, + LENGTH_KILOMETERS, + LENGTH_MILES, PRESSURE_HPA, PRESSURE_INHG, PRESSURE_KPA, @@ -455,14 +459,67 @@ async def test_custom_unit( @pytest.mark.parametrize( - "native_unit,custom_unit,state_unit,native_value,custom_value", + "native_unit,custom_unit,state_unit,native_value,custom_value,device_class", [ + # Distance + ( + LENGTH_KILOMETERS, + LENGTH_MILES, + LENGTH_MILES, + 1000, + 621, + SensorDeviceClass.DISTANCE, + ), + ( + LENGTH_CENTIMETERS, + LENGTH_INCHES, + LENGTH_INCHES, + 7.24, + 2.85, + SensorDeviceClass.DISTANCE, + ), + ( + LENGTH_KILOMETERS, + "peer_distance", + LENGTH_KILOMETERS, + 1000, + 1000, + SensorDeviceClass.DISTANCE, + ), # Smaller to larger unit, InHg is ~33x larger than hPa -> 1 more decimal - (PRESSURE_HPA, PRESSURE_INHG, PRESSURE_INHG, 1000.0, 29.53), - (PRESSURE_KPA, PRESSURE_HPA, PRESSURE_HPA, 1.234, 12.34), - (PRESSURE_HPA, PRESSURE_MMHG, PRESSURE_MMHG, 1000, 750), + ( + PRESSURE_HPA, + PRESSURE_INHG, + PRESSURE_INHG, + 1000.0, + 29.53, + SensorDeviceClass.PRESSURE, + ), + ( + PRESSURE_KPA, + PRESSURE_HPA, + PRESSURE_HPA, + 1.234, + 12.34, + SensorDeviceClass.PRESSURE, + ), + ( + PRESSURE_HPA, + PRESSURE_MMHG, + PRESSURE_MMHG, + 1000, + 750, + SensorDeviceClass.PRESSURE, + ), # Not a supported pressure unit - (PRESSURE_HPA, "peer_pressure", PRESSURE_HPA, 1000, 1000), + ( + PRESSURE_HPA, + "peer_pressure", + PRESSURE_HPA, + 1000, + 1000, + SensorDeviceClass.PRESSURE, + ), ], ) async def test_custom_unit_change( @@ -473,6 +530,7 @@ async def test_custom_unit_change( state_unit, native_value, custom_value, + device_class, ): """Test custom unit changes are picked up.""" entity_registry = er.async_get(hass) @@ -482,7 +540,7 @@ async def test_custom_unit_change( name="Test", native_value=str(native_value), native_unit_of_measurement=native_unit, - device_class=SensorDeviceClass.PRESSURE, + device_class=device_class, unique_id="very_unique", ) diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 8b9671fbe9c..1bb95888e92 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -85,6 +85,8 @@ def set_time_zone(): (None, "%", "%", "%", None, 13.050847, -10, 30), ("battery", "%", "%", "%", None, 13.050847, -10, 30), ("battery", None, None, None, None, 13.050847, -10, 30), + ("distance", "m", "m", "m", "distance", 13.050847, -10, 30), + ("distance", "mi", "mi", "m", "distance", 13.050847, -10, 30), ("humidity", "%", "%", "%", None, 13.050847, -10, 30), ("humidity", None, None, None, None, 13.050847, -10, 30), ("pressure", "Pa", "Pa", "Pa", "pressure", 13.050847, -10, 30), @@ -351,12 +353,16 @@ def test_compile_hourly_statistics_unsupported(hass_recorder, caplog, attributes @pytest.mark.parametrize( "units, device_class, state_unit, display_unit, statistics_unit, unit_class, factor", [ + (IMPERIAL_SYSTEM, "distance", "m", "m", "m", "distance", 1), + (IMPERIAL_SYSTEM, "distance", "mi", "mi", "m", "distance", 1), (IMPERIAL_SYSTEM, "energy", "kWh", "kWh", "kWh", "energy", 1), (IMPERIAL_SYSTEM, "energy", "Wh", "Wh", "kWh", "energy", 1), (IMPERIAL_SYSTEM, "monetary", "EUR", "EUR", "EUR", None, 1), (IMPERIAL_SYSTEM, "monetary", "SEK", "SEK", "SEK", None, 1), (IMPERIAL_SYSTEM, "gas", "m³", "m³", "m³", "volume", 1), (IMPERIAL_SYSTEM, "gas", "ft³", "ft³", "m³", "volume", 1), + (METRIC_SYSTEM, "distance", "m", "m", "m", "distance", 1), + (METRIC_SYSTEM, "distance", "mi", "mi", "m", "distance", 1), (METRIC_SYSTEM, "energy", "kWh", "kWh", "kWh", "energy", 1), (METRIC_SYSTEM, "energy", "Wh", "Wh", "kWh", "energy", 1), (METRIC_SYSTEM, "monetary", "EUR", "EUR", "EUR", None, 1), @@ -1548,6 +1554,8 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): [ ("battery", "%", 30), ("battery", None, 30), + ("distance", "m", 30), + ("distance", "mi", 30), ("humidity", "%", 30), ("humidity", None, 30), ("pressure", "Pa", 30), @@ -1635,6 +1643,8 @@ def test_compile_hourly_statistics_partially_unavailable(hass_recorder, caplog): [ ("battery", "%", 30), ("battery", None, 30), + ("distance", "m", 30), + ("distance", "mi", 30), ("humidity", "%", 30), ("humidity", None, 30), ("pressure", "Pa", 30), @@ -1708,6 +1718,10 @@ def test_compile_hourly_statistics_fails(hass_recorder, caplog): [ ("measurement", "battery", "%", "%", "%", None, "mean"), ("measurement", "battery", None, None, None, None, "mean"), + ("measurement", "distance", "m", "m", "m", "distance", "mean"), + ("measurement", "distance", "mi", "mi", "m", "distance", "mean"), + ("total", "distance", "m", "m", "m", "distance", "sum"), + ("total", "distance", "mi", "mi", "m", "distance", "sum"), ("total", "energy", "Wh", "Wh", "kWh", "energy", "sum"), ("total", "energy", "kWh", "kWh", "kWh", "energy", "sum"), ("measurement", "energy", "Wh", "Wh", "kWh", "energy", "mean"), From 53263ea9bc7602d7e5be585a61f191ca4d3846a9 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 27 Sep 2022 16:36:38 +0200 Subject: [PATCH 771/955] Revert "Add serial_number to device registry entries" (#79139) --- .../components/config/device_registry.py | 1 - homeassistant/helpers/device_registry.py | 13 +- .../components/config/test_device_registry.py | 3 - tests/helpers/test_device_registry.py | 136 +----------------- 4 files changed, 5 insertions(+), 148 deletions(-) diff --git a/homeassistant/components/config/device_registry.py b/homeassistant/components/config/device_registry.py index 118b898ec4c..8edd9f1f4d3 100644 --- a/homeassistant/components/config/device_registry.py +++ b/homeassistant/components/config/device_registry.py @@ -167,7 +167,6 @@ def _entry_dict(entry): "model": entry.model, "name_by_user": entry.name_by_user, "name": entry.name, - "serial_number": entry.serial_number, "sw_version": entry.sw_version, "via_device_id": entry.via_device_id, } diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 16ff67bed19..908db74d40d 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -32,7 +32,7 @@ DATA_REGISTRY = "device_registry" EVENT_DEVICE_REGISTRY_UPDATED = "device_registry_updated" STORAGE_KEY = "core.device_registry" STORAGE_VERSION_MAJOR = 1 -STORAGE_VERSION_MINOR = 4 +STORAGE_VERSION_MINOR = 3 SAVE_DELAY = 10 CLEANUP_DELAY = 10 @@ -83,7 +83,6 @@ class DeviceEntry: model: str | None = attr.ib(default=None) name_by_user: str | None = attr.ib(default=None) name: str | None = attr.ib(default=None) - serial_number: str | None = attr.ib(default=None) suggested_area: str | None = attr.ib(default=None) sw_version: str | None = attr.ib(default=None) via_device_id: str | None = attr.ib(default=None) @@ -181,10 +180,6 @@ class DeviceRegistryStore(storage.Store[dict[str, list[dict[str, Any]]]]): # Introduced in 2022.2 for device in old_data["devices"]: device["hw_version"] = device.get("hw_version") - if old_minor_version < 4: - # Introduced in 2022.10 - for device in old_data["devices"]: - device["serial_number"] = device.get("serial_number") if old_major_version > 1: raise NotImplementedError @@ -306,7 +301,6 @@ class DeviceRegistry: manufacturer: str | None | UndefinedType = UNDEFINED, model: str | None | UndefinedType = UNDEFINED, name: str | None | UndefinedType = UNDEFINED, - serial_number: str | None | UndefinedType = UNDEFINED, suggested_area: str | None | UndefinedType = UNDEFINED, sw_version: str | None | UndefinedType = UNDEFINED, via_device: tuple[str, str] | None = None, @@ -372,7 +366,6 @@ class DeviceRegistry: merge_identifiers=identifiers or UNDEFINED, model=model, name=name, - serial_number=serial_number, suggested_area=suggested_area, sw_version=sw_version, via_device_id=via_device_id, @@ -402,7 +395,6 @@ class DeviceRegistry: name: str | None | UndefinedType = UNDEFINED, new_identifiers: set[tuple[str, str]] | UndefinedType = UNDEFINED, remove_config_entry_id: str | UndefinedType = UNDEFINED, - serial_number: str | None | UndefinedType = UNDEFINED, suggested_area: str | None | UndefinedType = UNDEFINED, sw_version: str | None | UndefinedType = UNDEFINED, via_device_id: str | None | UndefinedType = UNDEFINED, @@ -487,7 +479,6 @@ class DeviceRegistry: ("model", model), ("name", name), ("name_by_user", name_by_user), - ("serial_number", serial_number), ("suggested_area", suggested_area), ("sw_version", sw_version), ("via_device_id", via_device_id), @@ -575,7 +566,6 @@ class DeviceRegistry: model=device["model"], name_by_user=device["name_by_user"], name=device["name"], - serial_number=device["serial_number"], sw_version=device["sw_version"], via_device_id=device["via_device_id"], ) @@ -618,7 +608,6 @@ class DeviceRegistry: "model": entry.model, "name_by_user": entry.name_by_user, "name": entry.name, - "serial_number": entry.serial_number, "sw_version": entry.sw_version, "via_device_id": entry.via_device_id, } diff --git a/tests/components/config/test_device_registry.py b/tests/components/config/test_device_registry.py index 0903c81a424..4f47e463751 100644 --- a/tests/components/config/test_device_registry.py +++ b/tests/components/config/test_device_registry.py @@ -64,7 +64,6 @@ async def test_list_devices(hass, client, registry): "model": "model", "name_by_user": None, "name": None, - "serial_number": None, "sw_version": None, "via_device_id": None, }, @@ -81,7 +80,6 @@ async def test_list_devices(hass, client, registry): "model": "model", "name_by_user": None, "name": None, - "serial_number": None, "sw_version": None, "via_device_id": dev1, }, @@ -108,7 +106,6 @@ async def test_list_devices(hass, client, registry): "model": "model", "name_by_user": None, "name": None, - "serial_number": None, "sw_version": None, "via_device_id": None, } diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index fe702b9c294..2c9a7956874 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -189,7 +189,6 @@ async def test_loading_from_storage(hass, hass_storage): "model": "model", "name_by_user": "Test Friendly Name", "name": "name", - "serial_number": "serial_no", "sw_version": "version", "via_device_id": None, } @@ -232,7 +231,6 @@ async def test_loading_from_storage(hass, hass_storage): model="model", name_by_user="Test Friendly Name", name="name", - serial_number="serial_no", suggested_area=None, # Not stored sw_version="version", ) @@ -263,8 +261,8 @@ async def test_loading_from_storage(hass, hass_storage): @pytest.mark.parametrize("load_registries", [False]) -async def test_migration_1_1_to_1_4(hass, hass_storage): - """Test migration from version 1.1 to 1.4.""" +async def test_migration_1_1_to_1_3(hass, hass_storage): + """Test migration from version 1.1 to 1.3.""" hass_storage[device_registry.STORAGE_KEY] = { "version": 1, "minor_version": 1, @@ -352,7 +350,6 @@ async def test_migration_1_1_to_1_4(hass, hass_storage): "model": "model", "name": "name", "name_by_user": None, - "serial_number": None, "sw_version": "new_version", "via_device_id": None, }, @@ -370,7 +367,6 @@ async def test_migration_1_1_to_1_4(hass, hass_storage): "model": None, "name_by_user": None, "name": None, - "serial_number": None, "sw_version": None, "via_device_id": None, }, @@ -389,8 +385,8 @@ async def test_migration_1_1_to_1_4(hass, hass_storage): @pytest.mark.parametrize("load_registries", [False]) -async def test_migration_1_2_to_1_4(hass, hass_storage): - """Test migration from version 1.2 to 1.4.""" +async def test_migration_1_2_to_1_3(hass, hass_storage): + """Test migration from version 1.2 to 1.3.""" hass_storage[device_registry.STORAGE_KEY] = { "version": 1, "minor_version": 2, @@ -477,7 +473,6 @@ async def test_migration_1_2_to_1_4(hass, hass_storage): "model": "model", "name": "name", "name_by_user": None, - "serial_number": None, "sw_version": "new_version", "via_device_id": None, }, @@ -495,126 +490,6 @@ async def test_migration_1_2_to_1_4(hass, hass_storage): "model": None, "name_by_user": None, "name": None, - "serial_number": None, - "sw_version": None, - "via_device_id": None, - }, - ], - "deleted_devices": [], - }, - } - - -@pytest.mark.parametrize("load_registries", [False]) -async def test_migration_1_3_to_1_4(hass, hass_storage): - """Test migration from version 1.3 to 1.4.""" - hass_storage[device_registry.STORAGE_KEY] = { - "version": 1, - "minor_version": 3, - "key": device_registry.STORAGE_KEY, - "data": { - "devices": [ - { - "area_id": None, - "config_entries": ["1234"], - "configuration_url": None, - "connections": [["Zigbee", "01.23.45.67.89"]], - "disabled_by": None, - "entry_type": "service", - "hw_version": "hw_version", - "id": "abcdefghijklm", - "identifiers": [["serial", "12:34:56:AB:CD:EF"]], - "manufacturer": "manufacturer", - "model": "model", - "name": "name", - "name_by_user": None, - "sw_version": "version", - "via_device_id": None, - }, - { - "area_id": None, - "config_entries": [None], - "configuration_url": None, - "connections": [], - "disabled_by": None, - "entry_type": None, - "hw_version": None, - "id": "invalid-entry-type", - "identifiers": [["serial", "mock-id-invalid-entry"]], - "manufacturer": None, - "model": None, - "name_by_user": None, - "name": None, - "sw_version": None, - "via_device_id": None, - }, - ], - "deleted_devices": [], - }, - } - - await device_registry.async_load(hass) - registry = device_registry.async_get(hass) - - # Test data was loaded - entry = registry.async_get_or_create( - config_entry_id="1234", - connections={("Zigbee", "01.23.45.67.89")}, - identifiers={("serial", "12:34:56:AB:CD:EF")}, - ) - assert entry.id == "abcdefghijklm" - - # Update to trigger a store - entry = registry.async_get_or_create( - config_entry_id="1234", - connections={("Zigbee", "01.23.45.67.89")}, - identifiers={("serial", "12:34:56:AB:CD:EF")}, - sw_version="new_version", - ) - assert entry.id == "abcdefghijklm" - - # Check we store migrated data - await flush_store(registry._store) - - assert hass_storage[device_registry.STORAGE_KEY] == { - "version": device_registry.STORAGE_VERSION_MAJOR, - "minor_version": device_registry.STORAGE_VERSION_MINOR, - "key": device_registry.STORAGE_KEY, - "data": { - "devices": [ - { - "area_id": None, - "config_entries": ["1234"], - "configuration_url": None, - "connections": [["Zigbee", "01.23.45.67.89"]], - "disabled_by": None, - "entry_type": "service", - "hw_version": "hw_version", - "id": "abcdefghijklm", - "identifiers": [["serial", "12:34:56:AB:CD:EF"]], - "manufacturer": "manufacturer", - "model": "model", - "name": "name", - "name_by_user": None, - "serial_number": None, - "sw_version": "new_version", - "via_device_id": None, - }, - { - "area_id": None, - "config_entries": [None], - "configuration_url": None, - "connections": [], - "disabled_by": None, - "entry_type": None, - "hw_version": None, - "id": "invalid-entry-type", - "identifiers": [["serial", "mock-id-invalid-entry"]], - "manufacturer": None, - "model": None, - "name_by_user": None, - "name": None, - "serial_number": None, "sw_version": None, "via_device_id": None, }, @@ -1043,7 +918,6 @@ async def test_update(hass, registry, update_events): name_by_user="Test Friendly Name", name="name", new_identifiers=new_identifiers, - serial_number="serial_no", suggested_area="suggested_area", sw_version="version", via_device_id="98765B", @@ -1065,7 +939,6 @@ async def test_update(hass, registry, update_events): model="Test Model", name_by_user="Test Friendly Name", name="name", - serial_number="serial_no", suggested_area="suggested_area", sw_version="version", via_device_id="98765B", @@ -1105,7 +978,6 @@ async def test_update(hass, registry, update_events): "model": None, "name": None, "name_by_user": None, - "serial_number": None, "suggested_area": None, "sw_version": None, "via_device_id": None, From 4fcd0f3e2343f0e95edee4e4d100c330dcb1a623 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 27 Sep 2022 17:00:06 +0200 Subject: [PATCH 772/955] Fix recorder fixtures (#79147) --- tests/conftest.py | 39 ++++++++++++++++++--------------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 1e7ac735125..dacd9d09c91 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -918,25 +918,22 @@ async def async_setup_recorder_instance( ) -> AsyncGenerator[SetupRecorderInstanceT, None]: """Yield callable to setup recorder instance.""" - async def async_setup_recorder( - hass: HomeAssistant, config: ConfigType | None = None - ) -> recorder.Recorder: - """Setup and return recorder instance.""" # noqa: D401 - nightly = ( - recorder.Recorder.async_nightly_tasks if enable_nightly_purge else None - ) - stats = ( - recorder.Recorder.async_periodic_statistics if enable_statistics else None - ) - with patch( - "homeassistant.components.recorder.Recorder.async_nightly_tasks", - side_effect=nightly, - autospec=True, - ), patch( - "homeassistant.components.recorder.Recorder.async_periodic_statistics", - side_effect=stats, - autospec=True, - ): + nightly = recorder.Recorder.async_nightly_tasks if enable_nightly_purge else None + stats = recorder.Recorder.async_periodic_statistics if enable_statistics else None + with patch( + "homeassistant.components.recorder.Recorder.async_nightly_tasks", + side_effect=nightly, + autospec=True, + ), patch( + "homeassistant.components.recorder.Recorder.async_periodic_statistics", + side_effect=stats, + autospec=True, + ): + + async def async_setup_recorder( + hass: HomeAssistant, config: ConfigType | None = None + ) -> recorder.Recorder: + """Setup and return recorder instance.""" # noqa: D401 await _async_init_recorder_component(hass, config) await hass.async_block_till_done() instance = hass.data[recorder.DATA_INSTANCE] @@ -945,13 +942,13 @@ async def async_setup_recorder_instance( await async_recorder_block_till_done(hass) return instance - return async_setup_recorder + yield async_setup_recorder @pytest.fixture async def recorder_mock(recorder_config, async_setup_recorder_instance, hass): """Fixture with in-memory recorder.""" - await async_setup_recorder_instance(hass, recorder_config) + yield await async_setup_recorder_instance(hass, recorder_config) @pytest.fixture From 7ead77eea6d536e13263fdf8cb33ae5dbbdddc7d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 27 Sep 2022 17:32:54 +0200 Subject: [PATCH 773/955] Correct typing of async_track_state_change (#79150) * Correct typing of async_track_state_change * Update integrations --- homeassistant/components/proximity/__init__.py | 5 ++++- homeassistant/helpers/event.py | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/proximity/__init__.py b/homeassistant/components/proximity/__init__.py index 47dd663561c..7a54b11ef34 100644 --- a/homeassistant/components/proximity/__init__.py +++ b/homeassistant/components/proximity/__init__.py @@ -157,9 +157,12 @@ class Proximity(Entity): return {ATTR_DIR_OF_TRAVEL: self.dir_of_travel, ATTR_NEAREST: self.nearest} def check_proximity_state_change( - self, entity: str, old_state: State | None, new_state: State + self, entity: str, old_state: State | None, new_state: State | None ) -> None: """Perform the proximity checking.""" + if new_state is None: + return + entity_name = new_state.name devices_to_calculate = False devices_in_zone = "" diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 1cea1860b38..107567c98ce 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -141,7 +141,9 @@ def threaded_listener_factory( def async_track_state_change( hass: HomeAssistant, entity_ids: str | Iterable[str], - action: Callable[[str, State | None, State], Coroutine[Any, Any, None] | None], + action: Callable[ + [str, State | None, State | None], Coroutine[Any, Any, None] | None + ], from_state: None | str | Iterable[str] = None, to_state: None | str | Iterable[str] = None, ) -> CALLBACK_TYPE: From 7c448416e1667427491ff55d3389a4c049a08106 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 27 Sep 2022 17:19:34 +0100 Subject: [PATCH 774/955] Add speed to SensorDeviceClass (#77953) * Add speed to SensorDeviceClass * Adjust recorder * Adjust tests * Adjust sensor UNIT_CONVERTERS * Add tests * Add websocket tests * Update strings.json --- .../components/recorder/statistics.py | 3 ++ .../components/recorder/websocket_api.py | 2 ++ homeassistant/components/sensor/__init__.py | 6 ++++ .../components/sensor/device_condition.py | 2 ++ .../components/sensor/device_trigger.py | 2 ++ homeassistant/components/sensor/recorder.py | 2 ++ homeassistant/components/sensor/strings.json | 2 ++ .../components/sensor/translations/en.json | 2 ++ .../components/recorder/test_websocket_api.py | 17 +++++++++++ tests/components/sensor/test_init.py | 29 +++++++++++++++++++ tests/components/sensor/test_recorder.py | 8 +++++ 11 files changed, 75 insertions(+) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 6f82626dc37..8d679cbeae6 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -37,6 +37,7 @@ from homeassistant.util.unit_conversion import ( EnergyConverter, PowerConverter, PressureConverter, + SpeedConverter, TemperatureConverter, VolumeConverter, ) @@ -127,6 +128,7 @@ STATISTIC_UNIT_TO_UNIT_CLASS: dict[str | None, str] = { EnergyConverter.NORMALIZED_UNIT: EnergyConverter.UNIT_CLASS, PowerConverter.NORMALIZED_UNIT: PowerConverter.UNIT_CLASS, PressureConverter.NORMALIZED_UNIT: PressureConverter.UNIT_CLASS, + SpeedConverter.NORMALIZED_UNIT: SpeedConverter.UNIT_CLASS, TemperatureConverter.NORMALIZED_UNIT: TemperatureConverter.UNIT_CLASS, VolumeConverter.NORMALIZED_UNIT: VolumeConverter.UNIT_CLASS, } @@ -136,6 +138,7 @@ STATISTIC_UNIT_TO_UNIT_CONVERTER: dict[str | None, type[BaseUnitConverter]] = { EnergyConverter.NORMALIZED_UNIT: EnergyConverter, PowerConverter.NORMALIZED_UNIT: PowerConverter, PressureConverter.NORMALIZED_UNIT: PressureConverter, + SpeedConverter.NORMALIZED_UNIT: SpeedConverter, TemperatureConverter.NORMALIZED_UNIT: TemperatureConverter, VolumeConverter.NORMALIZED_UNIT: VolumeConverter, } diff --git a/homeassistant/components/recorder/websocket_api.py b/homeassistant/components/recorder/websocket_api.py index 2b500fb428a..2ede11cf887 100644 --- a/homeassistant/components/recorder/websocket_api.py +++ b/homeassistant/components/recorder/websocket_api.py @@ -25,6 +25,7 @@ from homeassistant.util.unit_conversion import ( EnergyConverter, PowerConverter, PressureConverter, + SpeedConverter, TemperatureConverter, ) @@ -128,6 +129,7 @@ async def ws_handle_get_statistics_during_period( vol.Optional("energy"): vol.In(EnergyConverter.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.Any(VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS), } diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 29157d01660..1babc2d0084 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -62,6 +62,7 @@ from homeassistant.util.unit_conversion import ( BaseUnitConverter, DistanceConverter, PressureConverter, + SpeedConverter, TemperatureConverter, ) @@ -166,6 +167,9 @@ class SensorDeviceClass(StrEnum): # signal strength (dB/dBm) SIGNAL_STRENGTH = "signal_strength" + # speed (SPEED_*) + SPEED = "speed" + # Amount of SO2 (µg/m³) SULPHUR_DIOXIDE = "sulphur_dioxide" @@ -215,12 +219,14 @@ STATE_CLASSES: Final[list[str]] = [cls.value for cls in SensorStateClass] UNIT_CONVERTERS: dict[str, type[BaseUnitConverter]] = { SensorDeviceClass.DISTANCE: DistanceConverter, SensorDeviceClass.PRESSURE: PressureConverter, + SensorDeviceClass.SPEED: SpeedConverter, SensorDeviceClass.TEMPERATURE: TemperatureConverter, } UNIT_RATIOS: dict[str, dict[str, float]] = { SensorDeviceClass.DISTANCE: DistanceConverter.UNIT_CONVERSION, SensorDeviceClass.PRESSURE: PressureConverter.UNIT_CONVERSION, + SensorDeviceClass.SPEED: SpeedConverter.UNIT_CONVERSION, SensorDeviceClass.TEMPERATURE: { TEMP_CELSIUS: 1.0, TEMP_FAHRENHEIT: 1.8, diff --git a/homeassistant/components/sensor/device_condition.py b/homeassistant/components/sensor/device_condition.py index 6ecce8b1a13..08aeda46ba2 100644 --- a/homeassistant/components/sensor/device_condition.py +++ b/homeassistant/components/sensor/device_condition.py @@ -53,6 +53,7 @@ CONF_IS_PM25 = "is_pm25" CONF_IS_POWER = "is_power" CONF_IS_POWER_FACTOR = "is_power_factor" CONF_IS_PRESSURE = "is_pressure" +CONF_IS_SPEED = "is_speed" CONF_IS_REACTIVE_POWER = "is_reactive_power" CONF_IS_SIGNAL_STRENGTH = "is_signal_strength" CONF_IS_SULPHUR_DIOXIDE = "is_sulphur_dioxide" @@ -86,6 +87,7 @@ ENTITY_CONDITIONS = { SensorDeviceClass.PRESSURE: [{CONF_TYPE: CONF_IS_PRESSURE}], SensorDeviceClass.REACTIVE_POWER: [{CONF_TYPE: CONF_IS_REACTIVE_POWER}], SensorDeviceClass.SIGNAL_STRENGTH: [{CONF_TYPE: CONF_IS_SIGNAL_STRENGTH}], + SensorDeviceClass.SPEED: [{CONF_TYPE: CONF_IS_SPEED}], SensorDeviceClass.SULPHUR_DIOXIDE: [{CONF_TYPE: CONF_IS_SULPHUR_DIOXIDE}], SensorDeviceClass.TEMPERATURE: [{CONF_TYPE: CONF_IS_TEMPERATURE}], SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS: [ diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py index cd009842b97..a1275b202ed 100644 --- a/homeassistant/components/sensor/device_trigger.py +++ b/homeassistant/components/sensor/device_trigger.py @@ -54,6 +54,7 @@ CONF_POWER_FACTOR = "power_factor" CONF_PRESSURE = "pressure" CONF_REACTIVE_POWER = "reactive_power" CONF_SIGNAL_STRENGTH = "signal_strength" +CONF_SPEED = "speed" CONF_SULPHUR_DIOXIDE = "sulphur_dioxide" CONF_TEMPERATURE = "temperature" CONF_VOLATILE_ORGANIC_COMPOUNDS = "volatile_organic_compounds" @@ -85,6 +86,7 @@ ENTITY_TRIGGERS = { SensorDeviceClass.PRESSURE: [{CONF_TYPE: CONF_PRESSURE}], SensorDeviceClass.REACTIVE_POWER: [{CONF_TYPE: CONF_REACTIVE_POWER}], SensorDeviceClass.SIGNAL_STRENGTH: [{CONF_TYPE: CONF_SIGNAL_STRENGTH}], + SensorDeviceClass.SPEED: [{CONF_TYPE: CONF_SPEED}], SensorDeviceClass.SULPHUR_DIOXIDE: [{CONF_TYPE: CONF_SULPHUR_DIOXIDE}], SensorDeviceClass.TEMPERATURE: [{CONF_TYPE: CONF_TEMPERATURE}], SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS: [ diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 5196dc562df..d7c8ae38c5c 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -34,6 +34,7 @@ from homeassistant.util.unit_conversion import ( EnergyConverter, PowerConverter, PressureConverter, + SpeedConverter, TemperatureConverter, VolumeConverter, ) @@ -62,6 +63,7 @@ UNIT_CONVERTERS: dict[str, type[BaseUnitConverter]] = { SensorDeviceClass.ENERGY: EnergyConverter, SensorDeviceClass.POWER: PowerConverter, SensorDeviceClass.PRESSURE: PressureConverter, + SensorDeviceClass.SPEED: SpeedConverter, SensorDeviceClass.TEMPERATURE: TemperatureConverter, SensorDeviceClass.GAS: VolumeConverter, } diff --git a/homeassistant/components/sensor/strings.json b/homeassistant/components/sensor/strings.json index d79f1035f62..6a371177321 100644 --- a/homeassistant/components/sensor/strings.json +++ b/homeassistant/components/sensor/strings.json @@ -22,6 +22,7 @@ "is_pressure": "Current {entity_name} pressure", "is_reactive_power": "Current {entity_name} reactive power", "is_signal_strength": "Current {entity_name} signal strength", + "is_speed": "Current {entity_name} speed", "is_sulphur_dioxide": "Current {entity_name} sulphur dioxide concentration level", "is_temperature": "Current {entity_name} temperature", "is_current": "Current {entity_name} current", @@ -53,6 +54,7 @@ "pressure": "{entity_name} pressure changes", "reactive_power": "{entity_name} reactive power changes", "signal_strength": "{entity_name} signal strength changes", + "speed": "{entity_name} speed changes", "sulphur_dioxide": "{entity_name} sulphur dioxide concentration changes", "temperature": "{entity_name} temperature changes", "current": "{entity_name} current changes", diff --git a/homeassistant/components/sensor/translations/en.json b/homeassistant/components/sensor/translations/en.json index 7e2223521eb..0c4af7c1c32 100644 --- a/homeassistant/components/sensor/translations/en.json +++ b/homeassistant/components/sensor/translations/en.json @@ -25,6 +25,7 @@ "is_pressure": "Current {entity_name} pressure", "is_reactive_power": "Current {entity_name} reactive power", "is_signal_strength": "Current {entity_name} signal strength", + "is_speed": "Current {entity_name} speed", "is_sulphur_dioxide": "Current {entity_name} sulphur dioxide concentration level", "is_temperature": "Current {entity_name} temperature", "is_value": "Current {entity_name} value", @@ -56,6 +57,7 @@ "pressure": "{entity_name} pressure changes", "reactive_power": "{entity_name} reactive power changes", "signal_strength": "{entity_name} signal strength changes", + "speed": "{entity_name} speed changes", "sulphur_dioxide": "{entity_name} sulphur dioxide concentration changes", "temperature": "{entity_name} temperature changes", "value": "{entity_name} value changes", diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index 4790889951b..095b5c15d27 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -80,6 +80,16 @@ PRESSURE_SENSOR_PA_ATTRIBUTES = { "state_class": "measurement", "unit_of_measurement": "Pa", } +SPEED_SENSOR_KPH_ATTRIBUTES = { + "device_class": "speed", + "state_class": "measurement", + "unit_of_measurement": "km/h", +} +SPEED_SENSOR_MPH_ATTRIBUTES = { + "device_class": "speed", + "state_class": "measurement", + "unit_of_measurement": "mph", +} TEMPERATURE_SENSOR_C_ATTRIBUTES = { "device_class": "temperature", "state_class": "measurement", @@ -159,6 +169,9 @@ async def test_statistics_during_period(hass, hass_ws_client, recorder_mock): (PRESSURE_SENSOR_HPA_ATTRIBUTES, 10, 10, {"pressure": "Pa"}, 1000), (PRESSURE_SENSOR_HPA_ATTRIBUTES, 10, 10, {"pressure": "hPa"}, 10), (PRESSURE_SENSOR_HPA_ATTRIBUTES, 10, 10, {"pressure": "psi"}, 1000 / 6894.757), + (SPEED_SENSOR_KPH_ATTRIBUTES, 10, 10, {"speed": "m/s"}, 2.77778), + (SPEED_SENSOR_KPH_ATTRIBUTES, 10, 10, {"speed": "km/h"}, 10), + (SPEED_SENSOR_KPH_ATTRIBUTES, 10, 10, {"speed": "mph"}, 6.21371), (TEMPERATURE_SENSOR_C_ATTRIBUTES, 10, 10, {"temperature": "°C"}, 10), (TEMPERATURE_SENSOR_C_ATTRIBUTES, 10, 10, {"temperature": "°F"}, 50), (TEMPERATURE_SENSOR_C_ATTRIBUTES, 10, 10, {"temperature": "K"}, 283.15), @@ -564,6 +577,8 @@ async def test_statistics_during_period_bad_end_time( (METRIC_SYSTEM, POWER_SENSOR_KW_ATTRIBUTES, "kW", "W", "power"), (IMPERIAL_SYSTEM, PRESSURE_SENSOR_HPA_ATTRIBUTES, "hPa", "Pa", "pressure"), (METRIC_SYSTEM, PRESSURE_SENSOR_HPA_ATTRIBUTES, "hPa", "Pa", "pressure"), + (IMPERIAL_SYSTEM, SPEED_SENSOR_KPH_ATTRIBUTES, "km/h", "m/s", "speed"), + (METRIC_SYSTEM, SPEED_SENSOR_KPH_ATTRIBUTES, "km/h", "m/s", "speed"), (IMPERIAL_SYSTEM, TEMPERATURE_SENSOR_C_ATTRIBUTES, "°C", "°C", "temperature"), (METRIC_SYSTEM, TEMPERATURE_SENSOR_C_ATTRIBUTES, "°C", "°C", "temperature"), (IMPERIAL_SYSTEM, TEMPERATURE_SENSOR_F_ATTRIBUTES, "°F", "°C", "temperature"), @@ -1357,6 +1372,8 @@ async def test_backup_end_without_start( (METRIC_SYSTEM, POWER_SENSOR_KW_ATTRIBUTES, "W", "power"), (METRIC_SYSTEM, PRESSURE_SENSOR_PA_ATTRIBUTES, "Pa", "pressure"), (METRIC_SYSTEM, PRESSURE_SENSOR_HPA_ATTRIBUTES, "Pa", "pressure"), + (METRIC_SYSTEM, SPEED_SENSOR_KPH_ATTRIBUTES, "m/s", "speed"), + (METRIC_SYSTEM, SPEED_SENSOR_MPH_ATTRIBUTES, "m/s", "speed"), (METRIC_SYSTEM, TEMPERATURE_SENSOR_C_ATTRIBUTES, "°C", "temperature"), (METRIC_SYSTEM, TEMPERATURE_SENSOR_F_ATTRIBUTES, "°C", "temperature"), ], diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index ad672b56801..47cfc0ef148 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -16,6 +16,10 @@ from homeassistant.const import ( PRESSURE_INHG, PRESSURE_KPA, PRESSURE_MMHG, + SPEED_INCHES_PER_HOUR, + SPEED_KILOMETERS_PER_HOUR, + SPEED_MILES_PER_HOUR, + SPEED_MILLIMETERS_PER_DAY, STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT, @@ -520,6 +524,31 @@ async def test_custom_unit( 1000, SensorDeviceClass.PRESSURE, ), + # Speed + ( + SPEED_KILOMETERS_PER_HOUR, + SPEED_MILES_PER_HOUR, + SPEED_MILES_PER_HOUR, + 100, + 62, + SensorDeviceClass.SPEED, + ), + ( + SPEED_MILLIMETERS_PER_DAY, + SPEED_INCHES_PER_HOUR, + SPEED_INCHES_PER_HOUR, + 78, + 0.13, + SensorDeviceClass.SPEED, + ), + ( + SPEED_KILOMETERS_PER_HOUR, + "peer_distance", + SPEED_KILOMETERS_PER_HOUR, + 100, + 100, + SensorDeviceClass.SPEED, + ), ], ) async def test_custom_unit_change( diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 1bb95888e92..ad11206f583 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -94,6 +94,8 @@ def set_time_zone(): ("pressure", "mbar", "mbar", "Pa", "pressure", 13.050847, -10, 30), ("pressure", "inHg", "inHg", "Pa", "pressure", 13.050847, -10, 30), ("pressure", "psi", "psi", "Pa", "pressure", 13.050847, -10, 30), + ("speed", "m/s", "m/s", "m/s", "speed", 13.050847, -10, 30), + ("speed", "mph", "mph", "m/s", "speed", 13.050847, -10, 30), ("temperature", "°C", "°C", "°C", "temperature", 13.050847, -10, 30), ("temperature", "°F", "°F", "°C", "temperature", 13.050847, -10, 30), ], @@ -1563,6 +1565,8 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): ("pressure", "mbar", 30), ("pressure", "inHg", 30), ("pressure", "psi", 30), + ("speed", "m/s", 30), + ("speed", "mph", 30), ("temperature", "°C", 30), ("temperature", "°F", 30), ], @@ -1652,6 +1656,8 @@ def test_compile_hourly_statistics_partially_unavailable(hass_recorder, caplog): ("pressure", "mbar", 30), ("pressure", "inHg", 30), ("pressure", "psi", 30), + ("speed", "m/s", 30), + ("speed", "mph", 30), ("temperature", "°C", 30), ("temperature", "°F", 30), ], @@ -1741,6 +1747,8 @@ def test_compile_hourly_statistics_fails(hass_recorder, caplog): ("measurement", "pressure", "mbar", "mbar", "Pa", "pressure", "mean"), ("measurement", "pressure", "inHg", "inHg", "Pa", "pressure", "mean"), ("measurement", "pressure", "psi", "psi", "Pa", "pressure", "mean"), + ("measurement", "speed", "m/s", "m/s", "m/s", "speed", "mean"), + ("measurement", "speed", "mph", "mph", "m/s", "speed", "mean"), ("measurement", "temperature", "°C", "°C", "°C", "temperature", "mean"), ("measurement", "temperature", "°F", "°F", "°C", "temperature", "mean"), ], From d8a15f3ddadaad8cb676d763f2504e6a259eed5d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 27 Sep 2022 19:22:52 +0200 Subject: [PATCH 775/955] Fix Withings authentication to leverage default redirect URI (#79158) --- homeassistant/components/withings/common.py | 8 -------- tests/components/withings/common.py | 4 ++-- tests/components/withings/conftest.py | 5 ++++- 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/withings/common.py b/homeassistant/components/withings/common.py index 68238791f48..429360b272d 100644 --- a/homeassistant/components/withings/common.py +++ b/homeassistant/components/withings/common.py @@ -44,12 +44,10 @@ from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_entry_oauth2_flow, entity_registry as er from homeassistant.helpers.config_entry_oauth2_flow import ( - AUTH_CALLBACK_PATH, AbstractOAuth2Implementation, OAuth2Session, ) from homeassistant.helpers.entity import Entity -from homeassistant.helpers.network import get_url from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.util import dt @@ -1084,12 +1082,6 @@ def get_platform_attributes(platform: str) -> tuple[WithingsAttribute, ...]: class WithingsLocalOAuth2Implementation(AuthImplementation): """Oauth2 implementation that only uses the external url.""" - @property - def redirect_uri(self) -> str: - """Return the redirect uri.""" - url = get_url(self.hass, allow_internal=False, prefer_cloud=True) - return f"{url}{AUTH_CALLBACK_PATH}" - async def _token_request(self, data: dict) -> dict: """Make a token request and adapt Withings API reply.""" new_token = await super()._token_request(data) diff --git a/tests/components/withings/common.py b/tests/components/withings/common.py index f3855ae96f0..598e7ab8c33 100644 --- a/tests/components/withings/common.py +++ b/tests/components/withings/common.py @@ -201,14 +201,14 @@ class ComponentFactory: self._hass, { "flow_id": result["flow_id"], - "redirect_uri": "http://127.0.0.1:8080/auth/external/callback", + "redirect_uri": "https://example.com/auth/external/callback", }, ) assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP assert result["url"] == ( "https://account.withings.com/oauth2_user/authorize2?" f"response_type=code&client_id={self._client_id}&" - "redirect_uri=http://127.0.0.1:8080/auth/external/callback&" + "redirect_uri=https://example.com/auth/external/callback&" f"state={state}" "&scope=user.info,user.metrics,user.activity,user.sleepevents" ) diff --git a/tests/components/withings/conftest.py b/tests/components/withings/conftest.py index 787a2ee4cb0..ae4c07410c0 100644 --- a/tests/components/withings/conftest.py +++ b/tests/components/withings/conftest.py @@ -13,7 +13,10 @@ from tests.test_util.aiohttp import AiohttpClientMocker @pytest.fixture() def component_factory( - hass: HomeAssistant, hass_client_no_auth, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + hass_client_no_auth, + aioclient_mock: AiohttpClientMocker, + current_request_with_host: None, ): """Return a factory for initializing the withings component.""" with patch( From 12e4d18038559d1df75ddc43a4ab79a219dab2be Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 27 Sep 2022 18:37:52 +0100 Subject: [PATCH 776/955] Add volume to SensorDeviceClass (#77960) * Add volume to SensorDeviceClass * Adjust recorder * Adjust tests * Adjust sensor UNIT_CONVERTERS * Adjust recorder * Update strings.json --- .../components/recorder/websocket_api.py | 10 ++++--- homeassistant/components/sensor/__init__.py | 6 ++++ .../components/sensor/device_condition.py | 4 ++- .../components/sensor/device_trigger.py | 4 ++- homeassistant/components/sensor/recorder.py | 3 +- homeassistant/components/sensor/strings.json | 22 +++++++------- .../components/sensor/translations/en.json | 6 ++-- .../components/recorder/test_websocket_api.py | 30 +++++++++++++++++++ tests/components/sensor/test_init.py | 29 ++++++++++++++++++ tests/components/sensor/test_recorder.py | 22 +++++++++++--- 10 files changed, 113 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/recorder/websocket_api.py b/homeassistant/components/recorder/websocket_api.py index 2ede11cf887..5feb51000fb 100644 --- a/homeassistant/components/recorder/websocket_api.py +++ b/homeassistant/components/recorder/websocket_api.py @@ -13,8 +13,6 @@ from homeassistant.const import ( ENERGY_KILO_WATT_HOUR, ENERGY_MEGA_WATT_HOUR, ENERGY_WATT_HOUR, - VOLUME_CUBIC_FEET, - VOLUME_CUBIC_METERS, ) from homeassistant.core import HomeAssistant, callback, valid_entity_id from homeassistant.helpers import config_validation as cv @@ -27,6 +25,7 @@ from homeassistant.util.unit_conversion import ( PressureConverter, SpeedConverter, TemperatureConverter, + VolumeConverter, ) from .const import MAX_QUEUE_BACKLOG @@ -131,7 +130,7 @@ async def ws_handle_get_statistics_during_period( 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.Any(VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS), + vol.Optional("volume"): vol.In(VolumeConverter.VALID_UNITS), } ), } @@ -336,7 +335,10 @@ async def ws_adjust_sum_statistics( ENERGY_WATT_HOUR, ): return True - if statistics_unit == VOLUME_CUBIC_METERS and display_unit == VOLUME_CUBIC_FEET: + if ( + statistics_unit == VolumeConverter.NORMALIZED_UNIT + and display_unit in VolumeConverter.VALID_UNITS + ): return True return False diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 1babc2d0084..c7b8e9a4940 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -64,6 +64,7 @@ from homeassistant.util.unit_conversion import ( PressureConverter, SpeedConverter, TemperatureConverter, + VolumeConverter, ) from .const import CONF_STATE_CLASS # noqa: F401 @@ -185,6 +186,9 @@ class SensorDeviceClass(StrEnum): # voltage (V) VOLTAGE = "voltage" + # volume (VOLUME_*) + VOLUME = "volume" + DEVICE_CLASSES_SCHEMA: Final = vol.All(vol.Lower, vol.Coerce(SensorDeviceClass)) @@ -221,6 +225,7 @@ UNIT_CONVERTERS: dict[str, type[BaseUnitConverter]] = { SensorDeviceClass.PRESSURE: PressureConverter, SensorDeviceClass.SPEED: SpeedConverter, SensorDeviceClass.TEMPERATURE: TemperatureConverter, + SensorDeviceClass.VOLUME: VolumeConverter, } UNIT_RATIOS: dict[str, dict[str, float]] = { @@ -232,6 +237,7 @@ UNIT_RATIOS: dict[str, dict[str, float]] = { TEMP_FAHRENHEIT: 1.8, TEMP_KELVIN: 1.0, }, + SensorDeviceClass.VOLUME: VolumeConverter.UNIT_CONVERSION, } # mypy: disallow-any-generics diff --git a/homeassistant/components/sensor/device_condition.py b/homeassistant/components/sensor/device_condition.py index 08aeda46ba2..72ef4a62c48 100644 --- a/homeassistant/components/sensor/device_condition.py +++ b/homeassistant/components/sensor/device_condition.py @@ -58,9 +58,10 @@ CONF_IS_REACTIVE_POWER = "is_reactive_power" CONF_IS_SIGNAL_STRENGTH = "is_signal_strength" CONF_IS_SULPHUR_DIOXIDE = "is_sulphur_dioxide" CONF_IS_TEMPERATURE = "is_temperature" +CONF_IS_VALUE = "is_value" CONF_IS_VOLATILE_ORGANIC_COMPOUNDS = "is_volatile_organic_compounds" CONF_IS_VOLTAGE = "is_voltage" -CONF_IS_VALUE = "is_value" +CONF_IS_VOLUME = "is_volume" ENTITY_CONDITIONS = { SensorDeviceClass.APPARENT_POWER: [{CONF_TYPE: CONF_IS_APPARENT_POWER}], @@ -94,6 +95,7 @@ ENTITY_CONDITIONS = { {CONF_TYPE: CONF_IS_VOLATILE_ORGANIC_COMPOUNDS} ], SensorDeviceClass.VOLTAGE: [{CONF_TYPE: CONF_IS_VOLTAGE}], + SensorDeviceClass.VOLUME: [{CONF_TYPE: CONF_IS_VOLUME}], DEVICE_CLASS_NONE: [{CONF_TYPE: CONF_IS_VALUE}], } diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py index a1275b202ed..a2b92186410 100644 --- a/homeassistant/components/sensor/device_trigger.py +++ b/homeassistant/components/sensor/device_trigger.py @@ -57,9 +57,10 @@ CONF_SIGNAL_STRENGTH = "signal_strength" CONF_SPEED = "speed" CONF_SULPHUR_DIOXIDE = "sulphur_dioxide" CONF_TEMPERATURE = "temperature" +CONF_VALUE = "value" CONF_VOLATILE_ORGANIC_COMPOUNDS = "volatile_organic_compounds" CONF_VOLTAGE = "voltage" -CONF_VALUE = "value" +CONF_VOLUME = "volume" ENTITY_TRIGGERS = { SensorDeviceClass.APPARENT_POWER: [{CONF_TYPE: CONF_APPARENT_POWER}], @@ -93,6 +94,7 @@ ENTITY_TRIGGERS = { {CONF_TYPE: CONF_VOLATILE_ORGANIC_COMPOUNDS} ], SensorDeviceClass.VOLTAGE: [{CONF_TYPE: CONF_VOLTAGE}], + SensorDeviceClass.VOLUME: [{CONF_TYPE: CONF_VOLUME}], DEVICE_CLASS_NONE: [{CONF_TYPE: CONF_VALUE}], } diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index d7c8ae38c5c..564a5226f58 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -61,11 +61,12 @@ DEFAULT_STATISTICS = { UNIT_CONVERTERS: dict[str, type[BaseUnitConverter]] = { SensorDeviceClass.DISTANCE: DistanceConverter, SensorDeviceClass.ENERGY: EnergyConverter, + SensorDeviceClass.GAS: VolumeConverter, SensorDeviceClass.POWER: PowerConverter, SensorDeviceClass.PRESSURE: PressureConverter, SensorDeviceClass.SPEED: SpeedConverter, SensorDeviceClass.TEMPERATURE: TemperatureConverter, - SensorDeviceClass.GAS: VolumeConverter, + SensorDeviceClass.VOLUME: VolumeConverter, } # Keep track of entities for which a warning about decreasing value has been logged diff --git a/homeassistant/components/sensor/strings.json b/homeassistant/components/sensor/strings.json index 6a371177321..affc1a8e3e9 100644 --- a/homeassistant/components/sensor/strings.json +++ b/homeassistant/components/sensor/strings.json @@ -6,7 +6,10 @@ "is_battery_level": "Current {entity_name} battery level", "is_carbon_monoxide": "Current {entity_name} carbon monoxide concentration level", "is_carbon_dioxide": "Current {entity_name} carbon dioxide concentration level", + "is_current": "Current {entity_name} current", "is_distance": "Current {entity_name} distance", + "is_energy": "Current {entity_name} energy", + "is_frequency": "Current {entity_name} frequency", "is_gas": "Current {entity_name} gas", "is_humidity": "Current {entity_name} humidity", "is_illuminance": "Current {entity_name} illuminance", @@ -19,26 +22,27 @@ "is_pm10": "Current {entity_name} PM10 concentration level", "is_pm25": "Current {entity_name} PM2.5 concentration level", "is_power": "Current {entity_name} power", + "is_power_factor": "Current {entity_name} power factor", "is_pressure": "Current {entity_name} pressure", "is_reactive_power": "Current {entity_name} reactive power", "is_signal_strength": "Current {entity_name} signal strength", "is_speed": "Current {entity_name} speed", "is_sulphur_dioxide": "Current {entity_name} sulphur dioxide concentration level", "is_temperature": "Current {entity_name} temperature", - "is_current": "Current {entity_name} current", - "is_energy": "Current {entity_name} energy", - "is_frequency": "Current {entity_name} frequency", - "is_power_factor": "Current {entity_name} power factor", + "is_value": "Current {entity_name} value", "is_volatile_organic_compounds": "Current {entity_name} volatile organic compounds concentration level", "is_voltage": "Current {entity_name} voltage", - "is_value": "Current {entity_name} value" + "is_volume": "Current {entity_name} volume" }, "trigger_type": { "apparent_power": "{entity_name} apparent power changes", "battery_level": "{entity_name} battery level changes", "carbon_monoxide": "{entity_name} carbon monoxide concentration changes", "carbon_dioxide": "{entity_name} carbon dioxide concentration changes", + "current": "{entity_name} current changes", "distance": "{entity_name} distance changes", + "energy": "{entity_name} energy changes", + "frequency": "{entity_name} frequency changes", "gas": "{entity_name} gas changes", "humidity": "{entity_name} humidity changes", "illuminance": "{entity_name} illuminance changes", @@ -51,19 +55,17 @@ "pm10": "{entity_name} PM10 concentration changes", "pm25": "{entity_name} PM2.5 concentration changes", "power": "{entity_name} power changes", + "power_factor": "{entity_name} power factor changes", "pressure": "{entity_name} pressure changes", "reactive_power": "{entity_name} reactive power changes", "signal_strength": "{entity_name} signal strength changes", "speed": "{entity_name} speed changes", "sulphur_dioxide": "{entity_name} sulphur dioxide concentration changes", "temperature": "{entity_name} temperature changes", - "current": "{entity_name} current changes", - "energy": "{entity_name} energy changes", - "frequency": "{entity_name} frequency changes", - "power_factor": "{entity_name} power factor changes", + "value": "{entity_name} value changes", "volatile_organic_compounds": "{entity_name} volatile organic compounds concentration changes", "voltage": "{entity_name} voltage changes", - "value": "{entity_name} value changes" + "volume": "{entity_name} volume changes" } }, "state": { diff --git a/homeassistant/components/sensor/translations/en.json b/homeassistant/components/sensor/translations/en.json index 0c4af7c1c32..fd801bd1416 100644 --- a/homeassistant/components/sensor/translations/en.json +++ b/homeassistant/components/sensor/translations/en.json @@ -30,7 +30,8 @@ "is_temperature": "Current {entity_name} temperature", "is_value": "Current {entity_name} value", "is_volatile_organic_compounds": "Current {entity_name} volatile organic compounds concentration level", - "is_voltage": "Current {entity_name} voltage" + "is_voltage": "Current {entity_name} voltage", + "is_volume": "Current {entity_name} volume" }, "trigger_type": { "apparent_power": "{entity_name} apparent power changes", @@ -62,7 +63,8 @@ "temperature": "{entity_name} temperature changes", "value": "{entity_name} value changes", "volatile_organic_compounds": "{entity_name} volatile organic compounds concentration changes", - "voltage": "{entity_name} voltage changes" + "voltage": "{entity_name} voltage changes", + "volume": "{entity_name} volume changes" } }, "state": { diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index 095b5c15d27..e8d8093e131 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -100,6 +100,26 @@ TEMPERATURE_SENSOR_F_ATTRIBUTES = { "state_class": "measurement", "unit_of_measurement": "°F", } +VOLUME_SENSOR_FT3_ATTRIBUTES = { + "device_class": "volume", + "state_class": "measurement", + "unit_of_measurement": "ft³", +} +VOLUME_SENSOR_M3_ATTRIBUTES = { + "device_class": "volume", + "state_class": "measurement", + "unit_of_measurement": "m³", +} +VOLUME_SENSOR_FT3_ATTRIBUTES_TOTAL = { + "device_class": "volume", + "state_class": "total", + "unit_of_measurement": "ft³", +} +VOLUME_SENSOR_M3_ATTRIBUTES_TOTAL = { + "device_class": "volume", + "state_class": "total", + "unit_of_measurement": "m³", +} async def test_statistics_during_period(hass, hass_ws_client, recorder_mock): @@ -175,6 +195,8 @@ async def test_statistics_during_period(hass, hass_ws_client, recorder_mock): (TEMPERATURE_SENSOR_C_ATTRIBUTES, 10, 10, {"temperature": "°C"}, 10), (TEMPERATURE_SENSOR_C_ATTRIBUTES, 10, 10, {"temperature": "°F"}, 50), (TEMPERATURE_SENSOR_C_ATTRIBUTES, 10, 10, {"temperature": "K"}, 283.15), + (VOLUME_SENSOR_M3_ATTRIBUTES, 10, 10, {"volume": "m³"}, 10), + (VOLUME_SENSOR_M3_ATTRIBUTES, 10, 10, {"volume": "ft³"}, 353.14666), ], ) async def test_statistics_during_period_unit_conversion( @@ -266,6 +288,8 @@ async def test_statistics_during_period_unit_conversion( (ENERGY_SENSOR_KWH_ATTRIBUTES, 10, 10, {"energy": "Wh"}, 10000), (GAS_SENSOR_M3_ATTRIBUTES, 10, 10, {"volume": "m³"}, 10), (GAS_SENSOR_M3_ATTRIBUTES, 10, 10, {"volume": "ft³"}, 353.147), + (VOLUME_SENSOR_M3_ATTRIBUTES_TOTAL, 10, 10, {"volume": "m³"}, 10), + (VOLUME_SENSOR_M3_ATTRIBUTES_TOTAL, 10, 10, {"volume": "ft³"}, 353.147), ], ) async def test_sum_statistics_during_period_unit_conversion( @@ -583,6 +607,10 @@ async def test_statistics_during_period_bad_end_time( (METRIC_SYSTEM, TEMPERATURE_SENSOR_C_ATTRIBUTES, "°C", "°C", "temperature"), (IMPERIAL_SYSTEM, TEMPERATURE_SENSOR_F_ATTRIBUTES, "°F", "°C", "temperature"), (METRIC_SYSTEM, TEMPERATURE_SENSOR_F_ATTRIBUTES, "°F", "°C", "temperature"), + (IMPERIAL_SYSTEM, VOLUME_SENSOR_FT3_ATTRIBUTES, "ft³", "m³", "volume"), + (METRIC_SYSTEM, VOLUME_SENSOR_FT3_ATTRIBUTES, "ft³", "m³", "volume"), + (IMPERIAL_SYSTEM, VOLUME_SENSOR_FT3_ATTRIBUTES_TOTAL, "ft³", "m³", "volume"), + (METRIC_SYSTEM, VOLUME_SENSOR_FT3_ATTRIBUTES_TOTAL, "ft³", "m³", "volume"), ], ) async def test_list_statistic_ids( @@ -1376,6 +1404,8 @@ async def test_backup_end_without_start( (METRIC_SYSTEM, SPEED_SENSOR_MPH_ATTRIBUTES, "m/s", "speed"), (METRIC_SYSTEM, TEMPERATURE_SENSOR_C_ATTRIBUTES, "°C", "temperature"), (METRIC_SYSTEM, TEMPERATURE_SENSOR_F_ATTRIBUTES, "°C", "temperature"), + (METRIC_SYSTEM, VOLUME_SENSOR_FT3_ATTRIBUTES, "m³", "volume"), + (METRIC_SYSTEM, VOLUME_SENSOR_M3_ATTRIBUTES, "m³", "volume"), ], ) async def test_get_statistics_metadata( diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index 47cfc0ef148..f9b90dc5bd5 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -23,6 +23,10 @@ from homeassistant.const import ( STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT, + VOLUME_CUBIC_FEET, + VOLUME_CUBIC_METERS, + VOLUME_FLUID_OUNCE, + VOLUME_LITERS, ) from homeassistant.core import State from homeassistant.helpers import entity_registry as er @@ -549,6 +553,31 @@ async def test_custom_unit( 100, SensorDeviceClass.SPEED, ), + # Volume + ( + VOLUME_CUBIC_METERS, + VOLUME_CUBIC_FEET, + VOLUME_CUBIC_FEET, + 100, + 3531, + SensorDeviceClass.VOLUME, + ), + ( + VOLUME_FLUID_OUNCE, + VOLUME_LITERS, + VOLUME_LITERS, + 78, + 2.3, + SensorDeviceClass.VOLUME, + ), + ( + VOLUME_CUBIC_METERS, + "peer_distance", + VOLUME_CUBIC_METERS, + 100, + 100, + SensorDeviceClass.VOLUME, + ), ], ) async def test_custom_unit_change( diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index ad11206f583..3d4f48360fc 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -98,6 +98,8 @@ def set_time_zone(): ("speed", "mph", "mph", "m/s", "speed", 13.050847, -10, 30), ("temperature", "°C", "°C", "°C", "temperature", 13.050847, -10, 30), ("temperature", "°F", "°F", "°C", "temperature", 13.050847, -10, 30), + ("volume", "m³", "m³", "m³", "volume", 13.050847, -10, 30), + ("volume", "ft³", "ft³", "m³", "volume", 13.050847, -10, 30), ], ) def test_compile_hourly_statistics( @@ -359,18 +361,22 @@ def test_compile_hourly_statistics_unsupported(hass_recorder, caplog, attributes (IMPERIAL_SYSTEM, "distance", "mi", "mi", "m", "distance", 1), (IMPERIAL_SYSTEM, "energy", "kWh", "kWh", "kWh", "energy", 1), (IMPERIAL_SYSTEM, "energy", "Wh", "Wh", "kWh", "energy", 1), - (IMPERIAL_SYSTEM, "monetary", "EUR", "EUR", "EUR", None, 1), - (IMPERIAL_SYSTEM, "monetary", "SEK", "SEK", "SEK", None, 1), (IMPERIAL_SYSTEM, "gas", "m³", "m³", "m³", "volume", 1), (IMPERIAL_SYSTEM, "gas", "ft³", "ft³", "m³", "volume", 1), + (IMPERIAL_SYSTEM, "monetary", "EUR", "EUR", "EUR", None, 1), + (IMPERIAL_SYSTEM, "monetary", "SEK", "SEK", "SEK", None, 1), + (IMPERIAL_SYSTEM, "volume", "m³", "m³", "m³", "volume", 1), + (IMPERIAL_SYSTEM, "volume", "ft³", "ft³", "m³", "volume", 1), (METRIC_SYSTEM, "distance", "m", "m", "m", "distance", 1), (METRIC_SYSTEM, "distance", "mi", "mi", "m", "distance", 1), (METRIC_SYSTEM, "energy", "kWh", "kWh", "kWh", "energy", 1), (METRIC_SYSTEM, "energy", "Wh", "Wh", "kWh", "energy", 1), - (METRIC_SYSTEM, "monetary", "EUR", "EUR", "EUR", None, 1), - (METRIC_SYSTEM, "monetary", "SEK", "SEK", "SEK", None, 1), (METRIC_SYSTEM, "gas", "m³", "m³", "m³", "volume", 1), (METRIC_SYSTEM, "gas", "ft³", "ft³", "m³", "volume", 1), + (METRIC_SYSTEM, "monetary", "EUR", "EUR", "EUR", None, 1), + (METRIC_SYSTEM, "monetary", "SEK", "SEK", "SEK", None, 1), + (METRIC_SYSTEM, "volume", "m³", "m³", "m³", "volume", 1), + (METRIC_SYSTEM, "volume", "ft³", "ft³", "m³", "volume", 1), ], ) async def test_compile_hourly_sum_statistics_amount( @@ -1569,6 +1575,8 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): ("speed", "mph", 30), ("temperature", "°C", 30), ("temperature", "°F", 30), + ("volume", "m³", 30), + ("volume", "ft³", 30), ], ) def test_compile_hourly_statistics_unchanged( @@ -1660,6 +1668,8 @@ def test_compile_hourly_statistics_partially_unavailable(hass_recorder, caplog): ("speed", "mph", 30), ("temperature", "°C", 30), ("temperature", "°F", 30), + ("volume", "m³", 30), + ("volume", "ft³", 30), ], ) def test_compile_hourly_statistics_unavailable( @@ -1751,6 +1761,10 @@ def test_compile_hourly_statistics_fails(hass_recorder, caplog): ("measurement", "speed", "mph", "mph", "m/s", "speed", "mean"), ("measurement", "temperature", "°C", "°C", "°C", "temperature", "mean"), ("measurement", "temperature", "°F", "°F", "°C", "temperature", "mean"), + ("measurement", "volume", "m³", "m³", "m³", "volume", "mean"), + ("measurement", "volume", "ft³", "ft³", "m³", "volume", "mean"), + ("total", "volume", "m³", "m³", "m³", "volume", "sum"), + ("total", "volume", "ft³", "ft³", "m³", "volume", "sum"), ], ) def test_list_statistic_ids( From d991c173a274efd76ff11327d06fa7253eb9a32e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 27 Sep 2022 07:49:34 -1000 Subject: [PATCH 777/955] Add new distance device class to iBeacons (#79162) --- homeassistant/components/ibeacon/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/ibeacon/sensor.py b/homeassistant/components/ibeacon/sensor.py index 36a7917a9e6..4684efdf142 100644 --- a/homeassistant/components/ibeacon/sensor.py +++ b/homeassistant/components/ibeacon/sensor.py @@ -61,6 +61,7 @@ SENSOR_DESCRIPTIONS = ( native_unit_of_measurement=LENGTH_METERS, value_fn=lambda ibeacon_advertisement: ibeacon_advertisement.distance, state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.DISTANCE, ), ) From d11916758c420580a137ca569da3b1bc96c01041 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 27 Sep 2022 07:51:42 -1000 Subject: [PATCH 778/955] Handle timeout on pairing close in HomeKit Controller (#79133) --- homeassistant/components/homekit_controller/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index dca626d2abd..dac4afc0b22 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +import contextlib import logging import aiohomekit @@ -41,7 +42,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await conn.async_setup() except (AccessoryNotFoundError, EncryptionError, AccessoryDisconnectedError) as ex: del hass.data[KNOWN_DEVICES][conn.unique_id] - await conn.pairing.close() + with contextlib.suppress(asyncio.TimeoutError): + await conn.pairing.close() raise ConfigEntryNotReady from ex return True From 8eaa22cf90d4c701972d18100c2fec290a21ea97 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 27 Sep 2022 07:56:00 -1000 Subject: [PATCH 779/955] Break out esphome domain data (#79134) --- .coveragerc | 1 + homeassistant/components/esphome/__init__.py | 55 +-------------- .../components/esphome/domain_data.py | 68 +++++++++++++++++++ 3 files changed, 70 insertions(+), 54 deletions(-) create mode 100644 homeassistant/components/esphome/domain_data.py diff --git a/.coveragerc b/.coveragerc index 98992bed247..5c5d88b42c4 100644 --- a/.coveragerc +++ b/.coveragerc @@ -331,6 +331,7 @@ omit = homeassistant/components/esphome/camera.py homeassistant/components/esphome/climate.py homeassistant/components/esphome/cover.py + homeassistant/components/esphome/domain_data.py homeassistant/components/esphome/entry_data.py homeassistant/components/esphome/fan.py homeassistant/components/esphome/light.py diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 07b6d3071f6..acf8d33b6e0 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -2,7 +2,6 @@ from __future__ import annotations from collections.abc import Callable -from dataclasses import dataclass, field import functools import logging import math @@ -47,70 +46,18 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo, Entity, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event -from homeassistant.helpers.json import JSONEncoder from homeassistant.helpers.service import async_set_service_schema -from homeassistant.helpers.storage import Store from homeassistant.helpers.template import Template from .bluetooth import async_connect_scanner +from .domain_data import DOMAIN, DomainData # Import config flow so that it's added to the registry from .entry_data import RuntimeEntryData -DOMAIN = "esphome" CONF_NOISE_PSK = "noise_psk" _LOGGER = logging.getLogger(__name__) _R = TypeVar("_R") -_DomainDataSelfT = TypeVar("_DomainDataSelfT", bound="DomainData") - -STORAGE_VERSION = 1 - - -@dataclass -class DomainData: - """Define a class that stores global esphome data in hass.data[DOMAIN].""" - - _entry_datas: dict[str, RuntimeEntryData] = field(default_factory=dict) - _stores: dict[str, Store] = field(default_factory=dict) - - def get_entry_data(self, entry: ConfigEntry) -> RuntimeEntryData: - """Return the runtime entry data associated with this config entry. - - Raises KeyError if the entry isn't loaded yet. - """ - return self._entry_datas[entry.entry_id] - - def set_entry_data(self, entry: ConfigEntry, entry_data: RuntimeEntryData) -> None: - """Set the runtime entry data associated with this config entry.""" - if entry.entry_id in self._entry_datas: - raise ValueError("Entry data for this entry is already set") - self._entry_datas[entry.entry_id] = entry_data - - def pop_entry_data(self, entry: ConfigEntry) -> RuntimeEntryData: - """Pop the runtime entry data instance associated with this config entry.""" - return self._entry_datas.pop(entry.entry_id) - - def is_entry_loaded(self, entry: ConfigEntry) -> bool: - """Check whether the given entry is loaded.""" - return entry.entry_id in self._entry_datas - - def get_or_create_store(self, hass: HomeAssistant, entry: ConfigEntry) -> Store: - """Get or create a Store instance for the given config entry.""" - return self._stores.setdefault( - entry.entry_id, - Store( - hass, STORAGE_VERSION, f"esphome.{entry.entry_id}", encoder=JSONEncoder - ), - ) - - @classmethod - def get(cls: type[_DomainDataSelfT], hass: HomeAssistant) -> _DomainDataSelfT: - """Get the global DomainData instance stored in hass.data.""" - # Don't use setdefault - this is a hot code path - if DOMAIN in hass.data: - return cast(_DomainDataSelfT, hass.data[DOMAIN]) - ret = hass.data[DOMAIN] = cls() - return ret async def async_setup_entry( # noqa: C901 diff --git a/homeassistant/components/esphome/domain_data.py b/homeassistant/components/esphome/domain_data.py new file mode 100644 index 00000000000..9fabcf17d78 --- /dev/null +++ b/homeassistant/components/esphome/domain_data.py @@ -0,0 +1,68 @@ +"""Support for esphome domain data.""" +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import TypeVar, cast + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.json import JSONEncoder +from homeassistant.helpers.storage import Store + +from .entry_data import RuntimeEntryData + +STORAGE_VERSION = 1 +DOMAIN = "esphome" +_DomainDataSelfT = TypeVar("_DomainDataSelfT", bound="DomainData") + + +@dataclass +class DomainData: + """Define a class that stores global esphome data in hass.data[DOMAIN].""" + + _entry_datas: dict[str, RuntimeEntryData] = field(default_factory=dict) + _stores: dict[str, Store] = field(default_factory=dict) + _entry_by_unique_id: dict[str, ConfigEntry] = field(default_factory=dict) + + def get_entry_data(self, entry: ConfigEntry) -> RuntimeEntryData: + """Return the runtime entry data associated with this config entry. + + Raises KeyError if the entry isn't loaded yet. + """ + return self._entry_datas[entry.entry_id] + + def set_entry_data(self, entry: ConfigEntry, entry_data: RuntimeEntryData) -> None: + """Set the runtime entry data associated with this config entry.""" + if entry.entry_id in self._entry_datas: + raise ValueError("Entry data for this entry is already set") + self._entry_datas[entry.entry_id] = entry_data + if entry.unique_id: + self._entry_by_unique_id[entry.unique_id] = entry + + def pop_entry_data(self, entry: ConfigEntry) -> RuntimeEntryData: + """Pop the runtime entry data instance associated with this config entry.""" + if entry.unique_id: + del self._entry_by_unique_id[entry.unique_id] + return self._entry_datas.pop(entry.entry_id) + + def is_entry_loaded(self, entry: ConfigEntry) -> bool: + """Check whether the given entry is loaded.""" + return entry.entry_id in self._entry_datas + + def get_or_create_store(self, hass: HomeAssistant, entry: ConfigEntry) -> Store: + """Get or create a Store instance for the given config entry.""" + return self._stores.setdefault( + entry.entry_id, + Store( + hass, STORAGE_VERSION, f"esphome.{entry.entry_id}", encoder=JSONEncoder + ), + ) + + @classmethod + def get(cls: type[_DomainDataSelfT], hass: HomeAssistant) -> _DomainDataSelfT: + """Get the global DomainData instance stored in hass.data.""" + # Don't use setdefault - this is a hot code path + if DOMAIN in hass.data: + return cast(_DomainDataSelfT, hass.data[DOMAIN]) + ret = hass.data[DOMAIN] = cls() + return ret From 5eb50f63fdbd1dce3dc9a23b92103ca5a50d8c85 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 27 Sep 2022 08:06:10 -1000 Subject: [PATCH 780/955] Ensure bleak_retry_connector uses HaBleakClientWrapper (#79132) --- homeassistant/components/bluetooth/usage.py | 19 +++++++++++++ tests/components/bluetooth/test_usage.py | 30 +++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/homeassistant/components/bluetooth/usage.py b/homeassistant/components/bluetooth/usage.py index d282ca7415b..ba174f0306a 100644 --- a/homeassistant/components/bluetooth/usage.py +++ b/homeassistant/components/bluetooth/usage.py @@ -3,20 +3,39 @@ from __future__ import annotations import bleak +from bleak.backends.service import BleakGATTServiceCollection +import bleak_retry_connector from .models import HaBleakClientWrapper, HaBleakScannerWrapper ORIGINAL_BLEAK_SCANNER = bleak.BleakScanner ORIGINAL_BLEAK_CLIENT = bleak.BleakClient +ORIGINAL_BLEAK_RETRY_CONNECTOR_CLIENT = ( + bleak_retry_connector.BleakClientWithServiceCache +) def install_multiple_bleak_catcher() -> None: """Wrap the bleak classes to return the shared instance if multiple instances are detected.""" bleak.BleakScanner = HaBleakScannerWrapper # type: ignore[misc, assignment] bleak.BleakClient = HaBleakClientWrapper # type: ignore[misc] + bleak_retry_connector.BleakClientWithServiceCache = HaBleakClientWithServiceCache # type: ignore[misc,assignment] def uninstall_multiple_bleak_catcher() -> None: """Unwrap the bleak classes.""" bleak.BleakScanner = ORIGINAL_BLEAK_SCANNER # type: ignore[misc] bleak.BleakClient = ORIGINAL_BLEAK_CLIENT # type: ignore[misc] + bleak_retry_connector.BleakClientWithServiceCache = ORIGINAL_BLEAK_RETRY_CONNECTOR_CLIENT # type: ignore[misc] + + +class HaBleakClientWithServiceCache(HaBleakClientWrapper): + """A BleakClient that implements service caching.""" + + def set_cached_services(self, services: BleakGATTServiceCollection | None) -> None: + """Set the cached services. + + No longer used since bleak 0.17+ has service caching built-in. + + This was only kept for backwards compatibility. + """ diff --git a/tests/components/bluetooth/test_usage.py b/tests/components/bluetooth/test_usage.py index 2c6bacfd4cb..1bea3b149cd 100644 --- a/tests/components/bluetooth/test_usage.py +++ b/tests/components/bluetooth/test_usage.py @@ -5,6 +5,7 @@ from unittest.mock import patch import bleak from bleak.backends.device import BLEDevice +import bleak_retry_connector from homeassistant.components.bluetooth.models import ( HaBleakClientWrapper, @@ -75,3 +76,32 @@ async def test_bleak_client_reports_with_address(hass, enable_bluetooth, caplog) assert not isinstance(instance, HaBleakClientWrapper) assert "BleakClient with an address instead of a BLEDevice" not in caplog.text + + +async def test_bleak_retry_connector_client_reports_with_address( + hass, enable_bluetooth, caplog +): + """Test we report when we pass an address to BleakClientWithServiceCache.""" + install_multiple_bleak_catcher() + + with patch.object( + _get_manager(), + "async_ble_device_from_address", + return_value=MOCK_BLE_DEVICE, + ): + instance = bleak_retry_connector.BleakClientWithServiceCache( + "00:00:00:00:00:00" + ) + + assert "BleakClient with an address instead of a BLEDevice" in caplog.text + + assert isinstance(instance, HaBleakClientWrapper) + + uninstall_multiple_bleak_catcher() + + caplog.clear() + + instance = bleak_retry_connector.BleakClientWithServiceCache("00:00:00:00:00:00") + + assert not isinstance(instance, HaBleakClientWrapper) + assert "BleakClient with an address instead of a BLEDevice" not in caplog.text From b043a6ba887e8f925cfa97f3edf66b6f6d7fe4af Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Tue, 27 Sep 2022 11:07:19 -0700 Subject: [PATCH 781/955] Cleanup add browse media forked daapd #79009 (#79157) --- .../components/forked_daapd/browse_media.py | 12 +++++------- .../components/forked_daapd/media_player.py | 2 -- tests/components/forked_daapd/test_browse_media.py | 13 ++++++------- tests/components/forked_daapd/test_config_flow.py | 2 +- tests/components/forked_daapd/test_media_player.py | 4 ++-- 5 files changed, 14 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/forked_daapd/browse_media.py b/homeassistant/components/forked_daapd/browse_media.py index 9ea7104186e..88ca9ad60f8 100644 --- a/homeassistant/components/forked_daapd/browse_media.py +++ b/homeassistant/components/forked_daapd/browse_media.py @@ -55,13 +55,11 @@ OWNTONE_TYPE_TO_MEDIA_TYPE = { } MEDIA_TYPE_TO_OWNTONE_TYPE = {v: k for k, v in OWNTONE_TYPE_TO_MEDIA_TYPE.items()} -""" -media_content_id is a uri in the form of SCHEMA:Title:OwnToneURI:Subtype (Subtype only used for Genre) -OwnToneURI is in format library:type:id (for directories, id is path) -media_content_type - type of item (mostly used to check if playable or can expand) -Owntone type may differ from media_content_type when media_content_type is a directory -Owntown type is used in our own branching, but media_content_type is used for determining playability -""" +# media_content_id is a uri in the form of SCHEMA:Title:OwnToneURI:Subtype (Subtype only used for Genre) +# OwnToneURI is in format library:type:id (for directories, id is path) +# media_content_type - type of item (mostly used to check if playable or can expand) +# Owntone type may differ from media_content_type when media_content_type is a directory +# Owntone type is used in our own branching, but media_content_type is used for determining playability @dataclass diff --git a/homeassistant/components/forked_daapd/media_player.py b/homeassistant/components/forked_daapd/media_player.py index 3229e192884..05d417ac9d9 100644 --- a/homeassistant/components/forked_daapd/media_player.py +++ b/homeassistant/components/forked_daapd/media_player.py @@ -840,8 +840,6 @@ class ForkedDaapdMaster(MediaPlayerEntity): # This is the base level, so we combine our library with the media source return library(ms_result.children) return ms_result - # media_content_type should only be None if media_content_id is None - assert media_content_type return await get_owntone_content(self, media_content_id) async def async_get_browse_image( diff --git a/tests/components/forked_daapd/test_browse_media.py b/tests/components/forked_daapd/test_browse_media.py index 6c7b77b97ea..e90cbbff2aa 100644 --- a/tests/components/forked_daapd/test_browse_media.py +++ b/tests/components/forked_daapd/test_browse_media.py @@ -23,7 +23,7 @@ async def test_async_browse_media(hass, hass_ws_client, config_entry): autospec=True, ) as mock_api: config_entry.add_to_hass(hass) - await config_entry.async_setup(hass) + await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() mock_api.return_value.full_url = lambda x: "http://owntone_instance/" + x @@ -126,7 +126,7 @@ async def test_async_browse_media(hass, hass_ws_client, config_entry): }, ] - # Request playlist through WebSocket + # Request browse root through WebSocket client = await hass_ws_client(hass) await client.send_json( { @@ -148,7 +148,6 @@ async def test_async_browse_media(hass, hass_ws_client, config_entry): nonlocal msg_id for child in children: if child["can_expand"]: - print("EXPANDING CHILD", child) await client.send_json( { "id": msg_id, @@ -177,7 +176,7 @@ async def test_async_browse_media_not_found(hass, hass_ws_client, config_entry): autospec=True, ) as mock_api: config_entry.add_to_hass(hass) - await config_entry.async_setup(hass) + await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() mock_api.return_value.get_directory.return_value = None @@ -186,7 +185,7 @@ async def test_async_browse_media_not_found(hass, hass_ws_client, config_entry): mock_api.return_value.get_genres.return_value = None mock_api.return_value.get_playlists.return_value = None - # Request playlist through WebSocket + # Request different types of media through WebSocket client = await hass_ws_client(hass) msg_id = 1 for media_type in ( @@ -229,7 +228,7 @@ async def test_async_browse_image(hass, hass_client, config_entry): autospec=True, ) as mock_api: config_entry.add_to_hass(hass) - await config_entry.async_setup(hass) + await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() client = await hass_client() mock_api.return_value.full_url = lambda x: "http://owntone_instance/" + x @@ -279,7 +278,7 @@ async def test_async_browse_image_missing(hass, hass_client, config_entry, caplo autospec=True, ) as mock_api: config_entry.add_to_hass(hass) - await config_entry.async_setup(hass) + await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() client = await hass_client() mock_api.return_value.full_url = lambda x: "http://owntone_instance/" + x diff --git a/tests/components/forked_daapd/test_config_flow.py b/tests/components/forked_daapd/test_config_flow.py index e810b0eb957..328f47a0edf 100644 --- a/tests/components/forked_daapd/test_config_flow.py +++ b/tests/components/forked_daapd/test_config_flow.py @@ -225,7 +225,7 @@ async def test_options_flow(hass, config_entry): ) as mock_get_request: mock_get_request.return_value = SAMPLE_CONFIG config_entry.add_to_hass(hass) - await config_entry.async_setup(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) diff --git a/tests/components/forked_daapd/test_media_player.py b/tests/components/forked_daapd/test_media_player.py index 893b6c875e2..05a51e4defa 100644 --- a/tests/components/forked_daapd/test_media_player.py +++ b/tests/components/forked_daapd/test_media_player.py @@ -315,7 +315,7 @@ async def mock_api_object_fixture(hass, config_entry, get_request_return_values) mock_api.return_value.get_pipes.return_value = SAMPLE_PIPES mock_api.return_value.get_playlists.return_value = SAMPLE_PLAYLISTS config_entry.add_to_hass(hass) - await config_entry.async_setup(hass) + await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() mock_api.return_value.start_websocket_handler.assert_called_once() @@ -775,7 +775,7 @@ async def test_invalid_websocket_port(hass, config_entry): ) as mock_api: mock_api.return_value.get_request.return_value = SAMPLE_CONFIG_NO_WEBSOCKET config_entry.add_to_hass(hass) - await config_entry.async_setup(hass) + await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() assert hass.states.get(TEST_MASTER_ENTITY_NAME).state == STATE_UNAVAILABLE From a561b608bf0231ed5876773494c04a08719d370d Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Tue, 27 Sep 2022 11:32:05 -0700 Subject: [PATCH 782/955] Add spotify support to forked-daapd (#79136) --- .../components/forked_daapd/const.py | 2 + .../components/forked_daapd/manifest.json | 1 + .../components/forked_daapd/media_player.py | 34 +++++++++-- .../forked_daapd/test_browse_media.py | 57 ++++++++++++++++++- .../forked_daapd/test_media_player.py | 53 +++++++++++++++++ 5 files changed, 141 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/forked_daapd/const.py b/homeassistant/components/forked_daapd/const.py index bd0ab02e6c2..60e31bc707d 100644 --- a/homeassistant/components/forked_daapd/const.py +++ b/homeassistant/components/forked_daapd/const.py @@ -17,6 +17,8 @@ CAN_PLAY_TYPE = { MediaType.ALBUM, MediaType.GENRE, MediaType.MUSIC, + MediaType.EPISODE, + "show", # this is a spotify constant } CONF_LIBRESPOT_JAVA_PORT = "librespot_java_port" CONF_MAX_PLAYLISTS = "max_playlists" diff --git a/homeassistant/components/forked_daapd/manifest.json b/homeassistant/components/forked_daapd/manifest.json index 9a0372a193e..14d2132a165 100644 --- a/homeassistant/components/forked_daapd/manifest.json +++ b/homeassistant/components/forked_daapd/manifest.json @@ -4,6 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/forked_daapd", "codeowners": ["@uvjustin"], "requirements": ["pyforked-daapd==0.1.11", "pylibrespot-java==0.1.0"], + "after_dependencies": ["spotify"], "config_flow": true, "zeroconf": ["_daap._tcp.local."], "iot_class": "local_push", diff --git a/homeassistant/components/forked_daapd/media_player.py b/homeassistant/components/forked_daapd/media_player.py index 05d417ac9d9..9da1c1a1168 100644 --- a/homeassistant/components/forked_daapd/media_player.py +++ b/homeassistant/components/forked_daapd/media_player.py @@ -21,6 +21,12 @@ from homeassistant.components.media_player import ( MediaType, async_process_play_media_url, ) +from homeassistant.components.spotify import ( + async_browse_media as spotify_async_browse_media, + is_spotify_media_type, + resolve_spotify_media_type, + spotify_uri_from_media_browser_url, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT from homeassistant.core import HomeAssistant, callback @@ -677,6 +683,9 @@ class ForkedDaapdMaster(MediaPlayerEntity): media_id = play_item.url elif is_owntone_media_content_id(media_id): media_id = convert_to_owntone_uri(media_id) + elif is_spotify_media_type(media_type): + media_type = resolve_spotify_media_type(media_type) + media_id = spotify_uri_from_media_browser_url(media_id) if media_type not in CAN_PLAY_TYPE: _LOGGER.warning("Media type '%s' not supported", media_type) @@ -836,10 +845,27 @@ class ForkedDaapdMaster(MediaPlayerEntity): media_content_id, content_filter=lambda bm: bm.media_content_type in CAN_PLAY_TYPE, ) - if media_content_type is None: - # This is the base level, so we combine our library with the media source - return library(ms_result.children) - return ms_result + if media_content_type is not None: + return ms_result + other_sources: list[BrowseMedia] = ( + list(ms_result.children) if ms_result.children else [] + ) + if "spotify" in self.hass.config.components and ( + media_content_type is None or is_spotify_media_type(media_content_type) + ): + spotify_result = await spotify_async_browse_media( + self.hass, media_content_type, media_content_id + ) + if media_content_type is not None: + return spotify_result + if spotify_result.children: + other_sources += spotify_result.children + + if media_content_id is None or media_content_type is None: + # This is the base level, so we combine our library with the other sources + return library(other_sources) + + # media_content_type should only be None if media_content_id is None return await get_owntone_content(self, media_content_id) async def async_get_browse_image( diff --git a/tests/components/forked_daapd/test_browse_media.py b/tests/components/forked_daapd/test_browse_media.py index e90cbbff2aa..ff26b6f9315 100644 --- a/tests/components/forked_daapd/test_browse_media.py +++ b/tests/components/forked_daapd/test_browse_media.py @@ -3,9 +3,12 @@ from http import HTTPStatus from unittest.mock import patch -from homeassistant.components import media_source +from homeassistant.components import media_source, spotify from homeassistant.components.forked_daapd.browse_media import create_media_content_id -from homeassistant.components.media_player import MediaType +from homeassistant.components.media_player import BrowseMedia, MediaClass, MediaType +from homeassistant.components.spotify.const import ( + MEDIA_PLAYER_PREFIX as SPOTIFY_MEDIA_PLAYER_PREFIX, +) from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.setup import async_setup_component @@ -220,6 +223,56 @@ async def test_async_browse_media_not_found(hass, hass_ws_client, config_entry): msg_id += 1 +async def test_async_browse_spotify(hass, hass_ws_client, config_entry): + """Test browsing spotify.""" + + assert await async_setup_component(hass, spotify.DOMAIN, {}) + await hass.async_block_till_done() + config_entry.add_to_hass(hass) + await config_entry.async_setup(hass) + await hass.async_block_till_done() + with patch( + "homeassistant.components.forked_daapd.media_player.spotify_async_browse_media" + ) as mock_spotify_browse: + children = [ + BrowseMedia( + title="Spotify", + media_class=MediaClass.APP, + media_content_id=f"{SPOTIFY_MEDIA_PLAYER_PREFIX}some_id", + media_content_type=f"{SPOTIFY_MEDIA_PLAYER_PREFIX}track", + thumbnail="https://brands.home-assistant.io/_/spotify/logo.png", + can_play=False, + can_expand=True, + ) + ] + mock_spotify_browse.return_value = BrowseMedia( + title="Spotify", + media_class=MediaClass.APP, + media_content_id=SPOTIFY_MEDIA_PLAYER_PREFIX, + media_content_type=f"{SPOTIFY_MEDIA_PLAYER_PREFIX}library", + thumbnail="https://brands.home-assistant.io/_/spotify/logo.png", + can_play=False, + can_expand=True, + children=children, + ) + + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 1, + "type": "media_player/browse_media", + "entity_id": TEST_MASTER_ENTITY_NAME, + "media_content_type": f"{SPOTIFY_MEDIA_PLAYER_PREFIX}library", + "media_content_id": SPOTIFY_MEDIA_PLAYER_PREFIX, + } + ) + msg = await client.receive_json() + # Assert WebSocket response + assert msg["id"] == 1 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + + async def test_async_browse_image(hass, hass_client, config_entry): """Test browse media images.""" diff --git a/tests/components/forked_daapd/test_media_player.py b/tests/components/forked_daapd/test_media_player.py index 05a51e4defa..589f176db14 100644 --- a/tests/components/forked_daapd/test_media_player.py +++ b/tests/components/forked_daapd/test_media_player.py @@ -54,6 +54,7 @@ from homeassistant.components.media_player import ( MediaPlayerEnqueue, MediaType, ) +from homeassistant.components.media_source import PlayMedia from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, @@ -891,3 +892,55 @@ async def test_play_owntone_media(hass, mock_api_object): position=0, playback_from_position=0, ) + + +async def test_play_spotify_media(hass, mock_api_object): + """Test async play media with a spotify source.""" + initial_state = hass.states.get(TEST_MASTER_ENTITY_NAME) + await _service_call( + hass, + TEST_MASTER_ENTITY_NAME, + SERVICE_PLAY_MEDIA, + { + ATTR_MEDIA_CONTENT_TYPE: "spotify://track", + ATTR_MEDIA_CONTENT_ID: "spotify://open.spotify.com/spotify:track:abcdefghi", + ATTR_MEDIA_ENQUEUE: MediaPlayerEnqueue.PLAY, + }, + ) + state = hass.states.get(TEST_MASTER_ENTITY_NAME) + assert state.state == initial_state.state + assert state.last_updated > initial_state.last_updated + mock_api_object.add_to_queue.assert_called_with( + uris="spotify:track:abcdefghi", + playback="start", + position=0, + playback_from_position=0, + ) + + +async def test_play_media_source(hass, mock_api_object): + """Test async play media with a spotify source.""" + initial_state = hass.states.get(TEST_MASTER_ENTITY_NAME) + with patch( + "homeassistant.components.media_source.async_resolve_media", + return_value=PlayMedia("http://my_hass/song.m4a", "audio/aac"), + ): + await _service_call( + hass, + TEST_MASTER_ENTITY_NAME, + SERVICE_PLAY_MEDIA, + { + ATTR_MEDIA_CONTENT_TYPE: "audio/aac", + ATTR_MEDIA_CONTENT_ID: "media-source://media_source/test_dir/song.m4a", + ATTR_MEDIA_ENQUEUE: MediaPlayerEnqueue.PLAY, + }, + ) + state = hass.states.get(TEST_MASTER_ENTITY_NAME) + assert state.state == initial_state.state + assert state.last_updated > initial_state.last_updated + mock_api_object.add_to_queue.assert_called_with( + uris="http://my_hass/song.m4a", + playback="start", + position=0, + playback_from_position=0, + ) From 9084beda328ee29d98357379414a6c8d39db0af0 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 27 Sep 2022 20:33:28 +0200 Subject: [PATCH 783/955] Keep storing statistics for sensors which change device class (#79155) --- homeassistant/components/sensor/recorder.py | 14 +++- tests/components/sensor/test_recorder.py | 87 +++++++++++++++++++-- 2 files changed, 90 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 564a5226f58..a2f7e14a79d 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -156,10 +156,16 @@ def _normalize_states( entity_id: str, ) -> tuple[str | None, str | None, list[tuple[float, State]]]: """Normalize units.""" + old_metadata = old_metadatas[entity_id][1] if entity_id in old_metadatas else None state_unit: str | None = None - if device_class not in UNIT_CONVERTERS: - # We're not normalizing this device class, return the state as they are + if device_class not in UNIT_CONVERTERS or ( + old_metadata + and old_metadata["unit_of_measurement"] + != UNIT_CONVERTERS[device_class].NORMALIZED_UNIT + ): + # We're either not normalizing this device class or this entity is not stored + # normalized, return the states as they are fstates = [] for state in entity_history: try: @@ -176,10 +182,10 @@ def _normalize_states( if entity_id not in hass.data[WARN_UNSTABLE_UNIT]: hass.data[WARN_UNSTABLE_UNIT].add(entity_id) extra = "" - if old_metadata := old_metadatas.get(entity_id): + if old_metadata: extra = ( " and matches the unit of already compiled statistics " - f"({old_metadata[1]['unit_of_measurement']})" + f"({old_metadata['unit_of_measurement']})" ) _LOGGER.warning( "The unit of %s is changing, got multiple %s, generation of long term " diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 3d4f48360fc..c23a7cd9089 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -2142,9 +2142,9 @@ def test_compile_hourly_statistics_changing_units_3( @pytest.mark.parametrize( - "device_class, state_unit, statistic_unit, unit_class, mean, min, max", + "device_class, state_unit, statistic_unit, unit_class, mean1, mean2, min, max", [ - ("power", "kW", "W", None, 13.050847, -10, 30), + ("power", "kW", "W", None, 13.050847, 13.333333, -10, 30), ], ) def test_compile_hourly_statistics_changing_device_class_1( @@ -2154,7 +2154,8 @@ def test_compile_hourly_statistics_changing_device_class_1( state_unit, statistic_unit, unit_class, - mean, + mean1, + mean2, min, max, ): @@ -2194,7 +2195,7 @@ def test_compile_hourly_statistics_changing_device_class_1( "statistic_id": "sensor.test1", "start": process_timestamp_to_utc_isoformat(zero), "end": process_timestamp_to_utc_isoformat(zero + timedelta(minutes=5)), - "mean": approx(mean), + "mean": approx(mean1), "min": approx(min), "max": approx(max), "last_reset": None, @@ -2204,7 +2205,7 @@ def test_compile_hourly_statistics_changing_device_class_1( ] } - # Update device class and record additional states + # Update device class and record additional states in the original UoM attributes["device_class"] = device_class four, _states = record_states( hass, zero + timedelta(minutes=5), "sensor.test1", attributes @@ -2220,6 +2221,65 @@ def test_compile_hourly_statistics_changing_device_class_1( # Run statistics again, we get a warning, and no additional statistics is generated do_adhoc_statistics(hass, start=zero + timedelta(minutes=10)) wait_recording_done(hass) + statistic_ids = list_statistic_ids(hass) + assert statistic_ids == [ + { + "statistic_id": "sensor.test1", + "display_unit_of_measurement": state_unit, + "has_mean": True, + "has_sum": False, + "name": None, + "source": "recorder", + "statistics_unit_of_measurement": state_unit, + "unit_class": unit_class, + }, + ] + stats = statistics_during_period(hass, zero, period="5minute") + assert stats == { + "sensor.test1": [ + { + "statistic_id": "sensor.test1", + "start": process_timestamp_to_utc_isoformat(zero), + "end": process_timestamp_to_utc_isoformat(zero + timedelta(minutes=5)), + "mean": approx(mean1), + "min": approx(min), + "max": approx(max), + "last_reset": None, + "state": None, + "sum": None, + }, + { + "statistic_id": "sensor.test1", + "start": process_timestamp_to_utc_isoformat( + zero + timedelta(minutes=10) + ), + "end": process_timestamp_to_utc_isoformat(zero + timedelta(minutes=15)), + "mean": approx(mean2), + "min": approx(min), + "max": approx(max), + "last_reset": None, + "state": None, + "sum": None, + }, + ] + } + + # Update device class and record additional states in a different UoM + attributes["unit_of_measurement"] = statistic_unit + four, _states = record_states( + hass, zero + timedelta(minutes=15), "sensor.test1", attributes + ) + states["sensor.test1"] += _states["sensor.test1"] + four, _states = record_states( + hass, zero + timedelta(minutes=20), "sensor.test1", attributes + ) + states["sensor.test1"] += _states["sensor.test1"] + hist = history.get_significant_states(hass, zero, four) + assert dict(states) == dict(hist) + + # Run statistics again, we get a warning, and no additional statistics is generated + do_adhoc_statistics(hass, start=zero + timedelta(minutes=20)) + wait_recording_done(hass) assert ( f"The normalized unit of sensor.test1 ({statistic_unit}) does not match the " f"unit of already compiled statistics ({state_unit})" in caplog.text @@ -2244,13 +2304,26 @@ def test_compile_hourly_statistics_changing_device_class_1( "statistic_id": "sensor.test1", "start": process_timestamp_to_utc_isoformat(zero), "end": process_timestamp_to_utc_isoformat(zero + timedelta(minutes=5)), - "mean": approx(mean), + "mean": approx(mean1), "min": approx(min), "max": approx(max), "last_reset": None, "state": None, "sum": None, - } + }, + { + "statistic_id": "sensor.test1", + "start": process_timestamp_to_utc_isoformat( + zero + timedelta(minutes=10) + ), + "end": process_timestamp_to_utc_isoformat(zero + timedelta(minutes=15)), + "mean": approx(mean2), + "min": approx(min), + "max": approx(max), + "last_reset": None, + "state": None, + "sum": None, + }, ] } assert "Error while processing event StatisticsTask" not in caplog.text From e5630c6a66890d61788701302f0d930d5e76cc8d Mon Sep 17 00:00:00 2001 From: javicalle <31999997+javicalle@users.noreply.github.com> Date: Tue, 27 Sep 2022 22:25:36 +0200 Subject: [PATCH 784/955] New ZONNSMART TRVs (#79169) Adds a few missing ZONNSMART TRVs --- homeassistant/components/zha/climate.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zha/climate.py b/homeassistant/components/zha/climate.py index a0e6a59155a..ca3110dad60 100644 --- a/homeassistant/components/zha/climate.py +++ b/homeassistant/components/zha/climate.py @@ -760,10 +760,13 @@ class StelproFanHeater(Thermostat): @STRICT_MATCH( channel_names=CHANNEL_THERMOSTAT, manufacturers={ + "_TZE200_7yoranx2", "_TZE200_e9ba97vf", # TV01-ZG - "_TZE200_husqqvux", # TSL-TRV-TV01ZG "_TZE200_hue3yfsn", # TV02-ZG + "_TZE200_husqqvux", # TSL-TRV-TV01ZG "_TZE200_kly8gjlz", # TV05-ZG + "_TZE200_lnbfnyxd", + "_TZE200_mudxchsu", }, ) class ZONNSMARTThermostat(Thermostat): From cd4b0f53a1f34c3a7edcae2605547606c0535a89 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Tue, 27 Sep 2022 20:35:14 -0400 Subject: [PATCH 785/955] Bump ZHA quirks lib (#79175) --- 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 78d76d6556f..3f07d81ddb5 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -7,7 +7,7 @@ "bellows==0.33.1", "pyserial==3.5", "pyserial-asyncio==0.6", - "zha-quirks==0.0.79", + "zha-quirks==0.0.80", "zigpy-deconz==0.18.1", "zigpy==0.50.3", "zigpy-xbee==0.15.0", diff --git a/requirements_all.txt b/requirements_all.txt index 7d6711d1660..b03c1acb233 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2592,7 +2592,7 @@ zengge==0.2 zeroconf==0.39.1 # homeassistant.components.zha -zha-quirks==0.0.79 +zha-quirks==0.0.80 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 92d16c1951a..e03c8c3be02 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1790,7 +1790,7 @@ youless-api==0.16 zeroconf==0.39.1 # homeassistant.components.zha -zha-quirks==0.0.79 +zha-quirks==0.0.80 # homeassistant.components.zha zigpy-deconz==0.18.1 From 8c3e0d527d4bfcff3b0e166f93af0ef950c6f0da Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 27 Sep 2022 16:50:06 -1000 Subject: [PATCH 786/955] Bump dbus-fast to 0.17.0 (#79177) Changelog: https://github.com/Bluetooth-Devices/dbus-fast/compare/v1.15.1...v1.17.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 c7e71fcfd08..9a2f1f0e901 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -10,7 +10,7 @@ "bleak-retry-connector==2.1.3", "bluetooth-adapters==0.5.2", "bluetooth-auto-recovery==0.3.3", - "dbus-fast==1.15.1" + "dbus-fast==1.17.0" ], "codeowners": ["@bdraco"], "config_flow": true, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e4c2de90e75..96651efbb73 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ bluetooth-auto-recovery==0.3.3 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==38.0.1 -dbus-fast==1.15.1 +dbus-fast==1.17.0 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.3.0 diff --git a/requirements_all.txt b/requirements_all.txt index b03c1acb233..a4e2f6b00ef 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.15.1 +dbus-fast==1.17.0 # homeassistant.components.debugpy debugpy==1.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e03c8c3be02..6e46d73d7cc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -417,7 +417,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.15.1 +dbus-fast==1.17.0 # homeassistant.components.debugpy debugpy==1.6.3 From 1cf1d2c2bb394c3e307a2166f4f5f925f4d2ae74 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 27 Sep 2022 16:50:40 -1000 Subject: [PATCH 787/955] Update pySwitchbot for newer firmwares (#79174) changelog: https://github.com/Danielhiversen/pySwitchbot/compare/0.19.11...0.19.12 found while developing bluetooth proxies for esphome --- homeassistant/components/switchbot/manifest.json | 6 +++++- homeassistant/generated/bluetooth.py | 5 +++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index 2c8172750c3..cb81252fe21 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.19.11"], + "requirements": ["PySwitchbot==0.19.12"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ @@ -13,6 +13,10 @@ "@Eloston" ], "bluetooth": [ + { + "service_data_uuid": "00000d00-0000-1000-8000-00805f9b34fb", + "connectable": false + }, { "service_data_uuid": "0000fd3d-0000-1000-8000-00805f9b34fb", "connectable": false diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 8a9594f83ff..3036f691f00 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -263,6 +263,11 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ "local_name": "SensorPush*", "connectable": False, }, + { + "domain": "switchbot", + "service_data_uuid": "00000d00-0000-1000-8000-00805f9b34fb", + "connectable": False, + }, { "domain": "switchbot", "service_data_uuid": "0000fd3d-0000-1000-8000-00805f9b34fb", diff --git a/requirements_all.txt b/requirements_all.txt index a4e2f6b00ef..834f54a7182 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -40,7 +40,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.19.11 +PySwitchbot==0.19.12 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6e46d73d7cc..9d216c6b20d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -36,7 +36,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.19.11 +PySwitchbot==0.19.12 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From 40f5c317c7a124434e99987af7e44e230e387374 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 27 Sep 2022 16:50:56 -1000 Subject: [PATCH 788/955] Bump yalexs to 1.2.3 (#79170) Maps more missing activities Fixes #79119 changelog: https://github.com/bdraco/yalexs/compare/v1.2.2...v1.2.3 --- homeassistant/components/august/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index 3aef3f5960d..fdd6b52f740 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -2,7 +2,7 @@ "domain": "august", "name": "August", "documentation": "https://www.home-assistant.io/integrations/august", - "requirements": ["yalexs==1.2.2"], + "requirements": ["yalexs==1.2.3"], "codeowners": ["@bdraco"], "dhcp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index 834f54a7182..68496dbb0a7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2568,7 +2568,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==1.9.2 # homeassistant.components.august -yalexs==1.2.2 +yalexs==1.2.3 # homeassistant.components.yeelight yeelight==0.7.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9d216c6b20d..7d06dbd59d7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1775,7 +1775,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==1.9.2 # homeassistant.components.august -yalexs==1.2.2 +yalexs==1.2.3 # homeassistant.components.yeelight yeelight==0.7.10 From 7b708f4b35d964181386fddf7718858899033062 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 27 Sep 2022 16:51:46 -1000 Subject: [PATCH 789/955] Fix bluetooth active update coordinator not returning on failure (#79167) We also need to return on polling exception so we do not try to update. Fixes #79166 --- .../components/bluetooth/active_update_coordinator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/bluetooth/active_update_coordinator.py b/homeassistant/components/bluetooth/active_update_coordinator.py index b207f6fa2e1..37f049d3e07 100644 --- a/homeassistant/components/bluetooth/active_update_coordinator.py +++ b/homeassistant/components/bluetooth/active_update_coordinator.py @@ -117,12 +117,12 @@ class ActiveBluetoothProcessorCoordinator( "%s: Bluetooth error whilst polling: %s", self.address, str(exc) ) self.last_poll_successful = False - return + return except Exception: # pylint: disable=broad-except if self.last_poll_successful: self.logger.exception("%s: Failure while polling", self.address) self.last_poll_successful = False - return + return finally: self._last_poll = time.monotonic() From 38f3fa0762ea85229a263e5553fd17b01aa9ee4d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 27 Sep 2022 17:42:48 -1000 Subject: [PATCH 790/955] Handle invalid ISY data when the device is booting (#79163) --- homeassistant/components/isy994/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py index 301c86827e9..1b689e563dc 100644 --- a/homeassistant/components/isy994/__init__.py +++ b/homeassistant/components/isy994/__init__.py @@ -192,6 +192,10 @@ async def async_setup_entry( raise ConfigEntryNotReady( f"Invalid XML response from ISY; Ensure the ISY is running the latest firmware: {err}" ) from err + except TypeError as err: + raise ConfigEntryNotReady( + f"Invalid response ISY, device is likely still starting: {err}" + ) from err _categorize_nodes(hass_isy_data, isy.nodes, ignore_identifier, sensor_identifier) _categorize_programs(hass_isy_data, isy.programs) From b54458dfbade5e8e462245592d6bd27cb78f6ff0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 28 Sep 2022 08:41:33 +0300 Subject: [PATCH 791/955] Fix EZVIZ spelling case (#79164) * Fix EZVIZ spelling case The vendor seems consistent about all-uppercase spelling, so let's follow suit. * Revert changes to translations other than English --- homeassistant/components/ezviz/__init__.py | 8 ++++---- homeassistant/components/ezviz/binary_sensor.py | 6 +++--- homeassistant/components/ezviz/camera.py | 8 ++++---- homeassistant/components/ezviz/config_flow.py | 8 ++++---- homeassistant/components/ezviz/const.py | 2 +- homeassistant/components/ezviz/coordinator.py | 8 ++++---- homeassistant/components/ezviz/entity.py | 4 ++-- homeassistant/components/ezviz/manifest.json | 2 +- homeassistant/components/ezviz/sensor.py | 6 +++--- homeassistant/components/ezviz/strings.json | 10 +++++----- homeassistant/components/ezviz/switch.py | 6 +++--- homeassistant/components/ezviz/translations/en.json | 10 +++++----- tests/components/ezviz/__init__.py | 4 ++-- tests/components/ezviz/test_config_flow.py | 2 +- 14 files changed, 42 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/ezviz/__init__.py b/homeassistant/components/ezviz/__init__.py index 51931f4b104..fbd49102f3c 100644 --- a/homeassistant/components/ezviz/__init__.py +++ b/homeassistant/components/ezviz/__init__.py @@ -1,4 +1,4 @@ -"""Support for Ezviz camera.""" +"""Support for EZVIZ camera.""" import logging from pyezviz.client import EzvizClient @@ -39,7 +39,7 @@ PLATFORMS = [ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Set up Ezviz from a config entry.""" + """Set up EZVIZ from a config entry.""" hass.data.setdefault(DOMAIN, {}) if not entry.options: @@ -56,7 +56,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Fetch Entry id of main account and reload it. for item in hass.config_entries.async_entries(): if item.data.get(CONF_TYPE) == ATTR_TYPE_CLOUD: - _LOGGER.info("Reload Ezviz integration with new camera rtsp entry") + _LOGGER.info("Reload EZVIZ integration with new camera rtsp entry") await hass.config_entries.async_reload(item.entry_id) return True @@ -66,7 +66,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _get_ezviz_client_instance, entry ) except (InvalidURL, HTTPError, PyEzvizError) as error: - _LOGGER.error("Unable to connect to Ezviz service: %s", str(error)) + _LOGGER.error("Unable to connect to EZVIZ service: %s", str(error)) raise ConfigEntryNotReady from error coordinator = EzvizDataUpdateCoordinator( diff --git a/homeassistant/components/ezviz/binary_sensor.py b/homeassistant/components/ezviz/binary_sensor.py index 43ac914a50c..bab6fa5ca97 100644 --- a/homeassistant/components/ezviz/binary_sensor.py +++ b/homeassistant/components/ezviz/binary_sensor.py @@ -1,4 +1,4 @@ -"""Support for Ezviz binary sensors.""" +"""Support for EZVIZ binary sensors.""" from __future__ import annotations from homeassistant.components.binary_sensor import ( @@ -35,7 +35,7 @@ BINARY_SENSOR_TYPES: dict[str, BinarySensorEntityDescription] = { async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: - """Set up Ezviz sensors based on a config entry.""" + """Set up EZVIZ sensors based on a config entry.""" coordinator: EzvizDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ DATA_COORDINATOR ] @@ -52,7 +52,7 @@ async def async_setup_entry( class EzvizBinarySensor(EzvizEntity, BinarySensorEntity): - """Representation of a Ezviz sensor.""" + """Representation of a EZVIZ sensor.""" def __init__( self, diff --git a/homeassistant/components/ezviz/camera.py b/homeassistant/components/ezviz/camera.py index 307e1fac185..1f9de4f611c 100644 --- a/homeassistant/components/ezviz/camera.py +++ b/homeassistant/components/ezviz/camera.py @@ -57,7 +57,7 @@ async def async_setup_entry( entry: ConfigEntry, async_add_entities: entity_platform.AddEntitiesCallback, ) -> None: - """Set up Ezviz cameras based on a config entry.""" + """Set up EZVIZ cameras based on a config entry.""" coordinator: EzvizDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ DATA_COORDINATOR @@ -73,7 +73,7 @@ async def async_setup_entry( if item.unique_id == camera and item.source != SOURCE_IGNORE ] - # There seem to be a bug related to localRtspPort in Ezviz API. + # There seem to be a bug related to localRtspPort in EZVIZ API. local_rtsp_port = ( value["local_rtsp_port"] if value["local_rtsp_port"] != 0 @@ -174,7 +174,7 @@ async def async_setup_entry( class EzvizCamera(EzvizEntity, Camera): - """An implementation of a Ezviz security camera.""" + """An implementation of a EZVIZ security camera.""" def __init__( self, @@ -187,7 +187,7 @@ class EzvizCamera(EzvizEntity, Camera): local_rtsp_port: int, ffmpeg_arguments: str | None, ) -> None: - """Initialize a Ezviz security camera.""" + """Initialize a EZVIZ security camera.""" super().__init__(coordinator, serial) Camera.__init__(self) self.stream_options[CONF_USE_WALLCLOCK_AS_TIMESTAMPS] = True diff --git a/homeassistant/components/ezviz/config_flow.py b/homeassistant/components/ezviz/config_flow.py index 6c334291ee5..61b66280ae8 100644 --- a/homeassistant/components/ezviz/config_flow.py +++ b/homeassistant/components/ezviz/config_flow.py @@ -67,7 +67,7 @@ def _test_camera_rtsp_creds(data): class EzvizConfigFlow(ConfigFlow, domain=DOMAIN): - """Handle a config flow for Ezviz.""" + """Handle a config flow for EZVIZ.""" VERSION = 1 @@ -101,7 +101,7 @@ class EzvizConfigFlow(ConfigFlow, domain=DOMAIN): async def _validate_and_create_camera_rtsp(self, data): """Try DESCRIBE on RTSP camera with credentials.""" - # Get Ezviz cloud credentials from config entry + # Get EZVIZ cloud credentials from config entry ezviz_client_creds = { CONF_USERNAME: None, CONF_PASSWORD: None, @@ -311,14 +311,14 @@ class EzvizConfigFlow(ConfigFlow, domain=DOMAIN): class EzvizOptionsFlowHandler(OptionsFlow): - """Handle Ezviz client options.""" + """Handle EZVIZ client options.""" def __init__(self, config_entry: ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry async def async_step_init(self, user_input=None): - """Manage Ezviz options.""" + """Manage EZVIZ options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) diff --git a/homeassistant/components/ezviz/const.py b/homeassistant/components/ezviz/const.py index 5340f48d0f6..b9183772b6c 100644 --- a/homeassistant/components/ezviz/const.py +++ b/homeassistant/components/ezviz/const.py @@ -1,7 +1,7 @@ """Constants for the ezviz integration.""" DOMAIN = "ezviz" -MANUFACTURER = "Ezviz" +MANUFACTURER = "EZVIZ" # Configuration ATTR_SERIAL = "serial" diff --git a/homeassistant/components/ezviz/coordinator.py b/homeassistant/components/ezviz/coordinator.py index 8729aa4cf21..cc4537bb9b9 100644 --- a/homeassistant/components/ezviz/coordinator.py +++ b/homeassistant/components/ezviz/coordinator.py @@ -15,12 +15,12 @@ _LOGGER = logging.getLogger(__name__) class EzvizDataUpdateCoordinator(DataUpdateCoordinator): - """Class to manage fetching Ezviz data.""" + """Class to manage fetching EZVIZ data.""" def __init__( self, hass: HomeAssistant, *, api: EzvizClient, api_timeout: int ) -> None: - """Initialize global Ezviz data updater.""" + """Initialize global EZVIZ data updater.""" self.ezviz_client = api self._api_timeout = api_timeout update_interval = timedelta(seconds=30) @@ -28,11 +28,11 @@ class EzvizDataUpdateCoordinator(DataUpdateCoordinator): super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval) def _update_data(self) -> dict: - """Fetch data from Ezviz via camera load function.""" + """Fetch data from EZVIZ via camera load function.""" return self.ezviz_client.load_cameras() async def _async_update_data(self) -> dict: - """Fetch data from Ezviz.""" + """Fetch data from EZVIZ.""" try: async with timeout(self._api_timeout): return await self.hass.async_add_executor_job(self._update_data) diff --git a/homeassistant/components/ezviz/entity.py b/homeassistant/components/ezviz/entity.py index 2ab42a93286..e4debedc640 100644 --- a/homeassistant/components/ezviz/entity.py +++ b/homeassistant/components/ezviz/entity.py @@ -1,4 +1,4 @@ -"""An abstract class common to all Ezviz entities.""" +"""An abstract class common to all EZVIZ entities.""" from __future__ import annotations from typing import Any @@ -11,7 +11,7 @@ from .coordinator import EzvizDataUpdateCoordinator class EzvizEntity(CoordinatorEntity[EzvizDataUpdateCoordinator], Entity): - """Generic entity encapsulating common features of Ezviz device.""" + """Generic entity encapsulating common features of EZVIZ device.""" def __init__( self, diff --git a/homeassistant/components/ezviz/manifest.json b/homeassistant/components/ezviz/manifest.json index 47e2ec44e8a..985c96de806 100644 --- a/homeassistant/components/ezviz/manifest.json +++ b/homeassistant/components/ezviz/manifest.json @@ -1,6 +1,6 @@ { "domain": "ezviz", - "name": "Ezviz", + "name": "EZVIZ", "documentation": "https://www.home-assistant.io/integrations/ezviz", "dependencies": ["ffmpeg"], "codeowners": ["@RenierM26", "@baqs"], diff --git a/homeassistant/components/ezviz/sensor.py b/homeassistant/components/ezviz/sensor.py index a7334a3d18b..8e617aa3b3e 100644 --- a/homeassistant/components/ezviz/sensor.py +++ b/homeassistant/components/ezviz/sensor.py @@ -1,4 +1,4 @@ -"""Support for Ezviz sensors.""" +"""Support for EZVIZ sensors.""" from __future__ import annotations from homeassistant.components.sensor import ( @@ -44,7 +44,7 @@ SENSOR_TYPES: dict[str, SensorEntityDescription] = { async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: - """Set up Ezviz sensors based on a config entry.""" + """Set up EZVIZ sensors based on a config entry.""" coordinator: EzvizDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ DATA_COORDINATOR ] @@ -61,7 +61,7 @@ async def async_setup_entry( class EzvizSensor(EzvizEntity, SensorEntity): - """Representation of a Ezviz sensor.""" + """Representation of a EZVIZ sensor.""" coordinator: EzvizDataUpdateCoordinator diff --git a/homeassistant/components/ezviz/strings.json b/homeassistant/components/ezviz/strings.json index a8831d2ae34..91fa32ad9b2 100644 --- a/homeassistant/components/ezviz/strings.json +++ b/homeassistant/components/ezviz/strings.json @@ -3,7 +3,7 @@ "flow_title": "{serial}", "step": { "user": { - "title": "Connect to Ezviz Cloud", + "title": "Connect to EZVIZ Cloud", "data": { "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]", @@ -11,7 +11,7 @@ } }, "user_custom_url": { - "title": "Connect to custom Ezviz URL", + "title": "Connect to custom EZVIZ URL", "description": "Manually specify your region URL", "data": { "username": "[%key:common::config_flow::data::username%]", @@ -20,8 +20,8 @@ } }, "confirm": { - "title": "Discovered Ezviz Camera", - "description": "Enter RTSP credentials for Ezviz camera {serial} with IP {ip_address}", + "title": "Discovered EZVIZ Camera", + "description": "Enter RTSP credentials for EZVIZ camera {serial} with IP {ip_address}", "data": { "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]" @@ -36,7 +36,7 @@ "abort": { "already_configured_account": "[%key:common::config_flow::abort::already_configured_account%]", "unknown": "[%key:common::config_flow::error::unknown%]", - "ezviz_cloud_account_missing": "Ezviz cloud account missing. Please reconfigure Ezviz cloud account" + "ezviz_cloud_account_missing": "EZVIZ cloud account missing. Please reconfigure EZVIZ cloud account" } }, "options": { diff --git a/homeassistant/components/ezviz/switch.py b/homeassistant/components/ezviz/switch.py index 55a946f858a..58b28477412 100644 --- a/homeassistant/components/ezviz/switch.py +++ b/homeassistant/components/ezviz/switch.py @@ -1,4 +1,4 @@ -"""Support for Ezviz Switch sensors.""" +"""Support for EZVIZ Switch sensors.""" from __future__ import annotations from typing import Any @@ -19,7 +19,7 @@ from .entity import EzvizEntity async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: - """Set up Ezviz switch based on a config entry.""" + """Set up EZVIZ switch based on a config entry.""" coordinator: EzvizDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ DATA_COORDINATOR ] @@ -37,7 +37,7 @@ async def async_setup_entry( class EzvizSwitch(EzvizEntity, SwitchEntity): - """Representation of a Ezviz sensor.""" + """Representation of a EZVIZ sensor.""" _attr_device_class = SwitchDeviceClass.SWITCH diff --git a/homeassistant/components/ezviz/translations/en.json b/homeassistant/components/ezviz/translations/en.json index 9b5e273b0ad..c9f096e7995 100644 --- a/homeassistant/components/ezviz/translations/en.json +++ b/homeassistant/components/ezviz/translations/en.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured_account": "Account is already configured", - "ezviz_cloud_account_missing": "Ezviz cloud account missing. Please reconfigure Ezviz cloud account", + "ezviz_cloud_account_missing": "EZVIZ cloud account missing. Please reconfigure EZVIZ cloud account", "unknown": "Unexpected error" }, "error": { @@ -17,8 +17,8 @@ "password": "Password", "username": "Username" }, - "description": "Enter RTSP credentials for Ezviz camera {serial} with IP {ip_address}", - "title": "Discovered Ezviz Camera" + "description": "Enter RTSP credentials for EZVIZ camera {serial} with IP {ip_address}", + "title": "Discovered EZVIZ Camera" }, "user": { "data": { @@ -26,7 +26,7 @@ "url": "URL", "username": "Username" }, - "title": "Connect to Ezviz Cloud" + "title": "Connect to EZVIZ Cloud" }, "user_custom_url": { "data": { @@ -35,7 +35,7 @@ "username": "Username" }, "description": "Manually specify your region URL", - "title": "Connect to custom Ezviz URL" + "title": "Connect to custom EZVIZ URL" } } }, diff --git a/tests/components/ezviz/__init__.py b/tests/components/ezviz/__init__.py index bfb30b893eb..64dcbfc26eb 100644 --- a/tests/components/ezviz/__init__.py +++ b/tests/components/ezviz/__init__.py @@ -1,4 +1,4 @@ -"""Tests for the Ezviz integration.""" +"""Tests for the EZVIZ integration.""" from unittest.mock import patch from homeassistant.components.ezviz.const import ( @@ -74,7 +74,7 @@ async def init_integration( options: dict = ENTRY_OPTIONS, skip_entry_setup: bool = False, ) -> MockConfigEntry: - """Set up the Ezviz integration in Home Assistant.""" + """Set up the EZVIZ integration in Home Assistant.""" entry = MockConfigEntry(domain=DOMAIN, data=data, options=options) entry.add_to_hass(hass) diff --git a/tests/components/ezviz/test_config_flow.py b/tests/components/ezviz/test_config_flow.py index c31a43c1949..65cde18e914 100644 --- a/tests/components/ezviz/test_config_flow.py +++ b/tests/components/ezviz/test_config_flow.py @@ -1,4 +1,4 @@ -"""Test the Ezviz config flow.""" +"""Test the EZVIZ config flow.""" from unittest.mock import patch From 0c191df5986455640d759fde971e16fc39fc22d3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 27 Sep 2022 19:48:38 -1000 Subject: [PATCH 792/955] Bump ibeacon-ble to 0.7.1 (#79182) --- homeassistant/components/ibeacon/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ibeacon/manifest.json b/homeassistant/components/ibeacon/manifest.json index 273afdaa07f..9cecb399281 100644 --- a/homeassistant/components/ibeacon/manifest.json +++ b/homeassistant/components/ibeacon/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/ibeacon", "dependencies": ["bluetooth"], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [2, 21] }], - "requirements": ["ibeacon_ble==0.7.0"], + "requirements": ["ibeacon_ble==0.7.1"], "codeowners": ["@bdraco"], "iot_class": "local_push", "loggers": ["bleak"], diff --git a/requirements_all.txt b/requirements_all.txt index 68496dbb0a7..66385faa8fc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -898,7 +898,7 @@ iammeter==0.1.7 iaqualink==0.4.1 # homeassistant.components.ibeacon -ibeacon_ble==0.7.0 +ibeacon_ble==0.7.1 # homeassistant.components.watson_tts ibm-watson==5.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7d06dbd59d7..5af62e48bdc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -666,7 +666,7 @@ hyperion-py==0.7.5 iaqualink==0.4.1 # homeassistant.components.ibeacon -ibeacon_ble==0.7.0 +ibeacon_ble==0.7.1 # homeassistant.components.ping icmplib==3.0 From e360a4fa9e40ace622e31f309af265fc4d6bbcfd Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Wed, 28 Sep 2022 02:06:24 -0400 Subject: [PATCH 793/955] Bump aiopyarr to 22.9.0 (#79173) --- .../components/lidarr/config_flow.py | 16 ++--- homeassistant/components/lidarr/manifest.json | 2 +- .../components/radarr/config_flow.py | 11 ++-- homeassistant/components/radarr/manifest.json | 2 +- homeassistant/components/sonarr/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/lidarr/test_config_flow.py | 59 ++++++++++++------- tests/components/radarr/test_config_flow.py | 25 ++++++-- 9 files changed, 79 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/lidarr/config_flow.py b/homeassistant/components/lidarr/config_flow.py index 1b7f2b23c11..a0b73950766 100644 --- a/homeassistant/components/lidarr/config_flow.py +++ b/homeassistant/components/lidarr/config_flow.py @@ -5,7 +5,7 @@ from collections.abc import Mapping from typing import Any from aiohttp import ClientConnectorError -from aiopyarr import SystemStatus, exceptions +from aiopyarr import exceptions from aiopyarr.lidarr_client import LidarrClient import voluptuous as vol @@ -54,15 +54,16 @@ class LidarrConfigFlow(ConfigFlow, domain=DOMAIN): else: try: - result = await validate_input(self.hass, user_input) - if isinstance(result, tuple): + if result := await validate_input(self.hass, user_input): user_input[CONF_API_KEY] = result[1] - elif isinstance(result, str): - errors = {"base": result} except exceptions.ArrAuthenticationException: errors = {"base": "invalid_auth"} except (ClientConnectorError, exceptions.ArrConnectionException): errors = {"base": "cannot_connect"} + except exceptions.ArrWrongAppException: + errors = {"base": "wrong_app"} + except exceptions.ArrZeroConfException: + errors = {"base": "zeroconf_failed"} except exceptions.ArrException: errors = {"base": "unknown"} if not errors: @@ -95,7 +96,7 @@ class LidarrConfigFlow(ConfigFlow, domain=DOMAIN): async def validate_input( hass: HomeAssistant, data: dict[str, Any] -) -> tuple[str, str, str] | str | SystemStatus: +) -> tuple[str, str, str] | None: """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA with values provided by the user. @@ -108,4 +109,5 @@ async def validate_input( ) if CONF_API_KEY not in data: return await lidarr.async_try_zeroconf() - return await lidarr.async_get_system_status() + await lidarr.async_get_system_status() + return None diff --git a/homeassistant/components/lidarr/manifest.json b/homeassistant/components/lidarr/manifest.json index 6f7ad875a46..7d4e9bcede7 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.7.0"], + "requirements": ["aiopyarr==22.9.0"], "codeowners": ["@tkdrob"], "config_flow": true, "iot_class": "local_polling", diff --git a/homeassistant/components/radarr/config_flow.py b/homeassistant/components/radarr/config_flow.py index af74922402a..c37eeba4969 100644 --- a/homeassistant/components/radarr/config_flow.py +++ b/homeassistant/components/radarr/config_flow.py @@ -62,15 +62,16 @@ class RadarrConfigFlow(ConfigFlow, domain=DOMAIN): else: try: - result = await validate_input(self.hass, user_input) - if isinstance(result, tuple): + if result := await validate_input(self.hass, user_input): user_input[CONF_API_KEY] = result[1] - elif isinstance(result, str): - errors = {"base": result} except exceptions.ArrAuthenticationException: errors = {"base": "invalid_auth"} except (ClientConnectorError, exceptions.ArrConnectionException): errors = {"base": "cannot_connect"} + except exceptions.ArrWrongAppException: + errors = {"base": "wrong_app"} + except exceptions.ArrZeroConfException: + errors = {"base": "zeroconf_failed"} except exceptions.ArrException: errors = {"base": "unknown"} if not errors: @@ -130,7 +131,7 @@ class RadarrConfigFlow(ConfigFlow, domain=DOMAIN): async def validate_input( hass: HomeAssistant, data: dict[str, Any] -) -> tuple[str, str, str] | str | None: +) -> tuple[str, str, str] | None: """Validate the user input allows us to connect.""" host_configuration = PyArrHostConfiguration( api_token=data.get(CONF_API_KEY, ""), diff --git a/homeassistant/components/radarr/manifest.json b/homeassistant/components/radarr/manifest.json index 3ecb4247d87..5bc15b24069 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.7.0"], + "requirements": ["aiopyarr==22.9.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 6b9b45b75ec..0c5b68a7949 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.7.0"], + "requirements": ["aiopyarr==22.9.0"], "config_flow": true, "quality_scale": "silver", "iot_class": "local_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 66385faa8fc..4b16e8d8cb1 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.7.0 +aiopyarr==22.9.0 # homeassistant.components.qnap_qsw aioqsw==0.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5af62e48bdc..7a347fcf059 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.7.0 +aiopyarr==22.9.0 # homeassistant.components.qnap_qsw aioqsw==0.2.2 diff --git a/tests/components/lidarr/test_config_flow.py b/tests/components/lidarr/test_config_flow.py index 0ec48439012..b7859324340 100644 --- a/tests/components/lidarr/test_config_flow.py +++ b/tests/components/lidarr/test_config_flow.py @@ -1,7 +1,7 @@ """Test Lidarr config flow.""" from unittest.mock import patch -from aiopyarr import ArrAuthenticationException, ArrConnectionException, ArrException +from aiopyarr import exceptions from homeassistant import data_entry_flow from homeassistant.components.lidarr.const import DEFAULT_NAME, DOMAIN @@ -45,7 +45,7 @@ async def test_flow_user_form( async def test_flow_user_invalid_auth(hass: HomeAssistant) -> None: """Test invalid authentication.""" with _patch_client() as client: - client.side_effect = ArrAuthenticationException + client.side_effect = exceptions.ArrAuthenticationException result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, @@ -62,7 +62,7 @@ async def test_flow_user_invalid_auth(hass: HomeAssistant) -> None: async def test_flow_user_cannot_connect(hass: HomeAssistant) -> None: """Test connection error.""" with _patch_client() as client: - client.side_effect = ArrConnectionException + client.side_effect = exceptions.ArrConnectionException result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, @@ -76,10 +76,44 @@ async def test_flow_user_cannot_connect(hass: HomeAssistant) -> None: assert result["errors"]["base"] == "cannot_connect" +async def test_wrong_app(hass: HomeAssistant) -> None: + """Test we show user form on wrong app.""" + with patch( + "homeassistant.components.lidarr.config_flow.LidarrClient.async_try_zeroconf", + side_effect=exceptions.ArrWrongAppException, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_USER}, + data=MOCK_USER_INPUT, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"]["base"] == "wrong_app" + + +async def test_zero_conf_failure(hass: HomeAssistant) -> None: + """Test we show user form on api key retrieval failure.""" + with patch( + "homeassistant.components.lidarr.config_flow.LidarrClient.async_try_zeroconf", + side_effect=exceptions.ArrZeroConfException, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_USER}, + data=MOCK_USER_INPUT, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"]["base"] == "zeroconf_failed" + + async def test_flow_user_unknown_error(hass: HomeAssistant) -> None: """Test unknown error.""" with _patch_client() as client: - client.side_effect = ArrException + client.side_effect = exceptions.ArrException result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, @@ -93,23 +127,6 @@ async def test_flow_user_unknown_error(hass: HomeAssistant) -> None: assert result["errors"]["base"] == "unknown" -async def test_flow_user_failed_zeroconf(hass: HomeAssistant) -> None: - """Test zero configuration failed.""" - with _patch_client() as client: - client.return_value = "zeroconf_failed" - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={CONF_SOURCE: SOURCE_USER}, - ) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input=CONF_DATA, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "user" - assert result["errors"]["base"] == "zeroconf_failed" - - async def test_flow_reauth( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: diff --git a/tests/components/radarr/test_config_flow.py b/tests/components/radarr/test_config_flow.py index ffd6b5f5759..6aac4e369fe 100644 --- a/tests/components/radarr/test_config_flow.py +++ b/tests/components/radarr/test_config_flow.py @@ -1,7 +1,7 @@ """Test Radarr config flow.""" from unittest.mock import AsyncMock, patch -from aiopyarr import ArrException +from aiopyarr import exceptions from homeassistant import data_entry_flow from homeassistant.components.radarr.const import DEFAULT_NAME, DOMAIN @@ -115,7 +115,7 @@ async def test_wrong_app(hass: HomeAssistant) -> None: """Test we show user form on wrong app.""" with patch( "homeassistant.components.radarr.config_flow.RadarrClient.async_try_zeroconf", - return_value="wrong_app", + side_effect=exceptions.ArrWrongAppException, ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -125,14 +125,31 @@ async def test_wrong_app(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" - assert result["errors"] == {"base": "wrong_app"} + assert result["errors"]["base"] == "wrong_app" + + +async def test_zero_conf_failure(hass: HomeAssistant) -> None: + """Test we show user form on api key retrieval failure.""" + with patch( + "homeassistant.components.radarr.config_flow.RadarrClient.async_try_zeroconf", + side_effect=exceptions.ArrZeroConfException, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_USER}, + data={CONF_URL: URL, CONF_VERIFY_SSL: False}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"]["base"] == "zeroconf_failed" async def test_unknown_error(hass: HomeAssistant) -> None: """Test we show user form on unknown error.""" with patch( "homeassistant.components.radarr.config_flow.RadarrClient.async_get_system_status", - side_effect=ArrException, + side_effect=exceptions.ArrException, ): result = await hass.config_entries.flow.async_init( DOMAIN, From 9ee81fdd772a29cb024f583c8b2c3a2c81a3621e Mon Sep 17 00:00:00 2001 From: Vincent Knoop Pathuis <48653141+vpathuis@users.noreply.github.com> Date: Wed, 28 Sep 2022 08:28:31 +0200 Subject: [PATCH 794/955] Landis+Gyr Heat Meter: add heat previous year GJ as diagnostic (#78690) Add heat previous year GJ as diagnostic --- homeassistant/components/landisgyr_heat_meter/const.py | 8 ++++++++ tests/components/landisgyr_heat_meter/test_sensor.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/landisgyr_heat_meter/const.py b/homeassistant/components/landisgyr_heat_meter/const.py index 55a6c65892c..57a8f9d9be4 100644 --- a/homeassistant/components/landisgyr_heat_meter/const.py +++ b/homeassistant/components/landisgyr_heat_meter/const.py @@ -44,6 +44,14 @@ HEAT_METER_SENSOR_TYPES = ( native_unit_of_measurement=ENERGY_MEGA_WATT_HOUR, entity_category=EntityCategory.DIAGNOSTIC, ), + # Diagnostic entity for debugging, this will match the value in GJ of previous year indicated on the meter's display + SensorEntityDescription( + key="heat_previous_year_gj", + icon="mdi:fire", + name="Heat previous year GJ", + native_unit_of_measurement="GJ", + entity_category=EntityCategory.DIAGNOSTIC, + ), SensorEntityDescription( key="volume_previous_year_m3", icon="mdi:fire", diff --git a/tests/components/landisgyr_heat_meter/test_sensor.py b/tests/components/landisgyr_heat_meter/test_sensor.py index 505efb446b8..1a068093d0e 100644 --- a/tests/components/landisgyr_heat_meter/test_sensor.py +++ b/tests/components/landisgyr_heat_meter/test_sensor.py @@ -76,7 +76,7 @@ async def test_create_sensors(mock_heat_meter, hass): await hass.async_block_till_done() # check if 26 attributes have been created - assert len(hass.states.async_all()) == 26 + assert len(hass.states.async_all()) == 27 entity_reg = entity_registry.async_get(hass) state = hass.states.get("sensor.heat_meter_heat_usage") From 69bf77be12460924423933a20861338cdf468997 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Wed, 28 Sep 2022 08:43:31 +0200 Subject: [PATCH 795/955] Avoid multiline lambdas in Fritz!Smarthome sensors (#78524) * avoid multiline lambdas * update tests --- .../components/fritzbox/coordinator.py | 3 +- homeassistant/components/fritzbox/sensor.py | 80 +++++++++++++------ tests/components/fritzbox/test_climate.py | 16 ++++ tests/components/fritzbox/test_switch.py | 18 +++++ 4 files changed, 92 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/fritzbox/coordinator.py b/homeassistant/components/fritzbox/coordinator.py index a94a7483e18..47d2cdca005 100644 --- a/homeassistant/components/fritzbox/coordinator.py +++ b/homeassistant/components/fritzbox/coordinator.py @@ -52,8 +52,9 @@ class FritzboxDataUpdateCoordinator(DataUpdateCoordinator): if ( device.has_powermeter and device.present - and hasattr(device, "voltage") + and isinstance(device.voltage, int) and device.voltage <= 0 + and isinstance(device.power, int) and device.power <= 0 and device.energy <= 0 ): diff --git a/homeassistant/components/fritzbox/sensor.py b/homeassistant/components/fritzbox/sensor.py index 4467c9fe1ea..7253fdcf36e 100644 --- a/homeassistant/components/fritzbox/sensor.py +++ b/homeassistant/components/fritzbox/sensor.py @@ -49,6 +49,52 @@ class FritzSensorEntityDescription( """Description for Fritz!Smarthome sensor entities.""" +def suitable_eco_temperature(device: FritzhomeDevice) -> bool: + """Check suitablity for eco temperature sensor.""" + return device.has_thermostat and device.eco_temperature is not None + + +def suitable_comfort_temperature(device: FritzhomeDevice) -> bool: + """Check suitablity for comfort temperature sensor.""" + return device.has_thermostat and device.comfort_temperature is not None + + +def suitable_nextchange_temperature(device: FritzhomeDevice) -> bool: + """Check suitablity for next scheduled temperature sensor.""" + return device.has_thermostat and device.nextchange_temperature is not None + + +def suitable_nextchange_time(device: FritzhomeDevice) -> bool: + """Check suitablity for next scheduled changed time sensor.""" + return device.has_thermostat and device.nextchange_endperiod is not None + + +def suitable_temperature(device: FritzhomeDevice) -> bool: + """Check suitablity for temperature sensor.""" + return device.has_temperature_sensor and not device.has_thermostat + + +def value_electric_current(device: FritzhomeDevice) -> float: + """Return native value for electric current sensor.""" + if isinstance(device.power, int) and isinstance(device.voltage, int): + return round(device.power / device.voltage, 3) + return 0.0 + + +def value_nextchange_preset(device: FritzhomeDevice) -> str: + """Return native value for next scheduled preset sensor.""" + if device.nextchange_temperature == device.eco_temperature: + return PRESET_ECO + return PRESET_COMFORT + + +def value_scheduled_preset(device: FritzhomeDevice) -> str: + """Return native value for current scheduled preset sensor.""" + if device.nextchange_temperature == device.eco_temperature: + return PRESET_COMFORT + return PRESET_ECO + + SENSOR_TYPES: Final[tuple[FritzSensorEntityDescription, ...]] = ( FritzSensorEntityDescription( key="temperature", @@ -57,9 +103,7 @@ SENSOR_TYPES: Final[tuple[FritzSensorEntityDescription, ...]] = ( device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, - suitable=lambda device: ( - device.has_temperature_sensor and not device.has_thermostat - ), + suitable=suitable_temperature, native_value=lambda device: device.temperature, # type: ignore[no-any-return] ), FritzSensorEntityDescription( @@ -105,9 +149,7 @@ SENSOR_TYPES: Final[tuple[FritzSensorEntityDescription, ...]] = ( device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, suitable=lambda device: device.has_powermeter, # type: ignore[no-any-return] - native_value=lambda device: round(device.power / device.voltage, 3) - if device.power and getattr(device, "voltage", None) - else 0.0, + native_value=value_electric_current, ), FritzSensorEntityDescription( key="total_energy", @@ -124,8 +166,7 @@ SENSOR_TYPES: Final[tuple[FritzSensorEntityDescription, ...]] = ( name="Comfort Temperature", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, - suitable=lambda device: device.has_thermostat - and device.comfort_temperature is not None, + suitable=suitable_comfort_temperature, native_value=lambda device: device.comfort_temperature, # type: ignore[no-any-return] ), FritzSensorEntityDescription( @@ -133,8 +174,7 @@ SENSOR_TYPES: Final[tuple[FritzSensorEntityDescription, ...]] = ( name="Eco Temperature", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, - suitable=lambda device: device.has_thermostat - and device.eco_temperature is not None, + suitable=suitable_eco_temperature, native_value=lambda device: device.eco_temperature, # type: ignore[no-any-return] ), FritzSensorEntityDescription( @@ -142,35 +182,27 @@ SENSOR_TYPES: Final[tuple[FritzSensorEntityDescription, ...]] = ( name="Next Scheduled Temperature", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, - suitable=lambda device: device.has_thermostat - and device.nextchange_temperature is not None, + suitable=suitable_nextchange_temperature, native_value=lambda device: device.nextchange_temperature, # type: ignore[no-any-return] ), FritzSensorEntityDescription( key="nextchange_time", name="Next Scheduled Change Time", device_class=SensorDeviceClass.TIMESTAMP, - suitable=lambda device: device.has_thermostat - and device.nextchange_endperiod is not None, + suitable=suitable_nextchange_time, native_value=lambda device: utc_from_timestamp(device.nextchange_endperiod), ), FritzSensorEntityDescription( key="nextchange_preset", name="Next Scheduled Preset", - suitable=lambda device: device.has_thermostat - and device.nextchange_temperature is not None, - native_value=lambda device: PRESET_ECO - if device.nextchange_temperature == device.eco_temperature - else PRESET_COMFORT, + suitable=suitable_nextchange_temperature, + native_value=value_nextchange_preset, ), FritzSensorEntityDescription( key="scheduled_preset", name="Current Scheduled Preset", - suitable=lambda device: device.has_thermostat - and device.nextchange_temperature is not None, - native_value=lambda device: PRESET_COMFORT - if device.nextchange_temperature == device.eco_temperature - else PRESET_ECO, + suitable=suitable_nextchange_temperature, + native_value=value_scheduled_preset, ), ) diff --git a/tests/components/fritzbox/test_climate.py b/tests/components/fritzbox/test_climate.py index 26c3e5a3d34..faed4389310 100644 --- a/tests/components/fritzbox/test_climate.py +++ b/tests/components/fritzbox/test_climate.py @@ -140,6 +140,22 @@ async def test_setup(hass: HomeAssistant, fritz: Mock): ) assert ATTR_STATE_CLASS not in state.attributes + device.nextchange_temperature = 16 + + next_update = dt_util.utcnow() + timedelta(seconds=200) + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + + state = hass.states.get(f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_next_scheduled_preset") + assert state + assert state.state == PRESET_ECO + + state = hass.states.get( + f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_current_scheduled_preset" + ) + assert state + assert state.state == PRESET_COMFORT + async def test_target_temperature_on(hass: HomeAssistant, fritz: Mock): """Test turn device on.""" diff --git a/tests/components/fritzbox/test_switch.py b/tests/components/fritzbox/test_switch.py index 362fdfac951..852b41512f5 100644 --- a/tests/components/fritzbox/test_switch.py +++ b/tests/components/fritzbox/test_switch.py @@ -174,3 +174,21 @@ async def test_assume_device_unavailable(hass: HomeAssistant, fritz: Mock): state = hass.states.get(ENTITY_ID) assert state assert state.state == STATE_UNAVAILABLE + + +async def test_device_current_unavailable(hass: HomeAssistant, fritz: Mock): + """Test current in case voltage and power are not available.""" + device = FritzDeviceSwitchMock() + device.voltage = None + device.power = None + assert await setup_config_entry( + hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz + ) + + state = hass.states.get(ENTITY_ID) + assert state + assert state.state == STATE_ON + + state = hass.states.get(f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_electric_current") + assert state + assert state.state == "0.0" From c38b1e7727366149622e3a39d7c21b53b30723bc Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 28 Sep 2022 08:43:58 +0200 Subject: [PATCH 796/955] Improve check of new_entity_id in entity_registry.async_update_entity (#78276) * Improve check of new_entity_id in entity_registry.async_update_entity * Fix race in rfxtrx config flow * Make sure Event is created on time * Rename poorly named variable * Fix typing * Correct typing of _handle_state_change --- .../components/config/entity_registry.py | 22 ++++---- .../components/rfxtrx/config_flow.py | 30 ++++++++++- homeassistant/helpers/entity_registry.py | 29 ++++++++--- tests/helpers/test_entity_registry.py | 52 ++++++++++++++++++- 4 files changed, 111 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/config/entity_registry.py b/homeassistant/components/config/entity_registry.py index e006cd8062e..b024c3f0128 100644 --- a/homeassistant/components/config/entity_registry.py +++ b/homeassistant/components/config/entity_registry.py @@ -136,22 +136,18 @@ def websocket_update_entity(hass, connection, msg): changes = {} - for key in ("area_id", "device_class", "disabled_by", "hidden_by", "icon", "name"): + for key in ( + "area_id", + "device_class", + "disabled_by", + "hidden_by", + "icon", + "name", + "new_entity_id", + ): if key in msg: changes[key] = msg[key] - if "new_entity_id" in msg and msg["new_entity_id"] != entity_id: - changes["new_entity_id"] = msg["new_entity_id"] - if hass.states.get(msg["new_entity_id"]) is not None: - connection.send_message( - websocket_api.error_message( - msg["id"], - "invalid_info", - "Entity with this ID is already registered", - ) - ) - return - if "disabled_by" in msg and msg["disabled_by"] is None: # Don't allow enabling an entity of a disabled device if entity_entry.device_id: diff --git a/homeassistant/components/rfxtrx/config_flow.py b/homeassistant/components/rfxtrx/config_flow.py index 7b5fdc08261..2aa3bd20b8c 100644 --- a/homeassistant/components/rfxtrx/config_flow.py +++ b/homeassistant/components/rfxtrx/config_flow.py @@ -1,6 +1,7 @@ """Config flow for RFXCOM RFXtrx integration.""" from __future__ import annotations +import asyncio import copy import itertools import os @@ -23,12 +24,13 @@ from homeassistant.const import ( CONF_PORT, CONF_TYPE, ) -from homeassistant.core import callback +from homeassistant.core import State, callback from homeassistant.helpers import ( config_validation as cv, device_registry as dr, entity_registry as er, ) +from homeassistant.helpers.event import async_track_state_change from . import ( DOMAIN, @@ -343,9 +345,35 @@ class OptionsFlow(config_entries.OptionsFlow): if new_entity_id is not None: entity_migration_map[new_entity_id] = entry + @callback + def _handle_state_change( + entity_id: str, old_state: State | None, new_state: State | None + ) -> None: + # Wait for entities to finish cleanup + if new_state is None and entity_id in pending_entities: + pending_entities.remove(entity_id) + if not pending_entities: + wait_for_entities.set() + + # Create a set with entities to be removed which are currently in the state + # machine + pending_entities = { + entry.entity_id + for entry in entity_migration_map.values() + if not self.hass.states.async_available(entry.entity_id) + } + wait_for_entities = asyncio.Event() + remove_track_state_changes = async_track_state_change( + self.hass, pending_entities, _handle_state_change + ) + for entry in entity_migration_map.values(): entity_registry.async_remove(entry.entity_id) + # Wait for entities to finish cleanup + await wait_for_entities.wait() + remove_track_state_changes() + for entity_id, entry in entity_migration_map.items(): entity_registry.async_update_entity( entity_id, diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index f40c6347af7..e58dde19127 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -335,6 +335,25 @@ class EntityRegistry: """Check if an entity_id is currently registered.""" return self.entities.get_entity_id((domain, platform, unique_id)) + def _entity_id_available( + self, entity_id: str, known_object_ids: Iterable[str] | None + ) -> bool: + """Return True if the entity_id is available. + + An entity_id is available if: + - It's not registered + - It's not known by the entity component adding the entity + - It's not in the state machine + """ + if known_object_ids is None: + known_object_ids = {} + + return ( + entity_id not in self.entities + and entity_id not in known_object_ids + and self.hass.states.async_available(entity_id) + ) + @callback def async_generate_entity_id( self, @@ -352,15 +371,11 @@ class EntityRegistry: raise MaxLengthExceeded(domain, "domain", MAX_LENGTH_STATE_DOMAIN) test_string = preferred_string[:MAX_LENGTH_STATE_ENTITY_ID] - if not known_object_ids: + if known_object_ids is None: known_object_ids = {} tries = 1 - while ( - test_string in self.entities - or test_string in known_object_ids - or not self.hass.states.async_available(test_string) - ): + while not self._entity_id_available(test_string, known_object_ids): tries += 1 len_suffix = len(str(tries)) + 1 test_string = ( @@ -630,7 +645,7 @@ class EntityRegistry: old_values[attr_name] = getattr(old, attr_name) if new_entity_id is not UNDEFINED and new_entity_id != old.entity_id: - if self.async_is_registered(new_entity_id): + if not self._entity_id_available(new_entity_id, None): raise ValueError("Entity with this ID is already registered") if not valid_entity_id(new_entity_id): diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index 64579c25766..5538950260c 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -6,7 +6,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import EVENT_HOMEASSISTANT_START, STATE_UNAVAILABLE -from homeassistant.core import CoreState, callback +from homeassistant.core import CoreState, HomeAssistant, callback from homeassistant.exceptions import MaxLengthExceeded from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity import EntityCategory @@ -597,6 +597,56 @@ async def test_update_entity_unique_id_conflict(registry): assert registry.async_get_entity_id("light", "hue", "1234") == entry2.entity_id +async def test_update_entity_entity_id(registry): + """Test entity's entity_id is updated.""" + entry = registry.async_get_or_create("light", "hue", "5678") + assert registry.async_get_entity_id("light", "hue", "5678") == entry.entity_id + + new_entity_id = "light.blah" + assert new_entity_id != entry.entity_id + with patch.object(registry, "async_schedule_save") as mock_schedule_save: + updated_entry = registry.async_update_entity( + entry.entity_id, new_entity_id=new_entity_id + ) + assert updated_entry != entry + assert updated_entry.entity_id == new_entity_id + assert mock_schedule_save.call_count == 1 + + assert registry.async_get(entry.entity_id) is None + assert registry.async_get(new_entity_id) is not None + + +async def test_update_entity_entity_id_entity_id(hass: HomeAssistant, registry): + """Test update raises when entity_id already in use.""" + entry = registry.async_get_or_create("light", "hue", "5678") + entry2 = registry.async_get_or_create("light", "hue", "1234") + state_entity_id = "light.blah" + hass.states.async_set(state_entity_id, "on") + assert entry.entity_id != state_entity_id + assert entry2.entity_id != state_entity_id + + # Try updating to a registered entity_id + with patch.object( + registry, "async_schedule_save" + ) as mock_schedule_save, pytest.raises(ValueError): + registry.async_update_entity(entry.entity_id, new_entity_id=entry2.entity_id) + assert mock_schedule_save.call_count == 0 + assert registry.async_get_entity_id("light", "hue", "5678") == entry.entity_id + assert registry.async_get(entry.entity_id) is entry + assert registry.async_get_entity_id("light", "hue", "1234") == entry2.entity_id + assert registry.async_get(entry2.entity_id) is entry2 + + # Try updating to an entity_id which is in the state machine + with patch.object( + registry, "async_schedule_save" + ) as mock_schedule_save, pytest.raises(ValueError): + registry.async_update_entity(entry.entity_id, new_entity_id=state_entity_id) + assert mock_schedule_save.call_count == 0 + assert registry.async_get_entity_id("light", "hue", "5678") == entry.entity_id + assert registry.async_get(entry.entity_id) is entry + assert registry.async_get(state_entity_id) is None + + async def test_update_entity(registry): """Test updating entity.""" mock_config = MockConfigEntry(domain="light", entry_id="mock-id-1") From 3d3aa824b339553fd696663bee989ebcffc97219 Mon Sep 17 00:00:00 2001 From: Jack Boswell Date: Wed, 28 Sep 2022 19:46:13 +1300 Subject: [PATCH 797/955] Refactor Trend to use `async_setup_platform` (#78216) * Move trend constants to const.py * Migrate to async_setup_platform * Fix test * Reorder attrs --- homeassistant/components/trend/__init__.py | 1 - .../components/trend/binary_sensor.py | 37 ++++++++++--------- homeassistant/components/trend/const.py | 14 +++++++ tests/components/trend/test_binary_sensor.py | 2 +- 4 files changed, 34 insertions(+), 20 deletions(-) create mode 100644 homeassistant/components/trend/const.py diff --git a/homeassistant/components/trend/__init__.py b/homeassistant/components/trend/__init__.py index a56bb00d97b..b583f424da1 100644 --- a/homeassistant/components/trend/__init__.py +++ b/homeassistant/components/trend/__init__.py @@ -2,5 +2,4 @@ from homeassistant.const import Platform -DOMAIN = "trend" PLATFORMS = [Platform.BINARY_SENSOR] diff --git a/homeassistant/components/trend/binary_sensor.py b/homeassistant/components/trend/binary_sensor.py index b98d904cabd..e43032a580f 100644 --- a/homeassistant/components/trend/binary_sensor.py +++ b/homeassistant/components/trend/binary_sensor.py @@ -30,26 +30,26 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event -from homeassistant.helpers.reload import setup_reload_service +from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util.dt import utcnow -from . import DOMAIN, PLATFORMS +from . import PLATFORMS +from .const import ( + ATTR_GRADIENT, + ATTR_INVERT, + ATTR_MIN_GRADIENT, + ATTR_SAMPLE_COUNT, + ATTR_SAMPLE_DURATION, + CONF_INVERT, + CONF_MAX_SAMPLES, + CONF_MIN_GRADIENT, + CONF_SAMPLE_DURATION, + DOMAIN, +) _LOGGER = logging.getLogger(__name__) -ATTR_ATTRIBUTE = "attribute" -ATTR_GRADIENT = "gradient" -ATTR_MIN_GRADIENT = "min_gradient" -ATTR_INVERT = "invert" -ATTR_SAMPLE_DURATION = "sample_duration" -ATTR_SAMPLE_COUNT = "sample_count" - -CONF_INVERT = "invert" -CONF_MAX_SAMPLES = "max_samples" -CONF_MIN_GRADIENT = "min_gradient" -CONF_SAMPLE_DURATION = "sample_duration" - SENSOR_SCHEMA = vol.Schema( { vol.Required(CONF_ENTITY_ID): cv.entity_id, @@ -68,14 +68,14 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -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 trend sensors.""" - setup_reload_service(hass, DOMAIN, PLATFORMS) + await async_setup_reload_service(hass, DOMAIN, PLATFORMS) sensors = [] @@ -106,7 +106,8 @@ def setup_platform( if not sensors: _LOGGER.error("No sensors added") return - add_entities(sensors) + + async_add_entities(sensors) class SensorTrend(BinarySensorEntity): diff --git a/homeassistant/components/trend/const.py b/homeassistant/components/trend/const.py new file mode 100644 index 00000000000..6787dc08445 --- /dev/null +++ b/homeassistant/components/trend/const.py @@ -0,0 +1,14 @@ +"""Constant values for Trend integration.""" +DOMAIN = "trend" + +ATTR_ATTRIBUTE = "attribute" +ATTR_GRADIENT = "gradient" +ATTR_INVERT = "invert" +ATTR_MIN_GRADIENT = "min_gradient" +ATTR_SAMPLE_DURATION = "sample_duration" +ATTR_SAMPLE_COUNT = "sample_count" + +CONF_INVERT = "invert" +CONF_MAX_SAMPLES = "max_samples" +CONF_MIN_GRADIENT = "min_gradient" +CONF_SAMPLE_DURATION = "sample_duration" diff --git a/tests/components/trend/test_binary_sensor.py b/tests/components/trend/test_binary_sensor.py index f7ccee97b29..1439ca80e9f 100644 --- a/tests/components/trend/test_binary_sensor.py +++ b/tests/components/trend/test_binary_sensor.py @@ -3,7 +3,7 @@ from datetime import timedelta from unittest.mock import patch from homeassistant import config as hass_config, setup -from homeassistant.components.trend import DOMAIN +from homeassistant.components.trend.const import DOMAIN from homeassistant.const import SERVICE_RELOAD, STATE_UNKNOWN import homeassistant.util.dt as dt_util From 18be5f1387f44b325f24227d5e09c6cbbaabf60e Mon Sep 17 00:00:00 2001 From: G Johansson Date: Wed, 28 Sep 2022 08:49:37 +0200 Subject: [PATCH 798/955] SQL fix entry options save (#78145) * SQL fix options * Testing --- homeassistant/components/sql/config_flow.py | 5 +++-- tests/components/sql/test_config_flow.py | 9 ++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/sql/config_flow.py b/homeassistant/components/sql/config_flow.py index bcbece9f7f6..d8f4df814fc 100644 --- a/homeassistant/components/sql/config_flow.py +++ b/homeassistant/components/sql/config_flow.py @@ -155,6 +155,7 @@ class SQLOptionsFlowHandler(config_entries.OptionsFlow): db_url = user_input.get(CONF_DB_URL, db_url_default) query = user_input[CONF_QUERY] column = user_input[CONF_COLUMN_NAME] + name = self.entry.options.get(CONF_NAME, self.entry.title) try: validate_sql_select(query) @@ -169,8 +170,8 @@ class SQLOptionsFlowHandler(config_entries.OptionsFlow): return self.async_create_entry( title="", data={ - CONF_NAME: self.entry.title, - **self.entry.options, + CONF_NAME: name, + CONF_DB_URL: db_url, **user_input, }, ) diff --git a/tests/components/sql/test_config_flow.py b/tests/components/sql/test_config_flow.py index 96402e1bc7a..ea54745048e 100644 --- a/tests/components/sql/test_config_flow.py +++ b/tests/components/sql/test_config_flow.py @@ -6,6 +6,7 @@ from unittest.mock import patch from sqlalchemy.exc import SQLAlchemyError from homeassistant import config_entries +from homeassistant.components.recorder import DEFAULT_DB_FILE, DEFAULT_URL from homeassistant.components.sql.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -213,7 +214,6 @@ async def test_options_flow(hass: HomeAssistant, recorder_mock) -> None: "db_url": "sqlite://", "query": "SELECT 5 as size", "column": "size", - "value_template": None, "unit_of_measurement": "MiB", } @@ -266,7 +266,6 @@ async def test_options_flow_name_previously_removed( "db_url": "sqlite://", "query": "SELECT 5 as size", "column": "size", - "value_template": None, "unit_of_measurement": "MiB", } @@ -363,7 +362,6 @@ async def test_options_flow_fails_invalid_query( assert result4["type"] == FlowResultType.CREATE_ENTRY assert result4["data"] == { "name": "Get Value", - "value_template": None, "db_url": "sqlite://", "query": "SELECT 5 as size", "column": "size", @@ -415,12 +413,13 @@ async def test_options_flow_db_url_empty(hass: HomeAssistant, recorder_mock) -> ) await hass.async_block_till_done() + db_url = DEFAULT_URL.format(hass_config_path=hass.config.path(DEFAULT_DB_FILE)) + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { "name": "Get Value", - "db_url": "sqlite://", + "db_url": db_url, "query": "SELECT 5 as size", "column": "size", - "value_template": None, "unit_of_measurement": "MiB", } From 52307708c843b947a2d631f2fe7ddaa8bd9a90d7 Mon Sep 17 00:00:00 2001 From: Yuxin Wang Date: Wed, 28 Sep 2022 03:14:04 -0400 Subject: [PATCH 799/955] Refactor apcupsd to use config flow (#64809) * Add Config Flow to APCUPSd integration and remove YAML support. * Hide the binary sensor if user does not select STATFLAG resource. * Add tests for config flows. * Simplify config flow code. * Spell fix. * Fix pylint warnings. * Simplify the code for config flow. * First attempt to implement import flows to suppport legacy YAML configurations. * Remove unnecessary log calls. * Wrap synchronous update call with `hass.async_add_executor_job`. * Import the YAML configurations when sensor platform is set up. * Move the logger call since the variables are not properly set up. * Add codeowner. * Fix name field of manifest.json. * Fix linting issue. * Fix incorrect dependency due to incorrect rebase. * Update codeowner and config flows via hassfest. * Postpone the deprecation warning to 2022.7. * Import future annotations for init file. * Add an newline at the end to make prettier happy. * Update github id. * Add type hints for return types of steps in config flow. * Move the deprecation date for YAML config to 2022.12. * Update according to reviews. * Use async_forward_entry_setups. * Add helper properties to `APCUPSdData` class. * Add device_info for binary sensor. * Simplify config flow. * Remove options flow strings. * update the tests according to the changes. * Add `entity_registry_enabled_default` to entities and use imported CONF_RESOURCES to disable entities instead of skipping them. * Update according to reviews. * Do not use model of the UPS as the title for the integration. Instead, simply use "APCUPSd" as the integration title and let the device info serve as title for each device instead. * Change schema to be a global variable. * Add more comments. * Rewrite the tests for config flows. * Fix enabled_by_default. * Show friendly titles in the integration. * Add import check in `async_setup_platform` to avoid importing in sensor platform setup. * Add import check in `async_setup_platform` to avoid importing in sensor platform setup. * Update comments in test files. * Use parametrize instead of manually iterating different test cases. * Swap the order of the platform constants. * Avoid using broad exceptions. * Set up device info via `_attr_device_info`. * Remove unrelated test in `test_config_flow`. * Use `DeviceInfo` instead of dict to assign to `_attr_device_info`. * Add english translation. * Add `async_create_issue` for deprecated YAML configuration. * Enable UPS status by default since it could show "online, charging, on battery etc" which is meaningful for all users. * Apply suggestions from code review * Apply suggestion * Apply suggestion Co-authored-by: Martin Hjelmare --- .coveragerc | 4 +- CODEOWNERS | 2 + homeassistant/components/apcupsd/__init__.py | 146 ++++-- .../components/apcupsd/binary_sensor.py | 72 ++- .../components/apcupsd/config_flow.py | 86 ++++ .../components/apcupsd/manifest.json | 5 +- homeassistant/components/apcupsd/sensor.py | 440 +++++++++++------- homeassistant/components/apcupsd/strings.json | 26 ++ .../components/apcupsd/translations/en.json | 26 ++ homeassistant/generated/config_flows.py | 1 + requirements_test_all.txt | 3 + tests/components/apcupsd/__init__.py | 66 +++ tests/components/apcupsd/test_config_flow.py | 185 ++++++++ 13 files changed, 838 insertions(+), 224 deletions(-) create mode 100644 homeassistant/components/apcupsd/config_flow.py create mode 100644 homeassistant/components/apcupsd/strings.json create mode 100644 homeassistant/components/apcupsd/translations/en.json create mode 100644 tests/components/apcupsd/__init__.py create mode 100644 tests/components/apcupsd/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 5c5d88b42c4..b5111024120 100644 --- a/.coveragerc +++ b/.coveragerc @@ -62,7 +62,9 @@ omit = homeassistant/components/androidtv/diagnostics.py homeassistant/components/anel_pwrctrl/switch.py homeassistant/components/anthemav/media_player.py - homeassistant/components/apcupsd/* + homeassistant/components/apcupsd/__init__.py + homeassistant/components/apcupsd/binary_sensor.py + homeassistant/components/apcupsd/sensor.py homeassistant/components/apple_tv/__init__.py homeassistant/components/apple_tv/browse_media.py homeassistant/components/apple_tv/media_player.py diff --git a/CODEOWNERS b/CODEOWNERS index 3d610993dfe..5c39337af74 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -78,6 +78,8 @@ build.json @home-assistant/supervisor /tests/components/anthemav/ @hyralex /homeassistant/components/apache_kafka/ @bachya /tests/components/apache_kafka/ @bachya +/homeassistant/components/apcupsd/ @yuxincs +/tests/components/apcupsd/ @yuxincs /homeassistant/components/api/ @home-assistant/core /tests/components/api/ @home-assistant/core /homeassistant/components/apple_tv/ @postlund diff --git a/homeassistant/components/apcupsd/__init__.py b/homeassistant/components/apcupsd/__init__.py index 7cbf33f8b47..1e76d070a48 100644 --- a/homeassistant/components/apcupsd/__init__.py +++ b/homeassistant/components/apcupsd/__init__.py @@ -1,11 +1,15 @@ """Support for APCUPSd via its Network Information Server (NIS).""" +from __future__ import annotations + from datetime import timedelta import logging +from typing import Any, Final from apcaccess import status import voluptuous as vol -from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import CONF_HOST, CONF_PORT, Platform from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType @@ -13,22 +17,18 @@ from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) -DEFAULT_HOST = "localhost" -DEFAULT_PORT = 3551 -DOMAIN = "apcupsd" +DOMAIN: Final = "apcupsd" +VALUE_ONLINE: Final = 8 +PLATFORMS: Final = (Platform.BINARY_SENSOR, Platform.SENSOR) +MIN_TIME_BETWEEN_UPDATES: Final = timedelta(seconds=60) -KEY_STATUS = "STATFLAG" - -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) - -VALUE_ONLINE = 8 CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( { - vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_HOST, default="localhost"): cv.string, + vol.Optional(CONF_PORT, default=3551): cv.port, } ) }, @@ -36,25 +36,67 @@ CONFIG_SCHEMA = vol.Schema( ) -def setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Use config values to set up a function enabling status retrieval.""" - conf = config[DOMAIN] - host = conf[CONF_HOST] - port = conf[CONF_PORT] +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up integration from legacy YAML configurations.""" + conf = config.get(DOMAIN) + if conf is None: + return True - apcups_data = APCUPSdData(host, port) - hass.data[DOMAIN] = apcups_data + # We only import configs from YAML if it hasn't been imported. If there is a config + # entry marked with SOURCE_IMPORT, it means the YAML config has been imported. + for entry in hass.config_entries.async_entries(DOMAIN): + if entry.source == SOURCE_IMPORT: + return True - # It doesn't really matter why we're not able to get the status, just that - # we can't. - try: - apcups_data.update(no_throttle=True) - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Failure while testing APCUPSd status retrieval") - return False + # Since the YAML configuration for apcupsd consists of two parts: + # apcupsd: + # host: xxx + # port: xxx + # sensor: + # - platform: apcupsd + # resource: + # - resource_1 + # - resource_2 + # - ... + # Here at the integration set up we do not have the entire information to be + # imported to config flow yet. So we temporarily store the configuration to + # hass.data[DOMAIN] under a special entry_id SOURCE_IMPORT (which shouldn't + # conflict with other entry ids). Later when the sensor platform setup is + # called we gather the resources information and from there we start the + # actual config entry imports. + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][SOURCE_IMPORT] = conf return True +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Use config values to set up a function enabling status retrieval.""" + data_service = APCUPSdData( + config_entry.data[CONF_HOST], config_entry.data[CONF_PORT] + ) + + try: + await hass.async_add_executor_job(data_service.update) + except OSError as ex: + _LOGGER.error("Failure while testing APCUPSd status retrieval: %s", ex) + return False + + # Store the data service object. + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][config_entry.entry_id] = data_service + + # Forward the config entries to the supported platforms. + 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) + hass.data[DOMAIN].pop(entry.entry_id) + return unload_ok + + class APCUPSdData: """Stores the data retrieved from APCUPSd. @@ -62,26 +104,52 @@ class APCUPSdData: updates from the server. """ - def __init__(self, host, port): + def __init__(self, host: str, port: int) -> None: """Initialize the data object.""" - self._host = host self._port = port - self._status = None - self._get = status.get - self._parse = status.parse + self.status: dict[str, Any] = {} @property - def status(self): - """Get latest update if throttle allows. Return status.""" - self.update() - return self._status + def name(self) -> str | None: + """Return the name of the UPS, if available.""" + return self.status.get("UPSNAME") - def _get_status(self): - """Get the status from APCUPSd and parse it into a dict.""" - return self._parse(self._get(host=self._host, port=self._port)) + @property + def model(self) -> str | None: + """Return the model of the UPS, if available.""" + # Different UPS models may report slightly different keys for model, here we + # try them all. + for model_key in ("APCMODEL", "MODEL"): + if model_key in self.status: + return self.status[model_key] + return None + + @property + def sw_version(self) -> str | None: + """Return the software version of the APCUPSd, if available.""" + return self.status.get("VERSION") + + @property + def hw_version(self) -> str | None: + """Return the firmware version of the UPS, if available.""" + return self.status.get("FIRMWARE") + + @property + def serial_no(self) -> str | None: + """Return the unique serial number of the UPS, if available.""" + return self.status.get("SERIALNO") + + @property + def statflag(self) -> str | None: + """Return the STATFLAG indicating the status of the UPS, if available.""" + return self.status.get("STATFLAG") @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self, **kwargs): - """Fetch the latest status from APCUPSd.""" - self._status = self._get_status() + """Fetch the latest status from APCUPSd. + + Note that the result dict uses upper case for each resource, where our + integration uses lower cases as keys internally. + """ + self.status = status.parse(status.get(host=self._host, port=self._port)) diff --git a/homeassistant/components/apcupsd/binary_sensor.py b/homeassistant/components/apcupsd/binary_sensor.py index 25d50c06c97..7438e3236d4 100644 --- a/homeassistant/components/apcupsd/binary_sensor.py +++ b/homeassistant/components/apcupsd/binary_sensor.py @@ -1,43 +1,75 @@ """Support for tracking the online status of a UPS.""" from __future__ import annotations -import voluptuous as vol +import logging -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity -from homeassistant.const import CONF_NAME +from homeassistant.components.binary_sensor import ( + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import DOMAIN, KEY_STATUS, VALUE_ONLINE +from . import DOMAIN, VALUE_ONLINE, APCUPSdData -DEFAULT_NAME = "UPS Online Status" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string} +_LOGGER = logging.getLogger(__name__) +_DESCRIPTION = BinarySensorEntityDescription( + key="statflag", + name="UPS Online Status", + icon="mdi:heart", ) -def setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up an APCUPSd Online Status binary sensor.""" - apcups_data = hass.data[DOMAIN] + data_service: APCUPSdData = hass.data[DOMAIN][config_entry.entry_id] - add_entities([OnlineStatus(config, apcups_data)], True) + # Do not create the binary sensor if APCUPSd does not provide STATFLAG field for us + # to determine the online status. + if data_service.statflag is None: + return + + async_add_entities( + [OnlineStatus(data_service, _DESCRIPTION)], + update_before_add=True, + ) class OnlineStatus(BinarySensorEntity): - """Representation of an UPS online status.""" + """Representation of a UPS online status.""" - def __init__(self, config, data): + def __init__( + self, + data_service: APCUPSdData, + description: BinarySensorEntityDescription, + ) -> None: """Initialize the APCUPSd binary device.""" - self._data = data - self._attr_name = config[CONF_NAME] + # Set up unique id and device info if serial number is available. + if (serial_no := data_service.serial_no) is not None: + self._attr_unique_id = f"{serial_no}_{description.key}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, serial_no)}, + model=data_service.model, + manufacturer="APC", + hw_version=data_service.hw_version, + sw_version=data_service.sw_version, + ) + self.entity_description = description + self._data_service = data_service def update(self) -> None: """Get the status report from APCUPSd and set this entity's state.""" - self._attr_is_on = int(self._data.status[KEY_STATUS], 16) & VALUE_ONLINE > 0 + self._data_service.update() + + key = self.entity_description.key.upper() + if key not in self._data_service.status: + self._attr_is_on = None + return + + self._attr_is_on = int(self._data_service.status[key], 16) & VALUE_ONLINE > 0 diff --git a/homeassistant/components/apcupsd/config_flow.py b/homeassistant/components/apcupsd/config_flow.py new file mode 100644 index 00000000000..a191cf77117 --- /dev/null +++ b/homeassistant/components/apcupsd/config_flow.py @@ -0,0 +1,86 @@ +"""Config flow for APCUPSd integration.""" +from __future__ import annotations + +from typing import Any + +import voluptuous as vol + +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import selector +import homeassistant.helpers.config_validation as cv + +from . import DOMAIN, APCUPSdData + +_PORT_SELECTOR = vol.All( + selector.NumberSelector( + selector.NumberSelectorConfig( + min=1, max=65535, mode=selector.NumberSelectorMode.BOX + ), + ), + vol.Coerce(int), +) + +_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST, default="localhost"): cv.string, + vol.Required(CONF_PORT, default=3551): _PORT_SELECTOR, + } +) + + +class ConfigFlowHandler(ConfigFlow, domain=DOMAIN): + """APCUPSd integration config flow.""" + + 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=_SCHEMA) + + # Abort if an entry with same host and port is present. + self._async_abort_entries_match( + {CONF_HOST: user_input[CONF_HOST], CONF_PORT: user_input[CONF_PORT]} + ) + + # Test the connection to the host and get the current status for serial number. + data_service = APCUPSdData(user_input[CONF_HOST], user_input[CONF_PORT]) + try: + await self.hass.async_add_executor_job(data_service.update) + except OSError: + errors = {"base": "cannot_connect"} + return self.async_show_form( + step_id="user", data_schema=_SCHEMA, errors=errors + ) + + if not data_service.status: + return self.async_abort(reason="no_status") + + # We _try_ to use the serial number of the UPS as the unique id since this field + # is not guaranteed to exist on all APC UPS models. + await self.async_set_unique_id(data_service.serial_no) + self._abort_if_unique_id_configured() + + title = "APC UPS" + if data_service.name is not None: + title = data_service.name + elif data_service.model is not None: + title = data_service.model + elif data_service.serial_no is not None: + title = data_service.serial_no + + return self.async_create_entry( + title=title, + data=user_input, + ) + + async def async_step_import(self, conf: dict[str, Any]) -> FlowResult: + """Import a configuration from yaml configuration.""" + # If we are importing from YAML configuration, user_input could contain a + # CONF_RESOURCES with a list of resources (sensors) to be enabled. + return await self.async_step_user(user_input=conf) diff --git a/homeassistant/components/apcupsd/manifest.json b/homeassistant/components/apcupsd/manifest.json index 13a08685c68..aeead681e1b 100644 --- a/homeassistant/components/apcupsd/manifest.json +++ b/homeassistant/components/apcupsd/manifest.json @@ -1,9 +1,10 @@ { "domain": "apcupsd", - "name": "apcupsd", + "name": "APC UPS Daemon", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/apcupsd", "requirements": ["apcaccess==0.0.13"], - "codeowners": [], + "codeowners": ["@yuxincs"], "iot_class": "local_polling", "loggers": ["apcaccess"] } diff --git a/homeassistant/components/apcupsd/sensor.py b/homeassistant/components/apcupsd/sensor.py index 0f85c4c6854..5f76e6c1afa 100644 --- a/homeassistant/components/apcupsd/sensor.py +++ b/homeassistant/components/apcupsd/sensor.py @@ -12,7 +12,10 @@ from homeassistant.components.sensor import ( SensorEntity, SensorEntityDescription, ) +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( + CONF_HOST, + CONF_PORT, CONF_RESOURCES, ELECTRIC_CURRENT_AMPERE, ELECTRIC_POTENTIAL_VOLT, @@ -26,368 +29,387 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import DOMAIN +from . import DOMAIN, APCUPSdData _LOGGER = logging.getLogger(__name__) -SENSOR_PREFIX = "UPS " -SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( - SensorEntityDescription( +SENSORS: dict[str, SensorEntityDescription] = { + "alarmdel": SensorEntityDescription( key="alarmdel", - name="Alarm Delay", + name="UPS Alarm Delay", icon="mdi:alarm", ), - SensorEntityDescription( + "ambtemp": SensorEntityDescription( key="ambtemp", - name="Ambient Temperature", + name="UPS Ambient Temperature", icon="mdi:thermometer", ), - SensorEntityDescription( + "apc": SensorEntityDescription( key="apc", - name="Status Data", + name="UPS Status Data", icon="mdi:information-outline", + entity_registry_enabled_default=False, ), - SensorEntityDescription( + "apcmodel": SensorEntityDescription( key="apcmodel", - name="Model", + name="UPS Model", icon="mdi:information-outline", + entity_registry_enabled_default=False, ), - SensorEntityDescription( + "badbatts": SensorEntityDescription( key="badbatts", - name="Bad Batteries", + name="UPS Bad Batteries", icon="mdi:information-outline", ), - SensorEntityDescription( + "battdate": SensorEntityDescription( key="battdate", - name="Battery Replaced", + name="UPS Battery Replaced", icon="mdi:calendar-clock", ), - SensorEntityDescription( + "battstat": SensorEntityDescription( key="battstat", - name="Battery Status", + name="UPS Battery Status", icon="mdi:information-outline", ), - SensorEntityDescription( + "battv": SensorEntityDescription( key="battv", - name="Battery Voltage", + name="UPS Battery Voltage", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, icon="mdi:flash", ), - SensorEntityDescription( + "bcharge": SensorEntityDescription( key="bcharge", - name="Battery", + name="UPS Battery", native_unit_of_measurement=PERCENTAGE, icon="mdi:battery", ), - SensorEntityDescription( + "cable": SensorEntityDescription( key="cable", - name="Cable Type", + name="UPS Cable Type", icon="mdi:ethernet-cable", + entity_registry_enabled_default=False, ), - SensorEntityDescription( + "cumonbatt": SensorEntityDescription( key="cumonbatt", - name="Total Time on Battery", + name="UPS Total Time on Battery", icon="mdi:timer-outline", ), - SensorEntityDescription( + "date": SensorEntityDescription( key="date", - name="Status Date", + name="UPS Status Date", icon="mdi:calendar-clock", + entity_registry_enabled_default=False, ), - SensorEntityDescription( + "dipsw": SensorEntityDescription( key="dipsw", - name="Dip Switch Settings", + name="UPS Dip Switch Settings", icon="mdi:information-outline", ), - SensorEntityDescription( + "dlowbatt": SensorEntityDescription( key="dlowbatt", - name="Low Battery Signal", + name="UPS Low Battery Signal", icon="mdi:clock-alert", ), - SensorEntityDescription( + "driver": SensorEntityDescription( key="driver", - name="Driver", + name="UPS Driver", icon="mdi:information-outline", + entity_registry_enabled_default=False, ), - SensorEntityDescription( + "dshutd": SensorEntityDescription( key="dshutd", - name="Shutdown Delay", + name="UPS Shutdown Delay", icon="mdi:timer-outline", ), - SensorEntityDescription( + "dwake": SensorEntityDescription( key="dwake", - name="Wake Delay", + name="UPS Wake Delay", icon="mdi:timer-outline", ), - SensorEntityDescription( + "end apc": SensorEntityDescription( key="end apc", - name="Date and Time", + name="UPS Date and Time", icon="mdi:calendar-clock", + entity_registry_enabled_default=False, ), - SensorEntityDescription( + "extbatts": SensorEntityDescription( key="extbatts", - name="External Batteries", + name="UPS External Batteries", icon="mdi:information-outline", ), - SensorEntityDescription( + "firmware": SensorEntityDescription( key="firmware", - name="Firmware Version", + name="UPS Firmware Version", icon="mdi:information-outline", + entity_registry_enabled_default=False, ), - SensorEntityDescription( + "hitrans": SensorEntityDescription( key="hitrans", - name="Transfer High", + name="UPS Transfer High", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, icon="mdi:flash", ), - SensorEntityDescription( + "hostname": SensorEntityDescription( key="hostname", - name="Hostname", + name="UPS Hostname", icon="mdi:information-outline", + entity_registry_enabled_default=False, ), - SensorEntityDescription( + "humidity": SensorEntityDescription( key="humidity", - name="Ambient Humidity", + name="UPS Ambient Humidity", native_unit_of_measurement=PERCENTAGE, icon="mdi:water-percent", ), - SensorEntityDescription( + "itemp": SensorEntityDescription( key="itemp", - name="Internal Temperature", + name="UPS Internal Temperature", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, ), - SensorEntityDescription( + "lastxfer": SensorEntityDescription( key="lastxfer", - name="Last Transfer", + name="UPS Last Transfer", icon="mdi:transfer", + entity_registry_enabled_default=False, ), - SensorEntityDescription( + "linefail": SensorEntityDescription( key="linefail", - name="Input Voltage Status", + name="UPS Input Voltage Status", icon="mdi:information-outline", ), - SensorEntityDescription( + "linefreq": SensorEntityDescription( key="linefreq", - name="Line Frequency", + name="UPS Line Frequency", native_unit_of_measurement=FREQUENCY_HERTZ, icon="mdi:information-outline", ), - SensorEntityDescription( + "linev": SensorEntityDescription( key="linev", - name="Input Voltage", + name="UPS Input Voltage", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, icon="mdi:flash", ), - SensorEntityDescription( + "loadpct": SensorEntityDescription( key="loadpct", - name="Load", + name="UPS Load", native_unit_of_measurement=PERCENTAGE, icon="mdi:gauge", ), - SensorEntityDescription( + "loadapnt": SensorEntityDescription( key="loadapnt", - name="Load Apparent Power", + name="UPS Load Apparent Power", native_unit_of_measurement=PERCENTAGE, icon="mdi:gauge", ), - SensorEntityDescription( + "lotrans": SensorEntityDescription( key="lotrans", - name="Transfer Low", + name="UPS Transfer Low", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, icon="mdi:flash", ), - SensorEntityDescription( + "mandate": SensorEntityDescription( key="mandate", - name="Manufacture Date", + name="UPS Manufacture Date", icon="mdi:calendar", + entity_registry_enabled_default=False, ), - SensorEntityDescription( + "masterupd": SensorEntityDescription( key="masterupd", - name="Master Update", + name="UPS Master Update", icon="mdi:information-outline", ), - SensorEntityDescription( + "maxlinev": SensorEntityDescription( key="maxlinev", - name="Input Voltage High", + name="UPS Input Voltage High", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, icon="mdi:flash", ), - SensorEntityDescription( + "maxtime": SensorEntityDescription( key="maxtime", - name="Battery Timeout", + name="UPS Battery Timeout", icon="mdi:timer-off-outline", ), - SensorEntityDescription( + "mbattchg": SensorEntityDescription( key="mbattchg", - name="Battery Shutdown", + name="UPS Battery Shutdown", native_unit_of_measurement=PERCENTAGE, icon="mdi:battery-alert", ), - SensorEntityDescription( + "minlinev": SensorEntityDescription( key="minlinev", - name="Input Voltage Low", + name="UPS Input Voltage Low", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, icon="mdi:flash", ), - SensorEntityDescription( + "mintimel": SensorEntityDescription( key="mintimel", - name="Shutdown Time", + name="UPS Shutdown Time", icon="mdi:timer-outline", ), - SensorEntityDescription( + "model": SensorEntityDescription( key="model", - name="Model", + name="UPS Model", icon="mdi:information-outline", + entity_registry_enabled_default=False, ), - SensorEntityDescription( + "nombattv": SensorEntityDescription( key="nombattv", - name="Battery Nominal Voltage", + name="UPS Battery Nominal Voltage", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, icon="mdi:flash", ), - SensorEntityDescription( + "nominv": SensorEntityDescription( key="nominv", - name="Nominal Input Voltage", + name="UPS Nominal Input Voltage", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, icon="mdi:flash", ), - SensorEntityDescription( + "nomoutv": SensorEntityDescription( key="nomoutv", - name="Nominal Output Voltage", + name="UPS Nominal Output Voltage", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, icon="mdi:flash", ), - SensorEntityDescription( + "nompower": SensorEntityDescription( key="nompower", - name="Nominal Output Power", + name="UPS Nominal Output Power", native_unit_of_measurement=POWER_WATT, icon="mdi:flash", ), - SensorEntityDescription( + "nomapnt": SensorEntityDescription( key="nomapnt", - name="Nominal Apparent Power", + name="UPS Nominal Apparent Power", native_unit_of_measurement=POWER_VOLT_AMPERE, icon="mdi:flash", ), - SensorEntityDescription( + "numxfers": SensorEntityDescription( key="numxfers", - name="Transfer Count", + name="UPS Transfer Count", icon="mdi:counter", ), - SensorEntityDescription( + "outcurnt": SensorEntityDescription( key="outcurnt", - name="Output Current", + name="UPS Output Current", native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, icon="mdi:flash", ), - SensorEntityDescription( + "outputv": SensorEntityDescription( key="outputv", - name="Output Voltage", + name="UPS Output Voltage", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, icon="mdi:flash", ), - SensorEntityDescription( + "reg1": SensorEntityDescription( key="reg1", - name="Register 1 Fault", + name="UPS Register 1 Fault", icon="mdi:information-outline", + entity_registry_enabled_default=False, ), - SensorEntityDescription( + "reg2": SensorEntityDescription( key="reg2", - name="Register 2 Fault", + name="UPS Register 2 Fault", icon="mdi:information-outline", + entity_registry_enabled_default=False, ), - SensorEntityDescription( + "reg3": SensorEntityDescription( key="reg3", - name="Register 3 Fault", + name="UPS Register 3 Fault", icon="mdi:information-outline", + entity_registry_enabled_default=False, ), - SensorEntityDescription( + "retpct": SensorEntityDescription( key="retpct", - name="Restore Requirement", + name="UPS Restore Requirement", native_unit_of_measurement=PERCENTAGE, icon="mdi:battery-alert", ), - SensorEntityDescription( + "selftest": SensorEntityDescription( key="selftest", - name="Last Self Test", + name="UPS Last Self Test", icon="mdi:calendar-clock", ), - SensorEntityDescription( + "sense": SensorEntityDescription( key="sense", - name="Sensitivity", + name="UPS Sensitivity", icon="mdi:information-outline", + entity_registry_enabled_default=False, ), - SensorEntityDescription( + "serialno": SensorEntityDescription( key="serialno", - name="Serial Number", + name="UPS Serial Number", icon="mdi:information-outline", + entity_registry_enabled_default=False, ), - SensorEntityDescription( + "starttime": SensorEntityDescription( key="starttime", - name="Startup Time", + name="UPS Startup Time", icon="mdi:calendar-clock", ), - SensorEntityDescription( + "statflag": SensorEntityDescription( key="statflag", - name="Status Flag", + name="UPS Status Flag", icon="mdi:information-outline", + entity_registry_enabled_default=False, ), - SensorEntityDescription( + "status": SensorEntityDescription( key="status", - name="Status", + name="UPS Status", icon="mdi:information-outline", ), - SensorEntityDescription( + "stesti": SensorEntityDescription( key="stesti", - name="Self Test Interval", + name="UPS Self Test Interval", icon="mdi:information-outline", ), - SensorEntityDescription( + "timeleft": SensorEntityDescription( key="timeleft", - name="Time Left", + name="UPS Time Left", icon="mdi:clock-alert", ), - SensorEntityDescription( + "tonbatt": SensorEntityDescription( key="tonbatt", - name="Time on Battery", + name="UPS Time on Battery", icon="mdi:timer-outline", ), - SensorEntityDescription( + "upsmode": SensorEntityDescription( key="upsmode", - name="Mode", + name="UPS Mode", icon="mdi:information-outline", ), - SensorEntityDescription( + "upsname": SensorEntityDescription( key="upsname", - name="Name", + name="UPS Name", icon="mdi:information-outline", + entity_registry_enabled_default=False, ), - SensorEntityDescription( + "version": SensorEntityDescription( key="version", - name="Daemon Info", + name="UPS Daemon Info", icon="mdi:information-outline", + entity_registry_enabled_default=False, ), - SensorEntityDescription( + "xoffbat": SensorEntityDescription( key="xoffbat", - name="Transfer from Battery", + name="UPS Transfer from Battery", icon="mdi:transfer", ), - SensorEntityDescription( + "xoffbatt": SensorEntityDescription( key="xoffbatt", - name="Transfer from Battery", + name="UPS Transfer from Battery", icon="mdi:transfer", ), - SensorEntityDescription( + "xonbatt": SensorEntityDescription( key="xonbatt", - name="Transfer to Battery", + name="UPS Transfer to Battery", icon="mdi:transfer", ), -) -SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES] +} SPECIFIC_UNITS = {"ITEMP": TEMP_CELSIUS} INFERRED_UNITS = { @@ -406,36 +428,109 @@ INFERRED_UNITS = { PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_RESOURCES, default=[]): vol.All( - cv.ensure_list, [vol.In(SENSOR_KEYS)] + cv.ensure_list, [vol.In([desc.key for desc in SENSORS.values()])] ) } ) -def setup_platform( +async def async_setup_platform( hass: HomeAssistant, config: ConfigType, add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up the APCUPSd sensors.""" - apcups_data = hass.data[DOMAIN] - resources = config[CONF_RESOURCES] + """Import the configurations from YAML to config flows.""" + # We only import configs from YAML if it hasn't been imported. If there is a config + # entry marked with SOURCE_IMPORT, it means the YAML config has been imported. + for entry in hass.config_entries.async_entries(DOMAIN): + if entry.source == SOURCE_IMPORT: + return - for resource in resources: - if resource.upper() not in apcups_data.status: - _LOGGER.warning( - "Sensor type: %s does not appear in the APCUPSd status output", - resource, - ) + # This is the second step of YAML config imports, first see the comments in + # async_setup() of __init__.py to get an idea of how we import the YAML configs. + # Here we retrieve the partial YAML configs from the special entry id. + conf = hass.data[DOMAIN].get(SOURCE_IMPORT) + if conf is None: + return - entities = [ - APCUPSdSensor(apcups_data, description) - for description in SENSOR_TYPES - if description.key in resources - ] + _LOGGER.warning( + "Configuration of apcupsd in YAML is deprecated and will be " + "removed in Home Assistant 2022.12; Your existing configuration " + "has been imported into the UI automatically and can be safely removed " + "from your configuration.yaml file" + ) - add_entities(entities, True) + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2022.12.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + ) + + # Remove the artificial entry since it's no longer needed. + hass.data[DOMAIN].pop(SOURCE_IMPORT) + + # Our config flow supports CONF_RESOURCES and will properly import it to disable + # entities not listed in CONF_RESOURCES by default. Note that this designed to + # support YAML config import only (i.e., not shown in UI during setup). + conf[CONF_RESOURCES] = config[CONF_RESOURCES] + + _LOGGER.debug( + "YAML configurations loaded with host %s, port %s and resources %s", + conf[CONF_HOST], + conf[CONF_PORT], + conf[CONF_RESOURCES], + ) + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=conf + ) + ) + + return + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the APCUPSd sensors from config entries.""" + data_service: APCUPSdData = hass.data[DOMAIN][config_entry.entry_id] + + # The resources from data service are in upper-case by default, but we use + # lower cases throughout this integration. + available_resources: set[str] = {k.lower() for k, _ in data_service.status.items()} + + # We use user-specified resources from imported YAML config (if available) to + # determine whether to enable the entity by default. Here, we first collect the + # specified resources + specified_resources = None + if (resources := config_entry.data.get(CONF_RESOURCES)) is not None: + assert isinstance(resources, list) + specified_resources = set(resources) + + entities = [] + for resource in available_resources: + if resource not in SENSORS: + _LOGGER.warning("Invalid resource from APCUPSd: %s", resource.upper()) + continue + + # To avoid breaking changes, we disable sensors not specified in resources. + description = SENSORS[resource] + enabled_by_default = description.entity_registry_enabled_default + if specified_resources is not None: + enabled_by_default = resource in specified_resources + + entity = APCUPSdSensor(data_service, description, enabled_by_default) + entities.append(entity) + + async_add_entities(entities, update_before_add=True) def infer_unit(value): @@ -454,18 +549,39 @@ def infer_unit(value): class APCUPSdSensor(SensorEntity): """Representation of a sensor entity for APCUPSd status values.""" - def __init__(self, data, description: SensorEntityDescription): + def __init__( + self, + data_service: APCUPSdData, + description: SensorEntityDescription, + enabled_by_default: bool, + ) -> None: """Initialize the sensor.""" + # Set up unique id and device info if serial number is available. + if (serial_no := data_service.serial_no) is not None: + self._attr_unique_id = f"{serial_no}_{description.key}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, serial_no)}, + model=data_service.model, + manufacturer="APC", + hw_version=data_service.hw_version, + sw_version=data_service.sw_version, + ) + self.entity_description = description - self._data = data - self._attr_name = f"{SENSOR_PREFIX}{description.name}" + self._attr_entity_registry_enabled_default = enabled_by_default + self._data_service = data_service def update(self) -> None: """Get the latest status and use it to update our sensor state.""" + self._data_service.update() + key = self.entity_description.key.upper() - if key not in self._data.status: + if key not in self._data_service.status: self._attr_native_value = None - else: - self._attr_native_value, inferred_unit = infer_unit(self._data.status[key]) - if not self.native_unit_of_measurement: - self._attr_native_unit_of_measurement = inferred_unit + return + + self._attr_native_value, inferred_unit = infer_unit( + self._data_service.status[key] + ) + if not self.native_unit_of_measurement: + self._attr_native_unit_of_measurement = inferred_unit diff --git a/homeassistant/components/apcupsd/strings.json b/homeassistant/components/apcupsd/strings.json new file mode 100644 index 00000000000..1ca53c0e854 --- /dev/null +++ b/homeassistant/components/apcupsd/strings.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "no_status": "No status is reported from [%key:common::config_flow::data::host%]" + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]" + }, + "description": "Enter the host and port on which the apcupsd NIS is being served." + } + } + }, + "issues": { + "deprecated_yaml": { + "title": "The APC UPS Daemon YAML configuration is being removed", + "description": "Configuring APC UPS Daemon using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the APC UPS Daemon YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } + } +} diff --git a/homeassistant/components/apcupsd/translations/en.json b/homeassistant/components/apcupsd/translations/en.json new file mode 100644 index 00000000000..3674f3c69fe --- /dev/null +++ b/homeassistant/components/apcupsd/translations/en.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "no_status": "No status is reported from Host" + }, + "error": { + "cannot_connect": "Failed to connect" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port" + }, + "description": "Enter the host and port on which the apcupsd NIS is being served." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Configuring APC UPS Daemon using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the APC UPS Daemon YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The APC UPS Daemon 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 23da091c09c..ba6c76d329a 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -28,6 +28,7 @@ FLOWS = { "android_ip_webcam", "androidtv", "anthemav", + "apcupsd", "apple_tv", "arcam_fmj", "aseko_pool_live", diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7a347fcf059..62123ccce0b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -286,6 +286,9 @@ androidtv[async]==0.0.67 # homeassistant.components.anthemav anthemav==1.4.1 +# homeassistant.components.apcupsd +apcaccess==0.0.13 + # homeassistant.components.apprise apprise==1.0.0 diff --git a/tests/components/apcupsd/__init__.py b/tests/components/apcupsd/__init__.py new file mode 100644 index 00000000000..f6df277b37e --- /dev/null +++ b/tests/components/apcupsd/__init__.py @@ -0,0 +1,66 @@ +"""Tests for the APCUPSd component.""" +from collections import OrderedDict +from typing import Final + +from homeassistant.const import CONF_HOST, CONF_PORT + +CONF_DATA: Final = {CONF_HOST: "test", CONF_PORT: 1234} + +MOCK_STATUS: Final = OrderedDict( + [ + ("APC", "001,038,0985"), + ("DATE", "1970-01-01 00:00:00 0000"), + ("VERSION", "3.14.14 (31 May 2016) unknown"), + ("CABLE", "USB Cable"), + ("DRIVER", "USB UPS Driver"), + ("UPSMODE", "Stand Alone"), + ("MODEL", "Back-UPS ES 600"), + ("STATUS", "ONLINE"), + ("LINEV", "124.0 Volts"), + ("LOADPCT", "14.0 Percent"), + ("BCHARGE", "100.0 Percent"), + ("TIMELEFT", "51.0 Minutes"), + ("MBATTCHG", "5 Percent"), + ("MINTIMEL", "3 Minutes"), + ("MAXTIME", "0 Seconds"), + ("SENSE", "Medium"), + ("LOTRANS", "92.0 Volts"), + ("HITRANS", "139.0 Volts"), + ("ALARMDEL", "30 Seconds"), + ("BATTV", "13.7 Volts"), + ("LASTXFER", "Automatic or explicit self test"), + ("NUMXFERS", "1"), + ("XONBATT", "1970-01-01 00:00:00 0000"), + ("TONBATT", "0 Seconds"), + ("CUMONBATT", "8 Seconds"), + ("XOFFBATT", "1970-01-01 00:00:00 0000"), + ("LASTSTEST", "1970-01-01 00:00:00 0000"), + ("SELFTEST", "NO"), + ("STATFLAG", "0x05000008"), + ("SERIALNO", "XXXXXXXXXXXX"), + ("BATTDATE", "1970-01-01"), + ("NOMINV", "120 Volts"), + ("NOMBATTV", "12.0 Volts"), + ("NOMPOWER", "330 Watts"), + ("FIRMWARE", "928.a8 .D USB FW:a8"), + ("END APC", "1970-01-01 00:00:00 0000"), + ] +) + +# Minimal status adapted from http://www.apcupsd.org/manual/manual.html#apcaccess-test. +# Most importantly, the "MODEL" and "SERIALNO" fields are removed to test the ability +# of the integration to handle such cases. +MOCK_MINIMAL_STATUS: Final = OrderedDict( + [ + ("APC", "001,012,0319"), + ("DATE", "1970-01-01 00:00:00 0000"), + ("RELEASE", "3.8.5"), + ("CABLE", "APC Cable 940-0128A"), + ("UPSMODE", "Stand Alone"), + ("STARTTIME", "1970-01-01 00:00:00 0000"), + ("LINEFAIL", "OK"), + ("BATTSTAT", "OK"), + ("STATFLAG", "0x008"), + ("END APC", "1970-01-01 00:00:00 0000"), + ] +) diff --git a/tests/components/apcupsd/test_config_flow.py b/tests/components/apcupsd/test_config_flow.py new file mode 100644 index 00000000000..79bdd68bf17 --- /dev/null +++ b/tests/components/apcupsd/test_config_flow.py @@ -0,0 +1,185 @@ +"""Test APCUPSd config flow setup process.""" +from copy import copy +from unittest.mock import patch + +import pytest + +from homeassistant.components.apcupsd import DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.const import CONF_HOST, CONF_PORT, CONF_RESOURCES, CONF_SOURCE +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from . import CONF_DATA, MOCK_MINIMAL_STATUS, MOCK_STATUS + +from tests.common import MockConfigEntry + + +def _patch_setup(): + return patch( + "homeassistant.components.apcupsd.async_setup_entry", + return_value=True, + ) + + +async def test_config_flow_cannot_connect(hass: HomeAssistant) -> None: + """Test config flow setup with connection error.""" + with patch("apcaccess.status.get") as mock_get: + mock_get.side_effect = OSError() + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data=CONF_DATA, + ) + assert result["type"] == FlowResultType.FORM + assert result["errors"]["base"] == "cannot_connect" + + +async def test_config_flow_no_status(hass: HomeAssistant) -> None: + """Test config flow setup with successful connection but no status is reported.""" + with patch( + "apcaccess.status.parse", + return_value={}, # Returns no status. + ), patch("apcaccess.status.get", return_value=b""): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_status" + + +async def test_config_flow_duplicate(hass: HomeAssistant) -> None: + """Test duplicate config flow setup.""" + # First add an exiting config entry to hass. + mock_entry = MockConfigEntry( + version=1, + domain=DOMAIN, + title="APCUPSd", + data=CONF_DATA, + unique_id=MOCK_STATUS["SERIALNO"], + source=SOURCE_USER, + ) + mock_entry.add_to_hass(hass) + + with patch("apcaccess.status.parse") as mock_parse, patch( + "apcaccess.status.get", return_value=b"" + ), _patch_setup(): + mock_parse.return_value = MOCK_STATUS + + # Now, create the integration again using the same config data, we should reject + # the creation due same host / port. + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data=CONF_DATA, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + + # Then, we create the integration once again using a different port. However, + # the apcaccess patch is kept to report the same serial number, we should + # reject the creation as well. + another_host = { + CONF_HOST: CONF_DATA[CONF_HOST], + CONF_PORT: CONF_DATA[CONF_PORT] + 1, + } + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data=another_host, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + + # Now we change the serial number and add it again. This should be successful. + another_device_status = copy(MOCK_STATUS) + another_device_status["SERIALNO"] = MOCK_STATUS["SERIALNO"] + "ZZZ" + mock_parse.return_value = another_device_status + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data=another_host, + ) + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"] == another_host + + +async def test_flow_works(hass: HomeAssistant) -> None: + """Test successful creation of config entries via user configuration.""" + with patch("apcaccess.status.parse", return_value=MOCK_STATUS), patch( + "apcaccess.status.get", return_value=b"" + ), _patch_setup() as mock_setup: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == SOURCE_USER + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=CONF_DATA + ) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == MOCK_STATUS["MODEL"] + assert result["data"] == CONF_DATA + + mock_setup.assert_called_once() + + +@pytest.mark.parametrize( + "extra_status,expected_title", + [ + ({"UPSNAME": "Friendly Name"}, "Friendly Name"), + ({"MODEL": "MODEL X"}, "MODEL X"), + ({"SERIALNO": "ZZZZ"}, "ZZZZ"), + ({}, "APC UPS"), + ], +) +async def test_flow_minimal_status( + hass: HomeAssistant, extra_status: dict[str, str], expected_title: str +) -> None: + """Test successful creation of config entries via user configuration when minimal status is reported. + + We test different combinations of minimal statuses, where the title of the + integration will vary. + """ + with patch("apcaccess.status.parse") as mock_parse, patch( + "apcaccess.status.get", return_value=b"" + ), _patch_setup() as mock_setup: + status = MOCK_MINIMAL_STATUS | extra_status + mock_parse.return_value = status + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA + ) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"] == CONF_DATA + assert result["title"] == expected_title + mock_setup.assert_called_once() + + +async def test_flow_import(hass: HomeAssistant) -> None: + """Test successful creation of config entries via YAML import.""" + with patch("apcaccess.status.parse", return_value=MOCK_STATUS), patch( + "apcaccess.status.get", return_value=b"" + ), _patch_setup() as mock_setup: + # Importing from YAML will create an extra field CONF_RESOURCES in the config + # entry, here we test if it is properly stored. + resources = ["MODEL"] + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=CONF_DATA | {CONF_RESOURCES: resources}, + ) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == MOCK_STATUS["MODEL"] + assert result["data"] == CONF_DATA | {CONF_RESOURCES: resources} + + mock_setup.assert_called_once() From 63b7a236361d33335ee505d5da3b4b3cc94928ff Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 27 Sep 2022 21:29:09 -1000 Subject: [PATCH 800/955] Bump switchbot to fix assertion error on processing humidifer data (#79180) changelog: https://github.com/Danielhiversen/pySwitchbot/compare/0.19.12...0.19.13 --- 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 cb81252fe21..1d245d3fd81 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.19.12"], + "requirements": ["PySwitchbot==0.19.13"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 4b16e8d8cb1..5447f46af8a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -40,7 +40,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.19.12 +PySwitchbot==0.19.13 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 62123ccce0b..bd565cda02c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -36,7 +36,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.19.12 +PySwitchbot==0.19.13 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From 65788fad5784a04f691ffd71765d37a93dde20fb Mon Sep 17 00:00:00 2001 From: JQWeb <36139614+JQWeb@users.noreply.github.com> Date: Wed, 28 Sep 2022 10:05:58 +0200 Subject: [PATCH 801/955] Update roomba config flow description (#77974) * Update strings.json Automatic retrieval of the Roomba password is not possible when the iRobot App is opened on any device. If the iRobot App is open, the password retrieval will fail. - I added a sentence informing the users that they should make sure that the iRobot App is closed on all devices, before pressing the Home Button on the Roomba. - Also i added a sentence informing the users that they should make sure that the iRobot App is closed on all devices, in the failure message. This way, more users will have a succesful password retrieval, resulting in a smoother configuration experience for their Roomba into Home Assistant. * Update strings.json Added the sentence to the front of the message Added an "Important" notice to catch more attention. * Update homeassistant/components/roomba/strings.json Co-authored-by: Yevhenii Vaskivskyi * Update strings.json Co-authored-by: Erik Montnemery Co-authored-by: Yevhenii Vaskivskyi --- homeassistant/components/roomba/strings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/roomba/strings.json b/homeassistant/components/roomba/strings.json index b52d6443213..a644797c1be 100644 --- a/homeassistant/components/roomba/strings.json +++ b/homeassistant/components/roomba/strings.json @@ -18,11 +18,11 @@ }, "link": { "title": "Retrieve Password", - "description": "Press and hold the Home button on {name} until the device generates a sound (about two seconds), then submit within 30 seconds." + "description": "Make sure that the iRobot app is not running on any device. Press and hold the Home button on {name} until the device generates a sound (about two seconds), then submit within 30 seconds." }, "link_manual": { "title": "Enter Password", - "description": "The password could not be retrieved from the device automatically. Please follow the steps outlined in the documentation at: {auth_help_url}", + "description": "The password could not be retrieved from the device automatically. Please make sure that the iRobot app is not open on any device while trying to retrieve the password. Please follow the steps outlined in the documentation at: {auth_help_url}", "data": { "password": "[%key:common::config_flow::data::password%]" } From 4a432db611fcb3219515cabfe75a2e161cf42c5d Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 28 Sep 2022 10:29:55 +0200 Subject: [PATCH 802/955] Remove type ignore from bluetooth (#79146) --- homeassistant/components/bluetooth/manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index 4229b285939..37c24423231 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -111,7 +111,7 @@ def _prefer_previous_adv( def _dispatch_bleak_callback( - callback: AdvertisementDataCallback, + callback: AdvertisementDataCallback | None, filters: dict[str, set[str]], device: BLEDevice, advertisement_data: AdvertisementData, @@ -119,7 +119,7 @@ def _dispatch_bleak_callback( """Dispatch the callback.""" if not callback: # Callback destroyed right before being called, ignore - return # type: ignore[unreachable] # pragma: no cover + return # pragma: no cover if (uuids := filters.get(FILTER_UUIDS)) and not uuids.intersection( advertisement_data.service_uuids From 6ef33b1d39349b9f1f46579eaff49f46472ddbac Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 28 Sep 2022 10:37:34 +0200 Subject: [PATCH 803/955] Fix overriding a script's entity_id (#78765) --- homeassistant/components/script/__init__.py | 34 ++++++++--------- tests/components/script/test_init.py | 41 +++++++++++++++++++++ 2 files changed, 57 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py index dce99620e44..16099c18ebe 100644 --- a/homeassistant/components/script/__init__.py +++ b/homeassistant/components/script/__init__.py @@ -28,7 +28,7 @@ from homeassistant.const import ( STATE_ON, ) from homeassistant.core import HomeAssistant, ServiceCall, callback -from homeassistant.helpers import extract_domain_configs +from homeassistant.helpers import entity_registry as er, extract_domain_configs import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import make_entity_service_schema from homeassistant.helpers.entity import ToggleEntity @@ -237,7 +237,7 @@ async def _async_process_config(hass, config, component) -> bool: for config_key in extract_domain_configs(config, DOMAIN): conf: dict[str, dict[str, Any] | BlueprintInputs] = config[config_key] - for object_id, config_block in conf.items(): + for key, config_block in conf.items(): raw_blueprint_inputs = None raw_config = None @@ -264,16 +264,15 @@ async def _async_process_config(hass, config, component) -> bool: raw_config = cast(ScriptConfig, config_block).raw_config entities.append( - ScriptEntity( - hass, object_id, config_block, raw_config, raw_blueprint_inputs - ) + ScriptEntity(hass, key, config_block, raw_config, raw_blueprint_inputs) ) await component.async_add_entities(entities) async def service_handler(service: ServiceCall) -> None: """Execute a service call to script.